← blog 技术原理与实践 · 2026-10-19

从一次远程调用到 Dubbo 3:RPC 原理与注册中心选型串讲

自顶向下拆解 RPC 的完整链路与角色,讲透动态代理与反射如何撑起"远程调用像本地调用"的幻觉、ZooKeeper 为什么是 CP 而 Nacos 是 AP,最后落到 Dubbo 3 在代理、协议、序列化、服务发现各环节的设计取舍与平滑迁移策略。

30 min read

关于 RPC 的追问往往是一条链:RPC 的完整流程是什么?链路上有哪些角色?注册中心选 AP 还是 CP,为什么 ZooKeeper 是 CP、Nacos 是 AP?再往深一层,一个工业级框架(比如 Dubbo 3)在调用的每个环节分别做了什么选择,又为什么这么选?

这四个问题其实是同一条线索:从”一次调用如何发生”,到”框架如何在每个环节上做工程取舍”。这篇文章沿着这条线走一遍,前半程讲通用原理,后半程用 Dubbo 3 当作案例,看理论落地时每个选择背后的账是怎么算的。

一、RPC 的完整流程与角色

1.1 一次调用的完整链路

以 Dubbo、gRPC 这类框架为例,一次典型的 RPC 调用经历下面几步:

sequenceDiagram
    participant P as Provider
    participant R as 注册中心
    participant C as Consumer
    P->>R: 启动时注册服务地址
    C->>R: 订阅并缓存地址列表
    C->>P: 本地代理发起调用(序列化请求)
    P->>P: 反序列化后反射调用真实方法
    P-->>C: 返回值序列化回传
    C->>C: 代理反序列化,拿到结果

展开每一步:

  1. 服务注册:提供者启动时,把自己的地址(IP + 端口)和服务信息注册到注册中心。
  2. 服务发现:消费者启动时,从注册中心拉取(或订阅变更)提供者地址列表,缓存在本地。
  3. 本地调用:消费者调用的是一个本地接口代理(Stub / 动态代理),感觉不到网络的存在。
  4. 序列化:把方法名、参数类型、参数值打包成可传输的字节流(JSON、Protobuf、Hessian、Kryo 等)。
  5. 网络传输:字节流经协议(HTTP/2、TCP 自定义协议)发送,中间可能穿过负载均衡、熔断、重试等治理逻辑。
  6. 反序列化与反射调用:服务端解码出方法与参数,反射调用真实实现类。
  7. 结果返回:返回值同样经序列化、网络、反序列化,最终由消费者的代理交给业务代码。

RPC 的本质是让远程调用看起来像本地调用。代理 + 序列化 + 网络传输这三层,就是实现这个幻觉的全部秘密。

1.2 链路上的角色

角色职责
服务提供者(Provider / Server)实现业务逻辑,启动时注册服务,接收并处理请求
服务消费者(Consumer / Client)发起调用的一方,通过本地代理调用远程服务
注册中心(Registry)服务的电话簿:提供者注册、消费者发现、地址变更推送
代理 / Stub(Client Stub 与 Server Stub)屏蔽远程调用细节:客户端代理做序列化和网络发起,服务端 Stub 做解码和反射调用
序列化组件对象与字节流的互转(Protobuf、Hessian、Kryo、JSON 等)
网络传输层(Protocol)承载请求的通信协议,如 TCP 自定义协议、HTTP/2
监控与治理组件负载均衡、熔断限流、链路追踪、调用统计

前三个(Provider、Consumer、Registry)是最常说的三大角色,后面的组件都挂在它们之间。

二、反射调用:RPC 魔法的支点

2.1 为什么说反射是核心

RPC 要实现的幻觉是:调用者写的是本地接口调用,执行的却是远程方法。支撑这个幻觉的支点有两处,都靠反射:

  • 客户端:接口根本没有实现类,userService.getUser(1) 能编译通过、能被调用,是因为框架在运行时用动态代理生成了一个”假实现”,把调用拦截下来转成网络请求;
  • 服务端:收到的是一堆字节(接口名 + 方法名 + 参数),JVM 里没有任何语法成分能”根据字符串调用方法”,只有反射的 Method.invoke 能做到。

换句话说:动态代理负责”骗”调用方,反射负责让服务端”找得到”方法。两者合起来,远程调用才长得像本地调用。

