目录
概要
传统方式?
线程池理解?
基于QPS的设置思路?
总结?
概要
线程池是个既靠谱但又陌生的家伙, 像管家一样, 会踏踏实实的把你交代的任务完成, 但很死板, 没有自动安排人的能力, 需要你给它配好人手(线程实例)和承载容量(队列大小), 这些参数关系影响业务服务整体的吞吐量与并发能力表现, 所以在日常工作根据业务情况进行自定义配置很重要, 但线程池的参数并不好配置, 一方面线程池本身运作机制的理解需要一定的门槛, 另一方面参数调整需要分析实际业务场景, 强依赖工程师的个人经验.
传统方式?
线程数完全核数决定, 通常核心线程数是核数或核数*2, 最大线程数为核数*2或核数*4, 讲究一点的会根据业务偏向IO型或CPU型做些调整, 公式如下:
线程数 = CPU核心数 * (1 + 等待时间/CPU计算时间)
- 等待时间越长代表偏向于IO型, 为避免进入IO阻塞状态时cpu空闲下来, 可以多设置些线程, 尽可能提升CPU利用率.
- 计算时间越长代表偏向CPU型, 代表对CPU利用率已经足够的高了, 线程数就设置少些, 接近核数即可.
但问题在于等待时间和CPU计算时间难测量与得到, 且波动很大,可以作为优化方向指导, 但实际难以应用, 需要靠感觉和经验. 线程数设置过多不仅占用系统资源, 同时频繁的线程上下文切换也会带来性能的损耗, 而线程数设置过少发挥不出来处理器的并发能力.
队列方面, 会直接使用阻塞队列, 长度呢? 粗暴设置大点, 核数*64或128, 这种设置方式尤其是队列的设置过大会导致很难让非核心线程发挥作用, 且线程数设置的没必要过多,
本文阐述一种基于业务QPS的设置线程数和队列长度的实践思路.
线程池理解?
线程池相关的参数, 具体来说包含: 核心线程数、最大线程数、队列类型、队列大小。
要想配的好, 先了解下线程池的作用原理, 以及这些参数对线程池的影响.
任务被创建好后提交到线程池中, 可能会先后经过四个处理步骤: 核心线程 -> 队列缓存-> 非核心线程-> 拒绝策略, 不同步骤中的调度策略也不相同.
- 核心线程: 首先判断下核心线程是否已经完成初始化, 若均已完成初始化, 且有余力, 存在空闲线程实例, 那该任务就交给核心线程执行. 核心线程会在线程池创建时进行初始化, 常驻在线程池中, 不会被销毁掉. 其最大并发量由
- 队列缓存: 若核心线程已占满, 那就判断是否可先缓存到队列中, 若队列已满存不进去, 进入下一流程
- 队列常用两种类型: 阻塞队列与同步队列. 阻塞队列可设置容量, 可缓存一定数量的任务,与正常队列相比, 阻塞队列在空时会阻塞取任务的线程. 同步队列是一种只有一个元素的即存即取队列, 常应用于即时传递场景, 相当于无容量阻塞队列.
- 非核心线程: 当队列满了存不进去了, 线程池开始加人手帮忙了, 准确来说是加些临时工, 创建些非核心线程, 这些线程会优先处理源源不断的新进来的任务, 分担核心线程的压力, 没有新任务就从队列去取, 不过采取的是带有超时时间的poll方式, 一段时间拿不到处理任务后, 这些处于空闲状态的非核心线程会被回收, 以免占据系统资源, 临时工数量由最大线程数来决定.
- 最后一步是拒绝策略, 要么抛异常, 要么丢弃掉.
整个线程池的处理流程可类比餐馆: 有固定的正式员工, 通常数量会比较少, 因为常驻员工要交五险一金、要发工资, 成本较高, 临时工通常是天结, 干完就走, 但也不是无限招, 那数量怎么确定呢?另外客人来了可能会排队, 但也不能无限排, 实在处理不过来就忍痛拒绝客人用餐, 避免餐馆过载,影响正常客人的用餐.
基于QPS的设置思路?
线程数的确定与餐馆招多少员工、正式占比多少、预留多少临时工思路是一样的, 看总客人数和单个服务员一天所能服务的人数, 单人服务数由单个客人的用餐时长决定, 时长越长, 要求的员工数越多才能支持足够的订单量. 但客人的时长波动, 若按平均时长决定的员工数比较小, 但能够满足日常的客人用餐需求了; 最大时长决定的也是最大员工数, 该数量肯定会富裕, 不过可以应对节假日这种高峰期场景了. 所以根据avg确定核心线程数, 根据tp999或max决定最大线程数.
另外是队列长度, 就是容纳等待任务的数量, 对于流量稳定的服务来说, 其等候区可以设置小些, 为了提升用户体验, 不要让客人等, 多召些临时工就行, 来一个就赶紧招待了, 不让客人等太久, 若临时工招满了也招待不过来, 那就拒绝吧, 免得浪费客人时间, 因为流量稳定, 意味着当前客人到来的速率是稳定大于餐馆的最大处理能力, 若设置等待区, 等待区一直爆满, 队列积压, 导致每个任务都会等一段时间才能被处理, 拉高整体耗时, 此时应该做的是扩容而非等待, 这是同步队列的应用场景,.
另外一种阻塞队列, 适用于高低峰错位的流量不均的情况, 高峰时请求来了, 先在队列中等下, 等待线程依次处理, 高峰处理不过来, 但低峰就会慢慢消化掉, 就是耗时会长点, 若队列满了, 赶紧加临时线程, 避免请求处理失败了. 队列多长合适呢? 建议是核心线程数的3倍, 一个经验值. 过大就导致占据更多资源且整体耗时上涨, 需要及时加人手处理.
总结?
- 线程数
- 与总QPS与单个请求的耗时相关, 总QPS越多、耗时越长要求的线程数越多, 但qps和耗时都是波动的, 所以存在最大值与最小值的要求, 公式= 总qps/(1000/耗时), 耗时单位ms
- 最大线程数的最小值(核心线程数) = min(avg(qps) / {1000 / avg(耗时)}, processorNum*2}
- 最大线程数的最大值(总线程数) = min{max(qps) / {1000 / max(耗时)}, processorNum*4}
- 这种方式也把机器核数的限制考虑进来, 在机器核数与公式线程数之间取最小即可.
- 与总QPS与单个请求的耗时相关, 总QPS越多、耗时越长要求的线程数越多, 但qps和耗时都是波动的, 所以存在最大值与最小值的要求, 公式= 总qps/(1000/耗时), 耗时单位ms
- 队列:
- 对稳定流量, 选同步队列或将核心线程数设置大一些.
- 对高低峰不均流量, 选阻塞队列, 队列长度设置线程数的3~5倍是合适的, 看机器内存大小, 但不要使用无界队列(队列长度默认为整数最大值), 机器会疯.
更多有趣有料技术实践好文请关注, @ 猫叔的技术日记
感谢你的时间阅读本文, 希望给你带来收获!
线程池中线程数量与队列大小参数的如何设置实践-基于QPS的计算公式https://mp.weixin.qq.com/s/mlPxfFV2GjzS6e1qU7N5tA
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html