动态线程池(DynamicTp),动态调整Tomcat、Jetty、Undertow线程池参数篇

大家好,这篇文章我们来介绍下动态线程池框架(DynamicTp)的adapter模块,上篇文章也大概介绍过了,该模块主要是用来适配一些第三方组件的线程池管理,让第三方组件内置的线程池也能享受到动态参数调整,监控告警这些增强功能。


DynamicTp项目地址

目前500多star,感谢你的star,欢迎pr,业务之余给开源贡献一份力量

gitee地址https://gitee.com/yanhom/dynamic-tp

github地址https://github.com/lyh200/dynamic-tp


系列文章

美团动态线程池实践思路,开源了

动态线程池框架(DynamicTp),监控及源码解析篇


adapter已接入组件

adapter模块目前已经接入了SpringBoot内置的三大WebServer(Tomcat、Jetty、Undertow)的线程池管理,实现层面也是和核心模块做了解耦,利用spring的事件机制进行通知监听处理。

[图片上传失败...(image-de8e85-1650294498945)]

可以看出有两个监听器

  1. 当监听到配置中心配置变更时,在更新我们项目内部线程池后会发布一个RefreshEvent事件,DtpWebRefreshListener监听到该事件后会去更新对应WebServer的线程池参数。

  2. 同样监控告警也是如此,在DtpMonitor中执行监控任务时会发布CollectEvent事件,DtpWebCollectListener监听到该事件后会去采集相应WebServer的线程池指标数据。

要想去管理第三方组件的线程池,首先肯定要对这些组件有一定的熟悉度,了解整个请求的一个处理过程,找到对应处理请求的线程池,这些线程池不一定是JUC包下的ThreadPoolExecutor类,也可能是组件自己实现的线程池,但是基本原理都差不多。

Tomcat、Jetty、Undertow这三个都是这样,他们并没有直接使用JUC提供的线程池实现,而是自己实现了一套,或者扩展了JUC的实现;翻源码找到相应的线程池后,然后看有没有暴露public方法供我们调用获取,如果没有就需要考虑通过反射来拿了。


Tomcat内部线程池的实现

  • Tomcat内部线程池没有直接使用JUC下的ThreadPoolExecutor,而是选择继承JUC下的Executor体系类,然后重写execute()等方法,不同版本有差异。

1.继承JUC原生ThreadPoolExecutor(9.0.50版本及以下),并覆写了一些方法,主要execute()和afterExecute()

2.继承JUC的AbstractExecutorService(9.0.51版本及以上),代码基本是拷贝JUC的ThreadPoolExecutor,也相应的微调了execute()方法

注意Tomcat实现的线程池类名称也叫ThreadPoolExecutor,名字跟JUC下的是一样的,Tomcat的ThreadPoolExecutor类execute()方法如下:

public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            if (super.getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue)super.getQueue();
                try {
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }

        }
    }

