天天看點

可怕!那些你看不到的程序

誰動了我的Cpu資源!

首先我簡單解釋一下客戶所看到的問題。如下圖第三行,top統計Cpu總體使用情況,使用了八個名額。這八個名額分别是:使用者空間程序(us)、核心空間程序(sy)、高nice值的使用者空間程序(ni)、空閑(id)、空閑等待io(wa)、中斷上半部(hi)、中斷下半部(si)、以及steal時間(st)。理論上來講這八個名額之和應該是100%。這八個名額當中,id和wa是Cpu空閑時間的統計,這兩個值之和越小,說明Cpu越忙碌。客戶這台伺服器的id與wa之和是0,是以這台伺服器的Cpu使用率是100%,其中占比最大的是ni。

可怕!那些你看不到的程式

除了第三行Cpu總體統計名額之外,top會對Cpu的使用率,從程序次元上進行統計,也就是CPU這一列。因為這台伺服器是16核的,是以每個程序(多線程)的Cpu使用率可以超過100%,同時所有程序Cpu使用率之和不能超過上線1600%(平均到每個核是100%)。

這個問題的“見鬼”之處在于,雖然這個系統裡運作着787個程序,但這些程序使用Cpu之和,卻遠小于1600%這個值。

晴天霹靂:問題現場丢失

剛準備深入探究這個問題的時候,不幸的事情發生了。客戶這台機器重新開機了。重新開機之後問題消失!雖然問題現場丢失了,但客戶的質疑沒有改變。客戶強烈要求我們提供這台伺服器Cpu打滿的原因。

備注:很多時候,我們在遇到難以解釋的問題的時候,往往傾向于把問題歸結到和這個問題相關的“黑盒”的部分。這也是為什麼,很多客戶在遇到不容易解釋的現象的時候,會懷疑原因在虛拟化層,或在實體機層,有時候甚至會懷疑阿裡雲的産品是不是“缺斤短兩”了。

nice!

作為技術支援工程師,在沒有重制環境的情況下,為了滿足客戶的需求,我這邊做的第一件事情是,搞清楚ni這個名額的計算方法,跟客戶溝通這個名額背後的理論知識,然後期望客戶能夠了解,這個名額跟實體機沒有任何關系,純粹是虛拟機内部行為。

nice是什麼

在第一部分,我介紹Cpu八個統計名額的時候,提到了ni是高nice值的使用者空間程序的Cpu使用率。nice值是什麼呢,簡單來講,nice值代表着一個程序使用Cpu資源的優先程度。每個程序都會有一個與之對應的nice值,nice值越高,那麼這個程序使用Cpu的優先級就越低,獲得的處理器的時間相比較而言就會越少。而ni這個名額,統計的是系統中,所有nice值大于0的使用者空間程序的Cpu的使用率。

一般情況下程序預設的nice值是0,而當有些程序需要更高的執行優先級的時候,我們會減小這些程序的nice值。當然有一些并不需要在高優先級運作的程序,例如我們跑編譯程式gcc,去編譯一個核心,這個操作預計會花幾個小時,那麼我們可以增加這個gcc程序的nice值。

linux會把真正的使用者模式Cpu使用率拆分成兩部分顯示,nice值大于0的顯示為ni,小于等于0的顯示為us。

自己動手跑高ni

這裡我們做一個簡單的測試去驗證上邊的理論。我們使用for語句寫一個簡單的死循環程式loop,然後用objdump看代碼編譯之後的彙程式設計式。這段彙編非常簡單,前兩行準備堆棧指針;第三行初始化一個變量,這個變量位于堆棧上rpb-0x4這個位置;然後第四第五行重複遞增這個變量。

00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
4004f8: 83 45 fc 01 addl $0x1,-0x4(%rbp)
4004fc: eb fa jmp 4004f8 <main+0xb>
4004fe: 66 90 xchg %ax,%ax           

loop程序一旦被排程到一個Cpu上,那麼這個Cpu就會被打滿。如下兩張圖,左邊是nice值為0的情況,右邊是nice值為19的情況。程序nice值可以在圖下邊NI這一列看到。

可怕!那些你看不到的程式

下邊是Cpu使用率拆分到每個核上的情況。

可怕!那些你看不到的程式

不滿意的客戶

我跟客戶溝通ni這個名額背後的理論知識和我的結論:這個問題和實體機沒有什麼關系。對于我的結論,客戶是不接受的。客戶強調,在機器重新開機之前,他檢查了系統裡所有程序的Cpu的使用情況,他非常确定沒有發現任何異常。雖然當時系統裡有一百多個java程序,但是這些java程序的Cpu使用率都非常低。

時間大法,好!

