微服务优雅关停的核心问题
在Kubernetes滚动更新或手动部署时,旧版本Pod会被SIGTERM信号终止。如果进程直接退出,正在处理的请求会被中断,数据库事务可能处于半提交状态,消息队列中的消息可能丢失。优雅关停要解决的就是这个问题:收到终止信号后,停止接收新请求,等待已有请求处理完毕,释放外部资源,再安全退出。
优雅关停的完整流程包括:信号捕获、停止接受新连接、排空已有连接、关闭外部资源连接、退出进程。每个步骤都有超时保护,避免因个别请求阻塞导致进程无法退出。
HTTP服务的信号捕获与关停
Go标准库的http.Server自带Shutdown方法,支持优雅关停。核心实现:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
w.Write([]byte("ok"))
})
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
log.Println("server started on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("received signal: %v, shutting down...", sig)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("server shutdown error: %v", err)
}
log.Println("server stopped")
}
Shutdown方法的行为:首先关闭监听器停止接受新连接,然后等待已有连接处理完毕或超时。超时后强制关闭所有连接。15秒的超时值需要根据服务的最长请求处理时间设置,通常建议设置为最长请求时间的2倍。
gRPC服务的优雅关停
gRPC的优雅关停与HTTP不同,需要调用GracefulStop方法:
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
grpcServer := grpc.NewServer()
pb.RegisterYourServiceServer(grpcServer, &server{})
go func() {
if err := grpcServer.Serve(lis); err != nil {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down gRPC server...")
grpcServer.GracefulStop()
log.Println("gRPC server stopped")
}
gRPC的GracefulStop会等待所有正在处理的RPC调用完成。如果某些长连接RPC(如流式调用)持续时间很长,需要额外处理。可以在RPC方法中检查context的Done通道,当服务准备关停时cancel context,让流式调用主动退出。
外部资源连接的有序释放
微服务通常依赖数据库连接池、Redis连接、消息队列等外部资源。关停时需要按依赖关系逆序释放,先关闭消息消费者(不再消费新消息),再关闭HTTP/gRPC服务器(排空请求),最后关闭数据库和Redis连接:
func main() {
db := initDB()
redis := initRedis()
mqConsumer := initMQConsumer()
srv := initHTTPServer()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 1. stop consuming new messages
mqConsumer.Stop()
// 2. shutdown HTTP server, drain requests
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
srv.Shutdown(ctx)
// 3. close database connection
db.Close()
// 4. close redis connection
redis.Close()
log.Println("all resources released")
}
Kubernetes环境下的超时配置
Kubernetes的Pod终止流程与优雅关停配合至关重要。Pod收到删除请求后,Kubernetes执行以下步骤:
1. 将Pod从Service的Endpoints列表中移除(不再路由新请求)
2. 向Pod中的容器发送SIGTERM信号
3. 等待terminationGracePeriodSeconds(默认30秒)
4. 如果进程未退出,发送SIGKILL强制终止
terminationGracePeriodSeconds必须大于服务的优雅关停超时时间。如果服务关停需要15秒,建议设置terminationGracePeriodSeconds为20-25秒,留出缓冲。
readinessProbe的配置也很关键。当Pod收到SIGTERM时,readinessProbe应该立即失败,确保Kubernetes在Service层停止路由新请求到该Pod。但readinessProbe失败与Endpoints移除之间存在时间差(默认2秒),这就是为什么即使有readinessProbe,服务端也需要在关停时主动关闭监听器。
关停过程的可观测性
在关停过程中记录关键事件,有助于排查关停异常:
func gracefulShutdown(srv *http.Server, timeout time.Duration) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutdown: signal received")
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan struct{})
go func() {
srv.Shutdown(ctx)
close(done)
}()
select {
case <-done:
log.Printf("shutdown: completed in %v", time.Since(start))
case <-ctx.Done():
log.Printf("shutdown: timed out after %v, forcing exit", time.Since(start))
}
}
通过记录收到信号的时间、关停耗时、是否超时等信息,可以在日志中清晰地追踪每次关停过程。结合Prometheus的shutdown指标(如正在处理的请求数、连接排空耗时),可以建立完整的关停可观测体系,确保微服务在滚动更新过程中零请求丢失。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-wei-fu-wu-you-ya-guan-ting-shi-xian-xin-hao-chu/