Netflix發佈DBLog:一款通用的變化數據捕獲框架

變化數據捕獲 (CDC)允許從數據庫實時捕獲已提交的更改,並將這些更改傳播到下游消費者。在需要保持多個異構數據存儲同步(如MySQL和ElasticSearch)的用例中,CDC變得越來越流行,它解決了雙寫和分佈式事務等傳統技術存在的挑戰。

本文最初發佈於Medium,由InfoQ中文站翻譯並分享。

簡介

變化數據捕獲 (CDC)允許從數據庫實時捕獲已提交的更改,並將這些更改傳播到下游消費者。在需要保持多個異構數據存儲同步(如MySQL和ElasticSearch)的用例中,CDC變得越來越流行,它解決了雙寫和分佈式事務等傳統技術存在的挑戰。

在MySQL和PostgreSQL這樣的數據庫中,事務日誌是CDC事件的來源。由於事務日誌的保留期通常有限,所以不能保證包含全部的更改歷史。因此,需要轉儲來捕獲源的全部狀態。已有幾個開源CDC項目,它們通常使用相同的底層庫、數據庫API和協議。儘管如此,我們發現了許多不能滿足我們需求的限制,例如,在轉儲完成之前暫停日誌事件的處理,缺少按需觸發轉儲的能力,或者使用表級鎖來阻止寫流量的實現。

這是我們開發DBLog的動機,它在通用框架下提供日誌和轉儲處理。它支持的數據庫需要具有MySQL、PostgreSQL、MariaDB等系統中常見的一組特性。

DBLog的部分功能如下:

  • 按順序處理捕獲到的日誌事件。
  • 轉儲可以隨時進行,跨所有表,針對一個特定的表或者針對一個表的具體主鍵。
  • 以塊的形式獲取轉儲,日誌與轉儲事件交錯。通過這種方式,日誌處理可以與轉儲處理一起進行。如果進程終止,它可以在最後一個完成的塊之後恢復,而不需要從頭開始。這還允許在需要時對轉儲進行調整和暫停。
  • 不會獲取表級鎖,這可以防止影響源數據庫上的寫流量。
  • 支持任何類型的輸出,因此,輸出可以是流、數據存儲甚或是API。
  • 設計充分考慮了高可用性。因此,下游的消費者可以放心,只要源端發生變化,它們就可以收到變化事件。

要求

在之前的一篇博文中,我們討論了Delta,一個數據充實和同步平臺。Delta的目標是保持多個數據存儲的同步,其中一個存儲是事實的來源(如MySQL),其他存儲則是派生存儲(如ElasticSearch)。其中一個關鍵要求是,從事實消息源到目的地的傳播延遲要低,並且事件流是高度可用的。無論是同一團隊使用多個數據存儲庫,還是一個團隊擁有另一個團隊正在使用的數據,這些條件都適用。在我們介紹Delta的博文中,我們還描述了數據同步之外的用例,比如事件處理。

對於數據同步和事件處理用例,除了實時捕獲更改的能力外,還需要滿足以下要求:

  • 捕獲全部狀態。派生存儲(如ElasticSearch)最終必須存儲源的全部狀態。我們通過來自源數據庫的轉儲來提供此功能。
  • 隨時觸發修復。我們不將轉儲視爲一次性設置活動,而是打算在任何時候啓用轉儲:跨所有表、在特定表或特定主鍵上。當數據丟失或損壞時,這對於下游的修復至關重要。
  • 針對實時事件提供高可用性。實時更改的傳播具有高可用性要求;我們不希望出現事件流停止時間較長(如分鐘或更長)的情況。即使在進行修復的過程中,也需要滿足這個需求,這樣它們就不會停止實時事件。我們希望實時事件和轉儲事件交錯發生,以便兩者都取得進展。
  • 最小化數據庫影響。在連接到數據庫時,確保儘可能少地影響數據庫的帶寬併爲應用程序提供讀寫的能力,這非常重要。出於這個原因,最好避免使用可能阻塞寫入流(如表級鎖)的API。此外,還必須加入能夠調節日誌和轉儲處理的控制,或者在需要時暫停處理。
  • 將事件寫到任何輸出。對於流技術,Netflix使用了各種各樣的選項,如Kafka、SQS、Kinesis,甚至Netflix特有的流解決方案,如Keystone。儘管將流作爲輸出可能是一個不錯的選擇(如當擁有多個消費者的時候),但它並不總是一個理想的選擇(如只有一個消費者)。我們希望可以直接寫入目的地,而不需要通過流。目標可以是數據存儲或外部API。
  • 支持關係型數據庫。Netflix的一些服務使用RDBMS類型的數據庫,如通過AWS RDS使用MySQL或PostgreSQL。我們希望支持將這些系統作爲一個源,以便它們能夠提供它們的數據供進一步使用。

