天天看點

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

作者:一個即将退役的碼農

上一篇我們講解了性能優化的7類技術手段,本篇我們将從計算機資源層面向你講解,哪些系統元件容易出現性能瓶頸?以及如何判斷該系統元件是否達到了瓶頸?

計算機各個元件之間的速度往往很不均衡,比如 CPU 和硬碟,比兔子和烏龜的速度差還大,那麼按照我們前面介紹的木桶理論,可以說這個系統是存在着短闆的。

當系統存在短闆時,就會對性能造成較大的負面影響,比如當 CPU 的負載特别高時,任務就會排隊,不能及時執行。而其中,CPU、記憶體、I/O 這三個系統元件,又往往容易成為瓶頸,是以接下來我會對這三個方面分别進行講解。

CPU

首先介紹計算機中最重要的計算元件中央處理器 CPU,圍繞 CPU 一般我們可以:

  • 通過 top 指令,來觀測 CPU 的性能;
  • 通過負載,評估 CPU 任務執行的排隊情況;
  • 通過 vmstat,看 CPU 的繁忙程度。

具體情況如下。

1.top 指令 —— CPU 性能

如下圖,當進入 top 指令後,按 1 鍵即可看到每核 CPU 的運作名額和詳細性能。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

CPU 的使用有多個次元的名額,下面分别說明:

  • us 使用者态所占用的 CPU 百分比,即引用程式所耗費的 CPU;
  • sy 核心态所占用的 CPU 百分比,需要配合 vmstat 指令,檢視上下文切換是否頻繁;
  • ni 高優先級應用所占用的 CPU 百分比;
  • wa 等待 I/O 裝置所占用的 CPU 百分比,經常使用它來判斷 I/O 問題,過高輸入輸出裝置可能存在非常明顯的瓶頸;
  • hi 硬中斷所占用的 CPU 百分比;
  • si 軟中斷所占用的 CPU 百分比;
  • st 在平常的伺服器上這個值很少發生變動,因為它測量的是主控端對虛拟機的影響,即虛拟機等待主控端 CPU 的時間占比,這在一些超賣的雲伺服器上,經常發生;
  • id 空閑 CPU 百分比。

一般地,我們比較關注空閑 CPU 的百分比,它可以從整體上展現出來 CPU 的利用情況。

2.負載 —— CPU 任務排隊情況

如果我們評估 CPU 任務執行的排隊情況,那麼需要通過負載(load)來完成。除了 top 指令,使用 uptime 指令也能夠檢視負載情況,load 的效果是一樣的,分别顯示了最近 1min、5min、15min 的數值。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

如上圖所示,以單核作業系統為例,将 CPU 資源抽象成一條單向行駛的馬路,則會發生以下三種情況:

  • 馬路上的車隻有 4 輛,車輛暢通無阻,load 大約是 0.5;
  • 馬路上的車有 8 輛,正好能首尾相接安全通過,此時 load 大約為 1;
  • 馬路上的車有 12 輛,除了在馬路上的 8 輛車,還有 4 輛等在馬路外面,需要排隊,此時 load 大約為 1.5。

那 load 為 1 代表的是啥?針對這個問題,誤解還是比較多的。

很多人看到 load 的值達到 1,就認為系統負載已經到了極限。這在單核的硬體上沒有問題,但在多核硬體上,這種描述就不完全正确,它還與 CPU 的個數有關。例如:

  • 單核的負載達到 1,總 load 的值約為 1;
  • 雙核的每核負載都達到 1,總 load 約為 2;
  • 四核的每核負載都達到 1,總 load 約為 4。

是以,對于一個 load 到了 10,卻是 16 核的機器,你的系統還遠沒有達到負載極限。

3.vmstat —— CPU 繁忙程度

要看 CPU 的繁忙程度,可以通過 vmstat 指令,下圖是 vmstat 指令的一些輸出資訊。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

比較關注的有下面幾列:

  • b 如果系統有負載問題,就可以看一下 b 列(Uninterruptible Sleep),它的意思是等待 I/O,可能是讀盤或者寫盤動作比較多;
  • si/so 顯示了交換分區的一些使用情況,交換分區對性能的影響比較大,需要格外關注;
  • cs 每秒鐘上下文切換(Context Switch)的數量,如果上下文切換過于頻繁,就需要考慮是否是程序或者線程數開的過多。

