Redian新闻
>
一次 Dubbo 线程上下文类加载器的疑难杂症分析

一次 Dubbo 线程上下文类加载器的疑难杂症分析

科技

问题环境

dubbo:2.7.18
java:java8

问题背景

有业务(Java)的同学反馈,在接入了 devops 的某些 javaagent 以后会极大概率出现 dubbo 调用失败,dubbo 接口中用到的业务类都提示找不到,导致反序列化失败,部分日志输出如下:

[2023-02-09 19:22:58.982][][][DubboServerHandler-172.30.86.136:20880-thread-2][WARN][com.alibaba.com.caucho.hessian.io.SerializerFactory:686] Hessian/Burlap: 'com.seewo.kishframework.page.PageRequest' is an unknown class in org.springframework.boot.loader.LaunchedURLClassLoader@66373f77:
java.lang.ClassNotFoundException: com.seewo.kishframework.page.PageRequest

但是通过内存查看,com.seewo.kishframework.page.PageRequest 等失败的类都已经被org.springframework.boot.loader.LaunchedURLClassLoader@66373f77 加载。

离了个大谱。

源码初步分析

异常日志是在 com.alibaba.com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.String) 这个函数中

try 代码中首先通过 loadSerializedClass 加载待反序列化的类,然后用这个 classloader 获取这个类对应的反序列化对象 deserializer。

loadSerializedClass 函数首先调用 getClassFactory 获取单例的 _classFactory,然后使用这个单例就加载类。

这里的 ClassFactory 创建的时候传入的 classloader 是当前线程的 ContextClassLoader。

public class Hessian2SerializerFactory extends SerializerFactory {

    public Hessian2SerializerFactory() {
    }

    @Override
    public ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

}

在首次初始化以后,反序列的类都是由此 SerializerFactory 加载。通过内存分析,可以看到,ClassFactory 的 classloader 居然是 sun.misc.Launcher$AppClassLoader,这个 classloader 的 classpath 是不包含 springboot 管理的 BOOT-INF 下的包的,导致这个 AppClassLoader 自然加载不到对应的类。

因此经过分析,之前日志里输出是误导的。

Hessian/Burlap: 'com.seewo.kishframework.page.PageRequest' is an unknown class in org.springframework.boot.loader.LaunchedURLClassLoader@66373f77:
java.lang.ClassNotFoundException: com.seewo.kishframework.page.PageRequest

这里显示是用 org.springframework.boot.loader.LaunchedURLClassLoader 去加载类找不到,是完全迷惑的,压根就不是用这个类加载器去加载的,而是用 ClassFactory 中的 _loader 。

现在就要搞清楚,为什么 ClassFactory 中的 _loader 会不对。

debug 阶段

通过修改 hessian-lite 的源码,重新替换对应的类,加入了堆栈

看下是谁第一次调用导致了懒加载单例的初始化,第一次调用的堆栈如下:

可以看到 DubboServerHandler-172.30.86.136-thread-2 这个线程的 ContextClassLoader 确实不知为何被设置为了sun.misc.Launcher$AppClassLoader,然后看更多日志,发现除了这个线程其它的 DubboServerHandler 线程的 ContextClassLoader 都是正常的。

这下问题思路基本上清晰了。因为 DubboServerHandler-172.30.86.136-thread-2 这个线程的 ContextClassLoader 是 sun.misc.Launcher$AppClassLoader,这线程最早调用 com.alibaba.com.caucho.hessian.io.SerializerFactory#getClassFactory 导致 SerializerFactory 的 _loader 被赋值为了 sun.misc.Launcher$AppClassLoader,后面的线程也没有机会对单例的 SerializerFactory 重新赋值。

接下来就是分析,最早的线程 DubboServerHandler-172.30.86.136-thread-2 是由谁创建的。又因为业务反馈他们去掉 devops 的 dubbo 健康检查 javaagent 以后,就没有再出现,于是把焦点放在了这个插件上。

这个插件的功能也很简单,就是监听一个端口,收到健康检查的请求以后,提交一个 Echo 任务到 dubbo 的任务池里,如果任务池没有被占满,可以被执行,那么表示 dubbo 健康,否则 dubbo 线程池已经被占满,表示不健康。

EchoTask 就是一个 Runnable,里面啥都没做

上面的 ThreadPoolExecutor 是通过字节码注入的方式从 dubbo 框架中获取的

问题就出在这个健康检查 javaagent 往 dubbo 的线程池提交的这个任务。为了弄清楚这个问题,需要一点点🤏🏻 Java ContextClassLoader 的知识。

线程上下文类加载器由继承自父线程。如果父线程没有设置上下文类加载器,则线程将继承类加载器的默认实现。线程 Thread 创建的代码如下:

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals)
 
{
    this.name = name;
    Thread parent = currentThread();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
}

往线程池里执行一个 runnable 的过程,就是调用 addWorker 创建线程并执行

public class ThreadPoolExecutor extends AbstractExecutorService {

    public void execute(Runnable command) {
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true)) // 创建并执行线程
                return;
            c = ctl.get();
        }
    }
    private boolean addWorker(Runnable firstTask, boolean core) {
        Worker w = null;
        
        w = new Worker(firstTask); // 创建 worker
        final Thread t = w.thread;
        t.start(); // 启动线程
        return true;
  }
}

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable 
{
        
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this); // 创建线程
        }
}            

