大廠咋做多系統數據同步方案的?

1 背景

業務線與系統越來越多,系統或業務間數據同步需求也越頻繁。當前互聯網業務系統大多MySQL數據存儲與處理方案:

  • 隨信息時代爆炸,大數據量場景下慢慢凸顯短板,如:需對大量數據全文檢索,對大量數據組合查詢,分庫分表後的數據聚合查詢
  • 自然想到如何使用其他更適合處理該類問題的數據組件(ES)

因此,公司亟需一套靈活易用的系統間數據同步與處理方案,讓特定業務數據可很方便在其他業務或組件間流轉,助推業務快速迭代。

2 方案選型

當前業界針對系統數據同步較常見的方案有同步雙寫、異步雙寫、偵聽binlog等方式,各有優劣。本文以MySQL同步到ES案例講解。

2.1 同步雙寫

最簡單方案,在將數據寫到MySQL時,同時將數據寫到ES,實現數據雙寫。

優點

  • 設計簡單易懂
  • 實時性高

缺點

  • 硬編碼,有需要寫入MySQL的地方都要添加寫ES的代碼,導致業務強耦合
  • 存在雙寫可能失敗導致數據丟失的風險,如:
    • ES不可用
    • 應用系統和ES之間網絡故障
    • 應用系統重啓,導致系統來不及寫入ES
  • 對性能有較大影響,因爲每次業務操作都要加個ES操作,若對數據有強一致性要求,還需事務處理

2.2 異步雙寫

在同步雙寫基礎加個MQ,實現異步寫。

優點

  • 解決性能問題,MQ的性能基本比mysql高出一個數量級
  • 不易出現數據丟失問題,主要基於 MQ 消息的消費保障機制,比如 ES 宕機或者寫入失敗,還能重新消費 MQ 消息
  • 通過異步的方式做到了系統解耦,多源寫入之間相互隔離,便於擴展更多的數據源寫入

缺點

  • 數據同步實時性,由於MQ消費網絡鏈路增加,導致用戶寫入的數據不一定馬上看到,有延時
  • 雖在系統邏輯做到解耦,但存在業務邏輯裏依然需增加MQ代碼耦合
  • 複雜度增加:多個MQ中間件維護
  • 硬編碼問題,接入新的數據源需要實現新的消費邏輯

2.3 監聽binlog

第二種方案基礎上,主要解決業務耦合問題,所以引入數據變動自動監測與處理機制。

優點

  • 無代碼侵入,原有系統無需任何變化,無感知
  • 性能高,業務代碼完全無需新增任何多餘邏輯
  • 耦合度極低,完全無需關注原系統業務邏輯

缺點

  • 存在一定技術複雜度
  • 數據同步實時性可能有問題

基礎組件的設計主要考慮儘量做到對業務無侵入,業務接入無感知,同時系統耦合度低,綜上選型方案三,同時考慮該方案在可複用和可擴展還存在短板,所以在此基礎又做優化。

3 整體方案設計

3.1 概述

需求數據源都是MySQL,所以先考慮選擇組件對MySQL數據變動做實時監聽,業界成熟方案最熟悉的就是[canal],功能完善度,社區活躍度,穩定性等都符合。所以,基於canal對方案三優化,以滿足多系統數據同步,達到業務解耦、可複用、可擴展。

設計核心理念

通過統一的“消息分發服務”實現與Canal Client對接,並將消息按統一格式分發到不同MQ集羣,通過統一的“消息消費服務”去消費消息並回調業務接口,業務系統無需關注數據流轉,只需關注特定業務的數據處理和數據組裝。

“消息分發服務”和“消息消費服務”對各業務線,實現了數據流轉過程中的功能複用。“消息消費服務”中的可分發到不同的MQ集羣,和“消息消費服務”中的配置指定數據源輸出實現了功能擴展。

