關於 Redis Sentinel 特性與失效情境的幾點論述
原文由 Salvatore Sanfilippo 于 發布,訂閱此部落格
昨天,分散式系統專家 Aphyr 發了一則推文,提到某家不願具名的公司所經歷的 Redis Sentinel 問題:
「OH 關於 Redis Sentinel:『他們對 master 執行了 kill -9,結果造成了腦裂……』
『然後舊的 master 在沒有任何資料的情況下又冒了出來,並把這種空無一物的狀態複寫到所有其他節點。真的是只能從備份還原。』」
OMG,我心想我們有個很棘手的 bug。不過我試著向 Kyle 詢問更多資訊,他回覆說使用者其實完全把 master 行程的磁碟持久化關掉了。沒錯:master 是被刻意設定成重啟後會以被清空的資料集來啟動。
猜猜怎麼著?推特上立刻掀起了一陣論戰。大家都為 Redis 使用者深感憂心。可憐的 Redis 使用者!總是身處險境。
然而,雖然感到憂心忡忡無疑是明智的表現,我想走另一條路:提供更多資訊。而且,這篇部落格文章之所以值得一寫,是因為其實 Kyle 在用有限的脈絡回報問題後,過了幾則推文,就在我看來,精準地指出了我認為 Redis Sentinel 真正可以改進的地方——這與前述事件無關,而且已經在我的待辦清單上很久了。
不過在此之前,讓我們更仔細地檢視一下在這起引發論戰的事件中,Redis / Sentinel 的行為。
歡迎來到當機復原系統模型
現實世界中大多數的分散式系統,都必須被設計成能夠承受行程隨時可能重啟的事實。請注意,這與被網路分割而隔離的問題非常不同,後者是指無法與其他行程交換訊息。相反地,這是一個關於遺失狀態的問題。
更精確地說,如果一個分散式演算法被設計成要求行程在重啟後必須保證保留狀態,卻未能做到,那麼從技術上來說,它就經歷了拜占庭失效:狀態已損毀,該行程不再可靠。
現在,在一個由 Redis 實例與 Redis Sentinel 實例所組成的分散式系統中,重啟後的實例能夠用舊的資料集重新啟動是至關重要的。以被清空的資料集啟動是一種拜占庭失效,而 Redis Sentinel 無法從這類問題中復原。
但讓我們先退一步。實際上,Redis Sentinel 可能根本沒有直接牽涉到這類事件中。典型的例子是,如果一個設定錯誤的 master 重啟得夠快,以至於 Sentinel 完全沒有偵測到任何失效,會發生什麼事。
- 節點 A 是 master。
- 節點 A 被重啟,且持久化功能已關閉。
- Sentinel(或許)會發現節點 A 無法連線……但還沒達到設定的逾時門檻。
- 節點 A 再次上線了,但它是以完全空的資料集重啟的。
- 所有 slave 節點 B、C、D……都會開心地從它那裡同步一份空的資料集。
畢竟,一切都按照設定從 master 被清空了。而一切也都從 slave 被清空了,因為它們正在從被認為是資料集當前真相來源的對象進行複寫。
讓我們把 Sentinel 從這個等式中移除,也就是上面時間軸中的第「3」點,因為在這個範例情境中 Sentinel 根本沒有採取任何行動。
結果就是這樣。你有一個正在與 N 個 slave 進行複寫的 Redis master。master 被重啟,並被設定為以一份全新(空白)的資料集啟動。slave 又再次從它(一份空的資料集)進行複寫。
我想這對 Redis 使用者來說應該不是什麼大新聞,這就是 Redis 複寫的運作方式:slave 永遠會試圖成為其 master 的完整複本。然而,讓我們來考慮其他模型。
例如,Redis 實例可以有一個持久化在 RDB / AOF 檔案中的 Node ID。每次節點重啟時,它都會載入自己的 Node ID。如果 Node ID 不符,slave 就完全不會從該 master 複寫。更安全了,對吧?其實也只安全一點點。master 可能有不同的設定錯誤,所以在重啟後,它可能會因為快照莫名失敗而載入一份已是數週前的舊資料集。
所以,在一次糟糕的重啟後,我們雖然仍有正確的 Node ID,但資料集卻舊到基本上跟被清空差不多,只是更難以察覺而已。
然而,以僅僅讓系統稍微安全一點為代價,我們現在得到的是個可能更難維運的系統,以及因為 ID 不符而無法從 master 複寫的 slave,起因是類似於關閉持久化那樣的操作失誤,只是比那更不明顯得多。
那麼,讓我們換個話題,來看看一個 Sentinel 真正有參與、而且可以改進的失效模式。
不是每個複本都一樣
從技術上來說,Redis Sentinel 只提供一組非常有限、簡單易懂的保證。
- 只要能夠互相通訊,所有的 Sentinel 就會對設定達成一致。實際上,每個子分割區內部也永遠會達成一致。
- 沒有取得多數 Sentinel 行程的授權,Sentinel 就無法啟動容錯移轉。
- 容錯移轉有嚴格的順序:如果一次容錯移轉在時間上較晚發生,它就會有更大的設定「編號」(用 Sentinel 的術語來說就是 config epoch),而這個編號永遠會勝過較舊的設定。
- 最終,Redis 實例會被設定為對應到勝出的邏輯設定(也就是具有較大 config epoch 的那一個)。
這意味著資料集的語意是「最後一次容錯移轉者勝」。然而,這裡缺少的資訊是,在容錯移轉期間,會挑選哪一個 slave 來取代 master?這終究是一個根本性的問題。舉例來說,如果 Redis Sentinel 挑了一個被清空的 slave(剛用錯誤的設定重啟過),那就是 Sentinel 的問題。Sentinel 應該確保,即使在 Redis 是非同步複寫系統的限制下,它仍會盡力以使用者的最佳利益為出發點,挑選身邊最好的 slave,並且在沒有任何可行的 slave 可連線時,乾脆拒絕進行容錯移轉。
這是一個可以改進的地方,而以下是目前在 master 失效時挑選 slave 的方式:
- 如果一個 slave 曾經重啟過,且在重啟後從未與 master 連線並成功完成同步(資料傳輸),就會被跳過。
- 如果 slave 與其 master 斷線的時間超過設定逾時的 10 倍(也就是一組 Sentinel 認定 master 失效所需的不可達時間),就會被視為不具備候選資格。
- 在剩下的 slave 中,Sentinel 會挑選具有最佳「replication offset」的節點。
replication offset 是一個 Redis master-slave 複寫用來計算經由複寫通道發送了多少位元組的數字。它在許多方面都很有用,不僅僅是用於容錯移轉。例如,在網路分割後的部份重新同步中,slave 會向 master 要求:從偏移量 X 開始給我資料,那是我收到的最後一個位元組,以此類推。
然而,這個複寫數字在用於挑選要晉升的最佳 slave 時,有兩個問題。
- 它會在重啟後被重置。這乍看之下似乎無害,因為我們想挑數字較大的 slave,而且無論如何,重啟後若 slave 無法連線就會被跳過。然而,它一點也不無害,請繼續往下看。
- 它就只是一個數字:它並不代表某個 Redis slave 是從特定的 master 複寫而來。
另外請注意,當一個 slave 被晉升為 master 時,它會繼承 master 的 replication offset。所以除了重啟的情況外,這個數字會持續遞增。
為什麼「1」與/或「2」是不理想的選擇,而且可以改進?
想像一下這個設定。我們有節點 A、B、C、D、E。D 是目前的 master,並與 E 一起被隔離在少數分割區中。E 仍在從 D 複寫,從它們的角度來看一切正常。
然而,在多數分割區中,A、B、C 可以互相交換訊息,且 A 被選為 master。
稍後 A 重啟,重置了它的 offset。B 和 C 從它進行複寫,又從較低的 offset 開始。
一段時間後 A 失效,同時 E 重新加入多數分割區。
E 擁有的資料集比 B 和 C 的資料集還舊,然而它的 replication offset 卻比較高。不僅如此,E 甚至還可以聲稱它最近曾與它的 master 保持連線。
要改進這點其實很簡單。每個 Redis 實例都有一個「runid」,這是一個在每次 Redis 執行時都會改變的唯一 ID。這在部份重新同步時很有用,可避免從錯誤的 master 取得增量串流。slave 應該要公布它們最後是從哪個 master runid 成功複寫而來,而 Sentinel 的容錯移轉則應該確保只挑選那些確實是從它所要接管的那個 master 複寫而來的 slave。
一旦你把 replication offset 與特定的 runid 綁定,你得到的就會是一個絕對的指標,能衡量一個 slave 有多新。如果有兩個 slave 可用,且兩者都能聲稱與舊的 master 保持連續性,那麼擁有較高 replication offset 的那一個,就保證是最佳的選擇。
然而,這在所有資料不是那麼重要、但可用性很重要的情況下,也會引發可用性的疑慮。舉例來說,如果當 A 當機時,只有 E 變得可用,即使它過去是從 D 複寫的,有也總比沒有好。我會說,當你需要一個高可用的快取而一致性不是大問題時,使用像 memcached 那樣的 Redis 叢集(在 N 個 master 之間做客戶端一致性雜湊)才是正途。
請注意,即使不檢查 runid,光是讓 replication offset 在重啟後仍保持持久,就已經能大幅改善行為。在上述範例中,E 只有在與 slave 一起被隔離在少數分割區期間,收到的寫入比多數那一側的其他 slave 還多時,才會被選中。
TLDR:我們得修掉這個問題。這與在沒有資料集的情況下重啟 master 無關,但擁有一個更正確的實作是有用的。不過,這只會限制一類非常難以觸發的問題。
這已經在我的待辦清單上好一段時間了,也恭喜 Aphyr 在短短幾則推文的交流中,就指出了這個真實存在的實作問題。至於 Aphyr 所回報、來自那家不具名公司的失效事件,我認為目前要試圖防範嚴重的設定錯誤是不可行的,不過這清楚地顯示我們需要更好的 Sentinel 文件——比起現在那些試圖描述系統如何運作的文件,應該更循序漸進才對。更明智的做法可能是從一個常見且健全的設定出發,並附上一份「不要做」清單,例如:除非你能接受實例被清空,否則不要關閉持久化。
隨機一篇部落格
留言
登入後參與討論