概述
在單機數據庫領域,我們為每個事務都分配一個序列號,比如Oracle的SCN(SystemChangeNumber),MySQL的LSN(LogSequenceNumber),這個序列號可以是邏輯的,也可以是物理的。我們依賴這個序列號對系統中發生的事務進行排序,確保所有事務都有嚴格的先后關系。數據庫中所有的事務都按分配的序列號排序,對於任何時間點發生的讀,保證能讀到這個時間點之前的提交的事務,並且讀不到之后發生的事務。所以,一般來說,無論系統中序列號是邏輯還是物理的,都與真實的物理時間有一個對應的單獨遞增關系。在單機數據庫時代,這個相對容易做到,系統中有一個唯一的序列號分配器,保證有序。
來到分布式數據時代,一個數據庫系統不再只有一個節點可以處理事務,多個節點上發生的事務如何保證有序是本文想要討論的問題。最簡單的想法是,我們在數據庫系統中專門添加一個組件,這個組件作用就是分配時間戳,付出的代價是,任何一個事務提交,都需要有一個網絡RTT的消耗,並且分配時間戳的組件是整個系統的單點,可能成為系統的瓶頸。實際上,目前主流分布式數據庫系統還提供了兩個可選方案,一種是Spanner數據庫的TrueTime機制,另外一種CockroachDB數據庫的HLC(HybridLogicClock)機制。
HLC機制
先說說HLC的由來,我們前面提到分布式數據庫中,要為每個事務都分配一個合理的序列號比較麻煩,實際上這不單單是數據庫的問題,還是所有分布式系統中的共性問題,如何為系統中發生的事件排序。既然各個節點的物理時鍾不一致,不如都采用邏輯時鍾(LogicClock),邏輯時鍾只保證因果一致性,即不保證全局有序,只保證有先后順序的事件有序。哪些事件有先后關系,主要包括兩類,1.單節點內部先后發生的事件;2.節點間有通信的事件,發送消息的節點一定早於接收消息的節點。由於分配的序列號與物理時鍾完全無關,真實時間無法與序列號對應,導致無法用於實際的生產環境。HLC源於LC,是對LC的改進,同樣是保證因果序,但引入了物理時間戳作為序列號的一部分,這樣能與物理時鍾對應起來,采用物理時鍾+邏輯時鍾混合的方式作為序列號,提供一種事務序列號的分配方法。
接下里我們看看HLC是怎么實現的?HLC分配算法很簡單,源於[論文](https://cse.buffalo.edu/tech-reports/2014-04.pdf)。
l.j表示本地HLC,pt.j表示物理時間戳,c.j表示邏輯時間戳,對於發送事件或是本地事件,遞增邏輯時間戳部分,確保本地事件有序;對於接收事件節點,取本地時間戳和發送節點時間戳的最大值,如果物理時間戳部分相同,則遞增邏輯時間戳部分。整個邏輯是簡單清晰的,回歸到數據庫系統,則是事務提交時,如果是本地事務,則取本地HLC和本地物理時間的最大值,避免時鍾跳變;對於分布式事務,則選擇多個參與者節點中最大的HLC,並且推高各個節點的本地HLC,從而保證事務的序列號HLC一直都是單調遞增的。
在分布式數據庫領域引入HLC算法能為每個事務都分配一個合理序列號,並且保證有因果關系的事務通過序列號就能確定。在數據庫領域,怎么定義因果關系,對於單節點事務而言,如果事務嚴格時間先后發生順序(事務A提交后,事務B才開始),或者存在依賴關系(比如更新同一行),則認為存在因果關系;如果事務能並發執行提交,但不存在依賴關系,則不存在因果關系,事務序列號分配沒有約束。對於跨節點分布式事務而言,如果涉及到節點更新與其它事務有依賴關系,則存在因果序,否則沒有。數據庫系統引入HLC機制后,能夠保證有依賴關系的事務,分配的序列號也一定有先后順序。HLC由物理時鍾+邏輯時鍾兩部分組成,論文中建議48位作為物理時鍾位,16位作為邏輯時鍾位,那么提供的時鍾精度大約是15微妙,每個微妙的邏輯時鍾LC可以跳變65536次。
TrueTime機制
前面提到了TrueTime機制,在對比兩種分配序列號機制之前,先簡單介紹下TrueTime。我們之所以使用LC,HLC主要原因是我們各個節點的物理時鍾不一致,我們無法按單機思維來解決分布式系統問題。TrueTIme機制是通過在集群中引入原子鍾和GPS等硬件設備,來確保集群中各個節點的物理時鍾誤差在一定范圍內,配合特殊的事務commit-wait邏輯,保證全局的事務有序。
我們看一個問題,假設集群最大物理時鍾誤差是100,用戶先后執行了兩個事務A和B,分別在節點Node1和Node2上提交,Node1的物理時鍾比Node2物理時鍾快70,事務A的提交時間物理時間戳為120,記為t1;事務B的提交物理時間戳為55,記為t2,顯然從時間戳來看t1>t2,但是事務A卻先於事務B發生,這顯然與實際情況不符。由於事務A和B並沒有任何交集,按我們的定義,它們之間沒有因果關系,所以HLC機制不保證最終分配的時間戳HLC(A) < HLC(B)。
現在看看TrueTime機制如何給兩個事務排序。假設事務A提交時,通過TrueTime-API獲得的是一個時間戳是一個范圍,比如[t1-ε1,t1+ε1],事務B開始時,獲取的時間戳范圍是[t2-ε2,t2+ε2],由於事務A先於事務B發生,因此需要能保證t1+ε1 < t2-ε2,t2-t1 > ε1+ε2,那么顯然只要事務提交時,等待(ε1+ε2)時間,那么一定能滿足t1+ε1 < t2-ε2,也就是事務A提交時間戳<事務B提交的時間戳。實際上TrueTime機制保證的時間戳誤差最大在7ms,那么事務提交時,則必須等待大概14ms,才能保證事務有序。對於分布式事務,事務會跨多個節點,事務的時間戳會選取幾個節點中最大的時間戳。因此,實際上對於集群中任何一個節點無論是本地事務還是分布式事務,發生的先后順序都與時間戳順序一致,也就能保證所謂的外部一致性。TrueTime機制通過commit-wait的模式通過犧牲延遲,保證了全局順序。
TrueTime機制引入了特殊的硬件設備,外加commit-wait機制,通過犧牲一定的latency,來保證全局事務的順序。HLC機制對於寫請求則相對輕量,尤其是本地事務,沒有任何網絡交互,也不需要額外的時間等待,不同節點上有先后順序事務,實際上分配的序列號沒有嚴格的先后順序,只能保證因果序。
全局序VS因果序
如果采用專門的事務序列號分配組件服務,比如TSO(TimeStamp Oracle),顯然所有事務都是有序的,類似於單機數據庫;如果是TrueTime機制,對於任何一個時間戳,讀取的快照誤差都在指定的范圍(7ms以內),結合commit-wait,也能保證全局有序;而對於HLC機制,由於並沒有解決物理時鍾問題,要控制誤差可能需要借助NTP等服務,當然這個誤差可能在150ms或更多,不如原子鍾和GPS精確,如果采用commit-wait機制,事務延遲過大,就無法用於實際生產環境了,所以無法提供全局序。那么全局序和因果序對於讀有什么影響?對於快照讀,HLC機制如果只根據本地HLC時間戳生成,那么因為時鍾誤差,可能會漏掉部分已提交的事務;如果與所有節點通信並獲取HLC,那么至少當前的這個快照是能讀到所有已提交的事務的,但讀性能開銷就比較大了(與所有節點都有網絡交互)。當然,這個就看場景了,比如要建全局索引,通過與所有節點通信獲取HLC,實際上就相當於與所有節點都有因果關系,間接得到了一個全局一致性快照。這個快照點以前的事務都可以讀取到,這個點以后的事務都不讀到。顯然,全局序更貼合實際應用場景,但我們也可以具體問題具體分析,不能說只提供因果序的系統沒有價值。
總結
數據庫系統中,給事務分配合理的序列號,需要與實際發生的事務先后順序保證單調遞增關系。這個需求在單機數據庫時代很容易滿足,因為只有一個時鍾源。進入分布式數據庫時代,由於一個數據庫系統中有多個節點,每個節點都需要承擔分配事務序列號的任務,但天然的各個節點時鍾存在誤差,導致需要引入特殊的事務序列號分配機制,比如HLC機制,或者TrueTime機制。TrueTime機制本質就是硬件方案,將集群中節點間的物理時鍾誤差控制在很小的范圍內,結合commit-wait模式,保證所有事務全局有序,理論上要保證全局有序,TrueTime並非是充分條件,只需要commit-wait即可,就看wait的時間長短了。HLC機制則是軟件方案,只保證事務因果序,並且解決了本地時鍾跳變問題。HLC機制配合NTP服務,也能提供一定誤差范圍內的快照讀。
參考文檔
https://cse.buffalo.edu/tech-reports/2014-04.pdf