当前位置:首页 > 公众号精选 > 架构师社区
[导读]由于线上具体异常包含信息量过大,秉承让肥朝的粉丝没有难调试的代码的原则,我特意抽取了一个复现的demo放在了git,让你不在现场,一样享受到排查的快乐!但是最近,太多假粉伸手党拿到地址就跑,因此我把地址藏在本文某个角落,因此认真看文的才能找到!

前 言

直入主题,线上应用发现,偶发性出现如下异常日志

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

当然由于线上具体异常包含信息量过大,秉承让肥朝的粉丝没有难调试的代码的原则,我特意抽取了一个复现的demo放在了git,让你不在现场,一样享受到排查的快乐!但是最近,太多假粉伸手党拿到地址就跑,因此我把地址藏在本文某个角落,因此认真看文的才能找到!(重点)

又踩到Dubbo的坑,但是这次我笑不出来

由于工作性质的原因,上班时间根本抽不出时间做其他事,修bug,都只能下班时间来做,因此周六就到公司搬砖了。

又踩到Dubbo的坑,但是这次我笑不出来

什么是ConcurrentModificationException?

中文意思就是,并发修改异常。也就是我们常说的fail-fast(快速失败)。当然肥朝更认为,快速失败是一种思想,比如Spring会在启动的时候做大量的检查,什么bean找不到,依赖注入错误等等,都会把一些显而易见的错误检查出来,防止在项目跑着跑着期间再失败,也就是提前检查。无论是业务开发,还是基础组件开发,亦或是生活中,这个思想都是可以用到的。

那么,言归正传,这个异常到底什么意思啊。简单说就是,当一个集合在遍历的时候,他的元素也正在被修改。刚学java那会,我们边遍历边删除就会出现这个异常。ConcurrentModificationException的原理这些网上太多,肥朝就暂且不提。那么我们来看下异常栈。

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

好了,我们已经找到了RpcContext.getContext().getObjectAttachments()正在遍历。那么,只要找到谁在修改他就行了啊,就这?

又踩到Dubbo的坑,但是这次我笑不出来

难点分析

很明显,这里面并不存在遍历的同时修改元素,Dubbo的代码还不至于有这个明显的bug。出现ConcurrentModificationException,就有可能是,A线程在遍历,B线程在修改。

但是肥朝,你说了这么多,我还是没发现这个问题有什么难的啊!

这个问题难点主要在于,在Dubbo里面,RpcContext是对应一个线程的,你可以简单理解为ThreadLocal的增强版。也就是说,A线程拿出来的,和B线程拿出来的RpcContext都不是同一个,何来并发修改同一个之说?当然官方文档给了我一个启示

又踩到Dubbo的坑,但是这次我笑不出来

会不会有同学在线程开启前拿到RpcContext,然后在新线程中,做set操作(图中的get操作是没有问题的)。

又踩到Dubbo的坑,但是这次我笑不出来

于是,似乎豁然开朗的我,顺着这条线索,周六加了一天班,把代码翻了个遍,最后发现没有找到。

又踩到Dubbo的坑,但是这次我笑不出来

索然无味还是柳暗花明?

并发这东西,要么不出问题,一旦出问题都是很难找。观察了线上日志,重现概率很小,就一小段日志,并且业务方很忙,也没时间配合你查问题。于是只能顺着源码,把Dubbo的整个请求到响应的过程在脑海中快速过几遍,看看哪个环节有可能出问题,做了无数的假设。随着一次次的假设失败,在即将身体索然无味之际,还真发现了一些蛛丝马迹!(注意,本文所用到的,都是dubbo2.7.6)

我们先来看一下官方文档对RpcContext的介绍

又踩到Dubbo的坑,但是这次我笑不出来

好了,那么我问你,下面这段代码,love能输出什么?

@Service
public class AHelloServiceImpl implements AHelloService {

    @Reference
    private BHelloService bHelloService;

    @Override
    public String sayHello() throws Exception{

        RpcContext.getContext().setAttachment("我最爱的人是?","肥朝");
        bHelloService.sayHello();
        String love = RpcContext.getContext().getAttachment("我最爱的人是?");
        System.out.println("this is: " + love);
        Thread.sleep(10L);

        bHelloService.sayHello();

        return "欢迎关注微信公众号:肥朝";
    }
}

我在图都圈得这么明显了,看得懂中文都知道,发起一次远程调用后,参数会被清空,下面肯定get不到的啦。但是其实是get得到的,不要问肥朝为什么都知道图是有问题的,还特意圈起来骗你,我只想让你知道社会险恶。

源码细节

阅读过源码,和对源码有细节深入思考,效果是很大不一样的。

我们来看一下源码就知道了。文中说的会清除,对应的代码是怎么样的呢?

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

如果作为正常的客户端调用,那么,在调用后确实是会删除的。但是如果你对源码细节足够熟悉你就会发现,在org.apache.dubbo.rpc.filter.ContextFilter这个类中

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

