分佈式系統不可靠時鐘問題

今天我同樣結合《設計數據密集系統》來聊分佈式系統另外一個話題, 即分佈式系統的不可靠時鐘問題.

同步網絡與異步網絡時鐘對比

在前面我們講述了分佈式系統網絡不可靠的原因是由於我們數據中心服務通信採用的是異步網絡實現, 同樣地我們來看爲什麼同步與異步網絡在時鐘上也存在差異呢? 對此我們可以先看下面同步與異步網絡在時鐘控制上的差異:

通過上述我們可以清晰地知道:

對此我們對同步網絡以及異步網絡的時鐘精度做一個對比總結如下:

單調時鐘以及日時鐘的不可靠性

通過時鐘可以來反映我們系統想要表達的時間問題, 一般在我們應用程序會拆分兩類層次的含義:

持續時間: 描述的是一個事件發生的開始到結束整個過程經歷的時間

比如一個請求的 p99 耗時是多少, 這個就是我們需要依賴時鐘去測量應用程序請求開始到結束的持續時間.

時間點: 指事件發生的時候對應的那一時刻

比如我們的應用程序發生 NullPointerException 異常的時候日誌系統對應的時間戳, 這個就是時間點.

在現代計算機系統中時鐘又拆分爲單調時鐘以及日時鐘. 即:

日時鐘: 它根據某種日曆返回當前的日期和時間, 也稱爲我們牆上的時鐘時間, 比如 Java 的 System.currentTimeMillis(), 用於表示時間點. 日時鐘通常與網絡時間協議(NTP)同步,這意味着一臺機器上的時間戳(理想情況下)與另一臺機器上的時間戳含義相同. 然而由於存在硬件時鐘與 NTP 協議的不穩定, 不適合用於測量持續時間.

通過以上描述, 我們可以依賴日時鐘來描述我們事件發生的時間點. 但是不適合測量持續時間, 爲什麼? 我們先來看下單調時鐘: 

單調時鐘: 主要用於測量持續時間, 比如超時時間或者服務響應時間, 比如 Java 的 System.nanoTime() 就是單調時鐘. 需要注意一點就是單調時鐘僅在單機單進程下計算對應的差異才有意義, 否則沒有實際比較性, 因爲該時鐘可能是計算機啓動以來的納秒數,或者是類似的任意數值開始統計. 由於單調時鐘不需要與 NTP 同步, 但如果 NTP 發現本地石英晶體振盪器比 NTP 走得快或者慢, 就會調整單調時鐘向前走的頻率, 即時鐘微調.

我們可以看到單調時鐘始終是被保證向前移動, 因此在分佈式系統中單進程內計算持續時間可以採用單調時鐘而非日時鐘. 這是因爲日時鐘存在同步以及準確性問題如下:

因此在分佈式系統中我們設計應用程序也需要充分考慮到日時鐘存在不可靠的問題.

分佈式系統依賴時鐘帶來的問題

依賴時間戳進行事件排序導致亂序

如果兩個客戶端向一個分佈式數據庫寫入數據, 那麼誰先到達? 如果以最後寫入爲準機制 (LWW) 的話會發生什麼問題呢? 如下所示 (來自《設計數據密集系統》):

可以看到上述的時鐘在不同節點的差異, 我們可以看下上述的邏輯處理過程:

在上述的例子中由於 Node3 節點與 Node1 節點存在時鐘相差的間隔, 導致 Node1 先寫入的時間戳要大於 Node3 節點寫入的時間戳, 從而導致 Node2 節點接收到其他節點的複製命令時候, 由於併發寫衝突的存在且採用 LWW 機制最終導致 set x = 2 的命令被丟棄導致數據丟失.

那麼有什麼解決方案嗎? 這裏我們會用到一個概念稱爲邏輯時鐘, 即基於全局遞增計數器而非振盪的石英晶體, 同時在分佈式數據庫環境中, 要保證是全局性遞增, 邏輯時鐘不測量具體日期時間或者秒數, 僅測量事件的相對順序, 即事件發生在另一事件之前還是之後, 這樣對於 LWW 機制解決併發寫衝突是一種更爲安全的選擇.

而上述的單調時鐘或者是日時鐘我們稱爲物理時鐘. 但是邏輯時鐘依賴因果關係實現並需要具備持久化等機制防止丟失, 在實現上更爲複雜.

STW 引發數據寫入安全問題

假如現在有一個數據庫, 並且每個數據庫分區僅有一個主節點能夠接受寫入操作, 同時數據庫的主節點與其他副本節點通過租約實現共識, 也就是 Master 節點持有一份從其他節點獲取帶有過期時間的租約, 並在在租約的有效期內是能夠處理外部的寫入請求並將命令複製到其他節點實現同步直到租約過期, 如下:

每個 Node 節點對應的僞代碼如下:

上述代碼存在什麼問題? 主要有兩個方面:

因此在分佈式系統中我們必須要假定其中一個節點在執行過程中任何時刻都可能存在被暫停一段時間, 甚至是在函數執行的中間也不例外, 只有這樣的假定基礎上去設計我們構建的分佈式系統才能確保我們數據的可靠性以及完整性.

全局快照的同步時鐘

在上述我們提到使用全局單調遞增的計算器來保證我們執行事務前後的順序性, 也就是說我們的數據庫是分佈在多臺機器上甚至是多數據中心分佈, 那麼我們要實現一個全局的、單調遞增的計數器就會變得很困難, 因爲多數據中心存在跨區問題, 需要進行協調. 一般地我們要麼會增加同步互斥鎖機制, 類似數據庫的悲觀鎖僅允許一個操作後再進行下一個操作, 但是這樣會嚴重影響性能.

爲了提升性能併兼顧對外部提供讀取操作, 在我們數據庫層面中會引入一個快照隔離機制, 對於那些支持小型、快速的讀寫事務又能支持大型、長時間運行的只讀事務的數據庫而言是一項非常有用的功能, 但是也存在同樣的問題, 數據分佈在多臺機器怎麼辦呢? 快照隔離和我們處理事件的順序性有共同之處: 事務 B 如果能夠讀取事務 A 的數據, 那麼事務 B 的 ID 必須要高於事務 A, 否則快照就是不一致的.

目前在業界中, 谷歌雲 Spanner 就是基於日時鐘作爲事務 ID 實現多數據中心的快照隔離, 爲什麼它能夠利用日時鐘實現呢? 它主要採用 TrueTime API 明確報告本地時鐘的置信區間 (所謂的時鐘置信區間, 我們可以理解爲它是一個日時鐘的時間段而不是一個時間點, 即 [最早時間, 最晚時間]), 並基於以下觀察結果:

我們對比下邏輯時鐘實現以及 TrueTime 的不同維度:

分佈式系統問題小結

最後我對前面闡述的分佈式系統問題做一個總結如下, 即我們在搭建一個分佈式系統時需要建立對應的系統模型, 思考在面臨組件 / 服務故障、網絡延遲、時鐘依賴以及節點暫停等情況下要如何進行設計與取捨來保證分佈式系統的數據可靠性.

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/yWDDonbXrdHcLQiMT36dLQ