如何确定系统的最佳线程数?
- 确定系统的最佳线程数的3个step
- step1: 完成线程数的理论预估 (设计阶段)
- 第一类:IO 密集型线程池线程数预估
- 第二类:CPU密集线程池线程数预估
- 第三类:混合型线程池线程数预估
- step2: 完成线程数的压测验证 (设计阶段)
- step3: 完成线程数的线上调整 (生产阶段)
- 第一个维度:可监控预警
- 第二个维度:可在线调整
- 在线动态调整实操:结合Nacos 实现动态化线程池
- 1.结合Nacos 实现动态化线程池架构
- 2.Nacos 上的配置如下:
- 3.线程池配置和nacos配置变更监听
- 4.线程池配置的动态刷新
- 5.LinkedBlockingQueue 实现resize
- 在线动态监控实操:结合PGA实现Metric采集和预警
- 说在最后:有问题找老架构取经
- 部分历史案例
线程使用的两个核心规范
首先看编程规范中, 有两个很重要的,与线程有关的需要强制执行的规范:
规范一:【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
1)消耗内存资源:必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
2)消耗CPU资源:需要进行系统调用,以便在OS(操作系统)中创建和注册内核线程,大量内核线程调度会导致CPU上下文过度切换。
所以,Java高并发应用频繁创建和销毁线程的操作将是非常低效的,而且是不被编程规范所允许的。
如何降低Java线程的创建成本?必须使用到线程池。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
以上的内容,在尼恩的 《Java 高并发核心编程 卷2》 进行了详细介绍。
规范二:【强制】线程池不允许使用Executors去创建快捷线程池 ,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
通过以上规范,说明我们应用中,需要用自定义线程池。然而,由于构造一个线程池竟然有7个参数
7个重要参数中,最为重要的三个是:核心,最大线程数量, BlockingQueue。前两个参数和线程数量有关系, 后一个和内存资源消耗有关。
线程数设置太少或者阻塞队列太小, 会导致大量任务被拒绝,抛出RejectedExecutionException,触发线上的接口降级,用户体验很差。
二线程数设置太多或者阻塞队列太长,会导致资源消费高而有效负荷很小, 特别是阻塞队列设置过长,会导致频繁FullGC,甚至OOM。
如何确定系统的最佳线程数?来一个牛逼轰轰的答案
如何确定系统的最佳线程数,大体上分三步:
第一步,理论预估;
第二步,压测验证;
第三步,监控调整。
这也是尼恩给大家归纳的,最为理想的:可监控/可弹性的 线程池模式
step1: 完成线程数的理论预估 (设计阶段)
在尼恩的 《Java 高并发核心编程 卷2》 进行了详细介绍。
首先,按照任务类型对线程池进行分类, 分为三类,具体如下图:
具体,请参见在尼恩的 《Java 高并发核心编程 卷2》 1.7.1 小节。
第一类:IO 密集型线程池线程数预估
线程数就是 CPU的核数的2倍。
具体,请参见在尼恩的 《Java 高并发核心编程 卷2》 1.7.2 小节。
第二类:CPU密集线程池线程数预估
CPU密集型任务并行执行的数量应当等于CPU的核心数, 线程数就是 CPU的核数
具体,请参见在尼恩的 《Java 高并发核心编程 卷2》 1.7.3小节。
第三类:混合型线程池线程数预估
混合型线程池线程数预估, 参考下面的的公式:
职业救助站
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
技术自由圈
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢
最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数
具体,请参见在尼恩的 《Java 高并发核心编程 卷2》 1.7.4小节。
step2: 完成线程数的压测验证 (测试阶段)
过少的线程会造成任务拒绝,业务降级。
过多的线程会造成,额外的内存开销CPU开销,甚至会导致OOM。
所以,合理的线程池线程数,才是王道。
在设计阶段完成了step1的线程数的理论预估之后, 那么我们的理论值就出来了。
如何做验证呢?这里需要 压测。
根据公式:
服务器端最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量
前面线程等待时间,线程cpu时间都是 预估的 ,都是要验证的。
首先通过用户慢慢递增来进行性能压测,观察QPS。持续大的增加用户数, 压测出最大的吞吐量。
然后再 收集 最大的吞吐量场景的 线程等待时间,线程cpu时间, 再计算出最佳线程数。
step3: 完成线程数的线上调整 (生产阶段)
压测的场景,是有限的。而线上的业务, 是复杂的,多样的。
由于系统运行过程中存在的不确定性,很难一劳永逸地规划一个合理的线程数。
所以,需要进行生产阶段线程数的两个目标:
- 可监控预警
- 可在线调整
第一个维度:可监控预警
第二个维度:可在线调整
参数的在线动态调整:结合Nacos 实现动态化线程池
优秀的动态化线程池轮子,主要有:
- Hippo4J
- dynamic-tp
如果线上使用,可以使用这些轮子项目。
但是尼恩的是[技术自由圈]一个实战社群,必须自己从0到1,去撸一把代码,提升自己的水平。
1.结合Nacos 实现动态化线程池架构
结合Nacos 实现动态化线程池的参数在线调整,架构如下:
2.Nacos 上的配置如下:
3.线程池配置和nacos配置变更监听
4.线程池配置的动态刷新
5.LinkedBlockingQueue 实现resize
LinkedBlockingQueue 不支持 resize, 需要重新定制。自定义可以扩容的 LinkedBlockingQueue ,结构如下:
这里采用的是读写锁,对capacity 的设置,进行线程安全 保护:
读写锁的使用如下:
通过对capacity的安全修改,以达到动态扩展目的。