編程學習網 > 編程語言 > go > 優雅地關閉或重啟 go web 項目
2020
03-27

優雅地關閉或重啟 go web 項目


我們編寫的Web項目部署之后,經常會因為需要進行配置變更或功能迭代而重啟服務,單純的kill -9 pid的方式會強制關閉進程,這樣就會導致服務端當前正在處理的請求失敗,那有沒有更優雅的方式來實現關機或重啟呢?


什么是優雅關機?

優雅關機就是服務端關機命令發出后不是立即關機,而是等待當前還在處理的請求全部處理完畢后再退出程序,是一種對客戶端友好的關機方式。而執行Ctrl+C關閉服務端時,會強制結束進程導致正在訪問的請求出現問題。


如何實現優雅關機?

Go 1.8版本之后, http.Server 內置的 Shutdown()方法就支持優雅地關機,具體示例如下:

// +build go1.8

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    go func() {
        // 開啟一個goroutine啟動服務
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // 等待中斷信號來優雅地關閉服務器,為關閉服務器操作設置一個5秒的超時
    quit := make(chan os.Signal, 1) // 創建一個接收信號的通道
    // kill 默認會發送 syscall.SIGTERM 信號
    // kill -2 發送 syscall.SIGINT 信號,我們常用的Ctrl+C就是觸發系統SIGINT信號
    // kill -9 發送 syscall.SIGKILL 信號,但是不能被捕獲,所以不需要添加它
    // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信號轉發給quit
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)  // 此處不會阻塞
    <-quit  // 阻塞在此,當接收到上述兩種信號時才會往下執行
    log.Println("Shutdown Server ...")
    // 創建一個5秒超時的context
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    // 5秒內優雅關閉服務(將未處理完的請求處理完再關閉服務),超過5秒就超時退出
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown: ", err)
    }

    log.Println("Server exiting")
}


如何驗證優雅關機的效果呢?


上面的代碼運行后會在本地的8080端口開啟一個web服務,它只注冊了一條路由/,后端服務會先sleep 5秒鐘然后才返回響應信息。

我們按下Ctrl+C時會發送syscall.SIGINT來通知程序優雅地關機,具體做法如下:

1.打開終端,編譯并執行上面的代碼

2.打開一個瀏覽器,訪問127.0.0.1:8080/,此時瀏覽器白屏等待服務端返回響應。

3.在終端迅速執行Ctrl+C命令給程序發送syscall.SIGINT信號4.此時程序并不立即退出而是等我們第2步的響應返回之后再退出,從而實現優雅關機。


優雅地重啟


優雅關機實現了,那么該如何實現優雅重啟呢?

我們可以使用 fvbock/endless 來替換默認的 ListenAndServe啟動服務來實現, 示例代碼如下:

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/fvbock/endless"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "hello gin!")
    })
    // 默認endless服務器會監聽下列信號:
    // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
    // 接收到 SIGHUP 信號將觸發`fork/restart` 實現優雅重啟(kill -1 pid會發送SIGHUP信號)
    // 接收到 syscall.SIGINT或syscall.SIGTERM 信號將觸發優雅關機
    // 接收到 SIGUSR2 信號將觸發HammerTime
    // SIGUSR1 和 SIGTSTP 被用來觸發一些用戶自定義的hook函數
    if err := endless.ListenAndServe(":8080", router); err!=nil{
        log.Fatalf("listen: %s\n", err)
    }

    log.Println("Server exiting")
}


如何驗證優雅重啟的效果呢?


我們通過執行kill -1 pid命令發送syscall.SIGINT來通知程序優雅重啟,具體做法如下:

1.打開終端,go build -o graceful_restart編譯并執行./graceful_restart,終端輸出當前pid(假設為43682)

2.將代碼中處理請求函數返回的hello gin!修改為hello q1mi!,再次編譯go build -o graceful_restart

3.打開一個瀏覽器,訪問127.0.0.1:8080/,此時瀏覽器白屏等待服務端返回響應。

4.在終端迅速執行kill -1 43682命令給程序發送syscall.SIGHUP信號

5.等第3步瀏覽器收到響應信息hello gin!后再次訪問127.0.0.1:8080/會收到hello q1mi!的響應。

6.在不影響當前未處理完請求的同時完成了程序代碼的替換,實現了優雅重啟。


但是需要注意的是,此時程序的PID變化了,因為endless 是通過fork子進程處理新請求,待原進程處理完當前請求后再退出的方式實現優雅重啟的。所以當你的項目是使用類似supervisor的軟件管理進程時就不適用這種方式了。


總結

無論是優雅關機還是優雅重啟歸根結底都是通過監聽特定系統信號,然后執行一定的邏輯處理保障當前系統正在處理的請求被正常處理后再關閉當前進程。使用優雅關機還是使用優雅重啟以及怎么實現,這就需要根據項目實際情況來決定了。

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取