上周四晚上,我经历了入职以来最漫长的一夜。
那天晚上十点多,我正准备睡觉,手机突然响了,是运维群里的告警。我们的订单服务出现了大量超时,错误率飙升,已经影响到了用户下单。我赶紧打开电脑,开始排查。
这一查,就是一整夜。最后发现,导致故障的根本原因,是一行由AI生成的代码。
这篇文章我想完整记录这次故障的排查过程,从发现问题、紧急止血、根因定位到最终修复,把整个过程分享出来。不是为了甩锅给AI,而是想提醒所有用AI写代码的开发者,对AI生成的代码一定要保持警惕。
故障发生:晚上十点的告警
事情是这样的。
那天下午,我们发布了一个新版本,主要是优化订单创建的逻辑,提升下单的性能。发布之后监控看起来一切正常,错误率没有升高,响应时间也没有变慢,大家就都下班了。
结果到了晚上十点,订单量开始上升的时候,问题出现了。监控显示订单服务的P99响应时间从平时的200毫秒飙升到了5秒,错误率也从0.01%升到了8%,而且还在持续上升。
我第一时间登录服务器查看日志。日志里全是数据库连接超时的错误,看起来像是数据库连接池被打满了。但奇怪的是,数据库本身的负载并不高,CPU和内存都很正常,慢查询也没有明显增加。
这就很奇怪了。数据库连接池被打满,但数据库本身压力不大,说明连接不是被慢查询占住的,而是被什么东西卡住了,连接没有被释放。
我赶紧通知了团队的其他同事,大家都上线开始排查。
紧急止血:回滚还是限流
排查的同时,我们需要先止血,不能让故障继续扩大。
当时有两个选择,一个是回滚到上一个版本,另一个是先限流,降低订单服务的流量,边排查边看。
回滚是最稳妥的办法,但回滚需要时间,而且会丢失下午发布的新功能。限流的话,能快速降低压力,但用户体验会受影响,而且如果问题不是流量导致的,限流可能也没用。
我们先试了限流,把订单服务的流量降到了平时的30%。限流之后,错误率确实降下来了,但连接池还是满的,没有释放。这说明问题不是流量太大导致的,而是代码里有什么地方在持有连接不释放。
既然限流没用,我们决定回滚。回滚到上一个版本之后,连接池很快就恢复了正常,错误率也降下来了。这时候已经是晚上十一点半了,故障暂时得到了控制。
但我们都知道,回滚只是止血,根因还没找到。如果不找到根因,下次发布还会出同样的问题。于是我们决定,今晚必须把问题查清楚。
排查过程:一步步缩小范围
回滚之后,我们开始排查根因。
首先,我们对比了新版本和上一个版本的代码差异。改动主要集中在订单创建的几个方法里,大概改了两百多行代码。这些改动是一个同事做的,他说大部分代码是用AI生成的,他自己做了一些调整。
我们把改动的代码逐行看了一遍,表面上看逻辑没问题,就是把原来的一些同步操作改成了异步,优化了一些查询,应该是提升性能的,不应该导致连接池被打满。
但问题肯定出在这些改动里。我们决定把改动的代码一点点还原,看看到底是哪一行导致的问题。
我们先在测试环境复现了故障。用压测工具模拟高并发下单,果然,压了几分钟之后,连接池就被打满了。有了复现环境,排查就快多了。
我们用二分法,先还原一半的改动,压测,没问题;再还原另一半,压测,问题复现了。这样一步步缩小范围,最后定位到了一个方法。
这个方法是用来计算订单优惠的。看起来逻辑很简单,就是根据用户的等级和订单金额,计算优惠金额。但仔细看代码,我们发现了一个奇怪的地方。
根因:一行AI生成的代码
问题出在这一行代码上:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public BigDecimal calculateDiscount(Long userId, BigDecimal amount) {
// ... 计算优惠的逻辑
}这行代码是AI生成的。原来的代码没有@Transactional注解,AI在生成代码的时候,自动加上了这个注解,而且用的是REQUIRES_NEW传播级别。
REQUIRES_NEW是什么意思呢?就是每次调用这个方法,都会开启一个新的事务,挂起当前的事务。这个方法本身是查询操作,不需要事务,但加上了这个注解之后,每次调用都会从连接池拿一个连接,开启一个新事务。
问题在于,这个方法在订单创建的流程里被循环调用了。一个订单里有多个商品,每个商品都要调用一次这个方法计算优惠。也就是说,创建一个订单,这个方法会被调用好几次,每次都开启一个新事务,占用一个数据库连接。
在低并发的时候,连接池够用,问题不明显。但到了晚上订单量上来的时候,并发高了,大量的连接被这些新事务占用,而且因为外层事务还没提交,这些新事务的连接也不能释放,很快连接池就被打满了。
找到根因的时候,已经是凌晨三点多了。我们都松了一口气,但也很无语。就因为AI自动加了这么一行注解,导致了这么大的故障,我们排查了一整夜。
为什么会出现这个问题
找到了根因之后,我们复盘了一下,为什么会出现这个问题。
首先,AI生成代码的时候,会"自作主张"地加一些它认为合理的东西。比如这个@Transactional注解,AI可能觉得凡是数据库操作都应该加事务,于是就自动加上了。但它没有考虑到这个方法是查询操作,不需要事务,更没有考虑到REQUIRES_NEW在循环里调用会导致连接池问题。
其次,代码审查的时候没有发现这个问题。那个同事拿到AI生成的代码,主要看了业务逻辑对不对,没有注意到这个注解的变化。代码评审的时候,其他同事也没有注意到这个细节,毕竟一个注解的变化太不起眼了。
第三,测试没有覆盖到高并发场景。我们的测试主要是功能测试,测了下单流程能不能跑通,优惠计算对不对,但没有做高并发压测。这个问题在低并发下不会暴露,只有在高并发下才会出现连接池被打满的情况。
第四,对AI生成代码的警惕性不够。我们总觉得AI生成的代码逻辑对了就行,没有仔细检查那些"额外"加的东西。但恰恰是这些看起来合理的额外代码,可能隐藏着大问题。
修复和改进
找到根因之后,修复就很简单了。把那个@Transactional注解去掉,或者改成只读事务,问题就解决了。我们在测试环境验证了一下,压测了半个小时,连接池一切正常,没有再出现问题。
周五上午,我们重新发布了修复后的版本,线上一切正常。
但这次故障给我们敲了警钟,我们做了一系列改进。
第一,建立了AI生成代码的审查规范。AI生成的代码必须逐行审查,特别注意那些AI自动加的注解、配置、依赖,不能只看业务逻辑。审查的时候要问自己,这行代码是必要的吗?会不会有副作用?
第二,加强了发布前的压测。对于涉及数据库操作、高并发场景的改动,发布前必须做压测,确保在高并发下没有性能问题和资源泄漏。
第三,完善了监控告警。我们增加了数据库连接池使用率的监控,连接池使用率超过80%就告警,这样能更早发现问题,不用等到连接池被打满才知道。
第四,对团队做了AI编程的培训。分享了这次故障的经过,提醒大家对AI生成的代码保持警惕,不要盲目信任,要理解每一行代码的作用。
经验教训
这次排查了一夜的故障,给了我很多经验教训。
第一,AI生成的代码必须仔细审查。AI不是神,它会犯错,会自作主张,会生成看起来合理但有问题的代码。不要因为是AI写的就放松警惕,要像审查新手写的代码一样,逐行仔细看。
第二,特别注意AI自动加的东西。AI生成代码的时候,经常会自动加一些注解、配置、异常处理、日志等。这些东西看起来合理,但可能和你的项目不兼容,或者有副作用。要重点检查这些"额外"的代码。
第三,不要只测功能,还要测性能和并发。很多问题在功能测试中发现不了,只有在高并发、大数据量的情况下才会暴露。特别是涉及数据库连接、线程池、缓存这些资源的代码,一定要做压测。
第四,线上故障要先止血再排查。出了问题不要想着一下子找到根因,先回滚或者限流,把故障控制住,再慢慢排查。用户体验比什么都重要,不要让用户陪着你排查问题。
第五,排查问题要有方法论。这次我们用二分法缩小范围,很快就定位到了问题。排查问题不要瞎猜,要有系统的方法,一步步缩小范围,最后定位到根因。
第六,AI是工具,人要负责。AI能帮你写代码,但不能替你负责。代码出了问题,背锅的是你,不是AI。所以,你必须理解你提交的每一行代码,必须对代码的质量和安全负责。
写在最后
这次故障排查了一整夜,身体很累,但收获也很多。
AI编程确实能提升效率,能帮我们写很多重复的代码,让我们专注于更有创造性的工作。但AI不是万能的,它会犯错,会生成有问题的代码。作为开发者,我们不能因为有了AI就放弃思考,放弃审查,放弃对代码质量的追求。
这次的问题说大不大,说小不小。如果不是及时发现,可能会造成更大的损失。但它也给我们敲了警钟,让我们重新审视了AI编程的流程和规范。
我相信,随着AI技术的发展,AI生成的代码会越来越可靠。但在那之前,我们还是要保持警惕,用好AI这个工具,同时守住代码质量的底线。
最后用一句话来结束这篇文章:"AI写的代码,出了问题,锅还是你的。"
愿每一个开发者都能在AI时代写出高质量、可靠的代码,不再熬夜排查故障。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录