這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
退出運行中的程式,可以粗暴的kill -9 $PID,但這樣會破壞業務的完整性,有可能一個正在在執行的邏輯半途而費,從而產生不正常的垃圾資料。
本文總結在go語言中,如何能優雅的退出網路應用,涉及的知識包括:signal,channel,WaitGroup等。
從這裡:https://gobyexample.com/channel-synchronization 可以簡單瞭解到,在go中如何使用channel實現goroutines同步。
在nsq中,也使用了相同的機制,不過封裝更複雜了些。我們以nsqadmin中的實現為例進行簡單的分析。
程式碼片段1(來自:https://github.com/bitly/nsq/blob/master/nsqadmin/main.go):
exitChan := make(chan int)
signalChan := make(chan os.Signal, 1)
go func() {
<-signalChan
exitChan <- 1
}()
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
//....
nsqadmin.Main()
<-exitChan
nsqadmin.Exit()
上面的代碼正常執行後,會卡到倒數第二句的<-exitChan中,直到exitChan中有資料進入。
當我們通過命令列執行:kill -s SIGINT $PID時,signalChan中收到一個資訊,上面第四行代碼中的goroutines會停止阻塞,繼續向下執行,exitChann中加入一條資料。當exitChann中有了資料,倒數第二句也會停止阻塞,執行nsqadmin.Exit()實現優雅退出。
當然上面的代碼,也可以簡化:
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
//....
nsqadmin.Main()
<-signalChan
nsqadmin.Exit()
至於nsq為什麼沒有這麼做,還不太清楚。能力有限,體會不到其深意所在。
上面的例子只適用於兩個goroutines之間,一個處理完業務後給exitChan寫資料,主goroutines卡在exitChan上等資料。(主goroutines必須比處理業務的goroutines後退出)。
如果一個主線程下開了多個子goroutines,使用channel的方式就不夠優雅了。可以使用WaitGroup,關於WaitGroup的介紹可以參考:http://www.baiyuxiong.com/?p=913
在nsq中同樣使用了WaitGroup實現退出。
程式碼片段2(來自:https://github.com/bitly/nsq/blob/master/util/wait_group_wrapper.go)
type WaitGroupWrapper struct {
sync.WaitGroup
}
func (w *WaitGroupWrapper) Wrap(cb func()) {
w.Add(1)
go func() {
cb()
w.Done()
}()
}
程式碼片段3(來自:<a href="https://github.com/bitly/nsq/blob/master/nsqadmin/nsqadmin.go">https://github.com/bitly/nsq/blob/master/nsqadmin/nsqadmin.go</a>)
func (n *NSQAdmin) Main() {
httpListener, err := net.Listen("tcp", n.httpAddr.String())
if err != nil {
n.logf("FATAL: listen (%s) failed - %s", n.httpAddr, err)
os.Exit(1)
}
n.httpListener = httpListener
httpServer := NewHTTPServer(&Context{n})
n.waitGroup.Wrap(func() {
util.HTTPServer(n.httpListener, httpServer, n.opts.Logger, "HTTP")
})
n.waitGroup.Wrap(func() { n.handleAdminActions() })
}
func (n *NSQAdmin) Exit() {
n.httpListener.Close()
close(n.notifications)
n.waitGroup.Wait()
}
在程式碼片段2中,對waitGroup進行了簡單封裝,開啟goroutines前計數加1,執行完計數減1。
程式碼片段3中,Main()方法裡,調用了兩次waitGroup.Wrap()方法,參考程式碼片段2可以知道,這會啟動兩個子goroutines,並使waitGroup計數加2.而子goroutines中使用了http包監聽網路服務,阻塞goroutines,使得計數減1的操作不能被調用。
在我們的程式碼片段1中可以知道,命令列發送了kill以後,會執行程式碼片段3的Exit()方法,當方法裡的n.httpListener.Close()被調用後,網路服務中斷,程式碼片段2中的阻塞就會停止,進而執行計數減1的,當兩個子goroutines中計數各減1以後。Exit()方法中的n.waitGroup.Wait()就會繼續執行,主線程結束,程式退出。