2.2 客户端:动态代理拦截调用

一个最小化的客户端代理长这样:

// 代理类:把"方法调用"翻译成"请求对象"
public class RpcProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest req = new RpcRequest();
        req.setInterfaceName(method.getDeclaringClass().getName());
        req.setMethodName(method.getName());
        req.setParameterTypes(method.getParameterTypes()); // 关键
        req.setArguments(args);
        return client.send(req); // 序列化 → 网络发送 → 同步等待响应
    }
}

// 消费者拿到的就是这个"假实现"
UserService userService = (UserService) Proxy.newProxyInstance(
        loader, new Class[]{UserService.class}, new RpcProxy());

注意 parameterTypes 为什么必须传:方法是可以重载的,getUser(long id) 和 getUser(String name) 同名,服务端只靠方法名无法定位,必须靠参数类型列表唯一定位一个方法。

2.3 服务端:反射定位并调用真实方法

// 启动时注册:服务导出到本地缓存
Map<String, Object> serviceMap = Map.of(
    "com.demo.UserService", new UserServiceImpl()
);

// 收到请求后的处理逻辑
public Object handle(RpcRequest req) throws Exception {
    Object target = serviceMap.get(req.getInterfaceName()); // 1. 找到实现类实例
    Method method = target.getClass().getMethod(
            req.getMethodName(), req.getParameterTypes());   // 2. 反射定位方法
    method.setAccessible(true);                              // 3. 跳过访问检查
    return method.invoke(target, req.getArguments());        // 4. 真正执行
}

四步里第 2、4 步就是反射的核心:getMethod 用字符串还原出方法签名,invoke 把字节流里解出来的参数”打”到真实对象上。

2.4 反射的性能问题与框架的优化

裸反射是慢的:装箱拆箱、可变参数数组的组装、访问权限检查,而且 Method.invoke 对 JIT 不友好,无法内联,历史上还有反射调用计数超阈值后去优化的坑。主流框架的优化路线:

手段原理使用者
缓存 Method 对象getMethod 每次都遍历方法表,缓存后只查一次几乎所有框架的基本功
setAccessible(true)跳过安全检查同上
运行期字节码生成用 Javassist、ASM 直接生成 target.getUser(id) 这样的强类型调用代码,把反射”编译”掉Dubbo 的 ProxyFactory、FastJSON2 的序列化器
MethodHandle / invokedynamicJVM 层面对反射的替代品,可被 JIT 内联高版本 JDK 上的框架逐渐采用

另一个越来越重要的成本:GraalVM Native Image 里反射默认不可用,必须显式注册反射元数据。这是反射式 RPC(比如传统 Dubbo 应用)迁移 AOT 的主要开销,也是 Spring 6 推 AOT 元数据生成的背景之一。

2.5 反射不是唯一解

严格说,反射是 Java 等语言实现 RPC 的手段,不是 RPC 的必要组成。gRPC 的代码生成路线(protoc 生成强类型 Stub)就没有运行时反射这一环。两条路线的分工可以这样概括:

反射解决的是”不写模板代码”的问题;代码生成解决的是”调用快且类型安全”的问题。动态代理 + 反射是运行时生成调用逻辑,protobuf + Stub 是编译期生成调用逻辑——同一问题的两种解法。

三、注册中心:AP 还是 CP

3.1 判断标准

一句话:数据错了更痛,还是数据拿不到更痛?

服务发现的特殊性在于:注册中心只是”控制面”,真正的调用发生在”数据面”。消费者本地缓存了地址列表,注册中心短暂挂掉不影响存量调用;真正致命的是注册中心自己因为追求一致性而不可用,把控制面的故障扩散成数据面的全局雪崩。所以服务发现对可用性的要求,高于对一致性的要求——这也是当年 Eureka 那篇 “ZooKeeper is Incompatible with Service Discovery” 挑战 ZK 的核心论点。

3.2 ZooKeeper 为什么是 CP:ZAB 协议

ZAB(Zookeeper Atomic Broadcast)是一个崩溃恢复型共识协议,写路径是:

客户端写请求 → 任意 Follower 转发给 Leader
  → Leader 给 Proposal 分配全局递增的 zxid,广播给所有 Follower
  → 过半 Follower 持久化成功并 ACK
  → Leader 才提交(COMMIT),并响应客户端