每個程序上下文切換的具體數量,可以通過檢視記憶體映射檔案擷取,如下代碼所示:

[root@localhost ~]# cat /proc/2788/status

...voluntary_ctxt_switches: 93950nonvoluntary_ctxt_switches: 171204

記憶體

要想了解記憶體對性能的影響,則需要從作業系統層面來看一下記憶體的分布。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

我們在平常寫完代碼後,比如寫了一個 C++ 程式,去檢視它的彙編,如果看到其中的記憶體位址,并不是實際的實體記憶體位址,那麼應用程式所使用的,就是邏輯記憶體。學過計算機組成結構的同學應該都有了解。

邏輯位址可以映射到兩個記憶體段上:實體記憶體和虛拟記憶體,那麼整個系統可用的記憶體就是兩者之和。比如你的實體記憶體是 4GB,配置設定了 8GB 的 SWAP 分區,那麼應用可用的總記憶體就是 12GB。

1. top 指令

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

如上圖所示,我們看一下記憶體的幾個參數,從 top 指令可以看到幾列資料,注意方塊框起來的三個區域,解釋如下:

  • VIRT 這裡是指虛拟記憶體,一般比較大,不用做過多關注;
  • RES 我們平常關注的是這一列的數值,它代表了程序實際占用的記憶體,平常在做監控時,主要監控的也是這個數值;
  • SHR 指的是共享記憶體,比如可以複用的一些 so 檔案等。

2. CPU 緩存

由于 CPU 和記憶體之間的速度差異非常大,解決方式就是加入高速緩存。實際上,這些高速緩存往往會有多層,如下圖所示。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

Java 有大部分知識點是圍繞多線程的,那是因為,如果一個線程的時間片跨越了多個 CPU,那麼就會存在同步問題。

在 Java 中,和 CPU 緩存相關的最典型的知識點,就是在并發程式設計中,針對 Cache line 的僞共享(False Sharing)問題。

僞共享指的是在這些高速緩存中,以緩存行為機關進行存儲,哪怕你修改了緩存行中一個很小很小的資料,它都會整個重新整理。是以,當多線程修改一些變量的值時,如果這些變量都在同一個緩存行裡,就會造成頻繁重新整理,無意中影響彼此的性能。

CPU 的每個核,基本是相同的,我們拿 CPU0 來說,可以通過以下的指令檢視它的緩存行大小,這個值一般是 64。

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_sizecat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_sizecat /sys/devices/system/cpu/cpu0/cache/index2/coherency_line_sizecat /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size

當然,通過 cpuinfo 也能得到一樣的結果:

# cat /proc/cpuinfo | grep cache

cache size : 20480 KB

cache_alignment : 64

cache size : 20480 KB

cache_alignment : 64

cache size : 20480 KB

cache_alignment : 64

cache size : 20480 KB

cache_alignment : 64

在 JDK8 以上的版本,通過開啟參數 -XX:-RestrictContended,就可以使用注解 @sun.misc.Contended 進行補齊,來避免僞共享的問題。具體情況,在 12 課時并行優化中,我們再詳細講解。

3. HugePage

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

我們再回顧一下上文提到的這張圖,上面有一個 TLB 元件,它的速度很快,但容量有限,在普通的 PC 機上沒有什麼瓶頸。但如果機器配置比較高,實體記憶體比較大,那就會産生非常多的映射表,CPU 的檢索效率也會随之降低。

傳統的頁大小是 4KB,在大記憶體時代這個值偏小了,解決的辦法就是增加頁的尺寸,比如将其增加到 2MB,這樣,就可以使用較少的映射表來管理大記憶體。而這種将頁增大的技術,就是 Huge Page。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

同時,HugePage 也伴随着一些副作用,比如競争加劇,但在一些大記憶體的機器上,開啟後在一定程度上會增加性能。

4. 預先加載

另外,一些程式的預設行為也會對性能有所影響,比如 JVM 的 -XX:+AlwaysPreTouch 參數。

