Posted on ::

1. 前言

在微服务架构盛行的今天,RPC(远程过程调用)框架是系统间通信的基石。虽然 Dubbo、gRPC 等成熟框架珠玉在前,但为了深入理解分布式系统的核心原理,我决定参考 Dubbo 的设计理念,基于 Vert.xEtcd 自主研发一套轻量级、可扩展、高性能的 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 Number1 Byte魔数,用于快速校验是否为本框架数据包
Version1 Byte协议版本号
Serializer1 Byte序列化算法标识 (1-JSON, 2-Kryo, 3-Hessian)
Type1 Byte消息类型 (请求/响应/心跳)
Status1 Byte状态码
Request ID8 Bytes请求ID,用于异步请求的响应匹配
Body Length4 Bytes数据体长度
BodyVariable序列化后的请求数据

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)加载机制。

  • 实现原理
    1. 定义标准接口(如 Serializer)。
    2. 读取 META-INF/rpc/system 下的配置文件。
    3. 利用 双检锁单例模式 懒加载实例,并使用 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 的 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 提供了多种策略:

  1. 固定间隔重试:适用于短暂网络波动。
  2. 指数退避重试:防止请求风暴雪崩。
  3. Fail-Safe(失败安全):记录日志,不抛异常,适用于非关键业务。

5. 总结与展望

本项目通过整合 Vert.x 的高性能网络模型、Etcd 的强一致性协调能力以及 Java SPI 的插件化机制,实现了一个功能完备的 RPC 框架。

项目亮点总结:

  • 高性能:自定义 TCP 协议 + Vert.x Reactor 模型 + Kryo 序列化,最大化网络吞吐。
  • 高可用:Etcd 租约机制 + 自动/被动下线 + 负载均衡 + 容错重试。
  • 高扩展:全链路 SPI 机制,支持自定义序列化器、协议、负载均衡器。
  • 架构设计:熟练运用双检锁单例、装饰者、工厂、代理、观察者等多种设计模式。
Table of Contents