一个GO语言性能问题的发现和解决 | U刻

栏目: Go · 发布时间: 6年前

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

    栏目:技术分享

    事件起因

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

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

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

    问题分析

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

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

    一个GO语言性能问题的发现和解决 | U刻

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

    原因猜测

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

    • go1.8

    一个GO语言性能问题的发现和解决 | U刻

    • go1.9

    一个GO语言性能问题的发现和解决 | U刻

    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, 不需要拷贝页表, 加快创建速度,从测试效果看,稳定在几十微妙。

    一个GO语言性能问题的发现和解决 | U刻

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

    实验论证

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

    一个GO语言性能问题的发现和解决 | U刻

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

    一个GO语言性能问题的发现和解决 | U刻

    一个GO语言性能问题的发现和解决 | U刻

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

    写在最后

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

    正因为vfork的使用需要小心,因此go1.9准备加入vfork发布之前,有人提出代码不够健壮,因为rawVforkSyscall返回之后,在父进程段还执行指令,这样子进程有机会破坏双方的共享栈,因此提了一个commit去让rawVforkSyscall在返回后,在父进程段什么都不做直接return,解决这个互相影响,如图所示:

    一个GO语言性能问题的发现和解决 | U刻

    如有兴趣深入了解,可以看下这个commit 的review,Rob Pike等人都有发言。

    https://go-review.googlesource.com/c/go/+/46173

    一个GO语言性能问题的发现和解决 | U刻 一个GO语言性能问题的发现和解决 | U刻

    Post Views: 6

    *本平台所发布文章信息,版权归UCloud所有,如需转载请注明出处!

    0

查看TA的所有文章


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms and Data Structures

Algorithms and Data Structures

Kurt Mehlhorn、Peter Sanders / Springer / 2008-08-06 / USD 49.95

Algorithms are at the heart of every nontrivial computer application, and algorithmics is a modern and active area of computer science. Every computer scientist and every professional programmer shoul......一起来看看 《Algorithms and Data Structures》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具