一个去语言性能问题的发现和解决

  

本文是大U同事的一篇实操性经验贴,是发现问题,分析问题到解决问题的完整案例,借此分享,希望对各位有所帮助。

  

<强>事件起因

  

事情起因于公司一位同事在内部邮件组中发布了一个问题,一个使用了go1.8.3写的业务程序跑了一段时间后出现部分goroutine卡在等待一个锁ForkLock的现象,同事认为这是go1.8.3的bug,升级到go1.10后没有再重现。为了搞清楚这个事情,同事在github上发了问题:

  

https://github.com/golang/go/issues/26836期间也做了很多重现的尝试,但并未重现。

  

我浏览了一下出现该问题的业务代码,大概的使用方式是父进程调用os/exec下的命令开子进程执行shell命令.Command后面会调用golang封装的forkExec来开子进程并执行命令,forkExec使用了ForkLock。

  

<强>问题分析

  

ForkLock的存在是为了避免下面的情况:在有多个goroutine同时叉exec的情况下,为了子进程只继承它需要的文件描述符,需要在父进程在创建这些文件描述符的时候加上O_CLOEXEC标志,这样在子进程中这些描述符是关闭的,子进程按需把自己需要继承的描述符打开即可。

  在

Linux 2.6.27之后,打开文件或者管道,和设置O_CLOEXEC是一个原子操作,因此问题不大,但golang对内核版本的要求是2.6.23及以上,另外Unix系统中,开放和设置O_CLOEXEC是两个操作,如果在两个操作之间发生叉,子进程就可能继承它不需要的文件描述符,因此需要加锁。重点看下forkExec时候的源代码:

  

一个去语言性能问题的发现和解决

  

从问题的现象看,肯定是某goroutine在forkExecPipe或者forkAndExecInChild这两步卡住了,锁没释放,因此有些goroutine一直拿不到锁,饥饿致死.forkExecPipe最后调用的是内核pipe2, forkAndExecInChild最后调用的是内核克隆和exec。

  

<强>原因猜测

  

pipe2是一个快速系统调用,因此可能块的系统调用是克隆和高管,加上在go1.10上这个问题没有重现,对比go1.8代码和go1.9在forkAndExecInChild函数上的差异:

  

go1.8   

一个去语言性能问题的发现和解决

  

go1.9   

一个去语言性能问题的发现和解决

  

go1.9增加了CLONE_VFORK和CLONE_VM。只带SIGCHILD的克隆可以认为类似于叉(最后都是调用do_fork),叉的问题是,在父进程占用内存越大性能越差,具体可以看这个链接:

  https://bugzilla.redhat.com/show_bug.cgi?id=682922

  

这个案件2011年提出,今年7月还在更新,这个案件反馈的问题是,尽管Linux内核引入即写即拷机制,但叉的时候依然要拷贝页表项,进程虚拟内存越大,需要拷贝的页表项越多,因此叉越慢.Golang的讨论组有人测试过,堆大小在2 g的情况下,叉耗时可以到毫秒级别,正常是及几十微秒,上千倍差距。

  

Go1.9加上这两个参数是为了让子进程和父进程共享内存,相当于调用vfork,不需要拷贝页表项,加快创建速度,从测试效果看,稳定在几十微妙。

  

一个去语言性能问题的发现和解决

  

所以一个合理的猜测是,在低于go1.9版写的程序中,当程序内存占用足够大,而且创建进程频率足够频繁,会导致ForkLock长时间等待。

  

<>强实验论证

  

一个去语言性能问题的发现和解决

  

我用go1.8.3写了一个测试程序,在2核4 g的虚拟机(内核3.10.0-693.17.1.el7.x86_64)下测试。

  

在外部每隔10秒,给这个程序发SIGUSR1信号,打印运行时堆栈,运行一段时间后,部分goroutine获取ForkLock的时间越来越长。见下面两图:

  

一个去语言性能问题的发现和解决

  

一个去语言性能问题的发现和解决

  

而在go1.9及以上版本上并未出现上述情况,这个结果我觉得已经可以说明问题。升级版本到go1.9及以上版本可以解决该问题。

  

<强>写在最后

  

vfork是为了解决叉拷贝页表项导致的性能问题,而且大部分场景叉之后是调用执行,执行要把所有页表删除重置新的页表,实在没必要再拷贝页表项。但由于vfork父子进程共享内存,所以使用要很小心,如果子进程修改某个变量,会影响到父进程,而且内核会挂起父进程,让子进程先执行,这些限制基本限制vfork只适合跟exec的场景,不如叉通用。

一个去语言性能问题的发现和解决