如何應對Akka叢集出現腦裂故障?- Andrzej

banq發表於2020-05-27

Akka Cluster是一款非常不錯的軟體。如果正確使用並用於正確的用例,它可以解決可擴充套件的分散式系統世界中的許多難題。它可以為您提供一種分散式共識機制,在此基礎上,您可以實現Akka Persistence(事件溯源庫)所必需的分散式Single Writer Principle(單寫原則),尤其是對於像Cassandra、DynamoDB等這樣的分散式事件儲存。

Akka叢集的唯一問題是網路分割槽,主機無響應可能導致腦裂情況。在這種情況下,您將失去對單寫原則的所有保證,並且事件日誌可能已損壞。兩個(或多個)永續性參與者將同時為同一聚合產生事件。

正常事件是一連串的事件(對於給定的總XYZ),針對相同的節點單寫著UUID,用嚴格的無間隙的單調遞增的序列號,您將獲得類似於以下內容的資訊:

 persistence_id | sequence_nr | writer_uuid
----------------+-------------+--------------------------------------
        XYZ     |    49489583 | dde98298-7aae-4cce-a1a7-7cea478dfb52
        XYZ     |    49489584 | dde98298-7aae-4cce-a1a7-7cea478dfb52
        XYZ     |    49489584 | 69572683-ddf8-43d0-b4ff-bab8ee466d24
        XYZ     |    49489585 | dde98298-7aae-4cce-a1a7-7cea478dfb52
        XYZ     |    49489585 | 69572683-ddf8-43d0-b4ff-bab8ee466d24
        XYZ     |    49489586 | dde98298-7aae-4cce-a1a7-7cea478dfb52
        XYZ     |    49489586 | 69572683-ddf8-43d0-b4ff-bab8ee466d24
        XYZ     |    49489587 | 69572683-ddf8-43d0-b4ff-bab8ee466d24
        XYZ     |    49489588 | 69572683-ddf8-43d0-b4ff-bab8ee466d24

序列號為49489584、49489585、49489586的事件不應該是重複的,它們是完全不同的事件,由不同的寫入者產生(請參閱writer_uuid)。這樣,您的聚合將不再一致。

裂腦解析器

針對裂腦的第一道防線是裂腦解析器策略。一種演算法,可幫助節點確定當前叢集狀態和成員資格。最常見的策略是:

  1. 靜態定額:只要存活的節點數> =靜態定額引數,大多數節點都將生存。這種策略通常非常適合固定大小的群集。
  2. 保持多數:在群集大小為動態且靜態仲裁超出選項的情況下很有用。
  3. 保持最舊:當叢集大小是動態的並且您正在使用Cluster Singletons時很有用。
  4. 保持引用:如果一個節點處理一些關鍵資源並且如果沒有它,群集將無法執行,這是一個有趣的選擇。

有一些相當不錯的開源解決方案,例如akka-reasonable-downinglithium。就在最近,Lightbend決定將其裂腦解析器整合到Akka Cluster中,並使其開源。就我而言,我選擇了靜態仲裁策略,因此第一個庫就足夠了。將來,我可能會遷移到內建選項。

不幸的是,最近出現的生產問題向我證明,對於Akka Cluster中的裂腦,沒有裂腦解析器是防彈解決方案。

如何模擬裂腦?

這是裂腦模擬的祕訣:

  1. 使用迴圈負載均衡器在Akka群集上(至少3個節點)施加一些(寫入)負載
  2. 選擇一個節點,並暫停冷凍它,例如:docker pause命令
  3. 15秒後解凍節點*: docker unpause
  4. 檢查日誌或日記的一致性

在應用程式日誌中,您可能會注意到以下訊息:

2020–05–01T12:47:56 — Scheduled sending of heartbeat was delayed. Previous heartbeat was sent [15456] ms ago, expected interval is [1000] ms. This may cause failure detection to mark members as unreachable. The reason can be thread starvation, e.g. by running blocking tasks on the default dispatcher, CPU overload, or GC.

之後不久出現:

