前阵子刚把公司核心链路从单机房迁到同城双机房,本以为就是改改注册中心地址、扩一批机器的事,结果上线当晚监控就开始飘红:java.net.SocketTimeoutException: Read timed out和No provider available from registry交替出现。更尴尬的是,打开Nacos控制台,服务明明在线,每个机房都有节点,可消费者就是时而报超时、时而报没有提供者。
这问题看着像网络抖动,实际根源卡在 Dubbo 集群对多机房拓扑毫无感知。默认的随机负载均衡、默认的重试策略、默认的路由规则,全是在“单机房无脑可用”的前提下设计的,一旦跨机房,RPC 耗时、可用性、容错方式全都会变化。这篇文章就是针对 Dubbo 多机房部署场景,把超时异常和无提供者异常的成因拆开,再给出通过集群扩展(自定义 LoadBalance、Router、Cluster)让 Dubbo 适配多机房的完整方案。
内容适合正在做多机房改造、被跨机房调用问题折磨过的 Java 服务端同学。读完你至少能明白:两类异常到底是谁触发的,timeout 该怎么算,以及怎么用最少的代码让 Dubbo 具备“同机房优先、跨机房兜底”的能力。
1. 多机房部署下两类异常的真实成因
1.1 一次跨机房RPC调用比想象中多花多少时间
很多人对超时的第一反应是“接口慢了”,但在多机房场景里,慢的不一定是接口本身。
一次 Dubbo 调用在网络上要经历这些阶段:consumer 通过 Netty 发请求,走 TCP 连接,经过交换机、防火墙、光缆传输,到达 provider 所在机房的机器,处理完再原路返回。单这一趟,同机房可能只要 0.2ms,跨同城机房通常 2~10ms,跨地域或跨海那就在 30ms 以上了。
我按不同部署模式整理过一份耗时参考:
| 耗时来源 | 同机房 | 同城跨机房 | 异地跨机房 |
|---|---|---|---|
| 网络 RTT | 0.1~0.5ms | 1~10ms | 30~200ms |
| 序列化/反序列化 | 与数据量成正比 | 同上,但受带宽影响 | 明显放大 |
| 业务处理耗时 | 不变 | 不变 | 不变 |
| 排队等待(线程池饱和时) | 低 | 中 | 高 |
也就是说,跨机房调用天然要比同机房多出几毫秒到几十毫秒的固定成本。如果业务接口本身 P99 是 500ms,你在消费端把 timeout 写死成 300ms,那跨机房时就必然超时。这类超时并不是服务挂了,而是你的超时预算压根没覆盖掉网络开销。
1.2 No provider异常为什么“凭空出现”
无提供者异常比超时更让人头皮发麻,因为它在 Nacos 上明明能看到服务节点。
No provider available from registry这个报错,从字面理解是“注册中心里没有可用的提供者”。但实际触发它的情况可以五花八门:
- consumer 连接的 namespace 和 provider 不在一个 namespace。
- consumer 订阅的 group 和 provider 注册的 group 不一致。
- provider 进程起了,但注册动作还没完成,consumer 抢先订阅。
- Nacos 的健康检查还没来得及把不健康节点摘掉,consumer 本地缓存里已经全是死节点。
- 路由规则把 provider 全过滤了,剩下的 invoker 列表为空。
这才是多机房场景最坑的地方:网络抖动会让 Nacos 的临时实例被标记不健康,如果 consumer 收到的推送不及时,本地还拿着旧列表去调用,报的就是 No provider;如果路由规则里写死了某个机房的 IP 段,而那个机房刚好缩容,也会报 No provider。
所以排查这两类异常,不能只盯着业务代码,要把网络链路、注册中心数据、路由结果、集群容错策略全部串起来看。
2. 超时设置不是拍脑袋:参数计算与配置落地
2.1 搞懂timeout的优先级和覆盖关系
Dubbo 的 timeout 配置层级很容易让人迷糊,我直接说结论。
- 消费端配置优先级高于提供端。也就是说,consumer 那边配了 timeout,就以 consumer 的为准;没配才回落 provider。
- 方法级配置高于接口级,接口级高于全局默认。
- 全局默认值在最新版本里是 1000ms。
用 XML 举例,优先级从高到低是这样:
<!-- 方法级,最精确 --> <dubbo:reference id="userService" interface="com.example.UserService"> <dubbo:method name="getUserById" timeout="2000" retries="0"/> </dubbo:reference> <!-- 接口级 --> <dubbo:reference id="userService" interface="com.example.UserService" timeout="3000"/> <!-- 全局兜底 --> <dubbo:consumer timeout="5000"/>在多机房场景里,我强烈建议不要在 provider 端裸奔设 timeout。因为 provider 根本不知道调用方在哪个机房,只有当所有 consumer 都没有配置时,provider 的配置才会生效。正确的做法是:超时配置放消费端,并且结合下方说的耗时口径来算。
2.2 用P99的测量结果倒推合理timeout
真实项目中,timeout 是不能靠“感觉”设的。我的习惯是先拿到接口的耗时分布,再按公式推。
公式很简单:
timeout = P99 业务耗时 + 网络RTT预估 + 序列化传输耗时 + 重试预算
举个例子。你调用getUserById,单机压测下 P99 是 450ms,consumer 和 provider 分布在同城两个机房,网络 RTT 按 5ms 算,传输一个 2KB 的对象基本可忽略。那么 timeout 至少是 450 + 5 + 余量,我一般会再乘 1.5~2 倍,也就是 700~1000ms。
如果这个接口还会被做成失败重试,那情况就完全不同了。Dubbo 默认retries=2,意思是第一次失败后会再重试 2 次,总共最多执行 3 次。此时对于不幂等的写接口,超时后重试可能造成重复下单、重复扣款。所以我一般在消费端把写接口的 retries 直接设成 0,读接口看情况保留一次重试,但 timeout 相应放大。
还有一个小技巧:多机房链路刚上线时,不要直接用线上流量赌。做一轮小流量压测,从 consumer 所在机房发起请求,记录 Tomcat / Dubbo 线程池的耗时分布,再把监控里的 TP99 拉出来,用这个值算 timeout,基本就能避免无脑超时。
2.3 retries是双刃剑:超时后的雪崩效应
默认的集群容错failover在超时后会自动换节点重试。单机房场景下,一个节点超时,换一台机器可能就好了;但多机房场景下,如果 consumer 第一次调用打到跨机房节点超时,重试又随机选到另一个跨机房节点,等于把一个慢请求变成 3 个慢请求。
更恶心的是,consumer 侧的超时并不会中断 provider 侧的继续执行。场景是这样的:
- consumer 调用 provider A,设置 timeout=1000ms。
- provider A 实际执行需要 1500ms。
- consumer 在 1000ms 时抛超时,然后触发 failover 重试到 provider B。
- provider A 还在继续执行,稍后也返回了结果,但这个结果已经没人接收。
最终 provider A 和 B 都被执行了一遍。流量翻倍,数据库压力翻倍,超时又会引发更多超时。
所以我在多机房实战中有一个原则:凡是改写了数据的接口,一律 retries=0;读接口重试要控制次数;重试是否跨机房应由路由策略决定,而不是随机选。
这也是后面为什么要做集群扩展的核心理由——不是 Dubbo 默认不能用,而是默认策略在多机房下太“无脑”。
3. 无提供者异常:从注册中心到路由逐层排查
3.1 Nacos侧最容易踩的四个坑
先别急着写代码,Nacos 侧的问题占无提供者异常的一大半。以下四个坑,我在排查现场反复见到。
第一个是 namespace 隔离。每个环境一个 namespace 本来是好习惯,但 Dubbo 消费端和提供端如果配置的 namespace 不一致,两边数据完全不通。Nacos 控制台看起来一切正常,因为你是用某个 namespace 登录的,consumer 在另一个 namespace 下当然订阅不到任何 provider。
第二个是 group 不一致。Dubbo 服务默认组是DUBBO_DEFAULT_GROUP,如果你在 provider 里配了<dubbo:service group="pay"/>,consumer 侧没配,或者配成了其他组,No provider 马上出现。
第三个是网络分区导致的健康检查滞后。多机房场景里,如果 Nacos 和某个机房的网络抖动,provider 心跳发不过来,Nacos 端会把这个实例标记为不健康。临时实例默认 15 秒没心跳就是不健康,30 秒后会被移除。这期间如果 consumer 被推送了一个空列表,就会报无提供者。反过来,如果推送失败,consumer 本地还保留着已经死掉的节点,调用就会超时或拒绝连接。
第四个是 Dubbo 3.x 的应用级服务发现。3.x 默认走应用级发现,需要 Nacos 2.x 配合,同时依赖 metadata 信息。如果 provider 的dubbo.application.name配置不一致,或者 metadata center 不通,也会出现“Nacos 里有服务、consumer 却拿不到可用实例”的诡异现象。
排查无提供者异常,我第一步永远是打开 Nacos 控制台,切到消费端配置的 namespace,搜索对应的服务名,看一眼服务列表里有几个 IP。如果 IP 都在,再去查 consumer 日志里打印的注册中心地址和 group;如果 IP 不在,就要去 provider 机器上看启动日志,确认服务注册成功没有。
3.2 用消费者日志和Dubbo运维命令组合定位
当 Nacos 控制台显示服务在线,consumer 还报 No provider 时,就进入了最烧脑的阶段。这时候要善用 Dubbo 自带的运维能力。
consumer 启动日志里会打印关键信息:
[RegistryDirectory] Register: consumer://... to registry nacos://... [RegistryDirectory] Subscribe: provider://... No provider available for the service com.example.UserService from registry nacos://...注意看from registry后面的地址,确认 consumer 连接的是哪个 Nacos 集群。然后再通过 Dubbo QOS 端口进入运维命令,默认端口是 22222:
telnet 127.0.0.1 22222 dubbo> ls dubbo> ps dubbo> invoke com.example.UserService.getUserById(1001)ls可以看当前进程发布了哪些服务,ps可以看注册中心的信息,invoke可以手动发起一次调用。如果手动 invoke 能成功,说明 consumer 到 provider 的网络链路没问题,问题大概率出在路由过滤或动态配置上;如果 invoke 也报 No provider,那就必须去看 Directory 里的 invoker 列表到底是不是空的。
这里有另一个重要检查点:所有 provider 是否被路由规则过滤了。Dubbo 的 Router 执行顺序是在 Directory.list() 返回结果之后、负载均衡之前。假如你在配置中心配了条件路由,例如host = 10.20.*,而 provider 实际 IP 是10.30.*,那么即便注册列表有 10 个节点,经过路由后也可能变成 0 个,最终报错就是 No provider available。这类“假无提供者”在界面上完全看不出来,必须靠日志或者临时移除路由规则来验证。
3.3 路由过滤导致的“假无提供者”
我在生产环境就踩过一次自定义 Router 的坑。当时为了做机房优先,写了一个 RouterFactory,只保留同机房的 provider。逻辑本身没问题,但新机房刚上线时 provider 数量不足,某个服务的同机房节点全部缩容下线,自定义 Router 把跨机房节点全过滤了,consumer 直接报 No provider。
这个教训最终变成一条铁律:自定义 Router 过滤后要加空保护。也就是说,如果过滤后 invoker 列表为空,应当返回原始列表作为兜底,让 Dubbo 至少能调到跨机房节点,而不是连调用的机会都没有。
List<Invoker<T>> matched = doFilter(invokers); if (matched == null || matched.isEmpty()) { return invokers; } return matched;后面第 4 节的实战代码里,我会保留这个保护逻辑,这一点比任何花哨的路由策略都重要。
4. 集群扩展实战:机房优先的路由与负载均衡
4.1 为什么默认的随机负载均衡不友好
Dubbo 默认负载均衡是random,每个 provider 按权重随机选中。单机房部署没问题,多机房就不行了:如果机房 A 有 20 个节点,机房 B 有 5 个节点,随机策略下,机房 A 的 consumer 会有约 20% 的流量被分到机房 B,跨机房调用的比例几乎是不可控的。
跨机房调用本身不是问题,问题在于它放大了超时风险和容错压力。所以要做的第一件事,是让 Dubbo 的负载均衡具备“同机房优先”的感知能力。实现方式有两种:
- 自定义 LoadBalance:在候选集合中选择时给同机房节点更高权重。
- 自定义 Router:在候选集合生成阶段就过滤掉跨机房节点。
两者职责不同。Router 决定“哪些节点能调”,LoadBalance 决定“能调的节点里优先调谁”。实际项目里我倾向于组合使用:Router 做严格的同机房首选,LoadBalance 做同机房内部的权重分配。
4.2 先给provider打上机房标签
无论用哪种扩展,前提都是让 Dubbo 知道每个 provider 属于哪个机房。最简单的方案是直接在 provider 注册的 URL 参数里加自定义参数。
XML 方式:
<dubbo:provider id="zoneAProvider" parameters="zone=zoneA"/> <dubbo:service interface="com.example.UserService" provider="zoneAProvider"/>Java 注解方式:
@Service(parameters = {"zone=zoneA"}) public class UserServiceImpl implements UserService {}这样 provider 注册到 Nacos 时,URL 上会带有zone=zoneA参数。consumer 从注册中心拿到的 invoker 列表里,每个 invoker 的 URL 也带这个参数。判断机房就变得很简单:取本地 IP 对应的机房,再比较 provider 地址上的 zone 参数。
而且注意,机房判断最好用统一的配置映射。比如把“IP 段 -> 机房名”的映射放到一个配置类里:
public final class ZoneUtil { private static final Map<String, String> ZONE_MAP = new HashMap<>(); static { ZONE_MAP.put("10.10.", "zoneA"); ZONE_MAP.put("10.20.", "zoneB"); } public static String getZoneByHost(String host) { for (Map.Entry<String, String> entry : ZONE_MAP.entrySet()) { if (host.startsWith(entry.getKey())) { return entry.getValue(); } } return "unknown"; } }4.3 自定义LoadBalance:同机房优先的权重选择
自定义 LoadBalance 需要实现 Dubbo 的 SPI 扩展点。核心代码我写了一个简化版本:
package com.example.dubbo; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance; import java.util.List; import java.util.stream.Collectors; public class ZoneAwareLoadBalance extends AbstractLoadBalance { @Override protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String localZone = ZoneUtil.getZoneByHost(url.getHost()); List<Invoker<T>> sameZoneInvokers = invokers.stream() .filter(invoker -> localZone.equals( invoker.getUrl().getParameter("zone", "unknown"))) .collect(Collectors.toList()); // 没有同机房节点时,降级到全量节点,绝不能返回空 if (sameZoneInvokers.isEmpty()) { return invokers.get(0); } return sameZoneInvokers.get(0); } }这里为了可读性做了简化,实际上行业内会用加权随机算法在sameZoneInvokers里选一个,而不是直接取第一个。你可以把AbstractLoadBalance的getWeight方法和权重随机逻辑套进来,实现思路是:同机房节点权重设为 100,跨机房节点权重设为 1,那么绝大多数流量会落在同机房,少量流量做跨机房探测和兜底。
SPI 注册文件放在META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance:
zoneAware=com.example.dubbo.ZoneAwareLoadBalance消费端使用:
<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="zoneAware"/>这个方案的优点是改动小、见效快。缺点是它只影响“选谁”,不会把跨机房节点完全排除。如果业务要求必须同机房调用,跨机房一律不允许,那就要配合 Router 使用。
4.4 用Router做更严格的机房隔离
自定义 Router 同样走 SPI。先定义 RouterFactory:
package com.example.dubbo; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.cluster.Router; import org.apache.dubbo.rpc.cluster.RouterFactory; public class ZoneRouterFactory implements RouterFactory { @Override public <T> Router getRouter(URL url) { return new ZoneRouter(); } }然后实现 Router 的 route 方法:
package com.example.dubbo; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcException; import org.apache.dubbo.rpc.cluster.Router; import java.util.List; import java.util.stream.Collectors; public class ZoneRouter implements Router { @Override public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { if (invokers == null || invokers.isEmpty()) { return invokers; } // 从调用上下文里取目标机房,没指定就用本地机房 String targetZone = invocation.getAttachment("zone"); if (targetZone == null) { targetZone = ZoneUtil.getZoneByHost(url.getHost()); } List<Invoker<T>> matched = invokers.stream() .filter(invoker -> targetZone.equals( invoker.getUrl().getParameter("zone", "unknown"))) .collect(Collectors.toList()); // 关键:过滤为空时回退全量,防止 No provider if (matched.isEmpty()) { return invokers; } return matched; } }SPI 注册文件META-INF/dubbo/org.apache.dubbo.rpc.cluster.RouterFactory:
zone=com.example.dubbo.ZoneRouterFactory这段代码里有一个重要的取舍:当目标机房没有 provider 时,返回全量列表而不是空列表。这样做的代价是跨机房调用仍会发生,但换来的是服务可用性优先。如果业务上能接受“目标机房挂了就失败”,也可以把空列表直接返回,让上层容错策略兜底,但生产环境我强烈建议保留回退逻辑,先保命再谈隔离。
5. 集群容错策略扩展:同机房失败后怎么办
5.1 Dubbo内置Cluster的适用边界
Dubbo 内置了多种集群容错策略,多机房场景下要重新审视它们的适用性:
| 策略 | 行为 | 多机房适用性 |
|---|---|---|
| failover | 失败自动切换,默认重试 2 次 | 适合读接口,但重试可能跨机房放大流量 |
| failfast | 失败立即报错 | 适合写接口,避免重复执行,但多机房下可用性最低 |
| failsafe | 失败吞掉异常 | 适合日志等非关键调用,多机房下容易掩盖问题 |
| failback | 失败记录后定时重发 | 适合异步化场景,但你要自己处理幂等 |
| forking | 同时调用多个节点,取最早成功 | 严重放大流量,多机房慎用 |
默认 failover 在多机房下最令人担忧的一点是:它重试时不会考虑机房属性,还是会通过负载均衡重新选节点。也就是说,第一次调跨机房超时,第二次很可能又随机到跨机房,第三次也是。整个超时窗口被拉长,用户体验极差。
要解决这个问题,除了前面说的路由和负载均衡之外,还有一个更彻底的方案:自定义 Cluster,让整个容错过程就限定在“同机房优先”的大前提下。
5.2 自定义Cluster:先同机房、后跨机房
Dubbo 的 Cluster 扩展入口是Cluster接口,实际工作是在ClusterInvoker的doInvoke方法里完成的。我写了一个基于 FailoverClusterInvoker 的扩展:
package com.example.dubbo; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; import org.apache.dubbo.rpc.RpcException; import org.apache.dubbo.rpc.cluster.Directory; import org.apache.dubbo.rpc.cluster.LoadBalance; import org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class ZoneAwareClusterInvoker<T> extends FailoverClusterInvoker<T> { public ZoneAwareClusterInvoker(Directory<T> directory) { super(directory); } @Override protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { String localZone = ZoneUtil.getZoneByHost(getUrl().getHost()); List<Invoker<T>> localInvokers = invokers.stream() .filter(invoker -> localZone.equals( invoker.getUrl().getParameter("zone", "unknown"))) .collect(Collectors.toList()); // 同机房有节点,就在同机房范围内做 failover if (!localInvokers.isEmpty()) { return super.doInvoke(invocation, localInvokers, loadbalance); } // 同机房没节点,降级为全量 failover return super.doInvoke(invocation, invokers, loadbalance); } }然后是 Cluster 接口的实现:
package com.example.dubbo; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcException; import org.apache.dubbo.rpc.cluster.Cluster; import org.apache.dubbo.rpc.cluster.Directory; public class ZoneAwareCluster implements Cluster { @Override public <T> Invoker<T> join(Directory<T> directory, boolean buildFilterChain) throws RpcException { return new ZoneAwareClusterInvoker<>(directory); } }SPI 注册文件META-INF/dubbo/org.apache.dubbo.rpc.cluster.Cluster:
zoneAware=com.example.dubbo.ZoneAwareCluster这个方案的精妙之处在于:它把机房选择的逻辑和容错逻辑绑定在一起。同机房有可用节点时,failover 只会在同机房间切换,不会出现“同机房超时后重试到异地机房”的问题。同机房没有节点时,再降级到全量节点,保证起码能用。
用 XML 开启:
<dubbo:reference id="userService" interface="com.example.UserService" cluster="zoneAware"/>5.3 集群扩展组合使用的完整配置示例
实际项目中,我不会只用一个扩展。推荐组合是:
- Router 负责严格场景的机房隔离。
- LoadBalance 负责平时流量的同机房优先。
- Cluster 负责兜底容错策略。
消费端完整配置如下:
<dubbo:reference id="userService" interface="com.example.UserService" timeout="2000" retries="1" loadbalance="zoneAware" cluster="zoneAware"> </dubbo:reference>这样配置后,一次调用的路径是:
- 服务目录拿到全部 provider 列表。
- 自定义 Router 先按目标机房过滤,得到同机房节点列表。
- 自定义 LoadBalance 在同机房节点中按权重选择。
- 如果调用失败,自定义 Cluster 重试时仍然优先选择同机房节点。
- 如果同机房没有可用节点,降级到全量节点,避免 No provider。
这就把“同机房优先、跨机房兜底”的架构原则完整落到了 Dubbo 集群层,业务代码完全不用动。
6. 多机房Dubbo常见问题速查表
最后把这两年多机房排障过程中的高频问题整理成速查表,方便大家直接比对。
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| No provider,但Nacos控制台有节点 | namespace 不一致 | 登录 Nacos 查看服务所在 namespace | 统一 consumer/provider namespace |
| No provider,Nacos节点数正常 | group 不一致 | 对比两边 group 参数 | 统一 group 配置 |
| No provider,provider 刚发布 | 注册未完成就订阅 | 看 provider 注册日志 | 等待注册完成再做流量切换 |
| No provider,自定义 Router 后出现 | 路由过滤为空 | 临时移除 Router 验证 | Router 返回全量兜底 |
| 频繁 SocketTimeoutException | timeout 预算不足 | 拉监控 TP99 对比 RTT | 按 P99 + RTT 重算 timeout |
| 超时后流量突然翻倍 | failover 跨机房重试 | 看 Dubbo 线程池指标 | retries 降为 0 或自定义 Cluster |
| 同机房节点健康,跨机房超时严重 | 网络链路质量差 | ping + 抓包确认 | 减少跨机房流量,Router 强隔离 |
| 写接口超时后重复入库 | retries 未关闭 | 看调用日志重复请求数 | 写接口 retries=0 |
再补充三个避坑经验:
- provider 端的线程池耗尽也会导致 consumer 无脑超时。遇到大面积超时,先看 provider 的 Dubbo 线程池活跃线程数,而不是一上来就调大 timeout。
- Nacos 集群本身最好按机房就近部署。consumer 连接本机房的 Nacos 节点,能减少注册中心跨机房同步导致的心跳和推送延迟。
- 自定义扩展上线前,务必在测试环境验证“某机房全挂”的场景。我当时就是因为没测这个场景,上线后把跨机房兜底逻辑写成死循环,反而把故障范围扩大了。
最后聊点个人体会
做多机房改造,最容易犯的错误是“把单机房思路原封不动搬过来”。Dubbo 本身提供了非常灵活的 SPI 扩展机制,Router、LoadBalance、Cluster 都可以按需替换,但很多人直到线上出故障才想起来用。
我的建议是:多机房改造的第一周,就把自定义 LoadBalance 和 Cluster 写好,哪怕先不做严格隔离,也要让 Dubbo 对“同机房优先”这件事有感知。等业务稳定了,再逐步加上 Router 的强隔离规则。顺序反了,你会被 No provider 和超时异常追着跑。
另外一个细节是,所有自定义扩展都要做好降级保护。Dubbo 的集群能力再强,它也只是 RPC 框架,真正决定可用性的是你对“失败之后怎么办”的设计。宁可降级跨机房调用,也不要因为一个过滤逻辑把所有流量都打没。