溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

一個GO語言性能問題的發(fā)現(xiàn)和解決

發(fā)布時間:2020-06-29 06:28:00 來源:網(wǎng)絡 閱讀:222 作者:UCloud_TShare 欄目:云計算

本文是大 U 同事的一篇實操性經(jīng)驗貼,是發(fā)現(xiàn)問題、分析問題到解決問題的完整案例,借此分享,希望對各位有所幫助。

事件起因

事情起因于公司一位同事在內(nèi)部郵件組中 post 了一個問題,一個使用了 go1.8.3 寫的業(yè)務程序跑了一段時間后出現(xiàn)部分 goroutine 卡在等待一個鎖 ForkLock 的現(xiàn)象,同事認為這是 go1.8.3 的 bug,升級到 go1.10 后沒有再重現(xiàn)。為了搞清楚這個事情,同事在 github 上發(fā)了 issue:

https://github.com/golang/go/issues/26836,期間也做了很多重現(xiàn)的嘗試,但并未重現(xiàn)。

我瀏覽了一下出現(xiàn)該問題的業(yè)務代碼,大概的使用方式是父進程調(diào)用 os/exec 下的 Command 開子進程執(zhí)行 shell 命令。Command 后面會調(diào)用 golang 封裝的 forkExec 來開子進程并執(zhí)行命令,forkExec 使用了 ForkLock。

問題分析

ForkLock 的存在是為了避免下面的情況:在有多個 goroutine 同時 fork exec 的情況下, 為了子進程只繼承它需要的文件描述符,需要在父進程在創(chuàng)建這些文件描述符的時候加上 O_CLOEXEC 標志,這樣在子進程中這些描述符是關閉的,子進程按需把自己需要繼承的描述符打開即可。

Linux 在 2.6.27 之后,打開文件或者管道,和設置 O_CLOEXEC 是一個原子操作,因此問題不大,但 golang 對內(nèi)核版本的要求是 2.6.23 及以上,另外 Unix 系統(tǒng)中,open 和設置 O_CLOEXEC 是兩個操作,如果在兩個操作之間發(fā)生 fork, 子進程就可能繼承它不需要的文件描述符,因此需要加鎖。重點看下 forkExec 時候的源代碼:

一個GO語言性能問題的發(fā)現(xiàn)和解決

從問題的現(xiàn)象看,肯定是某 goroutine 在 forkExecPipe 或者 forkAndExecInChild 這兩步卡住了,鎖沒釋放,因此有些 goroutine 一直拿不到鎖,饑餓致死。forkExecPipe 最后調(diào)用的是內(nèi)核 pipe2,forkAndExecInChild 最后調(diào)用的是內(nèi)核 clone 和 exec。

原因猜測

pipe2 是一個快速系統(tǒng)調(diào)用,因此可能 block 的系統(tǒng)調(diào)用是 clone 和 exec, 加上在 go1.10 上這個問題沒有重現(xiàn),對比 go1.8 代碼和 go1.9 在 forkAndExecInChild 函數(shù)上的差異:

go1.8

一個GO語言性能問題的發(fā)現(xiàn)和解決

go1.9

一個GO語言性能問題的發(fā)現(xiàn)和解決

go1.9 增加了 CLONE_VFORK 和 CLONE_VM。只帶 SIGCHILD 的 clone 可以認為類似于 fork(最后都是調(diào)用 do_fork), fork 的問題是,在父進程占用內(nèi)存越大性能越差,具體可以看這個鏈接:

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

這個 case 2011 年提出,今年 7 月還在更新,這個 case 反饋的問題是,盡管 Linux kernel 引入 copy-on-write 機制,但 fork 的時候依然要拷貝頁表項,進程虛擬內(nèi)存越大,需要拷貝的頁表項越多,因此 fork 越慢。Golang 的討論組有人測試過,heap size 在 2G 的情況下,fork 耗時可以到毫秒級別, 正常是及幾十微秒,上千倍差距。

Go1.9 加上這兩個參數(shù)是為了讓子進程和父進程共享內(nèi)存,相當于調(diào)用 vfork, 不需要拷貝頁表項, 加快創(chuàng)建速度,從測試效果看,穩(wěn)定在幾十微妙。

一個GO語言性能問題的發(fā)現(xiàn)和解決

所以一個合理的猜測是,在低于 go1.9 版寫的程序中,當程序內(nèi)存占用足夠大,而且創(chuàng)建進程頻率足夠頻繁,會導致 ForkLock 長時間等待。

實驗論證

一個GO語言性能問題的發(fā)現(xiàn)和解決

我用 go1.8.3 寫了一個測試程序,在 2 核 4G 的虛擬機(kernel 3.10.0-693.17.1.el7.x86_64)下測試。

在外部每隔 10 秒,給這個程序發(fā) SIGUSR1 信號,打印運行時堆棧,運行一段時間后,部分 goroutine 獲取 ForkLock 的時間越來越長。見下面兩圖:

一個GO語言性能問題的發(fā)現(xiàn)和解決

一個GO語言性能問題的發(fā)現(xiàn)和解決

而在 go1.9 及以上版本上并未出現(xiàn)上述情況,這個結果我覺得已經(jīng)可以說明問題。升級版本到 go1.9 及以上版本可以解決該問題。

寫在最后

vfork 是為了解決 fork 拷貝頁表項導致的性能問題, 而且大部分場景 fork 之后是調(diào)用 exec,exec 要把所有頁表刪除重置新的頁表, 實在沒必要再拷貝頁表項。但由于 vfork 父子進程共享內(nèi)存,所以使用要很小心,如果子進程修改某個變量,會影響到父進程,而且 kernel 會掛起父進程,讓子進程先執(zhí)行,這些限制基本限制 vfork 只適合跟 exec 的場景,不如 fork 通用。

正因為 vfork 的使用需要小心,因此 go1.9 準備加入 vfork 發(fā)布之前,有人提出代碼不夠健壯,因為 rawVforkSyscall 返回之后,在父進程段還執(zhí)行指令,這樣子進程有機會破壞雙方的共享棧,因此提了一個 commit 去讓 rawVforkSyscall 在返回后,在父進程段什么都不做直接 return,解決這個互相影響,如圖所示:

一個GO語言性能問題的發(fā)現(xiàn)和解決

如有興趣深入了解,可以看下這個 commit 的 review,Rob Pike 等人都有發(fā)言。

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

一個GO語言性能問題的發(fā)現(xiàn)和解決

一個GO語言性能問題的發(fā)現(xiàn)和解決

更多技術干貨,請關注?“云計算總動員”?,我們一起在這里,用云計算改變未來。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI