马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
前言
相信接触过并发系统的小伙伴们基本都使用过线程池,或多或少调整过对应的参数。以 Java 中的经典模型来说,能够配置核心线程数、最大线程数、队列容量等等参数。- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue) {
- this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
- Executors.defaultThreadFactory(), defaultHandler);
- }
复制代码 一般情况下,我们设置参数步骤是:
- 确定业务属性,比如IO密集型、CPU密集型、混合型等。
- 参考理想化的线程计算模型算出理论值。如《Java并发编程实战》一书中的理想化模型:
工具推荐
本文则推荐一款工具,它不关心任务内部是如何实现的,而是通过计算运行时的各种系统指标(包括 CPU计算时间、IO等待时间、内存占用等)来直接计算线程池参数的。我们可以直接在这些参数的基础上,再配合压测进行调优,避免盲目调参。
这个工具叫做 dark_magic,直译就是黑魔法,源码参见 https://github.com/sunshanpeng/dark_magic。里面的备注已经很详细,本文不再赘述。只提一下系统指标的计算方式。
指标的计算方式
CPU计算时间 和 IO等待时间 的计算:
- 先执行两遍任务,进行预热。
- 获取当前线程的 CPU计算时间,记为 C1
- 再执行一遍任务
- 获取当前线程的 CPU计算时间,记为 C2
- 计算当前任务执行需要的 CPU计算时间:C2 - C1
- 计算当前任务执行中的 IO等待 时间:总耗时 - CPU计算时间
其中,计算当前线程的 CPU计算时间使用 rt.jar 包中的方法:- ManagementFactory.getThreadMXBean().getCurrentThreadCpuTime()
复制代码
内存占用的计算:
- 生成1000个(可配置)任务加入到阻塞队列中
- 循环调用 15次(可配置) System.gc() 函数,触发gc
- 记录目前的内存使用情况,记为 M0
- 再次生成1000个(可配置)任务加入到阻塞队列中
- 循环调用 15次(可配置) System.gc() 函数,触发gc
- 记录目前的内存使用情况,记为 M1
- 计算当前任务执行需要的内存:M1 - M0
其中,计算内存使用 rt.jar 包中方法:- Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
复制代码
使用方法
该工具的使用方法也很简单:
- 把你的业务代码封装为一个函数,放到 createTask 函数中。
- 设定 CPU使用率的期望值、队列占用内存的期望值。
- 执行,等待结果输出。
下面分别展示一个CPU密集型和IO密集型的输出(我们设置的 CPU 使用率期望值为 60%,队列占用内存的期望值为 10MB ):- # CPU密集型
- Target queue memory usage (bytes): 10240
- createTask() produced threadpool.AsyncCPUTask which took 40 bytes in a queue
- Formula: 10240 / 40
- * Recommended queue capacity (bytes): 256
- Number of CPU: 8
- Target utilization: 0.59999999999999997779553950749686919152736663818359375
- Elapsed time (nanos): 3000000000
- Compute time (nanos): 2949786000
- Wait time (nanos): 50214000
- Formula: 8 * 0.59999999999999997779553950749686919152736663818359375 * (1 + 50214000 / 2949786000)
- * Optimal thread count: 4.79999999999999982236431605997495353221893310546875000
复制代码 - # IO密集型
- Target queue memory usage (bytes): 10240
- createTask() produced threadpool.AsyncIOTask which took 40 bytes in a queue
- Formula: 10240 / 40
- * Recommended queue capacity (bytes): 256
- Number of CPU: 8
- Target utilization: 0.59999999999999997779553950749686919152736663818359375
- Elapsed time (nanos): 3000000000
- Compute time (nanos): 55528000
- Wait time (nanos): 2944472000
- Formula: 8 * 0.59999999999999997779553950749686919152736663818359375 * (1 + 2944472000 / 55528000)
- * Optimal thread count: 259.19999999999999040767306723864749073982238769531250000
复制代码
针对线程数的计算而言:
- 对于 CPU 密集型任务,IO等待时间(Wait time) 远远小于 CPU计算时间(Compute time)。计算出来的推荐核心线程数为 4.8。
- 对于 IO 密集型任务,IO等待时间(Wait time) 远远大于 CPU计算时间(Compute time)。计算出来的推荐核心线程数为 259。
而队列大小与任务中使用的对象大小有关,这里的内存使用是通过计算 gc 执行前后的内存大小差异得到的(本文中的例子均为 40 B)。由于该算法内部使用 System.gc() 触发 gc。但由于 gc 不一定真的会立刻执行,所以拿到的队列结果可能不一定准确,只能作为粗略参考。
总结
总的来说,dark_magic 这款工具以任务执行时的系统指标数据为基础,计算出比较合理的线程池参数,给我们进行后续的压测调参提供了相对比较合理的参考,值得推荐。
|