iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

系統設計就像九頭蛇:打造社群網站的 30 天系列 第 21

Day 21 一個人讓系統爆炸:Hot User 與 Hot Post

  • 分享至 

  • xImage
  •  

昨天在討論 Fan-out 時提到,擁有幾千萬追蹤者的帳號一發文,就可能讓 Fan-out on Write 的寫入量瞬間爆炸,所以今天就把這個狀況抽出來單獨討論。

問題不是「流量大」而是「流量太集中」

回想這個系列一路做的事:Load Balancer 把 Request 分散到多台 Server,Sharding 把資料分散到多個 Database 節點,Hash-based Sharding 甚至刻意打散資料,避免大家擠在同一個地方。

這些設計背後都有一個共同假設:流量會平均分散在各個節點、各個 Key 上。

但真實世界不是這樣運作的,假設有個世界知名人物叫老川,他發了一篇引起世界關注的貼文,短時間內湧入的按讚、留言、瀏覽,全部都集中在同一篇貼文、同一個使用者身上:

單一貼文流量集中造成 Hot Key 的示意圖

即使整個系統理論上還有很多餘裕,別的 Shard 很閒、別的 Cache Key 幾乎沒人查,但只要負責老川這篇貼文的那一個 Shard 被瞬間打爆,使用者感受到的就是「系統掛了」。

這種現象叫 Hot Key(或 Hot Partition、Hot Shard),跟「整體流量太大」是完全不同的問題。

整體流量大加機器通常有效,但流量太集中在少數幾個 Key 上,加再多機器也沒用,因為其他機器根本分不到這些 Request。

Hot Read:大量人同時讀同一份資料

老川這篇貼文的內容,會被大量使用者在短時間內反覆查詢。如果每次都直接打 Database,即使只是同一個 Shard 承受,也足以讓它撐不住。

最直接的做法是加強 Cache,把這篇熱門貼文的內容,用足夠長的 TTL 快取起來,讓絕大多數 Request 直接被 Cache 擋下,不需要碰到 Database。

但這裡有個容易被忽略的細節:如果這個 Cache Entry 剛好過期,會發生什麼事?

假設同一秒有上萬個 Request 都發現 Cache Miss,它們會同時跑去查 Database、同時想把結果寫回 Cache,Database 瞬間被這波「重新整隊」的流量打爆,這種現象叫 Cache Stampede(快取雪崩的一種)

常見的緩解方式包括:讓 TTL 加上一點隨機值,避免大量 Key 在同一時間點集體過期;或者採用「鎖」的機制,只讓第一個發現 Cache Miss 的 Request 去查 Database,其他人先等待,等它把資料寫回 Cache 後大家再一起讀。

對真的特別熱門的內容,甚至可以進一步把這份資料複製到多個 Cache 節點,分散讀取壓力而不是全部集中打到同一個 Cache。

不過背後藏著另一個問題:Request 進來的時候,要怎麼決定該去問哪一個 Cache 節點?

最直覺的做法,跟 Day 18 提到的 Hash-based Sharding 一樣:

node_id = hash(key) % N   // N 是目前的節點數量

這個公式在節點數量固定時運作得很好,但只要 N 一變,不管是加新的還是有一台掛掉,幾乎所有 Key 算出來的 node_id 都會跟著改變。

這正是 Day 18 提過的 Resharding 頭痛問題% N 這個公式只要 N 改變,等於把整個對照表打掉重練,原本存在 Node A 的 Key,突然被算成該去 Node C,快取瞬間大量失效,Database 又要重新扛一波打過來的流量,等於自己製造一次 Cache Stampede。

Consistent Hashing

Consistent Hashing(一致性雜湊) 解決的正是這個問題:當節點數量改變時,只讓一小部分 Key 需要搬家,而不是全部重新洗牌。

