grpc-go源码剖析六十五之服务器端HealthChecking原理介绍

已发表的技术专栏
0  grpc-go、protobuf、multus-cni 技术专栏 总入口

1  grpc-go 源码剖析与实战  文章目录

2  Protobuf介绍与实战 图文专栏  文章目录

3  multus-cni   文章目录(k8s多网络实现方案)

4  grpc、oauth2、openssl、双向认证、单向认证等专栏文章目录)

本节从服务器端角度来介绍HealthChecking的相关原理;

1、服务器端健康检测Watcher的核心思想

简单的说:

就是服务器端会将服务的最新状态更新到一个channel通道里,健康检测服务Watcher会从通道里获取最新的状态,

当状态跟以前的状态不一样时,服务器端就会将最新状态发送给客户端。

2、在健康检测服务启动下,服务器端处理客户端的请求主要经历了哪些阶段

主要经历的阶段如下:

  • 接收客户端的链接请求,建立底层链接;
  • 接收客户端流的请求,这里指的是客户端请求健康检测服务,如Watch服务
  • 对客户端请求的服务,如SayHello服务,获取服务状态,并反馈给客户端
  • 接收客户端真正的请求,如SayHello服务的请求,并将处理结果反馈给客户端

也就是说,在健康检测模式下,多执行了第2,3步;

本章节重点分析第2,3步;

3、服务器端是如何接收客户端的健康检测请求的?

跟以前的接收流程非常相似,只不过健康检测用的是双端流,而不是一元流;

我们以帧接收器作为入口分析;

进入grpc-go/internal/transport/http2_server.go文件中的HandleStreams方法里:

1func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) {
2defer close(t.readerDone)
3for {
4.		t.controlBuf.throttle()
5.		frame, err := t.framer.fr.ReadFrame()
67//---省略不相关代码
8switch frame := frame.(type) {
9case *http2.MetaHeadersFrame:
10if t.operateHeaders(frame, handle, traceCtx) {
11.				t.Close()
12break
13}
14//---省略不相关代码

进入第10行,头帧处理器里:

1func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream), traceCtx func(context.Context, string) context.Context) (fatal bool) {
2//---省略不相关代码
3if err := state.decodeHeader(frame); err != nil {
4//---省略不相关代码
5}
6//---省略不相关代码
7handle(s)

}

通过第3行的decodeHeader方法,可以获取到客户端请求的服务名称,如/grpc.health.v1.Health/Watch

handle是由参数传递进来的函数,即如下:
在grpc-go/server.go文件中的serveStreams(st transport.ServerTransport)

1func (s *Server) serveStreams(st transport.ServerTransport) {
2defer st.Close()
3var wg sync.WaitGroup

4.	st.HandleStreams(func(stream *transport.Stream) {
5.		wg.Add(1)
6if s.opts.numServerWorkers > 0 {
7.			data := &serverWorkerData{st: st, wg: &wg, stream: stream}
8select {
9case s.serverWorkerChannels[atomic.AddUint32(&roundRobinCounter, 1)%s.opts.numServerWorkers] <- data:
10default:
11go func() {
12.					s.handleStream(st, stream, s.traceInfo(st, stream))
13.					wg.Done()
14}()
15}
16} else {
17go func(){
18defer wg.Done()
19.				s.handleStream(st, stream, s.traceInfo(st, stream))
20}()
21}
22},
23//---省略不相关代码
24)
25.	wg.Wait()
26}

其中,handle,就是第4-22行传进去的匿名函数

假设,执行的是第19行:
进入grpc-go/server.go文件中的handleStream方法里:

1func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) {

2//---省略不相关代码
3.    service := sm[:pos]
4. 	method := sm[pos+1:]

5.  	srv, knownService := s.m[service]
6if knownService {
7if md, ok := srv.md[method]; ok {
8.	    		s.processUnaryRPC(t, stream, srv, md, trInfo)
9return
10}
11if sd, ok := srv.sd[method]; ok {
12.			s.processStreamingRPC(t, stream, srv, sd, trInfo)
13return
14}
15}
16//---省略不相关代码
17}

