1. 前言
在微服务架构盛行的今天,RPC(远程过程调用)框架是系统间通信的基石。虽然 Dubbo、gRPC 等成熟框架珠玉在前,但为了深入理解分布式系统的核心原理,我决定参考 Dubbo 的设计理念,基于 Vert.x 和 Etcd 自主研发一套轻量级、可扩展、高性能的 RPC 框架。
本框架的核心目标是高扩展性与高性能。通过自定义 TCP 协议解决网络传输瓶颈,利用 Java SPI 机制实现插件化架构,并借助 Etcd 实现强一致性的服务发现。
2. 核心架构设计
项目采用分层架构设计,各层之间松耦合,便于后续扩展。主要包含以下几个核心模块:
- 服务层:基于 Spring Boot Starter 实现注解驱动(
@RpcService,@RpcReference),对业务代码零侵入。 - 注册中心层:基于 Etcd 实现服务的注册、发现、心跳检测与动态感知。
- 代理层:封装网络传输细节,通过 JDK 动态代理实现像调用本地方法一样调用远程服务。
- 传输层:基于 Vert.x 实现高性能 TCP 服务器,解决半包粘包问题。
- 序列化层:支持多种序列化算法(Kryo, Hessian, JSON),通过 SPI 动态加载。
3. 关键技术剖析与实现
3.1 高性能网络通信:为什么选择 Vert.x?
在网络传输选型上,项目初期尝试过 HTTP 协议,但 HTTP 头部冗余严重,且无状态特性导致性能上限受限。最终,我选择了 自定义 TCP 协议 + Vert.x。
- Vert.x 的优势:不同于 Netty 的底层复杂性,Vert.x 提供了更高层次的 Reactor 模型抽象。它是全异步非阻塞的,利用 EventLoop 机制,单线程即可处理高并发连接,极大地减少了线程上下文切换的开销。
3.1.1 自定义协议设计
为了防止网络攻击并确保数据完整性,我设计了一套私有协议:
| 字段 | 长度 | 说明 |
|---|---|---|
| Magic Number | 1 Byte | 魔数,用于快速校验是否为本框架数据包 |
| Version | 1 Byte | 协议版本号 |
| Serializer | 1 Byte | 序列化算法标识 (1-JSON, 2-Kryo, 3-Hessian) |
| Type | 1 Byte | 消息类型 (请求/响应/心跳) |
| Status | 1 Byte | 状态码 |
| Request ID | 8 Bytes | 请求ID,用于异步请求的响应匹配 |
| Body Length | 4 Bytes | 数据体长度 |
| Body | Variable | 序列化后的请求数据 |
3.1.2 解决 TCP 半包与粘包
TCP 是流式协议,没有边界。我使用了 装饰者模式 对 Vert.x 的 RecordParser 进行了封装。
// 装饰者模式封装 RecordParser
public class TcpBufferHandlerWrapper implements Handler<Buffer> {
private final RecordParser recordParser;
public TcpBufferHandlerWrapper(Handler<Buffer> bufferHandler) {
// 初始化 Parser,先读取固定长度的头部信息
this.recordParser = RecordParser.newFixed(ProtocolConstant.HEADER_LENGTH);
this.recordParser.setOutput(new Handler<Buffer>() {
int size = -1;
Buffer resultBuffer = Buffer.buffer();
@Override
public void handle(Buffer buffer) {
if (-1 == size) {
// 读取头部,获取 body 长度
size = buffer.getInt(ProtocolConstant.BODY_LENGTH_OFFSET);
recordParser.fixedSizeMode(size); // 动态调整下一次读取长度
resultBuffer.appendBuffer(buffer);
} else {
// 读取 Body,组装完整包
resultBuffer.appendBuffer(buffer);
bufferHandler.handle(resultBuffer);
// 重置,准备读取下一个包
recordParser.fixedSizeMode(ProtocolConstant.HEADER_LENGTH);
size = -1;
resultBuffer = Buffer.buffer();
}
}
});
}
@Override
public void handle(Buffer buffer) {
recordParser.handle(buffer);
}
}
3.2 扩展性:SPI 机制
为了让框架支持替换组件(如更换序列化器或负载均衡策略),我实现了一套类似 Dubbo 的 SPI(Service Provider Interface)加载机制。
- 实现原理:
- 定义标准接口(如
Serializer)。 - 读取
META-INF/rpc/system下的配置文件。 - 利用 双检锁单例模式 懒加载实例,并使用
ConcurrentHashMap做缓存。
- 定义标准接口(如
public class SpiLoader {
// 缓存加载的类:Map<接口名, Map<别名, 实现类>>
private static final Map<String, Map<String, Class<?>>> loaderMap = new ConcurrentHashMap<>();
// 缓存实例:Map<实现类, 实例>
private static final Map<String, Object> instanceCache = new ConcurrentHashMap<>();
public static <T> T getInstance(Class<T> tClass, String key) {
// ... 省略非空校验
Class<?> implClass = loaderMap.get(tClass.getName()).get(key);
String implClassName = implClass.getName();
// 双检锁单例模式获取实例
if (!instanceCache.containsKey(implClassName)) {
synchronized (SpiLoader.class) {
if (!instanceCache.containsKey(implClassName)) {
instanceCache.put(implClassName, implClass.newInstance());
}
}
}
return (T) instanceCache.get(implClassName);
}
}
3.3 注册中心:Etcd 的深度应用
相比于 Eureka 的 AP 模型,Etcd 基于 Raft 算法保证了 CP(强一致性),更适合作为核心中间件的底座。
-
服务续期与下线:
- 利用 Etcd 的
Lease(租约) 机制实现心跳。服务提供者启动时绑定一个租约,并定期续约。 - 被动下线:一旦服务宕机,无法续约,TTL 过期后 Etcd 自动删除节点。
- 主动下线:通过 JVM
Runtime.getRuntime().addShutdownHook钩子函数,在系统关闭时主动清除注册信息。
- 利用 Etcd 的
-
消费端缓存与监听:
为了减少对 Etcd 的频繁访问,消费者端在本地缓存了服务列表。同时利用 Etcd 的 Watch 机制监听前缀,一旦服务上线或下线,实时更新本地缓存,实现了最终一致性。
3.4 负载均衡:一致性 Hash 算法
为了解决节点动态上下线导致的数据倾斜问题,除了传统的轮询和随机策略,我重点实现了一致性 Hash 负载均衡器。
-
核心逻辑:
使用 TreeMap 构建 Hash 环。为了避免节点过少导致的数据倾斜,引入了虚拟节点机制,将一个物理节点映射为多个虚拟节点散落在环上。
public class ConsistentHashLoadBalancer implements LoadBalancer {
private final TreeMap<Integer, ServiceMetaInfo> virtualNodes = new TreeMap<>();
private static final int VIRTUAL_NODE_NUM = 100;
@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceList) {
for (ServiceMetaInfo service : serviceList) {
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
int hash = getHash(service.getServiceAddress() + "#" + i);
virtualNodes.put(hash, service);
}
}
// 根据请求参数的 Hash 值选择节点
int requestHash = getHash(requestParams);
// ceilingEntry 返回大于等于 key 的最小键值对,实现顺时针查找
Map.Entry<Integer, ServiceMetaInfo> entry = virtualNodes.ceilingEntry(requestHash);
if (entry == null) {
entry = virtualNodes.firstEntry();
}
return entry.getValue();
}
}
3.5 动态代理与 Spring 整合
为了让用户无感知地调用远程服务,使用了 JDK 动态代理。
- 代理工厂:
ProxyFactory.getProxy(interfaceClass)。 - InvocationHandler:在
invoke方法中,封装 Request 对象(包含 method, args 等),执行负载均衡选择节点,发送 TCP 请求,并同步等待结果。
Spring Boot 整合:
编写自定义 Starter,通过 BeanPostProcessor 实现 Bean 的后置处理:
- 扫描带有
@RpcService的类,自动注册到 Etcd。 - 扫描带有
@RpcReference的字段,自动注入动态代理生成的桩对象。
4. 容错与重试机制
分布式系统中,网络抖动是常态。我集成了 Guava-Retrying 库,结合 SPI 提供了多种策略:
- 固定间隔重试:适用于短暂网络波动。
- 指数退避重试:防止请求风暴雪崩。
- Fail-Safe(失败安全):记录日志,不抛异常,适用于非关键业务。
5. 总结与展望
本项目通过整合 Vert.x 的高性能网络模型、Etcd 的强一致性协调能力以及 Java SPI 的插件化机制,实现了一个功能完备的 RPC 框架。
项目亮点总结:
- 高性能:自定义 TCP 协议 + Vert.x Reactor 模型 + Kryo 序列化,最大化网络吞吐。
- 高可用:Etcd 租约机制 + 自动/被动下线 + 负载均衡 + 容错重试。
- 高扩展:全链路 SPI 机制,支持自定义序列化器、协议、负载均衡器。
- 架构设计:熟练运用双检锁单例、装饰者、工厂、代理、观察者等多种设计模式。