核心模塊

  • canal:監聽數據源的數據變動
  • 消息分發服務:對接canal客戶端,拉取變化的數據,將消息解析爲JSON,按固定規則分發到MQ,MQ可根據業務配置指定到不同集羣,實現橫向擴展。由於變更數據可能批量,這裏會將消息拆分爲單條發送到MQ中,並且通過配置可以過濾掉一些業務上不需要的大字段,減少mq消息體
  • 消息消費服務:從配置表中加載MQ隊列,消費MQ中的消息,通過隊列、回調接口、ES索引三者映射,將消息POST給業務回調接口,接收到業務回調接口返回的操作指令和ES文檔後,寫入對應的ES索引。寫入失敗時插入補償表,等待補償。這裏ES索引可以根據業務配置指定到不同的集羣,實現橫向擴展
  • 任務調度系統:定時調用消息消費服務中的消息補償等定時任務接口
  • 業務回調服務:接收消息消費服務POST過來的消息,根據消息中的指令和數據,結合數據庫中的數據或下游服務接口返回的數據組裝ES文檔中所需要的數據,設置相應的操作指令返回給消息消費服務去寫入ES
  • 業務ES查詢服務:通過ES SDK查詢ES索引中的數據,通過接口返回給業務調用方

3.2 數據訂閱消息分發服務

將數據的訂閱與數據的消費通過MQ進行解耦,“數據訂閱消息分發服務”的職責是對接Canal Client,解析數據變更消息,轉換爲常用的JSON格式的消息報文,按照業務配置規則分發到不同的MQ集羣、路由。

3.2.1 基於Canal的數據變更監聽機制

Canal主要是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費:

  • MySQL master 將數據變更寫入二進制日誌(binary log,其中記錄叫二進制日誌事件binary log events,可通過 show binlog events查看)
  • MySQL slave 將 master 的 binary log events 拷貝到它的中繼日誌(relay log)
  • MySQL slave 重放 relay log 中事件,將數據變更反映它自己的數據

  • Canal server 和 client 端高可用依賴zk,啓動 canal server 和 client 時都會從 zk 讀信息
  • Canal server 和 client 啓動時都會去搶佔 zk 對應的 running 節點,保證只有一個 server 和 client 在運行,而 server 和 client 高可用切換也是基於監聽 running 節點
  • Canal server 要啓動某個 canal instance 時都先向 zookeeper 進行一次嘗試啓動判斷 (實現:創建 EPHEMERAL 節點,誰創建成功就允許誰啓動)
  • 創建 zookeeper 節點成功後,對應的 canal server 就啓動對應的 canal instance,沒有創建成功的 canal instance 就會處於 standby 狀態
  • 一旦 zookeeper 發現 canal server A 創建的節點消失後,立即通知其他的 canal server 再次進行步驟 1 的操作,重新選出一個 canal server 啓動 instance
  • Canal client 每次進行 connect 時,會首先向 zookeeper 詢問當前是誰啓動了 canal instance,然後和其建立鏈接,一旦鏈接不可用,會重新嘗試 connect

3.2.2 基於Canal Client 分攤設計提升系統處理效率

從Canal服務高可用設計可見,Canal Client當有多個實例啓動時,會保證只有一個實例在運行,消費binlog消息。而承載Canal Client的"數據訂閱消息分發服務"會部署在多臺服務器,由於服務發佈時每臺服務器啓動時間不同,所有Canal Client活躍實例都會集中在先啓動的那臺服務器運行,消費binlog消息。

其餘服務器運行的Canal Client都處備用狀態,不能充分利用每臺服務器資源。因此希望不同destination分攤在不同服務器執行,但所在服務器宕機時會自動轉移到其他服務器執行,這樣充分利用每一臺服務器,提供binlog消息消費性能。

爲此,引入elasticjob-lite組件,利用分片特性二次封裝,實現偵聽destination在某臺服務器中上下線的變更事件。

elasticjob-lite分片原理

ElasticJob 中任務分片項的概念,使任務可在分佈式環境運行,每臺任務服務器只運行分配給該服務器的分片。隨着服務器的增加或宕機,ElasticJob 會近乎實時的感知服務器數量的變更。

若作業分 4 片,用兩臺服務器執行,則每個服務器分到 2 片:

7.png

新增Job服務器時,ElasticJob 會通過註冊中心的臨時節點的變化感知到新服務器,並在下次任務調度時重分片,新服務器會承載一部分作業分片:

