自己动手写一个RPC框架

1,309 阅读6分钟

这是我参与8月更文挑战的第17天,活动详情查看: 8月更文挑战

应该很多人都尝试阅读过dubbo的源码,但是有时候读到一半就放弃了。只能说dubbo已经不是当年的dubbo了,进入Apache高速发展并且毕业,其功能的扩展也使得代码非常复杂。并且为了新特性(异步非阻塞、响应式、云原生等)孵化,也做了很多改动。当你阅读源码的时候,你会发现:

  1. 多个子模块,无从下手;
  2. 代码高度抽象,非常多的接口和继承;
  3. 使用很多的设计模式;

你可能还有其他问题,这些都会让你想要放弃。但是你其实要想明白的事是,dubbo作为一款分布式框架,不管它的功能再强大,它的主要功能还是一件事,那就是服务通信,而其他的都是锦上添花而已。为了完成这一个目标的代码就很少了,所以你可以先放下dubbo,尝试自己去实现一个RPC框架,不管你用什么技术,当你完成时你就会发现逻辑十分清晰。再回头去看dubbo,就明白它的每个模块的含义,不需要逐行阅读你也能够知道它是想做什么的,这就够了。我花了一段时间自己撸了一个简单版本的RPC框架,与大家分享一下。

概述

分布式服务框架主要包块服务消费端、服务提供端、服务数据网络传输序列化与反序列化、服务数据的通信机制、服务注册中心以及服务治理这几个部分。

RPC 架构.png

技术选型

  1. Spring:提供服务引入和发布的能力;
  2. Netty:提供高性能通信能力;
  3. Zookeeper:提供服务注册、服务发现等能力;
  4. 序列化相关(fastjson、protobuf、hessian、jackson);

序列化与反序列化

序列化是将对象的状态信息转换为可存储或传输的过程。反序列化则是序列化的逆向过程,将字节数组反序列化为对象,把字节序列回复为对象的过程称为对象的反序列化。

评价一个序列化算法优劣的两个重要指标:

  1. 序列化后的码流大小;
  2. 序列化本身的速度及系统资源开销大小。

各种常用的序列化的性能对比可以查看开源项目:jvm-serializers 。而在我的RPC框架中只实现了其中几个,如下:

Serializer.png

注册中心

分布式服务框架部署在多台不同的机器上,如下图所示:

服务集群.png

这将面临如下问题需要解决:

  • 集群A中的服务调用者如何发现集群B中的服务提供者。
  • 集群A中的服务调用者如何选择集群B中的某一台服务提供者机器发起调用。
  • 集群B中的服务某台提供者机器下线后,集群A中的服务调用者如何感知到这台机器的下线,不再对已下线的机器发起调用。
  • 集群B提供的某个服务如何获知集群A中的那些机器正在消费改服务。

以上问题都通过注册中心来解决。

服务的发布与引入

为了让RPC框架服务调用就像本地调用一样的目标,需要我们实现:

  • 再服务启动时,将服务提供者信息主动上报到服务注册中心进行服务注册。
  • 服务调用者启动的时候,将服务提供者信息从注册中心下拉到服务调用者机器本地缓存,服务调用者从本地缓存的服务提供者地址列表中,选择一台服务提供者发起调用。

因此我们可以基于Spring容器的特性来完成,提供了基于xml配置和注解配置两种方式。

底层通信

通信的本质是I/O。而Netty是著名的NIO开源框架,提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务端和客户端程序。

负载均衡策略

负载均衡的目的是将请求按照某种策略分布到多台机器上,使得系统能够实现横向扩展。分布式服务框架中实现负载均衡是通过软件算法来完成的。

在分布式服务框架中,负载均衡是服务端实现的,其实现原理如下:

  • 服务消费端在引用启动之初从服务注册中心获取服务提供者列表,缓存到服务调用端本地缓存。
  • 服务消费端发起服务调用之前,先通过某种策略或者算法从服务提供者列表本地缓存中选择本次调用的目标机器,再发起服务调用,从而完成负载均衡的功能。

服务治理

随着服务数增多,会产生很多问题:

  • 服务之间的依赖关系变得越来越复杂,靠人力很难梳理清楚整个链路之间的依赖关系。
  • 需要对每个服务本身的服务质量了如执掌才能保证整个链路服务的稳定性。
  • 沟通成本增加。
  • 当发现某个非关键服务出错率很高,对业务关键链路造成了影响,需要有一键降级的功能将服务从调用链路中摘除。
  • 对某个已有服务升级之后,需要在线上环境进行灰度发布或者AB测试。要求服务有自动分组能力,某个消费组的请求只打到对应的服务组上。
  • 服务部署集群中,可能某些机器配置更好,有更高的服务吞吐能力,能支持更高的QPS,要求可以对该机器配置更高的服务权重。
  • 当调用链路横跨多个服务、多个应用,对每一次调用需要有一个唯一标识将服务之间的调用串联起来,有助于排查问题。

服务治理是一个很大的主题,涵盖了非常多的内容。关于这部分,我没有提供具体的实现,只是提供了服务分组能力以及服务提供者和服务调用者获取接口。

小结

本代码地址为simple-rpc,不知道这样分化下来对你理解RPC框架是否有帮助,后面会对每块内容的目标和代码实现做详解,感谢阅读。