拷打AI-手撕Dubbo
从零手写一个简化版 Dubbo:一次 RPC 调用背后的完整链路
项目地址:https://gitee.com/sadboylushuowen/learn_dubbo
这段时间用AI做了一个 Java RPC 学习项目,目标、学习 Dubbo,而是按调用链一点点拆开:一次远程调用到底经历了什么,Dubbo 那些核心模块为什么要这么分层。
项目最后跑通了这样一条链路:
Consumer
-> @DubboReference
-> 动态代理
-> RpcInvocation
-> SPI 加载 LoadBalance
-> ClusterInvoker
-> RegistryDirectory
-> RegistryServer
-> SocketClient
-> SocketServer
-> LocalInvoker
-> Provider
-> RpcResult
换句话说,业务侧看起来只是调用一个本地接口:
demoService.sayHello("spring");
但底层已经经过了调用模型、序列化、网络通信、服务注册发现、动态代理、负载均衡、失败转移、SPI 扩展和注解集成。
这篇文章把整个过程复盘一遍,重点放在知识点和设计思路上。
1. RPC 最核心的问题
RPC 的目标很简单:
让 Consumer 像调用本地接口一样调用远程 Provider。
但真正实现时,会马上遇到几个问题:
调用哪个接口?
调用哪个方法?
参数是什么?
结果怎么返回?
对象怎么变成字节?
字节怎么通过网络传过去?
Consumer 怎么知道 Provider 地址?
多个 Provider 怎么选?
调用失败怎么办?
框架能力怎么扩展?
业务方怎么少写框架代码?
这个项目就是围绕这些问题一步一步展开。
2. 第一步:先把一次调用表达出来
最开始没有网络,也没有序列化,只做 RPC 调用模型。
一次远程调用至少要包含:
接口名
方法名
参数类型
参数值
请求 ID
附加参数
所以定义了 Invocation 和 RpcInvocation。
调用结果也要有统一表达:
返回值
异常
请求 ID
错误信息
所以定义了 Result 和 RpcResult。
接着用 Invoker 表示“一个可执行的服务”:
Invocation -> Invoker -> Result
这里的关键点是:RPC 不应该一开始就关心网络。先把“调用”这件事在 Java 里抽象清楚,后面网络层只是负责把这个调用对象传过去。
3. 第二步:对象变字节,字节变对象
远程调用不能直接传 Java 对象,最终传输的一定是字节。
所以抽出 Serialization 接口:
byte[] serialize(Object object);
<T> T deserialize(byte[] bytes, Class<T> type);
第一版用 JDK 序列化实现,也就是 ObjectOutputStream 和 ObjectInputStream。
这一步跑通后,请求和响应就可以这样转换:
RpcInvocation -> byte[] -> RpcInvocation
RpcResult -> byte[] -> RpcResult
这一阶段的重点不是 JDK 序列化有多好,而是明白:RPC 框架必须有统一的序列化抽象。以后换 JSON、Hessian、Kryo,本质上都是替换这个接口实现。
4. 第三步:Socket 网络通信和帧边界
有了字节之后,下一步才是网络。
第一版使用 JDK Socket 和 ServerSocket:
Consumer SocketClient 发送请求字节
Provider SocketServer 接收请求字节
Provider 执行本地 Invoker
Provider 返回响应字节
Consumer 反序列化得到 Result
这里一个很重要的细节是 SocketCodec。
TCP 是流协议,不天然知道一条消息从哪里开始、到哪里结束。如果直接读字节,可能出现半包、粘包问题。
所以用最简单的帧格式:
int length + byte[] body
先读 4 字节长度,再按长度读取完整 body。
这一步跑通后,最小远程调用链路就成立了:
Consumer -> Socket -> Provider -> Socket -> Consumer
5. 第四步:用 config 层封装过程
前三步跑通后,问题变成:业务方要写太多框架内部代码。
Provider 侧原来要手写:
LocalInvoker
RpcRequestHandler
SocketServer
server.start()
Consumer 侧原来要手写:
RpcInvocation
Serialization
SocketClient
deserialize Result
这些都不是业务代码。
所以新增 dubbo-config:
ServiceConfig
封装服务暴露
ReferenceConfig
封装服务引用
Provider 只需要表达:
serviceConfig.setInterfaceClass(DemoService.class);
serviceConfig.setRef(new DefaultDemoService());
serviceConfig.export();
Consumer 只需要表达:
referenceConfig.setInterfaceClass(DemoService.class);
这一步的核心思想是:
对外暴露意图
对内封装过程
这也是 Dubbo config 层存在的原因。
6. 动态代理:真正像调用本地接口
有了 ReferenceConfig 之后,Consumer 仍然需要手动写方法名和参数类型。
这还不像真正的 RPC。
所以引入 JDK 动态代理,让 Consumer 写成:
DemoService demoService = referenceConfig.get();
demoService.sayHello("shawn");
代理对象拦截接口方法调用,然后内部转换成:
Method -> RpcInvocation -> 远程调用 -> Result -> 返回值
动态代理解决的是“使用体验”问题。
业务方看到的是本地接口,框架看到的是一次标准化的远程调用。
7. 第五步:注册中心
前面是直连调用,Consumer 需要知道 Provider 地址。
这不适合真实场景。
所以新增 dubbo-registry:
Provider 启动时注册服务
Consumer 调用时按接口名查服务地址
第一版注册中心不是 ZooKeeper,也不是 Nacos,而是一个独立进程版的内存注册中心:
RegistryServer
持有内存 Map
Provider
register(interfaceName -> host:port)
Consumer
lookup(interfaceName)
这里有个容易踩的点:不能只用普通 static Map。
Provider 和 Consumer 是两个 JVM 进程,静态变量不共享。所以注册中心必须作为独立进程存在,Provider 和 Consumer 都通过 Socket 访问它。
这一步之后,Consumer 不再感知 Provider 地址。
它只知道:
我要调用 DemoService
地址由注册中心负责发现。
8. 第六步:多个 Provider 和负载均衡
真实 RPC 场景里,一个服务通常有多个 Provider:
DemoService
-> 127.0.0.1:20880
-> 127.0.0.1:20881
所以注册中心从单实例查询增强为:
lookupAll(interfaceName) -> List<ServiceInstance>
接着新增 dubbo-cluster:
Directory
负责拿到服务实例列表
LoadBalance
负责从多个实例里选一个
ClusterInvoker
负责串起 list -> select -> invoke
第一版负载均衡使用轮询:
provider-20880
provider-20881
provider-20880
provider-20881
这里开始进入 Dubbo 的服务治理思想。
注册发现解决的是:
有哪些 Provider?
负载均衡解决的是:
这次选哪个 Provider?
9. Failover:选中的 Provider 失败了怎么办
负载均衡只能解决“选谁”,不能解决“选中的挂了怎么办”。
所以继续增强 ClusterInvoker,实现最小 Failover:
调用当前 Provider
如果 Socket 异常,或者 Result.hasException()
从候选列表移除当前 Provider
重新选择下一个 Provider
如果成功就返回
如果全部失败再抛异常或返回失败 Result
这一步之后,调用链开始具备容错能力。
负载均衡和容错要分开理解:
LoadBalance:这次选谁
Failover:选的人失败了怎么办
Dubbo 的 cluster 能力,本质上就是围绕这两个问题继续扩展:路由、权重、失败转移、失败重试、失败广播、快速失败等。
10. SPI:让框架能力可插拔
前面实现了 RoundRobinLoadBalance,但如果代码里一直写:
new RoundRobinLoadBalance()
那框架能力还是写死的。
所以新增 dubbo-common 的 SPI 机制:
@SPI
@Extension
ExtensionLoader
META-INF/dubbo/{接口全名}
LoadBalance 被标记成扩展点:
@SPI("roundRobin")
public interface LoadBalance
RoundRobinLoadBalance 被标记成扩展实现:
@Extension("roundRobin")
public class RoundRobinLoadBalance implements LoadBalance
配置文件内容:
roundRobin=com.study.dubbo.cluster.loadbalance.RoundRobinLoadBalance
这样 ReferenceConfig 不再依赖具体实现类,而是通过扩展名加载:
ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension("roundRobin")
SPI 的价值是插件化。
以后新增随机负载均衡,不需要改主流程,只需要新增实现类和配置:
random=com.xxx.RandomLoadBalance
Dubbo 里很多核心能力都是类似思路:
Protocol
Registry
Serialization
LoadBalance
Cluster
Filter
ThreadPool
主链路只依赖接口,具体实现通过 SPI 插进去。
11. 最后一步:Spring 风格注解集成
框架内部能力都有了之后,最后要解决使用体验。
业务方不应该总是手写:
new ServiceConfig()
new ReferenceConfig()
所以新增 dubbo-spring:
@DubboService
@DubboReference
DubboAnnotationBootstrap
Provider 实现类可以写:
@DubboService(interfaceClass = DemoService.class, port = 20880)
public class DefaultDemoService implements DemoService
Consumer 可以写:
@DubboReference
private DemoService demoService;
第一版没有直接引入真实 Spring,而是用 DubboAnnotationBootstrap 模拟 Spring Bean 生命周期:
扫描对象上的 @DubboService
创建 ServiceConfig 并 export
扫描字段上的 @DubboReference
创建 ReferenceConfig 并 get 代理
反射注入字段
最终业务侧变成:
Provider 声明服务
Consumer 声明引用
框架负责中间过程
这一步和前面的 dubbo-config 思想是一致的:
对外暴露意图
对内封装过程
12. 最终模块关系
整个项目最后的分层大概是:
dubbo-demo
示例 API / Provider / Consumer
dubbo-spring
注解入口,隐藏 ServiceConfig / ReferenceConfig
dubbo-config
服务暴露和服务引用配置入口
dubbo-cluster
Directory / LoadBalance / ClusterInvoker / Failover
dubbo-registry
服务注册与服务发现
dubbo-remoting
Socket 通信、编解码、请求处理
dubbo-serialization
对象与字节转换
dubbo-rpc
Invocation / Result / Invoker 等核心调用模型
dubbo-common
SPI 扩展机制
依赖关系不是随便排的。
越底层越关注“基础能力”,越上层越关注“使用体验”。
13. 这个项目里最有价值的几个知识点
第一,RPC 的本质不是网络,而是调用模型。
网络只是传输手段。真正要先抽象的是:
Invocation
Result
Invoker
第二,TCP 传输必须处理帧边界。
int length + body 是最简单的协议格式,但足够说明问题。
第三,动态代理是 RPC 使用体验的关键。
没有代理,用户调用的是框架 API;有了代理,用户调用的是业务接口。
第四,注册中心解决的是地址发现,不是调用本身。
Provider 注册,Consumer 查询。注册中心只是让 Consumer 不再写死 Provider 地址。
第五,cluster 层解决的是服务治理。
多个 Provider 怎么选,失败后怎么办,都不应该放在 ReferenceConfig 里。
第六,SPI 解决的是扩展能力。
框架只依赖接口,具体实现通过扩展名加载。
第七,Spring 集成解决的是使用成本。
业务方只声明注解,框架把注解转换成服务暴露和服务引用。
14. 还可以继续补什么
这个项目已经能覆盖一个 RPC 框架的核心骨架,但离真实 Dubbo 还有很多工程化能力:
Netty 替代 JDK Socket
ZooKeeper / Nacos 注册中心
心跳和服务下线摘除
异步调用和 Future
请求超时控制
过滤器链 Filter
更多负载均衡策略
更多容错策略
真实 Spring BeanPostProcessor
@EnableDubbo 和包扫描
URL 配置模型
协议扩展
配置中心
监控和指标
但这些都属于在主链路之上的增强。
如果主链路没理解,直接看这些会很容易散。
15. 总结
这个项目最重要的收获不是“写了多少代码”,而是把 RPC 框架拆成了一条清晰的链:
本地接口调用
-> 动态代理
-> Invocation
-> 序列化
-> 网络传输
-> Provider 执行
-> Result
-> 注册发现
-> 负载均衡
-> 容错
-> SPI 扩展
-> 注解集成
从这个角度再去看 Dubbo 源码,会轻松很多。
因为你看到的不再是一堆类名,而是一条调用链上的不同角色:
谁表达调用?
谁负责传输?
谁负责发现地址?
谁负责选择 Provider?
谁负责失败重试?
谁负责加载扩展?
谁负责把框架封装成注解?
理解了这些问题,再看真正的 Dubbo,只是把学习版里的每个点做得更完整、更健壮、更工程化。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)