本文是大 U 同事的一篇实操性经验贴,是发现问题、分析问题到解决问题的完整案例,借此分享,但愿对各位有所帮助。git
事件原由github
事情原由于公司一位同事在内部邮件组中 post 了一个问题,一个使用了 go1.8.3 写的业务程序跑了一段时间后出现部分 goroutine 卡在等待一个锁 ForkLock 的现象,同事认为这是 go1.8.3 的 bug,升级到 go1.10 后没有再重现。为了搞清楚这个事情,同事在 github 上发了 issue:golang
https://github.com/golang/go/issues/26836,期间也作了不少重现的尝试,但并未重现。shell
我浏览了一下出现该问题的业务代码,大概的使用方式是父进程调用 os/exec 下的 Command 开子进程执行 shell 命令。Command 后面会调用 golang 封装的 forkExec 来开子进程并执行命令,forkExec 使用了 ForkLock。函数
问题分析post
ForkLock 的存在是为了不下面的状况:在有多个 goroutine 同时 fork exec 的状况下, 为了子进程只继承它须要的文件描述符,须要在父进程在建立这些文件描述符的时候加上 O_CLOEXEC 标志,这样在子进程中这些描述符是关闭的,子进程按需把本身须要继承的描述符打开便可。性能
Linux 在 2.6.27 以后,打开文件或者管道,和设置 O_CLOEXEC 是一个原子操做,所以问题不大,但 golang 对内核版本的要求是 2.6.23 及以上,另外 Unix 系统中,open 和设置 O_CLOEXEC 是两个操做,若是在两个操做之间发生 fork, 子进程就可能继承它不须要的文件描述符,所以须要加锁。重点看下 forkExec 时候的源代码:测试
从问题的现象看,确定是某 goroutine 在 forkExecPipe 或者 forkAndExecInChild 这两步卡住了,锁没释放,所以有些 goroutine 一直拿不到锁,饥饿致死。forkExecPipe 最后调用的是内核 pipe2,forkAndExecInChild 最后调用的是内核 clone 和 exec。google
缘由猜想云计算
pipe2 是一个快速系统调用,所以可能 block 的系统调用是 clone 和 exec, 加上在 go1.10 上这个问题没有重现,对比 go1.8 代码和 go1.9 在 forkAndExecInChild 函数上的差别:
go1.8
go1.9
go1.9 增长了 CLONE_VFORK 和 CLONE_VM。只带 SIGCHILD 的 clone 能够认为相似于 fork(最后都是调用 do_fork), fork 的问题是,在父进程占用内存越大性能越差,具体能够看这个连接:
https://bugzilla.redhat.com/show_bug.cgi?id=682922
这个 case 2011 年提出,今年 7 月还在更新,这个 case 反馈的问题是,尽管 Linux kernel 引入 copy-on-write 机制,但 fork 的时候依然要拷贝页表项,进程虚拟内存越大,须要拷贝的页表项越多,所以 fork 越慢。Golang 的讨论组有人测试过,heap size 在 2G 的状况下,fork 耗时能够到毫秒级别, 正常是及几十微秒,上千倍差距。
Go1.9 加上这两个参数是为了让子进程和父进程共享内存,至关于调用 vfork, 不须要拷贝页表项, 加快建立速度,从测试效果看,稳定在几十微妙。
因此一个合理的猜想是,在低于 go1.9 版写的程序中,当程序内存占用足够大,并且建立进程频率足够频繁,会致使 ForkLock 长时间等待。
实验论证
我用 go1.8.3 写了一个测试程序,在 2 核 4G 的虚拟机(kernel 3.10.0-693.17.1.el7.x86_64)下测试。
在外部每隔 10 秒,给这个程序发 SIGUSR1 信号,打印运行时堆栈,运行一段时间后,部分 goroutine 获取 ForkLock 的时间愈来愈长。见下面两图:
而在 go1.9 及以上版本上并未出现上述状况,这个结果我以为已经能够说明问题。升级版本到 go1.9 及以上版本能够解决该问题。
写在最后
vfork 是为了解决 fork 拷贝页表项致使的性能问题, 并且大部分场景 fork 以后是调用 exec,exec 要把全部页表删除重置新的页表, 实在不必再拷贝页表项。但因为 vfork 父子进程共享内存,因此使用要很当心,若是子进程修改某个变量,会影响到父进程,并且 kernel 会挂起父进程,让子进程先执行,这些限制基本限制 vfork 只适合跟 exec 的场景,不如 fork 通用。
正由于 vfork 的使用须要当心,所以 go1.9 准备加入 vfork 发布以前,有人提出代码不够健壮,由于 rawVforkSyscall 返回以后,在父进程段还执行指令,这样子进程有机会破坏双方的共享栈,所以提了一个 commit 去让 rawVforkSyscall 在返回后,在父进程段什么都不作直接 return,解决这个互相影响,如图所示:
若有兴趣深刻了解,能够看下这个 commit 的 review,Rob Pike 等人都有发言。
https://go-review.googlesource.com/c/go/+/46173
更多技术干货,请关注 “云计算总动员” ,咱们一块儿在这里,用云计算改变将来。