可以看出他是先调用父类的execute()方法,然后捕获RejectedExecutionException异常,再去判断如果任务队列类型是TaskQueue,则尝试将任务添加到任务队列中,如果添加失败,证明队列已满,然后再执行拒绝策略,此处submittedCount是一个原子变量,记录提交到此线程池但未执行完成的任务数(主要在下面要提到的TaskQueue队列的offer()方法用),为什么要这样设计呢?继续往下看!

  • Tomcat定义了阻塞队列TaskQueue继承自LinkedBlockingQueue,该队列主要重写了offer()方法。
 @Override
    public boolean offer(Runnable o) {
        //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

可以看到他在入队之前做了几个判断,这里的parent就是所属的线程池对象

1.如果parent为null,直接调用父类offer方法入队

2.如果当前线程数等于最大线程数,则直接调用父类offer()方法入队

3.如果当前未执行的任务数量小于等于当前线程数,仔细思考下,是不是说明有空闲的线程呢,那么直接调用父类offer()入队后就马上有线程去执行它

4.如果当前线程数小于最大线程数量,则直接返回false,然后回到JUC线程池的执行流程回想下,是不是就去添加新线程去执行任务了呢

5.其他情况都直接入队

  • 因为Tomcat线程池主要是来做IO任务的,做这一切的目的主要也是为了以最小代价的改动更好的支持IO密集型的场景,JUC自带的线程池主要是适合于CPU密集型的场景,可以回想一下JUC原生线程池ThreadPoolExecutor#execute()方法的执行流程

1.判断如果当前线程数小于核心线程池,则新建一个线程来处理提交的任务

2.如果当前线程数大于核心线程数且队列没满,则将任务放入任务队列等待执行

3.如果当前当前线程池数大于核心线程池,小于最大线程数,且任务队列已满,则创建新的线程执行提交的任务

4.如果当前线程数等于最大线程数,且队列已满,则拒绝该任务

可以看出当当前线程数大于核心线程数时,JUC原生线程池首先是把任务放到队列里等待执行,而不是先创建线程执行。

如果Tomcat接收的请求数量大于核心线程数,请求就会被放到队列中,等待核心线程处理,这样会降低请求的总体处理速度,所以Tomcat并没有使用JUC原生线程池,利用TaskQueue的offer()方法巧妙的修改了JUC线程池的执行流程,改写后Tomcat线程池执行流程如下:

1.判断如果当前线程数小于核心线程池,则新建一个线程来处理提交的任务

2.如果当前当前线程池数大于核心线程池,小于最大线程数,则创建新的线程执行提交的任务

3.如果当前线程数等于最大线程数,则将任务放入任务队列等待执行

4.如果队列已满,则执行拒绝策略

  • Tomcat核心线程池有对应的获取方法,获取方式如下
    public Executor doGetTp(WebServer webServer) {
        TomcatWebServer tomcatWebServer = (TomcatWebServer) webServer;
        return tomcatWebServer.getTomcat().getConnector().getProtocolHandler().getExecutor();
    }
  • 想要动态调整Tomcat线程池的线程参数,可以在引入DynamicTp依赖后,在配置文件中添加以下配置就行,参数名称也是和SpringBoot提供的Properties配置类参数相同,配置文件完整示例看项目readme介绍
spring:
  dynamic:
    tp:
      // 其他配置项
      tomcatTp:       # tomcat web server线程池配置
        minSpare: 100   # 核心线程数
        max: 400        # 最大线程数

Tomcat线程池就介绍到这里吧,通过以上的一些介绍想必大家对Tomcat线程池执行任务的流程都很清楚了吧。


Jetty内部线程池的实现

  • Jetty内部线程池,定义了一个继承自Executor的ThreadPool顶级接口,实现类有以下几个

[图片上传失败...(image-840b70-1650294498945)]

  • 内部主要使用QueuedThreadPool这个实现类,该线程池执行流程就不在详细解读了,感兴趣的可以自己去看源码,核心思想都差不多,围绕核心线程数、最大线程数、任务队列三个参数入手,跟Tocmat比对着来看,其实也挺简单的。
public void execute(Runnable job)
    {
        // Determine if we need to start a thread, use and idle thread or just queue this job
        int startThread;
        while (true)
        {
            // Get the atomic counts
            long counts = _counts.get();

            // Get the number of threads started (might not yet be running)
            int threads = AtomicBiInteger.getHi(counts);
            if (threads == Integer.MIN_VALUE)
                throw new RejectedExecutionException(job.toString());

            // Get the number of truly idle threads. This count is reduced by the
            // job queue size so that any threads that are idle but are about to take
            // a job from the queue are not counted.
            int idle = AtomicBiInteger.getLo(counts);

            // Start a thread if we have insufficient idle threads to meet demand
            // and we are not at max threads.
            startThread = (idle <= 0 && threads < _maxThreads) ? 1 : 0;

            // The job will be run by an idle thread when available
            if (!_counts.compareAndSet(counts, threads + startThread, idle + startThread - 1))
                continue;

            break;
        }

        if (!_jobs.offer(job))
        {
            // reverse our changes to _counts.
            if (addCounts(-startThread, 1 - startThread))
                LOG.warn("{} rejected {}", this, job);
            throw new RejectedExecutionException(job.toString());
        }

        if (LOG.isDebugEnabled())
            LOG.debug("queue {} startThread={}", job, startThread);

        // Start a thread if one was needed
        while (startThread-- > 0)
            startThread();
    }
  • Jetty线程池有提供public的获取方法,获取方式如下
    public Executor doGetTp(WebServer webServer) {
        JettyWebServer jettyWebServer = (JettyWebServer) webServer;
        return jettyWebServer.getServer().getThreadPool();
    }
  • 想要动态调整Jetty线程池的线程参数,可以在引入DynamicTp依赖后,在配置文件中添加以下配置就行,参数名称也是和SpringBoot提供的Properties配置类参数相同,配置文件完整示例看项目readme介绍
spring:
  dynamic:
    tp:
      // 其他配置项
      jettyTp:       # jetty web server线程池配置
        min: 100     # 核心线程数
        max: 400     # 最大线程数

Undertow内部线程池的实现

  • Undertow因为其性能彪悍,轻量,现在用的还是挺多的,wildfly(前身Jboss)从8开始内部默认的WebServer用Undertow了,之前是Tomcat吧。了解Undertow的小伙伴应该知道,他底层是基于XNIO框架(3.X之前)来做的,这也是Jboss开发的一款基于java nio的优秀网络框架。但Undertow宣布从3.0开始底层网络框架要切换成Netty了,官方给的原因是说起网络编程,Netty已经是事实上标准,用Netty的好处远大于XNIO能提供的,所以让我们期待3.0的发布吧,只可惜三年前就宣布了,至今也没动静,不知道是夭折了还是咋的,说实话,改动也挺大的,看啥时候发布吧,以下的介绍是基于Undertow 2.x版本来的

  • Undertow内部是定义了一个叫TaskPool的线程池顶级接口,该接口有如图所示的几个实现。其实这几个实现类都是采用组合的方式,内部都维护一个JUC的Executor体系类或者维护Jboss提供的EnhancedQueueExecutor类(也继承JUC ExecutorService类),执行流程可以自己去分析

[图片上传失败...(image-fe517c-1650294498945)]

  • 具体的创建代码如下,根据外部是否传入,如果有传入则用外部传入的类,如果没有,根据参数设置内部创建一个,具体是用JUC的ThreadPoolExecutor还是Jboss的EnhancedQueueExecutor,根据配置参数选择

[图片上传失败...(image-1d3a80-1650294498945)]

  • Undertow线程池没有提供public的获取方法,所以通过反射来获取,获取方式如下
    public Executor doGetTp(WebServer webServer) {

        UndertowWebServer undertowWebServer = (UndertowWebServer) webServer;
        Field undertowField = ReflectionUtils.findField(UndertowWebServer.class, "undertow");
        if (Objects.isNull(undertowField)) {
            return null;
        }
        ReflectionUtils.makeAccessible(undertowField);
        Undertow undertow = (Undertow) ReflectionUtils.getField(undertowField, undertowWebServer);
        if (Objects.isNull(undertow)) {
            return null;
        }
        return undertow.getWorker();
    }
  • 想要动态调整Undertow线程池的线程参数,可以在引入DynamicTp依赖后,在配置文件中添加以下配置就行,配置文件完整示例看项目readme介绍
spring:
  dynamic:
    tp:
      // 其他配置项
      undertowTp:   # undertow web server线程池配置
        coreWorkerThreads: 100  # worker核心线程数
        maxWorkerThreads: 400   # worker最大线程数
        workerKeepAlive: 60     # 空闲线程超时时间

总结

以上介绍了Tomcat、Jetty、Undertow三大WebServer内置线程池的一些情况,重点介绍了Tomcat的,篇幅有限,其他两个感兴趣可以自己分析,原理都差不多。同时也介绍了基于DynamicTp怎么动态调整线程池的参数,当我们做WebServer性能调优时,能动态调整参数真的是非常好用的。

再次欢迎大家使用DynamicTp框架,一起完善项目。

下篇文章打算分享一个DynamicTp使用过程中因为Tomcat版本不一致导致的监控线程halt住的奇葩问题,通过一个问题来掌握ScheduledExecutorService的原理,欢迎大家持续关注。


联系我

欢迎加我微信或者关注公众号交流,一起变强!

公众号:CodeFox

微信:yanhom1314

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容