简单理解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();
//共享channel
UserGrpc.newBlockingStub(chan);
//共享channel
UserPersonGrpc.newBlockingStub(chan);
所以不用,而且也不应该每次都创建一个 client 或者 connection,存根本身就是线程安全的。
2.失败重试
gRPC是一个 RPC 框架,框架自然要实现更多的逻辑,比如失败重试。
失败有多种情况:
连接失败
rpc还没有发出去
rpc发到了服务端,但是服务端还没有处理
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是不安全的