核心概念是把「節點」跟「Key」都用同一個 Hash Function,算到同一個環狀的數值空間上(想像成一個時鐘錶面,從 0 一路排到最大值之後又繞回 0)。規則很簡單:每個 Key 沿著環順時針走,遇到的第一個節點,就是負責它的節點。

Consistent Hashing
(參考 ByteByteGo 的圖)

如上圖,新增節點 S4 時,只要把它的 Hash 值算出來、放到環上 S0 跟 S3 之間的某個位置:只有「原本要繞過去找 S0」的 Key(也就是 K0),會改成找到 S4;環上其他位置的 Key,繞的還是原本那個節點,完全不受影響。

反過來,如果拿掉一個節點,效果也一樣:原本由它負責的那一段 Key,會改成順時針找到下一個節點,其他節點負責的範圍完全不變。

想像捷運環狀線,每一站負責照顧自己跟下一站之間的這段路線。如果哪天在兩站中間新開一個站,只有原本那一小段路線的乘客會改成在新站上下車,其他站的乘客完全不受影響,不需要重新規劃整條路線。

hash(key) % N 那種「牽一髮動全身」的做法比起來,Consistent Hashing 讓水平擴展(加節點)跟故障移除(減節點)都只造成局部的資料搬遷。

這對「動態增加 Cache 節點分攤熱門內容流量」特別重要,增減節點時不會因為重新分配把大部分快取瞬間打掉重練,不會反而自己製造一次 Cache Stampede。

實務上通常還會搭配 虛擬節點(Virtual Nodes):讓每個實體節點在環上對應好幾個位置,而不是只有一個點,避免節點分佈不均、其中一段落差太大又變成新的 Hot Spot。

Hot Write:大量人同時對同一筆資料寫入

比讀取更棘手的是寫入,假設這篇爆紅貼文同時湧入大量的 Like,而 Like Count 是資料庫裡的一個欄位,用最直覺的方式實作:

UPDATE posts SET like_count = like_count + 1 WHERE id = :post_id;

當上萬個 Request 同時對同一列資料執行這個 Update,它們會互相搶鎖、排隊等待,這一列資料就成了名符其實的 Hot Row。常見的緩解方向有兩種:

把計數器拆成多個

與其讓所有寫入都搶同一列,不如把 Like Count 拆成好幾份子計數(例如依照 Request 隨機分配到 10 個 shard 計數器之一),寫入時只更新其中一個子計數器,讀取時再把所有子計數器加總。

這樣寫入的壓力就被分散到多個獨立的資料列,而不是全部擠在同一列上。

別讓寫入直接打 Database

回顧之前提到的 Message Queue,與其讓每一次 Like 都同步搶著更新 Database,不如先把這個事件丟進 Queue,由背後的 Worker 慢慢批次處理、定期彙總更新,用非同步、緩衝的方式吸收這波瞬間流量,而不是讓 Database 直接硬扛流量海嘯。

小結

今天討論的問題,跟這個系列前面一路做的「均勻分散」剛好相反:

  • Sharding、Hashing 假設流量會平均分佈,但現實中總會有極端集中的 Hot User、Hot Post
  • Hot Read 靠 Cache 緩解,但要留意 Cache 過期瞬間引發的 Cache Stampede
  • 把熱門內容分散到多個 Cache 節點時,靠 Consistent Hashing 決定 Key 該找哪個節點,才能在加減節點時只搬動一小段資料,不會像 % N 一樣牽一髮動全身
  • Hot Write 可以靠拆分計數器、或透過 Message Queue 把同步寫入變成非同步緩衝

值得記住的是:一個系統的整體容量夠不夠,跟它能不能扛住「集中在單一 Key 上」的極端流量,其實是兩個不同的問題,後者往往才是真正把系統打垮的原因。


上一篇
Day 20 Feed 走向大型系統的 Fan-out 做法
下一篇
Day 22 深入探討 Search System
系列文
系統設計就像九頭蛇:打造社群網站的 30 天22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言