Go微服务优雅上下线:gRPC健康检查与流量无损切换实践

微服务上下线的流量损问题

微服务滚动更新时,如果旧实例直接被杀、新实例未就绪就接收流量,会导致请求失败。具体表现有两种:一是旧实例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/

(0)
小编小编
上一篇 6小时前
下一篇 6小时前

相关推荐