【夜读源码】grpc-java 客户端发送 (一)

Search for a command to run...

No comments yet. Be the first to comment.
this is a chinese tranlation from Google Best Practice For Java Libraries Google Java库最佳实践 Google Java库的最佳实践是一些规则,旨在减少使用相关的Java库时的问题。这些实践来自于多年来维护开源Java库的经验,并且受到了从错误中吸取的许多宝贵教训的启发。我们发现遵循这些规则可以得到质量更高的Java库,减少依赖冲突和其他类型的问题。这个规则列表是开放的,所以可能会不时地添加新的规则。 最佳实践...
本书的整体脉络如下: class diagram 表示对象的类型以及其间存在的静态关系。用一个长方形表示类,用连线表示类之间的关系。 具体类图 长方形 对象的类型,也就是类有三部分表示而成 类名 (class name) 属性 (attribute) 操作 (operation) 属性 (attribute) 其中抽象来说,类的成员变量 可以有两种表示, 属性表示 和 关联表示。 属性表示:就是画在框里, 一般写 包括 可见行, 类型,名称 ,也可以写默认值 和 注释。 也可以标注是否是 ...
gRPC 是 Google 开源的一套语言无关基于HTTP2协议的 RPC 框架,其中 Java 的实现称为 grpc-java,代码地址在 https://github.com/grpc/grpc-java。 RPC (remote process call)的含义是程序执行本地的一个方法(看上去像本地执行),实际上会在远程程序代码内执行,这个过程会涉及到网络通讯和传输协议。gRPC作为 RPC 一种实现,传输的网络协议是HTTP2,传输的内容按照 Protocol Buffer 进行编码,并...

mockito 是最常用的 java mock 库, 用于模拟创建各种对象和行为。本文会介绍一下 基础库 mockitio的用法,进行一些练习。mockitio 通常不单独使用,配合 Junit一起使用。 下面列举一下用法, 模拟对象 创建mock 对象,并且可以要求,这个对象的成员方法符合我们的行为要求。 Foo foo= Mockito.mock(Foo.class); 直接返回 直接返回,我们需要的值 when thenReturn @Test void returnWh...

接上文,一次 grpc 的请求包括四个步骤,客户端发送请求到网络,是第一步。发送过程,Stub 把请求传给了ClientCall, ClientCall 传给了 ClientStream,ClientStream 有Netty 的实现,内部会把数据传输到网络上。
本文主要介绍 blockingUnaryCall 是如何使用 ClientCall ClientChannel 工作的, waitAndDrain 和 ClientCall 的方法如何对应。
最外层的Stub,echo方法是生成的代码,里面只有一行,使用静态方法ClientCalls.blockingUnaryCall
blockingUnaryCall 的实现,
ListenableFuture 存放 response无论这里时序如何,还是会执行 PendingCall 发消息,最后通过观察者模式,把结果取回来。
小结:Pendingcall 发送客户端消息
ClientCall 有四个主要的方法,start,request,sendMessage, halfClose
Channel 相当于 ClientCall 的工厂,下面具体到类型进行分析。
PendingCall 的实现如下图

