效能調校 FAQ

綜合 更新於 Oct 9, 2026

一份實用、不綁語言的筆記:怎麼找出並修掉效能問題,最後再附上 Java 專屬的技巧。

總覽

黃金守則

  • **要量測,不要猜。**先做 profiling;人對瓶頸的直覺通常是錯的。
  • **只優化熱路徑。**90% 的時間花在一小部分程式碼上(Amdahl 定律)。
  • 先修最大的瓶頸,然後重新量測 —— 瓶頸會搬家。
  • 設一個目標。「夠快」是一個數字(例如 p99 < 200ms),不是一種感覺。
  • 小心微優化:犧牲可讀性換來微不足道的收益。

什麼時候才該調校

等你有了 (1) 可重現的工作負載、(2) 一個指標、(3) 一條基準線之後再說。


1) Profiling 的做法

一個可以反覆執行的循環:

text
1. Reproduce   → capture a realistic, repeatable workload
2. Measure     → get a baseline (latency, throughput, CPU, memory)
3. Locate      → profile to find the hot spot
4. Fix         → change ONE thing
5. Re-measure  → confirm improvement, watch for regressions elsewhere
6. Repeat      → until target is met

Profiling 的種類

種類 回答什麼問題 工具(舉例)
CPU/取樣 時間花在哪? perf、async-profiler、py-spy
配置(allocation) 是誰在製造垃圾? JFR、記憶體 profiler
牆鐘時間/追蹤 一個請求在哪裡等? 分散式追蹤、span
Heap dump 是誰抓著記憶體不放? heap 分析工具

延遲的百分位數

永遠不要只看平均值。要追蹤 p50 / p95 / p99 / p99.9。 使用者感受到的慢、以及 SLA 被打破,通常都是尾端延遲(p99)造成的。


2) 常見的瓶頸

CPU 密集

  • 症狀:CPU 使用率高、等待時間低。
  • 成因:演算法沒效率(大 n 上的 O(n^2))、過多的序列化/正規表示式、 緊湊迴圈、重複計算。
  • 解法:換更好的演算法/資料結構、memoization/快取、把工作批次化、 平行化(只有在核心還閒著時才有用)。

記憶體密集

  • 症狀:GC 時間長、開始換頁(swap)、OOM。
  • 成因:洩漏(沒有上限的快取或集合)、龐大的物件圖、裝箱。
  • 解法:快取設上限、用串流取代把全部載進記憶體、重複使用緩衝區、 修掉洩漏(不小心讓物件一直可達)。

I/O 密集(磁碟/檔案)

  • 症狀:CPU 低、等待時間高。
  • 解法:讀寫加緩衝、順序存取優於隨機存取、加大批次、 非同步/非阻塞 I/O、用壓縮拿 CPU 換 I/O。

資料庫密集

  • 通常是現實世界裡的第一名瓶頸。見第 4 節。

網路密集

  • 症狀:延遲主要由來回次數決定。
  • 成因:話太多的 API(N+1 次呼叫)、payload 太大、沒有重用連線。
  • 解法:把請求批次化、減少來回、連線池/keep-alive、 壓縮、把運算搬到離資料近的地方、靜態資源用 CDN。

3) 快取

快取是拿記憶體與資料新鮮度去換速度。等你確定讀取佔多數、而且資料會被重複用到, 再加它。

層次(用戶端 → 伺服器)

  • 瀏覽器/用戶端快取
  • CDN/邊緣快取(靜態資源與可快取的回應)
  • 應用程式的記憶體內快取(最快,但每個實例各一份)
  • 分散式快取(各實例共用)
  • 資料庫的緩衝區/查詢快取

幾個關鍵決定

  • 淘汰策略:LRU(最常見)、LFU、以 TTL 為準。
  • 失效:真正難的部分。做法有:TTL 過期、write-through、 write-behind、寫入時明確失效。
  • Cache-aside 模式(最常見):
    text
    read: check cache → miss → read DB → populate cache → return
    write: write DB → invalidate/update cache
    

常見陷阱

  • 快取擊穿/驚群:熱門 key 一過期,大量 miss 同時打到資料庫。 用鎖、請求合併,或把 TTL 加上抖動來緩解。
  • 資料過舊:明確決定你能容忍多舊的資料。

4) 資料庫查詢與索引調校

找出慢查詢

  • 用資料庫的 EXPLAIN / EXPLAIN ANALYZE 看查詢計畫。
  • 特別留意那些本來該走索引、卻變成全表掃描的地方。

索引

  • 在 WHERE、JOIN、ORDER BY 用到的欄位上建索引。
  • 複合索引的欄位順序很重要 —— 最左前綴原則。
  • 覆蓋索引:把所有要 select 的欄位都包進去,資料庫就完全不用回表。
  • 取捨:索引讓讀變快,但讓寫變慢、也佔空間。不要建過頭。

常見修法

問題 修法
N+1 查詢 批次/join/eager fetch
SELECT * 只選需要的欄位
缺索引 建合適的索引
offset 很大的分頁 改用 keyset(seek)分頁
鎖競爭 縮短交易、選對隔離級別
反覆的重量級讀取 快取、讀取複本
  • 用連線池 —— 建立連線很貴。
  • 讀取為主的分析場景,可以反正規化或預先算好(materialized view)。

5) JVM 調校基礎(Java)

程式碼和資料庫都調完了,才輪到 JVM 旗標。

Heap

  • -Xms / -Xmx:設定初始與最大 heap;伺服器上把兩者設成一樣, 避免調整大小造成的停頓。
  • 太小 → GC 太頻繁或 OOM。太大 → 停頓變長、白白佔記憶體。

垃圾收集器

  • G1(現代 JDK 的預設):均衡、以 region 為單位,通用的好選擇。
  • ZGC / Shenandoah:低停頓收集器,適合大 heap 或對延遲敏感的應用。
  • 目標:把 GC 停頓時間與 GC 佔用比例壓到最小。

先觀察再調

  • 打開 GC log,並使用 JDK Flight Recorder(JFR) 與 Mission Control。
  • 要看的數字:配置速率、停頓時間、晉升到老年代的量、full GC 之後的 heap 大小。

Java 程式碼層級的技巧

  • 迴圈裡串字串用 StringBuilder,不要用 +。
  • 集合先開好大小(new ArrayList<>(expectedSize)),避免擴容。
  • 熱迴圈裡避免自動裝箱(用 primitive 或 primitive stream)。
  • 重複使用建立成本高、且執行緒安全的物件:Pattern、DateTimeFormatter。
  • 優先用串流/延遲處理,而不是把一大坨集合都實體化出來。

6) 熱路徑的 Big-O

演算法上最大的一筆收益,通常來自降低複雜度等級:

從 到 怎麼做
O(n^2) 巢狀掃描 O(n) 用 hash map 查找取代內層迴圈
O(n) 的反覆查找 O(1) 用 Set/Map 取代 List
O(n log n) 重複排序 攤銷 O(1) 讓資料保持有序/用 heap
反覆重算 每次 O(1) Memoize/快取

熱路徑的檢查清單:

  1. 輸入大小 n 是多少?它會怎麼成長?
  2. 目前的複雜度是多少?有沒有不必要的巢狀迭代?
  3. 用對資料結構了嗎(查找用 hash、top-k 用 heap……)?
  4. 有沒有重複做的工作可以快取或預先算好?
  5. 能不能改成增量計算,而不是每次從頭來過?

經驗法則:在大輸入上把 O(n^2) 改成 O(n),勝過任何程度的常數項 JVM 調校。