現有解決方案

我們評估了一系列現有的開源產品,包括MaxwellSpinalTap、Yelp的MySQL StreamerDebezium。現有的解決方案在捕獲來自事務日誌的實時更改方面類似。例如使用MySQL的binlog複製協議,或者PostgreSQL的複製槽。

在轉儲處理方面,我們發現現有的解決方案至少存在以下一種限制:

  • 在處理轉儲時停止日誌事件處理。如果在轉儲過程中不處理日誌事件,則存在此限制。因此,如果轉儲量很大,日誌事件處理將會在很長一段時間內停止。當下遊消費者依賴實時更改的短傳播延遲時,這就會是個問題。
  • 缺少按需觸發轉儲的能力。大多數解決方案最初都是在引導階段或在事務日誌中檢測到數據丟失時執行轉儲。然而,根據需要觸發轉儲的能力對於引導新消費者(如新的ElasticSearch索引)或在數據丟失的情況下進行修復非常重要。
  • 通過鎖定表來阻塞寫流量。一些解決方案使用表級鎖來協調轉儲處理。根據實現和數據庫的不同,鎖定的時間可以很短,也可以持續整個轉儲過程。在後一種情況下,寫流量被阻塞,直到轉儲完成。在某些情況下,可以配置專用的讀副本,以避免影響主服務器上的寫操作。但是,這種策略並不適用於所有數據庫。例如,在PostgreSQL RDS中,只能從主服務器捕獲更改。
  • 使用專有數據庫的特性。我們發現,有些解決方案使用了高級的數據庫特性,這些特性是不能移植到其他系統上的,比如:使用MySQL的黑洞引擎,或者從創建PostgreSQL複製槽獲得一致的轉儲快照。這阻礙了跨數據庫的代碼重用。

最後,我們決定實現一種不同的方法來處理轉儲。它可以:

  • 轉儲事件和日誌交錯,以便兩者都可以進行
  • 允許隨時觸發轉儲
  • 不使用表級鎖
  • 使用標準的數據庫特性

DBLog框架

DBLog是一個基於Java的框架,能夠實時捕獲更改並獲取轉儲。轉儲以塊的形式獲取,以便可以與實時事件交錯,並且不會長時間停止實時事件處理。轉儲可以通過提供的API隨時獲取。這使得下游用戶可以在最初或稍後的修復時捕獲完整的數據庫狀態。

我們設計這個框架是爲了儘量減少對數據庫的影響。轉儲可以根據需要暫停和恢復。不管是對於失敗後的恢復,還是數據庫遇到瓶頸時停止處理,這都很重要。爲了不影響應用程序的寫操作,我們也不會獲取表級鎖。

DBLog允許將捕獲的事件寫入任何輸出,甚至是另一個數據庫或API。我們使用Zookeeper來存儲與日誌和轉儲處理相關的狀態,並將其用於羣首選舉。我們在構建DBLog時考慮到了可插拔性,可以根據需要替換實現(比如用其他東西替換Zookeeper)。

下面的小節將更詳細地說明日誌和轉儲處理。

日誌處理

該框架要求數據庫針對每個更改的行實時地按提交順序發出一個事件。事務日誌被認爲是這些事件的起源。數據庫將它們發送到DBLog可以使用的傳輸。我們使用術語“更改日誌”來表示該傳輸。事件的類型可以是創建更新刪除。對於每個事件,需要提供以下內容:日誌序列號、操作時的列狀態和操作時應用的模式。

