English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

guest
1 / ?
back to lessons

承載更多負載的兩種方式

歡迎

當服務開始在負載下不堪重負時,運維人員面臨一個選擇。將現有機器變大(更多 CPU、更多 RAM、更快的磁碟)。或者添加更多機器,讓每台機器執行相同的工作。

第一條路徑稱為垂直擴展(向上擴展)。第二條路徑稱為水平擴展(向外擴展)。

本課程將教你為何幾乎所有現代 Web 架構都選擇水平擴展,以及工作負載的哪項特性使這一選擇成為可能。答案隱藏在一個詞中:狀態。

課程結束時,您將理解:

- 垂直擴展與水平擴展的成本曲線,以及各自適用的情境

- 「有狀態」(stateful)與「無狀態」(stateless)在實務上的意義,以及為何其中一種能低成本地大量複製

- 在預期負載與突發負載下,計算副本群組規模的數學方法

- 確保某層級不會因超過排隊拐點(queueing knee)而崩潰的緩衝空間(headroom)規則

- 狀態必須存放的位置(它不會消失),以及如何將狀態從需要擴展的層級中移出

為何水平擴展在超過特定閾值時更具優勢

垂直擴展:一台更大的機器

優點:簡單。無需修改程式碼。無需協調。同一個流程現在擁有更多的 CPU。

缺點:存在上限。市面上最大的商用虛擬機具有有限的 RAM 與核心數。超過此限制,再多的錢也買不到更多餘裕。成本在供應商產品最佳效益點之後會呈超線性增長。該單一機器發生故障時,整個服務將中斷。

水平擴展:許多較小的機器

優點:沒有上限(取決於您願意為機器付費及協調的程度)。容量隨副本數量線性增加,可預測。單一副本故障僅移除 1/N 的容量,而非 100%。

缺點:需要工作負載支援。某些工作負載(單一大型資料庫、持有即時會話的有狀態遊戲伺服器)難以進行水平擴展。協調與負載分佈成為營運上的考量。

交叉點:任何需要從單一機器故障中倖存下來的生產服務,都必須在至少兩台機器上運行。一旦接受兩台機器,就已經選擇了水平擴展。從那以後,問題不再是「我們應該嗎?」,而是「我們能多便宜地增加下一個副本?」

關鍵促成因素:工作負載在機器本身不保留任何每請求狀態。這樣任何副本都可以回應任何請求,且增加副本無需協調即可增加容量。

垂直與水平擴展:成本曲線與上限

一個團隊在單台擁有 8 個 CPU 的 VM 上運行服務,處理 800 次請求/秒,CPU 使用率為 60%。他們預計未來一年流量將增長 4 倍。他們正在辯論:是租用一台 32-CPU 的 VM(垂直擴展),還是在負載平衡器後運行四台相同的 8-CPU VM(水平擴展)。你推薦哪個選項?並列出兩個超越原始容量的理由。

實務中的有狀態與無狀態

狀態不會消失,它只是移動

有狀態元件:持有資訊,若丟失將改變行為。例如:儲存使用者帳戶的資料庫、儲存工作階段權杖的快取、將長時間串流連線固定到特定使用者的工作程序。

無狀態元件:不持有任何丟失後會產生影響的資訊。例如:讀取請求、查詢資料庫並寫入回應的 Web 層。每個請求都是獨立的;該層在請求之間不保留任何記憶。

關鍵洞察:狀態不會從系統中消失。它會移動到專門設計來持有它的層級(例如資料庫、Redis 叢集、物件儲存)。面對流量的層級可以因此變為無狀態,而無狀態層級可以水平擴展,因為任何副本都可以回應任何請求。

實用測試:如果你隨機終止此層級中的一個程序並重新啟動它,是否有任何使用者會收到錯誤答案或丟失工作階段?如果是,則它持有狀態。如果不是,則它不持有狀態。

範例

- 一個讀取請求、查詢 Postgres 並傳回 JSON 的 Python Web 程序:無狀態。狀態儲存在 Postgres 中。

- 一個在本地記憶體中持有使用者購物車資料的 Python Web 程序:有狀態。終止該程序會導致購物車資料丟失。

- 一個持有與聊天使用者之間開放連線的 WebSocket 伺服器:在連線意義上是狀態化的。終止該程序會中斷連線;客戶端必須重新連線。通常這些系統在謹慎處理下(例如:黏性會話、一致性雜湊)仍可進行水平擴展。

