背景
我们的业务(权限业务)上有一个需求:给权限点批量绑定角色集合,绑定成功后需要更新每个角色对应的用户-权限点缓存。抽象下问题即为:A实体(权限点)与B实体(角色)集合有关联关系,B实体与C实体(用户)集合有关联关系。而更新用户权限点缓存量比较大,理论上为$|权限点| * |角色| * |用户|$次更新操作。其中一个权限点对应的角色数可能达到100+, 角色关联的用户数可能达到1000+。
为了更快返回,我开了一个线程池操作更新用户权限点缓存1
ExecutorService threadPool = Executors.newFixedThreadPool(100);
但是上线后,很快出现了CPU Idle掉底的情况。
反思
因为我们服务环境是4core 8G docker机器。线程池里面有100个线程,而更新用户-权限点的操作计算量大,为典型的计算密集型任务。CPU的每个core都被占满,而100个线程会导致大量的线程在较少的CPU和内存资源上发生竞争,频繁的线程上下文切换也会带来额外的性能开销,基于上述原因导致了CPU Idle掉底。
结论
线程CPU时间所占比例越高,需要越少线程。线程等待时间所占比例越高,需要越多线程。
线程池设置大小应该取决于给线程池处理什么样的任务以及机器的CPU core数量。任务类型不同,线程池大小的设置方式也是不同的。任务一般可分为:计算密集型,IO密集型,混合型。
- 计算密集型任务
尽量使用较小的线程池,一般为CPU核数+1(参考Java并发实战)。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。注意,这里推荐值是指机器集中做该任务并且只有一个线程池的情况下的推荐值。而一般WEB服务会同时处理很多个请求,因此任务的线程池需要再相应调低一些。 - IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
-混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
引申
一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。但是根据短板效应,真实系统吞吐量应该取决于系统短板,例如网络延迟、IO。因此为了提高系统真实吞吐量,应该:
- 尽量提高短板操作的并行化比率,例如多线程下载技术;
- 增强短板能力,比如NIO替代IO。
为什么Redis 单线程就性能高呢?
Redis是典型的单线程模型,它非常高效,基本操作能达到10w/s。从单线程角度看,部分原因在于:
- 多线程会存在线程上下文切换开销,单线程没有此种开销;
- 锁
更本质的原因为:Redis基本都是内存操作(很少IO操作),这种情况下单线程可以高效地利用CPU。而多线程使用场景一般是:存在相当比例的IO和网络操作。
参考
- Java并发编程实战 8.2节
- http://ifeve.com/how-to-calculate-threadpool-size/