Redis Cluster:為高效能付出了不安全的代價 - emil

banq發表於2021-08-12

本文旨在解釋為什麼 Redis 不適合用作 NoSQL 資料庫,其中持久化資料的永續性和一致性是必不可少的。
很難想到比 Redis 更廣為人知的資料儲存。在 Stack Overflow 上,它連續三年被評為最受歡迎的資料庫。它也是AWS 上最受歡迎的資料庫,勝過 MySQL 和 Postgres 之類的資料庫,以及亞馬遜專有的 NoSQL 產品。
澄清一下,這不是對 Redis 的攻擊。Redis 是出色的快取或輔助資料儲存,具有便捷的 NoSQL 功能、分片、複製和尾隨永續性。它在永續性方面放棄了的是用吞吐量和延遲彌補了。在正確的上下文中,Redis 是一個極好的技術選擇。
本次討論的背景是它用作高價值業務記錄的主要資料儲存,這些記錄受到嚴格的永續性要求,並且如果丟失或損壞則無法輕易恢復。

(省去分散式系統理論...點選標題見原文)
 
Redis 叢集屬於由領導者 - 追隨者架構支援的複製狀態機類別,在具有基於多數的仲裁系統的配置中包含多個程式。這是分散式系統文獻領域中眾所周知且經過實戰檢驗的拓撲。
根據我們宣告的永續性和一致性要求,系統需要滿足 Brewer CAP定理 的 CP(一致和分割槽容忍)標準— 換句話說,面對網路分割槽,它需要優先考慮資料一致性而不是可用性。我強烈懷疑大多數採用 Redis Cluster 並對永續性和一致性有嚴重期望的人自然會認為 Redis Cluster 是一個 CP 系統,而不是一個 AP 系統。也就是說,Redis 被認為是安全的,或者至少是足夠安全的。
 

安全定義
關於“足夠安全”的含義沒有普遍的共識,也不可能有統一的共識,因為安全要求是由每個組織獨特的商業驅動因素決定的。在這裡,我將嘗試制定一個較弱的安全定義,這對於相當多的應用程式可能是明智的。我從不同行業的眾多客戶的個人經驗中汲取了經驗,我的建議是大多數從業者都會做出的合理假設。有人可能會說,這種對安全性的削弱不夠客觀;我說,如果沒有某種基線,我們就無法繼續。
一個“足夠安全”的基於leader-follower的複製狀態機通常需要滿足以下標準:

  1. 領導者的單一性。最多有一個領導者可以在任何給定時間執行,並被配置中的仲裁副本(包括領導者)統一接受。前任被取代的領導者不能干涉當前領導者的行動。嚴格來說,安全屬性允許在瑣碎的情況下為零領導者;however, at some time there must be one elected leader for progress to eventually occur, thereby satisfying the liveness property.
  2. 複製功效。所有副本(包括領導者)都按照領導者規定的順序忠實地將領導者發出的更新應用於其狀態機。領導者執行的所有更新都會傳遞給法定數量的副本(包括領導者)。未由完整仲裁副本接受的寫入不得由主伺服器安裝或向啟動應用程式(客戶端)確認。此屬性與 CAP 定理的 CP 約束有關。
  3. 法定人數交集。根據我們之前的通訊,仲裁系統中的每個有效仲裁至少透過一個程式與其他仲裁相交。
  4. 一致的副本提升。在故障轉移期間,只有一致的副本可以爭奪領導權。其他人必須保持休眠狀態。一旦該副本被選中,它就會從領導者離開的地方繼續。注意,leader 上可能有未確認的寫入待處理;安全屬性不關心這些寫入;繼任領導人可以自行決定合併他們。
  5. 永續性有限的陳舊性。每個副本都附加到一個永續性儲存裝置上,該裝置可以在程式失敗後倖存下來。持久狀態反映了故障前程式的瞬態狀態,關於在某些商定的過時範圍內已被副本確認的寫入. 可以根據經過的時間或未提交記錄的數量來衡量有界過時。(後者更可取,因為它更準確地反映了最壞情況損失的大小。)在執行時,該程式不會丟棄其臨時儲存中的資料,直到持久儲存確認相關寫入。重新啟動過程可以確定最近提交的寫入的身份(偏移量、序列號等)。作為推論,重新啟動的過程絕不能自動承擔領導權,即使它在失敗之前碰巧成為領導者。

