架構優化:評估水平擴展與快取機制對系統吞吐量的影響
前言
🔗當系統流量成長時,該選擇加機器還是加快取,為了評估哪個比較有效益,試著建立了一個 Scaling Benchmark Project,透過限制硬體來模擬測試單體式架構如何有效且更低成本的提升系統吞吐量。
目標
🔗本專案實作了 CPU 密集型與 DB 密集型兩種 API,用以模擬不同維度的效能瓶頸(運算慢與查詢慢)。測試流程採用 k6 進行壓測,並結合 cAdvisor + Prometheus + Grafana 的監控體系,透過 Grafana 儀表板 (ID: 14282) 精準觀測每個容器的 CPU 耗用。
雖然一開始尋找基準線的測試結果顯示資料庫連線池設定為主要瓶頸,且能進一步透過 postgres-exporter 深度分析,但為了簡少變數並確保模擬環境的穩定性,目前實作先維持資料庫預設連線設定,專注於分析基礎效能加上快取與水平擴展的觀測與驗證。
伺服器規格
🔗處理器:4 cores
RAM:8G
硬碟空間:100GB
作業系統 Ubuntu Server 22.04 LTS
建議務必使用linux系統,一開始我是在windows使用Docker,但可能是跨一層的關係,grafana一直讀取不到正確的容器名稱。
使用技術
🔗- 應用層:Node.js + Express
- 資料庫:PostgreSQL(預填 10,000 筆使用者資料)
- 快取層:Redis
- 負載測試:k6
- 監控:Prometheus + Grafana + cAdvisor + InfluxDB
容器用途說明
🔗請參考專案檔 docker-compose.yml
app (server.js)
🔗透過 depends_on 與 healthcheck 確保 postgres 與 redis 就緒後才啟動。
負載模擬端點:
- CPU 密集型 (
/api/api-intensive):
- 行為:執行高運算量任務(如複雜計算),不涉及資料庫或快取
- 測試目的:模擬運算瓶頸,驗證水平擴展 (Scaling) 的效果。
- 資料庫密集型 (
/api/db-intensive):
- 行為:隨機生成 Key (1-100) 判斷是否有快取 (可依照壓測時長進行調整),若無則執行複雜資料庫運算。
- 快取策略:
- Redis 檢查:先確認快取是否存在。
- Cache Miss:執行複雜 JOIN 查詢,並回寫 Redis (TTL 60s)。
- Cache Hit:直接由 Redis 回傳數據。
- 測試目的:模擬資料庫運算瓶頸,並驗證 Redis 快取 在高併發下保護資料庫的效果。
資料存儲與緩存 (Storage & Cache)
🔗- PostgreSQL:資料庫主體,負責持久化資料與執行複雜查詢。
- Redis:快取層,減輕資料庫讀取壓力。
壓測工具 (Testing Tool)
🔗K6
🔗- 流量分佈:模擬真實流量,包含 50% CPU 密集型與 50% 資料庫密集型請求。
- 數據流向:壓測結果即時寫入 InfluxDB,供 Grafana 進行時序分析。
測試腳本路徑:load-tests/baseline-test.js
監控體系 (Monitoring Stack)
🔗| 組件 | 監控對象 | 說明 |
|---|---|---|
| Prometheus | 全系統指標 | 抓取並存儲各類 Exporter 的時序數據 (Metrics)。 |
| cAdvisor | 容器資源 | 收集所有容器的 CPU、記憶體、網路、磁碟 IO 使用量。 |
| Postgres-exporter | DB 運行狀態 | 專門抓取 PostgreSQL 內部數據(如 Slow Query、連線數)。 |
| InfluxDB | 壓測數據 | 專門儲存 K6 的測試指標(如 RPS, Latency)。 |
| Grafana | 可視化面板 | 將上述數據整合,繪製成圖表以便判斷系統瓶頸。 |
實際數據流程圖
[ 壓力測試層 ] [ 應用與資料庫層 ] +-------------+ +------------------+ +------------+ | k6 | --------> | App (xN) | -------> | Postgres | | (Load) | | + Redis | | (DB) | +-------------+ +------------------+ +------------+ | | | (Push) | | (Stats) | (Exporter) v v v +-------------+ +------------------+ +-------------------+ | InfluxDB | | cAdvisor | | Postgres-exporter | | (壓測指標) | | (容器資源) | | (DB 運行狀態) | +-------------+ +------------------+ +-------------------+ | | | | +------------+--------------+ | | | | (Pull) | v | +-------------------+ | | Prometheus | | | (系統指標) | +------------------------------+-------------------+ | | (Data Source) v +-----------------------+ | Grafana | | (可視化儀表板) | +-----------------------+
測試模式調整
🔗本專案透過調整 docker-compose.yml 中的環境變數與擴展參數來切換測試模式:
| 實驗場景 | USE_REDIS | replicas | 測試核心目標 |
|---|---|---|---|
| Baseline | false | 1 | 建立效能基準線,找出原始瓶頸。 |
| Cache Only | true | 1 | 評估 Redis 對 DB 密集型請求的優化率。 |
| Full Scale | true | 3 | 測試在高併發下,快取與多實例的協同效應。 |
測試流程
🔗啟動各個服務:postgres、redis、app、influxdb、prometheus、postgres-exporter、cadvisor、grafana。
- 啟動系統+監控面板
- 觀察監控面板,確認Grafana有抓取到各容器資料
- 執行K6測試
- 查看數據🎉
基本上都是直接在Grafana觀測,但若發現無數據等問題,則是要從源頭確認cAdvisor是否有資料,再看是否有寫進Prometheus,與資料格式是否符合Grafana的dashboard格式,此專案測試時使用較熱門的樣板ID:14282,監測各容器的CPU效能狀態,實際選擇可依照情況需求做調整。
建議觀察指標
🔗並不是專業測試😅,所以就提供幾個我認為比較重要的數據。
| 指標類別 | 具體指標 | 意義 |
|---|---|---|
| 吞吐量 | Requests/sec | 每秒處理請求數 |
| 延遲 | P50, P95, P99 | 回應時間百分位 |
| 錯誤率 | Error Rate | 失敗請求比例 |
| 資源使用 | CPU、Memory | 系統資源消耗 |
| 資料庫 | Query Time、Connections | 資料庫負載 |
| 快取 | Hit Rate | 快取命中率 |
如果只看 k6 的報告(外部觀測),而不結合 cAdvisor/Prometheus 的資源數據(內部觀測),你很難區分延遲的增加是因為「代碼效率低」、「資源被限制(Throttling)」還是「排隊等待(Queueing)」。
一個完整的效能評估應該將 「k6 外部指標」 與 「系統內部指標」 進行交叉驗證:
| 觀察對象 | 監控工具 | 關鍵指標 | 目的 |
|---|---|---|---|
| 外部用戶感官 | k6 | "RPS, P95 Latency" | 確認系統「表現」是否達標。 |
| 應用程式環境 | cAdvisor | CPU Usage / Throttle | 確認是否觸發了 Docker 的資源限制。 |
| 資料庫狀態 | PG Exporter | Active Connections | 確認是否卡在「連線數上限」。 |
| 快取效率 | Redis | Cache Hit Rate | 確認快取策略是否有效減少了 DB 負擔。 |
測試結果
🔗本測試模擬 100 名虛擬用戶(VUs) 並行,針對 API 密集型(純運算) 與 DB 密集型(涉及資料庫讀取) 兩種路徑進行負載測試。
| 測試階段 | 總吞吐量 (Throughput) | P(95) 延遲 | [API] < 200ms 達標率 | [DB] < 200ms 達標率 | 資源狀況 |
|---|---|---|---|---|---|
| 1. 基礎基準 (Baseline) | 155.54 req/s | 1328.01 ms | 97.04% | 2.32% | DB CPU 45%, App CPU 40% |
| 2. 導入快取 (Cache) | 174.94 req/s | 832.00 ms | 11.52% | 3.96% | DB CPU 降至 4%, App CPU ~47% |
| 3. 水平擴展 (Scaling) | 456.40 req/s | 384.34 ms | 63.73% | 39.61% | 3 個實例分擔負載 |
效能分析
🔗Tes1 識別資料庫瓶頸(Baseline)
🔗- 純運算 API 表現優異(97% 達標)。
- 涉及資料庫的請求達標率極低(2.32%)。
- P(95) 延遲高達 1.3 秒。
- 結論:系統主要瓶頸在於 資料庫 I/O 或查詢效率。
Tes2 快取導入與資源競爭副作用(Cache)
🔗- 成效
- 導入 Redis 後,Postgres CPU 使用率降至 4%。
- 證實快取有效減輕資料庫負擔。
- 非預期衰退
- 不需讀取快取的 API-intensive 任務達標率:從 97% 暴跌至 11.52%。
- 原因分析
- 瓶頸從 DB 轉移至 App 實例 雖然資料庫壓力消失了,但 App 必須額外承擔 Redis 通訊、邏輯判斷與字串處理。在高併發下,這些微小的操作累積成了新的瓶頸。
- 事件循環阻塞 (Event Loop Blocking) Node.js 採用單執行緒模型。當 db-intensive 任務頻繁進行快取檢查與結果判斷時,會佔用大量的主執行緒時間。這導致 api-intensive(純計算任務)即便不需要 Redis,也必須在任務隊列中排隊,造成嚴重的 「排隊延遲(Queueing Delay)」。
Test3 水平擴展解除阻塞(Scaling)
🔗- App 擴展至 3 個實例,解除單機執行緒飽和問題。
- 吞吐量提升至 456 req/s(較初始提升 194%)。
- DB-intensive 達標率:從 2.32% 躍升至 39.61%。
在解除 App 層阻塞後,快取的效益才得以完全釋放。
💡 關於測試指標的補充說明
🔗在本次的 Benchmark 過程中,雖然部署了完整的監控體系,但目前實作階段尚未深度整合 Postgres-exporter 的內部細節指標(如鎖定狀態、實際 Buffer Cache 命中率等)。主要以K6輸出報告與Grafana Dashboard ID:14282監測的基本容器數據做分析。
結論
🔗這次的模擬優化讓我很清楚地體會到,效能調校從來都不是靠單一解法就能搞定的事,而是一個「此消彼長」的平衡問題。
一開始導入快取,確實能很快解掉資料庫的瓶頸;但當流量持續拉高,壓力馬上就轉移到 App 本身,像是 CPU 使用率、執行緒數量、連線管理等問題就開始浮現。最後真正能撐住高併發的做法,反而是把 快取機制搭配水平擴展一起用,才能讓吞吐量接近線性成長,同時把整體延遲壓下來。
做這個專案的主要目的,其實是想練習用「數據」來做技術決策,而不是一遇到效能問題就先堆技術、避免 over-engineering。瓶頸在哪,就針對那裡下手才有效。例如在 CPU 密集型的場景下,盲目加 Redis 幾乎沒什麼幫助,這時候直接 scale out 才是比較實際的解法;反過來說,當問題明顯出在資料庫查詢時,引入快取層帶來的 ROI,往往比砸錢升級硬體高得多。
實際動手做、實際量測之後,也更能理解真實系統環境有多複雜。除了效能數字本身,還要考慮像是快取穿透、資料一致性、Load Balancer 的分配策略等問題,這些在理論或單純看架構圖時其實很容易被忽略。
回頭看,這個專案最大的收穫,並不是「用了 Redis」或「做了水平擴展」,而是建立了一套以問題為核心的決策流程。每一個技術選擇,都應該對應到一個能被量化、被驗證的瓶頸;否則就算技術本身很成熟,也可能只是替系統增加複雜度而已。
在測試過程中可以很明顯看到,無論是加快取還是擴機器,系統壓力其實不會消失,只是從 DB 轉移到 App 的 CPU、執行緒或連線管理層。這也讓我更確定,效能優化不是追求某一個指標的極致,而是在各種資源之間找到一個「夠用、可接受」的平衡點。
透過這次的數據驗證,也更容易分清楚什麼是必要的複雜度,什麼又是過度設計:
- CPU bound 的場景,加快取的邊際效益很低
- DB 成為瓶頸時,快取的投資報酬率遠高於硬體升級
- 沒有量測就談架構優化,大多只是猜測
這樣的過程,讓技術決策不再只是靠經驗或流行架構,而是建立在實際數據和可驗證結果之上。
專案
參考資料
- k6 Load Testing Documentation
- Prometheus Best Practices
- Grafana Dashboard Examples
- Docker Compose Networking
Alvin
軟體工程師,討厭瞎忙,喜歡用邏輯解決問題,努力在盲目追求成就感與放鬆的生活中取得平衡。