看完此篇你會知道,如何優雅的使用 HTTP Server
在 http
應用程序重啓時,若是咱們直接 kill -9
使程序退出,而後在啓動,會有如下幾個問題:linux
RST
);connection refused
;kill -9
仍然會讓正在處理的請求中斷;open too many files
錯誤;這些問題會形成很差的客戶體驗,嚴重的甚至影響客戶業務。因此,咱們須要以一種優雅的方式重啓/關閉咱們的應用,來達到熱啓動的效果,即:Zero Downtime
。git
(Tips:名詞解釋)
熱啓動
:新老程序(進程)無縫替換,同時能夠保持對client的服務。讓client端感受不到你的服務掛掉了;
Zero Downtime
: 0 宕機時間,即不間斷的服務;
Github: gracehttpgithub
通常狀況下,咱們是退出舊版本,再啓動新版本,總會有時間間隔,時間間隔內的請求怎麼辦?並且舊版本正在處理請求怎麼辦?
那麼,針對這些問題,在升級應用過程當中,咱們須要達到以下目的:golang
這樣,咱們就能實現 Zero Downtime
的升級效果。網絡
首先,咱們須要用到如下基本知識:
1.linux
信號處理機制:在程序中,經過攔截 signal
,並針對 signal
作出不一樣處理;
2.子進程繼承父進程的資源:一切皆文件,子進程會繼承父進程的資源句柄,網絡端口也是文件;
3.經過給子進程重啓標識(好比:重啓時帶着 -continue
參數),來實現子進程的初始化處理;併發
重啓時,咱們能夠在程序中捕獲 HUP
信號(經過 kill -HUP pid
能夠觸發),而後開啓新進程,退出舊進程。信號處理代碼示例以下:app
package gracehttp import ( "fmt" "os" "os/signal" "syscall" ) var sig chan os.Signal var notifySignals []os.Signal func init() { sig = make(chan os.Signal) notifySignals = append(notifySignals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGQUIT) signal.Notify(sig, notifySignals...) // 註冊須要攔截的信號 } // 捕獲系統信號,並處理 func handleSignals() { capturedSig := <-sig srvLog.Info(fmt.Sprintf("Received SIG. [PID:%d, SIG:%v]", syscall.Getpid(), capturedSig)) switch capturedSig { case syscall.SIGHUP: // 重啓信號 startNewProcess() // 開啓新進程 shutdown() // 退出舊進程 case syscall.SIGINT: fallthrough case syscall.SIGTERM: fallthrough case syscall.SIGTSTP: fallthrough case syscall.SIGQUIT: shutdown() } }
startNewProcess
shutdown
具體實現能夠參考 Githubide
經過限制 HTTP Server
的 accept
數量實現連接數的限制,來達到若是併發量達到了最大值,客戶端超時時間內能夠等待,但不會消耗服務端文件句柄數(咱們知道 Linux 系統對用戶能夠打開的最大文件數有限制,網絡請求也是文件操做)ui
channel
的緩衝機制實現,每一個請求都會獲取緩衝區的一個單元大小,知道緩衝區滿了,後邊的請求就會阻塞;go
的 select
機制,退出阻塞,並返回,再也不進行 accept
處理代碼以下:日誌
package gracehttp // about limit @see: "golang.org/x/net/netutil" import ( "net" "sync" "time" ) type Listener struct { *net.TCPListener sem chan struct{} closeOnce sync.Once // ensures the done chan is only closed once done chan struct{} // no values sent; closed when Close is called } func newListener(tl *net.TCPListener, n int) net.Listener { return &Listener{ TCPListener: tl, sem: make(chan struct{}, n), done: make(chan struct{}), } } func (l *Listener) Fd() (uintptr, error) { file, err := l.TCPListener.File() if err != nil { return 0, err } return file.Fd(), nil } // override func (l *Listener) Accept() (net.Conn, error) { acquired := l.acquire() tc, err := l.AcceptTCP() if err != nil { if acquired { l.release() } return nil, err } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(time.Minute) return &ListenerConn{Conn: tc, release: l.release}, nil } // override func (l *Listener) Close() error { err := l.TCPListener.Close() l.closeOnce.Do(func() { close(l.done) }) return err } // acquire acquires the limiting semaphore. Returns true if successfully // accquired, false if the listener is closed and the semaphore is not // acquired. func (l *Listener) acquire() bool { select { case <-l.done: return false case l.sem <- struct{}{}: return true } } func (l *Listener) release() { <-l.sem } type ListenerConn struct { net.Conn releaseOnce sync.Once release func() } func (l *ListenerConn) Close() error { err := l.Conn.Close() l.releaseOnce.Do(l.release) return err }
如今咱們把這個功能作得更優美有點,並提供一個開箱即用的代碼庫。
地址:Github-gracehttp
Zero-Downtime
);Server
添加(支持 HTTP
、HTTPS
);import "fevin/gracehttp" .... // http srv1 := &http.Server{ Addr: ":80", Handler: sc, } gracehttp.AddServer(srv1, false, "", "") // https srv2 := &http.Server{ Addr: ":443", Handler: sc, } gracehttp.AddServer(srv2, true, "../config/https.crt", "../config/https.key") gracehttp.Run() // 此方法會阻塞,直到進程收到退出信號,或者 panic
如上所示,只需建立好 Server
對象,調用 gracehttp.AddServer
添加便可。
kill -HUP pid
kill -QUIT pid
gracehttp.SetErrorLogCallback(logger.LogConfigLoadError)
此處提供了三個 Set*
方法,分別對應不一樣的日誌等級:
SetInfoLogCallback
SetNoticeLogCallback
SetErrorLogCallback
實際中,不少狀況會用到這種方式,不妨點個 star 吧!歡迎一塊兒來完善這個小項目,共同貢獻代碼。