两个关键推论:

推论一:分区时少数派主动”自杀”。 5 个节点被网络切成 2+3 两堆,只有拿到 3 票的那堆能继续工作;2 节点那堆发现自己没有 quorum,会停止对外服务,其上的客户端请求全部失败。宁可不可用,也不返回有分歧的数据。

推论二:Leader 宕机期间全集群不可写。 选主期间(通常几十秒级,极端情况更久),集群拒绝写请求,依赖它的注册与发现全部中断。

一个诚实的补充:ZK 的读请求默认由本节点直接返回,可能读到旧数据,所以它并非严格的线性一致;但在 CAP 分类里,它是分区时选择牺牲可用性的典型,通常归为 CP。对服务发现场景更麻烦的是会话模型的扰动放大:网络抖动或 GC 停顿导致会话超时,临时节点被批量删除,客户端先经历一波 Watch 风暴,再经历一波重新注册风暴。这在高变动、大规模的服务发现里是真实的生产事故来源。

3.3 Nacos 为什么是 AP:Distro 与数据分型

Nacos 做了一个 ZK 没做的设计:把数据分成两类,各用各的一致性协议。

数据类型典型例子协议模式
临时数据(内存态、心跳维持)服务发现的实例地址DistroAP
持久数据(落盘、必须一致)配置内容、持久服务RaftCP

Distro 的工作方式:

- 集群每个节点按 key 哈希负责一部分数据的"主权"
- 写请求路由到主权节点,写入本地内存后立即返回成功
- 然后异步把增量数据通过 gRPC 同步给其他节点
- 每个节点都持有全量数据,任何节点都能独立响应读请求
- 实例靠客户端心跳续命,超时自动摘除;节点挂了,实例重新心跳即可重建数据

于是分区时:每个节点继续用自己手里的地址列表服务消费者——可用性保住了,代价是列表可能短暂过期(新上线的实例别人还没同步到)。教科书式的 AP:最终一致 + 分区容忍。

Nacos 敢这么做的前提,是它想清楚了那道选择题:服务实例地址是”可再生”的数据——哪怕全丢了,实例心跳一打就重建;配置是”不可再生”的数据——错一份就出生产事故。所以前者 AP,后者 CP。“按数据性质选一致性”这个思路,比”Nacos 是 AP”这句话本身更值得记住。

3.4 选型决策

维度选 AP选 CP
数据性质临时的、可再生的(实例地址)持久的、不可再生的(配置、元数据)
数据错了的后果拿到失效地址,调用失败,客户端重试与熔断兜底节点间不一致,部分实例行为分裂,排查困难
数据拿不到的后果消费者无法发现服务,全局雪崩短暂不可写,业务等一等
数据变更频率高频(发布、扩容、抖动)低频
典型场景服务发现、集群成员管理配置中心、分布式锁、选主、命名服务

3.5 两个补充观点

客户端缓存可以把 CP 存储”用出 AP 效果”。 Kubernetes 是反例般的正面教材:etcd 明明是 CP(Raft),但 K8s 的服务发现工作得很好,因为 kube-proxy、CoreDNS 在节点本地有缓存和容错,数据面不直接依赖 etcd 的可用性。更精确的说法是:AP 不是唯一解,但”数据面绝不依赖控制面实时可用”是铁律;AP 注册中心只是让这条铁律更容易满足。

分区不是唯一要考虑的场景。 ZK 的问题不光是分区时 CP,还有前面说的”假下线”风暴。对高变动、大规模的服务发现,这种扰动放大的代价往往比分区本身更高——这也是 Nacos 用客户端心跳 + Distro 的重要原因。

四、Dubbo 3:工业级框架的答卷

Dubbo 3 的每一个选择,几乎都在回答同一个问题:如何在保留 Dubbo 2 易用性和治理能力的前提下,适配云原生和超大规模集群。先给总览,再逐环节展开。

