文章目錄
前言
grace是facebook公司爲golang服務開發的優雅重啓和零停機部署的開源庫。可以實現服務重啓時,舊有連接不斷,新服務啓動後,新連接連入新服務,如此客戶端無感知。
使用方法
(1)獲取
go get github.com/facebookgo/grace/gracehttp
mod可以使用如下方式引入:
require github.com/facebookgo/grace latest
(2)使用
gracehttp.Serve(
&http.Server{Addr: *address0, Handler: newHandler("Zero ")},
&http.Server{Addr: *address1, Handler: newHandler("First ")},
&http.Server{Addr: *address2, Handler: newHandler("Second")},
)
(3)重啓命令
sudo kill -USR2
pidof yourservername
pidof yourservername
是指需要重啓的服務進程id
源碼解析
啓動服務
// Serve will serve the given http.Servers and will monitor for signals
// allowing for graceful termination (SIGTERM) or restart (SIGUSR2).
func Serve(servers ...*http.Server) error {
a := newApp(servers)
return a.run()
}
func newApp(servers []*http.Server) *app {
return &app{
servers: servers,
http: &httpdown.HTTP{},
net: &gracenet.Net{},
listeners: make([]net.Listener, 0, len(servers)),
sds: make([]httpdown.Server, 0, len(servers)),
preStartProcess: func() error { return nil },
// 2x num servers for possible Close or Stop errors + 1 for possible
// StartProcess error.
errors: make(chan error, 1+(len(servers)*2)),
}
}
構造app,具體的處理邏輯均在app中進行。
注意:
此處註釋說明了Serve會啓動指定的http.Servers,並且會監聽系統信號以允許優雅結束(SIGTERM)或重啓服務(SIGUSR2),實際代碼中還支持SIGINT結束服務進程,我們可以根據需求指定信號來決定是結束還是重啓服務。
func (a *app) run() error {
// Acquire Listeners
//獲取所有http.Server服務地址的Listeners
if err := a.listen(); err != nil {
return err
}
// Some useful logging.
// logger的處理
if logger != nil {
if didInherit {
if ppid == 1 {
logger.Printf("Listening on init activated %s", pprintAddr(a.listeners))
} else {
const msg = "Graceful handoff of %s with new pid %d and old pid %d"
logger.Printf(msg, pprintAddr(a.listeners), os.Getpid(), ppid)
}
} else {
const msg = "Serving %s with pid %d"
logger.Printf(msg, pprintAddr(a.listeners), os.Getpid())
}
}
// Start serving.
// 啓動各http服務
a.serve()
// Close the parent if we inherited and it wasn't init that started us.
// 如果已經繼承,並且不是從init啓動,就說明是重啓進程,需要將原進程關閉。
if didInherit && ppid != 1 {
if err := syscall.Kill(ppid, syscall.SIGTERM); err != nil {
return fmt.Errorf("failed to close parent: %s", err)
}
}
// 此處爲最核心處理部分
waitdone := make(chan struct{})
go func() {
defer close(waitdone)
a.wait()
}()
select {
case err := <-a.errors://原有服務處理及關閉時發生錯誤
if err == nil {
panic("unexpected nil error")
}
return err
case <-waitdone://waitdone close後即可取值,此時意味着原有服務已完成且關閉
if logger != nil {
logger.Printf("Exiting pid %d.", os.Getpid())
}
return nil
}
}
//依次啓動指定的http.Servers
func (a *app) serve() {
for i, s := range a.servers {
a.sds = append(a.sds, a.http.Serve(s, a.listeners[i]))
}
}
具體服務啓動過程
過程如下:
-
構造server
-
設置ConnState,監聽各連接的狀態變化
-
啓動新協程,manage server的各chan信號
-
在新協程中正式啓動服務
Serve總入口
// 具體服務啓動管理
func (h HTTP) Serve(s *http.Server, l net.Listener) Server {
stopTimeout := h.StopTimeout
if stopTimeout == 0 {
stopTimeout = defaultStopTimeout
}
killTimeout := h.KillTimeout
if killTimeout == 0 {
killTimeout = defaultKillTimeout
}
klock := h.Clock
if klock == nil {
klock = clock.New()
}
ss := &server{
stopTimeout: stopTimeout,//stop超時時間
killTimeout: killTimeout,//kill超時時間
stats: h.Stats,//數據統計
clock: klock,//數據統計的定時器
oldConnState: s.ConnState,//原ConnState hook
listener: l,//服務listener
server: s,//具體服務
serveDone: make(chan struct{}),//服務啓動結束
serveErr: make(chan error, 1),//服務啓動錯誤
new: make(chan net.Conn),//新連接
active: make(chan net.Conn),//連接狀態Active
idle: make(chan net.Conn),//連接狀態Idle
closed: make(chan net.Conn),//連接狀態Closed
stop: make(chan chan struct{}),//stop通知
kill: make(chan chan struct{}),//kill通知
}
s.ConnState = ss.connState
// 管理連接conns
go ss.manage()
// 啓動http服務
go ss.serve()
return ss
}
ConnState注入hook
設置ConnState,注入hook監聽各連接的狀態變化,發送至server的各統計狀態的chan中。
// 處理服務的連接狀態變化
func (s *server) connState(c net.Conn, cs http.ConnState) {
if s.oldConnState != nil {
s.oldConnState(c, cs)
}
switch cs {
case http.StateNew:
s.new <- c
case http.StateActive:
s.active <- c
case http.StateIdle:
s.idle <- c
case http.StateHijacked, http.StateClosed:
s.closed <- c
}
}
manage管理各種chan信號的處理
manage處理了所有connState中監聽的連接變化,所有的連接會存儲在conns map中,隨着conn的變化進行增加、修改、刪除。
stop chan信號確認所有連接是否關閉,如存在未關閉的連接,則先關閉處理Idle狀態的連接,然後等待其他的狀態的連接繼續處理。
kill chan信號則強制關閉所有連接。
stop、kill chan信號及stopTimeout、killTimeout的使用均來自於Stop(結束服務)過程,後面會詳細分析。
stop chan信號在服務Stop時觸發,stop會優先關閉處於idle狀態的連接,然後等待其他狀態的連接繼續處理。當killTimeout觸發時,意味着
stop、kill chan信號對應stopTimeout、killTimeout參數。初始化中stopTimeout、killTimeout,如果有設置參數則使用設置的參數,沒有則使用默認參數(一分鐘)。stopTimeout是處理服務stop的超時時間,超時後,會觸發kill,killTimeout就是處理kill的超時時間。
特別說明:s.stats一直爲nil,其相關的代碼並不會執行,閱讀時可略過相關代碼。
// 管理各種chan信號的處理
// 說明:s.stats一直爲nil,此參數僅用來測試時使用,其相關的代碼可以略過
func (s *server) manage() {
defer func() {
close(s.new)
close(s.active)
close(s.idle)
close(s.closed)
close(s.stop)
close(s.kill)
}()
var stopDone chan struct{}
// 存儲服務的各連接的狀態
conns := map[net.Conn]http.ConnState{}
var countNew, countActive, countIdle float64
// decConn decrements the count associated with the current state of the
// given connection.
// 更新計數
decConn := func(c net.Conn) {
switch conns[c] {
default:
panic(fmt.Errorf("unknown existing connection: %s", c))
case http.StateNew:
countNew--
case http.StateActive:
countActive--
case http.StateIdle:
countIdle--
}
}
// setup a ticker to report various values every minute. if we don't have a
// Stats implementation provided, we Stop it so it never ticks.
// 僅在有使用Stats實現接口的情況開始定時器統計數據,實際代碼中並未使用,此處開啓後會被關閉
statsTicker := s.clock.Ticker(time.Minute)
if s.stats == nil {
statsTicker.Stop()
}
// 等待所有連接處理
for {
select {
case <-statsTicker.C: //每分鐘統計各狀態的連接數量,此項僅在測試源碼時生效,正式使用時,不生效
// we'll only get here when s.stats is not nil
s.stats.BumpAvg("http-state.new", countNew)
s.stats.BumpAvg("http-state.active", countActive)
s.stats.BumpAvg("http-state.idle", countIdle)
s.stats.BumpAvg("http-state.total", countNew+countActive+countIdle)
case c := <-s.new:
conns[c] = http.StateNew //存入New狀態連接
countNew++
case c := <-s.active:
decConn(c)
countActive++
conns[c] = http.StateActive //存入Active狀態連接
case c := <-s.idle:
decConn(c)
countIdle++
conns[c] = http.StateIdle //存入Idle狀態連接
// if we're already stopping, close it
if stopDone != nil { //已經完成stop,直接關閉連接
c.Close()
}
case c := <-s.closed:
stats.BumpSum(s.stats, "conn.closed", 1)
decConn(c)
delete(conns, c) //移除關閉的連接
// if we're waiting to stop and are all empty, we just closed the last
// connection and we're done.
if stopDone != nil && len(conns) == 0 {
close(stopDone)
return
}
case stopDone = <-s.stop:
// if we're already all empty, we're already done
// 如果連接處理完畢,關閉chan,
if len(conns) == 0 {
close(stopDone)
return
}
// close current idle connections right away
// 先關閉idle狀態的連接
for c, cs := range conns {
if cs == http.StateIdle {
c.Close()
}
}
// continue the loop and wait for all the ConnState updates which will
// eventually close(stopDone) and return from this goroutine.
case killDone := <-s.kill://強殺,超時時使用
// force close all connections
stats.BumpSum(s.stats, "kill.conn.count", float64(len(conns)))
for c := range conns {//強制關閉所有連接
c.Close()
}
// don't block the kill.
close(killDone)
// continue the loop and we wait for all the ConnState updates and will
// return from this goroutine when we're all done. otherwise we'll try to
// send those ConnState updates on closed channels.
}
}
}
正式的服務,則在新協程中調用http.Server的Serve啓動,啓動的錯誤發送至serveErr供啓動協程處理,如發生錯誤,則panic。若無錯誤,通知服務啓動結束。若存在Stop,則必須等server啓動結束後才能進行,這是必然的邏輯,因爲不可能stop一個未啓動的server。
// 正式啓動服務
func (s *server) serve() {
stats.BumpSum(s.stats, "serve", 1)
s.serveErr <- s.server.Serve(s.listener)
close(s.serveDone)
close(s.serveErr)
}
系統信號監聽
這部分是核心處理部分:
-
啓動信號監聽協程
-
監聽當前服務啓動及stop狀態,如有服務啓動及stop發生錯誤,則通過a.errors,通知啓動協程。
wait
// 等待原有http.Servers的連接處理完後,結束服務
func (a *app) wait() {
var wg sync.WaitGroup
wg.Add(len(a.sds) * 2) // Wait & Stop
// 監聽信號
go a.signalHandler(&wg)
for _, s := range a.sds {
go func(s httpdown.Server) {
defer wg.Done()
if err := s.Wait(); err != nil {
a.errors <- err
}
}(s)
}
wg.Wait()
}
wait內的WaitGroup包含了Wait & Stop的處理,Wait用以等待各服務的啓動狀態結果監聽,Stop則是用以結束進程(包含重啓)狀態監聽,只有Wait & Stop均完成後,wait才能真正結束。結合之前的代碼,此時啓動協程才能算處理結束。
signalHandler
// 系統信號監聽服務
func (a *app) signalHandler(wg *sync.WaitGroup) {
ch := make(chan os.Signal, 10)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
for {
sig := <-ch
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
// this ensures a subsequent INT/TERM will trigger standard go behaviour of
// terminating.
// 停止接收信號
signal.Stop(ch)
// 結束原有服務
a.term(wg)
return
case syscall.SIGUSR2:
err := a.preStartProcess()
if err != nil {
a.errors <- err
}
// we only return here if there's an error, otherwise the new process
// will send us a TERM when it's ready to trigger the actual shutdown.
if _, err := a.net.StartProcess(); err != nil {
a.errors <- err
}
}
}
}
gracehttp默認監聽的信號爲:SIGINT、SIGTERM、SIGUSR2。其中SIGINT、SIGTERM用以優雅結束服務,SIGUSR2用以優雅重啓服務。
關閉服務
關閉服務,目前由兩種形式:
-
我們通過kill命令操作
-
進程重啓時發現父進程時,主動調用syscall.Kill命令
這兩種形式實際結果是一致的,只是調用者不同。
//此處的term使用的WaitGroup就是wait中的wg
func (a *app) term(wg *sync.WaitGroup) {
for _, s := range a.sds {
go func(s httpdown.Server) {
defer wg.Done()
if err := s.Stop(); err != nil {
a.errors <- err
}
}(s)
}
}
func (s *server) Stop() error {
s.stopOnce.Do(func() {
defer stats.BumpTime(s.stats, "stop.time").End()
stats.BumpSum(s.stats, "stop", 1)
// first disable keep-alive for new connections
// 關閉keep-alive,讓原conn處理完關閉
s.server.SetKeepAlivesEnabled(false)
// then close the listener so new connections can't connect come thru
// 關閉listener,讓新連接無法連入
closeErr := s.listener.Close()
<-s.serveDone//等待服務啓動完畢,避免未啓動完成調用Stop
// then trigger the background goroutine to stop and wait for it
stopDone := make(chan struct{})
s.stop <- stopDone//通知stop下的連接處理
// wait for stop
select {
case <-stopDone://conn全部處理完畢
case <-s.clock.After(s.stopTimeout)://觸發超時
defer stats.BumpTime(s.stats, "kill.time").End()
stats.BumpSum(s.stats, "kill", 1)
// stop timed out, wait for kill
killDone := make(chan struct{})
s.kill <- killDone
select {
case <-killDone:
case <-s.clock.After(s.killTimeout):
// kill timed out, give up
stats.BumpSum(s.stats, "kill.timeout", 1)
}
}
if closeErr != nil && !isUseOfClosedError(closeErr) {
stats.BumpSum(s.stats, "listener.close.error", 1)
s.stopErr = closeErr
}
})
return s.stopErr
}
term會關閉所有的服務,並將關閉服務發生的錯誤發送至s.erros供啓動協程處理。
Stop的處理流程如下:
-
關閉keep-alive,新連接不再保持長連接
-
關閉listener,不再接受新連接
-
等待新服務進程啓動完畢
-
發送stop信號,確定連接是否處理完畢,未處理完則先關閉處理IDLE的連接
-
全部連接處理完,處理closeErr後返回
-
stopTimeout超時,則發送kill信號,強制關閉全部連接
-
全部連接強制關閉後,處理closeErr後返回
-
killTimeout超時,不再繼續處理,處理closeErr後返回
-
term會在關閉發生錯誤則通知啓動協程,所有服務全部正常退出後,wait結束,原進程結束。
啓動新進程
重啓的邏輯整體上就是,新服務進程啓動後,會主動發送TERM信號結束原進程,原進程會在處理完原有連接或超時後退出。
func (n *Net) StartProcess() (int, error) {
// 獲取http.Servers的Listeners
listeners, err := n.activeListeners()
if err != nil {
return 0, err
}
// Extract the fds from the listeners.
// 提取文件描述fds
files := make([]*os.File, len(listeners))
for i, l := range listeners {
files[i], err = l.(filer).File()
if err != nil {
return 0, err
}
defer files[i].Close()
}
// Use the original binary location. This works with symlinks such that if
// the file it points to has been changed we will use the updated symlink.
// 使用原執行文件路徑
argv0, err := exec.LookPath(os.Args[0])
if err != nil {
return 0, err
}
// Pass on the environment and replace the old count key with the new one.
// env添加listeners的數量
var env []string
for _, v := range os.Environ() {
if !strings.HasPrefix(v, envCountKeyPrefix) {
env = append(env, v)
}
}
env = append(env, fmt.Sprintf("%s%d", envCountKeyPrefix, len(listeners)))
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,
})
if err != nil {
return 0, err
}
return process.Pid, nil
}
總結
正常啓動程序(非重啓)到重啓:
-
所有服務啓動,進程監聽系統信號,啓動協程通過wait監聽服務協程啓動及stop狀態。
-
監聽到USR2信號,標識環境變量LISTEN_FDS,獲取服務執行文件路徑、參數、打開的文件描述及新增加的環境變量標識LISTEN_FDS,調用StartProcess啓動新進程
-
新進程啓動,處理新連接。新進程檢測到環境變量LISTEN_FDS及進程的父進程id,調用syscall.Kill結束原進程,新進程等待父進程(原服務進程)的退出。
-
父進程檢測到TERM信號,先停止接收系統信號,開始準備結束進程。若父進程存在未關閉的連接,則先關閉keep-alive,再關閉listener以阻止新連接連入。全部連接處理完關閉或超時後強制關閉所有連接後,wait內wg全部done。
-
wait處理結束,協程結束,父進程結束,僅留下新啓動的子進程服務。