Google Spanner 實現(xiàn)全球分布式的事務(wù)
今天我們來聊聊一個在分布式數(shù)據(jù)庫領(lǐng)域鼎鼎大名的系統(tǒng):Google Spanner。這篇 2012 年的論文石破天驚,因為它實現(xiàn)了一件被認(rèn)為極其困難甚至不切實際的事情: 全球分布式的事務(wù) 。
想象一下,你的數(shù)據(jù)分布在全球各地的數(shù)據(jù)中心,但你卻可以像操作一臺電腦上的數(shù)據(jù)庫一樣,執(zhí)行跨越這些數(shù)據(jù)中心的事務(wù),并且保證數(shù)據(jù)的一致性。這聽起來是不是很酷?Spanner 就做到了這一點。
核心挑戰(zhàn):從 F1 廣告系統(tǒng)說起
要理解 Spanner 為何如此設(shè)計,我們得先看看它最初要解決的問題。Google 的廣告后端系統(tǒng),代號 F1,最初是構(gòu)建在手動分片的 MySQL 數(shù)據(jù)庫上的。隨著業(yè)務(wù)增長,數(shù)據(jù)集達(dá)到了幾十個 TB,手動對這個支撐核心收入的數(shù)據(jù)庫進(jìn)行分片和擴(kuò)容,變成了一場噩夢。上一次折騰,花了兩年多的巨大努力,協(xié)調(diào)了數(shù)十個團(tuán)隊。
這里的 數(shù)據(jù)分片(Sharding) 是一種數(shù)據(jù)庫分區(qū)技術(shù),也常被稱為“水平分區(qū)”。想象你有一個巨大的、包含數(shù)億用戶信息的表,如果把這張表存在一臺服務(wù)器上,查詢和寫入的壓力很快就會讓這臺服務(wù)器不堪重負(fù)。
數(shù)據(jù)分片就是將這個大表“水平”切開,分成許多更小的、更容易管理的部分,每一部分就叫做一個 分片(shard) 。
在 F1 廣告系統(tǒng)的舊 MySQL 架構(gòu)中,分片就是 按客戶(customer) 來進(jìn)行的:
- 假設(shè) Google 有三個廣告客戶:A、B 和 C。
- 系統(tǒng)可以將客戶 A 的所有相關(guān)數(shù)據(jù)(比如廣告活動、賬單信息等)全部存放在 Shard 1 ,將客戶 B 的所有數(shù)據(jù)存放在 Shard 2 ,將客戶 C 的所有數(shù)據(jù)存放在 Shard 3 。
- 這樣一來,當(dāng)需要查詢客戶 A 的數(shù)據(jù)時,請求會直接路由到管理 Shard 1 的服務(wù)器上,而不會影響到其他服務(wù)器。這就將巨大的讀寫壓力分散到了多臺服務(wù)器上。當(dāng)客戶數(shù)量和數(shù)據(jù)量持續(xù)增長時,這種手動管理和重新分片的過程變得極其復(fù)雜和昂貴,這也是 F1 團(tuán)隊遷移到 Spanner 的主要原因之一。
F1 團(tuán)隊迫切需要一個新的系統(tǒng),這個系統(tǒng)需要滿足幾個關(guān)鍵需求:
- 更靈活的分片 :再也不想手動搞數(shù)據(jù)分片了。
- 同步復(fù)制和自動故障轉(zhuǎn)移 :
MySQL的主從復(fù)制在故障轉(zhuǎn)移時很麻煩,還可能丟數(shù)據(jù)。 - 跨分片的強(qiáng)一致性事務(wù) :業(yè)務(wù)邏輯需要跨任意數(shù)據(jù)的事務(wù)和一致性讀取。
當(dāng)時主流的 NoSQL 系統(tǒng)無法滿足 F1 對強(qiáng)事務(wù)語義的要求。于是,Spanner 應(yīng)運而生。
Spanner 藍(lán)圖:Paxos、分片與復(fù)制
Spanner 的基本架構(gòu)是將數(shù)據(jù)分片(sharded),然后將每個分片的副本(replicas)分布在多個地理位置不同的數(shù)據(jù)中心。這么做的好處顯而易見:
- 高可用性 :一個數(shù)據(jù)中心掛了,服務(wù)不受影響。
- 低延遲 :用戶可以就近讀取本地數(shù)據(jù)中心的副本,速度飛快。
數(shù)據(jù)的復(fù)制和一致性由 Paxos 共識算法來管理,每個數(shù)據(jù)分片都是一個獨立的 Paxos 組,擁有自己的 log 。
一個 Paxos 組就是負(fù)責(zé)管理一個數(shù)據(jù)分片的一組副本(replicas)。 這組副本使用 Paxos 共識算法來就所有的數(shù)據(jù)修改達(dá)成一致,確保數(shù)據(jù)的一致性和高可用性。每個 Paxos 組都有一個 Leader,負(fù)責(zé)處理寫入請求。
一個 Paxos 組的多個副本被 刻意地 分布在地理位置分散的多個數(shù)據(jù)中心里。這正是 Spanner 實現(xiàn)高可用性和災(zāi)難恢復(fù)能力的關(guān)鍵。例如,一個 Paxos 組可能有 5 個副本,分布在 3 個不同的數(shù)據(jù)中心,即使其中一個數(shù)據(jù)中心因為地震或斷電而完全失效,剩下的副本依然可以選舉出新的 Leader,繼續(xù)提供服務(wù)。
這里你可能會問, Spanner 可以用 Raft 算法替代 Paxos 嗎?
當(dāng)然可以。從這篇論文的層面來看,兩者沒有區(qū)別。不過,在 Spanner 被設(shè)計和構(gòu)建的那個年代,Raft 算法還沒誕生,而 Google 內(nèi)部已經(jīng)有了一套調(diào)優(yōu)得非常好、穩(wěn)定可靠的 Paxos 實現(xiàn)。所以,選擇 Paxos 是一個非常自然且實際的工程決策。
處理讀寫事務(wù):加了“保險”的兩階段提交
一個分片包含一部分特定的數(shù)據(jù),比如客戶 A、B、C 的數(shù)據(jù)。這些數(shù)據(jù) 整體 由一個 Paxos 組來管理。數(shù)據(jù) D、E、F 可能屬于另一個分片,因此由 另一個獨立的 Paxos 組 來管理。所以,數(shù)據(jù) ABC 不會 同時存在于其他分片上。一個事務(wù)可能會需要 同時訪問 多個分片的數(shù)據(jù)(比如從客戶 A 的賬戶轉(zhuǎn)賬到客戶 D 的賬戶),這種情況下,該事務(wù)就需要與多個 Paxos 組(管理客戶 A 的組和管理客戶 D 的組)進(jìn)行協(xié)調(diào)。
對于需要修改數(shù)據(jù)的 讀寫事務(wù)(read-write transactions) ,Spanner 沿用了經(jīng)典的 兩階段提交(Two-Phase Commit, 2PC) 協(xié)議,并將其構(gòu)建在 Paxos 之上,以保證原子性。
過程大致如下:
- 客戶端選擇一個 Paxos 組作為 事務(wù)協(xié)調(diào)者(Transaction Coordinator, TC) 。
- 所有要寫入的分片(參與者),首先會在其 Leader 副本上獲取 鎖(locks) 。
- 接著,參與者通過 Paxos 把“準(zhǔn)備”記錄(prepare record)寫入日志,這相當(dāng)于把鎖和要寫入的新值持久化并復(fù)制到了多數(shù)副本。
- 所有參與者都“準(zhǔn)備”好后,協(xié)調(diào)者會通過 Paxos 記錄一個最終的“提交”或“中止”決定。
- 最后,協(xié)調(diào)者通知所有參與者,參與者記錄最終決定并釋放鎖。
傳統(tǒng) 2PC 的一個巨大痛點是,如果協(xié)調(diào)者宕機(jī),所有參與者都會持有鎖并陷入阻塞狀態(tài),整個系統(tǒng)可能被卡住。Spanner 的巧妙之處在于, 協(xié)調(diào)者本身的狀態(tài)也是通過 Paxos 復(fù)制的 。這意味著即使協(xié)調(diào)者的 Leader 掛了,Paxos 組內(nèi)部會選出一個新的 Leader,它能從 log 中恢復(fù)狀態(tài),繼續(xù)推動事務(wù)完成,從而解決了 2PC 的阻塞問題。
當(dāng)然,這套流程涉及多次跨數(shù)據(jù)中心的網(wǎng)絡(luò)通信,所以讀寫事務(wù)的延遲并不低,在美國東西海岸之間完成一次事務(wù),延遲大約在 100 毫秒左右。
快速只讀事務(wù)的魔法:快照隔離與時間戳
F1 的工作負(fù)載中,絕大部分是 只讀事務(wù)(read-only transactions) 。為了極致的性能,Spanner 希望只讀事務(wù)能夠:
- 無鎖 :不使用鎖,避免和讀寫事務(wù)互相阻塞。
- 本地讀 :直接讀取本地數(shù)據(jù)中心的副本,避免跨數(shù)據(jù)中心通信。
這就帶來一個核心難題:本地副本的數(shù)據(jù)可能不是最新的,如何保證讀取到一致的數(shù)據(jù)呢?
Spanner 的答案是 快照隔離(Snapshot Isolation) 。簡單來說,就是給每個事務(wù)分配一個 時間戳(timestamp) 。讀寫事務(wù)在提交時獲得一個時間戳,其所有寫入操作都將關(guān)聯(lián)這個時間戳。只讀事務(wù)在開始時獲得一個時間戳,它只能看到所有時間戳早于它的事務(wù)的寫入結(jié)果,就像在那個時間點給整個數(shù)據(jù)庫拍了一張快照。
這樣一來,只讀事務(wù)就可以在不加鎖的情況下,讀到一份完整且一致的數(shù)據(jù)。
Spanner 的定海神針:TrueTime API
快照隔離解決了并發(fā)事務(wù)的視圖問題,但一個新的、更棘手的問題出現(xiàn)了: 時間 。分布式系統(tǒng)中的每臺機(jī)器都有自己的時鐘,它們不可能完美同步。如果時鐘不準(zhǔn),會發(fā)生什么?
- 如果只讀事務(wù)的時間戳 過大 ,它可能需要等待數(shù)據(jù)同步,導(dǎo)致延遲增加,但結(jié)果是對的。
- 如果只讀事務(wù)的時間戳 過小 ,它可能會讀到一份“舊”的快照,從而錯過一些實際上已經(jīng)提交的修改。這就違反了 Spanner 承諾的 外部一致性(external consistency) 。
這里,我們就要引出 Spanner 的核心創(chuàng)新,也是它的“定海神針”—— TrueTime 。
TrueTime API 并不返回一個精確的時間點,而是返回一個時間區(qū)間 TT.now() = [earliest, latest] 。它保證 真實的絕對時間 一定落在這個區(qū)間內(nèi)。這個區(qū)間的寬度(epsilon)代表了時鐘的不確定性,通常只有幾毫秒。Google 通過在每個數(shù)據(jù)中心部署帶有 GPS 接收器或原子鐘的時間主服務(wù)器(time master)來實現(xiàn)這一點。
有了 TrueTime ,Spanner 通過兩條規(guī)則來保證外部一致性:
- 時間戳分配 :為讀寫事務(wù)分配的提交時間戳
ts,是調(diào)用TT.now().latest的結(jié)果。 - 提交等待(Commit Wait) :一個讀寫事務(wù)在提交并釋放鎖之前,必須等待,直到
TT.now().earliest > ts。這個等待確保了當(dāng)事務(wù)的寫入結(jié)果對外部可見時,它的時間戳ts在真實時間中已經(jīng)成為過去時。
這個“提交等待”機(jī)制是關(guān)鍵。它保證了如果事務(wù) T1 在真實時間里先于 T2 完成,那么 T1 的時間戳一定小于 T2 的時間戳。這就保證了外部一致性。
Spanner 關(guān)于時間的假設(shè)與證明
Spanner 的天才之處在于,它并 不追求 完美的時鐘同步,而是 承認(rèn)并量化 時鐘的不確定性,并在此基礎(chǔ)上構(gòu)建出嚴(yán)格的數(shù)學(xué)保證。
TrueTime 的保證是什么?
TrueTime API 的核心承諾是:它返回的區(qū)間 [earliest, latest]一定包含 真實的絕對時間。我們不知道真實時間在區(qū)間的哪個點,但可以 100% 確定它就在這個區(qū)間內(nèi)。這個保證依賴于底層的物理設(shè)施—— GPS 和原子鐘的穩(wěn)定運行,以及對網(wǎng)絡(luò)延遲等不確定因素的精確計算。
可以假設(shè)時間主服務(wù)器(time master)一定準(zhǔn)確嗎?
在 Spanner 的模型中,我們必須 信任 這個時間體系能夠提供一個正確的時間 區(qū)間 。如果時間主服務(wù)器本身發(fā)生故障,給出了一個完全錯誤的、與真實時間毫無交集的區(qū)間,那么 Spanner 的外部一致性保證確實會被打破。這相當(dāng)于整個系統(tǒng)的“信任根基”被動搖了。因此,Google 在時間基礎(chǔ)設(shè)施的穩(wěn)定性和糾錯能力上投入了巨大努力,來確保這個基礎(chǔ)假設(shè)的成立。
Spanner 如何基于“不確定”的時間實現(xiàn)“確定”的一致性?
關(guān)鍵在于 提交等待(Commit Wait) 規(guī)則。Spanner 并沒有神奇地消除時間的不確定性,而是通過 等待 來抵消掉這份不確定性。
舉個例子:
- 一個讀寫事務(wù) T1 準(zhǔn)備提交,它獲得了一個時間戳
ts = TT.now().latest。 - 假設(shè)此刻的 TrueTime 區(qū)間是
[10:00:00.004, 10:00:00.006],那么ts = 10:00:00.006。 - Commit Wait 規(guī)則要求,T1 必須暫停,不能馬上讓其他事務(wù)看到它的寫入結(jié)果。它必須等到
TT.now().earliest越過10:00:00.006這個時間點。 - 比如,它一直等到 TrueTime 區(qū)間變?yōu)?nbsp;
[10:00:00.007, 10:00:00.009]時,因為007 > 006,等待結(jié)束,T1 的提交才真正對外可見。
通過這個等待,Spanner 保證了當(dāng) T1 的寫入結(jié)果可見時,它的時間戳 ts 在真實世界里 一定已經(jīng)成為了過去時 。任何在這之后開始的新事務(wù) T2,獲取到的時間戳一定會晚于 ts,從而保證了 T2 一定能看到 T1 的寫入,實現(xiàn)了外部一致性。
總結(jié)一下 :Spanner 并不是假設(shè)時鐘是完美的。它將時鐘的不確定性(epsilon)轉(zhuǎn)化為性能開銷(等待時間)。不確定性越大,Commit Wait 的時間就越長,性能就越差。但無論如何,只要 TrueTime 提供的區(qū)間是正確的,外部一致性的 正確性 就能得到保證。
外部一致性到底是什么?
我們剛才反復(fù)提到外部一致性。 它和我們常說的線性一致性、可串行化有什么關(guān)系呢?
外部一致性(External Consistency) 要求,如果事務(wù) T1 在真實時間中先于事務(wù) T2 開始執(zhí)行并完成,那么在數(shù)據(jù)庫的事務(wù)歷史中,T1 也必須排在 T2 前面。它的效果等同于將 線性一致性(linearizability) 的概念應(yīng)用于整個事務(wù),而不僅僅是單個操作。它也等同于 嚴(yán)格可串行化(strict serializability) ,即可串行化的順序必須與真實時間的發(fā)生順序一致。
那為什么外部一致性如此重要呢?
我們來看一個生動例子。假設(shè)你在圣何塞的數(shù)據(jù)中心,通過一個網(wǎng)頁服務(wù)修改了你們團(tuán)隊共享賬號的密碼。然后你馬上轉(zhuǎn)身,把新密碼告訴了坐在你旁邊的同事。你的同事立即在圣馬特奧的另一個數(shù)據(jù)中心嘗試用新密碼登錄。
如果沒有外部一致性,你的同事可能會連接到一個還沒同步到新密碼的數(shù)據(jù)副本上,導(dǎo)致登錄失敗,系統(tǒng)提示密碼錯誤。這顯然不符合用戶的直覺和預(yù)期。外部一致性則保證了你的同事一定能看到你剛剛完成的密碼修改,登錄成功。它保證了系統(tǒng)的行為和真實世界的時間順序是一致的。
Spanner 在真實世界中的應(yīng)用
最后,我們來回答一個大家可能關(guān)心的問題: 真的有人在用 Spanner 嗎?
答案是肯定的,而且是大規(guī)模使用。
- Google 內(nèi)部有數(shù)百個服務(wù)依賴 Spanner,包括我們前面提到的 F1 廣告系統(tǒng),以及 Zanzibar 授權(quán)系統(tǒng)等。
- 它作為 Cloud Spanner ,已經(jīng)成為 Google Cloud 平臺上的一項核心服務(wù),開放給外部客戶使用。
- 它的設(shè)計思想也深刻影響了業(yè)界,比如開源分布式數(shù)據(jù)庫 CockroachDB 就是基于 Spanner 的設(shè)計理念構(gòu)建的。
Spanner 的出現(xiàn),雄辯地證明了在全球范圍內(nèi)提供具有強(qiáng)一致性保證的分布式事務(wù)是切實可行的。它的核心思想,特別是 TrueTime 的設(shè)計,為構(gòu)建更強(qiáng)大的分布式系統(tǒng)開辟了新的道路。
























