題幹與適用場景
一個分散式鍵值儲存部署在三個可用區,共有 120 個實體節點、24 TiB 邏輯資料與 3 副本,每秒執行 200 萬次鍵定位。用戶端或儲存代理需要在本機決定主節點和兩個副本,定位路徑不能為每次請求存取中央服務。 節點會因擴縮容、維護或故障加入與離開,系統希望只搬移受影響的鍵,並避免一次成員變化造成全量冷快取或 資料搬遷。
本題只設計從 key 到實體節點的放置層,以及成員變化時的安全切換。值的讀寫一致性、衝突解決、磁碟引擎 與跨區域複寫不是主問題,但答案必須說明這些能力不會由一致性雜湊自動取得。題目假設鍵經過穩定、分布均勻的 64 位元雜湊;資料量、吞吐與 SLO 是面試演算條件,不代表任何公司的真實規模。
2026 年公開的中英文系統設計面試資料都直接考一致性雜湊、虛擬節點與擴縮容;Amazon Dynamo 論文則提供 它用於分割與副本放置的一手案例。這道題適合中高階後端、基礎架構與分散式系統職位,核心能力是把「少搬 資料」落實成可證明的所有權規則、版本化成員檢視與可驗證的遷移流程。
面試官考察點
第一,候選人能否解釋 hash(key) % N 為什麼在 N 改變時失效,而不只畫一個圓。好的回答會推導重新映射 比例,並指出固定邏輯分割區與直接對存活節點取模是兩種不同設計。
第二,能否區分鍵數量均衡與請求負載均衡。虛擬節點可以把多個小範圍分散給實體節點,也可近似表達容量權重; 單一超熱門鍵仍只有一個主擁有者,增加虛擬節點不會把這個鍵拆開。
第三,能否把資料平面與控制平面分開。資料平面應使用本機、不可變的環快照完成查找;控制平面負責成員身分、 健康、權重、快照版本與遷移狀態。讓每個用戶端依自己的健康檢查立即刪除節點,會讓同一個鍵同時出現多個 擁有者。
第四,能否說清楚「一致性雜湊只決定放置」。副本是否跨可用區、資料是否已複寫、故障節點如何復原、舊資料 何時刪除,都需要額外協定。只說「順時針找三個節點」還沒有完成可用性設計。
最後,候選人是否會比較固定邏輯分割區、rendezvous hashing 與 jump consistent hash,並用實際鍵分布、成員 抖動和快照不一致來驗證方案,而不是把某個虛擬節點數量當成通用常數。
回答前需要釐清的問題
- 目標是快取還是持久化儲存? 快取可在擴容後自然回填;持久化資料必須先複寫、追上增量,再切換所有權。
- 成員變化是任意加入與刪除,還是只在尾端增加編號桶? 任意成員適合雜湊環或 rendezvous hashing;連續編號、
主要在尾端加桶時可以評估 jump consistent hash。
- 負載按鍵數、位元組還是 QPS 衡量? 鍵數均勻不代表容量或流量均勻;權重和再平衡指標必須對應真正瓶頸。
- 節點容量相同嗎? 異質節點需要容量權重;權杖數只能近似權重,實際占比仍要用固定種子模擬和線上指標驗證。
- 副本有哪些故障域要求? 3 副本若落在同一可用區,機房故障仍會全部遺失;選副本時必須跳過相同實體節點
並滿足可用區限制。
- 允許多長的遷移視窗? 零停機切換需要快照版本、全量複寫、增量追趕和短期雙讀或轉送;允許冷啟動時可簡化。
- 誰發布成員檢視? 用戶端必須接收帶有 epoch 與校驗和的權威快照;不同用戶端各自推斷成員會產生分裂所有權。
- 是否需要範圍查詢? 雜湊會打散相鄰業務鍵;大量範圍掃描更適合按範圍分片或先映射到固定邏輯分割區。
30 秒回答框架
「我不會直接對 120 個存活節點取模,因為加到 121 個節點時,在均勻雜湊與穩定編號假設下,約有 120/121 的鍵改變桶。我要把鍵和虛擬節點權杖放進固定的 64 位元雜湊空間,鍵歸順時針第一個權杖;權杖表 排序後用二分搜尋,單次定位是 O(log V)。每個實體節點持有多段小範圍,容量較大的節點分配更多權杖。 副本繼續順時針選擇,但必須跳過同一實體節點並符合跨可用區限制。控制平面發布帶 epoch 的不可變環快照; 持久化儲存在複寫與追趕完成後才切換所有權。最後用固定種子模擬重新映射率、負載偏差、權重與故障域,並 單獨處理熱門鍵,因為虛擬節點無法解決單鍵傾斜。」
分步驟深入解答
第一步:先量化取模方案的重新映射成本
直接映射為:
owner = nodes[hash(key) % N]從 N 個穩定編號桶增加到 N + 1 個桶時,只有同時符合 hash % N = hash % (N + 1) 的鍵保留原桶。因為相鄰整數互質,在一個完整的 N × (N + 1) 餘數週期中,只有 N 個雜湊值符合條件。因此保留比例是 1 / (N + 1),重新映射比例是 N / (N + 1)。
本題從 120 增加到 121 個節點,期望約 120/121 = 99.17% 的鍵改變擁有者。24 TiB 邏輯資料中約 23.8 TiB 需要重新定位;即使是快取而不實際搬移資料,也會形成接近全量的冷未命中。這個推導依賴均勻雜湊、 穩定桶編號和直接以存活節點數取模。若鍵先進入固定數量的邏輯分割區,再由控制平面搬移少數分割區,存活 節點變化不會改變第一層取模公式。
第二步:定義雜湊環、權杖與本機查找
選擇固定的 64 位元雜湊函式和編碼規則,把鍵與虛擬節點權杖映射到同一空間。權杖按無號值排序,最大值之後 繞回零。一個鍵由順時針方向第一個權杖擁有;若二分搜尋越過陣列尾端,就回傳第一個權杖。
locate(key, snapshot):
h = stableHash64(key)
i = lowerBound(snapshot.sortedTokens, h)
if i == snapshot.sortedTokens.length:
i = 0
return snapshot.sortedTokens[i].physicalNodeId總權杖數為 V 時,查找複雜度是 O(log V),快照記憶體是 O(V)。權杖碰撞必須用 (token, physicalNodeId, vnodeIndex) 做確定性排序,不能依賴 map 覆寫順序。節點身分使用持久 UUID 或穩定部署 編號;把暫時 IP 當身分會在重啟更換 IP 時製造一次無意義的成員變化。
理想均衡下,增加第 121 個等容量節點只接收約 1/121 的鍵。對 24 TiB 邏輯資料,期望搬移約 24/121 TiB ≈ 203 GiB,而且來自新節點取得的多個小範圍。這是容量預算的期望值;有限權杖、值大小差異與 存取傾斜都會讓實際結果偏離,不能把 203 GiB 當成硬上限。
第三步:用虛擬節點改善範圍分布與容量權重
每個實體節點只放一個權杖時,隨機間隔差異可能很大,節點離開後負載也會集中給一個後繼。虛擬節點讓每個 實體節點擁有許多分散權杖,把大範圍切成較小的遷移單位。節點故障時,它的範圍會落到多個不同後繼,而不會 壓到單一機器。
權杖數量不能從面試文章抄一個常數。離線使用真實或近似的鍵、值大小和 QPS 分布逐步增加每節點權杖數,測量:
key_count_share, byte_share, qps_share
max_load / mean_load
coefficient_of_variation
snapshot_bytes and lookup_latency收益趨平且快照、更新與二分搜尋成本仍在預算內時停止。異質容量可以讓節點的目標權杖數近似正比於權重, 例如兩倍容量獲得約兩倍權杖;隨機權杖只提供統計近似。若權重必須精確,由控制平面明確分配固定邏輯分割區 通常更容易操作。
虛擬節點只平滑範圍總量。占 20% 請求的單一熱門鍵仍映射到一個主節點;應另用讀取副本、請求合併、近端 快取、業務拆鍵或限流。把虛擬節點從 100 增到 1,000 不會改變這個事實。
第四步:設計副本選擇與資料模型
主節點確定後,繼續順時針掃描權杖,收集不同實體節點,直到取得 3 個副本。遇到同一實體節點的另一個虛擬 權杖必須跳過;還要把可用區當成限制,而不是碰巧期待三個後繼分布在三個區。
RingSnapshot {
epoch,
hashAlgorithm,
tokens: [{ token, physicalNodeId, weight, zone, state }],
checksum,
activatedAt
}
Placement {
keyHash,
epoch,
owners: [{ physicalNodeId, zone, role }]
}資料平面以原子方式替換不可變快照。請求攜帶或記錄 epoch,伺服器發現過舊版本時可回傳新版本提示或轉送到 目前擁有者。控制平面必須驗證每組副本的實體節點唯一性和故障域;若健康節點不足,要明確進入副本不足狀態 並告警,不能重複選同一台機器假裝達到 3 副本。
一致性雜湊給出候選位置,沒有定義寫入確認。持久化儲存仍需決定寫入幾個副本才 ack、讀取如何處理版本差異、 修復如何校驗資料,以及網路分割時偏向一致性還是可用性。
第五步:讓成員變更成為版本化遷移
計畫加入節點可以使用以下狀態機:
joining -> copying -> catching_up -> active
active -> draining -> removed控制平面先依目前 epoch 計算範圍差異,新節點在 joining 狀態不接主流量。背景工作從舊擁有者複寫受影響 範圍,依鍵版本或日誌位置校驗完整性;複寫期間的新寫入進入增量日誌或雙寫。全量複寫完成後追上增量,在 校驗和與副本健康達到門檻時發布新 epoch,並原子切換路由。舊擁有者保留一段有界寬限期,處理舊快照請求與 回復,之後才刪除資料。
節點排空採用相同順序:先複寫與追上到新擁有者,再發布移除它的快照,最後停止並回收舊範圍。環的數學性質 只能告訴系統哪些範圍受影響,不能代替複寫、限速、校驗與回復。遷移器還要限制並行位元組數,避免 203 GiB 預期搬遷耗盡前台讀寫頻寬。
第六步:把故障與成員共識分開處理
節點突然失效時,系統無法先從它複寫資料。資料平面從現有副本繼續服務,修復器依權威成員 epoch 在新的健康 節點重建缺少的副本。短暫逾時不應立刻改環,否則抖動會反覆搬遷資料;健康管理器使用連續失敗、租約或控制 平面共識決定何時標示不可用,並區分暫時轉送與永久移除成員。
所有用戶端必須看到同一條有版本的成員演進。若用戶端 A 已刪除節點 X,用戶端 B 仍把 X 當主節點,同一個鍵 會同時寫到不同位置。控制平面可由共識儲存維護環設定,再以帶 epoch 和校驗和的快照散發;用戶端在短期版本 差異時使用伺服器轉送、雙讀或明確的重試協定,不能讓「最終會收到新設定」成為唯一正確性保證。
若控制平面無法使用,資料平面繼續使用最後一個已驗證快照。此時允許讀寫多久取決於儲存一致性和故障模型; 禁止在失去權威成員檢視時由各節點自行永久改環。
第七步:依限制比較替代方案
| 方案 | 適合條件 | 查找與狀態 | 主要代價 | |---|---|---|---| | 雜湊環 + 虛擬節點 | 成員任意變化,需要範圍與權重可見 | 排序權杖,查找 O(log V) | 快照和權杖調校,遷移協定仍要自建 | | Rendezvous hashing | 節點集較小,要直接選一個或多個最高分節點 | 樸素實作每鍵比較 O(N) | 節點多時計算高,但無環且 top-k 副本直觀 | | Jump consistent hash | 桶連續編號、主要在尾端增加 | 常數記憶體,快速映射到桶號 | 不擅長任意刪除,通常需再加桶到節點的間接層 | | 固定邏輯分割區 | 儲存系統需要可控遷移、精確權重與維運可見性 | 鍵先到分割區,控制平面再分配 | 維護分割區中繼資料與獨立再平衡器 |
若任何無狀態節點都能處理請求,普通負載平衡更簡單;一致性雜湊的價值來自「鍵必須保持擁有者」。若業務 主要做範圍查詢,雜湊打散順序的代價可能超過擴容少搬資料的收益。回答時先選擇放置抽象,再決定具體演算法。
第八步:用性質、負載與故障驗證設計
離線測試使用固定雜湊演算法、固定種子和數百萬個合成鍵,保留基準快照後依序執行加入節點、排空和故障:
- 測量
movedkeys / totalkeys,並確認未落在所有權差異範圍的鍵不會搬移。 - 同時按鍵數、位元組和 QPS 計算
max/mean與變異係數,不能只看鍵數直方圖。 - 為節點設定
1:2:4權重,檢查長期占比接近目標,並記錄權杖數增加後的邊際收益。 - 對每個鍵檢查 3 個副本是否來自不同實體節點與三個可用區。
- 模擬兩個 epoch 並存、快照封包遺失和校驗和錯誤,驗證轉送、重試與舊版本淘汰。
- 在複寫途中終止新舊節點,確認未完成遷移不會提早刪除唯一健康副本。
- 注入單一熱門鍵與成員反覆抖動,確認熱點保護與成員防抖各自生效。
線上監控每個 epoch 的採用率、鍵/位元組/QPS 偏差、遷移積壓與速度、舊版本請求、轉送率、副本不足範圍、 熱門鍵與雜湊計算延遲。擴容是否成功要由這些數值證明,不能只看「新節點已經出現在環上」。
高品質示範回答
「我先限定本題設計的是鍵放置層。120 個存活節點直接取模時,加到 121 個節點會讓約 120/121 的鍵改變桶; 對 24 TiB 邏輯資料接近全量重新定位。固定邏輯分割區可避免這個問題,但若選擇一致性雜湊,我會把鍵與節點 權杖放到固定 64 位元空間,鍵歸順時針第一個權杖。用戶端保存排序後的不可變快照,用二分搜尋,所以熱路徑 沒有中央呼叫,複雜度是 O(log V)。
每個實體節點會有多個虛擬權杖,把範圍和故障遷移分散;異質節點依容量分配不同權杖目標。我不會直接說每個 節點放 100 個,而會用真實鍵大小與 QPS 回放,比較最大值與平均值、變異係數、快照大小和定位延遲。虛擬節點 只能平滑範圍,單一熱門鍵仍要靠讀取副本、請求合併、拆鍵或限流處理。
副本從鍵位置繼續順時針選 3 個不同實體節點,並強制跨三個可用區。控制平面透過共識設定發布帶 epoch 和 校驗和的環快照。計畫擴容時,新節點先複寫範圍並追上增量,校驗成功後才啟用新 epoch;舊擁有者經過寬限期 再刪資料。突然故障則從健康副本服務與重建,不能把畫環當成復原協定。
最後我會用固定種子的數百萬鍵驗證重新映射率、依鍵數/位元組/QPS 的偏差、權重與副本故障域,再注入雙 epoch、遷移中斷、成員抖動與熱門鍵。若節點是連續編號桶,我會比較 jump hash;若需要精確、可控的遷移, 則優先固定邏輯分割區。」
常見錯誤
- 只畫雜湊環 → 沒有說明取模為何失敗或搬多少資料 → 推導重新映射比例並換算成題目資料量。
- 每個實體節點只放一個點 → 隨機間隔造成範圍傾斜,故障負載集中到一個後繼 → 使用多權杖並以回放決定數量。
- 把虛擬節點當成熱門鍵方案 → 單一鍵仍只有一個主擁有者 → 增加副本、合併請求、拆鍵或限流。
- 順時針取三個權杖作為三副本 → 三個權杖可能屬於同一機器或可用區 → 去除重複實體節點並套用故障域限制。
- 新節點出現就立即切流 → 持久資料尚未複寫,讀取會遺失 → 複寫、追上、校驗、發布 epoch 後再刪舊副本。
- 讓用戶端各自踢除故障節點 → 不一致成員檢視會產生分裂所有權 → 由權威控制平面發布版本化快照。
- 聲稱加節點嚴格只移動
1/N→ 有限權杖與權重會讓範圍不均 → 寫成均衡假設下的期望並測量實際分布。 - 固定寫「每節點 200 個 vnode」 → 沒有結合鍵大小、QPS、快照與查找成本 → 透過離線回放找收益趨平點。
- 忽略雜湊演算法版本 → 用戶端實作或編碼差異會把全部鍵重新定位 → 把演算法、編碼、epoch 與校驗和寫入快照。
- 所有分片題都用一致性雜湊 → 範圍查詢或精確維運可能更適合其他方案 → 比較固定分割區、rendezvous 與 jump hash。
追問及應對
追問一:為什麼增加一個節點時,雜湊環不是嚴格移動 1/(N+1)?
這個比例要求節點和鍵在環上均勻、所有節點等容量且權杖足夠多。有限隨機權杖會產生不同大小的區間,值大小 和 QPS 也可能傾斜。因此它是期望值。上線前應回放實際鍵、位元組與流量,並為遷移頻寬保留高於期望的安全 餘量;若必須精確控制每次搬遷量,固定邏輯分割區較合適。
追問二:三個副本已經跨可用區,為什麼還需要成員控制平面?
副本位置只在所有參與者對同一 epoch 達成一致時有意義。兩個用戶端使用不同環,可能把新寫入送到兩組不同 副本,之後都以為自己達到 3 副本。控制平面負責線性化成員設定並發布版本;資料平面在版本差異視窗透過轉送、 雙讀或拒絕過舊寫入來收斂。故障域限制不能取代成員順序。
追問三:一個租戶占了 40% QPS,但只有一個 key,增加 vnode 有用嗎?
沒有。虛擬節點改變範圍如何分給機器,不會把同一個雜湊值分給多個主節點。讀多場景可從多個副本讀取並做 請求合併;寫多場景要按業務維度拆鍵、把可合併狀態分片,或對該租戶限流。若寫入必須序列化,吞吐上限來自 單鍵一致性要求,應明確承認這個瓶頸。
追問四:擴容複寫期間仍有寫入,如何避免切換後遺失增量?
複寫工作記錄一個日誌位置或版本水位,先複製該水位前的全量範圍,再消費後續變更;也可在短時間向新舊 擁有者雙寫。只有新節點追到切換水位、校驗通過且副本健康時才發布新 epoch。舊擁有者保留寬限期處理延遲 請求,刪除前再次確認沒有副本不足範圍。
追問五:控制平面停止運作時能否繼續讀寫?
資料平面繼續使用最後一個帶有效校驗和的不可變快照,因此單次定位不受影響。節點故障後是否仍能寫入,取決 於剩餘副本數和寫入一致性策略;用戶端不能自行永久改環。系統應公開快照年齡,在超過安全視窗或無法滿足 寫入副本數時降級或停止寫入,保留讀取能力不能等同於安全寫入。
追問六:何時選擇 jump consistent hash?
當目標是連續編號的邏輯桶、桶主要在尾端增加,且需要常數記憶體的快速映射時,jump hash 更簡單。它不方便 任意刪除或帶身分的實體節點,通常要先把鍵映射到邏輯桶,再由控制平面把桶放到機器。這個間接層也讓儲存 系統能獨立搬移桶,而不改變鍵到桶的演算法。
追問七:如何升級雜湊函式而不造成全量錯誤路由?
雜湊函式名稱、種子和鍵編碼都屬於快照協定。升級時產生新 epoch,離線計算新舊所有權差異,依資料遷移流程 複寫並追上;切換視窗內請求攜帶演算法版本,伺服器可向新擁有者轉送。直接讓一部分用戶端使用新函式會造成 接近全量的分裂所有權,所以演算法升級要當成一次受控全量重新分片來處理。