简单理解gRPC调用过程

9/14/2022

gRPC的调用实在是太复杂了,很多配置,很多文档,如果细讲估计可以写一本书,所以先简单讲讲。

本篇内容:

  • 理解存根
  • 是否每次都要new一个client
  • 调用失败后的重试
  • 如何keep-alive
  • gRPC的一元模式和流模式

# 一、理解存根

# 1.存根和骨架

gRPC的调用过程,是客户端存根和服务端骨架的通信过程。

在客户端创建的是存根(stub),在服务端的是骨架(skeleton)。

这不只是 gRPC 中的概念,而是RPC中通用的概念。在java的RMI(远程方法调用)中也是一样的。

Concept Diagram

存根可以理解客户端持有的一个client,内含一个链接,但是多个存根可能共享一个连接

假设一个服务提供多个service,在客户端就需要创建多个client,但是由于server是同一个地址,那么只需要一个链接

go语言中,通过 dial 方法创建一个连接,后续的请求都可以使用这个client。如:

conn, err := grpc.Dial(*serverAddr, opts...)
defer conn.Close()
//共享连接
clientA := pb.NewUserClient(conn)
//共享连接
clientB := pb.NewPersonClient(conn)

在java中,只需要初始化一个 NettyChannel,后续的stub都共享这个channel

Channel chan = NettyChannelBuilder.forTarget(serverAddr)
                .negotiationType(NegotiationType.PLAINTEXT)
                .build();
//共享channel
UserGrpc.newBlockingStub(chan);
//共享channel
UserPersonGrpc.newBlockingStub(chan);

所以不用,而且也不应该每次都创建一个 client 或者 connection,存根本身就是线程安全的。

# 2.失败重试

gRPC作为一个 RPC 框架,框架自然要实现更多的逻辑,比如失败重试。

失败有多种情况:

  1. 连接失败
  2. rpc还没有发出去
  3. rpc发到了服务端,但是服务端还没有处理
  4. rpc 发到了服务端,但是服务端处理失败

其中前三种都是gRPC自动实现的,RPC设计了机制,能确定 “服务端是否开始了对client的调用的处理”。

第4种可以通过 ServiceConfig 来设置。

然而需要考虑是否需要自动重试

比如:调用失败时,有可能是服务端问题,重试会增加服务端的压力

而且重试需要服务端考虑复杂的幂等设计

客户端的重试是根据服务端的错误码来确定是否重试的,此时客户端的行为是自发的。

对于写操作的调用,如删除、创建等行为,要做好重放策略,会增加程序复杂度。

# 3.如何 keep-alive

gRPC是利用 HTTP2协议中的ping操作来检查当前的通道是否可用。

ping是周期性发送的,如果ping超时了一段时间,那么这个连接就被认为不可用了。

grpc定义了一些列参数,可用控制这个逻辑。

# 二、gRPC的模式

平时用的最多的模式,是一个请求对应一个响应的模式,称之为Unary,一元请求模式。

此外gRPC 还支持流模式,流模式有分为:

  • 服务端推送流
  • 客户端推送流
  • 双端互相推送流

# 并发安全的考虑

Unary 模式中的client是绝对并发安全的,可以并发地用在多个线程/GoRoutine中。

而stream模式中,需要避免在多个线程/GoRoutine中对同一个 stream 进行 sendMsg或RecvMsg。

换句话说,在1个stream上:

  • 一个GoRoutine调用SendMsg,而另一个goroutine同时调用RecvMsg是安全的。
  • 不同 GoRoutine 中同时调用SendMsg或者调用RecvMsg是不安全的