你不看代码直接听我说也行,这几段代码的意思是,在一个提供者的方法中,canRemove会设置为false的,所以,他们在这个方法体远程调用中,是没办法清空RpcContext的,需要在整体调用完才会清空。

我们再回顾一下案发现场

@Override
public String sayHello() throws Exception{

    bHelloService.sayHello();
    Thread.sleep(10L);
    bHelloService.sayHello();

    return "欢迎关注微信公众号:肥朝";
}

从目前得到的信息很明显知道,第一次远程调用,和第二次远程调用,用的是同一个RpcContext,并且,在第二次远程调用的时候。这个RpcContext的内容,给人动了手脚了。

那么,究竟是何人所为!我们随着镜头,再次深入源码!既然是RpcContext给人搞了,那么我们就从这里顺藤摸瓜,这里先省略肥朝的内心戏,我们来看重点。在RpcContext中发现一段可疑片段

public static void restoreContext(RpcContext oldContext) {
    LOCAL.set(oldContext);
}

接着继续顺丰摸瓜,发现调用这段代码的逻辑是

/**
 * tmp context to use when the thread switch to Dubbo thread.
 */

private RpcContext tmpContext;

private RpcContext tmpServerContext;
private BiConsumer beforeContext = (appResponse, t) -> {
    tmpContext = RpcContext.getContext();
    tmpServerContext = RpcContext.getServerContext();
    RpcContext.restoreContext(storedContext);
    RpcContext.restoreServerContext(storedServerContext);
};

private BiConsumer afterContext = (appResponse, t) -> {
    RpcContext.restoreContext(tmpContext);
    RpcContext.restoreServerContext(tmpServerContext);
};
public Result whenCompleteWithContext(BiConsumer fn) {
    this.responseFuture = this.responseFuture.whenComplete((v, t) -> {
        beforeContext.accept(v, t);
        fn.accept(v, t);
        afterContext.accept(v, t);
    });
    return this;
}
@Override
public Result invoke(Invocation invocation) throws RpcException {
    Result asyncResult;
    try {
        interceptor.before(next, invocation);
        asyncResult = interceptor.intercept(next, invocation);
    } catch (Exception e) {
        // onError callback
        if (interceptor instanceof ClusterInterceptor.Listener) {
            ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
            listener.onError(e, clusterInvoker, invocation);
        }
        throw e;
    } finally {
        interceptor.after(next, invocation);
    }
    return asyncResult.whenCompleteWithContext((r, t) -> {
        // onResponse callback
        if (interceptor instanceof ClusterInterceptor.Listener) {
            ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
            if (t == null) {
                listener.onMessage(r, clusterInvoker, invocation);
            } else {
                listener.onError(t, clusterInvoker, invocation);
            }
        }
    });
}

看不懂代码不要怕,肥朝大白话解释一下。你就想象一个Dubbo异步场景,Dubbo异步回调结果的时候,是会开启一个新的线程,那么,这个回调就和当初请求不在一个线程里面了,因此这个回调线程是拿不到当初请求的RpcContext。但是我们清空RpcContext是需要在一次请求结束的时候,也就是说,虽然异步回调是另外一个线程了,但是我们仍然需要拿到当初请求时候的RpcContext来走Filter,做清空等操作。上面那段代码就是做,切换线程怎么拿回之前的RpcContext

听完上面的分析,你是不是明白了点啥?新线程,还能拿到旧的RpcContext。那么,有这么一个场景,我们在通过提供者方法中,发起两个异步请求,第一个请求走FilteronResponse(响应结果)的时候,我们如果在FilterRpcContext.getContext().setAttachment操作,第二个请求又正好发起,而发起又会经历putAll这步骤,就会出现这个并发修改异常。于是乎,真相大白!

具体详情,亲自调试一番就会清楚,肥朝公众号回复modification获取git地址

拓展性思考

真相大白就结束了?熟悉肥朝的粉丝都知道,我们遇到问题,要尽量压榨问题的全部价值!比如,你说不要在拦截器中onResponse方法中用RpcContext.getContext().setAttachment这样的操作,但是我们确实有类似需要,那到底要怎么写代码又不说,你这样叫我怎么给你转发文章!

又踩到Dubbo的坑,但是这次我笑不出来

我们要知道怎么正确写代码,那直接去抄Dubbo其他拦截器的代码不就知道了?比如

@Activate(group = PROVIDER, order = -10000)
public class ContextFilter implements FilterFilter.Listener {


    @Override
    public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) {
        // pass attachments to result
        appResponse.addObjectAttachments(RpcContext.getServerContext().getObjectAttachments());
    }

}

我们很明显看到,你熟悉一下appResponse的api和他的作用,就很容易知道,有类似需求,代码应该怎么写了。我光告诉你怎么写代码没用啊,我要告诉你,遇到问题,怎么去抄正确代码,让你任何时候,都有得cao!


特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

又踩到Dubbo的坑,但是这次我笑不出来

长按订阅更多精彩▼

又踩到Dubbo的坑,但是这次我笑不出来

如有收获,点个在看,诚挚感谢

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