下一章 上一章 目录 设置
15、解决问题 ...
-
两个人用最快的速度出了门。凌晨两点的街道空旷得像被清空的回收站,路灯孤零零地亮着,把柏油路面照出一层冷白色的光。傅北辰开车的速度比平时快了很多,但依然稳——他的手握在方向盘上,指节微微泛白,目光直视前方,嘴唇抿成了一条线。
林溪坐在副驾驶上,膝盖上放着笔记本电脑,已经连上了公司的□□,正在查看实时的监控数据。屏幕上的曲线像垂死之人的心电图,一截一截地往下掉,每一秒都有几十个交易超时的报错在刷新。
“连接数已经飙到八百了。”她声音发紧,“池子的上限是五百。现在所有的新请求都在排队,超时时间是三秒,三秒之后拿不到连接就报错。”
“GC日志看了吗?”
“还没——看到了。Full GC的频率不正常,十分钟一次。堆内存的使用率在连接数飙升的时候同步上涨。”
“说明连接对象没有被回收。”傅北辰踩了一脚油门,车子加速驶过了一个黄灯,“连接泄漏导致连接对象堆积在堆内存里,GC没法回收,因为每个连接对象都还被某个线程引用着——那个引用没有被释放。”
“但是我的finally块——”
“到现场再看。”傅北辰打断了她,“现在不要猜,到了之后看堆栈。”
林溪合上电脑,深吸了一口气。她的手在微微发抖——不是因为冷,是因为紧张。这个支付系统每天处理几千万的交易流水,每宕机一分钟,损失都是六位数起步。更严重的是,用户付了钱但订单状态没更新,客诉电话已经在客服中心炸锅了。
傅北辰空出一只手,握住了她发抖的手。
“会好的。”他说,声音很轻,但很稳,“我们一起修。”
林溪握紧了他的手,感觉到他掌心的温度从指尖传过来,一点一点地把她的恐慌压下去。
到了公司,整个技术中心灯火通明。
运维组、后端组、DBA——所有相关的团队都已经被叫回来了。走廊里弥漫着速溶咖啡和焦虑的气味,每个人的表情都很凝重。老周站在会议室的白板前面,正在画故障的链路图,看到林溪和傅北辰进来,立刻招手。
“来了!快过来看——这是我们目前的分析。”
林溪走到白板前面,快速扫了一眼老周画的图。故障的链路已经很清晰了:用户请求 -> 支付网关 -> 支付核心模块 -> 数据库连接池 -> 数据库。瓶颈在连接池这一环,池子被耗尽了,所有后续请求都在等待连接,超时之后返回错误。
“你们那个支付模块的代码,”老周转过头看着林溪,“是不是最近刚重构过?”
“是。上周上线的。”林溪没有回避,“但我做了充分的测试,单元测试覆盖率百分之九十二,集成测试也跑过了,压测——”
“压测的并发量是多少?”傅北辰突然问。
林溪愣了一下:“……五百。”
“生产环境的峰值并发是多少?”
“……一千二。”
傅北辰看了她一眼,没有说任何责备的话,但他的沉默比任何话都更有分量。压测的并发量只有生产环境峰值的一半——这意味着她在测试环境里根本没有模拟出真实的生产压力,那个“理论上”没问题的代码,在高并发场景下暴露了隐藏的bug。
“先看代码。”傅北辰说,声音平静得像在做一次常规的code review,“不要追责,不要复盘,先解决问题。”
他走到林溪的工位前,坐下来,打开她的电脑。林溪站在他旁边,手指在键盘上敲击,快速定位到了支付模块的核心代码——那个负责管理数据库连接的部分。
代码在她面前展开,她一行一行地看过去,心跳越来越快。
finally块里确实有释放连接的逻辑——conn.close()。但她在释放之前加了一个判断:如果连接上有未提交的事务,就先回滚再释放。这个判断本身没有问题,问题在于——她在判断逻辑里调用了另一个方法,那个方法里又打开了一个新的数据库连接,用于记录回滚日志。
那个新的连接,没有在finally块里被释放。
因为它是在判断逻辑内部打开的,而finally块只释放了最外层的conn。内层的那个连接——用于写日志的那个——成了一个孤儿连接,永远挂在连接池里,永远不会被回收。
在高并发场景下,每一个请求都会产生一个这样的孤儿连接。五百个并发的时候,池子勉强撑得住。但到了一千二的时候,孤儿连接的数量在几分钟内就把池子填满了,然后所有正常的请求都拿不到连接,系统崩溃。
林溪盯着屏幕上的那段代码,整个人像是被人从头到脚浇了一盆冰水。
她写了一个bug。一个在低并发下不会暴露、只有在生产环境的高峰期才会炸开的bug。这个bug导致了整个支付系统的崩溃,造成了数以万计的交易失败,让公司的客服中心被客诉淹没。
“是我的问题。”她说,声音低得几乎听不清,“我在写日志的时候开了新的连接,忘记关了。”
她的声音在发抖,但她没有哭。她的眼眶很红,嘴唇抿得很紧,手指攥在身侧,指节泛白。
傅北辰没有说话。他快速地把代码看了一遍,然后打开终端,连上了数据库,执行了一条查询语句。屏幕上显示的数据让所有人都沉默了——孤儿连接的数量,三千七百二十一个。
三千七百二十一个连接,孤零零地挂在数据库里,占用着资源,等待着永远不会到来的关闭指令。
“可以修。”傅北辰说,打破了沉默,“不需要回滚代码。在写日志的方法里加上连接的释放逻辑就行。另外,连接池的配置需要改一下——把空闲连接的回收时间从三十分钟改成五分钟,这样即使以后再出现泄漏,孤儿连接也会被快速回收,不会把池子堵死。”
他转头看着林溪:“你来改。”
林溪抬起头,看着他。他的眼神很平静,没有责备,没有同情,只有一种纯粹的、基于信任的期待。
“这是你写的代码,”傅北辰说,“你来修。”
林溪深吸了一口气,坐到了自己的工位上。她的手指在键盘上悬了一秒,然后开始敲击。
她写得很快,但每一个字符都经过了反复的推敲。她在写日志的方法里加了一个finally块,确保无论发生什么异常,连接都会被正确关闭。她在连接的创建逻辑里加了一个追踪ID,这样以后如果再有泄漏,可以通过日志快速定位到是哪个请求创建的连接。她在连接池的配置文件中改了两个参数,把空闲连接的回收时间从三十分钟降到了五分钟,把最大等待时间从三秒降到了两秒,让用户在系统压力大的时候能更快地得到反馈,而不是傻等三秒然后超时。
她写了四十分钟,改了三处代码,优化了两个配置,写了一篇详细的变更说明。然后把代码提交,部署到预发布环境,跑了一遍回归测试——全部通过。
“可以上线了。”她对老周说。
老周点了点头,在变更审批单上签了字。
林溪按下部署按钮的时候,手指是稳的。但当屏幕上弹出“Deploy Success”的提示时,她的手指开始发抖——那种肾上腺素退潮之后的、无法控制的颤抖。
她靠在椅背上,闭上眼睛,感觉到心脏在胸腔里狂跳。
生产环境的数据在十分钟内恢复了正常。连接数从八百降到了一百二,GC频率从十分钟一次降到了一小时一次,交易成功率从百分之七十三回升到了百分之九十九点九七。
所有的曲线都回到了正常的轨道上。
走廊里的速溶咖啡味还在,但焦虑的气味已经散了。运维组的同事击了个掌,DBA打了个哈欠说“终于能回去睡觉了”,老周长长地松了一口气,拍了拍林溪的肩膀。
“修得好。”他说,“下次注意。”
林溪点了点头,没有说话。
等所有人都散了,办公室里只剩下她和傅北辰。
凌晨四点的技术中心安静得像一个被关机的服务器,只有空调系统的嗡嗡声和远处偶尔传来的电梯提示音。窗外的天空从纯黑变成了一种深沉的靛蓝色,像一块被洗了很多次的牛仔裤布料,边缘开始泛出一丝微弱的灰白。
林溪坐在工位上,对着屏幕,一动不动。
“林溪。”傅北辰的声音从旁边传来。
“嗯。”
“你在想什么?”
“我在想,我写了一个bug,让公司损失了多少钱。”
“不要想这个。”
“我在想,如果我在压测的时候把并发量调高一点,就能发现这个bug,就不会让它上线。”
“不要想这个。”