线程池优化与监控
发布于 3 年前 作者 qzhou 3946 次浏览 来自 分享

作者:明义、光线

部门:业务技术



一、前言

随着应用不断的迭代更新,零售工程内异步任务逐渐增多,包括网络请求、本地 DB 操作、轮询等任务使用的都是同一个线程池,线程池极易打满(比如网络一慢就容易阻塞线程池),导致任务堆积,造成「操作卡顿」的现象。用户看到操作没反应,可能会更频繁的尝试操作,再加上不断轮询的任务,应用的线程池队列会快速积压更多的任务,「卡顿」现象加剧,单一线的线程池已经无法支撑业务需求:

  1. 任务没有隔离,异步任务相互影响主要是长耗时影响短耗时任务
  2. 轮回任务开销大:没有统一的轮询处理方式,业务方需要自己创建线程或线程池(有些干脆就在默认的 I/O 线程池进行轮询),应用中存在大量的自建线程 池,增加无谓的资源消耗
  3. 缺少监控:缺乏线程池的监控和日志,线程池的运行状况和健康度无法衡量,一旦出现「卡顿」问题,排查非常困难,完全不知道是哪些任务造成的问题,伤害用户体验的同时,也极大的消耗开发人员的精力

图注:短时间任务暴涨的情况,在几毫秒内触发多次任务

二、整体设计

目标:

  1. 任务隔离,避免耗时任务影响交互
  2. 统一轮询,减少资源开销
  3. 任务监控,防止业务方使用错误
  4. 信息采集与监控,快速定位排查问题

优化的核心在于分离和监控

分离:

对原有的 I/O 线程池进行拆分,分离出不适合放在这个线程池的任务,保证 I/O 线程池能对大量、快速的本地任务给予更好的支持,而分离的关键就在于分离出「慢」和「多」的任务:

  • 避免耗时短的任务等待耗时长的任务
  • 避免频繁、大量执行的任务占据大量的线程池资源

具体的策略包括:

  • 将网络请求的线程分离出来,放入单独的线程池中,避免因网络任务慢阻塞本地的快速任务
  • 将轮询任务分离出来,当轮询的任务耗时过长或者线程池打满的场景下,会极速加剧线程池的恶化,最终让主流程完全瘫痪,轮询任务不适合放在通用的线程池中,通过将它放入通用的轮询任务线程池,统一化的对轮询任务进行管理

监控:

除了已知的网络任务&轮询任务需要分离出 I/O 线程池之外,还需要增加对线程池的监控,进一步分离出不适合放在I/O线程池的任务(同样是「慢」和「多」)

  1. 通过监控每个任务执行的时长,分离出长耗时(即「慢」)的任务,分析出是因为逻辑 bug 还是本身的复杂度正常占用,如果是正常占用就考虑是否合适继续放在默认的 I/O 线程池,最终目标是达成对「慢」的优化
  2. 另外零售工程中还存在短时间大量重复任务堆积的现象,当线程池接近或达到负荷状态时,监控线程池中的任务,找到批量的任务,然后进行优化,最终目标是达成对「多」的优化
  3. 通过上面的方式将「慢」的任务分离出去之后,并不代表这样就完事了,这些任务可能会消耗大量的系统资源,所以同样需要统一对这个线程池进行监控(主要是网络任务的监控),把慢网络请求找出来,达到对「慢」的进一步优化
  4. 通过 API 监控业务方使用合理性,比如:使用多线程轮询的合理性

三、技术实现

2.1 线程管理库

目标:

  1. 管理工程内所有线程池与线程创建
  2. 子线程任务监控
  3. 线程池隔离任务分开执行,使用不同线程池执行不同任务
  4. 轮询任务的统一与异常任务的过滤
  5. 线程池的自动最优设置
线程库结构图

线程池管理,目前会提供三种线程池:

  1. 网络线程池:针对网络任务。更改方式:直接在网络库替换业务方无感知
  2. IO 线程池:针对本地异步任务。更改方式:App 启动时 Hook RxJava 线程池替换业务方无感知
  3. 轮询线程池:针对轮询任务,需要业务方的接入
线程池 API 定义

3.2 轮询任务统一

  1. 禁止时间间隔小于 1 秒轮询任务,防止高频轮询消耗资源
  2. 所有轮询任务巡检间隔 1 秒检测一次
  3. 轮询检测是独立的线程,只做轮询检测,有任务时死循环间隔检测,没有任务时则睡眠等待下次注册执行,根据结果轮询执行或睡眠
  4. 线程任务模式有两种:

单线程回调: 表示最多占用一个线程。例:1 秒轮询回调 1 次,再回调前会判断上个任务是执行完成,如果没有执行完成则回调 pollTaskExceptionCallback(),如果已经执行完成则回调 pollTaskCallback()

多线程回调:表示最多占用 30 个线程。例:1 秒轮询回调 1 次,再回调前会判断当前任务占用线程总量,如果没有执行完成则回pollTaskExceptionCallback(),如果已经执行完成则回调。pollTaskCallback() 默认是单线程回调

5.业务方可以指定轮询次数,或设置无限轮询

6.轮询线程池动态扩容:初始线程为 30 个,如果使用线程等于核心线程则动态扩容 2 倍,最大限制 120 个核心线程

轮询任务流程图
轮询 API 定义

任务过滤:

轮询正常回调 pollTaskCallback() 方法,如果异常这时将回调到 pollTaskExceptionCallback() 方法。

异常任务过滤的实现:

  1. 记录所有任务。
  2. 每次执行任务时会进行校验,校验监听者是否已经把自己申请的线程全部使用,如果已经全部占用将回调异常方法,直接到任务有一个释放后再继续回调。
  3. 任务执行完后重设记录数据。

轮询异常回调有三种触发场景:

  1. 单线程任务没有释放超时。
  2. 多线程任务占用线程超过最大数量。
  3. 轮询线程池被全部占满超时。
任务过滤-流程图

四、工程更改

工程更改主要分为 5 块:

  1. 网关请求线程池的替换,在构建 RxJava observable 替换成自定义网络线程池。
  2. RxJava 默认线程池的替换,App 启动时把 RxJava Hook 设置成自定义 IO 线程池。
  3. 轮询业务接入统一轮询。
  4. 网线线程池使用错误拦截,目的防止线程使用错误,禁止 IO 线程网络请求,通过拦截器方式实现。
  5. 线程任务监听设置给 APM ,通过 APM 分析上报。
备注:只有轮询需要业务方逐个替代其它都正常使用不感知。

五、信息采集与监控

  1. 任务提交之后记录每个任务的调用栈信息以及任务提交的时间,之后在任务真正开始执行时会记录任务开始的时间,任务执行完成后即可算出一个任务完整的执行时间、任务等待时间等,这样就可以抓取出「慢」的任务
  2. 在任务执行完成时,如果线程池已满并且任务的等待时间超过阈值,则会拉出线程池任务栈的信息,用于查找出异常的任务
线程池监听 API
任务监听实现流程

改善效果:

卡顿定义:包含主线程超过300ms慢方法、ANR、本地子线程操作超过1s、线程池阻塞、页面渲染时间超过200ms。

卡顿次数趋势图

    图注:卡顿次数下降 76%


六、未来规划

  • 支持更多维度的任务监控,并增加自动报警能力
  • 针对不同的机型进行最优线程池配置,最大化复用系统资源
  • 逐渐完善 pthread、线程任务数量等监控能力
回到顶部