Gin實踐 連載七 Golang優雅重啓HTTP服務

優雅的重啓服務

在前面編寫案例代碼時,我相信你會想到html

每次更新完代碼,更新完配置文件後
就直接這麼 ctrl+c 真的沒問題嗎,ctrl+c到底作了些什麼事情呢?git

在這一節中咱們簡單講述 ctrl+c 背後的信號以及如何在Gin優雅的重啓服務,也就是對 HTTP 服務進行熱更新github

原文地址:Golang優雅重啓HTTP服務
項目地址:https://github.com/EDDYCJY/go...golang

ctrl + c

內核在某些狀況下發送信號,好比在進程往一個已經關閉的管道寫數據時會產生 SIGPIPE信號

在終端執行特定的組合鍵可使系統發送特定的信號給此進程,完成一系列的動做segmentfault

命令 信號 含義
ctrl + c SIGINT 強制進程結束
ctrl + z SIGTSTP 任務中斷,進程掛起
ctrl + \ SIGQUIT 進程結束 和 dump core
ctrl + d EOF
SIGHUP 終止收到該信號的進程。若程序中沒有捕捉該信號,當收到該信號時,進程就會退出(經常使用於 重啓、從新加載進程)

所以在咱們執行ctrl + c關閉gin服務端時,會強制進程結束,致使正在訪問的用戶等出現問題api

常見的 kill -9 pid 會發送 SIGKILL 信號給進程,也是相似的結果緩存

信號

本段中反覆出現信號是什麼呢?安全

信號是 Unix 、類 Unix 以及其餘 POSIX 兼容的操做系統中進程間通信的一種有限制的方式服務器

它是一種異步的通知機制,用來提醒進程一個事件(硬件異常、程序執行異常、外部發出信號)已經發生。當一個信號發送給一個進程,操做系統中斷了進程正常的控制流程。此時,任何非原子操做都將被中斷。若是進程定義了信號的處理函數,那麼它將被執行,不然就執行默認的處理函數less

全部信號

$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

怎樣算優雅

目的

  • 不關閉現有鏈接(正在運行中的程序)
  • 新的進程啓動並替代舊進程
  • 新的進程接管新的鏈接
  • 鏈接要隨時響應用戶的請求,當用戶仍在請求舊進程時要保持鏈接,新用戶應請求新進程,不能夠出現拒絕請求的狀況

流程

一、替換可執行文件或修改配置文件

二、發送信號量 SIGHUP

三、拒絕新鏈接請求舊進程,但要保證已有鏈接正常

四、啓動新的子進程

五、新的子進程開始 Accet

六、系統將新的請求轉交新的子進程

七、舊進程處理完全部舊鏈接後正常結束

實現優雅重啓

endless

Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+)

咱們藉助 fvbock/endless 來實現 Golang HTTP/HTTPS 服務從新啓動的零停機

endless server 監聽如下幾種信號量:

  • syscall.SIGHUP:觸發 fork 子進程和從新啓動
  • syscall.SIGUSR1/syscall.SIGTSTP:被監聽,但不會觸發任何動做
  • syscall.SIGUSR2:觸發 hammerTime
  • syscall.SIGINT/syscall.SIGTERM:觸發服務器關閉(會完成正在運行的請求)

endless 正正是依靠監聽這些信號量,完成管控的一系列動做

安裝

go get -u github.com/fvbock/endless

編寫

打開 gin-blogmain.go文件,修改文件:

package main

import (
    "fmt"
    "log"
    "syscall"

    "github.com/fvbock/endless"

    "gin-blog/routers"
    "gin-blog/pkg/setting"
)

func main() {
    endless.DefaultReadTimeOut = setting.ReadTimeout
    endless.DefaultWriteTimeOut = setting.WriteTimeout
    endless.DefaultMaxHeaderBytes = 1 << 20
    endPoint := fmt.Sprintf(":%d", setting.HTTPPort)

    server := endless.NewServer(endPoint, routers.InitRouter())
    server.BeforeBegin = func(add string) {
        log.Printf("Actual pid is %d", syscall.Getpid())
    }

    err := server.ListenAndServe()
    if err != nil {
        log.Printf("Server err: %v", err)
    }
}

endless.NewServer 返回一個初始化的 endlessServer 對象,在 BeforeBegin 時輸出當前進程的 pid,調用 ListenAndServe 將實際「啓動」服務

驗證

編譯
$ go build main.go
執行
$ ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
...
Actual pid is 48601

啓動成功後,輸出了pid爲 48601;在另一個終端執行 kill -1 48601 ,檢驗先前服務的終端效果

[root@localhost go-gin-example]# ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /auth                     --> ...
[GIN-debug] GET    /api/v1/tags              --> ...
...

Actual pid is 48601

...

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

能夠看到該命令已經掛起,而且 fork 了新的子進程 pid48755

48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

大體意思爲主進程(pid爲48601)接受到 SIGTERM 信號量,關閉主進程的監聽而且等待正在執行的請求完成;這與咱們先前的描述一致

喚醒

這時候在 postman 上再次訪問咱們的接口,你能夠驚喜的發現,他「復活」了!

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection


$ [GIN] 2018/03/15 - 13:00:16 | 200 |     188.096µs |   192.168.111.1 | GET      /api/v1/tags...

這就完成了一次正向的流轉了

你想一想,每次更新發布、或者修改配置文件等,只須要給該進程發送SIGTERM信號,而不須要強制結束應用,是多麼便捷又安全的事!

問題

endless 熱更新是採起建立子進程後,將原進程退出的方式,這點不符合守護進程的要求

http.Server - Shutdown()

若是你的Golang >= 1.8,也能夠考慮使用 http.ServerShutdown 方法

package main

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


    "gin-blog/routers"
    "gin-blog/pkg/setting"
)

func main() {
    router := routers.InitRouter()

    s := &http.Server{
        Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
        Handler:        router,
        ReadTimeout:    setting.ReadTimeout,
        WriteTimeout:   setting.WriteTimeout,
        MaxHeaderBytes: 1 << 20,
    }

    go func() {
        if err := s.ListenAndServe(); err != nil {
            log.Printf("Listen: %s\n", err)
        }
    }()
    
    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    <- quit

    log.Println("Shutdown Server ...")

    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    if err := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }

    log.Println("Server exiting")
}

小結

在平常的服務中,優雅的重啓(熱更新)是很是重要的一環。而 GolangHTTP 服務方面的熱更新也有很多方案了,咱們應該根據實際應用場景挑選最合適的

參考

本系列示例代碼

本系列目錄

拓展閱讀

相關文章
相關標籤/搜索