- 一個位於 Postgres 前方的 Redis 快取:對於快取內容而言是狀態化的,但如果快取未命中(cache miss)是可以接受的,則此設計是可以接受的。副本故障意味著快取未命中,而非資料遺失。

為水平擴展進行設計 = 將狀態從需要擴展的層級中移出。

審計可疑層級

一個團隊在反向代理後方使用 6 台後端 VM 執行推薦 API。該應用程式:從請求中讀取使用者 ID,從 Postgres 擷取該使用者近期的活動,執行評分演算法,並回傳一組推薦項目清單。有兩個非標準行為:

- 該應用程式在程序記憶體中維護一個「近期使用者活動」快取,在首次收到某使用者的請求時填充,並在後續請求中重複使用。

- 該應用程式使用黏性會話(sticky sessions):一旦使用者命中 VM #3,其所有後續請求都會發送到 VM #3(代理伺服器已設定為基於 Cookie 的黏性路由)。

識別這兩個行為中哪一個使該層級成為狀態化(stateful),並解釋如果團隊嘗試從 6 台 VM 擴展到 12 台 VM,什麼會壞掉。接著,提出一個重新設計方案,讓該層級能夠水平擴展,同時不失去使用者快取的效益。

副本公式

最簡單的容量公式

一旦某個層級(Tier)變為無狀態(Stateless),其規模調整便成為純數學計算。您需要足夠多的副本(Replicas),以確保穩態負載的到達與離開速率相同,並為突發流量預留緩衝空間。

公式:

副本數 = ⌈ (峰值負載 × 突發係數) / 單副本容量 ⌉ + 緩衝副本數

其中:

- peak_load(峰值負載):在正常運作期間,您預期的最大持續每秒請求數(RPS)

- surge_factor(突發係數):一個倍數,用於覆蓋超過峰值的短暫突發流量(對於可預測的流量,通常為 1.5 倍至 2 倍;對於病毒式傳播或不可預測的流量,則為 3 倍或更高)

- per_replica_capacity(單副本容量):在可接受的延遲與使用率下,單一副本每秒能處理的請求數(通常測量於 70% CPU 使用率時,而非飽和狀態)

- headroom(緩衝副本數):額外的副本,確保少數副本故障時不會導致該層級崩潰(對於小型集群,通常為 1-2 個副本;對於大型集群,通常為 10-20%)

範例演算:後端每個副本在 70% CPU 使用率下可處理 100 req/s。峰值負載為 600 req/s。預期偶爾會出現 2 倍的突增。希望系統能容忍 2 個副本故障。

replicas = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 個副本

80% 法則

單副本的容量並非飽和點。應在 70-80% CPU 使用率下測量容量,絕不可在 100% 時測量。

當使用率超過約 80% 後,佇列曲線會急劇上升:在 60% 使用率下僅需 10 ms 的佇列,在 90% 使用率下可能需要 80 ms。延遲(Latency)會比吞吐量(Throughput)更早達到瓶頸。(配套課程 geometry_of_stateless_horizontal_scaling 將從數學上推導此曲線。)

自動縮放與靜態配置

靜態配置:按峰值 × 突增緩衝進行配置,並接受非高峰時段低使用率帶來的成本。

自動縮放:控制器根據觀察到的使用率、目標延遲或佇列深度來增加或移除副本。

自動縮放注意事項:冷啟動時間至關重要。如果新副本需要 2 分鐘才能啟動,自動縮放無法應對 30 秒內的流量激增。成熟的自動縮放策略會維持一個預置副本的熱池,其數量略低於擴容閾值。

副本規模計算公式及實例演算

為新服務規劃叢集規模

您的團隊計畫推出一個影片金屬資料 API。基準測試顯示,單一副本在 70% CPU 使用率及 50 ms p99 延遲下可處理 250 req/s。行銷部門預測在黃金時段,峰值負載將達到 4,000 req/s。一場計畫中的促銷活動可能會短暫使負載激增至峰值的 3 倍。您希望該服務在 3 個副本同時故障的情況下仍能存活,且倖存副本的使用率不超過 80%。

套用副本公式來規劃上線時的叢集規模。請逐步展示您的計算過程,並解釋一個您的數字可能仍然錯誤的原因(冷啟動、快取預熱、相依服務延遲,或任何您能辯護的理由)。

冷啟動、緩慢排空及其他實際邊界

真實的叢集存在真實的邊界

