Go 服務優雅重啓 - facebook-grace 學習
【導讀】線上服務在發佈時有減少對當前業務影響的需求,會採用優雅重啓的方案。本文詳細介紹了 facebook 開源庫 grace 的使用。
逐步分析
猜測
查閱相關資料後,大概猜測出做法
服務重啓時,舊進程並不直接停止,而是用舊進程 fork 一個新進程,同時舊進程的所有句柄都 dup 到新進程。這時新的請求都由新的進程處理,舊進程在處理完自己的任務後,自行退出。
這只是大概流程,裏面還有許多細節需要考慮
分析 grace
github
https://github.com/facebookarchive/grace
流程簡述
-
利用啓動時的參數(包括命令行參數、環境變量等),重新啓動新進程。同時將當前 socket 句柄給新進程。
-
舊進程不再 Accept,待當前任務結束後,進程退出
源碼分析
如何啓動新進程
// facebookgo/grace/gracenet/net.go:206(省略非核心代碼)
func (n *Net) StartProcess() (int, error) {
listeners, err := n.activeListeners()
// 複製 socket 句柄
files := make([]*os.File, len(listeners))
for i, l := range listeners {
files[i], err = l.(filer).File()
defer files[i].Close()
}
// 複製標準 IO 句柄
allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...)
// 啓動新進程,並傳遞句柄
process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{
Dir: originalWD,
Env: env,
Files: allFiles,
})
return process.Pid, nil
}
這段代碼是啓動新進程的過程。
-
變量
files
保存listeners
句柄(即 socket 句柄) -
變量
allFiles
保存files
+stdout、stdin、stderr
句柄 -
os.StartProcess
啓動新進程,並傳遞父進程句柄
注:這裏傳遞的句柄只包括 socket 句柄與標準 IO 句柄。
舊進程如何退出
舊進程退出需要確保當前的請求全部處理完成。同時不再接收新的請求。
- 如何不接收新的請求
回答這個問題需要提到socket 流程
。
通常建立 socket 需要經歷以下四步:
-
socket
-
bind
-
listen
-
accept
通常,accept 處於一個循環中,這樣就能持續處理請求。所以若不想接收新請求,只需退出循環,不再 accept 即可。
- 如何確保當前請求全部處理完成
回答這個問題,我們需要給每一個連接賦予一系列狀態。恰好,net/http
包幫我們做好了這件事。
// GOROOT/net/http/server.go:2743
type ConnState int
const (
// 新連接剛建立時
StateNew ConnState = iota
// 連接處於活躍狀態,即正在處理的請求
StateActive
// 連接處於空閒狀態,一般用於 keep-alive
StateIdle
// 劫持狀態,可以理解爲關閉狀態
StateHijacked
// 關閉狀態
StateClosed
)
通過狀態,我們就能精確判斷所有請求是否處理完成。只要所有活躍(StateActive)的連接都成爲空閒(StateIdle)或者關閉(StateClosed)狀態。就可以保證請求全部處理完成。
具體代碼
// facebookgo/httpdown/httpdown.go:347
func ListenAndServe(s *http.Server, hd *HTTP) error {
// 監聽端口,提供服務
hs, err := hd.ListenAndServe(s)
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
// 監聽信號量 2 和 15(即 kill -2 -15)
select {
case <-signals:
signal.Stop(signals)
// hs.Stop() 開始停止服務
if err := hs.Stop(); err != nil {
return err
}
}
}
這段代碼是啓動服務的入口代碼
-
ListenAndServe 監聽端口,提供 http 服務
-
signal.Notify 註冊要監聽的信號量,這裏監聽
syscall.SIGTERM
和syscall.SIGINT
,即一般終止進程的信號量 -
hs.Stop() 停止服務,結束當前進程
可以看出,服務退出的邏輯都在hs.Stop()
// facebookgo/httpdown/httpdown.go:293
func (s *server) Stop() error {
s.stopOnce.Do(func() {
// 禁止 keep-alive
s.server.SetKeepAlivesEnabled(false)
// 關閉 listener, 不再接收請求
closeErr := s.listener.Close()
<-s.serveDone
// 通過 stop(一個 chan),傳遞關閉信號
stopDone := make(chan struct{})
s.stop <- stopDone
// 若在 s.stopTimeout 以內沒有結束,則強行 kill 所有連接。默認 s.stopTimeout 爲 1min
select {
case <-stopDone:
case <-s.clock.After(s.stopTimeout):
// stop timed out, wait for kill
killDone := make(chan struct{})
s.kill <- killDone
}
})}
Stop 方法
-
禁止 keep-alive
-
關閉 listener,即不再 accept 新請求
-
想 s.stop\(一個 chan) 傳遞關閉的信號
-
若 s.stopTimeout 時間內,沒有退出,則強行 kill 所有連接。
那麼,等待所有請求處理完畢的邏輯,應該處於消費 s.stop 的地方。
這裏我們注意到,最核心的結構體有這樣幾個屬性
// facebookgo/httpdown/httpdown.go:126
type server struct {
...
new chan net.Conn
active chan net.Conn
idle chan net.Conn
closed chan net.Conn
stop chan chan struct{}
kill chan chan struct{}
...
}
stop 和 kill 說過了,是用來傳遞停止和強行終止信號的。
其餘new
、active
、idle
、closed
是用來記錄處於不同狀態的連接的。
我們記錄了不同狀態的連接,那麼在關閉時,就能等連接處於 “空閒“或” 關閉“時再關閉它。
// facebookgo/httpdown/httpdown.go:233
case c := <-s.idle:
conns[c] = http.StateIdle
// 那些處於“活躍”的連接,會等到它轉爲“空閒”時,將其關閉
if stopDone != nil {
c.Close()
}
case c := <-s.closed:
// 所有連接關閉後,退出
if stopDone != nil && len(conns) == 0 {
close(stopDone)
return
}
case stopDone = <-s.stop:
// 所有連接關閉後,退出
if len(conns) == 0 {
close(stopDone)
return
}
// 關閉所有“空閒”連接
for c, cs := range conns {
if cs == http.StateIdle {
c.Close()
}
}
這裏可以看出,當接收到關閉信號時(stopDone = <-s.stop)
-
會遍歷所有 “空閒” 連接,將其關閉。
-
而那些處於 “活躍” 的連接,會等到它轉爲 “空閒” 時,將其關閉
-
在所有連接關閉後,退出
總結
進程重啓主要就是如何退出、如何啓動。grace 代碼量不多,以上敘述了核心的邏輯,有興趣的同學可以 fork github 源碼研讀。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Ct2VE1FgZ_bqhbFki38z5A