2020–05–01T12:48:03.291 — Marking node [akka://as@1.2.3.4:29292] as [Down]

取消凍結節點後,該節點決定殺死自己需要7秒鐘。在此期間,它將處理命令並執行寫操作。同時,另外兩個節點決定1.2.3.4不響應,因此應將其從群集中刪除,並且其所有職責(分片,持久參與者等)都應由它們處理。失去了單一作家原則,我們只是創造了一個裂腦。在某個時候,應該啟動一個新例項來替換髮生故障的節點,並且您可能會看到akka.persistence.journal.ReplayFilter類似以下內容的日誌訊息:

Invalid replayed event [sequenceNr=49489584, writerUUID=69572683-ddf8–43d0-b4ff-bab8ee466d24] from a new writer. An older writer already sent an event [sequenceNr=49489584, writerUUID=dde98298–7aae-4cce-a1a7–7cea478dfb52] whose sequence number was equal or greater for the same persistenceId [XYZ]. Perhaps, the new writer journaled the event out of sequence, or duplicate persistenceId for different entities?

或者:

There was already a newer writer whose last replayed event was [sequenceNr=49489584, writerUUID=69572683-ddf8–43d0-b4ff-bab8ee466d24] for the same persistenceId [XYZ].Perhaps, the old writer kept journaling messages after the new writer created, or duplicate persistenceId for different entities?

實際上,您應該很高興看到它們。我會在稍後解釋。

為什麼是7秒,為什麼是15秒?對於小型群集stable-after,裂腦解析器策略的引數預設值為7秒。15秒正好足以引起大腦分裂。對於更長的stable-after時間,您將需要凍結該節點大約比2 * stable-after獲得相同行為更多的時間。

什麼時候會發生腦裂?

上面的log語句中已經提到了一些現實生活中的情況:

  1. 執行緒飢餓,例如通過在預設排程程式上執行阻止任務,
  2. CPU過載
  3. 垃圾收集(GC)。

在我看來,這不是排程程式,也不是GC。CPU負載達到極限,因為在共享環境中部署的某些其他應用程式存在故障。Docker限制無濟於事,因為很難限制磁碟IO操作。由於某些惡意攻擊,CPU可能也會凍結。

如何在裂腦中生存?

您可以做的第一件事是微不足道的。您可以增加stabe-after 期限。這將使您有更多時間解凍應用程式並從情況中恢復。當然,這不會消除問題。此外,您不能將這段時間過長,因為這會增加總的故障轉移時間

1.非分散式資料庫

由於腦裂,日誌中事件損壞的問題將僅影響分散式事件儲存。如果選擇JDBC永續性外掛,則資料庫本身將防止您僅通過序列號上的唯一索引來破壞日誌。

2.分散式資料庫

如果您需要處理的負載和/或儲存量超出單個SQL所能承受的範圍,則基本上必須使用Cassandra或其他分散式資料庫。全域性唯一索引不再可用。這次,您的儲存將無法幫助您保持日記的一致性。

即使這樣,大腦裂開的情況也不會對您的應用程式一致性造成完全破壞。損壞的日誌會影響處理的兩個方面。寫側是永續性參與者狀態,讀側是投影,它基於永續性查詢讀取模型。兩者的處理方式不同。

讓我們從好訊息開始,儘管發生了腦裂故障,但由於重播filter,您的聚合狀態仍將保持一致。這是修復損壞的日記的非常聰明的機制。預設情況下,它僅跳過來自舊編寫器的事件(具有重複的序列號),並且您的聚合將與當前處理保持一致。

現在是壞訊息。讀取端-永續性查詢將為您提供具有所有事件的流(不進行任何過濾)。是的,您的讀取模型很可能會損壞。為什麼不能對流事件使用相同的過濾策略?這可能是單獨部落格文章的主題。長話短說,只有當您知道何時停止(在恢復路徑的情況下,您確切知道哪個事件是重播的事件)並且還需要批量處理事件時,才可以進行過濾。預設情況下,重播篩選器最多可批處理100個事件(akka.persistence.journal-plugin-fallback.replay-filter.window-size),以進行中斷檢測,因此,如果您的大腦分裂產生的錯誤事件多於window-size,那麼您的聚合狀態也會被破壞。在處理事件流時,按照定義,流沒有盡頭,如果引入批處理,它將增加恆定的額外延遲。這就是為什麼實時修復事件並非易事。在Akka Persistence Query中,這是不可能的,因為當前的api不提供對writer UUID的訪問,這對於此類事件過濾是必需的。

讀取模型損壞怎麼辦?這取決於。在某些情況下,手動修復將是最簡單的方法。我認為破壞事件的大腦應該是非常非常罕見的東西。可以通過基於彙總快照和大腦分裂後產生的事件刪除和重建投影來修復某些投影。其他人將需要產生一些其他的癒合事件,這將表明補償作用。一個非常優雅的解決方案,儘管成本很高。在某些情況下,不需要其他工作,例如,當您可以接受讀取模型中的某種程度的不準確性時。

從實際的角度來看,我還建議建立一個虛擬投影以僅檢測重複事件。這將幫助您驗證腦裂的範圍,並且監視此類異常會容易得多。

總結

毫無疑問,分散式系統具有挑戰性。一旦您進入這個世界,任何事情都是理所當然的。沒有什麼是免費的。準備好做出艱難的決定和妥協。至少您不會在工作中感到無聊:)

Akka Cluster裂腦故障可能發生在每個人身上,但是您可以做好準備。我的最後一條建議是對所有事物進行非常詳細的監視。之後,微調您的警報系統,不僅可以瞭解問題,還可以預測並阻止它們的發生。

 

相關文章