运维同学漏配了线程池队列,百万请求压垮了支付系统

2 02 3 年,我那个朋友公司出了个大问题,他们系统崩溃了。
原因就是线程池的队列没设置容量限制,导致任务无限堆积,最后内存溢出(OOM)了。

他们用的是一个LinkedBlockingQueue(),这东西是个无界队列,结果就是任务越积越多。
他们设置的最大线程数(2 00个)也太大,超出了系统承受能力。
堆内存被任务占用得太多(1 0万任务×1 MB=1 00GB),超过了JVM的最大堆内存(8 GB)。
结果就是线程爆炸,线程数从2 00暴涨到5 000+,操作系统都限制不住了,线程创建失败。

频繁的FullGC也让系统更不堪重负。
我朋友公司这事儿就是没好好配置线程池参数,比如队列应该用有界队列,比如ArrayBlockingQueue,容量要结合业务延迟要求来定。
最大线程数也不应该设那么高,一般不超过CPU核数的两倍。

解决方法就是重新配置线程池,使用有界队列,并且设置合理的核心线程数和最大线程数。
还要有拒绝策略,比如用CallerRunsPolicy。
还要监控和动态调参,压力测试也很重要。
总之,监控必须到位,动态调参能力要强,压力测试要真实模拟场景。

这个事故给他们敲了个大警钟,监控、调参、测试一个都不能少。
扩展阅读他们也看了,《Java并发编程实战》和Oracle官方文档,还有美团技术博客上的文章,这都是他们的宝贵经验。

线程池有哪几个重要参数?

哎哟,说起线程池,我真是又爱又恨啊。
记得那一年,我在公司接手一个大数据处理的项目,那可真是人山人海的任务,我那时候就傻眼了,不知道怎么分配线程。

首先,得确定核心线程数(corePoolSize),这个得根据机器的CPU核心数来定。
我那时候直接设置成了CPU核心数的两倍,心想反正闲着也是闲着,结果呢,机器都热得要命,效率反而下降了。
后来我调整到核心数的1 .5 倍,这才好多了。

然后是最大线程数(maximumPoolSize),这个得看任务量了。
我那时候一开始设置得太大,结果内存不够用,系统崩溃了好几次。
后来我根据内存大小和任务量,调整到合适的大小,这才稳定下来。

空闲线程存活时间(keepAliveTime)这个参数,我一开始没注意,结果很多线程一直闲着,浪费资源。
后来我设置了合适的存活时间,多余的线程该休息就休息,效率也提高了。

时间单位(unit)这个嘛,我一开始用秒,结果算错了,后来改用毫秒,就方便多了。

工作队列(workQueue)这个,我一开始用了个无界队列,结果任务太多,内存爆了。
后来改用有界队列,设置了个合理的容量,这才避免了内存溢出。

线程工厂(threadFactory)这个,我一开始没注意,结果线程名称都乱七八糟的,不好管理。
后来我自定义了一个线程工厂,把线程名称设置成有规律的,方便多了。

拒绝策略(handler)这个,我一开始用抛出异常,结果客户端崩溃了好几次。
后来改用丢弃任务,虽然有点浪费,但总比客户端崩溃好。

总之,线程池的参数配置得合理,对性能提升真的很大。
不过,这得根据实际情况来调整,不能一概而论。
这块儿我没碰过,我不敢乱讲,大家得根据自己的项目情况来定。

【技术分享】线程池核心线程数与最大线程数的区别|任务数超过最大线程数怎么办

说实话,线程池这东西用多了,核心线程数和最大线程数的区别还是容易搞混的。
我刚开始搞Java多线程那会儿,就经常把这两个搞错,后来碰了个壁才真正理解。

记得有次给一个老系统加功能,业务说要支持更高的并发量。
我直接把最大线程数拉到CPU核心数的两倍,结果系统一上线就崩了。
后来排查发现,核心线程数没设好,导致线程频繁创建销毁,CPU直接被干烧。
这就是典型的把核心线程当最大线程用了,后果很严重。

说白了,核心线程数就是线程池里"长期住户"。
比如你设置corePoolSize=5 ,那这个线程池至少得有5 个线程活着,就算没事干也在这儿待着。
我刚做架构师那会儿,带个实习生,他问我为啥银行系统总要开那么多空闲线程,我就给他举了这个例子。
银行转账任务多但响应要求高,核心线程多能保证随时响应。

最大线程数则是"最大容量"。
假设你maximumPoolSize=1 0,就算你塞1 00个任务进队列,只要线程池里线程不够1 0个,就会继续新开线程。
我之前在电商公司做秒杀系统,就遇到过这种情况。
用户量太猛,任务队列直接爆仓,最后不得不把最大线程数调到5 0,才勉强扛住。

有意思的是,任务超过最大线程数后的处理策略,才是真正考验架构能力的部分。
我见过太多系统因为策略选错导致问题。
AbortPolicy(默认策略)最直接,你想想那种情况:任务提交时直接抛出异常,就像你订了餐厅但没位子,服务员直接告诉你"没空"然后走人。
这种适合高可靠性场景,比如支付系统,丢任务比卡死用户更可怕。

CallerRunsPolicy(调用者跑)就特别有意思。
这种策略会把任务扔回调用者线程。
我有个项目用这个,结果发现线程池突然变慢了——原来把CPU密集型任务扔回用户线程了。
不过这种适合I/O密集型任务,能避免资源浪费。

DiscardPolicy(默默丢弃)看似最简单,但风险最大。
我之前有个监控系统用这个,半夜收到一堆告警,发现是系统把重要任务给丢了。
监控告警能不急吗?最后只能加重队列,结果队列满了又变成AbortPolicy了。
这种适合临时性任务,比如日志记录。

DiscardOldestPolicy(丢最老的)是个折中方案。
我重构一个老系统时用过,发现队列里总有些过时的任务意义不大。
就像你排队时发现前面有人插队,但忍了。
这种适合更新频繁的场景。

自定义策略这块我就没亲自跑过,但确实是个好东西。
我认识个老架构师,自己搞了个策略,把超时任务转存到另一个系统。
说实话,这种活儿挺考验功底的。

现在想想,线程池设计其实是个平衡术。
核心线程保日常,最大线程保峰值,饱和策略保底线。
我当年踩坑就是没搞懂这个平衡,现在带新人还是会反复讲这个。
系统上线前,一定要用压力测试把这几个参数调到最佳状态,不然问题来了真头疼。