除了上述標準之外,我還提出了幾個假設來補充弱安全屬性:
  1. 仲裁系統旨在容忍f 次故障,其中f ≥ 1。換句話說,我們正在處理一個高可用性系統,其中單個程式的故障不會導致系統無法執行。實際上,對於基於多數的仲裁系統,我們至少需要三個程式。
  2. 程式不受可能的共模故障的影響。共模故障涉及影響多個獨立元素的單一原因;例如,電源故障或物理入侵事件。將伺服器分散在不同的機架或更好的資料中心之間,可以降低共模故障的可能性。
  3. 過程配備了*P故障檢測器。所有失敗的程式最終都會被所有活著的程式懷疑;最終不會懷疑所有實時程式。準確度-完整性二元性的精確調整雖然對活躍度具有實際意義,但不會影響安全性。
  4. 弱持久的永續性。保留的資料以商定的可能性被原始地召回,該可能性隨著時間而減少。可以以足夠高的可能性檢測到資料丟失和損壞。

上面概述的弱安全特性與其傳統(強)變體之間的差異集中在標準 5 和假設 4 中。它們放寬了通常的永續性標準,這些標準幾乎肯定會在更理論的環境中假設。通常,標準 5 類似於以下內容:
持久狀態準確地反映了故障前程式的瞬態狀態,相對於已被副本確認的寫入。
同樣,假設 4 應為:
保留的資料在未來的每個點都會被原始地呼叫。
我們在這裡做出兩個重要的讓步。首先,我們說確認的寫入不一定要提交到穩定儲存,只要這些寫入在某個“合理”的時間內被重新整理,並且我們可以在重新啟動時準確地檢測到最後一次提交是什麼。在實踐中,這可能不是一個硬界限。
 

分析Redis
首先,來自官方檔案的幾個條款作為序言。有些甚至是非常有趣的。至少,Redis 的所有使用者都應該非常熟悉他們,無論他們對 Redis 或本文的看法如何。在整個分析過程中,我們將透過它們的編號來引用它們。

  1. 透過開箱即用的預設設定,Redis 可作為半持久資料儲存執行,犧牲永續性以支援寫入的吞吐量和延遲。這在官方文件的Redis 持久化章節中有詳細說明。具體來說,AOF(僅附加檔案)的預設重新整理間隔會留下一秒的視窗,在該視窗內確認的寫入可能會不可挽回地丟失。這是appendfsync everysec選項,它是 AOF 配置中的預設選項和推薦選項。
  2. 持久化規範列出了appendfsync always選項,其中所有暫存的 AOF 寫入都受制於fsync()返回之前;然而,在這種模式下,Redis的效能在官方文件中被描述為“非常[原文如此]非常慢,非常安全”。因此,appendfsync everysec推薦該選項,“非常快且非常安全”。讀者會發現 Redis 文件似乎迴避了實證或定量的斷言,而傾向於使用諸如“非常”、“慢”、“快”等定性形容詞。
  3. 關於複製章節表明 Redis 在領導者-跟隨者(用它的說法是主從)拓撲中採用非同步複製協議,其中從伺服器重放主伺服器應用的寫入,而與主伺服器對發起客戶端的響應無關。在主站響應時,從站可能會任意落後於主站。
  4. 從 3.0.0 版本開始,Redis 包含WAIT命令。WAIT在客戶端上阻塞,直到所有先前的寫入在指定的超時內複製到使用者特定數量的副本,並在超時過去之前返回已確認的副本數量。表面上,WAIT把Redis的非同步複製機制變成了同步複製機制。
  5. WAIT文件指出“WAIT不會使Redis的強一致店:同時同步複製是一個複製的狀態機的一部分,它不是唯一需要的東西”。它繼續澄清“Sentinel 和 Redis Cluster 都將盡最大努力在可用副本集中提升最佳副本。然而 [原文如此] 這只是盡力而為的嘗試 [原文如此] 因此仍有可能丟失同步複製到多個副本的寫入”。在這裡,文件沒有明確說明“盡力而為”的含義。具體而言,維護者不會公開最佳副本提升失敗的條件。我們有責任解決這個問題。
  6. 該WAIT文件並未聲稱在主節點和/或副本重新整理暫存寫入之前命令會阻塞,僅宣告“命令已成功傳輸並確認”由指定的最小副本數。換句話說,該WAIT命令是對各個副本的瞬時狀態而非持久狀態的斷言。
  7. 複製章規定,“Redis的是不是一個CP系統,具有很強的一致性”和“承認寫仍然可以在故障轉移期間丟失”。它繼續說道,“WAIT在故障事件後丟失寫入的可能性大大降低到某些難以觸發的故障模式”,但沒有詳細說明導致資料丟失的特定故障模式,或量化這些事件發生的可能性。表面上,這指的是第 5 條,這是不完整的。它也可能指的是第 6 條。Redis 將細節保留為眾所周知的“讀者練習”。
  8. Redis官方叢集規範描述了“從屬等級”演算法,其中從屬根據複製資料量建立相對於其對等方的等級,並將領導者選舉延遲一個與其等級大致成比例的時間。(我們說“大致”是因為等待時間包含一個隨機延遲元件。)更完整(排名接近 0)的從屬被設定為在不完整之前投票(排名接近 N-1,其中 N 是配置的副本數)奴隸。
  9. 在同一部分,叢集規範指出“masters 不會以任何方式選擇最好的 slaves”。沒有機制可以確保後續的領導者在倖存的副本中排名最好。

