线程池在Java服务中随处可见,但到底设置多少个线程是合适的往往见仁见智。这里,总结下个人看到的观点,结合个人的经验做一些总结。
int N_CPUS = Runtime.getRuntime().availiableProcessors();
这段代码是JDK提供的方案。一般来说在物理机器上是准确的。但是在云原生/虚拟化环境下,笔者就遇到获取结果比实际数量多的情况,导致线程数开的过多。因此,代码可以这么写,实际运行起来还是要再确认下。此外,所谓的可用处理器,实际上是可用的处理器核心,毕竟可能只有1颗处理器,但是有8个核心。
工程实践中的任务类型,可能是I/O密集型,可能是计算密集型,也有可能是两者的混合也就是混合型任务。虽然良好的设计应该是I/O过程和计算过程分开,但也会遇到遗留系统,因为种种历史原因有许多反常规的将两者合在一个线程中处理的逻辑。因此,凭经验确定的任务类型可能并不准确。最终,通过WAIT/COMPUTE比例来衡量任务类型更为合理。作为开发的我们,还是需要用数据说话。因此让这段逻辑连续运行几次,就能见分晓。以下命令和结果均为CentOS。
查看线程状态
// tpid 为 thread id,此处以grpc boss event loop thread为例
cat /proc/${tpid}/status
cat /proc/27792/status
Name: grpc-nio-boss-E
Umask: 0022
State: S (sleeping)
Tgid: 27738
Ngid: 0
Pid: 27792
PPid: 1
TracerPid: 0
Uid: 18930 18930 18930 18930
Gid: 1001 1001 1001 1001
FDSize: 256
Groups: 1001 3007 3030
VmPeak: 2958948 kB
VmSize: 2958944 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 550244 kB
VmRSS: 550244 kB
RssAnon: 536192 kB
RssFile: 14052 kB
RssShmem: 0 kB
VmData: 2795616 kB
VmStk: 132 kB
VmExe: 4 kB
VmLib: 19012 kB
VmPTE: 1460 kB
VmSwap: 0 kB
Threads: 53
SigQ: 0/14120
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000004
SigIgn: 0000000000000003
SigCgt: 2000000181005ccc
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
CapAmb: 0000000000000000
Seccomp: 0
Speculation_Store_Bypass: vulnerable
Cpus_allowed: 3
Cpus_allowed_list: 0-1
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
# 线程自愿上下文切换
voluntary_ctxt_switches: 12127
# 线程非自愿上下文切换
nonvoluntary_ctxt_switches: 4
非自愿上下文切换 / 资源上下文切换非常大 说明线程经常主动放弃CPU时间片,那通常是等待其他资源,例如I/O完成, 等待锁等。这里的单位是次,能够定性,但不能提供精确的运行/等待比例。
查看线程调度状态
// pid: process id, tpid: thread id
cat /proc/${pid or tpid}/sched
cat /proc/27792/sched
grpc-nio-boss-E (27792, #threads: 53)
-------------------------------------------------------------------
se.exec_start : 10308747122.791804
// 虚拟运行总时间(等待时间+使用CPU时间)
se.vruntime : 52693327.134406
// 实际运行总时间(使用CPU时间)
se.sum_exec_runtime : 1285.444999
// 跨CPU核心切换次数
se.nr_migrations : 2446
// 上下文切换总次数
nr_switches : 12131
// 自愿切换次数
nr_voluntary_switches : 12127
// 非自愿切换次数
nr_involuntary_switches : 4
se.load.weight : 1024
policy : 0
// 线程优先级
prio : 120
clock-delta : 36
mm->numa_scan_seq : 0
numa_migrations, 0
numa_faults_memory, 0, 0, 1, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1
通过se.se.vruntime ,se.sum_exec_runtime,我们可以得到一个相对合理的Wait/Compute比例。
严格来说,这个比例也不是那么地准确。因为,如果线程的任务有诸多的条件分支,导致任务实际是各不相同的,则该比例会有大的波动,失去参考意义。因此,糟糕的设计会带来无数的不确定性,这种不确定性限制了经验和理论的作用。
到这里,我们可以套用公式,HAPPY地去确定线程数了。不过实际情况可能还有些复杂,我们聊聊这些限制。
线程池中任务依赖其他下游资源。比如连接池中的连接。假设线程池设置为30,而连接池最大仅允许20,那么依然有10线程因获取不到链接而等待。又比如通过网络大批量传输数据,内核TCP窗口太小或对方接收太慢,导致Socket发送缓存总是被填满,此时发送线程也只能等待。
JVM可支配内存不足以支持更多的并发;通常进程中线程池不止一个,存在多个同类任务的线程池,线程优先级设置等都会影响最终的W/C比例。
以上就是今天要讲的内容,本文对设置线程数的方法,要素和工程限制结合个人做了些总结,希望对你有所帮助。如您有更好的方法,也欢迎拍砖,感谢您的阅读。