跳至內容
返回

後端仔的效能優化實戰(1)、觀測問題

發佈於:  at  08:00 上午

作為一個後端仔,除了新系統的開發、設計能力之外,擁有發現問題的能力也是必備技能之一。特別是在現在AI甚囂塵上的環境中,已經有無數的人都把問題交付給AI,企圖讓AI掌控全局。這件事我既無權也無能力批判對錯,但遠比是對是錯更重要的,是我們把決策權都給了AI,而忽略了自身能力的培養,這無疑是相當致命的。之所以致命,並不是他會帶來甚麼當下的危害。讓子彈飛一會兒,把本業給外包出去的後果往往不會在當下體現,好比美國在40年過後的現在,才發覺因國際化的工廠外移導致了本土生產力貧弱不堪,因此大幅鼓吹讓美企回流本國。AI也是同樣概念,七巨頭不停卷算力,但最後還是會卡在硬體資源,得看晶片半導體的臉色。

話說多了,總之我想強調的是,個人能力的培養依然是不可忽視的。我此次就是要透過AI再搭配實際測試,學習並驗證AI知識。

首先,在效能實戰方面,在我們一股腦要開始寫code優化之前,首先我們需要談談 什麼是效能低落? 現行的觀點,效能低落的指標通常有:

以下我們將逐個說明,讓讀者有個大概的印象。

CPU占用

CPU問題常見的成因有:大量的計算密集作業(如:加解密、Context Switch等等)、JVM 頻繁 GCSerialize 開銷、不當宣告執行緒、無窮迴圈等等。

Memory占用

講完CPU,接著就是Memory。實務上,很多時候GCMemory是綁在一起的——Memory管不好,GC就會頻繁被觸發,頻繁觸發的下場就是Stop The World(STW)經常出現, 此時服務停止但用戶繼續使用服務,新的用戶也不斷進來。在Thread不斷被建立的情境下,不同Thread搶占CPU資源使得CPU使用率高漲,每個Thread又占用Memory資源,最後讓CPUMemory都被拉進一趟渾水。所以Memory占用高,通常比CPU占用高更值得警惕,因為它通常會有連鎖反應。

其他常見的成因有:Java Heap配置問題、Memory Leak、IO直接讀取整個檔案、連線池沒有設定TTL等等。

GPU占用

相較於CPUMemoryGPU占用對傳統後端服務來說稍微冷門了些──除非你的服務有涉及AI推論(Inference)、影像/影片處理、或是機器學習相關的Batch Job,否則平常根本不會特別去監控這個指標。但現在AI服務滿天飛,模型推論、Embedding運算、影像辨識這類需求越來越常出現在後端架構裡,GPU占用也就不能不管。

GPU占用的常見成因有:Batch Size設定不當,Batch Size你可以理解為AI Model一次可以處理的資料量,設定得太小就會沒發揮GPU平行運算優勢,設定得太大又容易爆VRAM, 再來是模型本身沒做優化(如沒有量化Quantization、沒用TensorRT等推論加速框架)、多個Process搶用同一張GPU(沒做好資源隔離或排程),等等問題。

這部分你看過就好,反正我不是非常熟悉這部分,之後也不會講。

I/O Wait(硬碟讀寫延遲)

I/O Wait指的是CPU在等待磁碟(或其他I/O裝置)完成資料讀寫時,所產生的閒置時間。在這邊我先打個預防針提醒一下避免有人不知道,基本上資料讀取的快慢會根據存放位置有所不同,速度從高到低會是 暫存器快取記憶體(L1、L2)主記憶體硬碟,當中硬碟讀寫速度是最慢的。

I/O Wait的概念像是外部資源,他不會直接造成CPUMemory的負擔,但是 CPU/Memory都必須要等待 I/O Wait,所以他依然會造成系統反應速度下降與間接造成CPUMemory無法釋放資源。所以如果你看到CPU使用率不高、但系統反應卻很慢,I/O Wait通常是第一個該懷疑的對象。

常見成因有:磁碟讀寫速度過慢(像是傳統HDD)、大量的同步讀寫操作、Log寫入過於頻繁且沒有Buffer機制、資料庫查詢時沒有索引導致Full Table Scan等等。

值得一提的是,I/O Wait跟前面提到的MemoryGC問題常常互相牽連——Memory不夠導致SwapSwap又製造出大量I/OI/O延遲又拖慢整體回應時間,形成一個惡性循環。這也是為什麼觀測系統時,不能只看單一指標,而要把這些指標放在一起交叉比對,才能找到真正的根因。

Network Latency(網路延遲)

Network Latency指的是一個請求從發送到收到回應之間,所經過的時間。跟I/O Wait類似的地方在於,它同樣是一種「等待外部資源」的延遲,只是這次等的不是磁碟,而是網路的另一端——可能是資料庫、快取伺服器、第三方API,或是微服務架構下的其他Service

