ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Dubbo多机房改造:Router、LoadBalance、Cluster扩展解决超时与无提供者异常

Dubbo多机房改造:Router、LoadBalance、Cluster扩展解决超时与无提供者异常 最近帮一个团队做多机房改造前后被两个异常缠了两周多一个是消费端隔三差五报超时另一个是服务明明在跑消费端却一直报无提供者。这两个问题单拎出来都不算难可一旦放到Dubbo Nacos的多机房部署环境里排查链路瞬间长了一截而且很多在单机房环境下根本不会出现的“幽灵问题”全都会冒出来。改完之后我复盘了很久发现核心其实不在“调大timeout”或者“重启provider”这种表面操作上而在于怎么用Dubbo的集群扩展点去重新设计服务调用路径。本文就把这次多机房部署中遇到的超时异常、无提供者异常以及基于Router、LoadBalance、Cluster扩展的自定义方案完整记录下来包括配置、代码骨架、压测验证和踩坑过程。如果你也正在做多机房改造或者已经被这两个异常搞到头疼可以直接对照这份实战指南来排查。1. 多机房部署下的两种“老朋友”超时与无提供者1.1 先认清两张经典报错脸Dubbo的报错其实很有辨识度但很多人一看到大段异常堆栈就慌反而忽略了最关键的几行。超时异常在多机房场景下最常见的长这样org.apache.dubbo.rpc.RpcException: Failed to invoke the method sayHello in the service com.example.api.UserService. Tried 3 times of the providers [192.168.10.12:20880, 192.168.20.34:20880] (1/2) from the registry 127.0.0.1:8848 on the consumer 192.168.30.10 using the dubbo protocol. Last error is: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer is 3000 ms.关键信息有三处第一是Tried 3 times说明消费者不止调用了一次而是重试了两轮第二是报错的provider地址列表来自两个不同网段基本能看出流量确实跨了机房第三是Waiting server-side response timeout by scan timer is 3000 ms说明是在等待provider响应时超时。无提供者异常的经典文本则是这个org.apache.dubbo.rpc.RpcException: No provider available from registry 127.0.0.1:8848 for service com.example.api.UserService on the consumer 192.168.30.10 using the dubbo protocol. Please check if the provider has been started.看到No provider available from registry第一反应通常是“provider挂了”但这在多机房环境下往往是误导。provider明明活着注册中心也明明有数据可消费者就是拿不到可用节点。这种“眼见不一定为实”的错位感正是多机房部署排查难的核心原因。1.2 为什么多机房一上问题就集中爆发单机房环境里服务消费者和提供者都在同一个局域网网络RTT通常是0.2ms到0.5ms超时默认配置1秒甚至3秒都显得绰绰有余。但多机房一上跨地域的网络延迟立刻放大了一个量级。以上海到北京机房为例专线RTT大概在10ms到30ms公网环境下抖动更夸张加上TCP建连、线程池排队、GC停顿一次跨机房调用的端到端耗时很容易冲到几百毫秒以上。更麻烦的是Dubbo默认的重试机制是failover重试2次也就是总共最多尝试3次。跨机房调用一旦网络抖动第一次超时之后马上触发重试重试又打到同一个延迟很高的节点上连续3次失败调用方从原本几百毫秒的超时硬生生拖成了好几秒钟的全面阻塞。很多团队遇到这种情况就把timeout从1秒调到3秒再把retries调到0结果治标不治本流量高峰期照样超时只是报错变少了而已。无提供者异常在多机房环境下的爆发逻辑更隐蔽。最常见的原因是Nacos作为注册中心时多机房之间的服务发现信息同步存在延迟或分区隔离。消费者如果恰好拿着旧的、不完整的provider列表或者路由规则把本机房的provider全部过滤掉而跨机房节点又被配置拦截那最终筛选结果就是零个可用provider。这个时候注册中心显示有服务但你实际能调用的节点数确实为零。1.3 为什么常规排查手段经常失效单机房里我们习惯三板斧重启provider、清消费者缓存、看网络通不通。但这三板斧放到多机房几乎全部失灵。重启provider只能让注册中心重新推送一次服务列表如果问题是出在路由规则或者consumer订阅条件上重启一百次也没用。清缓存本质上只是强制让consumer重新拉一次注册数据但它拉回来的数据本身可能就是空列表。而“网络通不通”这件事就更微妙了跨机房往往有专线、有防火墙、有负载均衡设备端口通不代表调用链路健康也许中间某一跳已经丢包丢到惨不忍睹。我后来总结了一条经验多机房环境下超时和无提供者异常表面上像是“网络问题”或“服务问题”本质上更多是“流量路径问题”。也就是说你得先搞清楚这个请求到底应该走哪条链路、经过哪些路由筛选、最终落到哪个机房然后才能定位异常出在哪一环。这就需要我们把排查思路从“单点修复”升级成“链路分析”。2. 定位根因从注册中心、路由到调用链路的完整排查法2.1 超时异常排查链consumer、provider、网络三段定位遇到超时异常我习惯把它拆成三段来排查消费者侧、网络链路、提供者侧。这三段里有一条隐藏线索provider端到底收到请求了没有如果provider根本没收到请求那问题大概率在consumer端到provider的网络路径上如果provider收到了并且执行很快但consumer还是报超时那问题往往出在provider响应往回传的链路或者provider的线程池已经塞满请求在被服务的executor队列里干等。实际操作中第一步先看consumer端最近的调用耗时统计把“平均响应时间”和“TP99”做对比。如果平均耗时很低但TP99飙高基本可以确定是偶发网络抖动如果平均耗时本身就很高就要怀疑是不是provider处理能力不够。第二步直接看provider所在机器的线程池状态。Dubbo提供了线程池监控dubbo://协议的provider线程池一旦active线程数长期打满说明不是网络慢是provider自己忙不过来。这时候再去调大timeout、调小retries都只是把压力往后挪。网络段的排查建议用持续性的探测工具不要用ping测几个包就下结论而是用tcping或者专门的弱网检测工具跑5分钟以上记录丢包率和延迟分布。我当时就是用这种方式发现跨机房专线的丢包率平时只有0.1%但每到整点会突增到3%持续几十秒正好对应上定时任务和大数据批处理的流量高峰。2.2 无提供者异常排查链注册、发现、订阅三层过滤无提供者异常比超时异常更烧脑因为它看起来像是“没有服务”但实际可能只是“没有你想要的服务的特定版本/分组/机房”。我总结了一个三层过滤的排查顺序第一层是检查注册中心本身。登录Nacos控制台搜一下这个服务名看是否存在providers节点数量是否正常。别只看“服务列表”页面最好直接查看详情确认每个实例的IP、端口、权重、集群名是否符合预期。第二层是检查consumer的订阅条件。很多服务定义了group和version如果consumer的group或version跟provider不一致在注册中心明明能看到服务但consumer就是拿不到节点。第三层是检查路由规则。Dubbo的ConditionRouter、TagRouter、以及我们自己写的路由都会在拿到的provider列表基础上做过滤。多机房环境下最常见的坑就是你配置了一条路由规则把非本机房的节点全部过滤掉但本机房的provider因为某种原因没注册成功最终可用节点直接变成零报错信息就是No provider available。另外还有一个特别容易被忽略的点consumer本地缓存。Dubbo会缓存服务目录和provider列表如果注册中心短暂抖动推送了空列表consumer可能会优先使用缓存或者缓存过期之后重新拉取时拉到了不完整的列表。清理缓存之后能暂时恢复但如果不找出注册中心推送不完整的原因过一会儿又会复发。2.3 Nacos在多机房场景里的特殊“坑位”Nacos做注册中心单机房用起来很顺手但多机房部署时有几个特性必须提前想清楚。第一是namespace。很多人习惯一套环境一个namespace但如果多机房之间需要共享服务consumer和provider就必须用同一个namespace配置错了就会出现“控制台有数据consumer什么都拉不到”的情况。第二是group。group在Dubbo里对应服务分组可以通过dubbo:reference groupxxx指定如果consumer和provider的group不一致同样会报无提供者。第三个坑是Nacos的临时实例和持久化实例。临时实例依赖心跳续约client每隔5秒发一次心跳如果provider所在机器CPU打满或者网络阻塞导致心跳超时Nacos会认为实例不健康并把它摘除。多机房环境下机房之间的Nacos节点如果采用不同集群且数据同步延迟较大就可能出现A机房已经摘除了不健康实例B机房的consumer还拿着旧列表。这个时候超时异常和无提供者异常会交替出现非常迷惑。另外provider注册到Nacos的IP地址也要注意。多机房改造时很多provider机器内网IP段不同如果consumer跨机房访问时发现provider注册的是内网IP但那条内网路由跨机房根本不通异常表现就会从超时演变到无提供者。排查时要重点确认注册IP是不是对端机房可达的IP比如是否注册了公网地址或者专线网段地址。3. Dubbo集群扩展实战用Router、LoadBalance、Cluster解决跨机房难题3.1 Dubbo集群容错与调用链路速览在做集群扩展之前必须先理解Dubbo一次远程调用的完整链路。服务消费者拿到服务目录Directory后会先经过Router做筛选再从筛选结果中用LoadBalance选出最终要调用的节点最后通过Cluster来处理容错逻辑。默认情况下的调用链大致是consumer - Directory - Router - LoadBalance - Cluster - Invoker - providerCluster接口负责的重试、故障转移、失败标记都在这条链路上实现。Dubbo内置了几种Cluster策略每种策略的语义完全不同failover是失败自动切换其他节点重试适合幂等操作failfast是失败立即报错适合非幂等操作failsafe是失败直接吞掉异常返回空结果适合日志上报等场景failback是失败后记录并在后台定时重试forking是同时并发调用多个节点谁先返回就用谁。理解了这条链路你就会发现多机房的问题其实可以做得很优雅Router负责把流量优先路由到同机房节点LoadBalance负责在同机房节点里做均匀分配Cluster负责在同机房节点全部失败时再跨机房调用其他节点并且把超时和重试次数掌握在可控范围内。这三个扩展点组合起来就能解决大部分多机房部署下的超时和无提供者异常。3.2 方案一机房亲和路由负载均衡把流量留在本地多机房部署的第一原则是能用本地机房的服务就不要跨机房调用。跨机房调用的网络开销、故障放大效应都是超时异常的重要来源。实现机房亲和的关键是让每个provider在注册时携带机房信息。最常用的办法是在Nacos的cluster字段上做文章provider启动时配置clusterhz、clustersh或者在URL参数里加上zonehz这样的自定义参数。consumer端自定义一个Router把同机房的节点优先筛选出来。如果本机房节点数量满足要求就只返回本机房的invoker列表如果本机房节点数为零或者已经挂了再把其他机房的节点作为兜底返回。Router实现的关键代码骨架大致是这样的public class ZoneAwareRouter implements Router { Override public T ListInvokerT route(ListInvokerT invokers, URL url, Invocation invocation) { String localZone RpcContext.getContext().getAttachment(zone); if (localZone null || localZone.isEmpty()) { return invokers; } ListInvokerT localInvokers invokers.stream() .filter(invoker - localZone.equals(invoker.getUrl().getParameter(zone))) .collect(Collectors.toList()); return localInvokers.isEmpty() ? invokers : localInvokers; } }注意这里有个设计取舍如果本机房节点为空我选择返回全部invoker而不是空列表。这样做的目的是宁可跨机房调用也不让无提供者异常爆发。这个策略在多数场景下是对的但如果你有严格的机房隔离要求也可以改造为返回空列表再加一层降级逻辑。取舍的关键在于业务能不能接受跨机房调用比如订单支付类接口延迟敏感跨机房的延迟可能直接导致超时那就必须用“宁可降级不要跨机房”的策略。负载均衡侧我建议配合LeastActiveLoadBalance或者自定义负载均衡。因为同机房内部网络好延迟低最怕的是某个provider线程池被打满。LeastActive会根据每个provider当前活跃调用数来做分流活跃少的优先能有效避免流量倾斜。如果你们已经使用了自定义Router建议再写一个自定义LoadBalance优先选择同机房并且活跃数最少的节点两重条件叠加。3.3 方案二自定义Cluster让超时与故障转移变得可控默认的failover集群策略在多机房环境下有个问题它重试时会用同一套超时参数继续尝试其他节点而且重试次数是全局配置。如果本机房节点因为临时故障超时failover会立刻去尝试另一个本机房节点但如果整个机房都出现网络抖动它就会反复在本机房重试反而加重故障。自定义一个机房感知的Cluster可以让故障转移的顺序更合理先重试本机房本机房节点都失败后再跨机房。同时跨机房重试的超时时间要有独立控制避免跨机房调用使用跟本机房一样的超时导致整体等待时间被拉长。Cluster扩展的核心是返回一个自定义Invoker在Invoker的invoke方法中实现集群容错逻辑。大致骨架如下public class ZoneAwareCluster implements Cluster { Override public T InvokerT join(DirectoryT directory) throws RpcException { return new ZoneAwareClusterInvoker(directory); } static class ZoneAwareClusterInvokerT extends AbstractClusterInvokerT { ZoneAwareClusterInvoker(DirectoryT directory) { super(directory); } Override protected Result doInvoke(Invocation invocation, ListInvokerT invokers, LoadBalance loadbalance) throws RpcException { // 1. 先选本机房invoker列表 ListInvokerT localInvokers filterByZone(invokers, RpcContext.getContext().getAttachment(zone)); // 2. 本机房优先调用失败则跨机房重试 try { return doInvokeWithRetry(localInvokers.isEmpty() ? invokers : localInvokers, invocation, loadbalance); } catch (RpcException e) { if (e.isTimeout()) { // 超时异常走跨机房降级重试但只允许一次 return doInvokeWithRetry(retryOtherZone(invokers, localInvokers), invocation, loadbalance); } throw e; } } } }这里的核心逻辑是把“本机房优先”和“失败跨机房兜底”分成两个阶段并且只在超时场景才触发跨机房重试避免普通业务异常被无限放大。实际落地时我建议结合业务幂等性来控制跨机房重试次数非幂等操作不建议开启跨机房重试宁可让调用方感知失败后做业务补偿。3.4 方案三无提供者异常的“最后一公里”兜底即使有了机房亲和路由和自定义Cluster还是可能会出现极端情况provider集体失联、注册中心推送空列表、路由过滤后没有可用节点。这种时候无提供者异常依然会爆发。我的建议是在consumer侧加一道保险mock降级。Dubbo的mock支持force和fail两种前缀。force:return null是直接不调用远程服务本地直接返回空结果fail:return null是远程调用失败后再走本地降级。在多机房场景下对非核心链路我建议使用fail:return降级这样远程优先失败兜底不影响主流程。对核心链路不要轻易mock因为mock很容易掩盖provider故障导致数据不一致。mock的配置也很简单dubbo:reference iduserService interfacecom.example.api.UserService mockfail:return {quot;codequot;:500,quot;msgquot;:quot;service unavailablequot;} /还有一个更轻量的“兜底”思路利用provider启动后的预热检测。很多无提供者异常其实是provider启动后还没来得及注册到Nacosconsumer就已经开始调用。可以给provider配置一个延迟注册参数比如Dubbo的delay配置让provider在Spring容器初始化完成后延迟5秒再注册。这个小参数能避开的无提供者异常比你想的多得多。4. 实战落地配置、代码与验收全记录4.1 核心配置分组、命名空间与关键超时参数实战中我建议先把基础配置夯实再做扩展。第一步是统一Nacos的namespace和group最好由运维强制约束避免出现“A机房用了default groupB机房用了app group”这种低级错误。以下是consumer侧的配置模板你可以直接参考dubbo: registry: address: nacos://nacos-cluster:8848 group: DUBBO_SERVICES parameters: namespace: multi-idc-prod consumer: timeout: 3000 retries: 2 cluster: zoneAware loadbalance: zoneFirst check: false注意check: false这个配置很重要。consumer启动时如果强制检查provider是否存在而当时provider还没完成注册应用启动就会直接失败。多机房场景下建议保持check: false让应用在provider未就绪时也能启动等注册中心推送服务列表后再开始调用。provider侧的超时配置一般指的是服务端执行超时如果provider处理单个请求超过这个时间Dubbo会主动中断。我建议provider的timeout比consumer的timeout略小这样能尽早暴露provider的性能问题而不是让consumer一直傻等。比如consumer设置3000msprovider设置2500ms等于给整个调用链留出了500ms的网络和排队缓冲。4.2 扩展点实现Router、LoadBalance、Cluster的代码骨架把三个扩展点串起来你需要做的不是三个独立的类而是一整套SPI文件。Dubbo的SPI查找机制会从META-INF/dubbo/目录下加载对应的扩展类文件。以Cluster为例需要添加一个文件META-INF/dubbo/org.apache.dubbo.rpc.cluster.Cluster文件内容写到zoneAwarecom.example.dubbo.cluster.ZoneAwareClusterLoadBalance扩展点类似文件是org.apache.dubbo.rpc.cluster.LoadBalance内容写zoneFirstcom.example.dubbo.loadbalance.ZoneFirstLoadBalance自定义LoadBalance的核心逻辑是优先本机房本机房内再按活跃数选择public class ZoneFirstLoadBalance extends AbstractLoadBalance { Override protected T InvokerT doSelect(ListInvokerT invokers, URL url, Invocation invocation) { String localZone RpcContext.getContext().getAttachment(zone); if (localZone null) { return invokers.get(ThreadLocalRandom.current().nextInt(invokers.size())); } ListInvokerT local invokers.stream() .filter(invoker - localZone.equals(invoker.getUrl().getParameter(zone))) .collect(Collectors.toList()); ListInvokerT candidates local.isEmpty() ? invokers : local; // 选活跃数最少的节点 return candidates.stream() .min(Comparator.comparingInt(this::getActive)) .orElse(candidates.get(0)); } }注意getActive这个指标在Dubbo底层是有现成实现的通过RpcStatus.getStatus(invoker.getUrl())可以拿到当前活跃调用数。如果你的版本没有这个API也可以简化实现为随机选择加上权重核心思想不变。Router的SPI文件路径是org.apache.dubbo.rpc.cluster.Router同样需要把自己的Router实现类注册进去。加载SPI文件后在DubboReference或者XML配置中指定clusterzoneAware、loadbalancezoneFirst即可生效。4.3 压测与验证怎么证明这套扩展真的有用扩展做完不能只看代码不验证。我当时的验证分三步走。第一步是用MockProvider模拟多机房节点。在Nacos上注册两批provider分别标记zonehz和zoneshconsumer端通过RpcContext.getContext().setAttachment(zone, hz)模拟本机房调用请求。然后观察调用日志里实际命中的provider地址确认请求确实优先落在hz机房。第二步是故障演练。把hz机房的provider全部kill掉模拟机房整体故障观察consumer是否自动跨机房调用sh机房节点以及无提供者异常是否还会出现。如果扩展写得对流量会在短暂的超时重试后自动切到sh机房整个过程不需要人工干预。第三步是常规压测对比扩展前后的两项核心指标平均响应时间和错误率。压测工具直接用了团队现有的JMeter加自研压测平台压测场景模拟了3倍日常流量。我记录了一组实际数据未加扩展前跨机房错误率约1.2%TP99耗时达到1800ms加扩展后同机房流量占比从68%提升到93%跨机房调用量大幅下降TP99回落到400ms以内。错误率降到0.3%剩下的基本是provider自身的偶发超时。4.4 踩坑实录实际部署中我交过的学费这套方案看着清晰落地过程却是坑坑相连。第一个坑是timeout调得太大导致线程池被hang住。当时我为了降低超时异常把consumer的timeout从1秒调到5秒结果高峰期provider线程池被堆积的请求全部占满新请求进不来表现为“provider活着但没有反应”反而触发了更多的超时和无提供者异常。最后我把timeout调回3秒同时把线程池的queues参数调大才稳下来。第二个坑是自定义Router过滤太狠。初期版本里我一旦发现本机房没有provider就直接返回空列表结果本机房provider发布的时候出现了短暂空窗无提供者异常瞬间刷屏。改成“本机房为空则回退到全部节点”之后这类问题彻底消失。这个经验告诉我任何路由策略都要有兜底不能把“更优”当成“唯一”。第三个坑是Nacos的临时实例心跳与provider线程池的关系。有一回一个流量高峰时段某个provider频繁被Nacos判定为不健康并摘除摘除后consumer拿到的节点列表就少了触发重试重试又打到另一个节点上连锁反应差点拖垮整个机房。后来查发现是provider机器上GC停顿时间过长最长一次Full GC停顿了6秒导致心跳没能及时发送。我们在调整了JVM参数、把心跳超时阈值放宽之后这个连锁故障才消失。5. 多机房异常排查速查表5.1 两种异常的快速对照表为了让大家排查时能少走弯路我把两种异常的核心特征整理成了一张对照表建议直接收藏遇到问题先对着看异常类型典型日志关键词常见根因范围优先排查项超时异常Waiting server-side response timeout by scan timer、Tried N times网络延迟、provider线程池耗尽、consumer阻断1. provider执行时间 2. 网络探测 3. 线程池状态无提供者异常No provider available from registry、Please check if the provider has been started订阅条件不一致、路由过滤、注册中心同步延迟1. Nacos服务列表 2. group/version 3. Router过滤规则多机房特有IP地址跨机房不可达、Nacos集群间数据不一致注册IP问题、namespace/group不统一、临时实例心跳超时1. 注册IP 2. namespace 3. 心跳日志时间有限的话超时异常优先查provider性能无提供者异常优先查consumer的订阅和路由。查看的时候记得带上时间轴很多异常其实是同一个根因在不同时点的不同表现。5.2 高频场景排查清单如果你们的多机房环境里超时异常和无提供者异常交替出现按照下面的排查清单走一遍基本能覆盖90%的情况确认consumer和provider使用了同一个Nacos namespace和group先排除最基础的配置错位。登录Nacos控制台检查目标服务的provider实例健康状态重点看不健康实例数和摘除时间。检查provider注册到Nacos的IP是否为跨机房可达的IP可以用一条类似的命令从consumer所在机器直连测试telnet providerIP 20880或nc -vz providerIP 20880。检查consumer的URL参数里是否误配置了router、cluster、loadbalance路由规则会把provider列表过滤成空。检查provider是否配置了delay延迟注册避免服务启动时尚未注册就被consumer调用。检查consumer端是否有本地缓存重启consumer前先确认是否能通过重新订阅获取到完整列表。观察跨机房专线的持续丢包率和延迟曲线不要只测一次至少持续5分钟以上。把这个清单做成自动化巡检脚本挂到监控平台上每天跑一次能省下大量排查时间。多机房这套东西难点从来不在Dubbo本身而在于你想清楚流量到底该怎么走。只要流量路径想清楚了后面的扩展点都是明牌照着思路撸代码就行。
返回列表