Linux效能優化之某應用CPU使用率100%,該如何?
最常用什麼指標來描述系統的 CPU 效能呢?可能不是平均負載,也不是 CPU 上下文切換,而是另一個更直觀的指標—— CPU 使用率。
我們前面說過,CPU 使用率是單位時間內 CPU 使用情況的統計,以百分比的方式展示。諸 如 top、ps 之類的效能工具展示的 %user、%nice、 %system、%iowait 、%steal 等等,它們之間有什麼的不同嗎? 瞭解 CPU 使用率的內容,同時,我也會以我們最常用的反向代理伺服器 Nginx 為 例,一步步操作和分析中深入理解。
CPU 使用率
在上一期我曾提到,Linux 作為一個多工作業系統,將每個 CPU 的時間劃分為很短的時間片
為了維護 CPU 時間,Linux 通過事先定義的節拍率(核心中表示為 HZ),觸發時間中斷,並使用全域性變數 Jiffies 記錄了開機以來的節拍數。每發生一次時間中斷,Jiffies 的值就加 1。
節拍率 HZ 是核心的可配選項,可以設定為 100、250、1000 等。不同的系統可能設定不同數 值,你可以通過查詢 /boot/config 核心選項來檢視它的配置值。比如在我的系統中,節拍率設定成了 250,也就是每秒鐘觸發 250 次時間中斷。
$ grep 'CONFIG_HZ=' /boot/config-$(uname -r) CONFIG_HZ=250
同時,正因為節拍率 HZ 是核心選項,所以使用者空間程式並不能直接訪問。為了方便使用者空間程 序,核心還提供了一個使用者空間節拍率 USER_HZ,它總是固定為 100,也就是 1/100 秒。這 樣,使用者空間程式並不需要關心核心中 HZ 被設定成了多少,因為它看到的總是固定值 USER_HZ。
Linux 通過 /proc 虛擬檔案系統,向用戶空間提供了系統內部狀態的資訊,而 /proc/stat 提供 的就是系統的 CPU 和任務統計資訊。比方說,如果你只關注 CPU 的話,可以執行下面的命 令:
# 只保留各個 CPU 的資料 $ cat /proc/stat | grep ^cpu cpu280580 7407 286084 172900810 83602 0 583 0 0 0 cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0 cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0
這裡的輸出結果是一個表格。其中,第一列表示的是 CPU 編號,如 cpu0、cpu1 ,而第一行沒有編號的 cpu ,表示的是所有 CPU 的累加。其他列則表示不同場景下 CPU 的累加節拍數,它的單位是 USER_HZ,也就是 10 ms(1/100 秒),所以這其實就是不同場景下的 CPU 時間。
當然,這裡每一列的順序並不需要你背下來。你只要記住,有需要的時候,查詢 man proc 就 可以。不過,你要清楚 man proc 文件裡每一列的涵義,它們都是 CPU 使用率相關的重要指 標,你還會在很多其他的效能工具中看到它們。下面,我來依次解讀一下。
- user(通常縮寫為 us),代表使用者態 CPU 時間。注意,它不包括下面的 nice 時間,但包括 了 guest 時間。
nice(通常縮寫為 ni),代表低優先順序使用者態 CPU 時間,也就是程序的 nice 值被調整為 1- 19 之間時的 CPU 時間。這裡注意,nice 可取值範圍是 -20 到 19,數值越大,優先順序反而 越低。
- system(通常縮寫為 sys),代表核心態 CPU 時間。
- idle(通常縮寫為 id),代表空閒時間。注意,它不包括等待 I/O 的時間(iowait)。
- iowait(通常縮寫為 wa),代表等待 I/O 的 CPU 時間。
- irq(通常縮寫為 hi),代表處理硬中斷的 CPU 時間。
- softirq(通常縮寫為 si),代表處理軟中斷的 CPU 時間。
- steal(通常縮寫為 st),代表當系統執行在虛擬機器中的時候,被其他虛擬機器佔用的 CPU 時 間。
- guest(通常縮寫為 guest),代表通過虛擬化執行其他作業系統的時間,也就是執行虛擬機器 的 CPU 時間。
- guest_nice(通常縮寫為 gnice),代表以低優先順序執行虛擬機器的時間。
而我們通常所說的 CPU 使用率,就是除了空閒時間外的其他時間佔總 CPU 時間的百分比,用 公式來表示就是:
根據這個公式,我們就可以從 /proc/stat 中的資料,很容易地計算出 CPU 使用率。當然,也可 以用每一個場景的 CPU 時間,除以總的 CPU 時間,計算出每個場景的 CPU 使用率。
不過先不要著急計算,你能說出,直接用 /proc/stat 的資料,算的是什麼時間段的 CPU 使用率 嗎? 看到這裡,你應該想起來了,這是開機以來的節拍數累加值,所以直接算出來的,是開機以來的 平均 CPU 使用率,一般沒啥參考價值。
事實上,為了計算 CPU 使用率,效能工具一般都會取間隔一段時間(比如 3 秒)的兩次值,作 差後,再計算出這段時間內的平均 CPU 使用率,即
這個公式,就是我們用各種效能工具所看到的 CPU 使用率的實際計算方法。
現在,我們知道了系統 CPU 使用率的計算方法,那程序的呢?跟系統的指標類似,Linux 也給 每個程序提供了執行情況的統計資訊,也就是 /proc/[pid]/stat。不過,這個檔案包含的資料就 比較豐富了,總共有 52 列的資料。
當然,不用擔心,因為你並不需要掌握每一列的含義。還是那句話,需要的時候,查 man proc 就行。
回過頭來看,是不是說要檢視 CPU 使用率,就必須先讀取 /proc/stat 和 /proc/[pid]/stat 這兩 個檔案,然後再按照上面的公式計算出來呢?
當然不是,各種各樣的效能分析工具已經幫我們計算好了。不過要注意的是,效能分析工具給出 的都是間隔一段時間的平均 CPU 使用率,所以要注意間隔時間的設定,特別是用多個工具對比 分析時,你一定要保證它們用的是相同的間隔時間。
比如,對比一下 top 和 ps 這兩個工具報告的 CPU 使用率,預設的結果很可能不一樣,因為 top 預設使用 3 秒時間間隔,而 ps 使用的卻是程序的整個生命週期。
怎麼檢視 CPU 使用率
知道了 CPU 使用率的含義後,我們再來看看要怎麼檢視 CPU 使用率。說到檢視 CPU 使用率的 工具,我猜你第一反應肯定是 top 和 ps。的確,top 和 ps 是最常用的效能分析工具:
- top 顯示了系統總體的 CPU 和記憶體使用情況,以及各個程序的資源使用情況。
- ps 則只顯示了每個程序的資源使用情況。
比如,top 的輸出格式為:
# 預設每 3 秒重新整理一次 $ top top - 11:58:59 up 9 days, 22:47, 1 user, load average: 0.03, 0.02, 0.00 Tasks: 123 total, 1 running, 72 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 8169348 total, 5606884 free, 334640 used, 2227824 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7497908 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 root 20 0 78088 9288 6696 S 0.0 0.1 0:16.83 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.05 kthreadd 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H ...
這個輸出結果中,第三行 %Cpu 就是系統的 CPU 使用率,具體每一列的含義上一節都講過,只 是把 CPU 時間變換成了 CPU 使用率,我就不再重複講了。不過需要注意,top 預設顯示的是 所有 CPU 的平均值,這個時候你只需要按下數字 1 ,就可以切換到每個 CPU 的使用率了。
繼續往下看,空白行之後是程序的實時資訊,每個程序都有一個 %CPU 列,表示程序的 CPU 使用率。它是使用者態和核心態 CPU 使用率的總和,包括程序使用者空間使用的 CPU、通過系統調 用執行的核心空間 CPU 、以及在就緒佇列等待執行的 CPU。在虛擬化環境中,它還包括了執行 虛擬機器佔用的 CPU。
所以,到這裡我們可以發現, top 並沒有細分程序的使用者態 CPU 和核心態 CPU。那要怎麼查 看每個程序的詳細情況呢?你應該還記得上一節用到的 pidstat 吧,它正是一個專門分析每個進 程 CPU 使用情況的工具。
比如,下面的 pidstat 命令,就間隔 1 秒展示了程序的 5 組 CPU 使用率,包括:
- 使用者態 CPU 使用率 (%usr);
- 核心態 CPU 使用率(%system);
- 執行虛擬機器 CPU 使用率(%guest);
- 等待 CPU 使用率(%wait);
- 以及總的 CPU 使用率(%CPU)。
最後的 Average 部分,還計算了 5 組資料的平均值。
# 每隔 1 秒輸出一組資料,共輸出 5 組 $ pidstat 1 5 15:56:02 UID PID %usr %system %guest %wait %CPU CPU Command 15:56:03 0 15006 0.00 0.99 0.00 0.00 0.99 1 dockerd ... Average: UID PID %usr %system %guest %wait %CPU CPU Command Average: 0 15006 0.00 0.99 0.00 0.00 0.99 - dockerd
CPU 使用率過高怎麼辦? 通過 top、ps、pidstat 等工具,你能夠輕鬆找到 CPU 使用率較高(比如 100% )的程序。接 下來,你可能又想知道,佔用 CPU 的到底是程式碼裡的哪個函式呢?找到它,你才能更高效、更針對性地進行優化。
我猜你第一個想到的,應該是 GDB(The GNU Project Debugger), 這個功能強大的程式調 試利器。的確,GDB 在除錯程式錯誤方面很強大。但是,我又要來“挑刺”了。請你記住, GDB 並不適合在效能分析的早期應用。
為什麼呢?因為 GDB 除錯程式的過程會中斷程式執行,這在線上環境往往是不允許的。所以, GDB 只適合用在效能分析的後期,當你找到了出問題的大致函式後,線下再借助它來進一步調 試函式內部的問題。
那麼哪種工具適合在第一時間分析程序的 CPU 問題呢?我的推薦是 perf。perf 是 Linux 2.6.31 以後內建的效能分析工具。它以效能事件取樣為基礎,不僅可以分析系統的各種事件和核心性 能,還可以用來分析指定應用程式的效能問題。
使用 perf 分析 CPU 效能問題,我來說兩種最常見、也是我最喜歡的用法。
第一種常見用法是 perf top,類似於 top,它能夠實時顯示佔用 CPU 時鐘最多的函式或者指 令,因此可以用來查詢熱點函式,使用介面如下所示:
$ perf top Samples: 833 of event 'cpu-clock', Event count (approx.): 97742399 Overhead Shared Object Symbol 7.28% perf [.] 0x00000000001f78a4 4.72% [kernel] [k] vsnprintf 4.32% [kernel] [k] module_get_kallsym 3.65% [kernel] [k] _raw_spin_unlock_irqrestore ...
輸出結果中,第一行包含三個資料,分別是取樣數(Samples)、事件型別(event)和事件總 數量(Event count)。比如這個例子中,perf 總共採集了 833 個 CPU 時鐘事件,而總事件數 則為 97742399。
另外,取樣數需要我們特別注意。如果取樣數過少(比如只有十幾個),那下面的排序和百分比 就沒什麼實際參考價值了。
再往下看是一個表格式樣的資料,每一行包含四列,分別是:
- 第一列 Overhead ,是該符號的效能事件在所有采樣中的比例,用百分比來表示。
- 第二列 Shared ,是該函式或指令所在的動態共享物件(Dynamic Shared Object),如內 核、程序名、動態連結庫名、核心模組名等。
- 第三列 Object ,是動態共享物件的型別。比如 [.] 表示使用者空間的可執行程式、或者動態鏈 接庫,而 [k] 則表示核心空間。
- 最後一列 Symbol 是符號名,也就是函式名。當函式名未知時,用十六進位制的地址來表示。
還是以上面的輸出為例,我們可以看到,佔用 CPU 時鐘最多的是 perf 工具自身,不過它的比 例也只有 7.28%,說明系統並沒有 CPU 效能問題。 perf top 的使用你應該很清楚了吧。
接著再來看第二種常見用法,也就是 perf record 和 perf report。 perf top 雖然實時展示了系 統的效能資訊,但它的缺點是並不儲存資料,也就無法用於離線或者後續的分析。而 perf record 則提供了儲存資料的功能,儲存後的資料,需要你用 perf report 解析展示。
$ perf record # 按 Ctrl+C 終止取樣 [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ] $ perf report # 展示類似於 perf top 的報告
在實際使用中,我們還經常為 perf top 和 perf record 加上 -g 引數,開啟呼叫關係的取樣,方 便我們根據呼叫鏈來分析效能問題。
案例
下面我們就以 Nginx + PHP 的 Web 服務為例,來看看當你發現 CPU 使用率過高的問題後, 要怎麼使用 top 等工具找出異常的程序,又要怎麼利用 perf 找出引發效能問題的函式。
你的準備
案例 基於 Ubuntu 18.04,同樣適用於其他的 Linux 系統。我使用的案例環境如下所示:
- 機器配置:2 CPU,8GB 記憶體
- 預先安裝 docker、sysstat、perf、ab 等工具,如 apt install docker.io sysstat linuxtools-common apache2-utils
我先簡單介紹一下這次新使用的工具 ab。ab(apache bench)是一個常用的 HTTP 服務效能 測試工具,這裡用來模擬 Ngnix 的客戶端。由於 Nginx 和 PHP 的配置比較麻煩,我把它們打 包成了兩個 Docker 映象,這樣只需要執行兩個容器,就可以得到模擬環境。
注意,這個案例要用到兩臺虛擬機器,其中一臺用作 Web 伺服器,來模擬效能問題;另一臺用作 Web 伺服器的客戶端,來給 Web 服務增加壓力請求。使用兩臺虛擬機器是為了相互隔離,避 免“交叉感染”。
接下來,我們開啟兩個終端,分別 SSH 登入到兩臺機器上,並安裝上面提到的工具。還是同樣的“配方”。下面的所有命令,都預設假設以 root 使用者執行,如果你是普通使用者身份 登陸系統,一定要先執行 sudo su root 命令切換到 root 使用者。到這裡,準備工作就完成了。
不過,操作之前,我還想再說一點。這次案例中 PHP 應用的核心邏輯比較簡單,大部分人一眼 就可以看出問題,但你要知道,實際生產環境中的原始碼就複雜多了。
所以,我希望你在按照步驟操作之前,先不要檢視原始碼(避免先入為主),而是把它當成一個黑 盒來分析。這樣,你可以更好地理解整個解決思路,怎麼從系統的資源使用問題出發,分析出瓶 頸所在的應用、以及瓶頸在應用中的大概位置。
操作和分析
接下來,我們正式進入操作環節。
首先,在第一個終端執行下面的命令來執行 Nginx 和 PHP 應用:
$ docker run --name nginx -p 10000:80 -itd feisky/nginx $ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm
接著,我們來測試一下這個 Nginx 服務的效能。在第二個終端執行下面的 ab 命令:
# 併發 10 個請求測試 Nginx 效能,總共測試 100 個請求 $ ab -c 10 -n 100 http://192.168.0.10:10000/ This is ApacheBench, Version 2.3 <$Revision: 1706008 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, ... Requests per second: 11.63 [#/sec] (mean) Time per request: 859.942 [ms] (mean) ...
從 ab 的輸出結果我們可以看到,Nginx 能承受的每秒平均請求數只有 11.63。你一定在吐槽, 這也太差了吧。那到底是哪裡出了問題呢?我們用 top 和 pidstat 再來觀察下。
這次,我們在第二個終端,將測試的請求總數增加到 10000。這樣當你在第一個終端使用效能 分析工具時, Nginx 的壓力還是繼續。
繼續在第二個終端,執行 ab 命令:
$ ab -c 10 -n 10000 http://10.240.0.5:10000/
接著,回到第一個終端執行 top 命令,並按下數字 1 ,切換到每個 CPU 的使用率:
$ top ... %Cpu0 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 99.3 us, 0.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 21514 daemon 20 0 336696 16384 8712 R 41.9 0.2 0:06.00 php-fpm 21513 daemon 20 0 336696 13244 5572 R 40.2 0.2 0:06.08 php-fpm 21515 daemon 20 0 336696 16384 8712 R 40.2 0.2 0:05.67 php-fpm 21512 daemon 20 0 336696 13244 5572 R 39.9 0.2 0:05.87 php-fpm 21516 daemon 20 0 336696 16384 8712 R 35.9 0.2 0:05.61 php-fpm
這裡可以看到,系統中有幾個 php-fpm 程序的 CPU 使用率加起來接近 200%;而每個 CPU 的 使用者使用率(us)也已經超過了 98%,接近飽和。這樣,我們就可以確認,正是使用者空間的 php-fpm 程序,導致 CPU 使用率驟升。
那再往下走,怎麼知道是 php-fpm 的哪個函式導致了 CPU 使用率升高呢?我們來用 perf 分析 一下。在第一個終端執行下面的 perf 命令:
# -g 開啟呼叫關係分析,-p 指定 php-fpm 的程序號 21515 $ perf top -g -p 21515
按方向鍵切換到 php-fpm,再按下回車鍵展開 php-fpm 的呼叫關係,你會發現,呼叫關係最 終到了 sqrt 和 add_function。看來,我們需要從這兩個函式入手了。
我們拷貝出 Nginx 應用的原始碼,看看是不是呼叫了這兩個函式:
# 從容器 phpfpm 中將 PHP 原始碼拷貝出來 $ docker cp phpfpm:/app . # 使用 grep 查詢函式呼叫 $ grep sqrt -r app/ # 找到了 sqrt 呼叫 app/index.php: $x += sqrt($x); $ grep add_function -r app/ # 沒找到 add_function 呼叫,這其實是 PHP 內建函式
OK,原來只有 sqrt 函式在 app/index.php 檔案中呼叫了。那最後一步,我們就該看看這個文 件的原始碼了:
$ cat app/index.php <?php // test only. $x = 0.0001; for ($i = 0; $i <= 1000000; $i++) { $x += sqrt($x); } echo "It works!"
呀,有沒有發現問題在哪裡呢?我想你要笑話我了,居然犯了一個這麼傻的錯誤,測試程式碼沒刪 就直接釋出應用了。為了方便你驗證優化後的效果,我把修復後的應用也打包成了一個 Docker 映象,你可以在第一個終端中執行下面的命令來執行它:
# 停止原來的應用 $ docker rm -f nginx phpfpm # 執行優化後的應用 $ docker run --name nginx -p 10000:80 -itd feisky/nginx:cpu-fix $ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:cpu-fix
接著,到第二個終端來驗證一下修復後的效果。首先 Ctrl+C 停止之前的 ab 命令後,再執行下 面的命令:
$ ab -c 10 -n 10000 http://10.240.0.5:10000/ ... Complete requests: 10000 Failed requests: 0 Total transferred: 1720000 bytes HTML transferred: 90000 bytes Requests per second: 2237.04 [#/sec] (mean) Time per request: 4.470 [ms] (mean) Time per request: 0.447 [ms] (mean, across all concurrent requests) Transfer rate: 375.75 [Kbytes/sec] received ...
從這裡你可以發現,現在每秒的平均請求數,已經從原來的 11 變成了 2237。
你看,就是這麼很傻的一個小問題,卻會極大的影響效能,並且查詢起來也並不容易吧。當然, 找到問題後,解決方法就簡單多了,刪除測試程式碼就可以了。
小結
CPU 使用率是最直觀和最常用的系統性能指標,更是我們在排查效能問題時,通常會關注的第 一個指標。所以我們更要熟悉它的含義,尤其要弄清楚使用者(%user)、Nice(%nice)、系統 (%system) 、等待 I/O(%iowait) 、中斷(%irq)以及軟中斷(%softirq)這幾種不同 CPU 的使用率。比如說:
- 使用者 CPU 和 Nice CPU 高,說明使用者態程序佔用了較多的 CPU,所以應該著重排查程序的 效能問題。
- 系統 CPU 高,說明核心態佔用了較多的 CPU,所以應該著重排查核心執行緒或者系統呼叫的 效能問題。
- I/O 等待 CPU 高,說明等待 I/O 的時間比較長,所以應該著重排查系統儲存是不是出現了 I/O 問題。
- 軟中斷和硬中斷高,說明軟中斷或硬中斷的處理程式佔用了較多的 CPU,所以應該著重排查 核心中的中斷服務程式。
碰到 CPU 使用率升高的問題,你可以藉助 top、pidstat 等工具,確認引發 CPU 效能問題的來 源;再使用 perf 等工具,排查出引起效能問題的具體函式。
技巧:
1.cpu使用率,就是cpu被使用的比例,也就是空閒之外的使用比例。 發現cpu使用率高後,先用perf來抓取cpu消耗棧,很容易發現瓶頸。 另外,用mpstat -P ALL 來看各個cpu核心的使用率情況,因為top之類的看的是系統總使用率,不一定能發現問題,特別是多程序或者多執行緒應用。