引言:性能瓶頸調優
在實際的性能測試中,會遇到各種各樣的問題,比如 TPS 壓不上去等,導致這種現象的原因有很多,測試人員應配合開發人員進行分析,盡快找出瓶頸所在。
理想的性能測試指標結果可能不是很高,但一定是平緩的。
性能調優步驟
-
確定問題:根據性能監控的數據和性能分析的結果,確定性能存在的問題。
-
確定原因:確定問題之后,對問題進行分析,找出問題的原因。
-
確定解決方案(改服務器參數配置/增加硬件資源配置/修改代碼)。
-
驗證解決方案,分析調優結果。
注意:性能測試調優並不是一次完成的過程,針對同一個性能問題,上述步驟可能要經過多次循環才能最終完成性能調優的目標,即:測試發現問題 -> 找原因 -> 調整 -> 驗證 -> 分析 -> 再測試 ...
性能瓶頸概率分布
60%:數據庫瓶頸
- 數據庫服務器 CPU 使用率高(慢查詢、SQL 過多、連接數過多)
- 拋出連接數過多(連接池設置太小,導致連接排隊)
- 數據庫出現死鎖
25%:應用瓶頸
- 應用出現內存泄露
- 應用出現線程競爭/死鎖
- 程序代碼的算法復雜度
- 中間件、第三方應用出現異常
- 計算密集型任務引起 CPU 負載高
- I/O 密集型任務引起 I/O 負載高
10%:壓測工具瓶頸
- JMeter 單機負載能力有限,如果需要模擬的用戶請求數超過其負載極限,也會導致 TPS 壓不上去
5%:Linux 機器出現異常
- Linux 可用內存無法回收(開銷速率大於回收速率)
系統資源
-
CPU
- 監控內容:CPU 使用率、CPU 使用類型(用戶進程、內核進程)
- 瓶頸分析:CPU已壓滿(接近 100%),需要再看其他指標的拐點所出現的時刻是否與 CPU 壓滿的時刻基本一致。
-
內存
- 監控內容:實際內存、虛擬內存
- 瓶頸分析:內存不足時,操作系統會使用虛擬內存,從虛擬內存讀取數據,影響處理速度。
-
磁盤 I/O
- 監控內容:I/O 速度、磁盤等待隊列
- 瓶頸分析:磁盤 I/O 成為瓶頸時,會出現磁盤I/O繁忙,導致交易執行時在 I/O 處等待。
-
網絡
- 監控內容:網絡流量(帶寬使用率)、網絡連接狀態
- 瓶頸分析:如果接口傳遞的數據包過大,超過了帶寬的傳輸能力,就會造成網絡資源競爭, 導致 TPS 上不去。
發現了瓶頸后,只要對症下葯就可以了。簡單來說無論哪個地方出現瓶頸,只需要降低壓力或者增加這部分瓶頸資源(應用軟件沒有瓶頸或優化空間之后),即可緩解症狀。
- CPU 瓶頸:增加 CPU 資源。
- 內存瓶頸:增加內存、釋放緩存。
- 磁盤 I/O 瓶頸:更換性能更高的磁盤(如固態 SSD)。
- 網絡帶寬瓶頸;增加網絡帶寬。
CPU
后台服務的所有指令和數據處理都是由 CPU 負責,服務對 CPU 的利用率對服務的性能起着決定性的作用。
top 參數詳解
下面以 top 命令的輸出例,對 CPU 各項主要指標進行說明:
-
us(user):運行(未調整優先級的)用戶進程所消耗的 CPU 時間的百分比。
-
像 shell 程序、各種語言的編譯器、數據庫應用、web 服務器和各種桌面應用都算是運行在用戶地址空間的進程。
-
這些程序如果不是處於 idle 狀態,那么絕大多數的 CPU 時間都是運行在用戶態。
-
-
sy(system):運行內核進程所消耗的 CPU 時間的百分比。
-
所有進程要使用的系統資源都是由 Linux 內核處理的。當處於用戶態(用戶地址空間)的進程需要使用系統的資源時,比如需要分配一些內存、或是執行 I/O 操作、再或者是去創建一個子進程,此時就會進入內核態(內核地址空間)運行。事實上,決定進程在下一時刻是否會被運行的進程調度程序就運行在內核態。
-
對於操作系統的設計來說,消耗在內核態的時間應該是越少越好。通常 sy 比例過高意味着被測服務在用戶態和系統態之間切換比較頻繁,此時系統整體性能會有一定下降。
-
在實踐中有一類典型的情況會使 sy 變大,那就是大量的 I/O 操作,因此在調查 I/O 相關的問題時需要着重關注它。
-
大部分后台服務使用的 CPU 時間片中 us 和 sy 的占用比例是最高的。同時這兩個指標又是互相影響的,us 的比例高了,sy 的比例就低,反之亦然。
-
另外,在使用多核 CPU 的服務器上,CPU 0 負責 CPU 各核間的調度,CPU 0 上的使用率過高會導致其他 CPU 核心之間的調度效率變低。因此測試過程中需要重點關注 CPU 0。
-
-
ni(niced):用做 nice 加權的進程分配的用戶態 CPU 時間百分比。
- 每個 Linux 進程都有個優先級,優先級高的進程有優先執行的權利,這個叫做 pri。進程除了優先級外,還有個優先級的修正值。這個修正值就叫做進程的 nice 值。
- 這里顯示的 ni 表示調整過 nice 值的進程消耗掉的 CPU 時間。如果系統中沒有進程被調整過 nice 值,那么 ni 就顯示為 0。
- 一般來說,被測服務和服務器整體的 ni 值不會很高。如果測試過程中 ni 的值比較高,需要從服務器 Linux 系統配置、被測服務運行參數查找原因。
-
id(idle):空閑的 CPU 時間百分比。
-
一般情況下, us + ni + id 應該接近 100%。
-
線上服務運行過程中,需要保留一定的 id 冗余來應對突發的流量激增。
-
在性能測試過程中,如果 id 一直很低,吞吐量上不去,需要檢查被測服務線程/進程配置、服務器系統配置等。
-
-
wa(I/O wait):CPU 等待 I/O 完成時間百分比。
-
和 CPU 的處理速度相比,磁盤 I/O 操作是非常慢的。有很多這樣的操作,比如:CPU 在啟動一個磁盤讀寫操作后,需要等待磁盤讀寫操作的結果。在磁盤讀寫操作完成前,CPU 只能處於空閑狀態。
-
Linux 系統在計算系統平均負載時會把 CPU 等待 I/O 操作的時間也計算進去,所以在我們看到系統平均負載過高時,可以通過 wa 來判斷系統的性能瓶頸是不是過多的 I/O 操作造成的。
-
磁盤、網絡等 I/O 操作會導致 CPU 的 wa 指標提高。通常情況下,網絡 I/O 占用的 wa 資源不會很高,而頻繁的磁盤讀寫會導致 wa 激增。
-
如果被測服務不是 I/O 密集型的服務,那需要檢查被測服務的日志量、數據載入頻率等。
-
如果 wa 高於 10% 則系統開始出現卡頓;若高於 20% 則系統幾乎動不了;若高於 50% 則很可能磁盤出現故障。
-
-
hi:硬中斷消耗時間百分比。
-
si:軟中斷消耗時間百分比。
- 硬中斷是外設對 CPU 的中斷,即外圍硬件發給 CPU 或者內存的異步信號就是硬中斷信號;軟中斷由軟件本身發給操作系統內核的中斷信號。
- 通常是由硬中斷處理程序或進程調度程序對操作系統內核的中斷,也就是我們常說的系統調用(System Call)。
- 在性能測試過程中,hi 會有一定的 CPU 占用率,但不會太高。對於 I/O 密集型的服務,si 的 CPU 占用率會高一些。
-
st:虛擬機等待 CPU 資源的時間。
- 只有 Linux 在作為虛擬機運行時 st 才是有意義的。它表示虛機等待 CPU 資源的時間(虛機分到的是虛擬 CPU,當需要真實的 CPU 時,可能真實的 CPU 正在運行其它虛機的任務,所以需要等待)。
案例分析
現象:wa 與 id
-
wa(IO wait)的值過高,表示硬盤存在 I/O 瓶頸。
-
id(idle)值高,表示 CPU 較空閑。
-
如果 id 值高但系統響應慢時,有可能是 CPU 等待分配內存,此時應加大內存容量。
-
如果 id 值持續低於 10,那么系統的 CPU 處理能力相對較低,表明系統中最需要解決的資源是 CPU。
-
現象:CPU 的 us 和 sy 不高,但 wa 很高。
如果被測服務是磁盤 I/O 密集型服務,wa 高屬於正常現象。但如果不是此類服務,最可能導致 wa 高的原因有兩個:
-
服務對磁盤讀寫的業務邏輯有問題,讀寫頻率過高,寫入數據量過大,如不合理的數據載入策略、log 過多等,都有可能導致這種問題。
-
服務器內存不足,服務在 swap 分區不停的換入換出。
現象:CPU 與吞吐量
-
CPU 占用不高,吞吐量較低,可能是服務端線程池啟動太少。
-
CPU 占用很高,吞吐量較低,服務端處理慢,可能操作數據庫慢。
-
CPU 占用很高,吞吐量很高:
- 服務端處理能力強,需要調整線程數降低 CPU 使用率。
- 數據庫連接數、慢 SQL、文件句柄優化。
- 提升物理設備。
LOAD
Linux 的系統負載指在特定時間間隔內(一個 CPU 周期)運行隊列中的平均進程數。
(注意:Linux 中的 Load 體現的是整體系統負載,即 CPU 負載 + 磁盤負載 + 網絡負載 + 其余外設負載,並不能完全等同於 CPU 使用率。而在其余系統如 Unix,Load 還是只代表 CPU 負載。)
從服務器負載的定義可以看出,服務器運行最理想的狀態是所有 CPU 核心的運行隊列都為 1,即所有活動進程都在運行,沒有等待。
這種狀態下服務器運行在負載閾值下。
通常情況下,按照經驗值,服務器的負載應位於閾值的 70%~80%,這樣既能利用服務器大部分性能,又留有一定的性能冗余應對流量增長。
查看系統負載閾值的命令如下:
Linux 提供了很多查看系統負載的命令,最常用的是 top 和 uptime。
top 和 uptime 針對負載的輸出內容相同,都是系統最近 1 分鍾、5 分鍾、15 分鍾的負載均值
:
這三個數值的使用方法和 CPU 核數相關,首先確認 CPU 物理總核數:
-
/proc/cpuinfo 中的 processors 的最大值不一定是 CPU 的核數,有可能該 CPU 支持超線程技術,從而 processors 是物理核數的 2 倍。
-
這里我們需要准確的核數,具體方法為:找到 /proc/cpuinfo 文件中所有的 physical id 后的數值,取得最大的數值,加一后就是實際的 CPU 個數。然后查找任意一個 processors 下的 cpu cores,即是該顆 CPU 的核數,實際 CPU 個數乘以核數即為 CPU 的物理總核數。
示例:
[root@localhost home]# cat /proc/cpuinfo |grep "physical id"
physical id : 0
physical id : 0
[root@localhost home]# cat /proc/cpuinfo |grep "cpu cores"
cpu cores : 2
cpu cores : 2
物理 CPU 個數為 0+1=1 個,每個 CPU 的核數為 2 個,所以總的物理核數為 2x1=2。
計算結果說明該機器的在單位時間內可以處理的進程數是 2 個,如果單位時間內進程數超過 2 個,就會出現擁堵的情況,load 就會持續增高,增高到一定程度,就會出現系統崩潰等異常情況。
在性能測試過程中,系統負載是評價整個系統運行狀況最重要的指標之一。通常情況下:
-
負載測試時:系統負載應接近但不能超過閾值。
-
並發測試時:系統負載最高不能超過閾值的 80%。
-
穩定性測試時:系統負載應在閾值的 50% 左右。
機器針對突發情況的處理
-
如果 1 分鍾 load 很高,5 分鍾 load 較高,15 分鍾 load 起伏不大的情況下,說明該次高 load 為突發情況,可以容忍。
-
如果高 load 持續,導致 5 分鍾和 15 分鍾 load 都已經超過報警值,這時候需要考慮進行處理。
-
如果 15 分鍾 load 高於 1 分鍾 load,說明高 load 情況已經得到緩解。
內存
性能測試過程中對內存監控的主要目的是檢查被測服務所占用內存的波動情況。
top 參數詳解
在 Linux 系統中有多個命令可以獲取指定進程的內存使用情況,最常用的是 top 命令,如下圖所示:
-
VIRT:進程所使用的虛擬內存的總數。它包括所有的代碼,數據和共享庫,加上已換出的頁面,所有已申請的總內存空間。
-
RES:進程正在使用的沒有交換的物理內存(棧、堆)。申請內存后該內存段已被重新賦值。
-
SHR:進程使用共享內存的總數。該數值只是反映可能與其它進程共享的內存,不代表這段內存當前正被其他進程使用。
-
SWAP:進程使用的虛擬內存中被換出的大小。交換的是已經申請但沒有使用的空間(包括棧、堆、共享內存)。
-
DATA:進程除可執行代碼以外的物理內存總量,即進程棧、堆申請的總空間。
從上面的解釋可以看出,測試過程中主要監控 RES 和 VIRT。對於使用了共享內存的多進程架構服務,還需要監控 SHR。
free 參數詳解
free 命令顯示系統內存的使用情況,包括物理內存、交換內存(swap)和內核緩沖區內存。如果加上 -h 選項(控制顯示單位),輸出的結果會友好很多:
有時我們需要持續的觀察內存的狀況,此時可以使用 -s 選項並指定間隔的秒數:如 free -h -s 3 表示每隔 3 秒輸出一次內存的使用情況,直到按下 ctrl + c。
-
Mem 行:物理內存的使用情況。
-
Swap 行:交換空間的使用情況。
-
swap space 是磁盤上的一塊區域,可以是一個分區,也可以是一個文件,所以具體的實現可以是 swap 分區也可以是 swap 文件。當系統物理內存吃緊時,Linux 會將內存中不常訪問的數據保存到 swap 上,這樣系統就有更多的物理內存為各個進程服務,而當系統需要訪問 swap 上存儲的內容時,再將 swap 上的數據加載到內存中,這就是常說的換出和換入。
-
交換空間可以在一定程度上緩解內存不足的情況,但是它需要讀寫磁盤數據,所以性能不是很高。因此
當交換空間內存開始使用,則表明內存嚴重不足
。 -
如果系統內存充足或是做性能壓測的機器,可以使用 swapoff -a 關閉交換空間,或在 /etc/sysctl.conf 文件中設置 swappiness 值。
如果系統內存不富余,則需要根據物理內存的大小來設置交換空間的大小,具體的策略網上有很豐富的資料。
-
-
total 列:系統總的可用物理內存和交換空間大小。
-
used 列:已經被使用的物理內存和交換空間大小。
-
free 列:還有多少物理內存和交換空間可用使用(真正尚未被使用的物理內存數量)。
在吞吐量固定的前提下,如果內存持續上漲,那么很有可能是被測服務存在明顯的內存泄漏,需要使用 valgrind 等內存檢查工具進行定位。
-
shared 列:被共享使用的物理內存大小。
-
buffer/cache 列:被 buffer 和 cache 使用了的物理內存大小。
-
Linux 內核為了提升磁盤操作的性能,會消耗一部分空閑內存去緩存磁盤數據,就是 buffer 和 cache。
-
如果給所有應用分配足夠內存后,物理內存還有剩余,linux 會盡量再利用這些空閑內存,以提高整體 I/O 效率,其方法是把這部分剩余內存再划分為 cache 及 buffer 兩部分加以利用。
-
所以,
空閑物理內存不多,不一定表示系統運行狀態很差,因為內存的 cache 及 buffer 部分可以隨時被重用,在某種意義上,這兩部分內存也可以看作是額外的空閑內存。
-
-
available 列:還可以被應用程序使用的物理內存大小。
- 從應用程序的角度來說,
available = free + buffer + cache
。請注意,這只是一個很理想的計算方式,實際中的數據往往有較大的誤差。
- 從應用程序的角度來說,
釋放緩存內存
方式一:手動釋放緩存內存
snyc
echo 3 > /proc/sys/vm/drop_caches
free -m
方式二:修改 linux 配置自動釋放
/proc/sys/vm/drop_caches 這個值的 0 改為 1
磁盤 I/O
性能測試過程中,如果被測服務對磁盤讀寫過於頻繁,會導致大量請求處於 I/O 等待的狀態,系統負載升高,響應時間變長,吞吐量下降。
性能監控時的關注點
-
I/O 使用率:磁盤實際 I/O 是否已接近最大值,接近則有問題。
-
I/O 隊列:如果當前 I/O 隊列長度一直不為 0,則有問題。
固態硬盤:500M/s
機械硬盤:不超過 200M/s
iostat 參數詳解
Linux 下可以用 iostat 命令來監控磁盤狀態。
iostat -d 2 10 表示每 2 秒統計一次基礎數據,統計 10 次:
-
tps:該設備每秒的傳輸次數。“一次傳輸”意思是“一次 I/O 請求”。多個邏輯請求可能會被合並為“一次 I/O 請求”。“一次傳輸”請求的大小是未知的。
-
kB_read/s:每秒從設備(driveexpressed)讀取的數據量,單位為 Kilobytes。
-
kB_wrtn/s:每秒向設備(driveexpressed)寫入的數據量,單位為 Kilobytes。
-
kB_read:讀取的總數據量,單位為 Kilobytes。
-
kB_wrtn:寫入的總數量數據量,單位為 Kilobytes。
從 iostat -d 的輸出中,能夠獲得系統運行最基本的統計數據。但對於性能測試來說,這些數據不能提供更多的信息。需要加上 -x 參數。
iostat -x 參數詳解
如 iostat -x 2 10 表示每 2 秒統計一次更詳細數據,統計 10 次:
-
rrqm/s:每秒這個設備相關的讀取請求有多少被 Merge 了。
- 當系統調用需要讀取數據的時候,VFS 將請求發到各個 FS,如果 FS 發現不同的讀取請求讀取的是相同 Block 的數據,FS 會將這個請求合並 Merge。
-
wrqm/s:每秒這個設備相關的寫入請求有多少被 Merge 了。
-
await:每一個 I/O 請求的處理的平均時間(單位:毫秒)。
-
await 的大小一般取決於服務時間(svtcm)以及 I/O 隊列的長度和 I/O 請求的發出模式。假設 svtcm 比較接近 await,說明 I/O 差點沒有等待時間。
-
假設 await 遠大於 svctm(如大於 5),就要考慮 I/O 有壓力瓶頸,說明 I/O 隊列太長,應用得到的響應時間變慢。
假設響應時間超過了用戶能夠容許的范圍,這時可以考慮更換更快的磁盤。
-
-
svctm:I/O 平均服務時間。
-
%util:在統計時間內有百分之多少用於 I/O 操作。
-
例如,如果統計間隔 1 秒,該設備有 0.8 秒在處理 I/O,而 0.2 秒閑置,那么該設備的 %util = 0.8/1 = 80%,該參數暗示了設備的繁忙程度。
-
%util 接近100% 表明 I/O 請求太多,I/O 系統繁忙,磁盤可能存在瓶頸。
-
iostat -x 完整參數如下:
- rrqm/s: 每秒進行 merge 的讀操作數目。即 delta(rerge)/s
- wrqm/s: 每秒進行 merge 的寫操作數目。即 delta(wmerge)/s
- t/s: 每秒完成的讀 I/O 設備次數。即 delta(rioVs
- w/s: 每秒完成的寫 1/O 設備次數。即 delta(wio)/s
- rsec/s: 每秒讀扇區數。即 delta(rsect)/s
- ws0c/s: 每秒寫扇區數。即 deita(wsect)/s
- rkB/s: 每秒讀 K 字節數。是 rsect/s 的一半,因為每扇區大小為 512 字節。(需要計算)
- wkB/s: 每秒寫 K 字節數。是 wsect/s 的一半。(需要計算)
- avgrq+sz: 平均每次設備 I/O 操作的數據大小(扇區)。delta(rsect+wsect)/delta(rio+wio)
- avgqu-sz: 平均I/O隊列長度,即delta(avea)/s/1000(因為 aveq 的單位為毫秒)。
- await: 平均每次設備 I/O 操作的等待時間(毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
- svctm: 平均每次設備 I/O 操作的服務時間(毫秒)。即 delta(use)/delta(rio+wio)
- %util:一秒中有百分之多少的時間用於 I/O 操作,或者說一秒中有多少時間 I/O 隊列是非空的。即 delta(use)/s/1000(因為 use 的單位為毫秒)
網絡
性能測試中網絡監控主要包括網絡流量、網絡連接狀態的監控。
網絡流量監控
方法很多,網上有很多 shell 腳本。也可以使用 nethogs 命令。該命令與 top 類似,是一個實時交互的命令,運行界面如下:
在后台服務性能測試中,對於返回文本結果的服務,並不需要太多關注在流量方面。
理解帶寬
針對一些特定的應用,比如直播或網盤(文件上傳下載),帶寬瓶頸也是一個出現頻率較高的場景。
服務端的帶寬分為上行(out)和下行(in)帶寬(分別對應客戶端的下載和上傳)。
-
看視頻看新聞使用帶寬:客戶端的下載、服務端的上行帶寬。
-
服務端接收客戶端的數據使用帶寬:客戶端的上傳、服務端的下行帶寬。
一個 Web 服務器如各類新聞網站通常需要更多的服務端上行(out)帶寬;而郵件服務器、網盤服務器等則通常需要更多的服務端下行帶寬(in)。
理解帶寬速率公式
-
1 Mb/s 帶寬速度為 128 KB/s(1024Kb / 8KB)
-
100 Mb/s 帶寬速度為 12.5 Mb/s(考慮網絡損耗通常按 10M/s 或 1280KB/s 算)
示例:5000 萬像素手機拍一張照片,照片大小約 20MB,在下述帶寬下需要耗時:
-
10M 帶寬約 20 秒:耗時 = 流量 / 速率 = 20MB / (10Mb/8) = 20 / 1.25 = 16 秒(按 1MB/s=128KB/s 速度算即 20 秒)
-
100M 帶寬約 2 秒:耗時 = 流量 / 速率 = 20MB / (100Mb/8) = 20 / 12.5 = 1.6 秒(按 10MB/s=128KB/s 速度算即 2 秒)
-
1000M 帶寬約 0.2 秒:耗時 = 流量 / 速率 = 20MB / (1000Mb/8) = 20 / 125 = 0.16 秒(按 100MB/s=128KB/s 速度算即 0.2 秒)
案例分析
現象:從監控圖表可以看出,當前的網絡流量已經基本將網絡帶寬占滿,因此網絡存在瓶頸。
解決方案:
- 硬件解決:增加帶寬(帶寬便宜)。
- 軟件解決:分析對應業務操作的數據傳送內容是否可精簡;是否可以異步傳送。
網絡連接狀態監控
性能測試中對網絡的監控主要是監控網絡連接狀態的變化和異常
。
-
對於使用 TCP 協議的服務,需要監控服務已建立連接的變化情況(即 ESTABLISHED 狀態的 TCP 連接)。
-
對於 HTTP 協議的服務,需要監控被測服務對應進程的
網絡緩沖區的狀態、TIME_WAIT 狀態的連接數等
。
Linux 自帶的很多命令如 netstat、ss 都支持如上功能。
下圖是 netstat 對指定 pid 進程的監控結果:
完整命令輸出:
數據庫
慢查詢
更具體的慢 SQL 分析優化,可參見《MySQL 慢 SQL & 優化方案》。
如 MySQL 資源出現瓶頸,首先找慢查詢(超過自定義的執行時間閾值的 SQL)。
1)通過 SQL 語句定位到慢查詢日志的所在目錄,然后查看日志。
show variables like "slow%";
2)慢查詢日志在查詢結束以后才紀錄,所以在應用反映執行效率出現問題時,查詢慢查詢日志並不能定位問題。這時可以使用show processlist
命令查看當前 MySQL 正在進行的線程狀態,可以實時地查看 SQL 的執行情況。
示例:
mysql -uroot -p123456 -h127.0.0.1 -p3307 -e "show full processlist" |grep dbname |grep -v NULL
3)找到慢查詢 SQL 后可以用執行計划(explain)進行分析(或反饋給 DBA 和開發處理)。推薦最簡單的排查方式,步驟如下:
- 分析 SQL 是否加載了不必要的字段/數據。
- 分析 SQL 是否命中索引。
- 如果 SQL 很復雜,優化 SQL 結構。
- 如果表數據量太大,考慮分表。
- ……
連接數
數據庫連接池的使用率
-
當數據庫連接池被占滿時,如果有新的 SQL 語句要執行,只能排隊等待,等待連接池中的連接被釋放(等待之前的 SQL 語句執行完成)。
-
如果監控發現數據庫連接池的使用率過高,甚至是經常出現排隊的情況,則需要進行調優。
查看/設置最大連接數
-- 查看最大連接數
mysql> show variables like '%max_connection%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| extra_max_connections | |
| max_connections | 2512 |
+-----------------------+-------+
2 rows in set (0.00 sec)
-- 重新設置最大連接數
set global max_connections=1000;
在/etc/my.cnf 里面設置數據庫的最大連接數
[mysqld]
max_connections = 1000
查看當前連接數
mysql> show status like 'Threads%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 32 |
| Threads_connected | 10 |
| Threads_created | 50 |
| Threads_rejected | 0 |
| Threads_running | 1 |
+-------------------+-------+
5 rows in set (0.00 sec)
-
Threads_connected:表示當前連接數。跟 show processlist 結果相同。准確的來說,Threads_running 代表的是當前並發數。
-
Threads_running:表示激活的連接數。一般遠低於 connected 數值。
-
Threads_created:表示創建過的線程數。
-
如果我們在 MySQL 服務器配置文件中設置了 thread_cache_size,那么當客戶端斷開之后,服務器處理此客戶的線程將會緩存起來以響應下一個客戶而不是銷毀(前提是緩存數未達上限)。
-
如果發現 Threads_created 值過大的話,表明 MySQL 服務器一直在創建線程,這也是比較耗資源,因此可以適當增加配置文件中 thread_cache_size 值。
-
查詢服務器 thread_cache_size 的值
mysql> show variables like 'thread_cache_size';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 100 |
+-------------------+-------+
1 row in set (0.00 sec)
## 鎖
詳見《MySQL 事務和鎖》。
緩存命中率
-
通常,SQL 查詢是從磁盤中的數據庫文件中讀取數據。
-
若當某一個 SQL 查詢語句之前執行過,則該 SQL 語句及查詢結果都會被緩存下來,下次再查詢相同的 SQL 語句時,就會直接從數據庫緩存中讀取。(注意,MySQL 8 開始已廢棄查詢緩存功能。)
監控點
-
業務執行過程中 SQL 查詢時的緩存命中率(查詢語句讀取緩存的次數占總查詢次數的比例)。
-
如果緩存命中率過低,需要優化對應的代碼和 SQL 查詢語句,以提高緩存命中率。
案例分析
測試結果分析
結論:從目前的測試結果來看(如下圖所示),性能存在問題。
現象:並發數達到 50 時的 TPS 為 52,此時雖然響應時間為 4.4s(小於需求的 5s),但是數據庫服務器的 CPU 使用率非常高(接近 100%),因此需要重點關注數據庫的調優分析。
排查過程
-
使用 top 命令觀察,確定是 mysqld 導致還是其他原因。
- CPU 分為用戶 CPU 和內核 CPU。綜合其他的各項資源指標來分析,發現內存、磁盤IO、網絡等指標無任何異常,因此判斷此處不是內核 CPU 占用高,主要原因是用戶進程占用的 CPU 高。
- 確認目前 CPU 占用高的為 mysqld 進程。
-
分析數據庫服務器 CPU 高的可能原因:慢 SQL、SQL 語句過多、連接數過多等。
-
確認是否存在慢 SQL:
- 查看慢查詢日志,看看是否有超過預期指標的 SQL 語句,並分析排查:看看執行計划是否准確、索引是否缺失、數據量是否太大等。
- 目前案例經過慢查詢日志的分析,未存在慢查詢。
-
確認是否 SQL 語句過多或連接數過多:
- 使用
show full processlist
查看當前數據庫中正在執行的 SQL 語句及連接池的狀態,發現大量 SQL 在等待執行。 - 再結合操作過程中的系統日志進行分析,發現每進入一次商城首頁,就需要在數據庫中執行 19 條查詢 SQL。
- 使用
-
解決方案
- 硬件解決:增加 CPU。
- 軟件解決:為減少一次性加載過多 SQL,可考慮使用分批次、異步加載的方式(展示到什么位置,就查詢什么位置的數據)。
JAVA 應用
JVM
JVM(JAVA Virtual Machine):虛擬出來的空間,專門供 JAVA 程序運行。
JAVA 應用運行機制
JVM 體系結構介紹
-
JVM 內存分為三個大區,young 區(年輕代),old 區(年老代)和 perm 區(持久代),其中 young 區又包含三個區:Edgn 區、S0 區(From 區)、S1 區(To 區)。
-
young 區和 old 區屬於 heap(堆)區,占據堆內存;perm 區稱為持久代,不占據堆內存。
-
PermSpace 主要是存放靜態的類信息和方法信息、靜態的方法和變量、final 標注的常量信息等。
JAVA 運行時內存划分
重點關注:堆區(動態變化)
。我們常說的性能調優,指的就是堆中的性能調優。
監控點:因此在測試時,需要關注堆區的空間是否持續上升而沒有下降。
垃圾回收機制
什么是垃圾回收機制
-
垃圾回收指將內存中已申請並使用完成的那部分內存空間回收,供新申請使用。
-
垃圾回收機制都是針對堆區的內存進行的。
監控點
-
內存泄露:一個對象持有一個引用永遠不釋放,導致聲明周期過長,這樣持有的對象對了,內存就不夠用了,這樣就會頻繁 GC。
-
系統在做垃圾回收時,不能夠處理任何用戶業務的。如果垃圾回收過於頻繁,導致系統業務處理能力下降。
-
由於 Full GC 內存比較大,垃圾回收一次時間比較長,那么這段時間內都不能處理業務,對系統影響比較大,因此我們需要關注
Full GC 頻率
。
垃圾回收機制的運行步驟如下:
-
新程序執行時需要先申請內存空間,會先從年輕代中申請。
-
在年輕代滿了以后,就會進行垃圾回收
Young GC(Minor GC)
(所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程序的線程,但這段時間可以忽略不計)。 -
回收時檢查年輕代中的內存,是否還在使用。還在使用的部分會移存到生存區 2 中;不使用的部分則釋放,此時年輕代內存空間被清空。
-
新程序執行申請內存空間,再從年輕代申請。
-
年輕代又滿了,就會進行垃圾回收
Young GC
。還在使用的內存移存到生存區 1 中,並把生存區 2 中的內存也都存到生存區 1 中。此時就會清空年輕代和生存區 2。 -
循環上述 1-5 步。
-
如果部分內存在生存區中存活很久(內存在生存區中移動了 10 次左右),則將這部分內存放入到老年代中。
-
循環上述 1-7 步,直到老年代內存空間全部占滿,此時就要進行垃圾回收
Full GC(Major GC)
(Full Gc 會暫停所有正在執行的線程(Stop The World),來回收內存空間,這個時間需要重點考慮)。
JVM dump
什么是 JVM dump
在故障定位(尤其是 out of memory)和性能分析的時候,經常會用到一些文件來幫助我們排除代碼問題。這些文件記錄了 JVM 運行期間的內存占用、線程執行等情況,這就是我們常說的 dump 文件。
常用的有 heap dump 和 thread dump(也叫 javacore,或 java dump)。我們可以這么理解:heap dump 記錄內存信息的,thread dump 是記錄 CPU 信息的
。
-
當發現應用內存溢出或長時間使用內存很高的情況下,通過內存 dump 進行分析可找到原因。
-
當發現 cpu 使用率很高時,通過線程 dump 定位具體哪個線程在做哪個工作占用了過多的資源。
-
heap dump
-
heap dump 文件是一個二進制文件,指定時刻的 Java 堆棧的快照,是一種鏡像文件,它保存了某一時刻 JVM 堆中對象使用情況。
-
可以通過 Heap Analyzer工具分析 heap dump 文件,哪些對象占用了太多的堆棧空間,來發現導致內存泄露或者可能引起內存泄露的對象。
-
-
thread dump
-
thread dump 文件主要保存的是 java 應用中各線程在某一時刻的運行的位置,即執行到哪一個類的哪一個方法哪一個行上。
-
thread dump 是一個文本文件,打開后可以看到每一個線程的執行棧,以 stack trace 的方式顯示。
-
通過對 thread dump 的分析可以得到應用是否“卡”在某一點上,即在某一點運行的時間太長,如數據庫查詢時長期得不到響應,最終導致系統崩潰。
-
單個的 thread dump 文件一般來說是沒有什么用處的,因為它只是記錄了某一個絕對時間點的情況。比較有用的是,線程在一個時間段內的執行情況。
-
thread dump 文件在分析時特別有效,困為它可以看出在先后兩個時間點上,線程執行的位置。如果發現先后兩組數據中同一線程都執行在同一位置,則說明此處可能有問題,因為程序運行是極快的,如果兩次均在某一點上,說明這一點的耗時是很大的。通過對這兩個文件進行分析,查出原因,進而解決問題。
-
獲取 dump 文件
可以利用 JDK 自帶的工具獲取 thread dump 文件和 heap dump 文件,即 JDK_HOME/bin/ 目錄下的 jmap 和 jstack 這兩個命令。
1)獲取 heap dump 文件
./jmap -dump:format=b,file=heap.hprof 2576
這樣就會在當前目錄下生成 java 應用進程 pid 為 2576 的 heap.hprof 文件,這就是 heap dump 文件。
如果我們只需要將 dump 中存活的對象導出,那么可以使用 :live 參數:
jmap -dump:live,format=b,file=heapLive.hprof 2576
2)獲取 thread dump 文件
./jstack 2576 > thread.txt
這樣會將命令執行結果轉儲到 thread.txt,這就是 thread dump 文件。有了 dump 文件后,我們就能借助性能分析工具獲取 dump 文件中的信息(使用 top -H -p <pid> 找出某進程中要分析的線程 ID,然后將線程 ID 轉換為 16 進制后,在線程 dump 文件中搜索相關信息)。
打開 dump 文件
1)使用 JDK 自帶的 jhat 命令
jhat 是用來分析 java 堆的命令,可以將堆中的對象以 html 的形式顯示出來,包括對象的數量、大小等等,並支持對象查詢語言。
jhat -port 5000 heap.hrof
當服務啟動完成后,我們就可以在瀏覽器中,通過 http://localhost:5000/ 進行訪問,如下所示:
2)使用 eclipse MAT 工具
一般來說,應用程序的 dump 文件都是很大的,jdk 自帶命令難以分析這些大文件。在實際的生產環境下,我們必須要借助第三方工具,才能快速打開這些大文件,進行分析定位。
安裝好 eclipse mat 分析工具后,將 dump 文件導入 eclipse,點擊[Leak Suspects],找到跟公司有關的代碼進行分析。
分析 thread dump 文件
線程 dump 詳解
線程的狀態:
- NEW:未啟動,不會出現在 Dump 中。
- RUNNABLE:在虛擬機中執行的。
- BLOCKED:受阻塞並等待在監視器鎖。
- WAITTING:無限期等待另一個線程執行特定的操作。
- TIMED_WAITTING:有時限的等候另一個線程執行特定的操作。
- TERMINATED:已退出。
監視器:
調用修飾:
- locked <地址> 目標:注意臨界區對象鎖可重入,線程狀態為 RUNNABLE。
- waitting to lock <地址> 目標:還沒有獲得鎖,進入區等待,線程狀態為 BLOCKED
- waitting on <地址> 目標:獲得鎖了,等待區等待,線程狀態為 WAITTING,TIMED_WAITTING。
- parking to wait for <地址> 目標:線程原語,隨 current 包出現,與 synchronized 體系不同。
線程動作:
- runnable:線程狀態為 RUNNABLE。
- in Object.wait():等待區等待,線程狀態為 WAITTING 或 TIMED_WAITTING。
- waitting for monitor entry:進入區等待,線程狀態為 BLOCKED。
- waitting on condition:等待區等待,被 park。
- sleeping:休眠的線程,調用了 Thread.sleep()。
分析線程 dump 的入手點
-
進入區等待:BLOCKED、waitting to lock、waitting for monitor entry,這些詞表名代碼層面已經存在沖突。
-
持續進行的 IO:一般來說被捕捉到的 runnable 的 IO 調用都是有問題的,如 runnable 中有 JDBC 鏈接的代碼。
-
非線程調度的等待區等待:in Object.wait()(情況 1 可能會導致這個情況,造成大量線程堆積)。
-
“死鎖”問題的解決辦法
-
在最可能死鎖的時間點制作 dump。
-
找出引起大量線程阻塞的線程。
-
找出該線程阻塞的原因。
-
閱讀代碼,遍歷其他阻塞或等待的線程,以及它之前的調用是否會造成這個線程的等待。
-
-
注意:排除 GC 干擾,Full GC 時所有線程都會被阻塞住。
- 查看線程 dump 時,首先查看內存使用情況。
- 使用命令“-verbose:gc”,觀察是否有 Full GC 字樣。
分析 heap dump 文件
什么情況下需要分析堆 Dump
內存不足、GC 異常、懷疑代碼內存泄漏,這時需要制作堆 Dump,找出生命周期的錯誤關聯對象以及相關代碼。
JVM 內存模型
- 年輕代(Young Generation,包括Eden space、From space、To space)
- 年老代(Old Generation)
- 永久代(PermGen space)
兩種 GC
- YoungGen GC:Minor GC
- Full GC:Major GC
常見錯誤
- out of MemoryError:GC overhead limit exceed:回收時間占系統運行時間的 98% 以上,極有可能是內存泄漏導致的。
案例分析:JVM 堆內存溢出
JVM 堆內存回收詳細過程圖解:從下圖可以很清晰的看到,old 區空間占滿后會進行一次 FGC(稱為全量 GC),FGC 回收后如果 old 區空間還是不能容納新生成對象,那么便會產生 java 堆內存溢出[JAVA HEAP OOM]。
性能問題發現過程:
查看服務器上報錯日志,發現有如下報錯信息[java.lang.OutOfMemoryError: Java heap space];根據報錯信息確定是 jvm 堆內存空間不夠導致,於是使用 jvm 命令查看(下圖所示),發現此時 old 區內存空間已經被占滿了。
同時使用 jvisualvm 監控工具也發現 old 區空間被占滿(如下圖所示,單位為百分比),整個 heap 區空間已經無法再容納新對象進入。
建議:考慮大量數據一次性寫入內存場景。
案例分析:持久代內存溢出
PermSpace 主要是存放靜態的類信息和方法信息、靜態的方法和變量、final 標注的常量信息等。
現象:
壓測某系統接口,壓測前1分鍾左右 TPS 400 多,之后 TPS 直降為零,后台報錯日志:java.lang.OutOfMemoryError:PermGenspace,通過 jvm 監控工具查看持久代(perm區)空間被占滿,而 Old 區空閑。
問題定位:
通過注釋代碼塊定位問題,考慮到 perm 區溢出大部分跟類對象大量創建有關,故鎖定問題在序列化框架使用可能有問題。
-
獲取 JVM dump 文件。
-
安裝 eclipse mat 分析工具。
-
將 dump 文件導入 eclipse,點擊[Leak Suspects],找到跟公司有關的代碼進行分析。
解決方案:
跟開發溝通后選擇去掉 msgpack0.6 版本框架,采用 java 原生序列化框架。修改后系統 tps 穩定在 400 多,gc 情況正常。
-
修復前:
-
修復后:
類似問題如何避免:
- 去掉項目無用 jar 包。
- 避免大量使用類對象、大量使用反射。
案例分析:頻繁 FGC
現象:系統某接口頻繁 FGC。
問題排查及解決方案:
先查 JVM 內存信息找可疑對象,命令為:jmap -histo
從內存對象實例信息中發現跟 mysql 連接有關,然后檢測 mysql 配置信息:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
發現系統采用的是 spring 框架的數據源,沒有用連接池。
使用連接池的好處:連接復用,減少連接重復建立和銷毀造成的大量資源消耗。
然后換做 hikaricp 連接池做對比測試:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
壓測半小時未出現 fgc,問題得到解決。
類似問題如何避免:
- 研發規范統一 DB 連接池,避免研發誤用。
- 減少大對象、臨時對象使用。
案例分析:減少 mirror GC
現象:
假設,現在有億級流量電商的搶購活動,活躍用戶為 500 萬,付費轉化率為 10%。活躍時間在搶購的前幾分鍾,假設每秒產生 1000 單,而每台 Tomcat 的最高並發支持數為 500。現有三台服務器,均為 4 核 8g,每台服務器均部署 Tomcat,使用 nginx 做負載均衡。
有 300 單落在服務器 1 上,每單所在堆空間大小為 1Kb,每秒大約產生 300Kb 的堆對象。可以使用 lucene 來動態計算 javabean 所在堆空間的大小。下單還會產生其他對象,比如優惠券、庫存、積分等,此時放大 20 倍,也就是每秒產生 6000Kb 的對象。假設還會有訂單查詢的操作,此時再放大 10 倍,也就是每秒產生約 58MB 的對象。
此時,堆初始值大小和最大值大小均為 3072MB,老年代大小為 2048MB,新生代大小為 1024MB,Eden 區大小為 819MB,s0 和 s1 區大小均為 102MB。819Mb / 58Mb = 14 秒,即大約 14 秒 Eden 區爆滿,觸發 mirror Gc,此時停止應用程序的線程。
優化:
因而,此時需要調整 JVM 的配置參數:老年代大小為 1024MB,新生代大小為 2048MB,Eden 區大小為 1638MB,s0 和 s1 區大小均為 204MB。1638Mb/ 58Mb = 28秒,這樣能減少 mirror Gc,從而達到優化的效果。但更多的優化可根據實際線上 jvm 運行情況來看。
框架使用不當
案例分析:錯誤使用框架提供的 API
某系統本身業務邏輯處理能力很快(研發本機自測 tps 可以到達 2w 多),但是接入到 framework 框架后,TPS 最高只能到達 300 左右,而且系統負載很低。
問題排查:
這種現象說明系統可能是堵在了某塊方法上,根據這種情況一般采用線程 dump 的方式來查看系統具體哪些線程出現異常情況。
通過線程 dump 發現 [TIMED_WAITING]狀態的業務線程占比很高。
# 線程的狀態
* NEW:未啟動,不會出現在 Dump 中。
* RUNNABLE:在虛擬機中執行的。
* BLOCKED:受阻塞並等待在監視器鎖。
* WAITTING:無限期等待另一個線程執行特定的操作。
* TIMED_WAITTING:有時限的等候另一個線程執行特定的操作。
* TERMINATED:已退出。
根據線程 dump 信息,找到公司包名開頭的信息,然后從下往上查看線程 dump 信息,從信息中我們可以看到:
- framework.servlet.fServlet.doPost:框架 api 封裝了 servletdopost 方法做了某些操作。
- framework.servlet.fServlet.execute:框架 api 執行 servelt。
- framework.process.fProcessor.process:框架 api 進行自身邏輯處理。
- framework.filter.impl.AuthFilter.before:框架使用過濾器進行用戶權限過濾
- 。。。然后就是進行 http 請求操作。
由此判斷,就是在框架進行權限校驗這塊堵住了。之后跟開發溝通這塊的問題即可。
問題原因:
性能測試是驗證 A 系統的處理能力,但是在壓測程序里,A 系統卻調用了權限校驗系統,由於權限校驗系統處理能力只有 300 左右,從而拖慢了整個系統處理能力。
因此,需要在壓測過程中關閉對權限校驗系統調用,只壓 A 系統,這樣才能壓測出 A 真實的處理能力。
解決方案
去掉對 B 系統調用,即去掉權限校驗。
@Api(auth=true) 改為 @Api(auth=false)
案例分析:日志框架使用不當
某系統添加 LOGBACK 日志框架輸出日志(日志級別為 INFO)后,TPS 從 1000 降到 200 多:
從 JVISUALVM 工具看到有大量業務線程處於 BLOCKED 狀態:
優化方案:
日志降級、將日志級別改為 warn,減少日志輸出量。
后續建議:
-
合理設置日志級別、精簡日志輸出。
-
合理設置日志刷盤方式,同步 or 異步。
-
對於 DEBUG、INFO 日志打印、需要先判斷日志級別:
if(LOGGER.isDebugEnabled()){do log}
。
OS 內存溢出
問題現象:
某系統線上故障,系統假死,無法提供服務,服務器 ssh 無法登錄。
問題根因:
系統使用堆外內存,操作系統內核占用 cache 內存,當 cache 內存占滿后,無法釋放,導致物理內存 OOM(Out Of Memory)。
為什么會 OOM?
為什么會沒有內存了呢?原因不外乎有兩點:
-
分配的少了:比如虛擬機本身可使用的內存(一般通過啟動時的 JVM 參數指定)太少。
-
應用用的太多,並且用完沒釋放,浪費了。此時就會造成內存泄露或者內存溢出。
優化方案:
通過優化 linux 操作系統內核參數:min_free_kbytes