預設情況下,JVM 雖然配置了 Xmx、Xms 等參數,指定堆的初始化大小和最大大小,但它的記憶體在真正用到時,才會配置設定;但如果加上 AlwaysPreTouch 這個參數,JVM 會在啟動的時候,就把所有的記憶體預先配置設定。

這樣,啟動時雖然慢了些,但運作時的性能會增加。

I/O

I/O 裝置可能是計算機裡速度最慢的元件了,它指的不僅僅是硬碟,還包括外圍的所有裝置。那硬碟有多慢呢?我們不去探究不同裝置的實作細節,直接看它的寫入速度(資料未經過嚴格測試,僅作參考)。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

如上圖所示,可以看到普通磁盤的随機寫與順序寫相差非常大,但順序寫與 CPU 記憶體依舊不在一個數量級上。

緩沖區依然是解決速度差異的唯一工具,但在極端情況下,比如斷電時,就産生了太多的不确定性,這時這些緩沖區,都容易丢。由于這部分内容的篇幅比較大,我将在第 06 課時專門講解。

1. iostat

最能展現 I/O 繁忙程度的,就是 top 指令和 vmstat 指令中的 wa%。如果你的應用寫了大量的日志,I/O wait 就可能非常高。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

很多同學回報到,不知道有哪些便捷好用的檢視磁盤 I/O 的工具,其實 iostat 就是。你可以通過 sysstat 包進行安裝。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

上圖中的名額詳細介紹如下所示。

  • %util:我們非常關注這個數值,通常情況下,這個數字超過 80%,就證明 I/O 的負荷已經非常嚴重了。
  • Device:表示是哪塊硬碟,如果你有多塊磁盤,則會顯示多行。
  • avgqu-sz:平均請求隊列的長度,這和十字路口排隊的汽車也非常類似。顯然,這個值越小越好。
  • awai:響應時間包含了隊列時間和服務時間,它有一個經驗值。通常情況下應該是小于 5ms 的,如果這個值超過了 10ms,則證明等待的時間過長了。
  • svctm:表示操作 I/O 的平均服務時間。你可以回憶一下第 01 課時的内容,在這裡就是 AVG 的意思。svctm 和 await 是強相關的,如果它們比較接近,則表示 I/O 幾乎沒有等待,裝置的性能很好;但如果 await 比 svctm 的值高出很多,則證明 I/O 的隊列等待時間太長,進而系統上運作的應用程式将變慢。

2. 零拷貝

硬碟上的資料,在發往網絡之前,需要經過多次緩沖區的拷貝,以及使用者空間和核心空間的多次切換。如果能減少一些拷貝的過程,效率就能提升,是以零拷貝應運而生。

零拷貝是一種非常重要的性能優化手段,比如常見的 Kafka、Nginx 等,就使用了這種技術。我們來看一下有無零拷貝之間的差別。

(1)沒有采取零拷貝手段

如下圖所示,傳統方式中要想将一個檔案的内容通過 Socket 發送出去,則需要經過以下步驟:

  • 将檔案内容拷貝到核心空間;
  • 将核心空間記憶體的内容,拷貝到使用者空間記憶體,比如 Java 應用讀取 zip 檔案;
  • 使用者空間将内容寫入到核心空間的緩存中;
  • Socket 讀取核心緩存中的内容,發送出去。
java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

沒有采取零拷貝手段的圖

(2)采取了零拷貝手段

零拷貝有多種模式,我們用 sendfile 來舉例。如下圖所示,在核心的支援下,零拷貝少了一個步驟,那就是核心緩存向使用者空間的拷貝,這樣既節省了記憶體,也節省了 CPU 的排程時間,讓效率更高。

java性能優化實戰:系統元件的瓶頸有哪些以及如何判斷?

采取了零拷貝手段的圖

小結

我們學習了計算機中對性能影響最大的三個元件:CPU、記憶體、I/O,并深入了解了觀測它們性能的一些指令,這些方式可以幫我們大體猜測性能問題發生的地方。

但它們對性能問題,隻能起到輔助作用,不能幫助我們精準地定位至真正的性能瓶頸,還需要做更多深入的排查工作,收集更多資訊。

關于一系列更深入的工具,請參照我的這篇文章,非常全。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

幫你擷取性能資料,離“病竈”更近一步。