主要流程说明:

  • 第3行:service就是grpc.health.v1.Health
  • 第4行:method就是watch
  • 第6-14行:由于watch使用的是流描述StreamDesc,因此,执行的是第12行:以前我们介绍的测试用例,全部是一元,因此为走第8行;

其中,第11行的src.sd就是:(grpc-go/health/grpc_health_v1/health_grpc.pb.go接口文件):

1var _Health_serviceDesc = grpc.ServiceDesc{
2.	ServiceName: "grpc.health.v1.Health",
3.	HandlerType: (*HealthServer)(nil),
4.	Methods: []grpc.MethodDesc{
5{
6.			MethodName: "Check",
7.			Handler:    _Health_Check_Handler,
8},
9},
10.	Streams: []grpc.StreamDesc{
11{
12.			StreamName:    "Watch",
13.			Handler:       _Health_Watch_Handler,
14.			ServerStreams: true,
15},
16},
17.	Metadata: "grpc/health/v1/health.proto",
18}

src.sd就是第11-15行;

进入grpc-go/server.go文件中的processStreamingRPC方法里:

1func (s *Server) processStreamingRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, sd *StreamDesc, trInfo *traceInfo) (err error) {
2//---省略不相关代码
3if s.opts.streamInt == nil {
4.		appErr = sd.Handler(server, ss)
5} else {
6.		info := &StreamServerInfo{
7.			FullMethod:     stream.Method(),
8.			IsClientStream: sd.ClientStreams,
9.			IsServerStream: sd.ServerStreams,
10}
11.		appErr = s.opts.streamInt(server, ss, info, sd.Handler)
12}
13//---省略不相关代码

假设在服务器端没有设置拦截器,因此直接进入第4行,handle就是_Health_Watch_Handler,

进入grpc-go/health/grpc_health_v1/health_grpc.pb.go接口文件_Health_Watch_Handler函数里:

1func _Health_Watch_Handler(srv interface{}, stream grpc.ServerStream) error {
2.	   m := new(HealthCheckRequest)
3if err := stream.RecvMsg(m); err != nil {
4return err
5}

6return srv.(HealthServer).Watch(m, &healthWatchServer{stream})
7}

主要流程说明:

  • 第3行:从流获取消息,赋值给m;
    • 获取的消息,就是客户端要进行健康检测服务的名称;
    • 或者说,客户端想知道服务器端某个服务的当前健康状态,如SayHello服务的健康状态,此时m就是SayHello
  • 第7行:将srv强制转换为HealthServer,然后调用Watch健康检测方法

此时,我们已经知道服务器端是如何一步一步的执行到健康检测Watch服务的;接下来,详细的分析一下,Watch的原理。

4、服务器端健康检测的原理?

在grpc框架中health包中的sever.go文件中的Server结构体实现了HeatlthServer接口;

进入grpc-go/health/server.go文件中的Watch方法里:

1func (s *Server) Watch(in *healthpb.HealthCheckRequest, stream healthgrpc.Health_WatchServer) error {
2.	service := in.Service
3// update channel is used for getting service status updates.
4.	update := make(chan healthpb.HealthCheckResponse_ServingStatus, 1)
5.	s.mu.Lock()
6// Puts the initial status to the channel.
7if servingStatus, ok := s.statusMap[service]; ok {
8.		update <- servingStatus
9} else {
10.		update <- healthpb.HealthCheckResponse_SERVICE_UNKNOWN
11}

12// Registers the update channel to the correct place in the updates map.
13if _, ok := s.updates[service]; !ok {
14.		s.updates[service] = make(map[healthgrpc.Health_WatchServer]chan healthpb.HealthCheckResponse_ServingStatus)
15}
16.	s.updates[service][stream] = update
17defer func() {
18.		s.mu.Lock()
19delete(s.updates[service], stream)
20.		s.mu.Unlock()
21}()
22.	s.mu.Unlock()

23var lastSentStatus healthpb.HealthCheckResponse_ServingStatus = -1
24for {
25select {
26// Status updated. Sends the up-to-date status to the client.
27case servingStatus := <-update:
28if lastSentStatus == servingStatus {
29continue
30}
31.			lastSentStatus = servingStatus
32.			err := stream.Send(&healthpb.HealthCheckResponse{Status: servingStatus})
33if err != nil {
34return status.Error(codes.Canceled, "Stream has ended.")
35}
36// Context done. Removes the update channel from the updates map.
37case <-stream.Context().Done():
38return status.Error(codes.Canceled, "Stream has ended.")
39}
40}
41}

主要流程说明:

  • 第2行:service,就是客户端要获取服务的状态的名称,如SayHello, 服务器端需要反馈一下SayHello服务的状态
  • 第7-11行:从statusMap缓存里获取SayHello服务的状态
    • 若存在的话,就将状态servingStatus存储到通道update里
    • 若没有的话,就将状态SERVICE_UNKNOWN赋值给通道update里;说明服务器端不存在客户端要监测的服务
  • 第13-16行:判断服务SayHello是否在upates里,
    • 若存在,就初始化
  • 第16行:将最新的状态update缓存到updates里
  • 第20-40行:死循环式的监听服务的状态,并作出相应的处理
    • 第27行:当通道update里有数据时,需要赋值给servingStatus,
    • 第28-35行:判断服务SayHello的最新状态servingStatus是否跟上一个状态保持一致,根据结果做出相应的处理
      • 第29行:若一致的话,就继续监听SayHello的状态,即又从执行第24行开始执行
      • 第31-35行:若不一致的话,就将SayHello方法的最新状态赋值给lastSentStatus,即保留了当前状态
      • 并将SayHello服务的最新状态通过流发送给客户端,客户端根据SayHello服务的当前状态,做出相应的处理

当服务器端运行时发现某个服务不能正常对外提供服务,需要更新statusMap缓存里服务的状态:

  • 如果状态变化是发生在客户端向服务器端发起健康检测请求前的话,那么,当客户端向服务器端发起健康检测请求时,就会从watch方法的第2行,开始执行,即从头开始执行watch。
  • 如果状态变化是发生在客户端向服务器端发起健康检测请求后的话,那么,服务器端就会从第27行,开始执行,获取到最新的状态,发现状态更新,然后执行第32行,向服务器端直接发送该服务的最新状态。
    • 也就是说,在客户端跟服务器端传输数据阶段,健康检测的流是一直存在的话,只要服务器端的服务状态发生变化,就会向客户端发送该服务的最新状态,客户端根据状态作出相应调整;
      • 如将有问题的链接从Picker中移除,
      • 或者说将刚准备好的链接添加到Picker里;

5、健康检测运行时常见的异常场景处理?

在健康检测服务启动的场景下,可能会存在以下异常场景:

假设有两个服务器端提供SayHello服务,进程都正常启动:

场景一:一个SayHello服务不能对外提供服务,另外一个Sayhello服务可以对外提供服务,那么客户端是如何处理的?
  • 客户端跟服务器端底层已经建立起链接了,但是客户端的状态还没有变
  • 针对不能提供服务的服务器端,客户端会将链接状态更新为TransientFailure,但是链接不会断的;当该连接的状态重新更新为Ready时,还可以继续创建流,传输数据。
  • 针对可以对外提供服务的服务器端,客户端会将链接状态更新为Ready,生成Picker,即将此链接缓存到平衡器里,并且将链接状态更新为Ready,接下来,就可以创建流,传输数据了。

也就是说,当服务器端,只能有一个服务可以对外提供服务时,那么客户端会将其他链接的状态更新为TransientFailure,将可以提供服务的链接状态更新为Ready, 最后链接的状态也会Ready;

当通过平衡器选择连接时,只能选择连接状态为Ready的链接,进行流的创建,以及传输数据;

可见,利用健康检测服务,可以在真正传输数据前,将存在问题的服务器剔除掉,选择正常的服务器进行传输数据

场景二:异常情况发生在传输数据阶段,当客户端选择了一条Ready状态的链接,在传输数据阶段,突然接收到服务器的通知,该服务已经不能对外提供服务了,客户端如何处理?
  • 当客户端接收到服务器的通知后,会将该链接状态更新为TransientFailure,重新生成Picker,将该连接从缓存里移除;
  • 客户端支持数据发送重试机制,会重新发送数据;重新发送数据时,会重新从平衡器里选择状态为Ready的链接,进行重新传输数据

下一篇文章
  拦截器介绍

你可能感兴趣的:(golang,grpc,docker,kubernetes,微服务架构)