vlambda博客
学习文章列表

简单理解gRPC调用过程

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

本篇内容:

  • 理解存根

  • 是否每次都要new一个client

  • 调用失败后的重试

  • 如何keep-alive

  • gRPC的一元模式和流模式

一、理解存根

1.存根和骨架

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

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

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

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

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();//共享channelUserGrpc.newBlockingStub(chan);//共享channelUserPersonGrpc.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是不安全的