嘿,各位,今天咱们来聊聊那次闹得挺大的线程雪崩。四台机器同时 OOM,整个服务都瘫了,手机震动警报响个不停,情况紧急得不行。 事情是这样的,夜里十一点,运维这边手机震个不停,APM 的警报短信像雪片一样飞来,说是四台负载机同时 OOM,服务全线崩溃。大家赶紧重启机器,业务才勉强保住命,但问题根源到底在哪儿,还是一团迷雾。 先把机器重启了清空内存 Dump,团队就只能看 APM 的数据来找线索了。结果发现从 16 点开始,线程数疯狂上涨,3 万多线程跟正常 600 多个比起来简直是刺眼的存在。时间线跟代码部署对上了,看来这事儿跟这次代码改动脱不了干系。 翻了翻发布记录,果然只改动了一处:在 HttpClient 初始化的时候加了 evictExpiredConnections。本来打算把用完就关的配置打开,结果没想到居然给每一个请求都开了个后台定时清理线程。这一下可好,四台机器瞬间被数万条线程挤满了。 这时候咱们就得聊聊 HTTP 1.1 的 keep-alive 了。它本来是个好东西,能省掉很多创建断开 TCP 连接的开销,比如三次握手和四次挥手这种贵得要死的操作。图上展示得很清楚:同样三次请求,复用版就能省下两次创建断开的成本。 但这东西也有个大麻烦:超时和 FIN 的陷阱。服务器端如果超时了就会主动发 FIN 关掉连接;如果客户端在 FIN 还没收到的时候还在拼命发包,服务器就会回个 RST 过去。客户端一抓到这个 NoHttpResponseException 异常就得炸。这就是典型的“免费午餐”吃完后,垃圾清理不当反倒成了新隐患。 面对这种异常该怎么办?有两种策略:一种是重试机制,出了问题就自动重连避开已关闭的连接;另一种是定时清扫,在超时周期里主动关掉闲置的连接来避免 RST。这次加的 evictExpiredConnections 就是后者的开关。 关键的一点在于负载均衡下的“时间差”灾难。四台机器权重一样硬件也一样,结果在同一时刻到达了请求峰值。后台线程同步爆表之后就一起 OOM 了。 最后咱们总结一下止损方案:把 HttpClient 弄成单例的只保留一条清理线程;运维那边还得加上线程数阈值告警来提前扼杀隐患。 总之这次教训告诉我们:要读源码和官方警告;网络协议细节很关键;监控阈值是最后一道防线。