前言
每次更新完代碼,更新完配置文件后 就直接這么 ctrl+c
真的沒問題嗎,ctrl+c
到底做了些什么事情呢?
在這一節中我們簡單講述 ctrl+c
背后的信號以及如何在Gin
中優雅的重啟服務,也就是對 HTTP
服務進行熱更新
項目地址:https://github.com/EDDYCJY/go-gin-example
ctrl + c
內核在某些情況下發送信號,比如在進程往一個已經關閉的管道寫數據時會產生
SIGPIPE
信號
在終端執行特定的組合鍵可以使系統發送特定的信號給此進程,完成一系列的動作
命令 | 信號 | 含義 |
---|---|---|
ctrl + c | SIGINT | 強制進程結束 |
ctrl + z | SIGTSTP | 任務中斷,進程掛起 |
ctrl + \ | SIGQUIT | 進程結束 和 dump core |
ctrl + d | EOF | |
SIGHUP | 終止收到該信號的進程。若程序中沒有捕捉該信號,當收到該信號時,進程就會退出(常用於 重啟、重新加載進程) |
因此在我們執行ctrl + c
關閉gin
服務端時,會強制進程結束,導致正在訪問的用戶等出現問題
常見的 kill -9 pid
會發送 SIGKILL
信號給進程,也是類似的結果
信號
本段中反復出現信號是什么呢?
信號是 Unix
、類 Unix
以及其他 POSIX
兼容的操作系統中進程間通訊的一種有限制的方式
它是一種異步的通知機制,用來提醒進程一個事件(硬件異常、程序執行異常、外部發出信號)已經發生。當一個信號發送給一個進程,操作系統中斷了進程正常的控制流程。此時,任何非原子操作都將被中斷。如果進程定義了信號的處理函數,那么它將被執行,否則就執行默認的處理函數
所有信號
$ 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
怎樣算優雅
目的
- 不關閉現有連接(正在運行中的程序)
- 新的進程啟動並替代舊進程
- 新的進程接管新的連接
- 連接要隨時響應用戶的請求,當用戶仍在請求舊進程時要保持連接,新用戶應請求新進程,不可以出現拒絕請求的情況
流程
1、替換可執行文件或修改配置文件
2、發送信號量 SIGHUP
3、拒絕新連接請求舊進程,但要保證已有連接正常
4、啟動新的子進程
5、新的子進程開始 Accet
6、系統將新的請求轉交新的子進程
7、舊進程處理完所有舊連接后正常結束
實現優雅重啟
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-blog 的 main.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
了新的子進程 pid
為 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
大致意思為主進程(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.Server
的 Shutdown 方法
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")
}
小結
在日常的服務中,優雅的重啟(熱更新)是非常重要的一環。而 Golang
在 HTTP
服務方面的熱更新也有不少方案了,我們應該根據實際應用場景挑選最合適的