該公式假設副本能立即出現、立即接收流量,並立即釋放流量。這些假設在生產環境中均不成立。

冷啟動:新副本需要啟動作業系統、啟動程序、載入設定、預熱快取,並通過健康檢查。耗時從 5 秒(容器重啟)到 5 分鐘(完整虛擬機啟動 + 映像拉取)不等。自動擴展無法對短於此延遲的流量突增做出反應。

緩慢排空:從池中移除的副本需要時間來完成進行中的請求,然後才能終止。否則,使用者會看到被截斷的回應。反向代理支援排空(停止接受新請求,完成現有請求),但這需要幾秒到幾分鐘的時間。

熱池(Warm pool):生產環境的機群會維護一池已預先配置但處於閒置狀態的副本,隨時準備在收到訊號時承接流量。這是以少量的穩定成本換取快速的突增回應能力。

連線排空(Connection draining)與立即終止(immediate kill):優雅關閉(graceful shutdown)至關重要。觸發排空程序的 SIGTERM 訊號比 SIGKILL 耗時更長,但不會中斷使用者的請求。

健康檢查窗口(Health check window):剛啟動的副本可能在資料庫連線池尚未熱身(warm)之前,就通過了首次健康檢查;此時代理伺服器(proxy)會開始發送真實流量,導致前幾十個請求速度緩慢。請調整健康檢查,使其測試真實的請求路徑,而非僅檢查進程是否存活。

黏性蔓延(Stickiness creep):即使是名義上無狀態(stateless)的層級,隨著時間推移也會產生黏性(例如 CDN 快取、DNS 解析器快取、連線池)。對於那些聲稱「完全相同」卻行為不一的副本,應保持懷疑態度。

熱池(Warm Pool)或反應式自動縮放(Reactive Autoscaling)?

您的影片金屬資料 API(與上一題相同,規模設定為 51 個副本以應對穩定峰值與突增)在每次有新病毒式影片上傳時,都會經歷持續 30 秒、達到正常負載 5 倍的突增。目前的自動縮放功能需要 90 秒才能從冷啟動新增一個副本(包含映像檔拉取與熱身過程)。在這 90 秒的間隙中,延遲急劇上升,且部分請求會逾時。

提出解決方案。請選擇:(a) 保留反應式自動縮放但進行不同調優,(b) 配置一池閒置副本的熱池,或 (c) 永久性地為 5 倍峰值進行靜態配置。請為您的選擇提供理由,並指出其他兩個選項會帶來的其中一項成本。

在約束條件下設計無狀態層

綜合

您已了解為何水平擴展在超過一定規模後更具優勢、「狀態」在實務上的意義、如何根據預期負載與突發負載來規劃機群規模,以及水平擴展在邊界情況下的局限性。

應用全部四項。

為 feed.example.com(一個社交動態 API)設計後端層。約束條件:每個副本在 70% CPU 使用率下的容量為 200 req/s;預期峰值負載為 1500 req/s;突增係數為 2.5 倍(偶爾出現的熱門故事);需承受 2 個副本同時故障;冷啟動時間為 60 秒;突發流量可能持續 45 秒;預算允許保留部分閒置容量,但不允許永久配置 2.5 倍的資源。

計算穩態機群規模(展示計算過程),選擇您的突增處理策略(熱池 / 反應式自動擴縮 / 兩者皆有),並識別應用程式可能持有的一個每用戶狀態(per-user state),以及它必須遷移到哪裡才能保持該層無狀態。

此課程接下來將走向何方

本課程的後續方向

您現在已建立關於無狀態層(stateless tier)的運作心智模型:理解它為何能擴展、如何進行容量規劃、在哪些邊界條件下會發生故障,以及當您需要將負載推向需要擴展的層級時,狀態(state)必須遷移至何處。

本課程的下一節(cs_distsys_ingress_egress_separation)將探討一個更微妙的问题:即使無狀態層的規模配置完美無缺,當入站(incoming)與出站(outgoing)流量共用同一網路路徑時,仍可能以出人意料的方式失敗。經典案例涉及代理伺服器(proxy)嘗試連接自身的情況;解決方案則是將單一層級拆分為兩個具有不同職責的層級。

配套課程:geometry_of_stateless_horizontal_scaling 推導了排隊曲線(queueing curve)、將利特尔法則(Little's Law)應用於副本集群(replica fleet),以及 80% 利用率拐點(utilization knee)的幾何意義。

做得很好。繼續前進。