0 前言
提前先祝大家春节快乐!好了,先简单聊聊。
我从事的是大数据开发相关的工作,主要负责的是大数据计算这块的内容。最近 Hive 集群跑任务总是会出现 Thrift 连接 HS2 相关问题,研究了解了下内部原理,突然来了兴趣,就想着自己也实现一个 RPC 框架,这样可以让自己在设计与实现 RPC 框架过程中,也能从中了解和解决一些问题,进而让自己能够更好的发展(哈哈,会不会说我有些剑走偏锋?不去解决问题,居然研究 RPC。别急,这类问题已经解决了,后续我也会发文章详述的)。
1 RPC 流水线工程?
原理图上我已经标出来流程序号,我们来走一遍:
① Client 以本地调用的方式调用服务
② Client Stub 接收到调用后,把服务调用相关信息组装成需要网络传输的消息体,并找到服务地址(host:port),对消息进行编码后交给 Connector 进行发送
③ Connector 通过网络通道发送消息给 Acceptor
④ Acceptor 接收到消息后交给 Server Stub
⑤ Server Stub 对消息进行解码,并根据解码的结果通过反射调用本地服务
⑥ Server 执行本地服务并返回结果给 Server Stub
⑦ Server Stub 对返回结果组装打包并编码后交给 Acceptor 进行发送
⑧ Acceptor 通过网络通道发送消息给 Connector
⑨ Connector 接收到消息后交给 Client Stub,Client Stub 接收到消息并进行解码后转交给 Client
⑩ Client 获取到服务调用的最终结果
由此可见,主要需要 RPC 负责的是 2~9 这些步骤,也就是说,RPC 主要职责就是把这些步骤封装起来,对用户透明,让用户像调用本地服务一样去使用。
2 为 RPC 做个技术选型
序列化 / 反序列化首先排除 Java 的 ObjectInputStream 和 ObjectOutputStream,因为不仅需要保证需要序列化或反序列化的类实现 Serializable 接口,还要保证 JDK 版本一致,公司应用 So Many,使用的语言也众多,这显然是不可行的,考虑再三,决定采用 Objesess。
通信技术同样我们首先排除 Java 的原生 IO,因为进行消息读取的时候需要进行大量控制,如此晦涩难用,正好近段时间也一直在接触 Netty 相关技术,就不再纠结,直接命中 Netty。
高并发技术远程调用技术一定会是多线程的,只有这样才能满足多个并发的处理请求。这个可以采用 JDK 提供的 Executor。
服务注册与发现 Zookeeper。当 Server 启动后,自动注册服务信息(包括 host,port, 还有 nettyPort)到 ZK 中;当 Client 启动后,自动订阅获取需要远程调用的服务信息列表到本地缓存中。
负载均衡分布式系统都离不开负载均衡算法,好的负载均衡算法可以充分利用好不同服务器的计算资源,提高系统的并发量和运算能力。
非侵入式借助于 Spring 框架
RPC 架构图如下:
3 让 RPC 梦想成真
由架构图,我们知道 RPC 是 C / S 结构的。
3.1 先来一个单机版
单机版的话比较简单,不需要考虑负载均衡(也就没有 zookeeper),会简单很多,但是只能用于本地测试使用。而 RPC 整体的思想是:为客户端创建服务代理类,然后构建客户端和服务端的通信通道以便于传输数据,服务端的话,就需要在接收到数据后,通过反射机制调用本地服务获取结果,继续通过通信通道返回给客户端,直到客户端获取到数据,这就是一次完整的 RPC 调用。
3.1.1 创建服务代理
可以采用 JDK 原生的 Proxy.newProxyInstance 和 InvocationHandler 创建一个代理类。详细细节网上博客众多,就不展开介绍了。当然,也可以采用 CGLIB 字节码技术实现。
3.1.2 构建通信通道 & 消息的发送与接收
客户端通过 Socket 和服务端建立通信通道,保持连接。可以通过构建好的 Socket 获取 ObjectInputStream 和 ObjectOutputStream。但是有一点需要注意,如果 Client 端先获取 ObjectOutputStream,那么服务端只能先获取 ObjectInputStream,不然就会出现死锁一直无法通信的。
3.1.3 反射调用本地服务
服务端根据请求各项信息,获取 Method,在 Service 实例上反向调用该方法。
3.2 再来一个分布式版本
我们先从顶层架构来进行设计实现,也就是技术选型后的 RPC 架构图。主要涉及了借助于,Zookeeper 实现的服务注册于发现。
3.2.1 服务注册与发现
当 Server 端启动后,自动将当前 Server 所提供的所有带有 @ZnsService 注解的 Service Impl 注册到 Zookeeper 中,在 Zookeeper 中存储数据结构为 ip:httpPort:acceptorPort
当 Client 端启动后,根据扫描到的带有 @ZnsClient 注解的 Service Interface 从 Zookeeper 中拉去 Service 提供者信息并缓存到本地,同时在 Zookeeper 上添加这些服务的监听事件,一旦有节点发生变动(上线 / 下线),就会立即更新本地缓存。
3.2.2 服务调用的负载均衡
Client 拉取到服务信息列表后,每个 Service 服务都对应一个地址 list,所以针对连哪个 server 去调用服务,就需要设计一个负载均衡路由算法。当然,负载均衡算法的好坏,会关系到服务器计算资源、并发量和运算能力。不过,目前开发的 RPC 框架 zns 中只内置了 Random 算法,后续会继续补充完善。
3.2.3 网络通道
Acceptor
当 Server 端启动后,将同时启动一个 Acceptor 长连接线程,用于接收外部服务调用请求。内部包含了编解码以及反射调用本地服务机制。
Connector
当 Client 端发起一个远程服务调用时,ZnsRequestManager 将会启动一个 Connector 与 Acceptor 进行连接,同时会保存通道信息 ChannelHolder 到内部,直到请求完成,再进行通道信息销毁。
3.2.4 请求池管理
为了保证一定的请求并发,所以对服务调用请求进行了池化管理,这样可以等到消息返回再进行处理,不需要阻塞等待。
3.2.5 响应结果异步回调
当 Client 端接收到远程服务调用返回的结果时,直接通知请求池进行处理,No care anything!
4. 总结
本次纯属是在解决 Thrift 连接 HS2 问题时,突然来了兴趣,就构思了几天 RPC 大概架构设计情况,便开始每天晚上疯狂敲代码实现。我把这个 RPC 框架命名为 zns,现在已经完成了 1.0-SNAPSHOT 版本,可以正常使用了。在开发过程中,也遇到了一些平时忽略的小问题,还有些是工作工程中没有遇到或者遗漏的地方。因为是初期,所以会存在一些 bug,如果你感兴趣的话,欢迎提 PR 和 ISSUE,当然也欢迎把代码 clone 到本地研究学习。虽然就目前来看,想要做成一个真正稳定可投产使用的 RPC 框架还有短距离,但是我会坚持继续下去,毕竟 RPC 真的涉及到了很多点,只有真正开始做了,才能切身体会和感受到。Ya hoh! 终于成功实现了 v1.0,嘿嘿……
源码地址
zns 源码地址
zns 源码简单介绍:zns 由 zns-api, zns-common, zns-client, zns-server 四个核心模块组成。zns-service-api, zns-service-consumer, zns-service-provider 三个模块是对 zns 进行测试使用的案例。