vatt'ghern jaskier's ballads
本文 1 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

EC2 的 API 到今天都是 eventually consistent——不是設計缺陷,是為了把讀取流量搬離 primary 資料庫留下的代價。Zak van der Merwe 在 AWS 待了近十四年,前十年泡在 EC2 的 control plane 上,親眼看著這筆代價一路演變成 Aurora DSQL 想徹底解掉的問題。

控制平面掛了,跑著的機器還得活著

制平面聽起來是系統裡最無聊的一塊——它只負責記錄「應該存在什麼」,再想辦法讓「實際存在什麼」跟上這份記錄。這篇文章刊在 Werner Vogels 的部落格上,但第一人稱的經驗是 Zak van der Merwe 的:他先做了十年 EC2 的 control plane,現在做的是 Aurora DSQL,這近十四年的第一手材料說的是同一件事——這塊「無聊」的東西一旦設計錯,正在跑的機器會直接陪葬。

Werner 在文章開頭只擔任引言的角色。真正動筆講經驗的人是 Zak,後面所有的「我」都是他的視角,不是 Werner 的——他從 EC2 的健康檢查腳本一路寫到今天的 Aurora DSQL,這篇文章基本上是那十年加上後續轉去 DSQL 這幾年的濃縮筆記。

資料平面、控制平面,以及 static stability

Data plane 是運算能力本身——CPU、記憶體、硬碟、網路這些原始資源;control plane 則是把這些資源接到客戶手上的管道。這個分工聽起來抽象,Zak 給出的判準卻很具體:不管 control plane 發生什麼事,已經在跑的 VM 都得繼續動。他把這條原則叫 static stability,並直言這聽起來理所當然——正在跑的 VM 本來就該繼續跑。但這句「理所當然」不是紙上談兵,static stability 這件事,在他剛入行時給過他實際的安全感:不管 control plane 那邊出了什麼亂子,客戶已經在跑的機器不會因此陪葬。這條原則也可以直接搬到別的系統上當一條可檢驗的設計準則:不管 control plane 死不死,已經在跑的東西要不要繼續跑,先問這句話,就知道自己的系統站不站得住。

這個定義聽起來像是切系統圖時劃的一條抽象邊界,但它其實決定了工程師該把力氣花在哪裡:data plane 出問題,通常是單一機器、單一硬碟壞掉,故障範圍容易框定;control plane 出問題,動搖的往往是「客戶接下來還能不能對這個系統下指令」,影響範圍跟著它管的東西一起放大。下面這幾個詞的原文定義都比字面窄——點開看看,是不是跟直覺想的一樣。static stability 這句話讀起來像廢話:「已經在跑的東西應該繼續跑」,誰會反對?真正難的地方,是在 control plane 真的出事的那一刻,資料平面上那些正在跑的 VM 要怎麼在完全聯絡不到控制平面的情況下,還能自己撐住。這篇文章沒有花篇幅去講那個瞬間系統內部具體發生什麼——它把力氣放在另一件事上:control plane 這十幾年是怎麼一步步被拆、被搬、被自動化,才有資格談 static stability 這句話。

手機直接看定義,桌面滑鼠停在詞上看原文出處 · 5 個詞

這篇文章反覆用到幾個詞,原文給的定義都很窄,不是望文生義:control plane「把運算能力這些原始資源,接到客戶手上的管道」——對照 data plane 是運算能力本身。data plane「一組核心能力:原始的運算能力、硬體、網路」。static stability「不管 control plane 發生什麼事,已經在跑的 VM 都得繼續動」。blast radiusAZ 彼此獨立失效,任何單一故障的影響範圍因此縮小;也讓客戶能打造撐得過單一 AZ 失效的架構。eventually consistent這批 read replica 機隊,正是 EC2 API 到今天都是 eventually consistent 的原因。

讀寫分離換來的 eventual consistency

Zak 在 EC2 的第一份工作是幫整個機隊做健康檢查——一台一台 ping,判斷這台機器是不是還活著。他自己形容,那段經驗完全不是「魔法」:主機不斷掛掉,硬體亂搞怪,硬碟說壞就壞,這種故障頻率不是個案——他用「opposite of magic」形容那份工作,意思是機隊本身沒有任何魔法保護著,失敗才是常態,健康的機器反而是需要驗證的例外。control plane 要撐住這種規模,第一個要解的問題是讀取:他們把只讀 API 的流量導到新增的 read replica 上,這大幅降低了 primary 資料庫的負擔。