基於對 Redis Cluster 文件和“光鮮亮麗的小冊子”宣告的隨意閱讀,更不用說它在行業中的多產採用水平,大多數人會天真地認為 Redis Cluster 的工作方式大致如下。
當足夠的程式和網路連結是可選的時,寫入從主(程式A)映象到法定數量的副本。(建立在我們之前示例中的三程式配置的基礎上。)所有副本都參與八卦協議——定期交換心跳——其中每個副本都知道其他人的存在和複製高水位線。(高水位標記是最近複製寫入的標識。)如果客戶端呼叫 ,則寫入不會被客戶端確認,直到它們暫時反映在大多數程式上WAIT。(沒有WAIT,客戶端會立即返回。)此外,Redis 程式為每個連續的檢視分配一個單調遞增的紀元編號,以便副本同意它們屬於哪個檢視。
在某些時候,主節點要麼發生故障,要麼與其餘節點隔離。這兩種情況實際上是等價的。結果是臨時停機,因為客戶端要麼無法將其請求路由到主伺服器,要麼主伺服器無法將寫入傳遞到仲裁中,因此它永遠不會響應。
副本最終會注意到主節點沒有心跳,並透過其本地*P檢測器推測其故障。兩個副本都啟動一個倒數計時器,該計時器大致與其相對等級成比例。(新增了隨機性元素。)
最新副本的計時器最快到期,並且程式B確保C的投票成為新的主節點。Redis 使用一條名為FAILOVER_AUTH_REQUEST從對等副本獲得投票的訊息。那些支援將請求者指定為擬議紀元的新領導者的人以FAILOVER_AUTH_ACK. 既乙和Ç遞增其元計數器,從而排除甲來自干擾。
一旦B在接下來的 epoch紀元 中被命名為新的主節點,客戶端應用程式最終會發現這個事實並將它們的請求路由到B。可能有寫入在A 中排隊——也許A還活著;無論如何,這無關緊要——這些寫入尚未得到確認,它們的損失對客戶來說應該無關緊要。到那時,任何進行中的請求都將路由到新的主節點。
請記住,B和C無法判斷A是否發生故障、是否被分割槽或響應緩慢。如果A還活著,它也會啟動自己的倒數計時;然而,它將無法獲得足夠的選票來形成法定人數。A可以稍後重新加入,但它必須先經過一個發現過程;如果A試圖承擔主控權,則紀元計數器會在合併時適當地阻止它。現在應該很明顯,view-epoch二重唱是至關重要的。沒有它,主人將獲得無條件的許可,可以與他們的前slaver從節點發生關係。
理論如何與現實疊加?
Redis 叢集在其預設配置中是不安全的。
 
....

建議
提供的唯一明智的指導是針對我們在此處考慮的各種工作負載強烈避免使用 Redis Cluster。如果系統未能證明我們弱化的安全概念,我們可能會透過降低系統暴露和易受攻擊的事件發生的可能性來使其更安全一些。因此,如果無情的約束會阻止您從 Redis 切換到更合適的永續性堆疊,那麼以下內容可能會有所啟發。
我們對髒讀無能為力;這是你需要忍受的東西。WAIT在那裡使用對您沒有幫助。
.. 

結論
Redis Cluster 在任何配置下都是不安全的,即使考慮到已經做出的所有讓步。在其預設配置中,Redis 的效能非常出色,但客觀上並不安全。當我們確定配置時,Redis 顯示出微小的改進,但即使是我們願意考慮的最寬鬆的安全屬性也無法滿足,而效能權衡變得明顯。就目前而言,可以這麼說,“在任何速度下”都是不安全的。
Redis 是針對特定工作的有效工具,並在其他關鍵領域做出了某些妥協。知道這些區域是什麼!作為磁碟支援的快取,Redis 已經是無與倫比的。它的一些資料結構是偉大的。但是,如果您追求可擴充套件的 NoSQL 資料庫,該資料庫提供一致性、永續性和某種程度的隔離(即,一個體面的安全概念),那麼您會在市場上找到客觀上更適合您需求的替代品。






 

相關文章