修复代码漏洞
凌晨两点,修复告警群弹出一条消息:支付回调接口成功率从99.8%掉到71%。代码我盯着屏幕,漏洞第一反应不是修复查日志,而是代码翻最近两小时的发布记录。果然,漏洞运维在一点四十推了一个“小版本”,修复改动只有一行——把订单查询的代码超时时间从3秒改成了300毫秒。
问题就出在这。漏洞回调处理逻辑里,修复有一段代码先查订单状态,代码再决定是漏洞否更新。原本3秒够用,修复因为数据库偶尔有锁等待。代码改成300毫秒后,漏洞大量请求在查询阶段直接超时,代码捕获异常后走了“订单不存在”的分支,把本应成功的支付标记成了失败。更麻烦的是,这个分支里还写了一个重试队列,结果失败订单被反复重试,把数据库连接池打满了。
修复只用了十分钟。把超时改回3秒,加了一个熔断开关,再把重试逻辑改成指数退避。但排查花了四十分钟。为什么?因为日志里只打印了“订单查询失败”,没打具体异常类型,也没打当前超时配置。我不得不翻代码,找到那行catch (Exception e),才确认是超时异常被吞掉了。
这让我想起上个月另一个漏洞。用户上传头像,前端做了尺寸校验,后端没做。有人用脚本传了一张8000×8000的PNG,解压后占200MB内存,直接触发了OOM。修复方案很简单,后端加一道尺寸检查。但那个漏洞藏了三个月,因为测试用例只覆盖了正常尺寸。
代码漏洞很少是“写错了”那么简单。大多数时候,它是一连串小决策叠加的结果。超时时间设短一点,看起来更“敏捷”;异常捕获写宽一点,看起来更“健壮”;校验只做前端,看起来更“高效”。每个决策单独看都合理,合在一起就炸了。
我见过最隐蔽的一个漏洞,跟并发有关。一个优惠券领取接口,先查库存,再扣减。代码里加了锁,但锁的粒度不对——锁的是用户ID,不是优惠券ID。结果两个用户同时领同一张券,都能查到库存为1,然后都扣减成功,库存变成-1。修复方案是把锁换成基于优惠券ID的分布式锁。但上线后才发现,锁的过期时间设了30秒,而扣减逻辑里有一次远程调用,偶尔要35秒。锁提前释放,又出现超卖。最后改成锁续期,才彻底解决。
这些经历让我养成一个习惯:看代码时,先找“假设”。假设网络永远通、假设数据库永远快、假设输入永远合法、假设并发永远低。每个假设都是一颗雷。修复漏洞,本质上是把隐含的假设变成显式的检查。超时时间要可配置,异常要分类捕获,输入要前后端都校验,锁要覆盖整个临界区。
还有一点:别信“小改动”。那次超时从3秒改到300毫秒,提交信息写的是“优化查询性能”。没人觉得有问题。但生产环境的复杂性,从来不给“小改动”面子。我现在要求所有涉及超时、重试、锁、缓存的改动,必须附带压测报告。没有报告,不批发布。
凌晨三点,支付成功率回到99.9%。我关掉电脑,脑子里还在想那个被吞掉的超时异常。如果当初写代码的人多打一行日志,我可能二十分钟就能定位。但话说回来,如果当初没人改那个超时,这行日志永远用不上。漏洞修复就是这样,你永远在跟过去的自己和同事博弈。赢的方式只有一种:让下一个凌晨两点,少一条告警。
(责任编辑:焦点)
