TiDB是一个开源、兼容MySQL的分布式SQL数据库。其架构采用分层设计。存储层在Rust中实现,而计算层在Go中实现。像TiDB这样的分布式数据库是一个非常复杂的项目。它的性能取决于多种因素,例如运行它的操作系统和硬件平台,以及它使用的编译语言。
在本文中,我将分享一个导致TiDB在Advanced RISC Machine(ARM)平台上崩溃的特殊Go错误。
问题是什么?它是如何引起我的注意的?
TiDB经过了大量测试,以确保程序正确可靠,并捕获任何性能回归。对于新版本,我们会进行额外的性能和稳定性测试。
我们在TiDB 5.4的其中一个测试中首次发现了Go问题。在测试过程中,一名QA工程师报告数据库被卡住并且没有响应。我们再次进行了测试,但问题没有再次出现,所以当时我们没有给予足够的关注。
稍后,在TiDB6.0的发布测试中,我们遇到了相同的问题。这次我们可以稳定地复制它。我们还发现,使用ARM平台进行复制需要很长时间。有时是6-8小时,有时更长;但通常在24小时内。
问题看起来像什么?
出现此问题时,tidb服务器进程不再响应请求。然而,过程并没有恐慌,服务端口仍然打开。
如下面的监控界面所示,tidb-4-peer节点有问题。这个过程是活跃的。但是,由于它没有响应请求,因此没有为其记录数据。

登录到机器后,我观察到tidb服务器进程的CPU使用率为100%。在多核机器上,它只占用一个核。
Go Issue已验证
我想通过检查goroutine堆栈来了解当前进程正在做什么:
curl http://127.0.0.1:10080/debug/pprof/goroutine?debug=2 > goroutine.txt
但是,由于端口10080无法响应请求,此操作被卡住,无法获取相应的goroutine堆栈信息。
CPU使用率正好是一个内核的100%,所以我认为可能在某个地方存在死循环。复制在我们自己的测试环境中。这允许我们使用Delve(dlv),一个Go调试工具,来调试TiDB进程。
dlv attach 4637
如下面的调试日志所示,只有一个线程处于活动状态,而所有其他线程都处于futex状态。基本上,它们被锁定了。
(dlv) threads
* Thread 4637 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4639 at 0x132f504 /usr/local/go1.18/src/runtime/sys_linux_arm64.s:150 runtime.usleep
Thread 4640 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4641 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4642 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4643 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4644 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4645 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4646 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4647 at 0x132fb0c /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596 runtime.futex
Thread 4649 at 0x1320fa8 /usr/local/go1.18/src/runtime/symtab.go:1100 runtime.gentraceback
futex状态下线程的调用堆栈基本上在运行时。stopm函数。如下面的源代码片段所示,该调用将为垃圾收集(GC)执行一个停止世界操作,在该操作中,程序的执行被挂起,直到堆中的所有对象都被处理完毕。此操作需要获取锁,但失败了,因此操作挂起。
(dlv) bt
0 0x000000000132fb0c in runtime.futex
at /usr/local/go1.18/src/runtime/sys_linux_arm64.s:596
1 0x00000000012f6bbc in runtime.futexsleep
at /usr/local/go1.18/src/runtime/os_linux.go:66
2 0x00000000012cfff4 in runtime.notesleep
at /usr/local/go1.18/src/runtime/lock_futex.go:159
3 0x00000000013017e8 in runtime.mPark
at /usr/local/go1.18/src/runtime/proc.go:1449
4 0x00000000013017e8 in runtime.stopm
at /usr/local/go1.18/src/runtime/proc.go:2228
5 0x0000000001305e04 in runtime.exitsyscall0
at /usr/local/go1.18/src/runtime/proc.go:3937
6 0x000000000132c0f4 in runtime.mcall
at /usr/local/go1.18/src/runtime/asm_arm64.s:186
然后,我切换到活动线程,并在调用堆栈的底部找到gentraceback函数。
dlv) tr 4649
Switched from 4637 to 4649
(dlv) bt
0 0x0000000001320fa8 in runtime.funcdata
at /usr/local/go1.18/src/runtime/symtab.go:1100
1 0x0000000001320fa8 in runtime.gentraceback
at /usr/local/go1.18/src/runtime/traceback.go:357
2 0x0000000001cbcfdc in github.com/tikv/client-go/v2/txnkv/transaction.(*batchExecutor).process
at :01
3 0x0000000001cb1ee8 in github.com/tikv/client-go/v2/txnkv/transaction.(*twoPhaseCommitter).doActionOnBatches
at /root/go/pkg/mod/github.com/tikv/client-go/v2@v2.0.1-0.20220321123529-f4eae62b7ed5/txnkv/transaction/2pc.go:1003
4 0x0000000001cb125c in github.com/tikv/client-go/v2/txnkv/transaction.(*twoPhaseCommitter).doActionOnGroupMutations
at /root/go/pkg/mod/github.com/tikv/client-go/v2@v2.0.1-0.20220321123529-f4eae62b7ed5/txnkv/transaction/2pc.go:963
5 0x0000000001caf588 in github.com/tikv/client-go/v2/txnkv/transaction.(*twoPhaseCommitter).doActionOnMutations
at /root/go/pkg/mod/github.com/tikv/client-go/v2@v2.0.1-0.20220321123529-f4eae62b7ed5/txnkv/transaction/2pc.go:740
6 0x0000000001cbf758 in github.com/tikv/client-go/v2/txnkv/transaction.(*twoPhaseCommitter).commitMutations
at /root/go/pkg/mod/github.com/tikv/client-go/v2@v2.0.1-0.20220321123529-f4eae62b7ed5/txnkv/transaction/commit.go:213
7 0x0000000001cb6f9c in github.com/tikv/client-go/v2/txnkv/transaction.(*twoPhaseCommitter).execute.func2
at /root/go/pkg/mod/github.com/tikv/client-go/v2@v2.0.1-0.20220321123529-f4eae62b7ed5/txnkv/transaction/2pc.go:1585
8 0x000000000132e844 in runtime.goexit
at /usr/local/go1.18/src/runtime/asm_arm64.s:1259
我在dlv中运行了几次下一个命令,发现它始终卡在gentraceback代码块中。实际上,同一行在Go 1.18的相应块中用相同的打印参数重复多次。这表明此代码段中存在死循环。
进程的flame graph还表明,执行时间主要花在gentraceback和findfunc函数上。

