Go 語言實現優雅關機和重啓的示例很詳細
在生產環境中,服務的更新和維護是不可避免的。粗暴地終止服務會導致:
-
正在處理的請求被中斷
-
數據庫事務未完成
-
緩存數據丟失
-
客戶端連接異常
本文將深入探討如何使用 Go 語言實現:
-
優雅關機
:等待現有請求完成後再關閉服務
-
優雅重啓
:無縫切換新舊進程,實現零停機更新
優雅關機實現
核心機制
完整實現代碼
package main
import(
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
funcmain(){
// 創建路由
mux := http.NewServeMux()
mux.HandleFunc("/cook-hotpot",func(w http.ResponseWriter, r *http.Request){
// 模擬長時間處理任務(如煮火鍋)
time.Sleep(5* time.Second)
w.Write([]byte("您的火鍋已準備好,請慢用!"))
})
// 創建HTTP服務器
srv :=&http.Server{
Addr:":8080",
Handler: mux,
}
// 啓動服務器
gofunc(){
log.Println("火鍋店開始營業,監聽端口 8080...")
if err := srv.ListenAndServe(); err !=nil&& err != http.ErrServerClosed {
log.Fatalf("服務器錯誤: %v\n", err)
}
}()
// 等待中斷信號
quit :=make(chan os.Signal,1)
// 捕獲SIGINT(CTRL+C)和SIGTERM信號
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("收到關閉信號,火鍋店準備停止營業...")
// 設置5秒超時上下文
ctx, cancel := context.WithTimeout(context.Background(),5*time.Second)
defercancel()
// 優雅關閉服務器
if err := srv.Shutdown(ctx); err !=nil{
log.Fatalf("服務器關閉失敗: %v\n", err)
}
log.Println("火鍋店已安全關閉,所有顧客都已用餐完畢")
}
關鍵點解析
-
信號處理
:監聽
SIGINT(Ctrl+C) 和SIGTERM(kill 默認信號) -
超時控制
:通過
context.WithTimeout設置最大等待時間 -
資源釋放
:
Shutdown()會關閉所有空閒連接和監聽 socket
測試方法
-
啓動服務:
go run main.go -
發起請求:
curl http://localhost:8080/cook-hotpot -
立即發送停止信號:
Ctrl+C -
觀察日誌輸出,確認請求完成後再關閉
優雅重啓實現
核心機制
使用 endless 庫實現
package main
import(
"fmt"
"log"
"net/http"
"time"
"github.com/fvbock/endless"
)
funcmain(){
mux := http.NewServeMux()
mux.HandleFunc("/greet",func(w http.ResponseWriter, r *http.Request){
// 模擬處理時間
time.Sleep(3* time.Second)
w.Write([]byte("歡迎光臨,小區新保安在此!"))
})
// 使用endless創建服務器
srv := endless.NewServer(":8080", mux)
// 設置連接關閉超時時間
srv.BeforeBegin =func(add string){
log.Printf("當前進程PID: %d", os.Getpid())
}
// 啓動服務
log.Println("小區保安開始值班...")
if err := srv.ListenAndServe(); err !=nil{
if err != endless.ErrServerClosed {
log.Printf("服務器錯誤: %v\n", err)
}
}
log.Println("保安交接班完成,老保安下班")
}
關鍵點解析
-
平滑過渡
:新進程啓動後接管監聽端口
-
請求處理
:舊進程繼續處理已接收的請求
-
進程管理
:自動處理父子進程關係
測試方法
-
首次啓動:
go run main.go,記錄 PID -
發起請求:
curl http://localhost:8080/greet -
修改代碼並重新編譯
-
發送重啓信號:
kill -1 <PID> -
觀察新請求返回新內容,舊請求不受影響
進階實現方案
自定義優雅重啓實現
package main
import(
"context"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
var(
listener net.Listener
server *http.Server
)
funcmain(){
// 初始化服務器
setupServer()
// 監聽重啓信號
gowatchSignals()
// 啓動服務
if err := server.Serve(listener); err !=nil&& err != http.ErrServerClosed {
log.Fatalf("服務器錯誤: %v\n", err)
}
}
funcsetupServer(){
mux := http.NewServeMux()
mux.HandleFunc("/greet",func(w http.ResponseWriter, r *http.Request){
time.Sleep(3* time.Second)
w.Write([]byte("自定義優雅重啓實現!"))
})
server =&http.Server{Handler: mux}
// 複用監聽socket
var err error
listener, err = net.Listen("tcp",":8080")
if err !=nil{
log.Fatal(err)
}
}
funcwatchSignals(){
sig :=make(chan os.Signal,1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
for s :=range sig {
switch s {
case syscall.SIGHUP:// 優雅重啓
log.Println("收到重啓信號,開始優雅重啓...")
restartServer()
case syscall.SIGINT, syscall.SIGTERM:// 優雅關閉
log.Println("收到關閉信號,開始優雅關閉...")
shutdownServer()
return
}
}
}
funcrestartServer(){
// 啓動新進程
execFile := os.Args[0]
cmd := exec.Command(execFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Start(); err !=nil{
log.Printf("啓動新進程失敗: %v\n", err)
return
}
// 關閉舊進程
gofunc(){
time.Sleep(5* time.Second)// 給新進程啓動時間
shutdownServer()
}()
}
funcshutdownServer(){
ctx, cancel := context.WithTimeout(context.Background(),10*time.Second)
defercancel()
if err := server.Shutdown(ctx); err !=nil{
log.Printf("服務器關閉錯誤: %v\n", err)
}
log.Println("服務器已安全關閉")
}
生產環境最佳實踐
1. 健康檢查接口
mux.HandleFunc("/health",func(w http.ResponseWriter, r *http.Request){
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
2. 優雅關閉超時控制
// 分階段關閉
gofunc(){
<-quit
log.Println("開始優雅關閉...")
// 第一階段:停止接收新請求
if err := server.Shutdown(context.Background()); err !=nil{
log.Printf("第一階段關閉失敗: %v", err)
}
// 第二階段:強制關閉剩餘連接
ctx, cancel := context.WithTimeout(context.Background(),5*time.Second)
defercancel()
if err := server.Shutdown(ctx); err !=nil{
log.Printf("第二階段關閉失敗: %v", err)
}
}()
3. 多服務協調關閉
// 使用sync.WaitGroup等待多個服務關閉
var wg sync.WaitGroup
// HTTP服務器
wg.Add(1)
gofunc(){
defer wg.Done()
if err := httpServer.Shutdown(ctx); err !=nil{
log.Printf("HTTP服務器關閉錯誤: %v", err)
}
}()
// gRPC服務器
wg.Add(1)
gofunc(){
defer wg.Done()
grpcServer.GracefulStop()
}()
wg.Wait()
4. 容器化部署注意事項
# Dockerfile示例
FROM golang:1.18 as builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM alpine:latest
WORKDIR /root/
COPY--from=builder /app/server .
CMD ["./server"]
使用docker stop時會發送SIGTERM信號,確保正確處理:
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
常見問題解決方案
1. 殭屍進程處理
// 在父進程退出前等待子進程
cmd := exec.Command(...)
cmd.SysProcAttr =&syscall.SysProcAttr{
Setpgid:true,
Pgid:0,
}
// 父進程退出時殺死整個進程組
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
2. 端口複用
// 設置SO_REUSEPORT
lc := net.ListenConfig{
Control:func(network, address string, c syscall.RawConn)error{
var opErr error
err := c.Control(func(fd uintptr){
opErr = syscall.SetsockoptInt(
int(fd),
syscall.SOL_SOCKET,
syscall.SO_REUSEPORT,
1,
)
})
return opErr
},
}
listener, err := lc.Listen(context.Background(),"tcp",":8080")
3. 長連接處理
// 服務器配置
server :=&http.Server{
ReadTimeout:5* time.Second,
WriteTimeout:10* time.Second,
IdleTimeout:15* time.Second,// 自動關閉空閒連接
}
性能對比與選擇建議
總結與擴展閱讀
本文詳細介紹了 Go 語言實現優雅關機和重啓的多種方法,關鍵要點包括:
-
優雅關機
通過
http.Server.Shutdown()實現,確保完成現有請求 -
優雅重啓
可通過 endless 庫或自定義實現,實現零停機部署
-
生產環境
需要考慮健康檢查、超時控制和多服務協調
擴展學習方向:
-
Kubernetes 生命週期管理
-
服務網格 (Service Mesh) 中的流量管理
-
藍綠部署和金絲雀發佈策略
-
分佈式系統的領導者選舉模式
通過合理使用這些技術,可以構建出高可用、易維護的 Go 語言微服務架構。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4dxSLhyDFH_irNLBuN6FsQ