RPC 环节Dubbo 2 的做法Dubbo 3 的选择改变的动机
代理层Javassist 字节码代理保留(SPI 可换)已经够快,不动
调用模型同步为主,异步是补丁全链路 CompletableFuture异步不再有独立编程模型
序列化hessian2 默认fastjson2 自动协商(3.2+),Triple 下 Protobuf性能 + 安全
协议dubbo 私有 TCP 协议Triple(基于 HTTP/2,兼容 gRPC)云原生、网关、跨语言、流式
传输层Netty 长连接Netty + HTTP/2 多路复用,3.3 支持 HTTP/3消除大报文下的队头阻塞
服务发现接口级注册(URL 全量)应用级注册 + 元数据点对点拉取注册量从”接口数 × 实例数”降为”实例数”
元数据塞在注册中心的 URL 参数里独立元数据中心 + revision 聚合数据量与治理能力解耦
部署形态SDK onlySDK / Sidecar mesh / Proxyless mesh治理能力可选、可下沉

4.1 代理层:Javassist,保留不动

Dubbo 从 2.x 起就用 Javassist 在运行期生成代理类,而不是 JDK 动态代理。回顾第二节讲的反射开销:JDK 的 Method.invoke 无法被 JIT 内联,而 Javassist 生成的是强类型的直接调用代码,等于把反射”编译”掉了。

Dubbo 3 没有动这一层——因为这不是瓶颈,也不是云原生化 的障碍。改架构时只改有明确收益的部分;代理层通过 SPI(ProxyFactory 扩展点)保持可替换,就够了。

4.2 调用模型:全链路异步统一

Dubbo 2 的同步和异步是两套 API:async=true 加 Future 的语义破碎,RpcContext 基于 ThreadLocal,异步后上下文串味。Dubbo 3 把整个调用链改造为以 CompletableFuture 为一等公民:同步调用只是在异步结果外面包了一层 future.get(),Filter 链、Cluster 层、协议层内部全部流转 CompletableFuture。

为什么必须这么改:Triple 协议天生是异步的(HTTP/2 stream),流式调用(客户端流、服务端流、双向流)无法用”同步阻塞 + ThreadLocal”的模型表达。协议和编程模型必须一起改,否则新协议就是空中楼阁。

4.3 序列化:hessian2 到 fastjson2 的自动协商

Dubbo 3.2 引入序列化协议自动协商:两端都是 Dubbo 3 且 classpath 里有 fastjson2,就自动用 fastjson2,否则回退 hessian2,过程对用户透明。

换的理由:性能上 fastjson2 基于 ASM 生成序列化器,路径接近手写代码;安全上 hessian2 是反序列化漏洞的重灾区,fastjson2 默认开启防护。为什么不强制换:序列化是最难升级的环节(两端必须同时换),所以选择协商而非一刀切——又一次平滑迁移优先的取舍。

Triple 协议下则统一用 Protobuf(Binary 或 JSON),因为要跨语言、要与 gRPC 生态互通,Java 私有的对象序列化格式在这里没有生存空间。

4.4 协议:Triple,最大的一笔押注

dubbo 协议是基于 TCP 的高性能私有协议,缺点官方说得很直白:通用性差。私有二进制协议意味着:

  • Envoy、网关、K8s Ingress 这些通用基础设施读不懂你的流量,服务网格里 sidecar 无法基于内容路由,必须定制;
  • 跨语言要为每种语言实现一遍协议栈;
  • 没有流式语义;
  • HTTP 生态(鉴权、压缩、可观测性、调试工具)全部享受不到。

Triple 的选择是:基于 HTTP/2、100% 兼容 gRPC 的 wire format,但保留 Dubbo 的编程模型。为什么不是直接用 gRPC?

gRPCTriple
通信格式Protobuf over HTTP/2相同(兼容 gRPC)
编程模型必须先写 IDL,生成 StubJava 接口加注解即可,也支持 IDL
服务发现与治理官方不带,自己搭内置 Dubbo 的注册发现、负载均衡、熔断

Triple = gRPC 的互通性 + Dubbo 的易用性和治理体系。wire format 兼容是单向受益的:所有 gRPC 客户端、网关、代理天然认识 Triple 流量。

一个值得注意的兼容性设计:协议默认值至今仍是 dubbo(官方文档明确说”考虑到对过往版本的兼容性,各发行版本均默认使用 dubbo 协议”),但强烈建议新用户显式配置 tri。这不是技术犹豫,而是升级策略:老集群里有海量 2.x 消费者,默认值一变就是断崖。配套的迁移路径是 3.3 的”单端口双协议 + preferred-protocol 首选协议”:Provider 在同一端口同时发布 dubbo 和 triple,注册地址里带上首选协议标记,消费者逐个升级后自动切换,全程无感。

