分佈式系統不可靠時鐘問題
今天我同樣結合《設計數據密集系統》來聊分佈式系統另外一個話題, 即分佈式系統的不可靠時鐘問題.
同步網絡與異步網絡時鐘對比
在前面我們講述了分佈式系統網絡不可靠的原因是由於我們數據中心服務通信採用的是異步網絡實現, 同樣地我們來看爲什麼同步與異步網絡在時鐘上也存在差異呢? 對此我們可以先看下面同步與異步網絡在時鐘控制上的差異:
通過上述我們可以清晰地知道:
-
同步網絡存在全局時鐘控制, 異步網絡中每個節點是擁有自己的時鐘獨立運行, 並通過 NTP 網絡協議來協調糾正時鐘不同步問題, 但存在網絡延遲問題.
-
同步網絡發送消息需要先同步脈衝後再傳遞數據信息, 而異步網絡是通過開始以及結束位標誌進行數據同步保證數據的完整性.
對此我們對同步網絡以及異步網絡的時鐘精度做一個對比總結如下:
單調時鐘以及日時鐘的不可靠性
通過時鐘可以來反映我們系統想要表達的時間問題, 一般在我們應用程序會拆分兩類層次的含義:
持續時間: 描述的是一個事件發生的開始到結束整個過程經歷的時間
比如一個請求的 p99 耗時是多少, 這個就是我們需要依賴時鐘去測量應用程序請求開始到結束的持續時間.
時間點: 指事件發生的時候對應的那一時刻
比如我們的應用程序發生 NullPointerException 異常的時候日誌系統對應的時間戳, 這個就是時間點.
在現代計算機系統中時鐘又拆分爲單調時鐘以及日時鐘. 即:
日時鐘: 它根據某種日曆返回當前的日期和時間, 也稱爲我們牆上的時鐘時間, 比如 Java 的 System.currentTimeMillis(), 用於表示時間點. 日時鐘通常與網絡時間協議(NTP)同步,這意味着一臺機器上的時間戳(理想情況下)與另一臺機器上的時間戳含義相同. 然而由於存在硬件時鐘與 NTP 協議的不穩定, 不適合用於測量持續時間.
通過以上描述, 我們可以依賴日時鐘來描述我們事件發生的時間點. 但是不適合測量持續時間, 爲什麼? 我們先來看下單調時鐘:
單調時鐘: 主要用於測量持續時間, 比如超時時間或者服務響應時間, 比如 Java 的 System.nanoTime() 就是單調時鐘. 需要注意一點就是單調時鐘僅在單機單進程下計算對應的差異才有意義, 否則沒有實際比較性, 因爲該時鐘可能是計算機啓動以來的納秒數,或者是類似的任意數值開始統計. 由於單調時鐘不需要與 NTP 同步, 但如果 NTP 發現本地石英晶體振盪器比 NTP 走得快或者慢, 就會調整單調時鐘向前走的頻率, 即時鐘微調.
我們可以看到單調時鐘始終是被保證向前移動, 因此在分佈式系統中單進程內計算持續時間可以採用單調時鐘而非日時鐘. 這是因爲日時鐘存在同步以及準確性問題如下:
-
計算機的石英鐘存在漂移導致時間不精準, 可能存在向後跳躍;
-
本地時鐘與 NTP 服務的時間差較大拒絕同步會被重置;
-
NTP 服務配置錯誤導致;
-
NTP 同步到機器節點存在網絡無界延遲問題.
因此在分佈式系統中我們設計應用程序也需要充分考慮到日時鐘存在不可靠的問題.
分佈式系統依賴時鐘帶來的問題
依賴時間戳進行事件排序導致亂序
如果兩個客戶端向一個分佈式數據庫寫入數據, 那麼誰先到達? 如果以最後寫入爲準機制 (LWW) 的話會發生什麼問題呢? 如下所示 (來自《設計數據密集系統》):
可以看到上述的時鐘在不同節點的差異, 我們可以看下上述的邏輯處理過程:
-
首先 ClientA 在 42.004 秒時刻發起一個
set x = 1
的寫請求到 Node1 節點, 這個時候 Node1 分別向副本節點 Node2、Node3 也分別發起命令複製的寫操作請求; -
其次 ClientB 在 42.003 秒時刻也發起一個
set x = 2
的寫請求到 Node3 節點, 同樣地 Node3 節點也分別向 Node1、Node2 節點發起命令複製的寫請求; -
我們可以看到 Node2 節點接收到命令
set x = 1
對應的時間戳是 42.004 秒, 而命令set x = 2
對應的時間戳是 42.003 秒, 如果我們採用 LWW 機制, 即直接採用最新的時間戳的方式進行作爲解決併發寫衝突的機制, 那麼 Node2 節點就會丟棄掉set x = 2
的命令, 這個時候在數據庫節點 Node2 上就會神祕發現數據丟失了.
在上述的例子中由於 Node3 節點與 Node1 節點存在時鐘相差的間隔, 導致 Node1 先寫入的時間戳要大於 Node3 節點寫入的時間戳, 從而導致 Node2 節點接收到其他節點的複製命令時候, 由於併發寫衝突的存在且採用 LWW 機制最終導致 set x = 2
的命令被丟棄導致數據丟失.
那麼有什麼解決方案嗎? 這裏我們會用到一個概念稱爲邏輯時鐘, 即基於全局遞增計數器而非振盪的石英晶體, 同時在分佈式數據庫環境中, 要保證是全局性遞增, 邏輯時鐘不測量具體日期時間或者秒數, 僅測量事件的相對順序, 即事件發生在另一事件之前還是之後, 這樣對於 LWW 機制解決併發寫衝突是一種更爲安全的選擇.
而上述的單調時鐘或者是日時鐘我們稱爲物理時鐘. 但是邏輯時鐘依賴因果關係實現並需要具備持久化等機制防止丟失, 在實現上更爲複雜.
STW 引發數據寫入安全問題
假如現在有一個數據庫, 並且每個數據庫分區僅有一個主節點能夠接受寫入操作, 同時數據庫的主節點與其他副本節點通過租約實現共識, 也就是 Master 節點持有一份從其他節點獲取帶有過期時間的租約, 並在在租約的有效期內是能夠處理外部的寫入請求並將命令複製到其他節點實現同步直到租約過期, 如下:
每個 Node 節點對應的僞代碼如下:
上述代碼存在什麼問題? 主要有兩個方面:
-
判斷過期時間依賴外部節點的時間戳, 前文我們講到日時鐘存在不可靠, 如果時鐘不同步超過幾秒, 即本沒有到期但卻判斷到期導致重新發起續約請求重新選舉 master 節點, 在選舉過程中對外服務不可用直接丟棄寫請求; 如果時鐘後退幾秒鐘, 那麼也將導致其他節點重新發起 master 選舉, 如果是在跨 region 網絡分區的數據分佈下, 那麼將會出現兩份同時具備租約的數據節點, 即腦裂問題.
-
如果我們將上述更改爲本地協議爲單調時鐘呢? 其實也存在問題, 比如進程內部發生 GC / 代碼執行到某個 SafePoint / 進行刷盤 fsync 等操作導致進程暫停, 這個時候由於其他副本節點就會存在誤判 master 節點不可用, 導致重新選舉然後接受 client 的寫操作, 這個時候就會發生數據不一致甚至被損壞的問題.
因此在分佈式系統中我們必須要假定其中一個節點在執行過程中任何時刻都可能存在被暫停一段時間, 甚至是在函數執行的中間也不例外, 只有這樣的假定基礎上去設計我們構建的分佈式系統才能確保我們數據的可靠性以及完整性.
全局快照的同步時鐘
在上述我們提到使用全局單調遞增的計算器來保證我們執行事務前後的順序性, 也就是說我們的數據庫是分佈在多臺機器上甚至是多數據中心分佈, 那麼我們要實現一個全局的、單調遞增的計數器就會變得很困難, 因爲多數據中心存在跨區問題, 需要進行協調. 一般地我們要麼會增加同步互斥鎖機制, 類似數據庫的悲觀鎖僅允許一個操作後再進行下一個操作, 但是這樣會嚴重影響性能.
爲了提升性能併兼顧對外部提供讀取操作, 在我們數據庫層面中會引入一個快照隔離機制, 對於那些支持小型、快速的讀寫事務又能支持大型、長時間運行的只讀事務的數據庫而言是一項非常有用的功能, 但是也存在同樣的問題, 數據分佈在多臺機器怎麼辦呢? 快照隔離和我們處理事件的順序性有共同之處: 事務 B 如果能夠讀取事務 A 的數據, 那麼事務 B 的 ID 必須要高於事務 A, 否則快照就是不一致的.
目前在業界中, 谷歌雲 Spanner 就是基於日時鐘作爲事務 ID 實現多數據中心的快照隔離, 爲什麼它能夠利用日時鐘實現呢? 它主要採用 TrueTime API 明確報告本地時鐘的置信區間 (所謂的時鐘置信區間, 我們可以理解爲它是一個日時鐘的時間段而不是一個時間點, 即 [最早時間, 最晚時間]), 並基於以下觀察結果:
-
假設現在有兩個提交事務 ID 對應的時間戳 a 以及 b, 同樣也會對應 A 以及 B 兩個時鐘的置信區間 A 和 B;
-
如果 A 與 B 區間不存在重疊且 A 的最晚時間 <B 的最早時間, 那麼 A 一定是在 B 之前; 如果 A 的最早時間> B 的最晚時間, 那麼 B 一定是在 A 之前;
-
當 A 與 B 發生重疊的時候, 爲反映 A 與 B 發生前後的因果關係, Spanner 通過在提交讀寫事務的時候特意等待一段置信區間長度的時間, 爲了儘可能縮短等待時間, Spanner 需要儘可能減小時鐘的不確定性; 爲此谷歌在每個數據中心都部署一個全球定位系統(GPS)接收器或原子鐘, 使得時鐘能夠同步到誤差在大約 7ms 以內. 通過這樣操作, 它確保任何可能讀取該數據的事務都處於足夠晚的時間.
我們對比下邏輯時鐘實現以及 TrueTime 的不同維度:
分佈式系統問題小結
最後我對前面闡述的分佈式系統問題做一個總結如下, 即我們在搭建一個分佈式系統時需要建立對應的系統模型, 思考在面臨組件 / 服務故障、網絡延遲、時鐘依賴以及節點暫停等情況下要如何進行設計與取捨來保證分佈式系統的數據可靠性.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yWDDonbXrdHcLQiMT36dLQ