This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="加餐 | RPC框架代码实例详解" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/68/39b513329891576b8d47eaab85b37c68.mp3"></audio>
你好我是何小锋好久不见咱们专栏结课有段时间了这期间我和编辑冬青一起对整个课程做了复盘也认真挨个逐字看了结课问卷中的反馈其中呼声最高的是“想看RPC代码实例”今天我就带着你的期待来了。
还记得我在[[结束语]](https://time.geekbang.org/column/article/226573)提到过我在写这个专栏之前把公司内部我负责的RPC框架重新写了一遍。口说无凭现在这个RPC框架已经[开源](https://github.com/joyrpc/joyrpc),接受你的检阅。
下面我就针对这套代码做一个详细的解析,希望能帮你串联已学的知识点,实战演练,有所收获。
## RPC框架整体结构
首先说我们RPC框架的整体架构这里请你回想下[[第 07 讲]](https://time.geekbang.org/column/article/207137)在这一讲中我讲解了如何设计一个灵活的RPC框架其关键点就是插件化我们可以利用插件体系来提高RPC的扩展性使其成为一个微内核架构如下图所示
<img src="https://static001.geekbang.org/resource/image/a3/a6/a3688580dccd3053fac8c0178cef4ba6.jpg" alt="" title="插件化RPC">
这里我们可以看到我们将RPC框架大体分为了四层分别是入口层、集群层、协议层和传输层而这四层中分别包含了一系列的插件而在实际的RPC框架中插件会更多。在我所开源的RPC框架中就超过了50个插件其中涉及到的代码量也是相当大的下面我就通过**服务端启动流程、调用端启动流程、RPC调用流程**这三大流程来将RPC框架的核心模块以及核心类串联起来理解了这三大流程会对你阅读代码有非常大的帮助。
## 服务端启动流程
在讲解服务启动流程之前,我们先看下服务端启动的代码示例,如下:
```
public static void main(String[] args) throws Exception {
DemoService demoService = new DemoServiceImpl(); //服务提供者设置
ProviderConfig&lt;DemoService&gt; providerConfig = new ProviderConfig&lt;&gt;();
providerConfig.setServerConfig(new ServerConfig());
providerConfig.setInterfaceClazz(DemoService.class.getName());
providerConfig.setRef(demoService);
providerConfig.setAlias(&quot;joyrpc-demo&quot;);
providerConfig.setRegistry(new RegistryConfig(&quot;broadcast&quot;));
providerConfig.exportAndOpen().whenComplete((v, t) -&gt; {
if (t != null) {
logger.error(t.getMessage(), t);
System.exit(1);
}
});
System.in.read();
}
```
我们可以看出providerConfig是通过调用exportAndOpen()方法来启动服务端的,那么为何这个方法要如此命名呢?
我们可以看下 exportAndOpen 方法的代码实现:
```
public CompletableFuture&lt;Void&gt; exportAndOpen() {
CompletableFuture&lt;Void&gt; future = new CompletableFuture&lt;&gt;();
export().whenComplete((v, t) -&gt; {
if (t != null) {
future.completeExceptionally(t);
} else {
Futures.chain(open(), future);
}
});
return future;
}
```
这里服务的启动流程被分为了两个部分export创建Export对象以及open打开服务。而服务端的启动流程也被分为了两部分服务端的创建流程与服务端的开启流程。
### 服务端创建流程
<img src="https://static001.geekbang.org/resource/image/f4/5b/f411bc3b3dbfbfaefe08671b25a5f65b.jpg" alt="" title="服务端创建流程图">
这里的ProviderConfig是服务端的配置对象其中接口、分组、注册中心配置等等的相关信息都在这个配置类中配置流程的入口是调用ProviderConfig的export方法整个流程如下
1. 根据ProviderConfig的配置信息生成registryUrl注册中心URL对象与serviceUrl服务URL对象
1. 根据registryUrl调用Registry插件创建Registry对象Registry对象为注册中心对象与注册中心进行交互
1. 调用Registry对象的open方法开启注册中心对象也就是与注册中心建立连接
1. 调用Registry对象的subscribe方法订阅接口的配置信息与全局配置信息
1. 调用InvokerManager创建Exporter对象
1. InvokerManager返回Exporter对象。
服务端的创建流程实际上就是Exporter对象Exporter对象是调用器Invoker接口的子类Invoker接口有两个子类分别是Exporter与ReferExporter用来处理服务端接收的请求而Refer用来向服务端发送请求这两个类可以说是入口层最为核心的两个类。
在InvokerManager创建Exporter对象时实际上会有一系列的操作而初始化Exporter也会有一系列的操作如创建Filter链、创建认证信息等等。这里不再详细叙述你可以阅读下源码。
### 服务端开启流程
<img src="https://static001.geekbang.org/resource/image/70/cf/706ce659565164d457efc040c93976cf.jpg" alt="" title="服务端开启流程图">
创建完服务端的Exporter对象之后我们就要开启Exporter对象开启Exporter对象最重要的两个操作就是开启传输层中Server的端口用来接收调用端发送过来的请求以及将服务端节点注册到注册中心上让调用端可以发现到这个服务节点整个流程如下
1. 调用Exporter对象的open方法开启服务端
1. Exporter对象调用接口预热插件进行接口预热
1. Exporter对象调用传输层中的EndpointFactroy插件创建一个Server对象一个Server对象就代表一个端口了
1. 调用Server对象的open方法开启端口端口开启之后服务端就可以提供远程服务了
1. Exporter对象调用Registry对象的register方法将这个调用端节点注册到注册中心中。
这里无论是Exporter的open方法、Server的open还是Registry的register方法都是异步方法返回值为CompletableFuture对象这个流程的每个环节也都是异步的。
Server的open操作实际上是一个比较复杂的操作要绑定协议适配器、初始化session管理器、添加eventbus事件监听等等的操作而且整个流程完全异步并且是插件化的。
## 调用端启动流程
在讲解调用端启动流程之前,我们还是先看下代码示例,调用端启动代码示例如下:
```
public static void main(String[] args) {
ConsumerConfig&lt;DemoService&gt; consumerConfig = new ConsumerConfig&lt;&gt;(); //consumer设置
consumerConfig.setInterfaceClazz(DemoService.class.getName());
consumerConfig.setAlias(&quot;joyrpc-demo&quot;);
consumerConfig.setRegistry(new RegistryConfig(&quot;broadcast&quot;));
try {
CompletableFuture&lt;DemoService&gt; future = consumerConfig.refer();
DemoService service = future.get();
String echo = service.sayHello(&quot;hello&quot;); //发起服务调用
logger.info(&quot;Get msg: {} &quot;, echo);
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
System.in.read();
}
```
调用端流程的启动入口就是ConsumerConfig对象的refer方法ConsumerConfig对象就是调用端的配置对象这里可以看到refer方法的返回值是CompletableFuture与服务端相同调用端的启动流程也完全是异步的下面我们来看下调用端的启动流程。
<img src="https://static001.geekbang.org/resource/image/01/f3/01ab5a2288cfe1d0aadbe7a38da8c9f3.jpg" alt="" title="调用端启动流程">
调用端具体流程如下:
1. 根据ConsumerConfig的配置信息生成registryUrl注册中心URL对象与serviceUrl服务URL对象
1. 根据registryUrl调用Registry插件创建Registry对象Registry对象为注册中心对象与注册中心进行交互
1. 创建动态代理对象;
1. 调用Registry对象的Open方法开启注册中心对象
1. 调用Registry对象subscribe方法订阅接口的配置信息与全局配置信息
1. 调用InvokeManager的refer方法用来创建Refer对象
1. InvokeManager在创建Refer对象之前会先创建Cluster对象Cluser对象是集群层的核心对象Cluster会维护该调用端与服务端节点的连接状态
1. InvokeManager创建Refer对象
1. Refer对象初始化其中主要包括创建路由策略、消息分发策略、创建负载均衡、调用链、添加eventbus事件监听等等
1. ConsumerConfig调用Refer的open方法开启调用端
1. Refer对象调用Cluster对象的open方法开启集群
1. Cluster对象调用Registry对象的subcribe方法订阅服务端节点变化收到服务端节点变化时Cluster会调用传输层EndpointFactroy插件创建Client对象与这些服务节点建立连接Cluster会维护这些连接
1. ConsumerConfig调用Refer对象封装到ConsumerInvokerHandler中将ConsumerInvokerHandler对象注入给动态代理对象。
在调用端的开启流程中最复杂的操作就是Cluster对象的open操作以及Client对象的open操作。
Cluster对象是集群层的核心对象也是这个RPC框架中处理逻辑最为复杂的对象Cluster对象负责维护该调用端节点集群信息监听注册中心推送的服务节点更新事件调用传输层中的EndpointFactroy插件创建Client对象并且会通过Client与服务端节点建立连接发送协商信息、安全验证信息、心跳信息通过心跳机制维护与服务节点的连接状态。
Client对象的open操作也是有着一系列的操作比如创建Transport对象创建Channel对象生成并记录session信息等等。
Refer对象在构造调用链的时候其最后一个调用链就是Refer对象的distribute方法用来发送远程请求。
动态代理对象内部的核心逻辑就是调用ConsumerInvokerHandler对象的Invoke方法最终就是调用Refer对象我会在下面的RPC调用流程中详细讲下。
## RPC调用流程
讲解完了服务端的启动流程与调用端的启动流程下面我开始讲解RPC的调用流程。RPC的整个调用流程就是调用端发送请求消息以及服务端接收请求消息并处理之后响应给调用端的流程。
下面我就讲解下调用端的发送流程与服务端的接收流程。
### 调用端发送流程
<img src="https://static001.geekbang.org/resource/image/f8/aa/f875674fd3959193202abb38fdb956aa.jpg" alt="" title="调用端发送流程">
调用端发送流程如下:
1. 动态代理对象调用ConsumerInvokerHandler对象的Invoke方法
1. ConsumerInvokerHandler对象生成请求消息对象
1. ConsumerInvokerHandler对象调用Refer对象的Invoke方法
1. Refer对象对请求消息对象进行处理如设置接口信息、分组信息等等
1. Refer对象调用消息透传插件处理透传信息其中就包括隐式参数信息
1. Refer对象调用FilterChain对象的Invoker方法执行调用链
1. FilterChain对象调用每个Filter
1. Refer对象的distribute方法作为最后一个Filter被调用链最后一个执行。
1. 调用NodeSelecter对象的select方法NodeSelecter是集群层的路由规则节点选择器其select方法用来选择出符合路由规则的服务节点
1. 调用Route对象的route方法Route对象为路由分发器也是集群层中的对象默认为路由分发策略为Failover即请求失败后可以重试请求这里你可以回顾下[[第 12 讲]](https://time.geekbang.org/column/article/211261)在这一讲的思考题中我就问过异常重试发送在RPC调用中的哪个环节其实就在此环节
1. Route对象调用LoadBalance对象的select方法通过负载均衡选择一个节点
1. Route对象回调Refer对象的invokeRemote方法
1. Refer对象的invokeRemote方法调用传输层中Client对象向服务端节点发送消息。
在调用端发送流程中最终会通过传输层将消息发送给服务端这里对传输层的操作没有详细的讲解其实传输层内部的流程还是比较复杂的也会有一系列的操作比如创建Future对象、调用FutureManager管理Future对象、请求消息协议转换处理、编解码、超时处理等等的操作。
当调用端发送完请求消息之后,服务端就会接收到请求消息并对请求消息进行处理。接下来我们看服务端的接收流程。
### 服务端接收流程
<img src="https://static001.geekbang.org/resource/image/65/cb/65b25a2b06f6223e19adaddd992542cb.jpg" alt="" title="服务端接收流程">
服务端的传输层会接收到请求消息并对请求消息进行编解码以及反序列化之后调用Exporter对象的invoke方法具体流程如下
1. 传输层接收到请求触发协议适配器ProtocolAdapter
1. ProtocolAdapter对象遍历Protocol插件的实现类匹配协议
1. 匹配协议之后根据Protocol对象传输层的Server对象绑定该协议的编解码器Codec对象、Channel处理链ChainChannelHandler对象
1. 对接收的消息进行解码与反序列化;
1. 执行Channel处理链
1. 在业务线程池中调用消息处理链MessageHandle插件
1. 调用BizReqHandle对象的handle方法处理请求消息
1. BizReqHandle对象调用restore方法根据连接Session信息处理请求消息数据并根据请求的接口名、分组名与方法名获取Exporter对象
1. 调用Exporter对象的invoke方法Exporter对象返回CompletableFuture对象
1. Exporter对象调用FilterChain的invoke方法
1. FilterChain执行所有Filter对象
1. Exporter对象的invokeMethod方法作为最后一个Filter最后被调用
1. Exporter对象的invokeMethod方法处理请求上下文执行反射
1. Exporter对象将执行反射之后得到的请求结果异步通知给BizReqHandle对象
1. BizReqHandle调用传输层的Channel对象发送响应结果
1. 传输层对响应消息进行协议转换、序列化、编码,最后通过网络传输响应给调用端。
## 总结
今天我们剖析了一款开源的RPC框架的代码主要通过**服务端启动流程、调用端启动流程、RPC调用流程**这三大流程来将RPC框架的核心模块以及核心类串联起来。
在服务端的启动流程中核心工作就是创建和开启Exporter对象。ProviderConfig在创建Exporter对象之前会先创建Registry对象从注册中心中订阅接口配置与全局配置之后才会创建Exporter对象在Exporter开启时会启动一个Server对象来开启一个端口Exporter开启成功之后才会通过Registry对象向注册中心发起注册。
在调用端的启动流程中核心工作就是创建和开启Refer对象开启Refer对象中处理逻辑最为复杂的就是对Cluster的open操作Cluster负责了调用端的集群管理操作其中有注册中心服务节点变更事件的监听、与服务端节点建立连接以及服务端节点连接状态的管理等等。
调用端向服务端发起调用时会先经过动态代理之后会调用Refer对象的invoke方法Refer对象会先对要透传的消息进行处理再执行Filter链调用端最后一个Filter会根据配置的路由规则选择出符合条件的一组服务端节点之后调用Route对象的route方法route方法的内部逻辑会根据配置的负载均衡策略选择一个服务端节点最后向这个服务端节点发送请求消息。
服务端的传输层收到调用端发送过来的请求消息在对请求消息进行一系列处理之后如解码、反序列化、协议转换等等会在业务线程池中处理消息关键的逻辑就是调用Exporter对象的invoke方法Exporter对象的invoke方法会执行服务端配置的Filter链最终通过反射或预编译对象执行业务逻辑再将最终结果封装成响应消息通过传输层响应给调用端。
本讲在调用端向服务端发起调用时没有讲到异步调用实际上Refer对象的invoke方法的实现逻辑完全是异步的同样Exporter对象的invoke方法也是异步的Refer类与Exporter类都是调用端Invoker接口的实现类可以看下Invoker接口中invoke方法的定义:
```
/**
* 调用
*
* @param request 请求
* @return
*/
CompletableFuture&lt;Result&gt; invoke(RequestMessage&lt;Invocation&gt; request);
```
JoyRPC框架是一个纯异步的RPC框架所谓的同步只不过是对异步进行了等待。
入口层的核心对象就是Exporter对象与Refer对象这两个类承担了入口层的大多数核心逻辑。
集群层的核心对象就是Cluster对象与Registry对象Cluser对象的内部逻辑还是非常复杂的核心逻辑就是与Registry交互订阅服务端节点变更事件以及对与服务端节点建立的连接的管理这里我们对Cluser对象没有进行过多介绍你可以去查看代码。
协议层的核心对象就是Protocol接口的各个子类了。
接下来就是传输层了传输层的具体实现我们在本讲也没有过多介绍因为很难通过有限的内容把它讲解完整还是建议你去查看下源码一目了然。传输层是纯异步的并且是完全插件化的其入口就是EndpointFactroy插件通过EndpointFactroy插件获取一个EndpointFactroy对象EndpointFactroy对象是一个工厂类用来创建Client对象与Server对象。
对于一个完善的RPC框架今天我们仅是针对服务端启动流程、调用端启动流程、RPC调用流程这三个主流程做了一个大致的讲解真正实现起来还是要复杂许多因为涉及到了很多细节上的问题但主要脉络出来以后相信也会对你有很大帮助更多的细节就还是要靠你自己去阅读源码啦
今天的加餐分享就到这里,有任何问题,欢迎你在留言区与我交流!

View File

@@ -0,0 +1,69 @@
<audio id="audio" title="加餐 | 谈谈我所经历过的RPC" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/ca/4ee1974795f50a7833f34058f8e0cbca.mp3"></audio>
你好我是何小锋。上一讲我们学习了如何在线上环境里兼容多种RPC协议目的就是为了能够平滑地升级线上环境中已经存在的RPC框架同时我们也可以利用多协议的特点来支持不同的使用场景。
以上就是我们整个专栏关于技术内容的最后一讲了很幸运能够和你一起携手并肩走过这些日子这段时间我们跨了新年也经历了让人猝不及防的新冠肺炎。现在我才发现原来能够自由地呼吸新鲜空气也是一种幸福祝你平平安安度过这段艰难的日子期待我们能早日摘下口罩。为了感谢你这些日子的陪伴今天我们来换换脑子聊一些轻松的话题。我就和你分享分享我所经历过的RPC框架吧。
## 与RPC结缘
我1998从大学毕业的时候互联网并不是像今天这样如火如荼地进行着。那时候大部分IT公司都在耕耘数字化办公相关领域所以大学毕业的时候我也就随了大流进入了一个从事办公软件开发的公司也就是我们今天经常说的“传统软件行业”。
后来随着互联网的快速普及,各个行业都想借着这个机会跟互联网发生点关系,因为大家都知道互联网代表着未来,在咱国内借助互联网技术发展最快的行业代表就是电商和游戏。
得益于互联网的普及各个互联网公司有关技术的文章也是扑面而来比如A公司因为流量激增而导致整站长时间瘫痪B公司因为订单持续上涨而导致数据库压力太大等等。每当我看到类似技术文章的时候我都在想自己什么时候也可以遇到能用技术优化的方式来解决的问题呢。后来发现这些“设想”在我当时所在的行业里很难体会到所以2011年我毅然选择了加入京东。
那时候我们公司刚好处于一个业务高速发展期,但系统稳定性相比今天来讲的话,还处于一个比较稚嫩的阶段。在日常工作中,我们的研发人员不仅要面对来自业务方需求进度的压力,还要面对因为线上业务增长而导致的系统压力。这样的双重压力对我来说很新鲜,这种体验是我之前从未有过的,这让我感到很兴奋。
之前我自己也做了很长一段时间的业务系统开发虽然积累了不少实用的编程技巧也认真阅读过国内外很多优秀的开源代码包括像Spring、Netty等等这样优秀的框架但因为从来没有经历过这么多系统之间复杂调用的场景所以在入职后的一段时间里我的内心一直处于忐忑状态。
得幸于同事的热情帮助我很快地就融入了团队氛围。因为我们大团队是负责整个公司基础架构的可以简单理解成我们就是负责公司PaaS平台的建设既然是P,aaS平台那肯定少不了中间件而中间件里面最核心的应该就是RPC了因为RPC相对其它中间件来说使用频率是最高的也是我们构建分布式系统的基石。
从此我就踏上了RPC这条“不归路”了当然并不是因为这是条绝路而是这条路一直充满着挑战并没有让我觉得有停下来的想法。
## 使用过的RPC
因为我工作年限比较长所以编程经历相对也比较丰富在转Java之前我做过一段时间的.Net。早期因为主要做数字化办公软件所以那时候用.Net编程的机会比较多主要是因为很多应用采用.Net相对使用Java来说开发效率会高很多。但后面应用复杂之后.Net在性能、可维护性上都不如Java有优势而且.Net在很长一段时间内并不支持在Linux环境下部署所以我们就把应用都逐渐改成Java了。
### ICE
这就存在一个问题,我们是怎么把.Net应用平滑地切换到Java上呢原本是.Net应用之间的互相调用需要改成.Net跟Java之间互相通信。为了解决这个问题我们当时选择了一个比较古老的RPC框架ICE[https://zeroc.com](https://zeroc.com)),可能现在很多人都没有听说过了。
### Hessian
后面使用Java开发应用的机会越来越多而且随着Spring开发方式的大流行在Java应用里面使用ICE来完成应用之间的RPC调用就变得比较鸡肋了。所以当时我们用了一种新的RPC框架Hessian[http://hessian.caucho.com/](http://hessian.caucho.com/)这时我们就把Java应用之间的RPC调用方式改成了Hessian方式。
用Hessian的原因就是因为它可以很好地跟Spring进行集成对Spring项目的开发人员来说开发一个RPC接口就变得很容易了我们可以直接把Java类对外进行暴露用作RPC接口的定义而不需要像ICE一样先定义Stub。
单纯的从RPC框架角度出发Hessian是一款很优秀的产品即使放到今天它的性能和鲁棒性都有着很强的参考意义。但当业务发展壮大到一定程度后应用之间的调用就不仅仅需要考虑用什么RPC框架了更多的是需要考虑怎么去完成服务治理。
另外一个原因就是因为Hessian是没有服务发现功能的我们只能通过VIP暴露的方式完成调用我们需要给每个应用分配一个VIP把同一接口的所有服务提供方实例挂载到同一个VIP上。这种集中式流量转发架构就会使得提供VIP服务的LVS存在很大的压力而且集中式流量的转发会让调用方响应时间相对变长。
### Dubbo
为了解决类似Hessian这种集中式问题实现大规模应用服务化的落地国内的RPC框架Dubbo的做法就显得比较先进了。随着业务越来越复杂应用之间的调用关系也就变得更加错综复杂所以后面我们也是选择基于Dubbo进行扩展以完成RPC通信而服务发现则通过接入ZooKeeper集群来完成。通过这种现有框架的搭配我们完成了应用服务化的快速落地也同时完成了统一公司内部所有应用RPC框架的目标。
但随着微服务理念越来越流行很多应用的接口也是越拆越细导致我们ZooKeeper集群需要接入的接口数量越来越多还有就是因为我们每年的业务量是成倍增长为了让应用能够抗足够的调用量应用我们也需要经常扩容从而导致ZooKeeper集群接入的IP实例数也是呈数量级增长的这使得我们的ZooKeeper集群负荷特别重。
再有就是Dubbo相对有点复杂而且性能还有提高空间这使得我们不得不考虑新的方案。
### 自研RPC
在这种背景下我们决定自行研发一套适合自己业务场景的微服务解决方案包括RPC框架、服务治理以及多语言解决方案。
至此我们自研的RPC就一直平稳地支持着公司内的各种业务。
这几年在以Kubernetes为代表的基础设施演进过程中一个重要的关键词就是应用基础设施能力的下沉。在过去我们给应用提供RPC能力的时候都是需要应用引入Jar包方式来解决的在RPC里面我们要把服务发现、路由等一整套RPC解决方案都融入到这个Jar里面去。
### 未来
目前Kubernetes已成为基础设施的事实标准。而原先通过Jar包的方式封装的各种基础设施能力现在全都被Kubernetes项目从应用层拽到了基础设施中。那对于我们RPC来说也是一样我们需要把非业务功能从传统的RPC框架中剥离出来下沉到基础设施并且融入基础设施然后通过Mesh去连接应用和基础设施。
这也是RPC发展的下一个阶段完成所有应用的Mesh化。所以说RPC这条路没有尽头只有不断的挑战和乐趣。希望你也能爱上它
**那最后我还想和你谈谈我对RPC的看法。**
可能大家在谈论RPC时候都想着RPC只是解决应用之间调用的工具。从本质上来讲这没有什么问题但在现实中我们需要RPC解决更多的实际问题比如服务治理这些东西都是在使用RPC的过程中需要考虑的问题所以我个人认为RPC应该是一个比较泛的概念。
当然可能我们中大多数人现在是没有机会去完整实现一个新的RPC的这不仅是精力的问题更多是实际需求的问题那为什么我们还需要学好RPC呢我的想法很简单也非常实在就是因为RPC是我们构建分布式系统的基石就好比我们每次都是从“Hello World”开始学习一门新的编程语言。期待你能打牢这个基础总有一天你会体验到它的能量
今天的特别放送就到这里也非常期待听到你和RPC的故事。