4、AsyncTool框架的一些思考

前言

本文是专栏《AsyncTool框架原理源码解析》系列的第四篇文章 《AsyncTool框架的一些思考》,针对AsyncTool框架学习过程当中的一些思考,包括但不局限于框架本身。线程池、分布式系统调用链、缓存时钟类、扩展等,学习本身就是一个分散性思考的东西,学习一个东西,肯定也会涉及到不同的知识,思考的过程是很有价值的

专栏《AsyncTool框架原理源码解析》共有 5篇 文章,由浅入深,从实战使用再到源码、原理分析,包括但不仅局限于AsyncTool框架思考和总结,最后分享下我为AsyncTool框架开源项目贡献代码。以下是《专栏目录》:

  1. 《AsyncTool框架简介和分析实现》
  2. 《AsyncTool框架实战使用》
  3. 《AsyncTool框架原理源码解析》
  4. 《AsyncTool框架的一些思考》
  5. 《AsyncTool框架竟然有缺陷?》

1、关于线程池的思考?

作者在issue上推荐一个tomcat只使用一个不定长线程池。

AsyncTool关于多线程并发执行的问题。如果不指定线程池,默认使用不定长线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

注意newCachedThreadPool() 它的核心线程数是0,最大线程数是Integer.MAX_VALUE。可以说它的线程数是可以无限扩大的线程池

他使用的队列是没有存储空间的,只要请求过来了,如果没有空闲线程,就会创建一个新的线程。

这种不定长线程池,适合处理执行时间比较短的任务。在高并发下,如果在耗时长的RPC\IO操作中使用该线程池,势必会造成系统 线程爆炸

AsyncTool作者在京东使用的场景多数为低耗时(10ms)高并发,瞬间冲击的场景。这种高并发,且任务耗时较少的,适合使用不定长线程池

但是这种低耗时的场景也不多,对于耗时较长的场景,推荐使用自定义线程池,可以避免那些耗时长的任务长时间占用线程,造成线程 ”爆炸 “,容错率更高。

2、AsyncTool框架执行的任务如何监控?

1、背景:

大促备战前,一般都会对系统进行压测。使用AsyncTool框架的接口,压测结果不达标,通过监控系统的调用链追踪,发现trace调用链出现”断点“了,使用AsyncTool框架执行的任务调用链丢了,这是什么情况?

2、trace调用链:

  • trace:又名调用链,指对一次单一业务请求调用过程的跟踪

  • traceId:每一个trace都会被分配一个全局唯一的ID

3、为什么AsyncTool框架执行的任务trace丢了呢?

业务开发中,一般都会使用ThreadLocal保存一些上下文信息,但是在线程池中执行对应逻辑时,由于是不同线程所以无法获取之前线程的上下文信息。

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

4、如何解决?

京东的Pfinder和阿里李鼎大神开源的TransmittableThreadLocal(TTL)框架都可以解决分布式系统trace的传递问题。TTLJDKInheritableThreadLocal类进行了增强,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

他们的使用也很简单,对线程池包装一下即可。

//Pfinder
PfinderContext.executorServiceWrapper(new BaseThreadPoolExecutor(1000, 1000, 2, TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("skuCardWorkerCommonPool-pool-%d").build(),
        new PoolPolicy("商品卡片WORKER版线程池")));

//TTL
ExecutorService executorService = TtlExecutors.getTtlExecutorService(executorService);

这里没有做深入研究,感兴趣的小伙伴可以自行了解。

  1. TTL
  2. Pfinder

3、关于缓存时钟类的思考:SystemClock

AsyncTool框架任务的超时控制,是通过缓存时钟类 SystemClock控制的,获取当前时间使用SystemClock.now(),为什么不直接使用System.currentTimeMillis()

先看一下 SystemClock时钟类的设计。

(1)使用单例模式创建了一个时钟类SystemClock

(2)ScheduledExecutorServiceExecutorService 的子类,它基于 ExecutorService 功能实现周期执行的任务。

(3)创建了一个守护线程,每1ms对 AtomicLong now进行更新 System.currentTimeMillis(),因为守护线程的执行周期是每1ms执行一次,这里是有1ms的延迟。

/**
 * 用于解决高并发下System.currentTimeMillis卡顿
 * @author lry
 */
public class SystemClock {

