gRPC健康检查
gRPC提供健康检查机制,允许服务器应用程序发出他们的状态信号给对应的客户端,而不会断开与客户端的连接。例如,当服务器本身已经启动,但是它依赖的另一个服务不可用,该业务场景就可以使用健康检查机制。
健康检查机制通常结合负载均衡机制配套使用,当检查到后端服务状态异常时,选择正常的Node节点,进行RPC调用,知道异常Node节点正常为止。
注意: 健康检查机制需要服务名称,所以客户端需要配置服务名称。可以设置空字符串,表示指定主机、端口上所有服务的运行状况都需要被监控。
健康检查协议
基于请求-响应式的健康检查协议,客户端需要定期轮询服务器。当集群服务规模不大的时候,这并不是问题。然后当集群规模非常庞大时,大量的客户端发送健康检查请求,那么会占用服务器资源、网络带宽,进一步影响系统正常运行。因此需要将健康检查协议转换为基于流式监控的API。
需要注意:这里有存在一个细微的缺点,当服务端健康检查代码变得不健康时,可能存在以下情况导致服务无法发送数据:
- 服务器停止,客户端断开连接
- 健康检查服务中的问题导致,但实际服务正常运行,客户端不能感知服务端最新状态
健康检查API
客户端有两种模式检查服务端的状态:
-
请求-响应模式 - 客户端不断轮训服务端状态,该方式不优雅
Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
-
监听机制 - 服务端主动推送状态给客户端
Watch(*HealthCheckRequest, Health_WatchServer) error
健康检查的核心接口
//健康服务API 接口定义.
type HealthServer interface {
// 请求服务不可用,请求失败 状态为: NOT_FOUND.
Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
// 1. 执行watch方法请求服务状态,该方法返回服务当前状态;并且当服务状态改变是主动通知客户端
// 2. 如果请求不可用,会返回 “SERVICE_UNKNOWN”,后续当服务状态正常时,推送正常状态给客户端
// 3. 当客户端接收到 “UNIMPLEMENTED”,表示该服务不支持,不应该发送重试请求
// 当客户端接收到 其他状态包含 OK, 允许客户端在合适的时机发送重试请求
Watch(*HealthCheckRequest, Health_WatchServer) error
}
健康检查状态
const (
HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0
HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1
HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2
// Used only by the Watch method.
HealthCheckResponse_SERVICE_UNKNOWN HealthCheckResponse_ServingStatus = 3
)
客户端行为
默认情况下禁用客户端检查;服务所有者可以通过配置启动检查机制。即使在服务配置中启用了通道参数,也可以在客户端上使用通道参数来禁用健康检查。
客户端第一次建立连接,如果已经启用健康检查,会立即调用Watch()方法,channel状态为CONNECTING,直到第一次接收到Response返回。接收到服务端返回的健康检查Response,如果状态为正常,则channel状态改变为 READY。否则channel状态为TRANSIENT_FAILURE。当后端服务从不健康状态转换为健康状态时,子通道的连接状态从TRANSIENT_FAILURE直接转换为READY,其间不会停止CONNECTING。
调用Watch()方法返回UNIMPLEMENTED状态时,客户端将禁用健康检查,并不会发送重试请求,但是channel状态为 READY,可以正常通信。但是客户端将记录channel事件,同时记录eroor日志。
调用Watch()方法返回其他状态,channel状态为TRANSIENT_FAILURE,会发送重试请求。为避免集中重试请求造成网络拥堵,客户端在两次重试之间使用指数回退。当客户端在接收到服务端返回的Response是,重置回退状态,立即发送下一次请求。然后重试请求将受指数回退(简单的理解,就是确定重试请求的时间间隔)的影响。当下一次重试开始是,channel状态转换为 CONNECTING
Channel就绪条件
由于网络IO读写的异步性,启用健康检查机制后,客服端有可能在接收到服务健康状态之前,已经存在(待运行)RPC调用。此时如果直接调用RPC接口,就会出现一些未知的情况。当第一次建立连接是,该问题可能会影响到更多的RPC。因为可能存在很多RPC排队等待通道连接,这些RPC将会同时发送。
为了避免上述情况,客户端在channel通道就绪之前,必须等待初始健康检查响应。
Example 代码
完整代码
- github完整代码
- 客户端配置
serviceConfig := grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "round_robin", //负载均衡策略
"healthCheckConfig": {
"serviceName": "" //指定服务名称
}
}`)
服务端代码
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
//启动健康检查服务
healthcheck := health.NewServer()
healthgrpc.RegisterHealthServer(s, healthcheck)
pb.RegisterEchoServer(s, &echoServer{})
go func() {
// 异步检查依赖并切换状态
// 初始化设置为服务正常状态
next := healthpb.HealthCheckResponse_SERVING
for {
//设置服务健康状态
healthcheck.SetServingStatus(system, next)
if next == healthpb.HealthCheckResponse_SERVING {
// 暂停休眠后,模拟设置服务状态为不可用
next = healthpb.HealthCheckResponse_NOT_SERVING
} else {
// 恢复服务状态为可用状态
next = healthpb.HealthCheckResponse_SERVING
}
//暂停 模拟数据发送
time.Sleep(*sleep)
}
}()
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
客户端代码
- 客户端启动健康检查
// step 1 定义服务配置
var serviceConfig = `{
"loadBalancingPolicy": "round_robin",
"healthCheckConfig": {
"serviceName": ""
}
}`
// step2 开启负载均衡策略 并指定健康检查服务名称
options := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithResolvers(r),
grpc.WithDefaultServiceConfig(serviceConfig),
}
// step3 这一步非常关键 通过init方法启动客服端检查
import _ "google.golang.org/grpc/health"
-
客户端健康检查核心代码
-
初始化客户端健康检查
func init() { fmt.Println("client health check init ..") internal.HealthCheckFunc = clientHealthCheck }
-
重试间隔时间
var ( backoffStrategy = backoff.DefaultExponential backoffFunc = func(ctx context.Context, retries int) bool { d := backoffStrategy.Backoff(retries) //通过定时器 指定重试间隔时间 timer := time.NewTimer(d) select { case <-timer.C: return true case <-ctx.Done(): timer.Stop() return false } } )
-
健康检查核心逻辑
const healthCheckMethod = "/grpc.health.v1.Health/Watch" func clientHealthCheck(ctx context.Context, newStream func(string) (interface{}, error), setConnectivityState func(connectivity.State, error), service string) error { tryCnt := 0 retryConnection: for { // 连接失败 进行重试 // Backs off if the connection has failed in some way without receiving a message in the previous retry. if tryCnt > 0 && !backoffFunc(ctx, tryCnt-1) { return nil } tryCnt++ if ctx.Err() != nil { return nil } // 设置channel 为 connecting 状态 setConnectivityState(connectivity.Connecting, nil) //通过stream 连接流 连接server Watch 方法,完成健康检查数据连接通道 rawS, err := newStream(healthCheckMethod) if err != nil { continue retryConnection } s, ok := rawS.(grpc.ClientStream) // Ideally, this should never happen. But if it happens, the server is marked as healthy for LBing purposes. if !ok { // channel 设置为 ready 状态 (UNIMPLEMENTED) setConnectivityState(connectivity.Ready, nil) return fmt.Errorf("newStream returned %v (type %T); want grpc.ClientStream", rawS, rawS) } // 发送健康检查请求 if err = s.SendMsg(&healthpb.HealthCheckRequest{Service: service}); err != nil && err != io.EOF { // Stream should have been closed, so we can safely continue to create a new stream. continue retryConnection } s.CloseSend() //检查状态 resp := new(healthpb.HealthCheckResponse) for { err = s.RecvMsg(resp) // Reports healthy for the LBing purposes if health check is not implemented in the server. if status.Code(err) == codes.Unimplemented { setConnectivityState(connectivity.Ready, nil) return err } // Reports unhealthy if server's Watch method gives an error other than UNIMPLEMENTED. if err != nil { setConnectivityState(connectivity.TransientFailure, fmt.Errorf("connection active but received health check RPC error: %v", err)) continue retryConnection } // As a message has been received, removes the need for backoff for the next retry by resetting the try count. tryCnt = 0 if resp.Status == healthpb.HealthCheckResponse_SERVING { setConnectivityState(connectivity.Ready, nil) } else { setConnectivityState(connectivity.TransientFailure, fmt.Errorf("connection active but health check failed. status=%s", resp.Status)) } } } }
-
验证结果
-
启动服务端
//开启两个服务端进行,并设置不同的休眠时间 go run server/main.go -port=50051 -sleep=5s go run server/main.go -port=50052 -sleep=10s
-
启动客户端
go run client/main.go
-
结果截图