架构设计:设计一个灵活的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 框架
可扩展的架构 - 插件化架构
将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离,并提供接口的默认实现。 在 Java 里面,JDK 有自带的 SPI(Service Provider Interface)服务发现机制 ,它可以动态地为某个接口寻找服务实现。 使用 SPI 机制需要在 Classpath 下的 META-INF/services 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体实现类。
但在实际项目中,其实很少使用到 JDK 自带的 SPI 机制,
- 首先它
不能按需加载 ,ServiceLoader 加载某个接口实现类的时候,会遍历全部获取,也就是接口的实现类得全部载入并实例化一遍,会造成不必要的浪费。 - 另外就是扩展如果依赖其它的扩展,那就做不到自动注入和装配,这就很难和其他框架集成,比如扩展里面依赖了一个 Spring Bean,原生的 Java SPI 就不支持。
加上插件功能后, 包含核心功能体系与插件体系的RPC 框架:
这时,整个架构就变成了一个微内核架构,将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离并提供接口的默认实现。
这样的架构相比之前的架构,有很多优势:
- 首先它的可扩展性很好,实现了
开闭原则 ,用户可以非常方便地通过插件扩展实现自己的功能,而且不需要修改核心功能的本身; - 其次就是保持了核心包的精简,
依赖外部包少 ,这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。
总结
软件功能 + 系统的可拓展性
服务发现:到底是要CP还是AP
服务发现原理图
- 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
- 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。
为什么不使用 DNS?
DNS查询流程:
如果用 DNS 来实现服务发现,所有的服务提供者节点都配置在了同一个域名下,调用方的确可以通过 DNS 拿到随机的一个服务提供者的 IP,并与之建立长连接 存在问题:
- 如果这个 IP 端口下线了,服务调用者能否及时摘除服务节点呢?-- 不能
- 如果在之前已经上线了一部分服务节点,这时突然对这个服务进行扩容,那么新上线的服务节点能否及时接收到流量呢?-- 不能
为了提升性能和减少 DNS 服务的压力,DNS 采取了多级缓存机制 ,一般配置的缓存时间较长,特别是 JVM 的默认缓存是永久有效的,所以说服务调用者不能及时感知到服务节点的变化。
加一个负载均衡设备, 将域名绑定到这台负载均衡设备上,通过 DNS 拿到负载均衡的 IP。 这样服务调用的时候,服务调用方就可以直接跟 VIP 建立连接,然后由 VIP 机器完成 TCP 转发,如下图所示: 这个方案确实能解决 DNS 遇到的一些问题,但在 RPC 场景里面也并不是很合适,原因有以下几点:
- 搭建负载均衡设备或 TCP/IP 四层代理,需求额外成本;
- 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能;
- 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;
- 在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。
结论: DNS 或者 VIP 方案虽然可以充当服务发现的角色,但在 RPC 场景里面直接用还是很难的
|