    private final int period;

    private final AtomicLong now;

    /**
     * 单例模式
     */
    private static class InstanceHolder {
        private static final SystemClock INSTANCE = new SystemClock(1);
    }

    private SystemClock(int period) {
        this.period = period;
        this.now = new AtomicLong(System.currentTimeMillis());
        scheduleClockUpdating();
    }

    private static SystemClock instance() {
        return InstanceHolder.INSTANCE;
    }

    /**
     * 守护线程
     * 每1ms写一次now系统当前时间。
     */
    private void scheduleClockUpdating() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "System Clock");
            //守护线程,必须在线程启动前设置
            thread.setDaemon(true);
            return thread;
        });
        //定时周期执行任务
        scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
    }

    private long currentTimeMillis() {
        return now.get();
    }

    /**
     * 用来替换原来的System.currentTimeMillis()
     */
    public static long now() {
        return instance().currentTimeMillis();
    }
}

通过jstack可以看到后台始终运行着一个守护线程;

image-20220601214620578

System.currentTimeMillis()真的有性能问题吗?

说有性能问题的,有以下几种观点:

  • System.currentTimeMillis要访问系统时钟,这属于临界区资源,并发情况下必然导致多线程的争用。
  • System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道,System.currentTimeMillis 比 new一个普通对象耗时还要高100倍左右

要知道,Linux和Windows系统的时钟实现不同,windows系统的时钟访问,比linux要快很多(据说是Windows的黑科技)。

Linux系统有2中常用的时钟源,TSC、HPET,这些时钟源都属于硬件;

根据权威测试,Linux操作系统下,TSC时钟源比HPET时钟源快很多倍。也就是说,在Linux操作系统下,如果服务器使用HPET时钟源,可能会有性能问题,可能

有一位网友曾经遇见过HPET时钟源问题,并作了如下解释:

这个问题根源上是要切换时钟策略,本身代码角度来说应该是没有啥优化的必要。我就遇到过这个问题,场景就是高并发和大数据量,调用这个方法,现场部署的时候,相同程序个别机器性能奇差,一启动CPU 就打满了,机器都变的很卡,最终通过切换TSC时钟策略解决的。切换不了的机器,直接让服务器厂商把主板换了,然后就好了

具体可阅读如下文章:

4、为什么第一次请求耗时长?

1、背景:

项目中有一个接口,使用Asynctool框架并行编排多线程任务(RPC)。接口的核心逻辑是并行调用十几个rpc接口,汇总数据。

线程池配置是:1000核心,1000最大,1024队列。

2、现象:

发现了一个奇怪的现象。服务启动后,每次第一次请求接口时,就会有一个耗时较高的跳点,后续再调用就正常了,因为这个问题,接口压测的时候,都会先预热一下接口。

3、分析:

Rpc接口监控正常,没有跳点。感觉是CompletableFuture的问题,但又不知道问题出在哪里。(JDK版本是8u60)

经咨询AsyncTool作者,他们也有这种问题。

作者表示:CompletableFuture就第一次初始化时长。他初始化线程池那一下耗时500+,之后就不需要了

针对作者的回答,我在本地环境测试没有复现。可能在docker中的tomcat进程中运行会不一样,这个问题我觉得还需要使用arthas等工具,从头到尾的排查一下,奈何生产环境有较多限制,一直没进一步分析问题原因。

todo:待后面分析

5、全组任务超时?还是单步任务超时?

如果能够对单步任务执行超时,是不是每个执行任务的时间更可控了?

在gitee上,作者也对这个问题进行了讨论。

作者给出的解释是:如果要支持单步任务超时,每个任务的超时监控都需要一个额外的线程。会造成线程池翻倍。

退而求其次,使用了全组任务超时设置。实际上,全组任务超时,已经能满足绝大部分的场景了。

另外,在timewheel分支,实现了 一个线程监控所有任务的超时时间。原理是单线程时间轮对所有注册的wrapper进行检查

没有深入研究,感兴趣的自行了解。

image-20220609004940456

6、扩展

编排框架有哪些:

1、Gobrs-Async

2、Disruptor

3、parseq


🎉 如果喜欢这篇文章,请不要吝啬你的赞👍👍👍哦,创作不易,感谢!

请添加图片描述

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涛声依旧叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值