alibaba / transmittable-thread-local

📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.
https://github.com/alibaba/transmittable-thread-local
Apache License 2.0
7.66k stars 1.69k forks source link

TTL Agent增强方式下,如何解决,使用Tomcat的Http11Nio2Protocol协议,碰到的清理任定时任务的性能问题 #148

Closed lesterwang closed 4 years ago

lesterwang commented 5 years ago

Tomcat容器使用Http11Nio2Protocol协议时,请求的分发会执行到:

indexOf:929, ScheduledThreadPoolExecutor$DelayedWorkQueue (java.util.concurrent)
remove:958, ScheduledThreadPoolExecutor$DelayedWorkQueue (java.util.concurrent)
remove:1766, ThreadPoolExecutor (java.util.concurrent)
cancel:281, ScheduledThreadPoolExecutor$ScheduledFutureTask (java.util.concurrent)
finishRead:419, UnixAsynchronousSocketChannelImpl (sun.nio.ch)
finish:191, UnixAsynchronousSocketChannelImpl (sun.nio.ch)
onEvent:213, UnixAsynchronousSocketChannelImpl (sun.nio.ch)
run:293, EPollPort$EventHandlerTask (sun.nio.ch)
run:748, Thread (java.lang)

其中indexOf的逻辑为:

private int indexOf(Object x) {
    if (x != null) {
        if (x instanceof ScheduledFutureTask) {
            int i = ((ScheduledFutureTask) x).heapIndex;
            // Sanity check; x could conceivably be a
            // ScheduledFutureTask from some other pool.
            if (i >= 0 && i < size && queue[i] == x)
                return i;
            } else {
                for (int i = 0; i < size; i++)
                    if (x.equals(queue[i]))
                        return i;
            }
        }
    return -1;
}

在使用TTL Agent增强的情况下,ThreadPoolExecutor.remove(Runnable task)的参数task会被转换为com.alibaba.ttl.TtlRunnable,在执行

if (x instanceof ScheduledFutureTask)

语句时,执行结果永远为falseTtlRunnable会和队列中的每一个任务进行比较,且结果为false,在高并发的情况下,任务队列中的任务会有几千到几十万个不等,x.equals(queue[i])成为一个严重影响性能的操作,且无法及时清理掉任务进一步影响性能。

这种情况怎么处理比较合适呢?

oldratlee commented 5 years ago

问题收到,我先理解一下。


看你已经分析过源码了,方便分离一下问题,给一下 极简的(不涉及Tomcat)可运行复现问题的测试Demo代码吗?

如果能写成个UT就更好了: @lesterwang ❤️

lesterwang commented 5 years ago

@oldratlee 因为这个问题牵扯到

java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask

这个内部类(非public的),不使用tomcat容器很难复现这个问题,使用springBoot 1.5.x版本,然后添加一个bean:

@Bean
public EmbeddedServletContainerCustomizer tomcatCustomizer() {
    return container -> {
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            ((TomcatEmbeddedServletContainerFactory) container)
                    .setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
        }
    };
}

随意提供一个接口访问应该能复现这个问题。需要的话明天我再提供一个单独的项目方便测试。

这个问题产生的原因是: 当使用Http11Nio2Protocol协议处理请求时,会通过

ScheduledThreadPoolExecutor.reExecutePeriodic(RunnableScheduledFuture<?> task)

来添加任务,由于task既不是Runnable也不是Callable,所以不会被转换为TtlRunnable。 后续的执行都是使用ScheduledThreadPoolExecutor#DelayedWorkQueue这个内部类的方法,即使参数是Runnable也不会被TTL框架转换,最终加入到DelayedWorkQueue#queue队列里的任务是非TtlRunnable的。

同时Http11Nio2Protocol协议会通过

sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishRead(boolean var1)

方法来清理任务,该方法会调用到

java.util.concurrent.ThreadPoolExecutor.remove(Runnable task)

这时task会被转换为TtlRunnable,最终导致

ScheduledThreadPoolExecutor$DelayedWorkQueue.indexOf(Object x)

效率严重降低。

oldratlee commented 4 years ago

做了一个ScheduledThreadPoolExecutor/ScheduledFutureTask的使用Demo:

https://github.com/alibaba/transmittable-thread-local/blob/ddc2d090bfe17493893da7f4f233a66c8dbdadf5/src/test/java/com/alibaba/demo/scheduled_thread_pool_executor/ScheduledFutureTaskDemo.kt#L16-L24

在使用TTL Agent增强的情况下,与不使用TTL Agent一样, @lesterwang

PS: 对于Agent方式通过脚本方便些来完成运行(省得设置Agent参数等操作)

$ scripts/run-agent-for-class.sh com.alibaba.demo.scheduled_thread_pool_executor.ScheduledFutureTaskDemoKt

输出如下:

I'm a Runnable task, I'm working...
I'm a Runnable task, I'm working...
I'm a Runnable task, I'm working...
cancel
canceled: true # canceled成功之后,不再有定时任务的输出,即不再运行
Bye

对于 TTL Agent运行方式,Debug的断住 ScheduledFuturecancel方法: image

可以确认

oldratlee commented 4 years ago

因为整个 TTL及其Java Agent增强的实现 是

所以使用(无论是一般的业务使用,还是TomcatExecutor使用方式) 应该是不会有问题的。

如果Tomcat的实现很 HACK 的,非常依赖的Executor的内部具体的实现方式,是可能会出问题。 比如ScheduledThreadPoolExecutor.reExecutePeriodic是内部的具体实现流程,如果Tomcat的实现有直接调用,就很 HACK 了。

当然一般大家(含Tomcat的同学)不会 设计/实现 成这样。 😄 # HACK 让 自己的实现脆弱了,会因为依赖不稳定而常常出问题要改。


还是请 @lesterwang 给一个可运行复现问题的测试Demo/UT。❤️

复现的问题 是指 与不用TTL Agent对比出的 基础功能 或 性能 不一致的地方

我先Close这个Issue了, 如有可以运行复现问题的测试Demo/UT,请 @lesterwang 随时 Reopen 这个Issue。 🙌