经过进一步分析,我怀疑我们触发了Go运行时错误。其中一个线程进入了gentraceback函数的死循环,该函数需要全局锁。其他线程最终在运行时GC相关代码中执行,但在等待锁定时被卡住。最后,整个进程耗尽了一个线程上的CPU,而所有其他线程都在等待锁定。
在找到死循环之后,我试图通过检查其退出条件来修改Go代码以避免发生这种情况。正如所料,通过使用修改后的Go代码编译TiDB并绕过死循环,我们的数据库工作正常。然而,我们仍然需要一个官方解决方案。我们希望Go团队解决该bug的根本原因,而不是使用变通方法。
向Go Team提交问题
我的一位同事查看了Go的问题列表,看看这是否是已知的错误。我们发现了一个类似的问题,但它在Go 1.16和Go 1.17中已经修复。我们的问题可以在Go 1.1 8中触发,所以它是Go的新问题。我在Go存储库上提交了该问题。经过几轮问题复制和沟通,Go社区的@cherrymui找到了根本原因,并提供了一个可验证的修复方案。
总结和后续工作
当我们发现一个bug并打开一个问题时,故事并没有结束。正如我前面提到的,数据库是一个非常复杂的项目。底层操作系统、编译器、硬件都可能存在影响其稳定性和性能的错误。
这个Go bug有一些特殊之处。它在TiDB上没有稳定的触发器。我们还发现,具有不同TiDB版本的不同硬件系统可能会有不同程度的触发困难。经过几轮测试,我们能够在ARM平台上稳定地复制它。
我们跟踪了问题的进展,其他人后来也遇到了这个问题。Go团队将在1.18的后续版本中支持相应的修复。在TiDB方面,我们决定发布基于补丁Go的ARM平台的TiDB 5.4.14二进制文件,以及基于正式修复的Go版本的6.x更新版本。
如果您是对分布式系统感兴趣的Go开发人员,那么TiDB可能是一个很好的项目。
原文标题:How I Found a Go Issue on ARM that Crashed the Database Server
原文作者:Arthur Mao
原文链接:https://dzone.com/articles/how-i-found-a-go-issue-on-arm-that-crashed-the-dat