PendingCall 是一个 DelayClientCall,通过realChannel 创建了 ClientCallImpl 作为成员变量,因此 PendingCall 的 最终会执行 ClientCallImpl 的对应方法。
小结: PendingCall 代理了 ClientCallImpl,PendingCall 借助 ManagedChannelImpl 的RealChannel 创建realCall
Channel 那一块,由一下代码创建出 ManagedChannelImpl
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext().build();
ManagedChannelImpl 里面包含 ManagedChannelImpl.RealChannel 和上图串起来了。
https://cdn.hashnode.com/res/hashnode/image/upload/v1662278195441/NnO2uNjhW.png
小结: 静态方法通过SPI 创建了ManagedChannelImpl
在 blockingUnaryCall 里 channel.newCall 因此是 ManagedChannelOrphanWrapper 创建了 PendingCall, ManagedChannelOrphanWrapper 委托给 ManageChannelImpl , ManageChannelImpl 委托给 interceptorChannel , interceptorChannel 的创建方式是
ClientInterceptors.intercept(channel, interceptors), interceptorChannel 就是包了realchannel,
ManagedChannelImpl 的是实现中,
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions) {
return interceptorChannel.newCall(method, callOptions);
}
** 小结: ManagedChannelImpl 使用带拦截的RealChannel 创建的 PendingCall
大概思考一下,如果底层是Channel,Channel通常要进行网络连接,这个会放在另外一个线程里。 createChannel 之后有一个netty 线程进行连,等到ready 之后才把pending 的任务都执行一下。
接口上,这里主要是包了一个reprocess 方法, 可以和 InternalConfigSelector 进行配合。(todo) reprocess 的作用就是 给 PendingCall 初始化 ClientCallImpl
PendingCall 的大部分方法还是 DelayClientCall,那么主要看 DelayClientCall 的作用
A call that queues requests before a real call is ready to be delegated to.
再 realCall 准备完成之前,把请求放到队列里。
- List<Runnable> pendingRunnables
- boolean passThrough
请求指的是 Runnable 的任务, 当任务执行完成之后,标志位 passThrough 为 true;再执行 DelayClientCall, 就会执行 ClientCallImpl;
通过断点可以看到, Runnable 任务有四个,分别是执行ClientCallImpl 的 start,request,sendMessage, halfClose。
因此,PendingCall 执行方法的时候时候,不会真的执行,而且把方法放入list 中,等到万事具备,执行 DelayClientCall 的 setCall 方法返回一个Runable的 drainPendingCalls,可以依次执行这些任务。
TODO passThrough 什么时候 为true ?
Stub 中会有线程池,ClientCall 和 channel 也会用到线程池,下面梳理一下线程池,用于后面分析这些pending任务的时序。下面发现有两个
ManageChannelImpl 里有有 executor ,默认值是 executorPool get出来的。
executorPool 相当于一个ExecutorFactory, 默认值在GrpcUtil里有一个static 成员变量
return Executors.newCachedThreadPool(getThreadFactory(grpc-default-executor + "-%d", true));
这样,就可以用在ManageChannelImpl 一些任务的执行。
任务1
new Runnable() {
@Override
public void run() {
exitIdleMode();
}
}
另外netty 有两个线程 , ThreadPerTaskExecutor 这个是,NettyChannelBuilder 先弄了一个 executorPool,再从 executorPool 里创建出来的。 executorPool 的默认值是 SharedResourcePool.forResource(Utils.*DEFAULT_WORKER_EVENT_LOOP_GROUP*);
名称是 grpc-default-worker-ELG, 这个是
因此 DelayedClientCall 的 pendings 任务还是 main 主线程执行的,用了一个无线程的线程池.
目前已经了解到Channel 和 ClientCall 的创建和相互关系,也了解到线程池和时序。
那么futureUnaryCall 这个异步 和 drain 的过程是怎么实现的呢?
ListenableFuture<RespT> responseFuture = futureUnaryCall(call, req);
相当于 创建了四个 Runnable的任务, 这四个任务,pendingCall 执行了四个方法,但是最终的ClientCallImpl 并没有执行这四个方法,而且变成 Runnable 放入List 中。
executor.waitAndDrain();
executor 的 drain 方法,才会执行这四个方法
getUnchecked(responseFuture);
ThreadlessExecutor 怎么工作ListenableFuture 怎么接住结果的execute 方法把runnable 放进队列里, waiter 在RPC 结束后会变成SHUTDOWN 常量
execute 就是一个 unpark通知,那waiter 是进入park 状态, waiter 醒来之后会干什么?
@Override
public void execute(Runnable runnable) {
add(runnable);
Object waiter = this.waiter;
if (waiter != SHUTDOWN) {
LockSupport.unpark((Thread) waiter); // no-op if null
} else if (remove(runnable) && rejectRunnableOnExecutor) {
throw new RejectedExecutionException();
}
}
看 waitAndDrain 方法
1 先队列取任务 2.如果没有任务,waiter = Thread.currentThread(); 并且继续取任务,进入park 状态,循环等待,取到了任务,就把waiter 设置为空
如果一直没有任务,就会hang住,除非中断,也没有new thread;
用法应该先
或者
小结: ThreadlessExecutor 的作用是使用unpark 协调两个线程的任务,形成生产消费的模式
execute方法 是放任务并唤醒,waiterAndDrain方法是等待,一次性取完,
虽然waiterAndDrain 是一次性执行完,但 responseFuture 只要不是Done, 就会一直执行下去。
根据断点,执行了以下五个任务,分别由不同的线程生产
任务1. penndingCall的reprocess 里的组合任务 DelayedPendingCall.drainPendingCalls 和 PendingCallRemoval (grpc-default-executor-0 生产 main 执行) ???
任务2. DelayedPendingCall里的drainPendingCalls 的最后一步 DrainListenerRunnable 任务 (main 生产 main 执行) 貌似是main 执行上一个任务的最后产生的,就是让listener也清空一下call
任务3. DelayedClientTransport.reprocess 里 stream.createRealStream(transport); (grpc-nio-worker-ELG-1-2)
任务4. ClientCallImpl$ClientStreamListenerImpl 的 headersRead 的任务 (grpc-nio-worker-ELG-1-2)
任务5 ClientCallImpl$ClientStreamListenerImpl 的 MessagesAvailable 的任务 (grpc-nio-worker-ELG-1-2)
3,4,5 都是传输时候的,一个建stream连接,两个读消息, 读完了消息需要 drain 一下任务,返回给listener,
可以看到这些任务里并没有 clientCall 的四个任务, 这四个任务是在任务1里执行。
放入时机: Main 线程 ListenableFuture<RespT> responseFuture = futureUnaryCall(call, req); Stub 直接使用 DelayedClientCall 实例依次 执行了 四个方法,放入了四个任务
取出时机: Main 线程 Stub 在 waitAndDrain 的时候,执行了任务1, DelayedPendingCall.drainPendingCalls , DelayedPendingCall.drainPendingCalls 会把四个方法都执行.
串起来起来,就是运行的时候,
创建Channel 套娃, Channel 套娃创建 ClientCall套娃 外层,PendingCall 在 reprocess 的时候, 使用realChannel 创建clientCallImpl , clientCallImpl 有四个方法,发起客户端的请求.
时序上大体:
1和2 是同时执行的,但是3 和 1 同一段代码里,顺序已经保证了, 3 会等2, 因此 ,3 最后执行就行。
断点日志如下:
trace 线程 main 添加任务 realCall.start()
trace 线程 grpc-default-executor-0 添加任务 drainingPendingCall
trace 线程 main 添加任务 realCall.send()
trace 线程 main 执行任务 drainingPendingCall
因此,PendingCall有四个方法要执行,主线程准备好任务,grpc 线程创建 创建clientCallImpl 之后,通知主线程执行 drainPendingCalls。 借助于 ThreadlessExecutor 完成了 IPC
// TODO 画一个时序图
MORE:
整理清楚了,其实还挺简单的。
更深入, Stub 层: 为什么需要 grpc-default-executor 来准备任务,grpc-default 应该是有一些准备底层的工作,把 clientCallImpl 设置好之后,比如联通了,才让调用开始执行。