今天談談分布式事務的時序問題。在說這個問題之前首先說說這為什么是個問題。
單機場景
對於數據庫來說,讀到已經commit的數據是最基本的要求。一般來說,為了性能,讀寫不互相阻塞,現在的數據庫系統(Oracle,MySQL,OceanBase,Spanner,CockRoachDB,HBase)幾乎無一例外的使用MVCC技術來達到這個目的。說白了,就是數據有多個版本,每次寫產生新的更大的版本。讀事務可以指定某個版本讀,即快照讀,數據庫返回比指定的版本小的最大的版本的數據。當然也可以不指定,即讀最新的已經commit的版本的數據。從時序上來看,越后寫的數據,版本號越大,很顯然,這個版本號可以通過實現一個單機內單調遞增的counter來解決,counter從0開始以1遞增。但是這樣做,快照讀搞不定:查找2015年3月29日1點的最新數據。這是因為這個counter和時間沒有任何關系。那么顯然,時間戳作為版本號再適合不過了。在單機上,即使出現clock skew(即單機上先后兩次調gettimeofday取到的wall time,后面一次取到的wall time反而更小),維護一個單機內單調遞增的時間戳很容易辦到。可以看出,在單機情況下,滿足了Linearizability: T2在T1 commit成功后start,T2的commit timestamp一定大於T1的commit timestamp。下面看看多機的情況。
多機場景
在多機情況下,如何滿足Linearizability。
還是以寫事務T1(修改x),T2(修改y)為例,時序上T2在T1 commit之后start,由於不同的服務器的時鍾不一樣,有些快有些慢,導致T2可能拿到比T1更小的時間戳。
舉個例子:
假設機器M1的時鍾比M2的時鍾快30,T1事務在M1上提交,獲得commit timestamp 200,隨后T2事務在M2上開始並提交,由於M2時鍾更慢30,T2的commit timestamp可能是180。隨后來了一個讀事務T3,讀x和y,分配的讀版本號可能是190,結果他只能都到T2的值,不能讀到T1 !
問題的根源在於機器之間的時鍾不同,沒有全局時鍾。
Google的Spanner(看這 和 這)和Percolator(看這和這)都是搞了一個全局時鍾來解決,區別在於Percolator的全局時鍾就是基於固定的一台服務器產生,所有的事務獲取commit時間戳都問這個全局時鍾服務器要,自然保證了單調遞增。問題,顯而易見,單點,性能,擴展性。Spanner利用原子鍾和GPS接收器,實現了一個較為精確的時鍾,這個時鍾叫做TrueTime,每次調用TrueTime API返回的是一個時間區間,而不是一個具體的值,這個TrueTime保證的是真實時間(absolute time/real time)一定在這個區間內,這個區間范圍通常大約14ms,甚至更小。
下面說說Spanner是如何保證Linearizability(external consistency)。
事務的執行過程中,Spanner保證每個事務最后得到的commit timestamp介於這個事務的start和commit之間。基於這個條件,如果T2在T1 commit完成后才start,那么顯然,T2的commit timestamp肯定大於T1的timestamp。
Spanner是如何保證每個事務最后得到的commit timestamp介於這個事務的start和commit之間?
在事務開始階段調用一次TrueTime,返回[t-ε1,t1+ε1],在事務commit階段時再調用一次TrueTime,返回[t2-ε2,t2+ε2],根據TrueTime的定義,顯然,只要t1+ε1<t2-ε2,那么commit timestamp肯定位於start和commit之間。等待的時間大概為2ε,大約14ms左右。可以說,這個延時基本上還可以接受。
至於讀請求,直接調用TrueTime API,拿着右界去讀即可。
如果沒有TrueTime,怎么做到Linearizability
CockRoachDB是一個前Google員工創業的開源項目,基本上可以認為就是Spanner的開源實現。機器時鍾通過NTP同步,基本可以保證機器間誤差在150ms左右。
如果按照Spanner的做法,寫事務提交時每次都需要等待150ms,性能基本不可接受,當然CockRoachDB可以讓客戶端選擇是否使用這種方案,這種方法實現了Linearizability,可以性能太差,因為時鍾誤差太大,和Spanner的高精度時鍾沒法比。
CockRoachDB做了一點work around,同時實現了一種比Linearizability更relax一點的一致性模型,可以保證下面兩種情況的Linearizability。
單客戶端事務
CockRoachDB 實現了單個客戶端的Linearizability,保證同一個客戶端先后發出去的兩個事務T1和T2,T2的commit timestamp比T1的commit timestamp更大。方法就是T1事務執行完成會將commit timestamp返回給客戶端,客戶端執行T2時提供一個更大的時間戳給server,告訴server,T2的commit timestamp必須比這個時間戳更大。這樣就保證了單個客戶端的Linearizability。
相關事務
假設有兩個客戶端C1和C2,C1先執行寫事務T1,請求發送給了機器M1,其中需要修改x,T1 commit后,C2寫事務T2 start,請求發給了機器M2,事務也需要修改x,CockRoachDB可以保證T2分配到的commit timestamp比T1更大。
說這個之前,先看看如何界定兩個事件的先后順序。
通過捕捉兩個事件的因果關系可以給兩個事件定序,主要基於如下兩條規則:
- 如果事件e和事件f發生在同一個節點,並且事件e在事件f之前發生,那么e happened before f
- 如果發送消息m記作事件e,接收到消息m記作事件f,那么e happened before f(happened before后續記作hb)
Vector Clock可以用來維護這種因果關系,基本原理就是在一個有N個節點的集群中,每個機器都維護一個大小為N的數組(Vector),數組記作VC,機器i上的VC[k]代表機器i對機器k的clock的認知。每個機器i在發消息m時都會將本地的VC[i]加1(更新本地的clock),然后用它標記消息m,最后把消息發出。每個接收到消息的機器都會取自己的clock和消息中的clock的最大值來更新自己的clock(更新本地的clock)。這個clock實際上就是Logical Clock。Logical Clock越大說明這台機器的"時間"越靠后。在這個思想中,實際上,假設的是機器和機器之間的物理時鍾差是無窮大的,只要兩台機器之間沒有進行過消息交互,那么這兩台機器互相之間對對方沒有任何知識。那么,顯然,由於這種logical clock的實現和物理時間沒有任何關系,在真實的系統中,無法滿足快照讀:讀2015年3月29日之前1點的最新數據。
而Spanner的TrueTime API和上述方法是兩個極端,完全不捕捉事務之間的因果關系,純粹的根據TrueTime來對事件進行定序。
而CockRoachDB使用了Hybrid Logical Clock(HLC),它是另外一種Logical Clock的實現,它將Logical Clock和物理時鍾(wall time)聯系起來,並且他們之間的誤差在一個固定的值之內。這個值是由NTP決定的。每台機器更新HLC的算法和上面描述VC的過程大同小異。這種Logical Clock的實現非常簡單,這里就不展開,具體看這篇論文。實際上,HLC帶來了兩點好處:
- 使用HLC的系統可以支持快照讀。系統里的數據的版本號用HLC來標識,當接到時間戳為t的快照讀請求后,根據NTP誤差將t轉換成HLC,然后拿着這個HLC時間去系統中查即可。
- 捕捉了事務間的因果關系,從而實現了有因果關系事務之間的Linearizability。
回到這一小節最開始的例子:
假設有兩個客戶端C1和C2,C1先執行寫事務T1,請求發送給了機器M1,其中需要修改x,T1 commit后,C2寫事務T2 start,請求發給了機器M2,事務也需要修改x,CockRoachDB可以保證T2分配到的commit timestamp比T1更大。
那么只要分布式事務的coordinator在確定事務的commit timestamp的過程中詢問各個參與者participants的本地HLC,選取其中最大的HLC作為事務的commit timestamp,即可滿足Linearizability的要求。
可以看出,CockRoachDB實際上實現了兩種一致性級別,第一種就是Linearizability,實現方式和Spanner一樣,commit的時候都需要等(實際上,Spanner不是每次都要等,而CockRoachDB每次都需要等),但是由於其時鍾誤差很大,實際性能很差。第二種就是比Linearizability更寬松一點的一致性,這種一致性級別可以保證同一個客戶端的Linearizability和相關事務的Linearizability。
參考資料
Spanner: Google’s Globally-Distributed Database
Logical Physical Clocks
and Consistent Snapshots in Globally Distributed Databases
Beyond TrueTime: Using AugmentedTime for Improving Spanner
Spencer Kimball on CockroachDB, talk given at Yelp, 9/5/2014