天天看點

使用 Go 語言實作優雅的伺服器重新開機使用 Go 語言實作優雅的伺服器重新開機

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

使用 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 &amp;&amp; nerr.timeout() {

fmt.println(“stop accepting connections”)

return

注意這個操作與關閉listener有所不同。這樣程序仍在監聽伺服器端口,但連接配接請求會被作業系統的網絡棧排隊,等待一個程序接受它們。 

go提供了一個原始類型forkexec來産生新程序.你可以與這個新程序共享某些消息,例如檔案描述符或環境參數。

execspec := &amp;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 &lt;- struct{}{}

select {

case &lt;-timeout.c:

return waittimeouterror

case &lt;-wait:

return nil

socket傳遞配合forkexec使用确實是一種無幹擾更新程序的有效方式,在最大時間上,新的連接配接會等待幾毫秒——用于服務的啟動和恢複socket,但這個時間很短。

原文釋出時間:2014-12-26

本文來自雲栖合作夥伴“linux中國”