一个查询从8秒压到40毫秒,工程师以为功德圆满。上线当天,1万并发直接把数据库打挂。
这不是段子。Deploy & Pray团队上周刚踩完这个坑,完整复盘发在了技术社区。他们的经历像极了一个经典陷阱:优化了局部,却毁掉了全局。
第一阶段:慢查询的"外科手术"
问题始于一个搜索接口。用户输入关键词,系统返回匹配结果,原始查询平均耗时8秒。
团队定位到瓶颈:全表扫描加多表关联,数据量上去后性能断崖式下跌。工程师决定动手——加索引、改写查询逻辑、砍掉不必要的字段返回。
优化后单测成绩:40毫秒。200倍的提升。
「当时我们觉得这事成了。」团队成员在复盘里写道。测试环境反复验证,响应稳定,资源占用合理。按常规流程,这时候就该庆祝了。
但他们漏掉了一个变量:真实世界的并发。
第二阶段:压测缺席的致命盲区
新查询上线,流量逐渐切过来。起初几百用户,一切正常。然后峰值突然冲到1万并发,数据库连接池耗尽,CPU飙到100%,服务雪崩。
40毫秒的查询,怎么就把数据库打死了?
团队事后分析发现两个致命点。第一,新查询虽然快,但执行计划里用到了一个新加的复合索引。这个索引在并发读取时表现优异,却和写入操作产生了严重的锁竞争。
第二,查询返回的数据量被低估了。单条40毫秒,乘以1万并发,再加上每个请求触发的后续关联查询,数据库的I/O瞬间被撑爆。
「快」和「能扛」完全是两回事。前者是单用户视角,后者是系统视角。
第三阶段:索引锁的隐藏代价
深入排查后,团队锁定了真正的凶手:索引锁(index lock)机制。
他们的业务场景是写密集型——用户搜索的同时,后台持续有数据写入和更新。MySQL的InnoDB引擎在处理索引时,为了保证一致性,会对索引页加锁。新加的复合索引字段恰好是高频更新列,读请求和写请求在索引层形成了队列。
单个查询40毫秒,但锁等待时间累加起来,部分请求拖到了数秒。连接池被这些慢请求占满,新请求进不来,老请求出不去,经典死锁。
团队用了一个精妙的类比:「就像把一条乡间小路拓宽成了高速公路,但没改红绿灯逻辑。车多了,路口反而堵成停车场。」
索引优化在只读场景下是神药,在读写混合场景下可能是毒药。
第四阶段:修复与教训
紧急回滚后,团队重新设计了解决方案。核心思路不是让查询更快,而是让系统更稳。
具体做了三件事。一,拆分读写流量,搜索请求走只读副本,彻底隔离主库的写入压力。二,引入缓存层,热点查询结果缓存30秒,直接砍掉90%的数据库访问。三,重写索引策略,去掉高频更新字段的复合索引,改用覆盖索引(covering index)减少回表。
二次上线后,同等1万并发,数据库CPU稳定在30%以下。
复盘文档里有一段话被大量转发:「我们花了两周优化查询速度,却忘了问一句——如果1万人同时点搜索,会发生什么?」
一个被反复验证的悖论
这个案例戳中了很多技术团队的痛点。性能优化往往从单请求入手,但生产环境的问题是立体的。
Deploy & Pray的工程师算了一笔账:优化前的8秒查询,虽然慢,但数据库连接占用时间长,天然限制了并发度,反而起到了「削峰」的副作用。优化后的40毫秒查询,连接释放快,并发能力被彻底释放,系统瞬间被流量冲垮。
换句话说,有时候慢是一种保护机制。把它变快,等于拆掉了护栏。
团队最后把这件事写进了 onboarding 文档,作为新工程师的必修课。标题很直接:「为什么我的优化杀死了生产环境」。
评论区有人追问:如果当初做了压测,能提前发现吗?作者的回复很诚实——「能发现一部分,但索引锁的竞争程度和数据分布强相关,模拟数据很难复现真实场景的写入模式。」
这大概是工程领域的永恒困境:你可以预防已知的风险,却永远在为未知的组合买单。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.