go被設計為一種背景語言,它通常也被用于後端程式中。服務端程式是go語言最常見的軟體産品。在這我要解決的問題是:如何幹淨利落地更新正在運作的服務端程式。

image
不關閉現有連接配接:例如我們不希望關掉已部署的運作中的程式。但又想不受限制地随時更新服務。
socket連接配接要随時響應使用者請求:任何時刻socket的關閉可能使使用者傳回'連接配接被拒絕'的消息,而這是不可取的。
新的程序要能夠啟動并替換掉舊的。
<a target="_blank"></a>
在基于unix的作業系統中,signal(信号)是與長時間運作的程序互動的常用方法.
sigterm: 優雅地停止程序
sighup: 重新開機/重新加載程序 (例如: nginx, sshd, apache)
如果收到sighup信号,優雅地重新開機程序需要以下幾個步驟:
伺服器要拒絕新的連接配接請求,但要保持已有的連接配接。
啟用新版本的程序
将socket“交給”新程序,新程序開始接受新連接配接請求
舊程序處理完畢後立即停止。
伺服器程式的共同點:持有一個死循環來接受連接配接請求:
for {
conn, err := listener.accept()
// handle connection
}
跳出這個循環的最簡單方式是在socket監聽器上設定一個逾時,當調用listener.settimeout(time.now())後,listener.accept()會立即傳回一個timeout err,你可以捕獲并處理:
if err != nil {
if nerr, ok := err.(net.err); ok && nerr.timeout() {
fmt.println(“stop accepting connections”)
return
注意這個操作與關閉listener有所不同。這樣程序仍在監聽伺服器端口,但連接配接請求會被作業系統的網絡棧排隊,等待一個程序接受它們。
go提供了一個原始類型forkexec來産生新程序.你可以與這個新程序共享某些消息,例如檔案描述符或環境參數。
execspec := &syscall.procattr{
env: os.environ(),
files: []uintptr{os.stdin.fd(), os.stdout.fd(), os.stderr.fd()},
fork, err := syscall.forkexec(os.args[0], os.args, execspec)
[…]
你會發現這個程序使用完全相同的參數os.args啟動了一個新程序。
正如你先前看到的,你可以将檔案描述符傳遞到新程序,這需要一些unix魔法(一切都是檔案),我們可以把socket發送到新程序中,這樣新程序就能夠使用它并接收及等待新的連接配接。
但fork-execed程序需要知道它必須從檔案中得到socket而不是建立一個(有些興許已經在使用了,因為我們還沒斷開已有的監聽)。你可以按任何你希望的方法來,最常見的是通過環境變量或指令行标志。
listenerfile, err := listener.file()
log.fatalln("fail to get socket file descriptor:", err)
listenerfd := listenerfile.fd()
// set a flag for the new process start process
os.setenv("_graceful_restart", "true")
env: os.environ(),
files: []uintptr{os.stdin.fd(), os.stdout.fd(), os.stderr.fd(), listenerfd},
// fork exec the new version of your server
然後在程式的開始處:
var listener *net.tcplistener
if os.getenv("_graceful_restart") == "true" {
// the second argument should be the filename of the file descriptor
// however, a socker is not a named file but we should fit the interface
// of the os.newfile function.
file := os.newfile(3, "")
listener, err := net.filelistener(file)
// handle
var bool ok
listener, ok = listener.(*net.tcplistener)
if !ok {
} else {
listener, err = newlistenerwithport(12345)
到此為止,就這樣,我們已經将其傳到另一個正在正确運作的程序,對于舊伺服器的最後操作是等其連接配接關閉。由于标準庫裡提供了sync.waitgroup結構體,用go實作這個功能很簡單。
每次接收一個連接配接,在waitgroup上加1,然後,我們在它完成時将計數器減一:
wg.add(1)
go func() {
handle(conn)
wg.done()
}()
至于等待連接配接的結束,你僅需要wg.wait(),因為沒有新的連接配接,我們等待wg.done()已經被所有正在運作的handler調用。
timeout := time.newtimer(time.minute)
wait := make(chan struct{})
wg.wait()
wait <- struct{}{}
select {
case <-timeout.c:
return waittimeouterror
case <-wait:
return nil
socket傳遞配合forkexec使用确實是一種無幹擾更新程序的有效方式,在最大時間上,新的連接配接會等待幾毫秒——用于服務的啟動和恢複socket,但這個時間很短。
原文釋出時間:2014-12-26
本文來自雲栖合作夥伴“linux中國”