Dubbo 如何实现像本地方法一样调用远程方法的?

#中间件#面试八股

Dubbo 如何实现像本地方法一样调用远程方法的?

结论先行:靠三层“伪装”——接口代理(Proxy)把方法调用截获成 Invocation 对象,Invoker 抽象把“本地调用”和“远程调用”统一成同一个 invoke 方法,动态代理(JDK 或 Javassist)让调用方拿到的对象长得和本地实现一模一样。序列化与网络收发被完全隐藏在代理之后。

一、伪装的三层

业务代码            userService.getById(1L)
                    │  看起来是本地方法调用
                    ▼
动态代理 Proxy      InvokerInvocationHandler.invoke()
                    │  方法名 + 参数类型 + 实参 → RpcInvocation
                    ▼
Invoker 抽象        invoker.invoke(invocation)
                    ├── 本地暴露:JvmInvoker,直接反射调用实现类
                    └── 远程暴露:DubboInvoker,序列化后走网络
                    ▼
网络层              Netty 发送 → Provider 解码 → 反射执行 → 响应

关键在于:Invoker 是统一抽象。同一份业务代码,注入的是本地引用就本地执行(同一 JVM 内走 injvm 协议,不经过网络),注入的是远程引用就走网络,调用方无感。

二、代理是怎么生成的

方式生成物特点
JDK 动态代理实现同接口的 $Proxy 类必须基于接口,反射调用,启动快
Javassist 字节码直接生成实现类少一层反射,调用更快,默认方式
// 框架内部等价逻辑:为接口生成代理
Invoker<UserService> invoker = proxyFactory.getProxy(invoker); // 传入真实 invoker
UserService userService = (UserService) invoker.getInterface()
        .cast(proxy);   // 业务侧拿到的就是这个对象

业务侧写法只有一行:

@DubboReference(version = "1.0.0", check = false, timeout = 1000)
private UserService userService;

public void demo() {
    User user = userService.getById(1L);   // 与本地方法写法完全一致
}

三、透明化必须补齐的四件事

问题处理方式
异常怎么抛Provider 侧异常序列化回传,消费端还原为原异常类型;框架异常(超时、无提供者)单独包装
超时怎么算调用前记录时间戳,Future.get(timeout) 超时抛 RpcException
重试怎么做cluster 层按 retries 重试其他 invoker,幂等方法才可开启
上下文怎么传RpcContext 的 attachment 随请求头透传,如 traceId、租户 ID
// 隐式传参:不改变方法签名也能把上下文带到提供者
RpcContext.getClientAttachment().setAttachment("traceId", "abc-123");
userService.getById(1L);

四、同步语义下线程在做什么

“像本地方法”意味着调用线程必须原地等结果,等待期间线程是阻塞的:

阶段消费端线程状态说明
发起调用RUNNABLE组装 Invocation、序列化、写 socket
等待响应WAITING挂在 DefaultFuture.get() 上,由响应唤醒
收到响应RUNNABLEIO 线程完成 future.complete(),等待线程被唤醒
超时RUNNABLE定时任务让 Future 异常完成,抛出 RpcException

因此高并发下同步 RPC 的线程数约等于“QPS × 平均耗时”,这也是异步调用存在的理由。

常见追问

  • 追问:透明调用的代价是什么?网络不可靠性被隐藏了,开发者容易忘记超时、重试、幂等、序列化兼容这些分布式问题,所以“像本地一样”只是语法层面。
  • 追问:为什么默认用 Javassist 而不是 JDK 代理?Javassist 生成的是真实方法调用,避免反射开销;但接口变更时需要重新生成,JDK 代理更省事,可通过 proxy=jdk 切换。
  • 追问:injvm 协议是什么?同一 JVM 内优先走本地调用,跳过序列化与网络,只在本地没有暴露时才走远程。

小结

  • 代理截获调用,Invocation 承载方法语义,Invoker 抹平本地与远程差异。
  • 异常、超时、重试、上下文透传是“透明”必须补齐的四块拼图。
  • injvm 优先是 Dubbo 的默认优化,本地有实现就不走网络。
  • 语法透明不等于语义透明,分布式约束仍需业务自己兜住。
笔记加载中…