暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在ARM上发现导致数据库服务器崩溃的Go问题

原创 eternity 2022-10-12
759

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节点有问题。这个过程是活跃的。但是,由于它没有响应请求,因此没有为其记录数据。

image.png

登录到机器后,我观察到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函数上。

image.png

经过进一步分析,我怀疑我们触发了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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论