背景
最近參與的項目是基於 OpenStack 提供容器管理能力,豐富公司 IaaS 平台的能力。日常主要工作就是在開源的 novadocker 項目(開源社區已停止開發)基礎上進行增強,與公司的其他業務組件進行對接等。
周末給下游部門的 IaaS 平台進行了一次升級,主要升級了底層操作系統,基本用例跑通過,沒出現什么問題,皆大歡喜。
結果周一一到公司就傳來噩耗,性能測試發現業務進程運行在容器中比業務進程運行在宿主機上吞吐量下降了 100 倍,這讓周一顯得更加陰暗。
周一
先找下游了解了下業務模型,他們說已經把業務模型最簡化了,當前的模式是:業務進程運行在容器中,通過與主機共享 IPC namespace 的方式來使用共享內存與宿主機上的 Daemon 進程進行通信,整個過程不涉及磁盤讀寫、網絡交互等。
擼起袖子開始干,定位第一步,當然是找瓶頸了,分析下到底問題出在哪。
top
用到的第一個命令自然是 top,top 是 linux 里一個非常強大的命令,通過它基本上能看到系統中的所有指標。
上面是 top 命令運行時的一個示意圖,比較重要的指標都已經標了出來:
1 處表示系統負載,它表示當前正在等待被 cpu 調度的進程數量,這個值小於系統 vcpu 數(超線程數)的時候是比較正常的,一旦大於 vcpu 數,則說明並發運行的進程太多了,有進程遲遲得不到 cpu 時間。這種情況給用戶的直觀感受就是敲任何命令都卡。
2 處表示當前系統的總進程數,通常該值過大的時候就會導致 load average 過大。
3 處表示 cpu 的空閑時間,可以反應 cpu 的繁忙程度,該值較高時表示系統 cpu 處於比較清閑的狀態,如果該值較低,則說明系統的 cpu 比較繁忙。需要注意的是,有些時候該值比較高,表示 cpu 比較清閑,但是 load average 依然比較高,這種情況很可能就是因為進程數太多,進程切換占用了大量的 cpu 時間,從而擠占了業務運行需要使用的 cpu 時間。
4 處表示進程 IO 等待的時間,該值較高時表示系統的瓶頸可能出現在磁盤和網絡。
5 處表示系統的剩余內存,反應了系統的內存使用情況。
6 處表示單個進程的 cpu 和內存使用情況。關於 top 命令中各個指標含義的進一步描述可以參見:
http://www.jb51.net/LINUXjishu/34604.html
使用 top 命令查看了下系統情況,發現一切正常。load average 也不高,task 也不多,cpu 和內存都還很空閑,就連 IO 等待時間都很低,也沒有哪個進程的 cpu 和內存使用率偏高,一切都很和諧,沒有瓶頸!
當然,沒有瓶頸是不可能的。由於我們的容器都是綁核的,所以很有可能是分配給容器的那些核是處於繁忙狀態,而由於總核數較多,將 cpu 的使用率給拉了下來。於是又按下了“1”鍵,切換到詳細模式下:
在這種模式下,可以看到每個 vcpu 的使用情況。一切依然很和諧,詭異的和諧。
看來從 cpu 這塊是看不出來什么了,那就繼續看看是不是磁盤搞的鬼吧。
iostate
iostate 命令是用來查看磁盤使用情況的一個命令,經驗告訴我們,磁盤和網絡已經成為影響性能的最大嫌疑犯。
使用 iostate 工具時,通常只用關注最后一行(%util)即可,它反映了磁盤的繁忙程度。雖然下游部門已經說了他們跑的用例是一個純內存的場景,不涉及磁盤讀寫。但是客戶的話信得住,母豬也能上樹,我還是要跑一下 iostate,看下磁盤情況怎么樣。結果,依舊很和諧,磁盤使用率基本為零。
后面還嘗試了觀察網絡指標,發現確實也沒有網絡吞吐,完了,看來問題沒那么簡單。
周二
雖然周一看似白忙活了一天,但是也得到了一個重要結論:這個問題不簡單!不過簡單的在資源層面時分析不出來啥了,得寄出性能分析的大殺器——perf 了。
perf+ 火焰圖
perf 是 linux 下一個非常強大的性能分析工具,通過它可以分析出進程運行過程中的主要時間都花在了哪些地方。
之前沒太使用過 perf,因此剛開始進行分析,就自然而然地直接使用上了 perf+ 火焰圖這種最常見的組合:
-
安裝 perf。
yum install perf
-
下載火焰圖工具。
git clone https://github.com/brendangregg/FlameGraph.git
-
采樣。
perf record -e cpu-clock -g -p 1572(業務進程 id)
一段時間(通常 20s 足夠)之后 ctrl+c,結束采樣。
-
用 perf script 工具對 perf.data 進行解析。
perf script -i perf.data &> perf.unfold。
PS:如果在容器中運行的程序有較多的依賴,則該命令解析出來的符號中可能會有較多的“Unregistered symbol…”錯誤,此時需要通過
--symfs
參數指定容器的rootfs
位置來解決該問題。獲取容器rootfs
的方法根據 docker 的 storagedriver 的不同而有所不同,如果是device mapper
類型,則可以通過 dockerinspect 找到容器的rootfs
所在位置,如果是overlay
類型,則需要通過 dockerexport 命令將該容器的rootfs
導出來,如果是富容器的話,一般都有外置的rootfs
,直接使用即可。 -
將 perf.unfold 中的符號進行折疊。
./stackcollapse-perf.pl perf.unfold &> perf.folded
-
最后生成 svg 圖。
/flamegraph.pl perf.folded > perf.svg
最后就能得到像下面這種樣子的漂亮圖片。通常情況下,如果程序中有一些函數占用了大量的 CPU 時間,則會在圖片中以長橫條的樣式出現,表示該函數占用了大量的 CPU 時間。
然而,perf+ 火焰圖在這次並沒有起到太大的作用,反復統計了很多次,並沒有出現夢寐以求的“長橫條”,還是很和諧。
perf stat
perf+ 火焰圖並沒有起到很好的效果,就想換個工具繼續試試,但是找來找去、請教大神,也沒找到更好的工具,只好繼續研究 perf 這個工具。
perf 除了上面提到的 record(記錄事件)、script(解析記錄的事件)命令之外,還有其他一些命令,常用的有 report(與 script 類似,都是對 perf record 記錄的事件進行解析,不同之處在於 report 直接解析程序中的運行熱點,script 的擴展性更強一點,可以調用外部腳本對事件數據進行解析)、stat(記錄進程一段時間之內觸發的事件數)、top(實時分析程序運行時熱點)、list(列出 perf 可以記錄的事件數)等命令。
這些命令挨個試了個遍,終於在 perf stat 命令這塊有了突破:
使用 perf stat 對業務進程運行在物理機和容器上分別進行統計,發現業務進程運行在容器中時,大部分事件(task-clock、context-switches、cycles、instructions 等)的觸發次數是運行在物理機上時的百分之一。
這是什么原因呢?一定是什么東西阻塞住了程序的運轉,這個東西是什么呢?
前面已經分析了,不是磁盤,不是網絡,也不是內存,更不是 cpu,那還有什么呢??
周三
是什么原因阻塞住了程序的運轉呢?百思不得其解,百問不得其解,百猜不得其解,得,還是得動手,上控制變量法。
運行在容器中的程序和運行在物理機上有什么區別呢?我們知道,docker 容器 =cgroup+namespace+secomp+capability+selinux,那么就把這些技術一個個都去掉,看到底是哪個特性搞的鬼。
在這 5 個技術中,后三個都是安全相關的,都有開關可以控制,經過測試,發現把后三個都關掉之后,性能還是很差,說明這 3 個技術是無辜的,開始排查 cgroup 和 namespace。
首先懷疑的當然是 cgroup,畢竟它就是做資源限制的,很有可能一不小心限制錯了,就把業務給限制了。
cgexec
cgexec 是 cgroup 提供的一個工具,可以在啟動時就將程序運行到某個 cgroup 中,因此我們可以將業務程序運行在物理機上,但是放到業務容器所在的 cgroup 中,看看性能會不會下降。
具體用法如下:
cgexec -g *:/system.slice/docker-03c2dd57ba123879abab6f7b6da5192a127840534990c515be325450b7193c11.scope ./run.sh
通過該命令即可將 run.sh 運行在與容器 03c2dd57 相同的 cgroup 中。在多次測試之后,發現這種情況下,業務進程的運行速度沒有受到影響,cgroup 被洗白,那么真相只有一個——凶手就是 namespace。
周四
雖然說凶手已經確定是 namespace,但是 namespace 家族也有一大票人,有 ipc namespace、pid namespace 等等,還需要進一步確定真凶。
nsenter
nsenter 是一個 namespace 相關的工具,通過它可以進入某個進程所在的 namespace。在 docker exec 命令出現之前,它唯一一個可以進入 docker 容器的工具,在 docker exec 出現之后,nsenter 也由於其可以選擇進入哪些 namespace 而成為 docker 問題定位的一個極其重要的工具。
通過如下命令,即可進入容器所在的 mount namespace。
nsenter --target $(docker inspect --format '{{.State.Pid}}' 容器 id) --mount bash
同理,通過如下命令即可進入容器所在的 IPC namespace 和 pid namespace。
nsenter --target $(docker inspect --format '{{.State.Pid}}' 容器 id) --ipc --pid bash
在不斷的將業務進程在各個 namespace 之間切換后,終於進一步鎖定了真凶:mount namespace。測試發現,一旦將業務進程放置到容器所在的 mount namespace,性能就會急劇下降。
這是為什么呢?這是為什么呢?mount namespace 到底做了什么,會有這么大的影響?
離開公司時已經時 12 點,隔壁工位的專家還在那電話會議,“我看哈,這一幀,在這個節點還是有的,但是到了這呢,就沒了,它。。”,“那是找到丟包原因了?”,“別急嘛,我慢慢看,看這個節點。”。
在回家的出租車上,想想這個問題已經四天了,想想隔壁工位的專家都要成神了,還是會遇到棘手的問題,突然有種崩潰的感覺,費了九牛二虎之力猜把眼淚憋回去,不然還真怕嚇着出租車師傅。
周五
早上起床還和女朋友談起這事,說我最近遇到了一個大難題,想想已經好多年沒遇到這么難搞的問題了。上一次遇到這種級別的問題,還是大四剛進實驗室的時候,那時候有個問題花了一整周的時間才解決。要是今天我還解決不了這個問題,就破記錄了。
記得當時是需要在實驗室搭建 eucalyptus 集群,之前每次搭建都很順利,但是那次卻奇怪,怎么都搭建不成功,研究了一周之后,才發現是因為那台服務器的 bios 時間被還原了,eucalyptus 在安裝過程中發現當前時間不在自己的有效時間內(太早了),因此一直安裝失敗。
言歸正傳,mount namespace 為什么有這么大的威力呢?它到底影響什么了呢?實在想不通,就去請教了下大神,大神想了想,回了我句,試試 ldd?
ldd
ldd 是什么?
當然這句話我沒去問大神,轉身自己查去了。
ldd 是 list, dynamic, dependencies 的縮寫,意思是列出動態庫依賴關系。頓時豁然開朗,mount namespace 隔離出了自己的文件系統,所以容器內外可以使用不同的依賴庫,而不同的依賴庫就可能造成無數種影響。
於是開始通過 ldd 對比容器中業務進程的依賴庫與宿主機上業務進程的依賴庫,最終發現容器中的 glibc 庫與宿主機上的 glibc 庫版本不一致,很可能是這個原因導致性能下降的。
於是將容器中的 glibc 庫版本替換為宿主機上的 glibc 庫之后,容器內業務的性能終於恢復了,猜想得到證實。
下班回家,一身輕松!
路過隔壁工位的專家時候,他剛好起身接水,順口問道,“怎么樣,平哥,丟失的那一幀找到沒?”,“嗨,別提了,繼續看”
后記
為什么容器內外 glibc 版本不一致就導致性能下降了呢?
這是和業務模型相關的,前面提到,下游的業務模型是通過與主機共享 IPC namespace 的方式來使用共享內存與宿主機上的 daemon 進程進行通信。而 glibc 在一次升級中,更新了信號量的數據結構(如下),就會導致在共享內存通信時,由於數據格式不一致,每次信號量通信都超時,從而影響了程序運行效率。