以前處理系統夯機問題的時候,偶爾會走投無路。想象一下,一個複雜的系統中,運作着上千甚至上萬的程序。而夯機則意味着,系統裡的這些程序,像一團亂麻一樣,糾纏在了一起。這個時候,隻有從這些程序中整理出依賴關系,才能知道哪些程序是夯機問題的trouble maker,而哪些程序又是夯機問題的受害者。理清這些關系,大部分情況下,我們是靠理清資源的持有與等待關系。

可怕!那些你看不到的程式

可惜的是,這種分析方法并不是萬能的。系統為了節省管理成本,隻會有選擇地維護其中某些資源的持有與等待關系。

在我們不能用這種方法分析問題的時候,另外一種方法就派上了用場。這種方法就是分析程序進入等待狀态的先後順序。我們稱這種方法叫“時間大法”。

挖礦程式

在因為無法重制問題而“走投無路”的時候,“時間大法”給了我希望。首先,在sa日志裡我找到了Cpu達到100%的開始時間是4月29日淩晨6點40。接着,我翻遍了系統裡幾乎所有的檔案,發現有兩個配置檔案在6點39被建立。而存放這兩個配置檔案的目錄,則有兩個非常可疑的庫檔案libxmr-stak-c.a和libxmr-stak-backend.a。Google這兩個檔案,發現這是門羅币挖礦程式使用的名字。

可怕!那些你看不到的程式

還是不滿意的客戶

當把上邊的發現同步給客戶的時候,客戶還是覺得證據不足。而且客戶再次強調,他當時看了所有系統裡運作的程序,如果有可疑的程序使用Cpu異常的話,他肯定早發現了。因為客戶的堅持,壓力再次回到了我們這一邊。

隐藏linux程序方法一二三

如果客戶所說的是真實情況的話,那麼有什麼方法可以隐藏linux程序,讓客戶不能從ps或top的輸出中,讀到程序資訊呢?比較常用的三種方法是:建立程序的時候,把pid設定成為0;直接修改ps和top代碼;或者hook libc裡readdir和opendir等函數(因為ps和top的實作,直接使用了readdir和opendir等libc庫函數,來讀取/proc檔案及其子目錄)。

這個時候我突然想起自己之前曾經看到過的,在6點39被更改的另外一個檔案ld.so.preload。第一次檢查這個檔案的時候,看到這個檔案裡被寫了一條libjdk.so,想當然的以為這個檔案和java有關,是以忽略了這條資訊。

可怕!那些你看不到的程式

我知道事情的真相了!

這個時候,事情的全貌就顯現出來了。在6點39分,有人給ld.so.preload增加了一個庫檔案。從那以後,所有的程序,啟動的時候都會首先加載這個庫,然後再加載其他庫。這就産生一個效果,如果程序調用一個外部函數,這個函數的實作本來在其他庫檔案裡,但是這個預先加載的庫實作了同樣的函數,那麼動态連結會先使用預先加載的這個庫裡定義的這個函數。

記得上一次使用這個技巧的時候,還是多年前在寫opengl trace工具的時候。後來轉投微軟系,linux上這些技巧就淡忘了。基本上來說,使用ld.so.preload,我們可以實作filter類工具,在filter工具中實作過濾,追蹤,參數檢查等功能。當然為了保證程序正常運作,我們的同名過濾函數,最終還是會調用原來的函數。

驗證了一下,系統裡所有的程序,因為重新開機,都加載了libjdk這個庫檔案到自己的位址空間裡。下圖是讀bash程序/proc/<pid>/maps内容的輸出。

可怕!那些你看不到的程式

libjdk的雕蟲小技

這個庫libjdk和java沒有什麼關系,他非常小,實作也非常簡單。以緻于我們甚至可以通過讀彙編來了解它的行為。就如之前猜測的一樣,這個庫hook了readdir之類的函數,對讀取/proc檔案夾的操作做了過濾,是以客戶在使用top或者ps指令的時候,得到的結果都是被過濾過的結果。這裡不會對libjdk彙編代碼進行深入分析,但是提供一個strings輸出的這個庫檔案裡包含的串。從這些串中,我們也能對這個庫的行為猜個大概。

可怕!那些你看不到的程式

後記

回顧這個問題的處理過程,憑良心講,這個問題本來并不算是什麼疑難雜症。可能抓個core dump,分分鐘就能搞定。但兩件事情極大的增加了這個問題的排查難度,一個是問題環境丢失,一個是客戶的堅持。

當然如果不是問題環境丢失,那麼我也不會去嘗試其他的排查思路,如果不是客戶的堅持,我也不會做到把彙編代碼都拿出來做證據的這種程度。客戶的高要求,不斷的敦促,是我們不斷提升服務能力的重要驅動力。

原文釋出時間為:2018-05-30

本文作者:聲東

本文來自雲栖社群合作夥伴“

阿裡技術

”,了解相關資訊可以關注“

”。