dubbo 的 ThreadFactory 是 com.alibaba.dubbo.common.utils.NamedThreadFactory,里面构造了线程名。

public Thread newThread(Runnable runnable)
{
 String name = mPrefix + mThreadNum.getAndIncrement();
    Thread ret = new Thread(mGroup,runnable,name,0);
    ret.setDaemon(mDaemo);
    return ret;
}

从上面的代码可以分析出,创建的 DubboServerHandler 线程上下文类加载器,继承调用 new Thread 的父线程上下文类加载器。

可以用最简单的一个 demo 来看验证上面的结论。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyRunnable implements Runnable {

    public void run() {
        System.out.println("TCCL:" + Thread.currentThread().getContextClassLoader());
    }
}

class MyClassLoader extends ClassLoader {

}
public class Test01 {

    public static void main(String[] args) {
        Thread.currentThread().setContextClassLoader(new MyClassLoader());
        ExecutorService executors = Executors.newFixedThreadPool(100);
        executors.execute(new MyRunnable()) ;
    }
}


上面的代码输出

输出
TCCL:MyClassLoader@48140564

通过注入 org.apache.dubbo.common.threadlocal.NamedInternalThreadFactory.newThread 可以看到这个方法创建了 DubboServerHandler-172.30.102.71:20880-thread-1 线程。

于是这里的情况就很清楚了,因为 dubbo-health 这个 javaagent 是由 sun.misc.Launcher$AppClassLoader 加载的,其执行的线程上下文类加载器也是 sun.misc.Launcher$AppClassLoader,导致项目启动以后,k8s 不停发起健康检查触发了 javaagent 向 dubbo 线程池提交任务,导致了头几个 DubboServerHandler 线程(DubboServerHandler-xx-thread-1、DubboServerHandler-xx-thread-2等)的上下文成为了 sun.misc.Launcher$AppClassLoader

当真实流量进来,被分派到头几个 DubboServerHandler 处理,此时它第一个调用 SerializerFactory 的 getClassFactory 导致 ClassFactory 的 _loader 被设置为了 sun.misc.Launcher$AppClassLoader,这个单例开始继续祸害了。

如果初始状态往 dubbo 线程池提交任务是一个很危险的事情,一定要保障线程的上下文是正确的,不然就悲剧了。

修改方法

这里修改方法也简单,只需要将提交任务的时候把自己 javaagent 的 classloader 切换为业务的 classloader 即可。

后记

发现旧版本的 dubbo 没有这个问题,是因为它是用 SerializerFactory 的 _loader,而不是 ClassFactory 中的 _loader

至于为什么,我就不想知道了。


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
來楚生册页小品DuckDuckGo宣布阻止所有Google登录弹框来来来,看看芝加哥Downtown的退休房如何优雅限制 Kubernetes 集群中文件描述符与线程数量【原创】从王沪宁的文革学历说开去用这4招 优雅的实现Spring Boot 异步线程间数据传递China’s Holiday Box Office Sees a Subdued Opening未来加载中XBB毒株迅猛来袭,XBB和XBB.1.5到底是什么?各国的传播情况如何?图片迟迟加载不了、一片马赛克?谷歌开源模型优先显示图像受关注部分拜登政府如惊弓之鸟!F-22击落的疑似民间爱好者的玩具气球,40万美元的导弹击落12美元的气球世界五大疑难杂症,排第一的竟然不是癌症?可重复使用航天运载器发展历程TruEdu Book Club & Writing Class 添加新的课程时间啦Java老掉牙的面试问题:线程交替打印问题,分析实操一下​外贸老板的疑惑,订单都 TMD 去哪儿了?已发现16例本土XBB病例!中疾控称3个月内XBB变异株大规模流行的可能性极低!清华发现纳米抗体,对XBB、BF.7等均保持活性过年必读的家庭疑难杂症问答集SpringBoot 使用线程池如何控制主线程和子线程的事务清北微软深挖GPT,把上下文学习整明白了!和微调基本一致,只是参数没变而已Jeff 蒋浩:家居疑难杂症的“专科医生”AI赋能视频译制,微软和人大提出自动视频译制技术VideoDubber中国推动解决国际治理的“疑难杂症”积木沙发 – LOVESAVXBB毒株是什么?“阳康”后还会感染XBB毒株吗?关于XBB毒株,这些事情你需要知道!Go 语言体系下的微服务框架选型: Dubbo-go我们为什么要请教练使用 ChatGPT 解决我的疑惑,附带注册方法。什么是In-Context Learning(上下文学习)?估值超73亿元,创新药物研发商科伦博泰晋身独角兽;GPU独角兽摩尔线程B轮融资15亿元丨睿兽分析投融资周报中国开源社区健康案例——Apache Dubbo社区阿里云盘致歉:系统故障,全平台无法加载内容RBC、BMO官宣:今年,只要这几类加拿大留学生!重新审视Prompt优化问题,预测偏差让语言模型上下文学习更强美国黑心公司雇佣102个童工,用化学药剂清洗肉类加工设备!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。