8.png

當作業服務器在運行中宕機時,註冊中心同樣會通過臨時節點感知,並將在下次運行時將分片轉移至仍存活的服務器,以達到作業高可用。本次由於服務器宕機而未執行完的作業,則可以通過失效轉移的方式繼續執行。

3.2.3 資源隔離

該系統使用方包含公司各業務線,如何保障線上問題後,各業務不相互影響。

在MQ集羣和隊列級別都支持基於業務的資源隔離;將從canal中拉取出來的變更消息,按規則分發到不同MQ集羣,設置統一路由鍵規則, 以便各業務在對接時申請自己業務的MQ隊列,按需綁定對應MQ集羣和消息路由。

  • MQ集羣路由

通過配置將不同的destination映射到不同的MQ集羣和ZK集羣,可達到性能橫向擴展。

  • MQ消息路由規則

canal從binlog中獲取消息後,將批量消息拆分成單條消息,進行分片規則運算後發送到指定rabbitmq交換機和路由鍵,以便根據不同業務場景,按不同業務規則綁定到不同隊列,通過消費服務進行消息消費處理,同時會建立一個名爲“exchange.canal”的exchange,類型爲 topic,路由鍵規則:key.canal.{destination}.{database}.{table}.{sharding},sharding按pkName-value排序後的hashcode取模分片,隊列命名規則約定:queue.canal.{appId}.{bizName} 如:

queue.canal.trade_center.order_search.0 綁定 key.canal.dev-instance.trade_order.order_item.0
queue.canal.trade_center.order_search.0 綁定 key.canal.dev-instance.trade_order.order_extend.0
...

3.3 數據訂閱消息消費服務

爲實現消息的消費與業務系統解耦,獨立出"數據訂閱消費服務”。消費從”數據訂閱消息分發服務“中投遞的數據變更MQ消息,根據業務配置回調指定的業務回調接口。業務回調接口負責接收數據變更消息,組裝需要執行的ES文檔信息,返回給消費服務進行ES數據操作。

3.3.1 執行指令

從binlog訂閱的消息有3類操作:INSERT,UPDATE,DELETE,這裏新增一個SELECT指令,作用是業務回調接口在收到該指令後,從數據庫中重新獲取最新的數據組裝成需要執行的ES文檔信息,返回給消費服務進行ES數據操作。

主要應用在全量同步,部分同步,文檔刷新,消息補償等場景。

3.3.2 增量同步

  • MQ隊列動態加載

新的業務功能上線時,會配置對應的隊列綁定相關的路由鍵,訂閱到業務場景需要的數據變更的消息。爲避免每次有新業務接入需要重新更新消費服務代碼,重新發布服務,需實現能定時加載配置表數據,實現動態添加MQ隊列偵聽的功能。

使用SimpleMessageListenerContainer容器設置消費隊列的動態監聽。爲每個MQ集羣創建一個SimpleMessageListenerContainer實例,並動態註冊到Spring容器。

  • 業務隊列綁定規則

一個業務通常對應一個ES索引,一或多個MQ隊列(隊列綁定路由鍵的規則見: MQ消息分片規則):

10.jpeg

  • MQ消息順序消費

一個queue,有多個consumer去消費, 因爲無法保證先讀到消息的 consumer 一定先完成操作,所以可能導致順序錯亂。因爲不同消息都發到了一個queue,然後多個消費者又消費同一個queue的消息。爲此,可創建多個queue,每個消費者只消費一個queue, 生產者按規則把消息放入同一queue(見:3.4.4.2 MQ消息分片規則),這樣同一個消息就只會被同一個消費者順序消費。

服務通常集羣部署,天然每個queue就會有多個consumer。爲解決這問題引入elasticjob-lite對MQ分片,如有2個服務實例,5個隊列,可讓實例1消費隊列1、2、3,讓實例2消費隊列4、5。當其中有一個實例1掛掉時會自動將隊列1、2、3的消費轉移到實例2上,當實例1重啓啓動後隊列1、2、3的消費會重新轉移到實例1。

