最近非常關注的一件事情就是 Google Spanner Cloud 的發布,這應該算是 NewSQL 又一個里程碑的事件。NewSQL 的概念應該就是在 12 年 Google Spanner 以及 F1 的論文發表之后,才開始慢慢流行,然后就開始有企業嘗試根據 paper 做自己的 NewSQL,譬如國外的 CockroachDB 以及國內我們 PingCAP。
Spanner 的論文在很早就發布了,國內也有很多中文翻譯,這里筆者只是想聊聊自己對 Spanner 的理解,以及 Spanner 的一些關鍵技術的實現,以及跟我們自己的 TiDB 的相關對比。
CAP
在分布式領域,CAP 是一個完全繞不開的東西,大家應該早就非常熟悉,這里筆者只是簡單的再次說明一下:
- C:一致性,也就是通常說的線性一致性,假設在 T 時刻寫入了一個值,那么在 T 之后的讀取一定要能讀到這個最新的值。
- A:完全 100% 的可用性,也就是無論系統發生任何故障,都仍然能對外提供服務。
- P:網絡分區容忍性。
在分布式環境下面,P 是鐵定存在的,也就是只要我們有多臺機器,那么網絡隔離分區就一定不可避免,所以在設計系統的時候我們就要選擇到底是設計的是 AP 系統還是 CP 系統,但實際上,我們只要深入理解下 CAP,就會發現其實有時候系統設計上面沒必要這么糾結,主要表現在:
- 網絡分區出現的概率很低,所以我們沒必要去刻意去忽略 C 或者 A。多數時候,應該是一個 CA 系統。
- CAP 里面的 A 是 100% 的可用性,但實際上,我們只需要提供 high availability,也就是僅僅需要滿足 99.99% 或者 99.999% 等幾個 9 就可以了。
Spanner 是一個 CP + HA 系統,官方文檔說的可用性是優于 5 個 9 ,稍微小于 6 個 9,也就是說,Spanner 在系統出現了大的故障的情況下面,大概 31s+ 的時間就能夠恢復對外提供服務,這個時間是非常短暫的,遠遠比很多外部的系統更加穩定。然后鑒于 Google 強大的自建網絡,P 很少發生,所以 Spanner 可以算是一個 CA 系統。
TiDB 在設計的時候也是一個 CP + HA 系統,多數時候也是一個 CA 系統。如果出現了 P,也就是剛好對外服務的 leader 被隔離了,新 leader 大概需要 10s+ 以上的時間才能選舉出來對外提供服務。當然,我們現在還不敢說系統的可用性在 6 個 9 了,6 個 9 現在還是我們正在努力的目標。
當然,無論是 Spanner 還是 TiDB,當整個集群真的出現了災難性的事故,導致大多數節點都出現了問題,整個系統當然不可能服務了,當然這個概率是非常小的,我們可以通過增加更多的副本數來降低這個概率發生。據傳在一些關鍵數據上面,Spanner 都有 7 個副本。
TrueTime
最開始看到 Spanner 論文的時候,我是直接被 TrueTime 給驚艷到了,這特么的完全就是解決分布式系統時間問題的一個核彈呀(銀彈我可不敢說)。
在分布式系統里面,時間到底有多么重要呢?之前筆者也寫過一篇文章來聊過分布式系統的時間問題,簡單來說,我們需要有一套機制來保證相關事務之間的先后順序,如果事務 T1 在事務 T2 開始之前已經提交,那么 T2 一定能看到 T1 提交的數據。
也就是事務需要有一個遞增序列號,后開始的事務一定比前面開始的事務序列號要大。那么這跟時間又有啥關系呢,用一個全局序列號生成器不就行呢,為啥還要這么麻煩的搞一個 TrueTime 出來?筆者覺得有幾個原因:
- 全局序列號生成器是一個典型的單點,即使會做一些 failover 的處理,但它仍然是整個系統的一個瓶頸。同時也避免不了網絡開銷。但全局序列號的實現非常簡單,Google 之前的 Percolator 以及現在 TiDB 都是采用這種方式。
- 為什么要用時間?判斷兩個事件的先后順序,時間是一個非常直觀的度量方式,另外,如果用時間跟事件關聯,那么我們就能知道某一個時間點整個系統的 snapshot。在 TiDB 的用戶里面,一個非常典型的用法就是在游戲里面確認用戶是否謊報因為回檔丟失了數據,假設用戶說在某個時間點得到某個裝備,但后來又沒有了,我們就可以直接在那個特定的時間點查詢這個用戶的數據,從而知道是否真的有問題。
- 我們不光可以用時間來確定以前的 snapshot,同樣也可以用時間來約定集群會在未來達到某個狀態。這個典型的應用就是 shema change。雖然筆者不清楚 Spanner schema change 的實現,但 Google F1 有一篇Online, Asynchronous Schema Change in F1論文提到了相關的方法,而 TiDB 也是采用的這種實現方式。簡單來說,對于一個 schema change,通常都會分幾個階段來完成,如果集群某個節點在未來一個約定的時間沒達到這個狀態,這個節點就需要自殺下線,防止因為數據不一致損壞數據。
使用 TrueTime,Spanner 可以非常方便的實現筆者提到的用法,但 TureTime 也并不是萬能的:
- TureTime 需要依賴 atomic clock 和 GPS,這屬于硬件方案,而 Google 并沒有論文說明如果構造 TrueTime,對于其他用戶的實際并沒有太多參考意義。
- TureTime 也會有誤差范圍,雖然非常的小,在毫秒級別以下,所以我們需要等待一個最大的誤差時間,才能確保事務的相關順序。
Transaction
Spanner 默認將數據使用 range 的方式切分成不同的 splits,就跟 TiKV 里面 region 的概念比較類似。每一個 Split 都會有多個副本,分布在不同的 node 上面,各個副本之間使用 Paxos 協議保證數據的一致性。
Spanner 對外提供了 read-only transaction 和 read-write transaction 兩種事物,這里簡單的介紹一下,主要參考 Spanner 的白皮書。
Single Split Write
假設 client 要插入一行數據 Row 1,這種情況我們知道,這一行數據鐵定屬于一個 split,spanner 在這里使用了一個優化的 1PC 算法,流程如下:
- API Layer 首先找到 Row 1 屬于哪一個 split,譬如 Split 1。
- API Layer 將寫入請求發送給 Split 1 的 leader。
- Leader 開始一個事務。
- Leader 首先嘗試對于 Row 1 獲取一個 write lock,如果這時候有另外的 read-write transaction 已經對于這行數據上了一個 read lock,那么就會等待直到能獲取到 write lock。
- 這里需要注意的是,假設事務 1 先 lock a,然后 lock b,而事務 2 是先 lock b,在 lock a,這樣就會出現 dead lock 的情況。這里 Spanner 采用的是
wound-wait
的解決方式,新的事務會等待老的事務的 lock,而老的事務可能會直接 abort 掉新的事務已經占用的 lock。
- 這里需要注意的是,假設事務 1 先 lock a,然后 lock b,而事務 2 是先 lock b,在 lock a,這樣就會出現 dead lock 的情況。這里 Spanner 采用的是
- 當 lock 被成功獲取到之后,Leader 就使用 TrueTime 給當前事務綁定一個 timestamp。因為用 TrueTime,我們能夠保證這個 timestamp 一定大于之前已經提交的事務 timestamp,也就是我們一定能夠讀取到之前已經更新的數據。
- Leader 將這次事務和對應的 timestamp 復制給 Split 1 其他的副本,當大多數副本成功的將這個相關 Log 保存之后,我們就可以認為該事務已經提交(注意,這里還并沒有將這次改動 apply)。
- Leader 等待一段時間確保事務的 timestamp 有效(TrueTime 的誤差限制),然后告訴 client 事務的結果。這個
commit wait
機制能夠確保后面的 client 讀請求一定能讀到這次事務的改動。另外,因為commit wait
在等待的時候,Leader 同時也在處理上面的步驟 6,等待副本的回應,這兩個操作是并行的,所以commit wait
開銷很小。 - Leader 告訴 client 事務已經被提交,同時也可以順便返回這次事務的 timestamp。
- 在 Leader 返回結果給 client 的時候,這次事務的改動也在并行的被 apply 到狀態機里面。
- Leader 將事務的改動 apply 到狀態機,并且釋放 lock。
- Leader 同時通知其他的副本也 apply 事務的改動。
- 后續其他的事務需要等到這次事務的改動被 apply 之后,才能讀取到數據。對于 read-write 事務,因為要拿 read lock,所以必須等到之前的 write lock 釋放。而對于 read-only 事務,則需要比較 read-only 的 timestamp 是不是大于最后已經被成功 apply 的數據的 timestamp。
- Leader 將事務的改動 apply 到狀態機,并且釋放 lock。
TiDB 現在并沒有使用 1PC 的方式,但不排除未來也針對單個 region 的 read-write 事務,提供 1PC 的支持。
Multi Split Write
上面介紹了單個 Split 的 write 事務的實現流程,如果一個 read-write 事務要操作多個 Split 了,那么我們就只能使用 2PC 了。
假設一個事務需要在 Split 1 讀取數據 Row 1,同時將改動 Row 2,Row 3 分別寫到 Split 2,Split 3,流程如下:
- client 開始一個 read-write 事務。
- client 需要讀取 Row 1,告訴 API Layer 相關請求。
- API Layer 發現 Row 1 在 Split 1。
- API Layer 給 Split 1 的 Leader 發送一個 read request。
- Split1 的 Leader 嘗試將 Row 1 獲取一個 read lock。如果這行數據之前有 write lock,則會持續等待。如果之前已經有另一個事務上了一個 read lock,則不會等待。至于 deadlock,仍然采用上面的
wound-wait
處理方式。 - Leader 獲取到 Row 1 的數據并且返回。
- . Clients 開始發起一個 commit request,包括 Row 2,Row 3 的改動。所有的跟這個事務關聯的 Split 都變成參與者 participants。
- 一個 participant 成為協調者 coordinator,譬如這個 case 里面 Row 2 成為 coordinator。Coordinator 的作用是確保事務在所有 participants 上面要不提交成功,要不失敗。這些都是在 participants 和 coordinator 各自的 Split Leader 上面完成的。
- Participants 開始獲取 lock
- Split 2 對 Row 2 獲取 write lock。
- Split 3 對 Row 3 獲取 write lock。
- Split 1 確定仍然持有 Row 1 的 read lock。
- 每個 participant 的 Split Leader 將 lock 復制到其他 Split 副本,這樣就能保證即使節點掛了,lock 也仍然能被持有。
- 如果所有的 participants 告訴 coordinator lock 已經被持有,那么就可以提交事務了。coordinator 會使用這個時候的時間點作為這次事務的提交時間點。
- 如果某一個 participant 告訴 lock 不能被獲取,事務就被取消
- 如果所有 participants 和 coordinator 成功的獲取了 lock,Coordinator 決定提交這次事務,并使用 TrueTime 獲取一個 timestamp。這個 commit 決定,以及 Split 2 自己的 Row 2 的數據,都會復制到 Split 2 的大多數節點上面,復制成功之后,就可以認為這個事務已經被提交。
- Coordinator 將結果告訴其他的 participants,各個 participant 的 Leader 自己將改動復制到其他副本上面。
- 如果事務已經提交,coordinator 和所有的 participants 就 apply 實際的改動。
- Coordinator Leader 返回給 client 說事務已經提交成功,并且返回事務的 timestamp。當然為了保證數據的一致性,需要有
commit-wait
。
TiDB 也使用的是一個 2PC 方案,采用的是優化的類 Google Percolator 事務模型,沒有中心的 coordinator,全部是靠 client 自己去協調調度的。另外,TiDB 也沒有實現 wound-wait
,而是對一個事務需要操作的 key 順序排序,然后依次上 lock,來避免 deadlock。
Strong Read
上面說了在一個或者多個 Split 上面 read-write 事務的處理流程,這里在說說 read-only 的事務處理,相比 read-write,read-only 要簡單一點,這里以多個 Split 的 Strong read 為例。
假設我們要在 Split 1,Split 2 和 Split 3 上面讀取 Row 1,Row 2 和 Row 3。
- API Layer 發現 Row 1,Row 2,和 Row 3 在 Split1,Split 2 和 Split 3 上面。
- API Layer 通過 TrueTime 獲取一個 read timestamp(如果我們能夠接受 Stale Read 也可以直接選擇一個以前的 timestamp 去讀)。
- API Layer 將讀的請求發給 Split 1,Split 2 和 Split 3 的一些副本上面,這里有幾種情況:
- 多數情況下面,各個副本能通過內部狀態和 TureTime 知道自己有最新的數據,直接能提供 read。
- 如果一個副本不確定是否有最新的數據,就像 Leader 問一下最新提交的事務 timestamp 是啥,然后等到這個事務被 apply 了,就可以提供 read。
- 如果副本本來就是 Leader,因為 Leader 一定有最新的數據,所以直接提供 read。
- 各個副本的結果匯總然會返回給 client。
當然,Spanner 對于 Read 還有一些優化,如果我們要進行 stale read,并且這個 stale 的時間在 10s 之前,那么就可以直接在任何副本上面讀取,因為 Leader 會每隔 10s 將最新的 timestamp 更新到其他副本上面。
現在 TiDB 只能支持從 Leader 讀取數據,還沒有支持 follower read,這個功能已經實現,但還有一些優化需要進行,現階段并沒有發布。
TiDB 在 Leader 上面的讀大部分走的是 lease read,也就是只要 Leader 能夠確定自己仍然在 lease 有效范圍里面,就可以直接讀,如果不能確認,我們就會走 Raft 的 ReadIndex 機制,讓 Leader 跟其他節點進行 heartbeat 交互,確認自己仍然是 Leader 之后在進行讀操作。
小結
隨著 Spanner Cloud 的發布,我們這邊也會持續關注 Spanner Cloud 的進展,TiDB 的原始模型就是基于 Spanner + F1 搭建起來,隨著 Spanner Cloud 更多資料的公布,TiDB 也能有更多的參考。