<RPC实战与核心原理>学习笔记 --- 进阶篇

架构设计:设计一个灵活的RPC框架

RPC 就是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证在服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的

RPC 架构设计

网络传输, 保证可靠性 --> TCP
①传输模块, 收发二进制数据, 屏蔽网络传输的复杂性

②协议模块
序列化过程: 用户请求基于方法调用,方法出入参数都是对象数据,对象在网络中传输需要转成二进制
协议封装: 在方法调用参数的二进制数据后面增加“断句”符号来分隔出不同的请求,在两个“断句”符号中间放的内容就是请求的二进制数据
压缩功能: 在方法调用参数或者返回值的二进制数据大于某个阈值的情况下,通过压缩框架进行无损压缩,然后在另外一端也用同样的压缩算法进行解压,保证数据可还原
目的: 保证数据在网络中可以正确传输

③Bootstrap 模块
屏蔽细节, 让研发人员感觉不到本地调用和远程调用的区别
if 用到Spring, 希望可以把一个 RPC 接口定义成一个 Spring Bean,并且这个 Bean 也会统一被 Spring Bean Factory 管理,可以在项目中通过 Spring 依赖注入到方式引用

以上组成单机版本的RPC, (Point to Point)版本的 RPC 框架, 没有集群能力
④集群模块
集群能力,就是针对同一个接口有着多个服务提供者,但这多个服务提供者对于调用方来说是透明的,所以在 RPC 里面还需要给调用方找到所有的服务提供方,并需要在 RPC 里面维护好接口跟服务提供者地址的关系,这样调用方在发起请求的时候才能快速地找到对应的接收地址,这就是“服务发现”。

但服务发现只是解决了接口和服务提供方地址映射关系的查找问题,这更多是一种“静态数据”。说它是静态数据是因为,对于 RPC 来说,每次发送请求的时候都是需要用 TCP 连接的,相对服务提供方 IP 地址,TCP 连接状态是瞬息万变的,所以 RPC 框架里面要有连接管理器去维护 TCP 连接的状态。

有了集群之后,提供方可能就需要管理好这些服务了,那 RPC 就需要内置一些服务治理的功能,比如服务提供方权重的设置、调用授权等一些常规治理手段。而服务调用方在每次调用前,都需要根据服务提供方设置的规则,从集群中选择可用的连接用于发送请求。

四层RPC 框架
<RPC实战与核心原理>学习笔记 --- 进阶篇

可扩展的架构 - 插件化架构

将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离,并提供接口的默认实现。
在 Java 里面,JDK 有自带的 SPI(Service Provider Interface)服务发现机制,它可以动态地为某个接口寻找服务实现。
使用 SPI 机制需要在 Classpath 下的 META-INF/services 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体实现类。

但在实际项目中,其实很少使用到 JDK 自带的 SPI 机制,

  1. 首先它不能按需加载,ServiceLoader 加载某个接口实现类的时候,会遍历全部获取,也就是接口的实现类得全部载入并实例化一遍,会造成不必要的浪费。
  2. 另外就是扩展如果依赖其它的扩展,那就做不到自动注入和装配,这就很难和其他框架集成,比如扩展里面依赖了一个 Spring Bean,原生的 Java SPI 就不支持。

加上插件功能后, 包含核心功能体系与插件体系的RPC 框架:
<RPC实战与核心原理>学习笔记 --- 进阶篇

这时,整个架构就变成了一个微内核架构,将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离并提供接口的默认实现。

这样的架构相比之前的架构,有很多优势:

  1. 首先它的可扩展性很好,实现了开闭原则,用户可以非常方便地通过插件扩展实现自己的功能,而且不需要修改核心功能的本身;
  2. 其次就是保持了核心包的精简,依赖外部包少,这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。

总结

软件功能 + 系统的可拓展性

服务发现:到底是要CP还是AP

服务发现原理图

<RPC实战与核心原理>学习笔记 --- 进阶篇

  1. 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
  2. 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。

为什么不使用 DNS?

DNS查询流程:
<RPC实战与核心原理>学习笔记 --- 进阶篇

如果用 DNS 来实现服务发现,所有的服务提供者节点都配置在了同一个域名下,调用方的确可以通过 DNS 拿到随机的一个服务提供者的 IP,并与之建立长连接
存在问题:

  1. 如果这个 IP 端口下线了,服务调用者能否及时摘除服务节点呢?-- 不能
  2. 如果在之前已经上线了一部分服务节点,这时突然对这个服务进行扩容,那么新上线的服务节点能否及时接收到流量呢?-- 不能

为了提升性能和减少 DNS 服务的压力,DNS 采取了多级缓存机制,一般配置的缓存时间较长,特别是 JVM 的默认缓存是永久有效的,所以说服务调用者不能及时感知到服务节点的变化。

加一个负载均衡设备, 将域名绑定到这台负载均衡设备上,通过 DNS 拿到负载均衡的 IP。
这样服务调用的时候,服务调用方就可以直接跟 VIP 建立连接,然后由 VIP 机器完成 TCP 转发,如下图所示:
<RPC实战与核心原理>学习笔记 --- 进阶篇
这个方案确实能解决 DNS 遇到的一些问题,但在 RPC 场景里面也并不是很合适,原因有以下几点:

  1. 搭建负载均衡设备或 TCP/IP 四层代理,需求额外成本;
  2. 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能;
  3. 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;
  4. 在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。

结论: DNS 或者 VIP 方案虽然可以充当服务发现的角色,但在 RPC 场景里面直接用还是很难的

上一篇:linux--目录结构(2)


下一篇:尝试解析加入域中域控制器的DNS名称失败的原因