实例分享:记一次故障引起的线程池使用的思考

1、悬案

某日某晚 8 时许,一阵急促的报警电话响彻有赞分销员技术团队的工位,小虎同窗,小峰同窗纷纷打开监控平台一探究竟。分销员系统某核心应用,接口响应所有超时,dubbo 线程池被所有占满,并堆积了大量待处理任务,整个应用没法响应任何外部请求,处于“夯死”的状态。面试

正当虎峰两位同窗焦急的以各类姿式查看应用的各项指标时,5分钟过去了,应用竟然本身自动恢复了。看似虚惊一场,但果然如此吗?spring

2、勘查线索

2.1 QPS

“是否是又有商家没有备案就搞活动了啊”,小虎同窗如此说道。的确,对于应用忽然夯死,你们可能第一时间想到的就是流量突增。流量突增会给应用稳定性带来不小冲击,机器资源的消耗的增长至殆尽,就像咱们去自助餐厅胡吃海喝到最后一口水都喝不下,固然也就响应不了新的请求。咱们查看了 QPS 的情况。并发

事实让人失望,应用的 QPS 指标并无出现陡峰,处于一个相对平缓的上下浮动的状态,小虎同窗不由一口叹气,看来不是流量突增致使的.框架

2.2 GC

“是否是 GC 出问题了”,框架组一位资深的同窗说道。JVM 在 GC 时,会由于 Stop The World 的出现,致使整个应用产生短暂的停顿时间。若是 JVM 频繁的发生 Stop The World,或者停顿时间较长,会必定程度的影响应用处理请求的能力。可是咱们查看了 GC 日志,并无任何的异常,看来也不是 GC 异常致使的。运维

2.3 慢查

“是否是有慢查致使整个应用拖慢?”,DBA 同窗提出了本身的见解。当应用的高 QPS 接口出现慢查时,会致使处理请求的线程池中(dubbo 线程池),大量堆积处理慢查的线程,占用线程池资源,使新的请求线程处于线程池队列末端的等待状态,状况恶劣时,请求得不到及时响应,引起超时。但遗憾的是,出问题的时间段,并未发生慢查。工具

2.4 TIMEDOUT

问题至此已经扑朔迷离了,可是咱们的开发同窗并无放弃。仔细的小峰同窗在排查机器日志时,发现了一个异常现象,某个平时不怎么报错的接口,在1秒内被外部调用了 500 屡次,此后在那个时间段内,根据 traceid 这 500 屡次请求产生了 400 多条错误日志,而且错误日志最长有延后好几分钟的。ui

这是怎么回事呢?这里有两个问题让咱们迷惑不解:线程

  1. 500 QPS 彻底在这个接口承受范围内,压力还不够。3d

  2. 为何产生的错误日志可以被延后好几分钟。调试

日志中明显的指出,这个 http 请求 Read timed out。http 请求中读超时设置过长的话,最终的效果会和慢查同样,致使线程长时间占用线程池资源(dubbo 线程池),简言之,老的出不去,新的进不来。带着疑问,咱们翻到了代码。

可是代码中确实是设置了读超时的,那么延后的错误日志是怎么来的呢?咱们已经接近真相了吗?

3、破案

咱们难免对这个 RestTemplateBuilder 起了疑心,是这个家伙有什么暗藏的设置嘛?机智的小虎同窗,针对这个工具类,将线上的状况回放到本地进行了模拟。咱们构建了 500 个线程同时使用这个工具类去请求一个 http 接口,这个 http 接口让每一个请求都等待 2 秒后再返回,具体的作法很简单就是 Thread.sleep(2000),而后观察每次请求的 response 和 rt。

咱们发现 response 都是正常返回的(没有触发 Read timed out),rt是规律的5个一组而且有 2 秒的递增。看到这里,你们是否是感受到了什么?对!这里面有队列!经过继续跟踪代码,咱们找到了“元凶”。

这个工具类默认使用了队列去发起 http 请求,造成了相似 pool 的方式,而且 pool active size 仅有 5

如今咱们来还原下整个案件的通过:

  1. 500 个并发的请求同时访问了咱们应用的某个接口,将 dubbo 线程池迅速占满(dubbo 线程池大小为 200),这个接口内部逻辑须要访问一个内网的 http 接口

  2. 因为某些不可抗拒因素(运维同窗还在辛苦奋战),这个时间段内这个内网的 http 接口所有返回超时

  3. 这个接口发起 http 请求时,使用队列造成了相似 pool 的方式,而且 pool active size 仅有 5,因此消耗完 500 个请求大约须要 500/5*2s=200s,这 200s 内应用自己承担着大约 3000QPS 的请求,会有大约 3000*200=600000 个任务会进入 dubbo 线程池队列(如悬案中的日志截图)。PS:整个应用固然就凉凉咯。

  4. 消耗完这 500 个请求后,应用就开始慢慢恢复(恢复的速率与时间能够根据正常 rt 大体算一算,这里就不做展开了)。

4、思考

到这里,你们内心的一块石头已经落地。但回顾整个案件,无非就是咱们工做中或者面试中,常常碰到或被问到的一个问题:“对象池是怎么用的呢?线程池是怎么用的呢?队列又是怎么用的呢?它们的核心参数是怎么设置的呢?”。答案是没有标准答案,核心参数的设置,必定须要根据场景来。拿本案举例,本案涉及两个方面:

4.1 发起 http 请求的队列

这个使用队列造成的 pool 的场景是侧重 IO 的操做IO 操做的一个特性是须要有较长的等待时间,那咱们就能够为了提升吞吐量,而适当的调大 pool active size(反正你们就一块儿等等咯),这和线程池的的 maximum pool size 有着殊途同归之处。那调大至多少合适呢?能够根据这个接口调用状况,平均 QPS 是多少,峰值 QPS 是多少,rt 是多少等等元素,来调出一个合适的值,这必定是一个过程,而不是一次性决定的。那又有同窗会问了,我是一个新接口,我不知道历史数据怎么办呢?对于这种状况,若是条件容许的话,使用压测是一个不错的办法。根据改变压测条件,来调试出一个相对靠谱的值,上线后对其观察,再决定是否须要调整。

4.2 dubbo 线程池

在本案中,对于这个线程池的问题有两个,队列长度与拒绝策略。队列长度的问题显而易见,一个应用的负载能力,是能够经过各类手段衡量出来的。就像咱们去餐厅吃饭同样,顾客从上桌到下桌的平均时间(rt)是已知的,餐厅一天存储的食物也是已知的(机器资源)。当餐桌满了的时候,新来的客人须要排队,若是不限制队列的长度,一个餐厅外面排上个几万人,队列末尾的老哥好不容易轮到了他,但他已经饿死了或者餐厅已经没吃的了。这个时候,咱们就须要学会拒绝。能够告诉新来的客人,大家今天晚上是排不上的,去别家吧。也能够把那些吃完了,可是赖在餐桌上聊天的客人赶赶走(虽然现实中这么挺不礼貌,但也是一些自助餐限时2小时的缘由)。回到本案,若是咱们调低了队列的长度,增长了适当的拒绝策略,而且能够把长时间排队的任务移除掉(这么作有必定风险),能够必定程度的提升系统恢复的速度

最后补一句,咱们在使用一些第三方工具包的时候(就算它是 spring 的),须要了解其大体的实现,避免因参数设置不全,带来意外的“收获”。

相关文章
相关标签/搜索