最常用什麼指標來描述系統的 CPU 性能呢?可能不是平均負載,也不是 CPU 上下文切換,而是另一個更直觀的指標—— CPU 使用率。 我們前面說過,CPU 使用率是單位時間內 CPU 使用情況的統計,以百分比的方式展示。諸 如 top、ps 之類的性能工具展示的 %user、%nice、 %sys ...
最常用什麼指標來描述系統的 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 cpu 280580 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之類的看的是系統總使用率,不一定能發現問題,特別是多進程或者多線程應用。