3.3 之后 Triple 进一步吃下了 REST:基于同一套 HTTP 协议栈,加注解就能以 REST 风格发布 HTTP 服务(支持 Spring MVC、JAX-RS 方言),并且支持 HTTP/3(QUIC,通过 Alt-Svc 协商自动升级)。意图很清晰:让 Triple 同时覆盖东西向(RPC)和南北向(HTTP/REST)流量,用户不再需要”Web 框架管南北向、RPC 框架管东西向”两套栈。

4.5 传输层:HTTP/2 多路复用

dubbo 协议默认单条 TCP 长连接跑所有请求。单连接模型在小报文高并发下很高效(连接少、握手少),但大报文会阻塞同连接上的其他请求。Triple 换成 HTTP/2 后天然获得 stream 多路复用:一条连接上并发成百上千个 stream,互不阻塞;3.3 再进一步支持 HTTP/3 解决 TCP 层队头阻塞和弱网重连。

这笔账可以这样算:私有协议当年”省掉 HTTP 头”的性能收益,在 HTTP/2 头压缩(HPACK)加多路复用面前已经不剩多少,而互联互通的代价却越来越大——账算过来,就是 Triple。

4.6 服务发现:接口级到应用级,变更最剧烈的一环

Dubbo 2 的接口级模型:每个接口注册一条完整 URL(含全部配置参数),注册中心的数据量等于接口数 × 实例数。官方给出的实测量级:一个有 10 个 Service、部署 100 个实例的应用,产生 1000 条地址数据;阿里巴巴、工商银行这类规模的集群,注册中心容量触顶,消费端框架常驻内存超过 40%。数据被两个维度同时放大:100 个唯一地址被放大 10 倍,10 份唯一服务元数据被放大 100 倍。

flowchart TB
    subgraph G2 ["Dubbo 2:接口级发现"]
        A2["注册中心数据量 = 接口数 × 实例数"]
        B2["每条 URL 携带全部配置元数据"]
    end
    subgraph G3 ["Dubbo 3:应用级发现"]
        A3["注册中心只存应用名到实例地址"]
        B3["富元数据走元数据中心与点对点拉取"]
    end
    G2 -->|"注册量与业务规模解耦"| G3

Dubbo 3 的解法分三步:

  1. 注册中心只存”应用名 → 实例地址”。数据量降为实例数,与接口数彻底解耦——业务 RPC 重构不再影响服务发现集群的容量稳定性,且与 K8s、Spring Cloud 的发现模型对齐。
  2. 元数据不再走注册中心,改为点对点拉取。Dubbo 3 内置了一个 MetadataService 元数据服务,Consumer 拿到精简地址后,直接 RPC 调用 Provider 拉取”这个实例导出了哪些接口、什么配置”,在消费端聚合还原出类似 Dubbo 2 的运行态 URL 模型。
  3. revision 去重。内容相同(同一套接口和配置)的实例计算出相同 revision,共享一份元数据——100 个实例部署得完全一样,元数据实际只有 1 份。

应用级模型带来一个新问题:Consumer 只知道自己要消费哪些接口,怎么知道该订阅哪个应用?Dubbo 3 引入独立的元数据中心,Provider 启动时上报”接口名 → 应用名”映射(一对多)。Consumer 订阅前先查这个映射,转换成应用名再做精确订阅。

迁移路径同样贯彻平滑:Provider 双注册(register-mode=all),Consumer 双订阅(APPLICATION_FIRST 智能决策),且两份地址完全隔离、绝不混用(要么全用接口级、要么全用应用级,第一次地址通知时定死),确认全部迁移后再切单注册。

这一节可以提炼一个观点:Dubbo 3 的服务发现 = Spring Cloud 的数据模型 + Dubbo 的治理能力。注册中心只当”电话簿”(AP 足矣,呼应第三节的选型讨论),治理所需的富元数据走旁路。把”发现的大规模”和”治理的精细化”这两件冲突的事拆到两条链路上,不再让注册中心同时背两个 KPI。

4.7 集群容错与负载均衡:保留加增强

