【從一台 Server 到分散式架構】第 10 篇:分散式世界的三選二——CAP 定理
上一篇,小明他們把登入機制改成了 JWT 無狀態架構,但美國的用戶仍然在抱怨:「圖片跟影片載入慢到像在爬!」
原因很簡單:小明他們所有的伺服器都架在台灣。用戶發出的每一個請求,都要跨越太平洋來回傳送資料。光是網路延遲(Network Latency)就要幾百毫秒,這是物理定律,任何程式最佳化都救不了。
「那就去美國開一個節點吧!」小明說。
聽起來很簡單,做起來卻是整個分散式系統設計中最燒腦的一步。因為一旦資料要同時存在兩個不同的地方,你就必須正面迎擊整個分散式架構的核心矛盾:CAP 定理(CAP Theorem)。
先從一個生活比喻說起
小明的餐廳生意越做越大,終於決定去美國西岸開分店。
現在,台灣總店跟美國分店各有一本帳本(資料庫)。每當有事件發生,兩本帳本都要記錄。問題來了:
- 台灣總店剛剛接到一筆「使用者 A 購買了課程 X」的訂單,已經寫進台灣的帳本。
- 幾毫秒後,美國的使用者 B 查詢「課程 X 還有多少名額?」——但這個查詢打到了美國分店的帳本。
- 問題:台灣剛寫入的資料,美國同一時間看到的是最新的嗎?
如果跨太平洋的資料同步需要 1 到 5 秒(網路傳輸加上各節點確認寫入的時間),那在這幾秒之內,台灣和美國的帳本就是不一致的。
1 到 5 秒對人類雖然幾乎也感覺不到,但問題不在「人有沒有感覺」,而在這幾秒內可能發生多少事。在一個每秒鐘處理幾千筆交易的高流量平台,幾秒的不一致窗口,就是幾千筆「可能讀到舊資料」的查詢。如果其中有幾筆是「確認某堂課還有名額→下訂」,你可能就賣出去了根本不存在的名額。
這個情境,就是 CAP 定理想要解決的核心問題。
CAP 到底是什麼?
CAP 是三個英文單字的縮寫,代表分散式系統中的三個性質:
C — Consistency(一致性):任何時刻,不管你問哪一台機器,拿到的回答都是最新、最正確的那份資料。台灣剛寫完,美國你馬上去問,答案是一致的。
A — Availability(可用性):系統隨時都可以回應請求,不會說「對不起,現在服務中斷,請稍後再試」。就算某些節點出了問題,系統仍然給你一個答案(雖然這個答案可能不是最新的)。
P — Partition Tolerance(分區容錯):就算網路在兩個節點之間斷線了(這稱為「網路分區 Network Partition」),系統仍然可以繼續運作,不會整個崩潰。
💡 CAP 定理說:在一個分散式系統裡,這三件事最多只能同時做到兩件,你永遠無法三者兼得。
為什麼不能三個全選?
想像台灣節點跟美國節點之間的網路突然斷線了(這就是「網路分區」發生了)。此時你面臨一個無法迴避的兩難:
- 如果你想繼續保持「可用性(A)」:美國節點不能讓使用者的請求在那邊空等,它必須繼續回應。但它只有自己本地那份可能是舊的資料,這就勢必違反「一致性(C)」。
- 如果你想繼續保持「一致性(C)」:美國節點必須確保自己跟台灣的資料完全同步後才能回應。但偏偏網路斷了,它根本收不到台灣的最新資料,所以只能停止服務、拒絕回應,這就違反了「可用性(A)」。
只要不可避免的網路分區發生,A 和 C 天生就是對立的,你只能選一個。這就是為什麼 CAP 只能三選二。
為什麼 P(分區容錯)幾乎無法放棄?
你可能會問:「那我放棄 P,選擇 C 和 A 不就好了?」
問題是,在真實的網路世界裡,網路斷線是遲早會發生的事。台灣與美國之間的海底光纜偶爾會被船錨刮斷,機房與機房之間的交換器偶爾會出故障。如果你的系統不要求「分區容錯」,就代表你的系統假設「網路永遠不會斷」——但這在現實中根本做不到。
所以實務上,P(分區容錯)幾乎是所有分散式系統的必備條件,大家真正的取捨只在 C 和 A 之間做選擇:
-
選 CP(一致性 + 分區容錯):網路斷線時,系統寧願拒絕回應(犧牲可用性),也不要給出一個「可能是舊的」錯誤答案。→ 適合金融交易、庫存管理等「資料不能出錯」的場景。
-
選 AP(可用性 + 分區容錯):網路斷線時,系統仍然繼續服務(犧牲一致性),你可能拿到稍微舊一點的資料,但至少系統不會掛。→ 適合社群動態、內容推薦等「稍微有延遲無所謂」的場景。
實際案例:Facebook 的按讚數
想想 Facebook 或 Dcard 的按讚數。
當一篇爆紅的文章瞬間被幾十萬人按讚時,Facebook 在全球各地的資料中心幾乎不可能讓所有人在同一毫秒看到完全一致的按讚數字。你在台灣看到的是「49,231 個讚」,你在美國的朋友同一秒看到的可能是「49,218 個讚」。
幾秒後,兩個資料中心的資料同步了,大家看到的數字又趨於一致。這就叫做最終一致性(Eventual Consistency)。
Facebook 選擇的是 AP 策略:允許各個節點的資料暫時不一致(犧牲強一致性),但系統隨時保持可用(保證高可用性),資料最終會收斂到正確狀態。
對 Facebook 來說,這是完全可以接受的取捨——按讚數差個幾個,沒有人會出大事。
回到小明的課程平台:他應該怎麼選?
小明現在知道了,他在台美兩個地區同時部署的時候,無可避免地要做出取捨。他思考了一下自己的業務需求:
「購買課程、扣款」 → 這絕對不能有不一致!使用者付了錢卻看不到課程,或是同一堂課被賣超量,都是不可接受的商業災難。這部份必須強制走 CP:下單時強制打到同一個「主節點」,寧願稍慢,也要確保強一致性。
「首頁熱門課程排行、評分、留言」 → 稍微延遲幾百毫秒倒無所謂,使用者不會注意到某堂課的評論數現在顯示 1034 還是 1038。這部份可以放膽走 AP:讀取可從最近的節點拿資料,允許各地看到的數字稍有不同,系統保持絕對順暢。
這種「不同資料用不同策略」的混合做法,在大型系統裡非常普遍,工程師的判斷力就體現在「知道哪些資料需要強一致性、哪些可以接受最終一致性」。
實務上怎麼做到 CP 或 AP?
理論說完了,具體工程上怎麼實現這兩種策略呢?
實現 CP(強一致)
核心手段:把所有寫入都導向同一個「主節點(Primary)」,採用同步複製。
還記得我們在第 06 篇學過的「讀寫分離」嗎?這裡也是類似的概念,但多了跨地區的維度:
- 全球只有一個 Primary 寫入節點(例如只放在台灣),所有地區的寫入請求,不管是台灣用戶還是美國用戶,都得跨洋打到台灣的主節點去。
- 寫入確認後,主節點同步複製(Synchronous Replication)到美國節點——必須等美國節點也確認收到,才告訴用戶「成功」。
- 代價是寫入延遲增加,且如果兩地網路斷線,系統寧願停止服務也不亂寫。
適合使用的技術:PostgreSQL / MySQL 的同步複製模式、ZooKeeper、etcd。
實現 AP(最終一致)
核心手段:每個地區都有獨立的可寫節點,非同步複製,讀最近的節點。
- 台灣和美國各自有一個可以接受寫入的本地節點,用戶直接寫入最近的節點。
- 台灣發生的寫入,非同步地(Asynchronously)複製到美國,不等確認就回傳成功給使用者。
- 兩邊節點定期對帳同步,如果同一筆資料兩邊都有修改,系統會有一套「衝突解決策略(Conflict Resolution)」來決定誰的版本算數。
適合使用的技術:DynamoDB Global Tables、Cassandra、CouchDB,這些資料庫天生就是為多寫、最終一致設計的。
實現混合策略
實際上,不同的資料表甚至不同的查詢路徑,可以走不同的策略。這個「分流」大部分情況是直接在應用程式的程式碼裡決定的:
def purchase_course(user_id, course_id):
# 涉及金錢,強制走 CP:打台灣主節點,等待確認
primary_db.execute("INSERT INTO orders ...", consistent=True)
def get_popular_courses():
# 只是讀取排行榜,走 AP:打最近的本地節點,接受稍舊資料
local_db.execute("SELECT * FROM popular_courses ...", region="nearest")在更大規模的系統裡,這個切分會上升到**服務邊界(Service Boundary)**的層次:不同的功能模組(例如:訂單服務、課程瀏覽服務)各自擁有自己的資料庫,天然就走不同的一致性策略。你要用 Proxy(像是 ProxySQL)來做透明路由也可以,但維運成本較高,通常只有在不想改應用程式碼的情況下才會用。
整體的路由流向如下:
小結與預告
今天我們學到了分散式系統中最重要的一個理論框架:
- C(一致性):任何節點任何時刻看到的資料都一樣。
- A(可用性):系統隨時可以回應,不中斷服務。
- P(分區容錯):網路斷線時系統仍可運作(幾乎是必選項)。
- 三者最多選二,實務上是在 CP(強一致但可能暫時不可用) 與 AP(高可用但可能暫時不一致) 之間做選擇。
小明的平台現在跨越了亞洲與美洲,系統越來越像一個真正的分散式系統。 但這同時帶出了一個新問題:當系統越來越大,過去的那個「一個大程式全部搞定」的結構,也開始讓工程師們叫苦連天。改 A 功能要動到 B 功能,一個 deploy 就像在拆炸彈一樣驚心動魄。
是時候讓這個大廚房開始分頂了——這就是我們接下來要談的:第 11 篇:單體開始痛苦——一改 code 全站都要 deploy。我們下一篇見。