在使用gozero框架的开发数据库管理服务的过程中,经常出core。通过dlv查看core的位置,各个地方都有,典型的有以下两种:
- runtime在执行init函数时,某个空串对应的地址上其长度不对,应该是0,却是一个负值。
- runtime中初始化timer时,timer的某个应该为正整数的属性,变成了负值。
明确是踩了内存,但一时间找不到思路。
0x01 怀疑是CGO由于CGO和Go本身使用的内存模型不同,CGO比较容易出现踩内存的问题。加上出core的程序,为了使用一个公共库,服务链接了一个CGO库,所以开始也怀疑是CGO的问题。一开始也不能稳定地复现出来。
- 不使用CGO编译一个可执行程序。在命令行中,开启ulimited后,反复调用,出core。使用的复现脚本如下。
while [ 1 ]; do ./main done
这种方式可以复现之后,就排除了CGO的问题。
0x02 怀疑是三方库的问题使用如下的代码,可以稳定复现。
package main
import _ "gopkg.in/yaml.v2"
func main() {
}
对于出现的core,使用dlv和gdb都看过,同样是没有头绪。后来同事发现说,踩的内存模式是16bytes一组的。把怀疑点指向了Go中使用的AVX寄存器,intel处理器中,AVX寄存器有128bit,和256bit的模式,而且Go的内存分配时,也正是使用了AVX寄存器来初始化分配的内存。
尝试使用以下方法确认是否是AVX寄存器内容异常的锅。
将Go运行时中,初始化内存的Plan9汇编代码进行了修改,如下所示,不使用AVX寄存器。
TEXT ·TestClear(SB), NOSPLIT, $0-16 MOVQ ptr+0(FP), DI MOVQ n+8(FP), BX XORQ AX,AX redo: TESTQ BX, BX JEQ _0 CMPQ BX, $2 JBE _1or2 CMPQ BX, $4 JBE _3or4 CMPQ BX, $8 JB _5through7 // 8的整数倍 _8: MOVQ AX, (DI) SUBQ $8, BX ADDQ $8, DI JMP redo _1or2: MOVB AX, (DI) MOVB AX, -1(DI)(BX*1) RET _0: RET _3or4: MOVW AX, (DI) MOVW AX, -2(DI)(BX*1) RET _5through7: MOVL AX, (DI) MOVL AX, -4(DI)(BX*1) RET
重新编译后,仍然会出core。再次失去了方向。
0x03 柳暗花明当时已经容易复现出来了,PM也没有太着急了。然后尝试在Go社区看有没有人遇到同样的异常。搜runtime的issue出来的也有许多。只到偶尔看到了这个issue https://hub.fastgit.org/golang/go/issues/40722,随机崩溃的异常,跟我遇到的有点相似。其实仔细一看,并不是十分符合。主要差别点有:
- issue中说到的内核版本是3.18。而我们的内核版本是4.xx的。
- 它的异常是bad p,我们的异常更随机,都是非法长度非法内存类的异常。
不过仔细看了issue中社区大牛的回复,说是5.x内核与go 1.14之后的版本相处的不太好,主要是因为内核在FPU寄存器使用上有个BUG[https://lkml.org/lkml/2019/11/27/362],需要是GCC9才能触发。社区专门有个页面跟踪这个问题https://golang.org/wiki/LinuxKernelSignalVectorBug。但与我们的情况又不完全相符,我们的内核是4.xx,GCC是5.x的。在查看了patch之后,发现我们在用的内核源码中,也有同样的逻辑,也就是说问题可能是存在的。
自己尝试合入了内核的patch后,编译出了vm,替换到系统上重启生效后,真的就不出core了。问题解决了,还是要分析下那个内核问题。
0x04 内核问题Go 1.14之后引入了异步抢占调度特性。这个特性是非常好的,可以最大限度地利用CPU,实现高并发。简单来说是,当一个G阻塞时,它占用着一个内核线程,单纯从用户态,无法让这个线程中断执行。Go的调度器很不幸就是工作在用户态的。所以为了实现异步抢占,Go 1.14之后引入基于信号中断机制的抢占机制。当一个G运行在一个M上时,向M发送抢占信号,强制M退出当前上下文,进入到中断处理逻辑中,这时调度器就可以调度其他G了。详细分析可以参考这篇bloghttps://www.cnblogs.com/luozhiyun/p/14589730.html。
按照内核patch的描述,可知内核某些场景下,会把fpu寄存器内容保存在内存中,如果刚好保存后,触发了缺页中断,这时其他进程可能会修改了fpu寄存器的内容。缺页中断返回后,内核将控制权交还给用户态,就会导致之前保存的fpu内容不是进程期望的。而Go的异步抢占刚好要使用信号机制,对fpu寄存器(包含AVX YMM/XMM寄存器)有依赖,所以就会有各种异常了。
0x05 总结这个问题的解决过程虽然有点运气成份,但之前的思路是正确的。一开始能稳定复现后,就确定不是Go自己的问题的。处理这种随机出现的异常时,就要尽可能地加快它的复现频率,试探出它的复现条件。思路就会慢慢出现了。



