深夜十一点,我还在沙发上打瞌睡,手机突然疯狂震动。打开一看,全是 APM 的报警短信,四台负载机同时 OOM 了。没办法,运维只能紧急重启,好歹让业务续了口气。可这事儿太怪了,根本查不出具体是哪儿出的毛病。 重启完了内存 Dump 也没了,只能回头看监控。这不看不要紧,从下午四点开始线程数就像坐火箭一样往上窜,3 万条跟平常的 600 条一对比,差距大得吓人。更巧的是,这个时间线刚好跟那次代码发布对上了。 接着我去看了代码版本差异,就发现了一处改动:HttpClient 初始化的时候多了个 evictExpiredConnections。本来以为这是个“用完就关”的小优化,结果它干了件坏事——每发一个请求就启动一个定时清理线程,瞬间把四台机器的后台线程数给挤爆了。 这其实是 keep-alive 的锅。HTTP 1.1 默认开着 keep-alive 是为了复用 TCP 连接,少花点儿钱在三次握手和四次挥手上。省下钱的代价是可能会有问题。 比如服务器端超时了就会发 FIN 包去关连接;要是客户端在 FIN 包还没到的时候还在发包,服务器就会发 RST 包给客户端。这时候客户端一接收到就会报 NoHttpResponseException。 遇到这种情况咋办?要么重试重连躲着已关闭的连接走;要么就定时清扫闲置的。“evictExpiredConnections”其实就是定时清扫这一招的开关。 最倒霉的是四台机器当时权重一样、硬件也一样,大家都在同一时间到达了请求峰值。后台线程齐刷刷爆表了,四台机器就集体 OOM 了。 为了保住命,我把 HttpClient 设成了单例只保留一条定时清理线程;运维那边还加装了线程数阈值告警。这样就能在真的 OOM 前先把异常扼杀在摇篮里。 复盘下来,这次事故归根到底还是对库特性理解得不够深、网络知识掌握得太少。把 HttpClient 当成单例用、把 keep-alive 当成万能钥匙、把定时任务当成免费的午餐,这任何一个小疏忽都会引火烧身。 所以啊,新功能上线前一定要先翻翻源码看看官方的警告;网络协议这些细节往往能决定成败;最后监控阈值这堵墙得砌牢了。