😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法
等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!
目录
- 题目
- 答案
题目
💬技术栈:Dubbo
🔍简历内容:独立定制Cluster扩展解决同机房请求无法连通。
🚩面试问:某公司有一个多机房部署的系统,线上运行一直比较稳定,但最近,部分流量请求先出现超时异常,紧接着出现无提供者异常,最后部分功能不可用。怎么办?
(1)超时异常日志:
Caused by: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2022-10-25 20:14:16.718, end time: 2022-10-25 20:14:16.747, client elapsed: 1 ms, server elapsed: 28 ms, timeout: 5 ms, request: Request [id=2, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[Geek], attachments={path=com.hmilyylimh.cloud.facade.demo.DemoFacade, remote.application=dubbo-04-api-boot-consumer, interface=com.hmilyylimh.cloud.facade.demo.DemoFacade, version=0.0.0, timeout=5}]], channel: /192.168.100.183:62231 -> /192.168.100.183:28043at org.apache.dubbo.remoting.exchange.support.DefaultFuture.doReceived(DefaultFuture.java:212)at org.apache.dubbo.remoting.exchange.support.DefaultFuture.received(DefaultFuture.java:176)at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.notifyTimeout(DefaultFuture.java:295)at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.lambda$run$0(DefaultFuture.java:282)at org.apache.dubbo.common.threadpool.ThreadlessExecutor$RunnableWrapper.run(ThreadlessExecutor.java:184)at org.apache.dubbo.common.threadpool.ThreadlessExecutor.waitAndDrain(ThreadlessExecutor.java:103)at org.apache.dubbo.rpc.AsyncRpcResult.get(AsyncRpcResult.java:193)... 29 more
(2)无提供者异常和原因
org.apache.dubbo.rpc.RpcException: Failed to invoke the method sayHello in the service com.hmilyylimh.cloud.facade.demo.DemoFacade. No provider available for the service com.hmilyylimh.cloud.facade.demo.DemoFacade from registry 127.0.0.1:2181 on the consumer 192.168.100.183 using the dubbo version 3.0.7. Please check if the providers have been started and registered.at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.checkInvokers(AbstractClusterInvoker.java:366)at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:73)at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:340)at org.apache.dubbo.rpc.cluster.router.RouterSnapshotFilter.invoke(RouterSnapshotFilter.java:46)at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:321)at org.apache.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:99)... 48 more
💡建议暂停思考10s,你有答案了嘛?如果你有不同题解,欢迎评论区留言、打卡。
答案
1.从第一条关键异常信息中看到有一个 Caused by 引发的 TimeoutException 异常类,认为发生了超时异常,但单从消费方无法定位真实原因。于是开始求证目标IP的健康状况
。
Caused by: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response...
2.小航先在 CAT 上发现了目标 IP 在出问题期间几乎没有任何流量进来
,在 Prometheus 上发现在最近一段时间内 TCP 的连接耗时特别大,基本上都是有 SYN 请求握手包,但是没有 SYN ACK 响应包
。然后又找网络人员帮忙实时 tcpdump 抓包测试,结果仍然发现没有 SYN ACK 响应包
。好了,可以确认目标 IP 处于不可连通的状态,接着小航去开始确认目标 IP服务是宕机了还是流量被拦截。
3.继续查看日志:
org.apache.dubbo.rpc.RpcException: Failed to invoke the method sayHello…No provider available for the service … from registry 127.0.0.1:2181…
从第二条关键异常信息中,No provider available
,显然找不到服务提供者,继续定位堆栈信息:
at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.checkInvokers(AbstractClusterInvoker.java:366)
at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:73)
对应代码:
protected void checkInvokers(List<Invoker<T>> invokers, Invocation invocation) {// 检查传入的 invokers 服务提供者列表,若集合为空,则会抛出无提供者异常if (CollectionUtils.isEmpty(invokers)) {// 抛出的 RpcException 异常信息中,会有 No provider available 明显的关键字throw new RpcException(RpcException.NO_INVOKER_AVAILABLE_AFTER_FILTER, "Failed to invoke the method "+ invocation.getMethodName() + " in the service " + getInterface().getName()+ ". No provider available for the service " + getDirectory().getConsumerUrl().getServiceKey()+ " from registry " + getDirectory().getUrl().getAddress()+ " on the consumer " + NetUtils.getLocalHost()+ " using the dubbo version " + Version.getVersion()+ ". Please check if the providers have been started and registered.");}
}
定位代码后推断:消费方在内存中找不到对应的提供者,才会提示无提供者异常
。
继续分析checkInvokers[invokers 列表] => FailoverClusterInvoker => FailoverCluster => cluster = “failover” => 消费方订阅 => 消费方启动或注册中心节点变更会更新invokers列表源数据 => 源数据为什么更新没了 => 消费方已经处于运行状态,只是在调用的时候发生了无提供者异常 => 注册中心的节点发生了变更。
貌似找到了问题所在,小航重新正向复盘:节点宕机,造成提供方节点与注册中心断开心跳连接 => 注册中心会删掉提供方的IP节点 => 消费方感知到注册中心的节点发生变更 => 更新消费方本地的源数据信息 => 消费方在 checkInvokers 中发现 invokers 为空。
4.Ip一些正常一些不正常,首先排除人为恶搞因素,小航思索良久,和公司网工小李探讨了一番,得知它们全都是同一个机房的,最终问题定位到:机房A的某些提供者宕机了,且机房A的消费者状态正常,但把机房A的消费者拉下水导致各种功能无法正常运转
。
5.核心问题定位后,小航同志开始制作解决方案:机房之间防火墙彻底隔离,同机房已经调用不通,防止消费方硬着头皮一直从宕机的服务拿到响应内容。现在的应用大多数都是数十秒才启动成功,显然同机房请求死路一条,那么消费者的请求发给谁才能通呢?显然:中间商。
于是小航同志开始设计技术方案:消费方遇到无提供者异常后 => 调用转发服务 => 转发服务找可用机房的可用提供者并发起调用并拿到结果
。貌似可行。不过为了避免这段代码,需要给每个消费者写,小航提炼了一个公共插件
,既做到了代码公用只维护一份代码,又做到了对应用的非侵入特性,一举两得。
6.结果新的问题又来了,该在调用的哪个环节进行转发服务的处理呢?小航同志琢磨了一下,哪里出错,就从哪里开始,要调用转发服务,就需要先捕获到无提供者异常
,翻阅源码:FailoverClusterInvoker#doInvoke => checkInvokers被 protected 修饰 => 可以被子类重写 => dubbo提供了一个检测 invokers 是否可用的机制。小航同志于是一顿操作:定义TransferClusterInvoker 类 => 继承 FailoverClusterInvoker => 重写 checkInvokers
7.CR很快就驳回来了,checkInvokers 只是意在让你检测
,与设计意图不符。
8.小航又重新更换思路:FailoverClusterInvoker => doInvoke被 public 修饰 => 在子类 TransferClusterInvoker 中调用父类 FailoverClusterInvoker 的 doInvoke 方法
=> 进行 try…catch… 捕获精准异常 => 既不会破坏设计者的意图,还能精准处理无提供者异常后转发调用。
public class TransferClusterInvoker<T> extends FailoverClusterInvoker<T> {// 按照父类 FailoverClusterInvoker 要求创建的构造方法public TransferClusterInvoker(Directory<T> directory) {super(directory);}// 重写父类 doInvoke 发起远程调用的接口@Overridepublic Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {try {// 先完全按照父类的业务逻辑调用处理,无异常则直接将结果返回return super.doInvoke(invocation, invokers, loadbalance);} catch (RpcException e) {// 这里就进入了 RpcException 处理逻辑// 当调用发现无提供者异常描述信息时则向转发服务发起调用if (e.getMessage().toLowerCase().contains("no provider available")){// TODO 从 invocation 中拿到所有的参数,然后再处理调用转发服务的逻辑return doTransferInvoke(invocation);}// 如果不是无提供者异常,则不做任何处理,异常该怎么抛就怎么抛throw e;}}
}
9.核心逻辑已实现,接下来就是代码中触发TransferClusterInvoker,参考 FailoverCluster的编码 => 定义TransferCluster类 => 实现Cluster接口 => 创建 TransferClusterInvoker处理调用至转发服务器 => Dubbo SPI配置。
public class TransferCluster implements Cluster {// 返回自定义的 Invoker 调用器@Overridepublic <T> Invoker<T> join(Directory<T> directory, boolean buildFilterChain) throws RpcException {return new TransferClusterInvoker<T>(directory);}
}
10.下班
🔨总结梳理:Cluster作为路由层,封装多个提供方的路由及负载均衡,并桥接注册中心以 Invoker 为中心发起调用,哪些应用场景可以考虑集群扩展呢?
(1)同机房请求无法连通时,可以考虑转发HTTP请求至可用提供者。
(2)内网本机访问测试环境无法连通时,可以转发请求至HTTP协议的接口,然后在接口中泛化调用各种Dubbo服务。
(3)如果针对接口的多个提供者需要做适应当前公司业务的筛选、剔除、负载均衡之类的诉求时,也是可以考虑集群扩展的。
总之,不管是无提供者问题,还是公司特殊定制化筛选负载问题,核心都是在针对接口的所有提供者做逻辑处理,提供者为空做转发兼容处理,提供者不为空做特殊筛选负载处理,目的都是在调用时做一种容错兼容处理,让应用程序更加健壮稳定。
📌 [ 笔者 ] 文艺倾年
📃 [ 更新 ] 2025.4.27
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!