Facebook Memcached 系統(tǒng)解決超大規(guī)模緩存一致性問題
今天我們來深入聊一聊 Facebook 是如何將一個簡單的開源緩存軟件 memcached
,打造成一個能夠支撐起全球最大社交網(wǎng)絡(luò)、每秒處理數(shù)十億次請求的分布式緩存系統(tǒng)的。
我們會沿著 Facebook 的擴(kuò)展之路,從單個集群內(nèi)的優(yōu)化,到跨集群、跨地區(qū)的擴(kuò)展,逐步揭開其架構(gòu)設(shè)計的精妙之處。
為什么要用 Memcache?基礎(chǔ)架構(gòu)概覽
在開始之前,我們先解決一個基本問題:Facebook 為什么不直接從數(shù)據(jù)庫(比如 MySQL)讀取數(shù)據(jù),而要費(fèi)這么大勁去構(gòu)建一個緩存系統(tǒng)?
答案很簡單: 快 。對于 Facebook 這種讀多寫少(read-heavy)的業(yè)務(wù)場景,用戶的動態(tài)、評論、好友列表等信息被瀏覽的次數(shù)遠(yuǎn)超被創(chuàng)建的次數(shù)。如果每次讀取都請求數(shù)據(jù)庫,MySQL 完全扛不住如此巨大的讀取壓力。而 memcached
是一個完全基于內(nèi)存的鍵值存儲(key-value store),速度比基于磁盤的數(shù)據(jù)庫快幾個數(shù)量級。
核心架構(gòu):旁路緩存(Look-Aside Cache)
Facebook 使用 memcache
的模式非常經(jīng)典,稱為 旁路緩存 。流程如下:
- 讀(Read) :Web 服務(wù)器需要數(shù)據(jù)時,首先會帶著一個
key
去memcache
里查找。
- 緩存命中 (Cache Hit) :直接拿到數(shù)據(jù),返回給用戶。
- 緩存未命中 (Cache Miss) :服務(wù)器會去后端數(shù)據(jù)庫(或其他服務(wù))查詢,拿到數(shù)據(jù)后,一方面返回給用戶,另一方面把這份數(shù)據(jù)用
set(key, value)
的方式寫回memcache
,方便下次讀取。
- 寫(Write) :當(dāng)數(shù)據(jù)發(fā)生變更時,Web 服務(wù)器會先更新數(shù)據(jù)庫。為了保證緩存數(shù)據(jù)的一致性,它并 不是 去更新
memcache
里的值,而是直接向memcache
發(fā)送一個delete(key)
命令,將舊的緩存數(shù)據(jù)刪除。
為什么寫操作是 delete
而不是 update
?
論文中提到,因為 delete
是 冪等 (idempotent) 的。簡單說,重復(fù)執(zhí)行多次 delete
和執(zhí)行一次的效果是一樣的,這在復(fù)雜的網(wǎng)絡(luò)環(huán)境中能簡化系統(tǒng)設(shè)計,避免因重試等機(jī)制導(dǎo)致數(shù)據(jù)錯亂。而如果兩個 update
請求因為網(wǎng)絡(luò)延遲亂序到達(dá),就可能導(dǎo)致緩存中存儲的是一個舊版本的數(shù)據(jù)。
(上面可能面臨陳舊數(shù)據(jù)寫入/臟數(shù)據(jù)的問題,解決方案參考下文“減少負(fù)載:租約 (Leases) 的妙用”)
另外,需要明確兩個術(shù)語:
memcached
:指開源的、單機(jī)版的緩存軟件本身。memcache
:在本文中,特指 Facebook 基于memcached
構(gòu)建的整個分布式緩存系統(tǒng)。
Memcached 與 Redis:現(xiàn)代緩存技術(shù)選型
我們來解決一個非常實際的工程問題:memcached
在今天用得還多嗎?它和現(xiàn)在非常流行的 Redis
相比,我們應(yīng)該如何選擇?
首先,memcached
本身是一個單機(jī)的內(nèi)存緩存程序。我們常說的“部署一個 memcached 集群”,實際上是指通過客戶端的路由邏輯(或像 mcrouter
這樣的代理),將不同的 key
分發(fā)到不同的 memcached
實例上,從而在邏輯上構(gòu)成一個分布式系統(tǒng)。
在今天的技術(shù)選型中,Redis
因其豐富的功能和靈活性,在很多場景下已經(jīng)成為首選。但 memcached
憑借其極致的簡潔和性能,依然在特定領(lǐng)域(尤其是需要大規(guī)模、低延遲純緩存的場景)擁有一席之地。它們的對比可以總結(jié)如下:
特性 | Memcached | Redis |
核心定位 | 純粹的內(nèi)存緩存 (Cache) | 內(nèi)存數(shù)據(jù)結(jié)構(gòu)服務(wù)器 (In-Memory Data Store) |
數(shù)據(jù)類型 | 只支持簡單的字符串 (Key-Value)。 | 支持字符串、列表、哈希、集合、有序集合等豐富的數(shù)據(jù)結(jié)構(gòu)。 |
性能 | 多線程 架構(gòu)。在簡單的 | 單線程 執(zhí)行命令(I/O 多路復(fù)用)。避免了鎖的開銷,但CPU密集型操作會阻塞其他請求。Redis 6.0 后引入了多線程 I/O。 |
持久化 | 不支持 。數(shù)據(jù)只存在于內(nèi)存,服務(wù)器重啟后數(shù)據(jù)全部丟失,純粹用于緩存。 | 支持 。提供 RDB(快照)和 AOF(日志)兩種持久化方式,可作為數(shù)據(jù)庫使用。 |
高級功能 | 功能極簡,只有 | 支持事務(wù)、發(fā)布/訂閱 (Pub/Sub)、Lua 腳本、Streams 等高級功能。 |
內(nèi)存管理 | 使用 Slab Allocator 。預(yù)先分配固定大小的內(nèi)存塊,可能因數(shù)據(jù)大小與塊大小不匹配而造成一定的空間浪費(fèi)。 | 內(nèi)存管理更精細(xì),支持多種淘汰策略。 |
選型建議:
- 選擇
Memcached
:如果你的需求非常純粹,就是一個高速、高并發(fā)、分布式的鍵值緩存,且不需要復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和持久化。它的簡潔性和多線程帶來的高吞吐在某些大規(guī)模場景下是巨大優(yōu)勢。 - 選擇
Redis
:絕大多數(shù)場景下的更優(yōu)選擇。如果你需要使用哈希、列表等數(shù)據(jù)結(jié)構(gòu),或者需要消息隊列、排行榜(有序集合)、分布式鎖等功能,Redis
提供了開箱即用的解決方案。
集群之內(nèi):攻克延遲與負(fù)載
當(dāng)服務(wù)器規(guī)模擴(kuò)展到數(shù)千臺時,即便都在一個數(shù)據(jù)中心內(nèi),也會遇到巨大的挑戰(zhàn)。Facebook 的優(yōu)化主要集中在兩個方面:降低訪問延遲和減少緩存未命中帶來的負(fù)載。
降低延遲:解決全連接與 Incast 擁塞
在一個集群中,一臺 Web 服務(wù)器為了響應(yīng)單次用戶請求,可能需要與成百上千臺 memcached
服務(wù)器通信,這被稱為“ 全連接 (all-to-all) ”的通信模式。這種模式極易引發(fā)網(wǎng)絡(luò)擁堵,尤其是 Incast 擁塞(當(dāng)大量服務(wù)器同時向一個目標(biāo)發(fā)送數(shù)據(jù)時,會導(dǎo)致交換機(jī)緩沖區(qū)溢出而大量丟包)。
Facebook 的解決方案是從客戶端和服務(wù)端兩方面入手:
客戶端并行與批量請求
Web 服務(wù)器的客戶端庫會將一次頁面請求所需的所有數(shù)據(jù)依賴關(guān)系構(gòu)建成一個有向無環(huán)圖(DAG),從而最大限度地并行發(fā)起請求 。同時,它會將多個 key
的請求合并成一個批量請求(batched request),平均一次會打包 24 個 key
。
這里提到的 DAG(有向無環(huán)圖),它并 不是 由緩存系統(tǒng)自動生成的,而是 由應(yīng)用程序的業(yè)務(wù)邏輯定義的 。
舉個例子,渲染一個用戶個人主頁,可能需要以下數(shù)據(jù):
- 用戶基本信息(
user_info
) - 用戶的好友列表(
friend_list
) - 用戶的最新動態(tài)(
latest_posts
)
這三者都依賴于 user_id
。一旦獲取到 user_id
,這三份數(shù)據(jù)之間沒有依賴關(guān)系,可以并行去獲取。應(yīng)用框架會根據(jù)代碼中聲明的這種依賴關(guān)系,構(gòu)建出一個 DAG,然后調(diào)度 memcache
客戶端盡可能地并行執(zhí)行獨(dú)立的請求分支,同時將同一分支上的多個請求打包,從而最小化網(wǎng)絡(luò)往返次數(shù)和等待時間。
協(xié)議選擇與連接優(yōu)化
- 對于
get
請求,使用UDP
協(xié)議。UDP
無連接的特性減少了建立和維護(hù)TCP
連接的開銷,從而降低了延遲。雖然UDP
不可靠,但 Facebook 的網(wǎng)絡(luò)設(shè)施質(zhì)量很高,實踐中只有約 0.25% 的get
請求被丟棄,客戶端直接將這些視為緩存未命中即可。 - 對于
set
和delete
這類必須保證可靠性的操作,則通過TCP
完成。為了避免海量 Web 線程與memcached
服務(wù)器建立過多TCP
連接,他們引入了一個名為mcrouter
的代理進(jìn)程。mcrouter
部署在 Web 服務(wù)器本地,負(fù)責(zé)將來自多個線程的TCP
請求聚合起來,再統(tǒng)一轉(zhuǎn)發(fā)給memcached
服務(wù)器,極大地減少了連接數(shù)和服務(wù)器資源消耗。
客戶端流量控制
為了解決 Incast 擁塞,客戶端實現(xiàn)了一個滑動窗口(sliding window)機(jī)制來控制未完成的請求數(shù)量。當(dāng)窗口過小,請求串行化會增加延遲;當(dāng)窗口過大,則會引發(fā)網(wǎng)絡(luò)擁堵和丟包。通過動態(tài)調(diào)整窗口大小,可以在兩者之間找到一個平衡點(diǎn)。
減少負(fù)載:租約 (Leases) 的妙用
緩存未命中會給后端數(shù)據(jù)庫帶來巨大壓力。Facebook 引入了一種非常巧妙的機(jī)制—— 租約 (Leases) ,它一舉解決了兩個老大難問題: 陳舊數(shù)據(jù)寫入(stale sets)和驚群效應(yīng)(thundering herds) 。
問題一:陳舊數(shù)據(jù)寫入 (Stale Sets)
想象這個場景:
- 客戶端 C1 請求
key
,緩存未命中。 - C1 去數(shù)據(jù)庫查詢,得到
value=1
。但此時 C1 因為某些原因卡頓了一下。 - 在此期間,另一個用戶更新了數(shù)據(jù),數(shù)據(jù)庫中
key
的值變?yōu)?nbsp;2
,并刪除了緩存。 - 客戶端 C2 也請求
key
,緩存未命中,從數(shù)據(jù)庫拿到value=2
,并成功寫入緩存。 - 卡頓的 C1 終于恢復(fù),將它持有的舊數(shù)據(jù)
value=1
寫入了緩存,覆蓋了新數(shù)據(jù)。
此時,緩存中就存在了一個臟數(shù)據(jù)。
問題二:驚群效應(yīng) (Thundering Herds)/緩存雪崩
當(dāng)一個熱點(diǎn) key
(比如首頁上的熱門新聞)突然失效(被更新或刪除),成千上萬的請求會同時穿透緩存,打到數(shù)據(jù)庫上,可能瞬間壓垮數(shù)據(jù)庫。
租約的解決方案
租約是 memcached
在處理緩存未命中時,返回給客戶端的一個 64 位 令牌 (token) 。
解決 Stale Sets
客戶端在 set
數(shù)據(jù)時必須帶上這個租約令牌。memcached
會驗證令牌的有效性。在上面的例子中,當(dāng)數(shù)據(jù)被更新時,memcached
會將與該 key
相關(guān)的租約(C1 持有的)標(biāo)記為無效。因此,當(dāng) C1 帶著過期的租約來寫緩存時,請求會被直接拒絕,從而保證了緩存不會被舊數(shù)據(jù)污染。這類似于一種 樂觀鎖 或 load-link/store-conditional
操作。
解決 Thundering Herds
memcached
服務(wù)器會對租約的發(fā)放進(jìn)行 速率限制 ,例如,針對同一個 key
,10 秒內(nèi)只發(fā)放一個租約。這樣,當(dāng)熱點(diǎn) key
失效時,只有第一個請求的客戶端能拿到租約并去查詢數(shù)據(jù)庫。其他客戶端在 10 秒內(nèi)再次請求時,會收到一個特殊通知,告訴它們“稍等片刻再試”。通常,第一個客戶端在毫秒級時間內(nèi)就能把新數(shù)據(jù)寫回緩存。后續(xù)的客戶端重試時,就能直接命中緩存了。通過這種方式,原本成千上萬的數(shù)據(jù)庫請求被縮減到只有一次,效果極其顯著。
故障處理:Gutter 服務(wù)器
如果一臺 memcached
服務(wù)器宕機(jī),所有指向它的請求都會失敗,這同樣會形成一股流量洪峰沖擊數(shù)據(jù)庫。
如何處理緩存節(jié)點(diǎn)宕機(jī)?
一個常見的想法是將宕機(jī)節(jié)點(diǎn)上的 key
通過哈希算法重新分布到其他健康的節(jié)點(diǎn)上。但 Facebook 指出這是 危險的 ,因為如果某個 key
是熱點(diǎn) key
,它可能會將大量請求帶到新的節(jié)點(diǎn)上,導(dǎo)致該節(jié)點(diǎn)也被壓垮,從而引發(fā) 級聯(lián)故障 (cascading failures) 。
Facebook 的方案是設(shè)立一個專門的、小規(guī)模的服務(wù)器池,叫做 Gutter 。這些服務(wù)器平時基本是空閑的。當(dāng)客戶端訪問一臺 memcached
服務(wù)器超時后,它不會重新哈希,而是會把請求重試到 Gutter 服務(wù)器上。Gutter 作為一個臨時的緩存替代品,接管了失效服務(wù)器的負(fù)載,從而保護(hù)了后端數(shù)據(jù)庫。Gutter 中緩存項的過期時間很短,以避免數(shù)據(jù)一致性問題。
這引出了另一個問題:為什么 Gutter 服務(wù)器要保持空閑,而不是也用來提供普通服務(wù)?因為當(dāng)一臺服務(wù)器故障時,接替它的 Gutter 服務(wù)器需要擁有足夠的、未被占用的處理能力來承接前者的全部負(fù)載。
區(qū)域之內(nèi):跨集群復(fù)制
隨著業(yè)務(wù)增長,單個集群的規(guī)??傆猩舷?。Facebook 將一個數(shù)據(jù)中心劃分為多個 前端集群 (frontend clusters) ,它們共享一個 存儲集群 (storage cluster) (即數(shù)據(jù)庫)。這個整體被稱為一個 區(qū)域 (region) 。
這種架構(gòu)下,最大的挑戰(zhàn)是如何處理跨集群的數(shù)據(jù)復(fù)制和一致性問題。
區(qū)域性失效 (Regional Invalidations)
由于每個前端集群都有自己獨(dú)立的 memcache
,一份數(shù)據(jù)可能被緩存在多個集群中。當(dāng)數(shù)據(jù)庫中的數(shù)據(jù)更新時,必須通知所有集群刪除對應(yīng)的舊緩存。
這個任務(wù)由一個叫 mcsqueal
的守護(hù)進(jìn)程完成。它部署在數(shù)據(jù)庫服務(wù)器上,通過實時監(jiān)控數(shù)據(jù)庫的提交日志(binlog),來捕獲數(shù)據(jù)變更操作。一旦捕獲到變更,mcsqueal
就會解析出需要失效的 memcache key
,然后將刪除請求廣播給該區(qū)域內(nèi)所有前端集群的 memcache
。
這種基于 變更數(shù)據(jù)捕獲 (Change Data Capture, CDC) 的異步失效模式,比讓 Web 服務(wù)器直接廣播失效請求更可靠、更高效,因為它能更好地批量處理請求,并且基于可靠的數(shù)據(jù)庫日志,可以重放丟失的失效消息。
區(qū)域性失效:mcsqueal
的可靠之道
mcsqueal
詳細(xì)機(jī)制:基于 變更數(shù)據(jù)捕獲 (Change Data Capture, CDC) 的系統(tǒng)是保證跨集群緩存一致性的基石,其流程遠(yuǎn)比 Web 服務(wù)器直接發(fā) delete
請求要精妙和可靠。
詳細(xì)流程如下:
- 修改數(shù)據(jù)庫事務(wù) :當(dāng) Web 服務(wù)器要更新數(shù)據(jù)時,它發(fā)給數(shù)據(jù)庫的事務(wù)不僅僅是
UPDATE
或INSERT
語句。它還會附加元數(shù)據(jù),明確指出這個數(shù)據(jù)庫變更會影響到哪些memcache
的key
。 - 寫入可靠日志 :數(shù)據(jù)庫執(zhí)行事務(wù),并將數(shù)據(jù)變更和這些需要失效的
key
一同記錄到其高可靠的提交日志中(例如 MySQL 的 binlog)。這是最關(guān)鍵的一步,因為日志是持久化且有序的。 mcsqueal
監(jiān)控與解析 :mcsqueal
守護(hù)進(jìn)程像一個忠實的哨兵,持續(xù)不斷地監(jiān)控(tail)數(shù)據(jù)庫的提交日志。當(dāng)它讀到新的變更記錄時,就會從中解析出那些需要被刪除的key
。- 廣播與中繼 :
mcsqueal
將提取出的刪除請求,批量打包后,廣播給區(qū)域內(nèi)所有前端集群。這些請求并非直接打到memcached
服務(wù)器上,而是先發(fā)送給每個集群中專門負(fù)責(zé)中繼的mcrouter
實例。 mcrouter
精準(zhǔn)投遞 :mcrouter
收到批量包后,將其解開,并根據(jù)每個key
的哈希值,將刪除命令精準(zhǔn)地路由到該key
所在的具體memcached
服務(wù)器上。
這套機(jī)制的核心優(yōu)勢在于 可靠性 和 效率 。因為失效指令記錄在數(shù)據(jù)庫的持久化日志里,即使中途 mcsqueal
進(jìn)程崩潰或者網(wǎng)絡(luò)中斷,恢復(fù)后只需從上次中斷的位置繼續(xù)讀取日志即可,保證了失效消息“ 不丟不重 ” 。同時,高效的批量處理也極大地降低了網(wǎng)絡(luò)開銷。
冷集群預(yù)熱 (Cold Cluster Warmup)
當(dāng)一個新的前端集群上線,或者一個集群因維護(hù)重啟時,它的緩存是空的(稱為“冷集群”),此時所有請求都會穿透到數(shù)據(jù)庫,造成巨大沖擊。
為了解決這個問題,F(xiàn)acebook 設(shè)計了 冷集群預(yù)熱 機(jī)制。簡單來說,就是讓冷集群里的客戶端在緩存未命中時,不去請求數(shù)據(jù)庫,而是去請求一個緩存數(shù)據(jù)齊全的“熱集群”的 memcache
。這相當(dāng)于用其他集群的緩存來幫助新集群“預(yù)熱”。為了處理這期間可能發(fā)生的競爭問題(例如,數(shù)據(jù)剛在熱集群被更新,冷集群就來讀取舊數(shù)據(jù)),他們在向冷集群發(fā)送 delete
命令時,會附加一個短暫的“ 拒絕寫入 ”時間(如 2 秒)。這能有效防止臟數(shù)據(jù)被寫入冷集群的緩存中。
當(dāng)一個新集群(冷集群)上線時,讓它從一個已有集群(熱集群)的緩存中拉取數(shù)據(jù)預(yù)熱,是一個非常聰明的辦法。但如何處理競爭問題,以及 2 秒的“拒絕寫入”時間是怎么回事?
預(yù)熱機(jī)制與競爭問題
預(yù)熱的基本流程是:冷集群的客戶端在緩存未命中后,會向熱集群發(fā)送 get
請求,拿到數(shù)據(jù)后再嘗試 add
到本地緩存中。
這里的競爭在于:在冷集群客戶端從“獲取數(shù)據(jù)”到“寫入本地緩存”的這個時間窗口內(nèi),原始數(shù)據(jù)可能已經(jīng)被更新了。此時,失效消息會同時發(fā)往熱集群和冷集群。如果冷集群的客戶端動作不夠快,就可能把已經(jīng)過期的舊數(shù)據(jù)寫入本地緩存。
“拒絕寫入”的妙用
為了解決這個問題,F(xiàn)acebook 對 delete
命令做了擴(kuò)展。發(fā)往冷集群的失效刪除命令,會附帶一個參數(shù),即 拒絕寫入期 (hold-off time) ,默認(rèn)為 2 秒。
當(dāng)冷集群的 memcached
服務(wù)器收到這樣一條命令后,它會:
- 刪除指定的
key
。 - 在該
key
上設(shè)置一個 臨時鎖 ,在接下來的 2 秒內(nèi), 拒絕 所有針對這個key
的add
或set
操作。
現(xiàn)在我們再看預(yù)熱流程:當(dāng)冷集群客戶端帶著從熱集群取回的舊數(shù)據(jù)嘗試 add
到本地時,如果一個失效消息剛剛到達(dá),這個 add
操作就會因為這個 2 秒的鎖而 失敗 。
這個失敗是一個非常重要的信號,它告訴客戶端:“你手上的數(shù)據(jù)已經(jīng)舊了!” 客戶端捕獲到這個失敗后,就會放棄這次預(yù)熱操作,轉(zhuǎn)而直接從權(quán)威數(shù)據(jù)源——數(shù)據(jù)庫——去獲取最新的數(shù)據(jù)。
為什么是 2 秒?如果延遲更長怎么辦?
- 為什么是 2 秒? 這是一個基于海量實踐數(shù)據(jù)得出的 工程經(jīng)驗值 (heuristic) 。2 秒被認(rèn)為足以覆蓋絕大多數(shù)情況下,失效消息在數(shù)據(jù)中心內(nèi)部網(wǎng)絡(luò)中的傳播和處理延遲。
- 如果延遲超過 2 秒呢? 論文坦誠地承認(rèn),這種情況理論上是可能發(fā)生的。如果一條失效消息因為極端網(wǎng)絡(luò)擁堵等原因,延遲超過了 2 秒才到達(dá),那么一個 stale 的值的確可能被成功寫入緩存。
但這正是 Facebook 架構(gòu)哲學(xué)的一個完美體現(xiàn):他們愿意接受這種極小概率的、短暫的數(shù)據(jù)不一致,來換取 巨大的運(yùn)維收益 ——將一個集群的預(yù)熱時間從幾天縮短到幾小時。這是一種在“一致性”和“可用性/性能”之間做出的清醒且務(wù)實的權(quán)衡。
跨越區(qū)域:全球一致性挑戰(zhàn)
為了降低全球用戶的訪問延遲和容災(zāi),F(xiàn)acebook 在世界各地部署了多個區(qū)域(數(shù)據(jù)中心)。架構(gòu)上,他們設(shè)定一個 主區(qū)域 (master region) ,存放主數(shù)據(jù)庫,其他區(qū)域作為 從區(qū)域 (replica regions) ,存放只讀的數(shù)據(jù)庫副本。數(shù)據(jù)通過 MySQL 的復(fù)制機(jī)制從主區(qū)域同步到從區(qū)域。
這里最大的挑戰(zhàn)是 復(fù)制延遲 (replication lag) 。如果一個歐洲用戶更新了他的頭像(寫請求被路由到美國的主區(qū)域),然后立即刷新頁面(讀請求在本地的歐洲從區(qū)域),他很可能因為復(fù)制延遲而看不到自己的更新。
為了解決這個問題,F(xiàn)acebook 使用了一種叫 遠(yuǎn)程標(biāo)記 (remote marker) 的機(jī)制。 當(dāng)一個寫請求發(fā)生在從區(qū)域時:
- 客戶端首先在 本地
memcache
中設(shè)置一個標(biāo)記key
(如remote_marker_for_key_X
)。 - 然后將寫請求發(fā)送到 主區(qū)域 的數(shù)據(jù)庫。
- 同時刪除 本地
memcache
中真正的key_X
。
當(dāng)后續(xù)的讀請求來到從區(qū)域:
- 它首先請求
key_X
,緩存未命中。 - 接著它會檢查是否存在
remote_marker_for_key_X
。 - 如果標(biāo)記存在,說明這個
key
剛剛在主區(qū)域被更新,本地數(shù)據(jù)庫副本的數(shù)據(jù)可能是舊的。于是,客戶端會將這次讀請求 直接路由到主區(qū)域 去查詢最新數(shù)據(jù),從而避免讀到舊數(shù)據(jù)。
這是一種典型的 用延遲換取一致性 的權(quán)衡。雖然跨區(qū)域讀取會慢一些,但保證了更好的用戶體驗(直接請求美國數(shù)據(jù)比用戶等待數(shù)據(jù)復(fù)制到歐洲區(qū)域更快)。
總結(jié)與啟示
Facebook 的 Memcache 架構(gòu)是一個在工程實踐中不斷演化、充滿權(quán)衡的杰作。從它的演進(jìn)中,我們可以學(xué)到幾個核心思想,這對我們?nèi)粘5南到y(tǒng)設(shè)計和面試都大有裨益:
- 分離關(guān)注點(diǎn) :將緩存系統(tǒng)和持久化存儲系統(tǒng)分離,使得兩者可以獨(dú)立擴(kuò)展和優(yōu)化。
- 將復(fù)雜性推向客戶端 :
memcached
服務(wù)器本身極其簡單,大部分復(fù)雜邏輯如路由、序列化、錯誤處理、服務(wù)發(fā)現(xiàn)等都放在了無狀態(tài)的客戶端(或mcrouter
)中。這使得系統(tǒng)核心組件非常穩(wěn)定,而客戶端邏輯可以快速迭代和部署。 - 為失敗而設(shè)計 :從 Gutter 系統(tǒng)到
mcsqueal
的可靠重放,整個系統(tǒng)處處體現(xiàn)著對故障的思考,致力于避免單點(diǎn)故障和級聯(lián)故障。 - 簡單為王 :系統(tǒng)設(shè)計中充滿了務(wù)實的權(quán)衡,例如容忍短暫的陳舊數(shù)據(jù)以換取極高的性能和可用性,避免引入過于復(fù)雜但收益有限的優(yōu)化。
希望今天的講解能幫助大家更深入地理解這個經(jīng)典的分布式緩存系統(tǒng)。這其中的很多設(shè)計思想,比如租約、CDC、Gutter 模式等,都是非常值得我們在自己的項目中借鑒的。