Awesome Scalability

發佈

【架構師的自我修養】01 - [Wix] Scaling to 100M:當快取變成毒藥?打破 Cache 迷思

【架構師的自我修養】01 - [Wix] Scaling to 100M:當快取變成毒藥?打破 Cache 迷思
Photo by david hebert / Unsplash

平常在遇到效能瓶頸或是預防流量增長希望加速系統反應時間,我們會有一種膝跳反射式的直覺在說: 流量很大?加個 Cache 就對了。好像 Redis 或 Memcached 是效能問題的萬靈丹一樣 這是【架構師的自我修養】系列的第一篇筆記。這個系列記錄了我從 Awesome Scalability 找尋經典技術文章並閱讀後進行自我辯證跟探討的過程,不只看 "怎麼做",更要探討 "為什麼"

原文連結:Scaling to 100M: To Cache or Not to Cache? - Wix Engineering

平常在遇到效能瓶頸或是預防流量增長希望加速系統反應時間,我們會有一種膝跳反射式的直覺在說: 流量很大?加個 Cache 就對了。好像 Redis 或 Memcached 是效能問題的萬靈丹一樣

這是【架構師的自我修養】系列的第一篇筆記。這個系列記錄了我從 Awesome Scalability 找尋經典技術文章並閱讀後進行自我辯證跟探討的過程,不只看 "怎麼做",更要探討 "為什麼"

今天的主題來自 Wix Engineering 的經典案例。這篇文章檢討了對資料庫效能的刻板印象,盲目引入快取不僅可能是多此一舉,甚至在某些場景下,快取反而成為系統的雷

初始技術債

Wix 早期的架構是典型的 Tomcat + Hibernate + Ehcache + MySQL,不過選擇這套架構只是因為早期的BE前一份工作有用這 tech stack 的經驗

這邊就有個疑問,為什麼一開始就要放 Ehcache,這其實源自於工程界一個普遍的假設:

MySQL(或大多數RDBMS)太慢了,無法應對成長中的流量。為了保護資料庫,我們必須在前面擋一層 In-Memory Cache

聽起來非常合理,而這也是大多數工程師(包括我)在設計初期的直覺。我們認為 Cache 是為了加速讀取減輕 DB 負載,但 Wix 的故事告訴我們,這個假設在特定場景下是完全錯誤的

非預期的錯誤: 解藥變成毒藥

轉折點發生在某次系統更新後出現了 Data Corruption,當 Wix 的工程師退版並修復了資料庫中的錯誤數據,使用者看到的頁面依然是壞掉的

最終問題出在了 Ehcache

  • Black Box: Ehcache 缺乏直觀的管理介面,工程師無法手動處理壞掉的資料
  • 病毒式的同步: 當他們試圖重啟部分伺服器來清除狀態時,這些剛啟動的乾淨節點,自動從其他還沒重啟的節點同步了錯誤的快取資料,讓錯誤像病毒一樣在集群中不斷複製,無法根除

最終,他們被迫選擇了最屈辱的解法:同時重啟所有伺服器,才徹底清除了錯誤狀態

資料庫其實比想像中快

在經歷了 Downtime 的慘痛教訓後,Wix 團隊決定挑戰那個核心假設。他們回頭去測試 MySQL 的極限,結果得出了一個反直覺的結論

只要滿足以下三個前提:

  1. 給予足夠的記憶體(讓 DB 能夠利用 Buffer Pool / Disk Cache)
  2. 使用 Primary KeyIndex 進行單列讀取 (Single row read)
  3. 不使用 Joins

即便在擁有一億筆資料的大表中,MySQL 依然能達到 Sub-millisecond 的讀取速度

對於簡單的 Key-Value 式查詢(例如 SELECT * FROM users WHERE id = ?),RDBMS 的效能完全足以應付高流量,根本不需要 Cache。我們以為的「慢」,很多時候是因為我們濫用了 join,或是沒給 DB 足夠的記憶體

