一个GO语言性能问题的发现和解决
本文是大 U 同事的一篇实操性经验贴,是发现问题、分析问题到解决问题的完整案例,借此分享,希望对各位有所帮助。
事件起因
事情起因于公司一位同事在内部邮件组中 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 时候的源代码:
从问题的现象看,肯定是某 goroutine 在 forkExecPipe 或者 forkAndExecInChild 这两步卡住了,锁没释放,因此有些 goroutine 一直拿不到锁,饥饿致死。forkExecPipe 最后调用的是内核 pipe2,forkAndExecInChild 最后调用的是内核 clone 和 exec。
原因猜测
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
更多技术干货,请关注 “云计算总动员” ,我们一起在这里,用云计算改变未来。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Choerodon 的微服务之路(二):Choerodon 的微服务网关
本文是 Choerodon 猪齿鱼微服务系列文章的第二篇。在《Choerodon的微服务之路(一):如何迈出关键的第一步》中,我们了解到在微服务架构中,一个完整的单体应用被拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持最低限度的集中式管理。本篇将介绍Choerodon在搭建微服务网关时考虑的一些问题以及两种常见的微服务网关。 ▌文章的主要内容包括: 为什么要使用API Gateway 两种Gateway 模式 Choerodon 的网关 对于Choerodon 而言,前端通过ReactJs实现,后端服务则通过Java,GoLang等多种语言实现。我们通过将后端拆分成许多个单独的业务服务,选择不同的语言切实地帮助我们来实现系统功能,这种面向服务的模式给我们带来了开发的便捷性,但是也带来了新的问题。服务之间如何做到相互通信,前端与后端又是如何进行通信的,是我们需要去解决的问题。 回到微服务架构的领域,如果要解决基本的通信问题,基本上只要解决下面三个问题就可以了。 服务网络通信能力 服务间的数据交互格式 服务间如何相互发现与调用 除了这些基本的问...
- 下一篇
ajax异步加载jqgrid之动态创建
之前写过一篇过于ajax异步加载jqgrid的文章,那个只是一个特殊的情况,如果创建不同数据库表的jqgrid,必须分别写servlet,dao层和连接池,很麻烦,今天我写了一个万能的写法,客官只要在前台js中写入表格名称和字段名称即可实现功能! 下面就是代码实例,关于jqgrid,ajax的介绍这里暂时不多说,有不懂的可以参考我之前的文章! 代码实例 JSP层 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>定义>类型</title> <link rel="stylesheet" href="css/homepage.css"> <link type="text/css" rel="stylesheet...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8编译安装MySQL8.0.19
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2配置默认Tomcat设置,开启更多高级功能