前言
最近在學習Golang,在架設http server時,發現Golang在v1.8推出了一個叫做Graceful Shutdown的功能,說來慚愧,先前寫的幾個小專案雖然也都是http server,卻沒有使用過像這樣的功能,所以用一篇小文章來記錄一下。
什麼是Graceful Shutdown?
先想像一下今天已經有一個已經上線的電商網站服務,使用者會在這個網站上面瀏覽、購買商品,當這個網站要升版時,服務就必須暫停 — — 意思是,所有還在網站上進行的交易、連線都會被中斷。若是像這樣強制關閉服務,待下一次服務啟動時,我們可能就會發現資料上會有預期外的錯誤與差異。
Graceful Shutdown直譯來說就是「優雅地關機」,這個意思是,當伺服器收到終止的指令後,如果手上還有正在執行的process,它會先處理完,之後才會真的關閉服務,這麼作不僅可以保障資料的一致與完整性,我們也不需要害怕突然中止程式可能會導致非預期的錯誤。
普通的HTTP server
以下我會先介紹「沒有」Graceful Shutdown機制且強制關閉服務的觀察現象,在這邊,HTTP server的實作會透過Gin framework來實現。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
log.Println("starting server...")
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(10 * time.Second)
c.String(http.StatusOK, "hello there")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
srv.ListenAndServe()
}
根據以上設計,當我們前往localhost:8080後,等十秒會收到從server回傳的字串;若我們在這十秒內強制中止server,client連線也就會跟著被強制中斷,導致終端機噴出以下錯誤:
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server
Graceful Shutdown in HTTP server
前一次實驗的錯誤是因為當我們結束server服務時,還有一個client連線還沒有收到預期的回覆,卻被強制中斷連線所導致的錯誤。
而Golang推出的Graceful Shutdown功能,則是當我們結束server服務時,他會先把連接阜給關閉,接著,對於剩下還沒執行完的連線,會等待他們執行完畢後並一一關閉。
首先我們一樣先定義好server的連接阜以及router。
log.Println("starting server...")
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"hello": "sophie",
})
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
創建一個在背景運行的goroutine,並在goroutine裡執行server 服務。這麼作是為了不要讓server的運行與後續我們要讓client連線執行完畢的後續流程互相干擾。
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to initialize server: %v\n", err)
}
}()
宣告一個接收os.Signal訊號的channel,如果系統的SIGINT或者SIGTERM訊號被發出,就會關閉quit通道。
換句話說,如果關閉服務的訊號一直沒有被發出,channel就會一直卡在那邊,以維持正常運作的狀態;當訊號發出後,通道被關閉,後續流程就會被啟動。
編按:當我們直接kill process時,會收到SIGINT訊號;若這個服務包在docker中,而我們執行docker rm 時,會收到SIGTERM訊號。
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
當關閉訊號發出後,我們傳入一個context,讓程式等待十秒的時間(這邊的等待時間可以根據自己需求作更新),同時,我們將這個context傳入Shutdown函式,讓程式可以在規定的時間內完成所有正在執行的請求,如果規定的時間到了,仍有請求尚未完成,那麼就會強制關閉連線。
也就是說,假設我們設定的時間不夠久,整個作法就還是強制關閉連線,因此在時間設定上,必須考量到自身server處理資料的時間。
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
//確保程式結束後,釋放相關資源
defer cancel()
log.Println("Shutting down server...")
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v\n", err)
}
大功告成,整體的程式碼可以參照以下:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
log.Println("starting server...")
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(10 * time.Second)
c.JSON(http.StatusOK, gin.H{
"hello": "sophie",
})
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to initialize server: %v\n", err)
}
}()
log.Printf("Listening on port %v\n", srv.Addr)
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Println("Shutting down server...")
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v\n", err)
}
}
使用改版後的程式碼運行後,會發現第一次實驗出現的錯誤消失了,並且成功拿到從server端回傳的字串。
Reference
Go by Example: Signals https://gobyexample.com/signals
[Go 教學] 什麼是 graceful shutdown? https://blog.wu-boy.com/2020/02/what-is-graceful-shutdown-in-golang/
Gin Web Framework: Graceful restart or stop https://gin-gonic.com/docs/examples/graceful-restart-or-stop/