引入 Cache 的四個指引

Wix 由他們的經驗整理出了四項評估是否需要 Cache 的 Trade-offs 思考點

  1. How do you invalidate the cache?
    這是思考架構的時候常見的兩難,如何確保 DB 更新時能跟 Cache 同步,最終一致性(Eventual consistency)的延遲是否能接受
  2. How do you see the values in the cache (black box vs. white box cache)?
    你是把資料放進一個黑箱嗎?出錯時你能多快 Debug?至少比起 Ehcache,現在常用的 redis 可以查詢現有的資料,同時我也看過自幹 in-memory cache 的系統,提供了不少無法穩定 reproduce 的 bug
  3. What happens in a cold start of your system? Can the system cope with traffic if the cache is empty?
    如果處理不了代表系統很脆弱,隨時可能發生 Cache Avalanche
  4. What is the performance penalty of using the cache?
    Cache cluster 同時也代表了每次查詢增加的一次網路流量,不論是否 cache hit,因此也要審慎評估 Miss Rate
Netflix 為了解決問題 3,自己實作了一套跨 region 的 cache warmer 方案,雖然解決了問題但帶來很大的系統複雜度,這也是值得權衡的一點
當然隨流量增加肯定無法逃離 memory 速度遠大於 disk 這件事,但現在是否真的抵達那個階段是需要多加考量的

什麼時候真的需要 Cache

Wix 證明了簡單查詢不需要 Cache,反過來說當簡單查詢不堪使用的時候便是引入 cache 的場景

  • 簡單查詢也不堪負荷 (Hot Key):
    • 原因:單一 DB 節點有物理極限(IOPS、網路頻寬、Lock 競爭),無法承受瞬間爆量集中訪問
    • 案例:名人貼文、秒殺商品
    • 解法:必須使用 Cache(如 Redis)
  • 複雜計算
    • 案例:多表 Join、聚合運算(Aggregation)、複雜排序
    • 原因:這類查詢對 DB 計算成本過高
    • 解法:Cache 預先計算好的結果

實戰應用:短網址系統的混合架構

既然簡單查詢不需要 Cache,那 System Design 面試中常見的短網址系統 (URL Shortener) 該怎麼設計?我們不需要 Redis 了嗎?這時候更該釐清不同的極端場景來考慮是否使用 Cache

  • Hot Key: 例如某個名人發了一條連結,或是秒殺商品,單一 DB 節點超過負荷,無法承受瞬間百萬級的 QPS,這時候必須用 Cache
  • Long Tail: 網路上絕大多數的短網址是幾年沒人點一次的,如果把這些資料都塞進 Redis,是極大的資源浪費

既然同樣的請求卻存在不同場景,採取混合模式是在設計像 Bit.ly 這樣的系統時比較適當的想法,這邊採用 LRU (Least Recently Used) 策略:

  • 熱點資料 (Hot Data):因為頻繁被存取,會一直維持在 LRU Cache 的頂端。這部分流量由 Redis 擋下,保護 DB 不被瞬間流量衝垮
  • 長尾資料 (Cold Data):因為無人存取,會被 LRU 機制自然淘汰
  • 穿透查詢:當使用者真的點擊了一個冷門連結,請求會穿透 Cache 打到 DB。這時回歸 Wix 的教訓: 因為是簡單的 Primary Key Lookup,MySQL 可以輕鬆處理這些請求

結語:做減法的智慧

讀完 Wix 的這段歷程,我最大的體悟是:在實作產品的過程中,我也常掉入為了解決某個小問題而認為不得不引入更複雜的架構,但事實上,能用最簡單的工具解決問題,往往才是最高明的設計

「To Cache or Not to Cache?」點出了學會克制 Over-engineering 的衝動,並在理解每個組件(如 MySQL)的真實極限後,做出最精準的決策,更是架構師的一種修煉

希望這篇筆記能幫助你在未來的系統設計中,多一份「不加 Cache」的底氣