想要学好微服务,RPC框架肯定是一个绕不开的话题。在我看来,任何框架都是为了后续开发者的便利,帮我们封装好可重复利用的工具或者某些固定流程,RPC框架也不例外。RPC框架最重要的作用就是帮我们将多个服务连接起来,即实现远程调用 ,让我们能像调用本地方法一样调用别的服务里的方法。
简而言之,RPC框架就是将复杂的网络通信、序列化等操作封装起来,这是最重要的。当然,一个完整的生产级RPC框架还融合了更多模块来保证其健壮性和高性能,比如服务注册与发现、重试机制、容错(熔断)机制、基于TCP的自定义协议、负载均衡等。这篇文章为了"浅出",我们先搭建一个简易RPC框架的骨架,讲讲各个核心模块的作用,暂不深入挖掘具体实现。
一、 网络通信
首先,我们想要调用另一个服务的方法,必然要通过网络进行通信。最常见的就是消费者发送一个请求,提供者处理后再返回结果。那么,这个请求里需要包含哪些信息,提供者才能准确地找到并执行对应的方法呢?
核心信息包括:
- 接口全限定名 :比如
com.example.UserService。告诉提供者"我要调用哪个服务接口"。 - 方法名 :比如
getUserById。告诉提供者"我要调用这个接口下的哪个方法"。 - 参数类型 :比如
(java.lang.Long)。方法可能会重载,只有方法名不够,还需要参数类型才能唯一确定一个方法。 - 参数值 :比如
123L。执行方法所需要的具体数据。 - 版本号(可选):如果接口有多个不兼容的版本,需要版本号来做区分。
通信协议的选择:
- HTTP:优点在于简单、通用,任何语言都支持。但对于高性能的RPC内部调用来说,HTTP协议的头部信息(Header)较大,效率不是最高。
- TCP + 自定义协议:高性能RPC框架(如Dubbo、gRPC)的常见选择。可以设计非常紧凑的二进制协议,减少不必要的网络开销,性能极高。这才是RPC框架网络层的精髓。
简单来说,网络通信模块就是RPC的远程调用的"核心",负责将请求从客户端运送到服务端,再把结果运送回来。
二、 代理对象
既然我们的目标是"像调用本地方法一样",那么我们在客户端代码里就不应该看到任何网络操作的痕迹(比如HttpClient)。如何实现这一点?答案就是:动态代理。
代理对象是RPC框架的"魔法师",它对外伪装成真正的服务接口。当你调用 userService.getUserById(123)时,你实际上调用的是代理对象的方法。
这个代理对象在背后默默地做了以下事情:
- 拦截调用:它拦截了你对所有接口方法的调用。
- 信息封装:它将你调用的方法名、参数类型、参数值等信息,按照和服务器约定好的格式封装起来。
- 委托通信模块 :它将封装好的数据交给网络通信模块发送给服务端。
- 等待并返回结果:它同步等待网络返回,拿到结果数据后,再原样返回给你。
这样,作为开发者的你,只需要关心接口的定义和调用,完全感知不到背后的网络请求。代理模式是实现RPC透明性的关键技术。
三、 注册中心
在简单的RPC调用中,服务提供者的IP和端口可以直接写在消费者的配置里。但在微服务架构中,服务实例众多且会动态变化(扩缩容、宕机),这种"硬编码"的方式就无法工作了。这时就需要注册中心。
注册中心就像是微服务世界的 "服务列表",存储了各种服务状态以及信息,让Consummer知道给哪个ip中的哪个端口发送消息。
- 服务注册 :当一个服务提供者启动时,它会将自己的服务名(如
UserService)和网络地址(如192.168.1.10:8080)注册到注册中心。 - 服务发现 :当一个服务消费者需要调用
UserService时,它不再需要硬编码地址,而是去注册中心查询所有可用的UserService实例的地址列表。 - 健康检查:注册中心会定期检查服务提供者的健康状态(心跳机制),如果发现某个实例宕机,就将其从地址列表中移除,确保消费者不会调用到已失效的节点。
常见的注册中心有:Nacos、Zookeeper、Consul、Eureka等。 注册中心的存在是实现服务治理和高可用的基石。

四、 优化与扩展
一个基础的RPC框架有了以上三个部分就可以运行了。但要成为一个成熟的生产级框架,还需要很多可优化和扩展的点。
1. 多种序列化方式
序列化是将对象转换为字节流的过程,反序列化则是其逆过程。不同的序列化协议在速度、体积、可读性、跨语言支持上各有优劣。提供多种选择(如JSON、Hessian、Protobuf、Kryo)可以让用户根据具体场景(如高性能、跨语言)进行权衡。
2. 多种负载均衡策略
当同一个服务有多个提供者时,消费者需要决定调用哪一个。这就是负载均衡。常见的策略有:
- 随机:从可用列表中随机选择一个。
- 轮询:依次调用每一个提供者。
- 最少活跃调用数:优先调用当前处理请求最少的服务器。
- 一致性哈希:相同参数的请求总是发到同一提供者,适用于需要缓存的场景。
3. 多种网络通信协议
除了支持基本的TCP自定义协议,还可以扩展支持HTTP/2、gRPC等协议,以适应不同的网络环境或生态集成。
4. SPI机制
SPI(Service Provider Interface)是一种服务发现机制。 它将接口的实现类配置在文件中,程序在运行时读取文件来加载具体的实现。不同于SpringBoot里将配置信息写在application中,可以实现某些配置信息与业务代码分离。SPI的作用是实现动态加载,其结合application就可以完美实现上述多种协议、多种序列化机制的动态选择。 利用SPI,框架的所有核心组件(如序列化、负载均衡、注册中心)都可以做成可插拔的,极大地提升了框架的扩展性。这是Dubbo等优秀框架设计的精髓。
5. 重试机制与容错机制
网络是不可靠的,调用可能会失败。框架需要提供容错策略。
-
重试机制:调用失败后自动重试,通常需要配合幂等设计。
-
容错策略:
- Failover(故障转移):失败后自动重试其他服务器。
- Failfast(快速失败):失败后立即报错,用于非幂等操作。
- Failsafe(安全失败):失败后忽略,仅记录日志。
- 熔断器机制:当故障达到一定阈值,自动"熔断",防止故障雪崩,并在一段时间后尝试恢复。
总结
让我们再串一下整个流程:消费者通过代理对象 调用接口方法 -> 代理对象将调用信息序列化 -> 通过网络通信 模块发送请求 -> 请求首先被发往注册中心 进行服务发现,获取真实的服务地址 -> 请求被发送到服务提供者 -> 提供者反序列化请求,通过反射执行本地方法 -> 将结果按原路返回。