但 scaling read 這件事在 EC2 上花了好幾年,做法是手動加 replica,代價是接受 eventual consistency——這批 read replica 機隊,正是 EC2 API 到今天都是 eventually consistent 的原因。Zak 引用 Marc Brooker 的說法:這替客戶留下了一筆不小的認知負擔。換句話說,「讀到的資料可能不是最新的」這件事,其實是一個擴展手段用久了留下的既成事實,而且這筆帳到今天都還掛在每一個呼叫這些只讀端點的客戶頭上。這是整篇文章第一次出現的模式,後面還會重複:control plane 遇到擴展問題時,先用最快能做到的手段解決眼前的負擔(這裡是加 replica),代價往往當下不用付,卻會變成一個長期存在、換算成客戶認知負擔的語意,一直算在客戶頭上,直到平台本身有能力自動處理一致性為止。

把 region 切成 AZ,再把 AZ 切成 cell

讀寫分離解決了讀取的擴展問題,但 control plane 本身還沒有被拆開。文章沒有描述 AZ 分裂之前的架構長什麼樣子,只寫了把一個 region 拆成多個 AZ 這個動作本身——從這個動作往回推,合理的推測是:分裂前整個 region 共用一份 control plane 與一份 primary 資料庫,任何單一故障的影響範圍也就是整個 region。這一句是推論,不是文章原句。第一階段的解法,是把每個 AWS region 拆成多個 availability zone(AZ),每個 AZ 各自有獨立的 control plane、獨立的 MySQL 資料庫。這個拆分同時幫到擴展性跟可用性——AZ 彼此獨立失效,任何單一故障的 blast radius 因此縮小,也讓客戶能夠打造可以撐過單一 AZ 失效的架構,成為後來許多 AWS 客戶架構的基礎。這一句值得多停一秒:AZ 分裂原本是 EC2 自己為了縮小內部 blast radius 而做的工程決定,結果變成客戶自己設計高可用架構時可以直接倚賴的一塊地基——內部的架構決策,變成了外部可以拿來設計的介面。這也呼應 Zak 整篇文章的一個潛在教訓:每一次把可用性或一致性的責任從人手上交給系統,都得先在別處付一筆代價——AZ 分裂付的代價是數年工程時間。下面這張圖可以點開分裂前、AZ、cell 三層,看每一層各自把故障範圍縮到多小、又把責任放在誰身上。

第二階段是對內的:他們把每個 AZ 內部再分片成 cell。這兩個工程都花了數年的工程時間,因為牽動了非常多服務的程式碼。分片留下的代價很具體——程式碼裡任何一個碰資料庫的地方,都得知道自己該路由到哪個 shard。用主鍵查詢還算單純,一旦牽扯到 join,或者資料本身不照分片邊界對齊,事情立刻變棘手。連分片本身怎麼切都沒有標準答案——照帳號分,還是照資源分?不同服務依照自己的存取模式各自決定,沒有一個放諸四海皆準的答案。

把 AZ 分裂跟 cell 分片放在一起看,能看出同一個模式:每往下切一層,切開的成本並沒有消失,只是從「單一故障的影響範圍」搬到了「誰要來做這個決定」——AZ 分裂是集中做一次、對外可見的架構決策;cell 分片是分散到每個服務、對內隱形的路由決策,後者反而更難有一個團隊能一次做完。

點任一層看它扛什麼責任 · 3 層

每切一層,故障的影響範圍縮小一圈 分裂前 1 個 region ・ 1 個 control plane ・ 1 個 primary MySQL 文章未描述分裂前的架構,這一層是推測 AZ AZ-a 獨立 control plane + 獨立 MySQL AZ-b 獨立 control plane + 獨立 MySQL cell cell 1 cell 2 cell 3 AZ-a 內部再分片——AZ-b 切法相同,圖上不重複畫出 分裂前 → AZ → cell:範圍越切越小,blast radius 跟著縮 同一份原始碼裡,每個碰資料庫的地方都得知道自己路由到哪個 shard

