微服务优雅关机的核心问题
后端开发中,微服务的优雅关机(Graceful Shutdown)直接影响线上服务的可用性。粗暴终止进程会导致进行中的请求被截断、数据库事务未提交、消息队列消费位移未确认,造成数据不一致和业务异常。Go语言因其天然的并发模型,实现优雅关机有独特的优势,但也有需要注意的细节。
基础信号处理与关机流程
Go语言中优雅关机的第一步是正确捕获操作系统信号:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 启动HTTP服务
go func() {
log.Printf("Server listening on %s", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 阻塞等待信号
sig := <-quit
log.Printf("Received signal: %v, starting graceful shutdown...", sig)
// 设置关机超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 优雅关机:停止接受新连接,等待现有请求完成
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
log.Println("Server shutdown completed")
}
server.Shutdown(ctx)的行为是:立即停止接受新连接、等待现有请求处理完成或超时、超时后强制关闭。这段代码覆盖了最基本的HTTP服务优雅关机场景。
连接排空与资源释放
实际的微服务优雅关机远不止关闭HTTP监听。需要按顺序完成:停止接受新流量、排空正在处理的请求、释放数据库连接、确认消息消费位移、关闭GRPC连接、清理临时资源。
完整的优雅关机管理器
package graceful
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
type CleanupFunc func(ctx context.Context) error
type ShutdownManager struct {
cleanups []CleanupFunc
mu sync.Mutex
shutting bool
timeout time.Duration
}
func NewShutdownManager(timeout time.Duration) *ShutdownManager {
return &ShutdownManager{
cleanups: make([]CleanupFunc, 0),
timeout: timeout,
}
}
// Register 按LIFO顺序注册清理函数(先注册的最后执行)
func (m *ShutdownManager) Register(fn CleanupFunc) {
m.mu.Lock()
defer m.mu.Unlock()
m.cleanups = append(m.cleanups, fn)
}
// Wait 阻塞等待信号并执行优雅关机
func (m *ShutdownManager) Wait() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("Received %v, shutting down...", sig)
m.mu.Lock()
m.shutting = true
cleanups := make([]CleanupFunc, len(m.cleanups))
copy(cleanups, m.cleanups)
m.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
// 反向执行清理(后注册的先清理,类似defer)
for i := len(cleanups) - 1; i >= 0; i-- {
if err := cleanups[i](ctx); err != nil {
log.Printf("Cleanup step failed: %v", err)
}
}
log.Println("Graceful shutdown completed")
}
// IsShuttingDown 提供关机状态查询,供健康检查使用
func (m *ShutdownManager) IsShuttingDown() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.shutting
}
与Kubernetes配合的关机策略
微服务部署在Kubernetes中时,优雅关机需要与Pod生命周期配合:
func main() {
shutdown := graceful.NewShutdownManager(30 * time.Second)
// 1. 注册健康检查关机状态
healthHandler := &HealthHandler{shutdown: shutdown}
http.HandleFunc("/healthz", healthHandler.Check)
// 2. 注册HTTP服务关机
server := &http.Server{Addr: ":8080"}
shutdown.Register(func(ctx context.Context) error {
return server.Shutdown(ctx)
})
// 3. 注册数据库连接关闭
var db *sql.DB
shutdown.Register(func(ctx context.Context) error {
return db.Close()
})
// 4. 注册消息队列位移确认
var consumer *KafkaConsumer
shutdown.Register(func(ctx context.Context) error {
consumer.Close() // 确认最后的消费位移
return nil
})
// 健康检查在关机时返回503,Kubernetes从Service摘除Pod
shutdown.Register(func(ctx context.Context) error {
healthHandler.SetNotReady()
// 等待Kubernetes检测到503并摘除(留足时间)
time.Sleep(5 * time.Second)
return nil
})
go server.ListenAndServe()
shutdown.Wait()
}
关键细节:健康检查端点要先于HTTP Shutdown返回503,让Kubernetes从Service Endpoints中摘除Pod,新流量不再路由到该Pod。之后等待5秒让Kubernetes完成端点更新,再开始排空现有请求。
长连接与GRPC服务的优雅关机
GRPC服务的优雅关机比HTTP更复杂,因为存在长连接流式调用:
func setupGRPCShutdown(grpcServer *grpc.Server, shutdown *graceful.ShutdownManager) {
shutdown.Register(func(ctx context.Context) error {
// 停止接受新请求
grpcServer.GracefulStop()
// 如果超时,强制停止
done := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
grpcServer.Stop() // 强制停止
return ctx.Err()
}
})
}
对于正在处理的GRPC流式请求,服务端可以在关机前主动发送GoAway帧,客户端收到后应切换到新连接。在微服务调用链中,上游服务优雅关机时要主动通知下游重新连接,避免上游已关闭但下游仍在向旧地址发送请求的窗口期错误。
Go微服务优雅关机的生产实践要点
生产环境除了代码实现,还需要在Kubernetes部署配置中正确设置terminationGracePeriodSeconds(应大于关机超时时间加10秒余量)、preStop钩子(添加sleep让Kubernetes有足够时间更新端点)、就绪探针(关机时返回NotReady)。Go运行时的Goroutine泄漏也需要关注——关机时应取消所有context,确保没有Goroutine泄漏导致进程无法退出。最终目标是整个关机过程在30秒内完成,包括流量排空、资源释放和进程退出。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-wei-fu-wu-you-ya-guan-ji-shi-xian-cong-xin-hao/