常見成因有:DNS解析緩慢、跨區域、跨國的網路請求、TCP三向交握(Three-way Handshake)與TLS Handshake的額外開銷、下游服務本身回應緩慢(把延遲轉嫁給了你)、微服務架構下呼叫鏈過長(一個Request要經過好幾層Service才能拿到結果,每一層都在疊加延遲)、Connection Pool不足導致Request必須排隊等待可用連線。

Thread / Connection Pool 使用率

Thread PoolConnection Pool的概念是──「先準備好一批可重複使用的資源,避免每次都重新建立」。差別在於Thread Pool管理的是執行緒,Connection Pool管理的是連線(資料庫連線、HTTP連線等)。

這兩個指標是徵兆,當CPU/Memory使用率暴漲,需要找出瓶頸時,可以往這些方向盤查的徵兆。假如Thread Pool或是Connection Pool炸了,接下來就是炸CPUMemory,然後最後就會炸到你,可喜可賀。

當你看到Thread Pool使用率長期貼著上限。這背後的原因可能是:某個Blocking呼叫卡住了Thread不放(例如同步呼叫外部API又沒設Timeout)、資料庫查詢太慢導致Thread遲遲無法釋放、或是單純Thread Pool Size設定得太保守,明明硬體資源夠用卻沒開放足夠的執行緒。

Connection Pool則常見於資料庫連線或HTTP Client。如果Connection Pool的使用率長期偏高、甚至常常出現「等不到可用連線」的Timeout錯誤,常見成因有:連線用完忘記歸還(尤其是例外發生時,沒有用try-with-resourcesfinally正確關閉/釋放連線)、Pool Size設定過小、單一Request佔用連線的時間過長(例如一個Transaction裡包了太多不必要的邏輯,遲遲不Commit釋放連線)。

GC(Garbage Collection)頻率與耗時

GC的概念是——負責回收沒人用的物件、釋放Memory空間,讓Heap不會無限膨脹。但GC不是沒有代價,尤其是頻率跟耗時這兩件事,一旦失控,就會造成CPU/Memory的雙重問題,通常GC指標要配合CPU/Memory一起看。

如果GC三不五時就被觸發,代表物件產生的速度太快。這通常意味著程式碼裡有大量「用完即丟」的暫時性物件被建立(常見於前面提過的序列化、字串拼接、Stream操作產生的中間物件),或是Heap配置得太小,撐不住正常流量下的物件產生速度。

如果單次GC執行時間拉長,或是出現Full GC(針對Old Generation做的完整回收),服務就會出現明顯的延遲尖峰,甚至讓Health Check誤判服務掛掉。造成Full GC耗時拉長的常見原因有:Old Generation裡塞了太多長期存活的物件(可能是真正的Memory Leak,也可能是Cache沒設淘汰機制)、Heap設得太大導致單次掃描範圍過廣、GC演算法選得不適合(例如高流量低延遲場景卻用了不擅長處理大Heap的演算法)。

GC這個指標比較特別的地方在於,它幾乎是CPUMemory問題的交叉點——GC頻繁通常伴隨CPU使用率升高,GC耗時過長則常常對應Memory配置或物件生命週期管理的問題。所以觀測GC,某種程度上就是在同時觀測CPUMemory這兩件事的健康狀況。

Throughput(吞吐量)

Throughput指的是系統在單位時間內,能夠處理的請求數量(常見單位如TPSQPS——Transactions/Queries Per Second)。通常要評估一個系統的整體效能、就會使用Throughput來看,使用場景經常是在壓力測試或是高流量的場景下,觀測Throughput的變化。

如何提升Throughput常常是一個整體系統的問題,實際上它牽涉到前面所有指標,算是一個集大成的判斷基準。

Response Time(回應時間,含 P50 / P95 / P99)

Response Time指的是從發出請求到收到回應,總共花費的時間。跟Throughput一樣,它也是一個綜合指標,但兩者看的角度不同——Throughput回答的是「系統整體能撐多少請求」,Response Time回答的是「使用者實際感受到的等待時間」。

這邊要特別提一下P50P95P99這幾個名詞,因為只看平均值(Average)很容易被誤導,所以我們還必須看:

在真實場景光盯著平均值是不夠的,P99(甚至P99.9)才是抓出系統長尾問題的關鍵。前面的任何一個環節出狀況,最終都會反映在Response Time上升這件事上。

現在你知道我們怎麼評估效能低落的指標了,指標很多,且各問題經常是連鎖反應,有時進行了改善也絕非當下就見效,所以在修改之前,更重要的是辨識問題的手段,足夠的觀測與評估才能避免你做白工。所以下一篇,我們就要接著提到我們怎麼去評估與監控這些數值。


建議修改
在以下平台分享此文章:

上一篇
後端仔的效能優化實戰(2)、Spring Boot Actuator
下一篇
Buffer Pool 與 MySQL Query Cache