點上方任一層

Region 分裂前 · 責任邊界

文章沒有細描述分裂前的架構,只講了「我們把一個 region 拆成多個 AZ」這個動作本身——合理的推測是,分裂前整個 region 共用一份 control plane 與一份 primary 資料庫。

後果:任何單一故障(不管是 control plane 還是資料庫)的影響範圍是整個 region。

AZ · 責任邊界

每個 AZ 各自有獨立的 control plane、獨立的 MySQL 資料庫。AZ 彼此獨立失效,任何單一故障的 blast radius 因此縮小。

額外效果:這也成為客戶打造可以撐過單一 AZ 失效架構的基礎,是刻意對外暴露給客戶使用的能力。

Cell · 責任邊界

AZ 內部再分片成 cell,是第二階段、對內的工程,花了數年時間,因為牽動許多服務的程式碼。

留下的代價:每個碰資料庫的地方都要知道路由到哪個 shard;join 或跨分片邊界的查詢變棘手;shard key 照帳號還是照資源分沒有標準答案。

這張圖背後的順序不是巧合:Zak 明確把它分成兩個階段——先對外的 AZ 分裂,再對內的 cell 分片。對正在考慮怎麼拆自己系統 blast radius 的工程師,這個順序本身就是一個可以參考的材料:先處理「客戶看得到、也能利用的」故障隔離,再處理「客戶看不到、只有內部路由邏輯在乎的」分片決策。兩者都各自花上數年工程時間,很少有團隊能同時擠出兩份預算,這也是為什麼 AZ 分裂與 cell 分片被拆成先後兩個階段,而不是一次做完。

沒有安全速率更新系統的年代

AZ 跟 cell 解決的是架構層面的 blast radius,但在自動化程度不夠的年代,連「幫整個機隊打安全更新」這種基本操作都要靠人力硬扛。Zak 描述,一旦發現安全漏洞、整個機隊都得 patch,他們手上沒有一個系統可以說「用安全的速率去更新每一台主機」——真實的做法是把整個團隊找齊,把所有主機分一分,排班巡防。他直白地說:你沒辦法用這種方式去 patch 一個百萬規模的機隊。control plane 最終要做的一件事,就是把人徹底踢出這個迴圈。

這件事跟前面講的 static stability 是同一條線索的兩端:static stability 保證的是「control plane 掛了,VM 不會跟著掛」;而分班巡防要解的是反過來的問題——「要主動改動每一台主機時,control plane 能不能自己保證這件事做得安全、做得完」。沒有這套系統之前,安全靠的是人力換來的巡防班表,離「照安全速率自動跑」的機制還很遠。這也是為什麼 Zak 把「把人踢出迴圈」講得這麼直接:規模一旦跨過某個門檻,人力排班這個方法本身就不再是選項。

拖動中間分隔線比較 · 人力分班 vs 安全速率自動更新

沒有安全速率更新系統 recruit the whole team subdivide all the hosts assign shifts 終端顏色 = 已被某班人力手動確認 patch 你沒辦法用這種方式 patch 百萬規模的機隊 control plane 接手之後 go update every host at a safe rate 波前顏色 = control plane 目前正在更新的一批 control plane 把人徹底踢出這個迴圈

互動圖表

沒有安全速率更新系統時,整隊人力分班手動巡防機隊;這種方式撐不住百萬規模,control plane 最終把人徹底踢出這個迴圈。

Aurora DSQL 把責任收進平台

Zak 前十年做的是 EC2 的 control plane,之後轉去做 Aurora DSQL——他形容這個專案大概在 2021 年前後開始真正加速。DSQL 把前面幾節提到的每一項責任,都收進了平台本身。連線層面:DSQL 底下沒有一台伺服器扛著你的資料庫,每條連線各自起一個 Firecracker micro-VM,等於每條連線都是自己的小型 head node——某條連線掛了,受影響的只有那一條連線,不會波及整個應用,沒有人會被叫醒,也不需要誰決定要不要 failover。他說,自己現在已經不用管 standby 了,因為架構把人從那個痛苦的迴圈裡整個拿掉。這其實是把 static stability 那條原則搬到了更細的粒度上:早年 EC2 談的是「control plane 掛了,VM 得繼續跑」,範圍是整台機器;DSQL 談的是「某條連線掛了,其他連線得繼續跑」,範圍縮小到單一連線。

