微服务直接关机的风险场景
在Kubernetes环境中滚动更新或手动缩容时,Pod收到SIGTERM信号后有30秒的宽限期(terminationGracePeriodSeconds)。如果服务在收到信号后直接退出,正在处理的请求会中断,数据库事务会处于未提交状态,与消息中间件的连接会被强制断开可能导致消息丢失或重复消费。优雅关机的目标是在退出前完成三件事:停止接收新请求、排空正在处理的请求、关闭所有外部连接。
信号捕获与关机流程编排
Go语言中优雅关机的基础是正确捕获系统信号并编排关机流程。以下是生产级实现:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
Handler: newRouter(),
}
go func() {
if err := server.ListenAndServe();
err != http.ErrServerClosed {
log.Fatalf("HTTP异常: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit,
syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("收到信号 %v,开始关机", sig)
ctx, cancel := context.WithTimeout(
context.Background(),
25*time.Second,
)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("关机超时: %v", err)
}
closeExternalConnections()
log.Println("服务已关闭")
}
注意Shutdown的context超时设置为25秒而非30秒,留出5秒作为安全余量,避免被K8s在宽限期结束时SIGKILL强制终止。
连接排空:让正在处理的请求自然完成
server.Shutdown()会关闭监听器停止接收新连接,但已建立的HTTP连接会继续处理。对于长连接场景(WebSocket、gRPC流),需要额外的连接排空逻辑:
type ConnectionTracker struct {
wg sync.WaitGroup
}
func (ct *ConnectionTracker) Add() {
ct.wg.Add(1)
}
func (ct *ConnectionTracker) Done() {
ct.wg.Done()
}
func (ct *ConnectionTracker)
WaitWithTimeout(timeout time.Duration) bool {
done := make(chan struct{})
go func() {
ct.wg.Wait()
close(done)
}()
select {
case <-done:
return true
case <-time.After(timeout):
return false
}
}
func (ct *ConnectionTracker)
Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ct.Add()
defer ct.Done()
next.ServeHTTP(w, r)
})
}
数据库连接与消息中间件的优雅关闭
数据库连接池和消息中间件连接的关闭顺序很重要。原则是先关闭消息消费者(不再消费新消息),再等待正在处理的消息完成,然后关闭数据库连接(确保事务提交),最后关闭消息生产者:
func closeExternalConnections() {
log.Println("关闭消息消费者...")
consumer.Close()
log.Println("等待消息处理完成...")
messageWg.Wait()
log.Println("关闭数据库连接...")
if err := db.Close(); err != nil {
log.Printf("数据库关闭异常: %v", err)
}
log.Println("关闭消息生产者...")
producer.Close()
}
gRPC服务的优雅关机
gRPC服务的优雅关机比HTTP更复杂,因为需要处理流式RPC:
func startGRPCServer() {
lis, _ := net.Listen("tcp", ":9090")
grpcServer := grpc.NewServer()
pb.RegisterYourServiceServer(
grpcServer, &yourServiceImpl{})
go func() {
grpcServer.Serve(lis)
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit,
syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("gRPC开始关机")
done := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(done)
}()
select {
case <-done:
log.Println("gRPC已关闭")
case <-time.After(25 * time.Second):
log.Println("gRPC关机超时")
grpcServer.Stop()
}
}
Kubernetes环境下的健康检查配合
在K8s中优雅关机还需要配合readiness探针。当Pod收到SIGTERM后,应该立即将readiness探针标记为不健康,使Service不再将新流量路由到该Pod:
var ready atomic.Bool
func readinessHandler(
w http.ResponseWriter, r *http.Request) {
if ready.Load() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(
http.StatusServiceUnavailable)
}
}
func onShutdown() {
ready.Store(false)
log.Println("停止接收新流量")
}
完整的优雅关机不是简单的信号捕获加server.Shutdown(),而是需要将HTTP连接排空、数据库事务收尾、消息中间件消费者停止、gRPC流式RPC完成这些操作按正确顺序编排,并与K8s的readiness探针和terminationGracePeriodSeconds协调配合。每个环节的超时设置都要留出安全余量,避免被K8s强制SIGKILL。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-wei-fu-wu-you-ya-guan-ji-shi-zhan-cong-xin-hao-bu-huo/