gRPC 健康检查

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 代码

完整代码

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
    
  • 结果截图

    在这里插入图片描述

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
k8s探针可以用于检测应用程序的健康状态,并根据结果决定是否将流量转发到该容器。对于PHP应用程序,可以使用以下方法实现k8s探针的健康检查: 1. HTTP 探针:通过向应用程序的某个HTTP端点发送请求,来检查应用程序的健康状态。你可以在应用程序中创建一个特定的路径或端点,用于检查PHP应用程序的健康状况。例如,可以使用`/health`路径作为探针路径。当k8s探针发送HTTP请求到该路径时,应用程序可以返回一个合适的HTTP状态码来指示其健康状态。 2. TCP 探针:通过尝试与应用程序的特定TCP端口建立连接来检查应用程序的健康状态。对于PHP应用程序,你可以使用应用程序监听的端口作为探针目标端口,并通过尝试与该端口建立连接来检查应用程序的健康状况。 至于使用gRPC实现k8s探针的健康检查,你可以按照以下步骤进行操作: 1. 在你的PHP应用程序中,使用gRPC框架创建一个gRPC服务。 2. 在该gRPC服务中实现一个健康检查方法,该方法可以返回一个表示应用程序健康状态的gRPC响应。 3. 在k8s的Pod配置中,配置一个gRPC探针,指定要调用的gRPC服务和健康检查方法。 4. k8s将定期调用该gRPC探针,并根据返回的状态决定容器的健康状况。 需要注意的是,实现gRPC探针需要你的PHP应用程序具备gRPC支持,并且在k8s集群中使用的镜像中已经安装了gRPC扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值