每個更改都序列化爲DBLog的事件格式,併發送給寫入器,以便將其傳遞到輸出。向寫入器發送事件是一種非阻塞操作,因爲寫入器在自己的線程中運行,並在內部緩衝區中收集事件。緩衝的事件按順序寫入輸出。該框架允許自定義格式化程序插件,以便將事件序列化爲自定義格式。輸出是一個簡單的接口,允許插入任何需要的目標,例如流、數據存儲甚至API。

轉儲處理

轉儲是必需的,因爲事務日誌的保留時間有限,所以無法用它們重新構造完整的源數據集。轉儲以塊的形式獲取,這樣它們就可以與日誌事件交錯,從而允許它們同時進行。爲塊的每個選定行生成一個事件,並以與日誌事件相同的格式進行序列化。這樣,如果事件源於日誌或轉儲,下游使用者就無需擔心。日誌和轉儲事件都通過同一寫入器發送到輸出。

可以通過API隨時調度針對所有表、特定表或表的特定主鍵的轉儲。每個表的轉儲請求按配置好的大小分塊執行。此外,可以配置延遲來延緩新塊的處理,在此期間只允許日誌事件處理。塊大小和延遲實現了日誌和轉儲事件處理之間的平衡,並且可以在運行時更新這兩個設置。

在選擇塊時會按主鍵升序對錶進行排序,塊中行的主鍵大於前一個塊的最後一個主鍵。數據庫需要有效地執行此查詢,這通常適用於實現了主鍵範圍掃描的系統。

圖1 將一個包含4列(c1-c4)且以c1爲主鍵(pk)的表分塊。Pk列的類型爲integer,塊大小爲3。塊2的選擇以c1 > 4爲條件。

塊需要以一種不會長時間阻塞日誌事件處理的方式來獲取,並且要保留日誌更改的歷史,這樣,如果選取的行是舊值,就不能覆蓋來自日誌事件的較新的狀態。

爲了實現這一點,我們在更改日誌中創建可識別的水印事件,以便對塊選擇進行排序。水印是通過源數據庫中的一個表實現的。表存儲在專用的命名空間中,因此不會與應用程序表發生衝突。存儲UUID字段的表只包含一行。通過將這一行更新爲特定的UUID來生成水印。行更新將導致一個最終通過更改日誌接收的更改事件。

通過使用水印,轉儲採取以下步驟:

  1. 暫停日誌事件處理。
  2. 通過更新水印表生成低水印。
  3. 爲下一個塊運行SELECT語句,並將結果集存儲在內存中,按主鍵索引。
  4. 通過更新水印表生成高水印。
  5. 繼續將接收到的日誌事件發送到輸出。監視日誌中的高低水印事件。
  6. 接收到低水印事件後,開始從結果集中刪除所有在低水印之後接收到的日誌事件主鍵的條目。
  7. 接收到高水印事件後,將所有剩餘的結果集條目發送到輸出,然後再處理新的日誌事件。
  8. 如果出現更多的塊,請轉到步驟1。

假定SELECT從一致的快照返回狀態,該快照表示歷史上某個特定點之前提交的更改。或者說:考慮到到那時爲止的更改,SELECT在更改日誌的特定位置上執行。數據庫通常不公開與select語句執行相對應的日誌位置(MariaDB是一個例外)。

我們方法的核心思想是在變更日誌上確定一個窗口,它保證包含SELECT。由於確切的選擇位置是未知的,所有與該窗口內的日誌事件發生衝突的選中行將被刪除。這可以確保選擇的塊不會覆蓋日誌更改的歷史。通過寫入低水印來打開窗口,然後運行選擇,最後通過寫入高水印來關閉窗口。爲了實現這一功能,SELECT必須讀取低水印或之後的最新狀態(如果該選擇還包括在低水印寫之後和讀之前提交的寫,則沒有問題)。

圖2a和2b說明了塊選擇算法。我們提供了一個示例表,k1到k6爲主鍵。每個更改日誌條目表示主鍵的創建、更新或刪除事件。在圖2a中,我們展示了水印的生成和塊的選擇(步驟1到步驟4)。在圖2b中,我們重點看下從位於水印之間的主鍵結果集中刪除選定塊的行(步驟5到7)。

圖 2a——塊選擇的水印算法(步驟1-4)

圖 2b——塊選擇的水印算法(步驟5-7)

