上周把线上服务从Go 1.16升级到Go 1.17 beta版本,本来以为是一次常规升级,没想到上线后出现了一个诡异的间歇性崩溃。我花了整整一夜排查,最终发现是一个非常隐蔽的问题。本文记录整个排查过程,希望能帮到遇到类似问题的朋友。

一、故障现象

升级后第一天,服务运行正常,压测也通过了。但第二天凌晨开始,监控告警突然响起,服务出现间歇性崩溃,表现为进程突然退出,没有任何panic日志。

崩溃的频率不固定,有时候半小时一次,有时候几小时一次。崩溃前没有任何征兆,CPU、内存、网络指标都正常。最诡异的是,同样的代码在测试环境跑了几天都没问题,一到线上就崩。

因为是凌晨,用户量不大,但这个问题不解决我不敢睡觉,于是开始了漫长的排查之旅。

二、初步排查

1. 查看日志。 首先看应用日志,崩溃前没有任何异常输出。再看系统日志,发现进程是被OOM Killer杀掉的。但监控显示内存使用率只有60%左右,不应该触发OOM。

2. 检查内存。 我开始怀疑是内存泄漏,但pprof的heap profile显示内存使用很平稳,没有持续增长。goroutine数量也正常,没有泄漏。

3. 复现问题。 在测试环境用同样的流量压测,跑了几个小时都没有复现。这让问题更加棘手——无法复现的Bug最难排查。

三、深入排查

初步排查没有头绪,我开始换思路。

1. 对比版本差异。 回滚到Go 1.16后,问题消失了。这说明问题确实和Go 1.17有关。我开始研究Go 1.17的变更日志,看看有哪些可能导致崩溃的变化。

Go 1.17的一个重要变化是引入了寄存器调用约定(register-based calling convention),函数参数和返回值不再全部通过栈传递,而是部分通过寄存器传递。这个变化理论上能提升性能,但也可能引入兼容性问题。

2. 检查cgo调用。 我们的服务用了一个cgo库来调用C实现的压缩算法。我开始怀疑是cgo和新的寄存器调用约定之间存在兼容性问题。

写了一个小测试程序,反复调用这个cgo库,跑了几分钟就崩溃了。终于在测试环境复现了!

3. 定位具体问题。 通过gdb调试崩溃的进程,发现崩溃发生在cgo调用返回后,栈上的某个变量被意外覆盖了。进一步分析发现,是因为C代码修改了某个寄存器,但Go 1.17的运行时认为这个寄存器应该被保留,导致后续使用这个寄存器时读到了错误的值。

简单来说,就是Go 1.17的新调用约定和这个C库之间存在寄存器保存的冲突。C代码没有按照Go的约定保存某些寄存器,而Go 1.17比1.16更依赖寄存器传递参数,所以问题暴露了出来。

四、解决方案

定位到问题后,解决方案就比较清晰了。

1. 短期方案:禁用寄存器调用约定。 Go 1.17提供了一个环境变量GOEXPERIMENT=noregabi,可以禁用新的寄存器调用约定,回退到传统的栈传递方式。设置这个环境变量后重新编译,问题立刻消失了。

这是最快的临时解决方案,但牺牲了Go 1.17的性能提升。

2. 中期方案:修复cgo库。 联系了cgo库的维护者,报告了这个问题。对方确认是C代码没有正确保存寄存器,发布了修复版本。升级库后,即使启用寄存器调用约定也不会崩溃了。

3. 长期方案:减少cgo依赖。 这次事件让我们意识到cgo的风险。我们开始评估用纯Go实现替代这个C库,虽然性能会略有下降,但可以避免类似的兼容性问题,也简化了交叉编译和部署。

五、排查过程中的教训

这一夜的排查让我收获了几个教训:

1. 不要急于升级大版本。 Go的每个大版本虽然都强调向后兼容,但底层的变化可能引入隐蔽的问题。升级前最好在预发环境充分测试,尤其是使用了cgo或unsafe的项目。

2. 关注版本变更日志。 升级前仔细阅读变更日志,重点关注运行时、编译器、cgo相关的变化。Go 1.17的寄存器调用约定是一个重大变化,我们当时没有给予足够重视。

3. 保留旧版本的回滚能力。 幸好我们保留了Go 1.16编译的镜像,发现问题后可以快速回滚,没有造成太大影响。如果无法回滚,后果会严重得多。

4. 监控要覆盖系统层面。 我们的应用监控很完善,但系统层面的监控不够细致。如果当时能更早发现OOM Killer的记录,排查速度会快很多。

5. 无法复现的问题要换思路。 一开始在测试环境无法复现,我花了很多时间在错误的方向上。后来通过对比版本差异、聚焦cgo调用,才找到了正确的排查方向。

六、写在最后

这个Bug从凌晨两点排查到早上八点,整整六个小时。找到原因的时候,虽然很累,但那种豁然开朗的感觉让一切都值得了。

Go 1.17正式版发布后,这个cgo库的兼容性问题应该会被更多人遇到。希望我的排查经历能帮到大家,如果你在升级Go 1.17后遇到类似的间歇性崩溃,不妨检查一下cgo调用和寄存器调用约定的兼容性。

最后提醒一句:新版本虽好,升级需谨慎。尤其是在生产环境,稳永远比新重要。