专栏首页Golang语言社区一个GO语言性能问题的发现和解决

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

1

事件起因

事情起因于公司一位同事在内部邮件组中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。

2

问题分析

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。

3

原因猜测

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长时间等待。

4

实验论证

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

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

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

5

写在最后

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

— END —

本文分享自微信公众号 - Golang语言社区(Golangweb)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 僵尸进程的问题

    1、僵尸进程的产生 在AIX操作系统实施的进程结构中,每一个进程都有一个父进程。当一个进程结束时会通知它的父进程,从而该进程的父进程会收集该进程的状态信息。若...

    李海彬
  • 在Go程序中实现服务器重启的方法

    Go被设计为一种后台语言,它通常也被用于后端程序中。服务端程序是GO语言最常见的软件产品。在这我要解决的问题是:如何干净利落地升级正在运行的服务端程序。 目标:...

    李海彬
  • go语言的指针

    在学习go语言的时候,谈到了指针。之前很害怕指针,因为在传说中,指针这玩意儿据说狠不好弄,且有很多程序员都死在这上面。可是,这毕竟是go语言借鉴C语言为了提升速...

    李海彬
  • 操作系统学习笔记-3:进程

    之前的单道批处理系统,程序是串行执行的,内存中可能只需要记录单一程序的程度段和数据段即可;但是现在使用的是多道批处理系统,多个程序并发执行,内存中就可能存在多个...

    Chor
  • 为什么你的docker容器刚启动就停了

    很多docker初学者,在运行容器的时候,或者是写第一个dockerfile的时候,问题最多的就是容器启动后就停了,怎么看都觉得命令没有问题,容器也没有错误日志...

    李俊鹏
  • python3--进程

    进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的

    py3study
  • linux 批量杀死多个进程 kill

    (ps|grep python|awk '{print $1}')|xargs kill -9

    一个会写诗的程序员
  • 进程控制实验--fork()

    进程的控制 实验目的 1、掌握进程另外的创建方法 2、熟悉进程的睡眠、同步、撤消等进程控制方法 实验内容 1、用fork( )创建一个进程,再调用exec( )...

    猿人谷
  • 2.进程 原

    (1)终端用户请求 (2)父进程请求 (3)负荷调节需要(一般在实时操作系统中使用) (4)操作系统的需要

    青木
  • [Linux] Linux系统(进程管理)

    进程:当我们运行程序时,Linux会为程序创建一个特殊的环境,包含程序运行的所有资源,这个环境就称为进程

    陶士涵

扫码关注云+社区

领取腾讯云代金券