注意,如果一個或多個事務在低水印和高水印之間提交了大量的行更改,則可能會出現大量的日誌事件。這就是爲什麼我們的方法在步驟2-4期間會短暫地暫停日誌處理,從而保證不會遺漏水印。這樣,日誌事件處理就可以在以後逐個事件地恢復,最終發現水印,而不需要緩存日誌事件條目。日誌處理暫停的時間很短,因爲步驟2-4預計會比較快:水印更新是單個的寫操作,而SELECT操作有一定的範圍。

在第7步接收到高水印後,非衝突的塊行將被提交寫入,以便按順序發送到輸出。這是一個非阻塞操作,因爲寫入器在單獨的線程中運行,允許在步驟7之後快速恢復日誌處理。然後,日誌事件處理將繼續處理高水位之後發生的事件。

在圖2c中,我們使用與圖2a和2b相同的示例來描述整個塊選擇的寫順序。出現在高水位之前的日誌事件首先被寫入。然後是塊結果的其餘行(洋紅色)。最後是在高水位之後發生的日誌事件。

圖 2c——輸出寫入順序。日誌與轉儲事件交錯。

數據庫支持

爲了使用DBLog,數據庫需要從提交更改和非過期讀取的線性歷史中提供更改日誌。這些條件由MySQL、PostgreSQL、MariaDB等系統來實現,因此框架可以在這些類型的數據庫中通用。

到目前爲止,我們增加了對MySQL和PostgreSQL的支持。集成日誌事件需要使用不同的庫,因爲每個數據庫都使用了一個專有協議。對於MySQL,我們使用shyiko/ MySQL -binlog-connector來實現binlog複製協議,以便從MySQL主機接收事件。對於PostgreSQL,我們通過wal2json插件使用複製槽。更改通過由PostgreSQL jdbc驅動程序實現的流複製協議接收。對於捕獲的每個更改的模式,MySQL和PostgreSQL的確定方式是不同的。在PostgreSQL中,wal2json包含列名和類型以及列值。MySQL模式的更改則必須通過接收的binlog事件進行跟蹤。

轉儲處理是使用SQL和JDBC集成的,只需要實現塊選擇和水印更新。MySQL和PostgreSQL使用相同的代碼,其他類似的數據庫也可以使用相同的代碼。轉儲處理本身不依賴於SQL或JDBC,並且允許集成滿足DBLog框架要求的數據庫,即使它們使用不同的標準。

圖 3——DBLog高階架構

高可用性

DBLog使用主動-被動架構。一個實例是主動的,其他的是被動的。我們利用Zookeeper進行羣首選舉,從而確定活動實例。領導權是一種租約,如果沒有及時更新,就會丟失,讓另一個實例接管。我們目前爲每個AZ部署一個實例(通常我們有3個AZ),因此,如果一個AZ宕機,另一個AZ中的實例可以繼續處理,保證總停機時間最少。被動實例也可以跨區域,但建議在與數據庫主機相同的區域內進行操作,以保持較低的更改捕獲延遲。

生產使用情況

DBLog是Netflix MySQL和PostgreSQL連接器的基礎,這些連接器在Delta中使用。Delta自2018年起用於生產,用於Netflix studio應用程序中的數據存儲同步和事件處理。在DBLog之上,Delta連接器使用自定義的事件序列化器,因此,在將事件寫入輸出時會使用Delta的事件格式。Netflix特有的流被用作輸出,比如Keystone

圖4——Delta連接器

除了Delta之外,DBLog還用於爲其他Netflix數據移動平臺構建連接器,這些平臺有自己的數據格式。

敬請關注

DBLog還有一些本文沒有涉及的功能,比如:

  • 能夠在不使用鎖的情況下捕獲表模式。
  • 模式存儲集成。存儲發送到輸出的每個事件的模式,並在每個事件的有效負載中包含到模式存儲的引用。
  • 單調寫模式。確保一旦針對特定行寫入了狀態,之後就不能寫入稍早的狀態。通過這種方式,下游消費者只會看到前向狀態轉換,而不需要在時間上來回切換。

我們計劃在2020年開源DBLog及更多文檔。

原文鏈接:

DBLog: A Generic Change-Data-Capture Framework

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章