RabbitMQ消費順序錯亂原因通常是隊列消費是單機多線程消費或消費者是集羣部署,由於不同的消息都發送到了同一個 queue 中,多個消費者都消費同一個 queue 的消息。如消費者A執行增加,消費者B執行修改,消費者C執行刪除,但消費者C執行比消費者B快,消費者B又比消費者A快,導致消費 binlog 執行到ES時順序錯亂,本該增加、修改、刪除,變成刪除、修改、增加。

對此,可給 RabbitMQ 創建多個 queue,每個消費者單線程固定消費一個 queue 的消息,生產者發送消息的時候,同一個單號的消息發送到同一個 queue 中,由於同一個 queue 的消息有序,那同一單號的消息就只會被一個消費者順序消費,從而保證消息順序性:

但如何保證集羣模式下,一個隊列只在一臺機器上進行單線程消費,若這臺機器宕機如何進行故障轉移。 對此,引入elasticjob-lite對MQ分片,如有2個服務實例,5個隊列,我們可以讓實例1消費隊列1、2、3,讓實例2消費隊列4、5。當其中有一個實例1掛掉時會自動將隊列1、2、3的消費轉移到實例2上,當實例1重啓啓動後隊列1、2、3的消費會重新轉移到實例1。

對消息順序消費敏感的業務場景,通過隊列分片提升整體併發度。對消息順序消費不敏感業務場景也可配置成某隊列集羣消費或單機併發消費。針對不同的業務場景合理選擇不同的配置方案,提升整體性能。

3.3.3 全量同步

通過Canal獲取的變更消息只能滿足增量訂閱數據的業務場景,然而我們通常我們還需要進行一次全量的歷史數據同步後增量數據的訂閱纔會有意義。對於業務數據表的id是自增模式時,可以通過給定一個最小id值,最大id值,然後進行切片,如100個一片,生成MQ報文,發送到MQ中。消費MQ消息後對消息進行組裝,生成模擬增量數據變更的消息報文,走原有的增量消息回調的方式同步數據。

3.3.4 部分同步

有的時候我們需要修復指定的數據,或業務表的id是非自增模式的,需要進行全量同步。可以通過部分同步的接口,指定一組需要同步的id列表,生成分片MQ報文,發送到MQ中。消費服務接收到同步MQ消息後對消息進行組裝,生成模擬增量數據變更的消息報文,走原有的增量消息回調的方式同步數據。

3.3.5 刷新文檔

當我們ES索引中有大批量的數據異常,需要重新刷新ES索引數據時,可以通過生成一個全量同步的任務,分頁獲取指定ES索引的文檔ID列表,模擬生成部分同步消息報文,發送到MQ中。消費MQ消息後對消息進行組裝,生成模擬增量數據變更的消息報文,走原有的增量消息回調的方式同步數據。

3.3.6 消息補償

將同步失敗的消息存儲到消息重試表中,通過Job執行補償,便於監控。補償時將消息重置爲 SELECT 類型的MQ報文。業務回調接口接收到消息後會從數據庫中獲取最新的數據更新ES文檔。

3.4 ES SDK功能擴展

目前ES官方推薦使用的客戶端是RestHighLevelClient,我們在此基礎上進行了二次封裝開發,主要從擴展性和易用性方面考慮。

3.4.1、常用功能封裝

  • 使用工廠模式,方便註冊和獲取不同ES集羣對應的RestHighLevelClient實例,爲業務端使用時對ES集羣的擴展提供便利。
  • 對RestHighLevelClient的主要功能進行二次封裝如:索引的存在判斷、創建、更新、刪除;文檔的存在判斷、獲取、新增、更新、保存、刪除、統計、查詢。 降低開發人員使用RestHighLevelClient的複雜度,提高開發效率。

3.4.2、ES查詢數據權限隔離

對於一些有數據隔離需求的業務場景,我們提供了一個ES數據隔離插件。在ES SDK中設計了一個搜索過濾器的接口,採用攔截器的方式對統計文檔,搜索文檔等方法的搜索條件參數進行攔截過濾。

