微服务上下线的流量损问题
微服务滚动更新时,如果旧实例直接被杀、新实例未就绪就接收流量,会导致请求失败。具体表现有两种:一是旧实例SIGTERM后仍有进行中的长连接请求被强制中断;二是新实例启动后Kubernetes立即将流量打入,但应用尚未完成初始化。Go微服务的gRPC协议场景下,这个问题更加突出,因为gRPC使用长连接和多路复用,连接生命周期管理比HTTP更复杂。本文通过gRPC健康检查协议配合Kubernetes生命周期钩子,实现流量无损切换。
gRPC健康检查协议集成
gRPC官方定义了标准健康检查协议grpc.health.v1.Health,Kubernetes的grpc健康检查原生支持该协议。在Go服务中集成健康检查:
package main
import (
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
lis, _ := net.Listen("tcp", ":50051")
server := grpc.NewServer()
healthServer := health.NewServer()
grpc_health_v1.RegisterHealthServer(server, healthServer)
// 应用初始化完成后设置为SERVING
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
// 注册业务服务...
server.Serve(lis)
}
空字符串表示整体服务健康状态,也可为每个gRPC Service设置独立健康状态。
Kubernetes gRPC健康检查配置
Kubernetes 1.24+原生支持gRPC健康检查探针:
apiVersion: v1
kind: Pod
spec:
containers:
- name: my-grpc-service
image: registry.yunthe.com/my-service:v1
ports:
- containerPort: 50051
startupProbe:
grpc:
port: 50051
failureThreshold: 30
periodSeconds: 2
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 15
periodSeconds: 20
startupProbe在启动阶段允许最多60秒(30次x2秒)完成初始化,初始化期间不触发readiness和liveness检查。readinessProbe失败时Pod从Service Endpoints摘除,新请求不再路由到该Pod。
优雅下线:SIGTERM信号处理
Kubernetes杀Pod时先发送SIGTERM,等待terminationGracePeriodSeconds(默认30秒)后再SIGKILL。Go服务需捕获SIGTERM信号并执行优雅关闭:
func main() {
// ...server初始化...
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh
// 1. 先标记为NOT_SERVING,拒绝新请求
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
// 2. 等待readinessProbe失败,Kubernetes摘除Endpoints
time.Sleep(12 * time.Second) // 大于readinessProbe.periodSeconds
// 3. 停止接收新连接,等待已有请求完成
server.GracefulStop()
}
关键步骤是标记NOT_SERVING后等待一个readinessProbe周期,确保Kubernetes完成Endpoints摘除。这个等待时间必须大于readinessProbe的periodSeconds,否则可能存在时间窗口内仍有流量打入。
preStop钩子配合流量排空
另一种更可靠的方案是在Pod的preStop钩子中主动排空流量:
spec:
containers:
- name: my-grpc-service
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 45
preStop钩子在Pod被标记为Terminating后、SIGTERM发送前执行。Kubernetes在标记Terminating的同时将Pod从Endpoints摘除,因此preStop的sleep期间新请求已不再路由进来,而旧连接可以继续处理。
组合策略:preStop sleep 15秒(排空流量)+ SIGTERM后GracefulStop等待现有请求完成(最多30秒),总超时45秒。实测这个方案可将滚动更新期间的请求失败率从0.5%-2%降低到接近零。
gRPC客户端重试与连接管理
即使服务端做了优雅上下线,客户端也需要正确处理连接状态变化。Go gRPC客户端配置:
conn, err := grpc.Dial(
"my-service:50051",
grpc.WithDefaultServiceConfig(`{
"methodConfig": [{
"name": [{"service": "mypackage.MyService"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
},
"timeout": "5s"
}]
}`),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second,
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}),
)
retryPolicy对UNAVAILABLE状态码自动重试,cover住后端Pod摘除瞬间的连接中断。keepalive参数确保空闲连接不会被中间件超时断开。
全链路验证与压测指标
优雅上下线方案上线前需通过压测验证。使用ghz或custom压测工具模拟滚动更新期间的请求:
# 持续发送请求,同时触发rolling update
ghz -c 50 -n 10000 -x 300 my-service:50051 mypackage.MyService/DoWork
# 另一个终端执行
kubectl rollout deployment my-service -n production
关注三个指标:
1. 请求成功率:优雅上下线后应不低于99.95%
2. P99延迟:rolling update期间P99可接受10-30%的波动,不应出现秒级超时
3. Pod终止耗时:正常场景下Pod在15-20秒内完成Terminating,超过45秒需排查是否有未释放的连接
如果压测期间仍有零星失败,检查Service Mesh(如Istio)的Endpoint更新延迟——Istio的Pilot推送Endpoint变更到Envoy有1-2秒延迟,需在preStop中增加对应的等待时间。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-wei-fu-wu-you-ya-shang-xia-xian-grpc-jian-kang-jian-cha/