golang 程序啟動一個 http 服務時,若服務被意外終止或中斷,會讓現有請求連接突然中斷,未處理完成的任務也會出現不可預知的錯誤,這樣即會造成服務硬終止;為了解決硬終止問題我們希望服務中斷或退出時將正在處理的請求正常返回並且等待服務停止前作的一些必要的處理工作。
我們可以看一個硬終止的例子:
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.Shutdown
在具體用應用中我們可以配合 signal.Notify 函數來監聽系統退出信號來完成程序優雅退出;
特別注意:server.ListenAndServe() 方法在 Shutdown 時會立刻返回,Shutdown 方法會阻塞至所有連接閑置或 context 完成,所以 Shutdown 的方法要寫在主 goroutine 中。
優雅退出實驗1:
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 內完成,可以適當調整處理時間方便我們觀察實驗結果)
通過該實驗我們能看到,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。
給大家推薦一個框架來快速構建帶優雅退出功能的 http 服務,詳見: https://www.cnblogs.com/zhucheer/p/12341595.html