周五下午4点58分,代码审查通过,CI绿灯,我点了合并。48小时后,监控大屏像被拔了电源——0台服务器响应请求。
这不是压力测试的剧本,是2023年Q3某个真实周一的早晨。内存泄漏像缓慢漏水的水龙头,周末没人听见,等发现时地板已经泡穿了。
4:58 PM:一个"无害"的优化
改动很小。一个搜索服务的缓存层重构,把原来的同步加载改成了异步批量预取。本地测试通过,单元测试覆盖率92%,内存曲线平稳得像机场跑道。
代码审查时,同事问了一句:"这个CompletableFuture的链式调用,超时处理在哪?"我指了行号:"这里,30秒熔断。"他没再追问。我们都漏看了另一行——异步任务没绑定请求生命周期,用户连接断开后,后台线程还在拼命攒数据。
内存泄漏的阴险之处:它不在测试环境发作。
测试数据量太小,异步任务赶在用户断开前就完工了。生产环境有200万日活,平均会话时长47分钟,任务队列越积越长,像餐厅后厨没洗的盘子。
周末:沉默的膨胀
周六凌晨2:17,第一台服务器告警:堆内存使用率87%。值班工程师扫了眼图表,曲线平缓上升,没触发自动扩容阈值。他备注了"观察",继续刷剧。
周日凌晨,第八台服务器加入集群。新机器分担了流量,老机器的内存压力暂时缓解——这不是修复,是麻醉。泄漏速度被稀释了,但总量在累积。
周日晚上11点,集群平均内存占用91%。自动扩容策略启动,又加了4台。这时候每台服务器都在泄漏,只是泄漏速度刚好低于新机器的加入速度。系统像一艘不断换木板的船,没人发现木板在腐烂。
周一 9:03 AM:级联崩溃
早高峰流量涌入。新请求需要内存,老请求占着不释放,垃圾回收器(GC)开始疯狂运转。Full GC间隔从小时级压缩到分钟级,再压缩到秒级。
9:03,第一台服务器GC overhead limit exceeded,进程僵死。负载均衡器把它踢出池子,流量涌向剩下的11台。每台多分9%的请求,内存泄漏速度陡增。
9:07,三台同时挂掉。剩余八台每台多分37%流量。这时候系统进入死亡螺旋:越忙越漏,越漏越忙。
9:12,最后一台服务器在尝试响应健康检查时,堆内存溢出(OOM),整个集群归零。监控大屏那个"0",是我职业生涯见过最刺眼的数字。
复盘:三个被低估的假设
事后复盘写了14页,核心失误不是技术深度,是决策时的隐性假设。
第一,假设"异步=更快",没问"异步的代价是什么"。异步解除了I/O阻塞,但引入了生命周期管理的复杂度。请求和任务解耦后,谁来当那个关灯的人?
第二,假设"监控会告诉我们"。我们有200多个监控指标,唯独没设"异步任务队列深度"。内存使用率是结果,队列深度才是先兆。盯着体温计,没摸额头。
第三,假设"周末流量低=安全窗口"。内存泄漏不看流量,看时间。48小时的缓慢泄漏,足够把任何规模的堆填平。周五下午部署高危变更,本质是用生产环境当测试环境。
修复花了6小时,重构花了3周。
临时方案是重启+限流,把异步改回同步,性能下降40%但稳定。长期方案引入了请求作用域的线程池,每个用户会话绑定一个任务队列,连接断开时强制取消未完成任务。代码量增加了三倍,但每条线程都有身份证和紧急联系人。
那位周末值班的工程师后来跟我说,他看了眼图表觉得"趋势可控"。我问他,如果当时看到内存曲线和连接数曲线背离——内存涨、连接数跌——会不会警觉?他说会。但这个指标我们当时没有。
现在有了。每次部署搜索服务变更,值班手册第一行写着:"检查异步任务队列深度,检查内存/连接数背离率。"
那个周一之后,我养成了一个习惯:周五下午4点后的合并请求,必须回答一个问题——"如果这个改动导致渐进式故障,第一个被淹没的指标是什么?"
答案不能是"内存使用率"。那是溺水者的最后一声咳嗽,不是救生哨。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.