/**
* 搜索過濾器
*/
public interface SearchSourceBuilderFilter {
    String getFilterName();
    void filter(SearchSourceBuilder searchSourceBuilder);
}

13.jpeg

4 坑

4.1 Canal相關

4.1.1 Canal Admin 部署時需注意的配置項

  • 如何支持HA:'canal.instance.global.spring.xml' 設置爲 'classpath:spring/default-instance.xml'
  • 設置合適的並行線程數:canal.instance.parser.parallelThreadSize,我們當前設置的是16,如果該配置項被註釋掉,那麼可能會導致解析阻塞
  • 開啓tsdb導致的各種問題:canal默認開啓tsdb功能,也就是會通過h2數據庫緩存解析的表結構,但是實際情況下,如果上游變更了表結構,h2數據庫對應的緩存是不會更新的,這個時候一般會出現神奇的解析異常,異常的信息一般如下:‘Caused by: com.alibaba.otter.canal.parse.exception.CanalParseException: column size is not match for table’,該異常還會導致一個可怕的後果:解析線程被阻塞,也就是binlog事件不會再接收和解析。目前認爲比較可行的解決方案是:禁用tsdb功能,也就是canal.instance.tsdb.enable設置爲false。如果不禁用tsdb功能,一旦出現了該問題,必須要「先停止」Canal服務,接着「刪除」$CANAL_HOME/conf/目標數據庫實例標識/h2.mv.db文件,然後「啓動」Canal服務。目前我們是設置爲禁用的。
  • 設置合理的訂閱級別:其配置項是‘canal.instance.filter.regex’;庫表訂閱的配置建議配置到表級別,如果定義到庫級別一方面會消費一些無效的消息,給下游的MQ等帶來不必要的壓力。還有可能訂閱到一些日誌表等這類有着大字段數據的消息,消息過大在JSON化的時候可能導致內存溢出異常。針對這個問題我們進行大字段過濾和告警的改造。

4.1.2 binlog文件不存在,導致同步異常

如果發現Canal Client 長時間獲取不到binlog消息,可以去Canal Admin 後臺去看一下Instance管理中的日誌。大概率會出現“could not find first log file name in binary log index file”,這個是因爲zk集羣中緩存了binlog信息導致拉取的數據不對,包括定義了binlog position但是啓動服務後不對也是同樣的原因。

解決:

  • 單機部署的刪除canal/conf/$instance目錄中的meta.dat文件
  • 集羣模式需要進入zk刪除/otter/canal/destinations/xxxxx/1001/cursor,然後重啓canal

4.2 ES updateByQuery問題

ES的Update By Query對應的就是關係型數據庫的update set ... where...語句;該命令在ES上支持得不是很成熟,可能會導致的問題有:批量更新時非事務模式執行(允許部分成功部分失敗)、大批量操作會超時、頻繁更新會報錯(版本衝突)、腳本執行太頻繁時又會觸發斷路器等。我們的解決辦法也比較簡單,直接在生產環境放棄使用updateByQuery方法,配置成使用先查詢出符合條件的數據,然後分發到MQ中單條分別更新的模式。

5 規劃

  • 問題時及時報警,特別在業務連續性監控上,如系統內特定組件工作異常導致數據同步流中斷,是後續需重點優化的方向
  • 有些對實時性要求較高的業務依賴該系統進行數據同步,隨着業務量越來越大,該方案當前當前採用的MQ組件在性能和高可用性都有所欠缺,後續打算採用性能更好,可用性機制更完善的MQ組件
  • 由於採用小步快跑迭代,設計更多考慮線上運行順暢性,而忽略新業務接入便利性,目前一個新的業務服務對接數據同步系統,需要維護人員做不少配置文件,數據庫等相關的修改,並做人工確認,隨着接入需求越來越頻繁,亟需一個管理後臺,提升接入的效率和自動化度

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都國企技術專家兼架構,多家大廠後臺研發和架構經驗,負責複雜度極高業務系統的模塊化、服務化、平臺化研發工作。具有豐富帶團隊經驗,深厚人才識別和培養的積累。

參考:

本文由博客一文多發平臺 OpenWrite 發佈!

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