golang 程序啓動一個 http 服務時,若服務被意外終止或中斷,會讓現有請求鏈接忽然中斷,未處理完成的任務也會出現不可預知的錯誤,這樣即會形成服務硬終止;爲了解決硬終止問題咱們但願服務中斷或退出時將正在處理的請求正常返回而且等待服務中止前做的一些必要的處理工做。golang
咱們能夠看一個硬終止的例子:瀏覽器
mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(5 * time.Second) fmt.Fprintln(w, "Hello world!") }) server := &http.Server{ Addr: ":8080", Handler: mux, } server.ListenAndServe()
啓動服務後,咱們能夠訪問 http://127.0.0.1:8080 頁面等待 5s 會輸出一個 「Hello world!」, 咱們能夠嘗試 Ctrl+C 終止程序,能夠看到瀏覽器馬上就顯示沒法鏈接,這表示鏈接馬上就中斷了,退出前的請求也未正常返回。函數
在 Golang1.8 之後 http 服務有個新特性 Shutdown 方法能夠優雅的關閉一個 http 服務, 該方法須要傳入一個 Context 參數,當程序終止時其中不會中斷活躍的鏈接,會等待活躍鏈接閒置或 Context 終止(手動 cancle 或超時)最後才終止程序,官方文檔詳見:https://godoc.org/net/http#Server.Shutdownspa
在具體用應用中咱們能夠配合 signal.Notify 函數來監聽系統退出信號來完成程序優雅退出;code
特別注意:server.ListenAndServe() 方法在 Shutdown 時會馬上返回,Shutdown 方法會阻塞至全部鏈接閒置或 context 完成,因此 Shutdown 的方法要寫在主 goroutine 中。server
優雅退出實驗1:blog
func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(5 * time.Second) fmt.Fprintln(w, "Hello world!") }) server := &http.Server{ Addr: ":8080", Handler: mux, } go server.ListenAndServe() listenSignal(context.Background(), server) } func listenSignal(ctx context.Context, httpSrv *http.Server) { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) select { case <-sigs: fmt.Println("notify sigs") httpSrv.Shutdown(ctx) fmt.Println("http shutdown") } }
咱們建立了一個 listenSignal 函數來監聽程序退出信號 listenSignal 函數中的 select 會一直阻塞直到收到退出信號,而後執行 Shutdown(ctx) 。事件
能夠看到,咱們是從新開啓了一個 goroutine 來啓動 http 服務監聽,而 Shutdown(ctx) 在主 goroutine 中,這樣才能等待全部鏈接閒置後再退出程序。文檔
啓動上述程序,咱們訪問 http://127.0.0.1:8080 頁面等待 5s 會輸出一個 「Hello world!」 在等待期間,咱們能夠嘗試 Ctrl+C 關閉程序,能夠看程序控制臺會等待輸出後纔打印 http shutdown 同時瀏覽器會顯示輸出內容;而關閉程序以後再新開一個瀏覽器窗口訪問 http://127.0.0.1:8080 則新開的窗口直接斷開沒法訪問。(這些操做須要在 5s 內完成,能夠適當調整處理時間方便咱們觀察實驗結果)get
經過該實驗咱們能看到,Shutdown(ctx) 會阻止新的鏈接進入並等待活躍鏈接處理完成後再終止程序,達到優雅退出的目的。
固然咱們還能夠進一步證實 Shutdown(ctx) 除了等待活躍鏈接的同時也會監聽 Context 完成事件,兩者有一個觸發都會觸發程序終止;
咱們將代碼稍做修改以下:
func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(10 * time.Second) fmt.Fprintln(w, "Hello world!") }) server := &http.Server{ Addr: ":8080", Handler: mux, } go server.ListenAndServe() listenSignal(context.Background(), server) } func listenSignal(ctx context.Context, httpSrv *http.Server) { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) select { case <-sigs: timeoutCtx,_ := context.WithTimeout(ctx, 3*time.Second) fmt.Println("notify sigs") httpSrv.Shutdown(timeoutCtx) fmt.Println("http shutdown") } }
咱們將 http 服務處理修改爲等待 10s, 監聽到退出事件後 ctx 修改爲 3s 超時的 Context,運行上述程序,而後 Ctrl+C 發送結束信號,咱們能夠直觀的看到,程序在等待 3s 後就終止了,此時即便 http 服務中的處理還沒完成,程序也終止了,瀏覽器中也直接中斷鏈接了。
須要注意的問題:咱們在 HandleFunc 中編寫的處理邏輯都是在主 goroutine 中完成的和 Shotdown 方法是一個同步操做,所以 Shutdown(ctx) 會等待完成,若是咱們的處理邏輯是在新的 goroutine 中或是一個像 Websock 這樣的長鏈接,則Shutdown(ctx) 不會等待處理完成,若是須要解決這類問題仍是須要利用 sync.WaitGroup 來進行同步等待。
技術總結:
1. Shutdown 方法要寫在主 goroutine 中;
2.在主 goroutine 中的處理邏輯纔會阻塞等待處理;
3.帶超時的 Context 是在建立時就開始計時了,所以須要在接收到結束信號後再建立帶超時的 Context。