問題背景
生產環境重要且復雜,許多的操作需要在任何場景都要保證正常運行。
如果我們對線上服務進行更新的步驟如下:
kill -9
服務- 再啟動服務
那么將不可避免的出現以下兩個問題:
- 未處理完的請求,被迫中斷,數據一致性被破壞
- 新服務啟動期間,請求無法進來,導致一段時間的服務不可用現象
一般有三種方案處理以上問題:
- 生產環境會通過四層(lb)->七層(gateway)->服務,那么可以通過流量調度的方式實現平滑重啟
- k8s管理
- 程序自身完成平滑重啟(本章介紹)
什么事平滑重啟
進程在不關閉其所監聽端口的情況下進行重啟,並且重啟的整個過程保證所有請求都能被正確處理。
主要步驟:
- 原進程(父進程)先
fork
一個子進程,同時讓fork
出來的子進程繼承父進程所監聽的socket
。 - 子進程完成初始化后,開始接收
socket
的請求。 - 父進程停止接收新的請求,並將當下的請求處理完,等待連接空閑后,平滑退出。
信號(Signal)
服務的平滑重啟,主要依賴進程接收的信號(實現進程間通信),這里簡單的介紹Golang
中信號的處理:
發送信號
- kill: 命令允許用戶發送一個特定的信號給進程
- raise: 庫函數可以發送特定的信號給當前進程
在Linux下運行man kill
可以查看此命令的介紹和用法。
kill -- terminate or signal a process
The kill utility sends a signal to the processes specified by the pid operands.
Only the super-user may send signals to other users' processes.
常用信號類型
信號的默認行為:
- term:信號終止進程
- core:產生核心轉儲文件並退出
- ignore:忽略信號
- stop:信號停止進程
- cont:信號恢復一個已停止的進程
信號 | 值 | 默認動作 | 說明 |
---|---|---|---|
SIGHUP | 1 | Term | HUP (hang up):終端控制進程結束(終端連接斷開) |
SIGINT | 2 | Term | INT (interrupt):用戶發送INTR字符(Ctrl+C)觸發(強制進程結束) |
SIGQUIT | 3 | Core | QUIT (quit):用戶發送QUIT字符(Ctrl+/)觸發(進程結束) |
SIGKILL | 9 | Term | KILL (non-catchable, non-ignorable kill):無條件結束程序(不能被捕獲、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用戶自定義信號1 |
SIGUSR2 | 31,12,17 | Term | 用戶自定義信號2 |
SIGKILL | 15 | KILL (non-catchable, non-ignorable kill) | TERM (software termination signal):程序終止信號 |
信號接收測試
package main import ( "log" "os" "os/signal" "syscall" ) func main() { sigs := make(chan os.Signal) signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2) // 監聽所有信號 log.Println("listen sig") signal.Notify(sigs) // 打印進程id log.Println("PID:", os.Getppid()) s := <-sigs log.Println("退出信號", s) }
go run main.go
## --> listen sig ## --> PID: 4604 kill -s HUP 4604 # --> Hangup: 1
實現案例
demo:
func main() { sigs := make(chan os.Signal) signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2) // 監聽所有信號 log.Println("listen sig") signal.Notify(sigs) // 打印進程id log.Println("PID:", os.Getppid()) go func() { for s := range sigs { switch s { case syscall.SIGHUP: log.Println("startNewProcess...") startNewProcess() log.Println("shutdownParentProcess...") shutdownParentProcess() case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: log.PrintLn("Program Exit...", s) case syscall.SIGUSR1: log.Println("usr1 signal", s) case syscall.SIGUSR2: log.Println("usr2 signal", s) default: log.Println("other signal", s) } } }() <-sigs }
推薦組件
shutdown優雅退出
go 1.8.x后,golang在http里加入了shutdown方法,用來控制優雅退出。
package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) func main() { s := http.NewServeMux() s.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) log.Println(w, "Hello world!") }) server := &http.Server{ Addr: ":8090", Handler: s, } 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: log.Println("notify sigs") httpSrv.Shutdown(ctx) log.Println("http shutdown") } }
小結
在平常的生產環境中,優雅的重啟是一個不可缺少的環節,無論是在go進程層間,或者上層的服務流量調度層面,都有許多的方案,選擇最適合團隊,保證項目穩定才是最重要的。