讀取層面:DSQL 自動加 read replica,而且這是他參與打造的 control plane 的核心工作之一——應用程式讀取流量突然衝高,DSQL 自己處理,讀到的資料永遠是強一致的。在 EC2 年代對客戶說了多年「請稍後再試」之後,這個特性到現在還是讓他吃驚——這句吃驚出自同一個人先造出問題、又親手拆掉問題之後的感受,比單純介紹一個新功能多了一層份量。分片層面:DSQL 自動幫你的 workload 分片,不用自己想——它直接拿掉了原本「照帳號分還是照資源分」的兩難,而且使用者仍然可以用複雜交易、多表 join、次索引這些 Postgres 功能,資料庫自己在背後處理擴展需求。對照前一節「照帳號分還是照資源分沒有標準答案」的困境,DSQL 讓工程師不用再問這個問題,也不用幫忙選答案——這跟手動加 replica、手動選 shard key 是同一種工程負擔,只是 DSQL 把它挪到更早的時間點解決掉了。

對照前面幾節,這三件事分別對應的是三筆舊帳:read replica 手動加、留下 eventual consistency;cell 分片手動做、留下「照帳號還是照資源」的兩難;一旦出安全漏洞,靠的是人力排班而不是系統。這些問題並沒有消失,DSQL 只是把解決它們的責任,從個別工程師與個別團隊的判斷,收回一個共用的平台裡。下面這張表把三筆帳攤開來對照,第四行則是連到前一節的巡防班表——同一個「把人踢出迴圈」的動作,這次發生在連線層級,而不是整個機隊層級。

同樣三件事,責任從工程師手上轉移到平台身上——多數欄位取自文章原句;第一行 EC2 欄的說法是從 DSQL 那句反推的對照,不是原文的描述。
面向 EC2 傳統模式 Aurora DSQL
連線隔離 沒有單獨一台伺服器對應一條連線的說法;一次故障會波及整個資料庫層 每條連線各自一個 Firecracker micro-VM,一條掛了只影響那一條連線
Read replica 手動加,接受 eventual consistency 當代價 自動加,讀取永遠強一致
Sharding 每個服務自己決定照帳號還是照資源分,程式碼裡處處要知道路由到哪個 shard 自動分片,仍可用複雜交易、多表 join、次索引
失效時人力介入 沒有安全速率更新系統時,整隊分班手動 patch 全機隊 沒有人被叫醒,不需要人決定 failover 或 standby

把這些階段攤開來看,順序其實有跡可循:先是第一份工作見識到機隊故障是常態,接著讀寫分離換來 eventual consistency,然後 region 拆 AZ、AZ 再切 cell,兩個階段都各自吃掉數年工程時間,最後才轉去做把這一切自動化的 DSQL。

拖動把手看每一階段解決了什麼問題 · 6 個階段

第一份工作
拖動把手,看每個階段解決了什麼、留下什麼代價。

六個階段串起來,剛好回應了開頭那句話:EC2 API 到今天都是 eventually consistent,是這條路徑上某一步留下的代價。DSQL 把同一條路徑重新走過一次,換來的是讀取永遠強一致。同一個問題,十幾年後同一個人重新解了一次;這次解法收進了平台,不再是某個團隊手上的一份工程排期。這大概是整篇文章裡,最能說明「control plane 該做什麼」的一個例子:把解決問題的責任,從人身上搬到系統上。

這打開的東西:Zak 沒有把話說死——他承認自己不知道一年後軟體會長什麼樣子,理由是整個行業還在裝修,牆還沒封起來;他確定的只有一件事:腳下的地基不會塌,才走得快,而那些真正值得花一輩子解的問題,向來是需要判斷力的問題,不是顧好記帳層別垮掉的問題。他的期望是 DSQL 能把這塊地基還給下一代 builder,把時間還給客戶,讓他們去看看那些原本沒空看的角落——這正是他在 EC2 時代一直希望能多留一點空間去做的事。