本文是大 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
更多技術乾貨,請關注 「雲計算總動員」 ,咱們一塊兒在這裏,用雲計算改變將來。