这一层 Dubbo 3 基本继承了 Dubbo 2 的设计,因为它恰恰是 Dubbo 的护城河而非负担:

  • 容错策略:Failover(默认,失败自动换节点重试)、Failfast、Failsafe、Failback、Forking、Broadcast——把”重试”做成调用方的集群语义,而不是让每个业务自己写 try-catch;
  • 负载均衡:Random(默认)、RoundRobin、LeastActive、ConsistencyHash,Dubbo 3 增加了自适应负载均衡(基于节点实时负载的 P2C 思路),把”最闲优先”从静态统计升级为动态探测;
  • Filter 链 + SPI:整个 Invoker 链是责任链,每个环节都是 Adaptive SPI 扩展点——这是 Dubbo 能长出庞大生态(鉴权、tracing、限流全靠 Filter 插进来)的架构根基,3.x 严格保留了它。

4.8 部署形态:三态并存

Dubbo 3 的最终愿景是让治理能力可选、可下沉:

  • 传统 SDK 模式:治理能力全在客户端,性能最好;
  • Sidecar mesh:Triple 基于 HTTP/2,Envoy 等 sidecar 可以直接解析流量做路由——这正是私有 dubbo 协议做不到、必须发明 Triple 的原因之一。但 sidecar 每跳增加两段网络和两次代理,性能损耗大;
  • Proxyless mesh:客户端直连,控制面(如 Istio)只下发 xDS 配置给 Dubbo SDK——保留 mesh 的统一控制面,又不付 sidecar 的性能税。

这也解释了为什么应用级服务发现如此重要:sidecar 和 xDS 的世界只认”应用、服务”粒度,不认识 Dubbo 的接口粒度。应用级发现是 Dubbo 融入 mesh 生态的入场券。

五、总结:Dubbo 3 的三条主线

回头看,Dubbo 3 的所有选择可以收拢成三条主线:

  1. 协议云原生化(Triple):用 HTTP/2 换掉私有协议,得到网关友好、跨语言、流式、mesh 兼容,代价用”wire format 兼容 gRPC”来抵消;
  2. 服务发现规模化(应用级):注册中心数据从 O(接口数 × 实例数) 降到 O(实例数),富元数据旁路化,规模与治理能力解耦;
  3. 迁移平滑化:序列化自动协商、双注册双订阅、单端口双协议——每一个破坏性变更都配了一条对用户无感的升级路径,这本身就是 Dubbo 3 最重要的架构选择。

原理层的结论同样可以收拢成一句话:RPC 的本质是”让远程调用看起来像本地调用”,代理和反射负责幻觉本身,注册中心的 AP/CP 取舍和框架的工程化设计,负责让这个幻觉在生产环境的规模、故障和升级中撑住不破。

Sources

  1. Dubbo 支持的 RPC 通信协议 — Apache Dubbo 官方文档
  2. 应用级服务发现 — Apache Dubbo 官方文档
  3. 应用级地址发现迁移指南 — Apache Dubbo 官方文档
  4. 应用级服务发现 vs 接口级服务发现 — Apache Dubbo 官方文档
  5. 元数据中心概述 — Apache Dubbo 官方文档
  6. 升级到 triple 协议 — Apache Dubbo 官方文档
  7. Apache Dubbo 3.3 全新发布:Triple X 领衔,开启微服务通信新时代 — Apache Dubbo 官方博客 (2024-09-11)

Related

On this page 一、RPC 的完整流程与角色1.1 一次调用的完整链路1.2 链路上的角色二、反射调用:RPC 魔法的支点2.1 为什么说反射是核心2.2 客户端:动态代理拦截调用2.3 服务端:反射定位并调用真实方法2.4 反射的性能问题与框架的优化2.5 反射不是唯一解三、注册中心:AP 还是 CP3.1 判断标准3.2 ZooKeeper 为什么是 CP:ZAB 协议3.3 Nacos 为什么是 AP:Distro 与数据分型3.4 选型决策3.5 两个补充观点四、Dubbo 3:工业级框架的答卷4.1 代理层:Javassist,保留不动4.2 调用模型:全链路异步统一4.3 序列化:hessian2 到 fastjson2 的自动协商4.4 协议:Triple,最大的一笔押注4.5 传输层:HTTP/2 多路复用4.6 服务发现:接口级到应用级,变更最剧烈的一环4.7 集群容错与负载均衡:保留加增强4.8 部署形态:三态并存五、总结:Dubbo 3 的三条主线