乾貨 | 百萬QPS,秒級延遲,攜程基於實時流的大數據基礎層建設

作者簡介

 

紀成,攜程數據開發總監,負責金融數據基礎組件及平臺開發、數倉建設與治理相關的工作。對大數據領域開源技術框架有濃厚興趣。


、背景



2017年9月攜程金融成立,本着踐行金融助力旅行的使命,開始全面開展集團風控和金融業務,需要在攜程DC構建統一的金融數據中心,實現多地多機房間的數據融合,滿足離線和在線需求; 涉及數千張mysql表到離線數倉 、實時數倉、在線緩存的同步工作。 由於跨地域、實時性、準確性、完整性要求高,集團內二次開發的DataX(業界常用的離線同步方案)無法支持。 以mysql-hive同步爲例,DataX通過直連MySQL批量拉取數據,存在以下問題:

1)性能瓶頸:隨着業務規模的增長,離線批量拉取的數據規模越來越大,影響mysql-hive鏡像表的產出時間,進而影響數倉下游任務。對於一些需要mysql-hive小時級鏡像的場景更加捉襟見肘。

2)影響線上業務:離線批量拉取數據,可能引 起慢查詢,影 響業務庫的線上服務。

3)無法保證冪等:由於線上 庫在實時 更新,在批量拉取SQL不變的情況下,每次執行可能產生不一樣的結果。比如指定了create_time 範圍,但一批記錄的部分字段(比如支付狀態)時刻在變化。也即無法產出一個明確的mysql-hive鏡像 , 對於一些對時點要求非常高的場景(比如離線對賬) 無法接受。

4)缺乏對DELETE的支持:業務庫做了DELETE操作後,只有整表全量拉取,才能在Hive鏡像裏體現。

二、方案概述


    
基於上述背景,我們設計了一套基於binlog實時流的數據基礎層構建方案,並取得了預期效果。架構如圖,各模塊簡介:

1)webUI做binlog採集的配置,以及mysql->hive,mysql→實時數倉,mysql→在線緩存的鏡像配置工作。

2)canal負責binlog採集 ,寫入kafka ;其中kafka在多地部署,並通過專線實現topic的實時同步。

3)spark-streaming 負責將binlog寫入HDFS。

4)merge 離線調度的ETL作業,負責將HDFS增量和 snap 合併成新的 snap。

5)mirror 負責將binlog事件更新到實時數倉、在線緩存。

6)基礎服務:包括歷史數據的重放,數據校驗,全鏈路監控,明文檢測等功能。


三、詳細介紹


     
本章將以mysql-hive鏡像爲例,對技術方案做詳細介紹。

3.1.binlog採集

    
canal是阿里巴巴開源的Mysql binlog增量訂閱和消費組件,在業界有非常廣泛的應用,通過實時增量採集binlog ,可以降低對mysql 的壓力,細粒度的還原數據的變更過程,我們選型canal 作爲binlog採集的基礎組件,根據應用場景做了二次開發,其中raw binlog → simple binlog 的消息格式轉換是重點。

下面是binlog採集的架構圖:


canal 在1.1.4版本引入了canal-admin工程,支持面向WebUI的管理能力;我們採用原生的canal-admin 對binlog採集進行管理 ,採集粒度是 mysql instance級別。

Canal Server會向canalAdmin 拉取所屬集羣下的所有mysql instance 列表,針對每個mysql instance採集任務,canal serve r通過在zo okeeper創建臨時節點的方式實現HA,並通過zookeeper實現binlog position的共享。

canal 1.1.1版本引入MQProducer 原生支持kafka消息投遞 , 圖中instance active 從mysql 獲取實時的增量raw binlog數據,在MQProducer 環節進行raw binlog → simple binlog的消息轉換,發送至kafka。我們按照instance 創建了對應的kafka topic,而非每個database 一個topic , 主要考慮到同一個mysql instance 下有多個database,過多的topic (partition) 導致kafka隨機IO增加,影響吞吐。發送Kafka時以schemaName+tableName作爲partitionKey,結合producer的參數控制,保證同一個表的binlog消息按順序寫入kafka。

參考producer參數控制:

max.in.flight.requests.per.connection=1retries=0acks=all

topic level 的配置:

topic partition 3副本, 且min.insync.replicas=2

從保證數據的順序性、容災等方面考慮,我們設計了一個輕量級的SimpleBinlog消息格式:


  • binlogOffset:全局序列ID,由${timestamp}${seq} 組成,該字段用於全局排序,方便Hive做row_number 取出最新鏡像,其中seq是同一個時間戳下自增的數字,長度爲6。

  • executeTime:binlog 的執行時間。

  • eventType:事件類型:INSERT,UPDATE,DELETE。

  • schemaName:庫名,在後續的spark-streaming,mirror 處理時,可以根據分庫的規則,只提取出前綴,比如(ordercenter_001 → ordercenter) 以屏蔽分庫問題。

  • tableName:表名,在後續的spark-streaming,mirror 處理時,可以根據分表規則,只提取出前綴,比如(orderinfo_001 → orderinfo ) 以屏蔽分表問題。

  • source:用於區分simple binlog的來源,實時採集的binlog 爲 BINLOG, 重放的歷史數據爲 MOCK 。

  • version:版本

  • content:本次變更的內容,INSERT,UPDATE 取afterColumnList,DELETE 取beforeColumnList。


金融當前部署了4組canal 集羣,每組2個物理機節點,跨機房DR部署,承擔了數百個mysql instance binlog採集工作。Canal server 自帶的性能監控基於Prometheus實現,我們通過實現 PrometheusScraper 主動拉取核心指標,推送到集團內部的Watcher監控系統上,配置相關報警,其中各mysql instance 的binlog採集延遲是全鏈路監控的重要指標。

系統上線初期遇到過canal-server instance腦裂的問題,具體場景是active instance 所在的canal-server ,因網絡問題與zookeeper的鏈接超時,這時候standby instance 會搶佔創建臨時節點,成爲新的active;也就出現了2個active 同時採集並推送binlog的情況。解決辦法是active instance 與zookeeper鏈接超時後,立即自kill,再次發起下一輪搶佔。

3.2 歷史數據重放


有兩個場景需要我們採集歷史數據:

1)首次做 mysql-hive鏡像 ,需要從mysql加載歷史數據;
2)系統故障(丟數等極端情況),需要從mysql恢復數據。

有兩種方案:

1)從mysql 批量拉取歷史數據,上傳到HDFS 。需要考慮批量拉取的數據與 binlog 採集產出的mysql-hive鏡像的格式差異,比如去重主鍵的選擇,排序字段的選擇等問題。

2) 流式方式, 批量從mysql 拉取歷史數據,轉換爲simple binlog消息流寫入kafka,同實時採集的simple binlog流複用後續的處理流程。在合併產生mysql-hive鏡像表時,需要確保這部分數據不會覆蓋實時採集的simple binlog數據。

我們選用了更簡單易維護的方案2,並開發了一個binlog-mock 服務,可以根據用戶給出的庫、表(前綴)以及條件,按批次(比如每次select 10000行)從mysql查詢數據,組裝成simple_binlog消息發送kafka。

對於mock的歷史數據,需要注意:

1)保證不覆蓋後續實時採集的binlog:simple binlog消息裏binlogOffset字段用於全局排序,它由${timestamp}+${seq}組成,mock的這部分數據 timestamp 爲發起SQL查詢的時間戳向前移5分鐘,seq爲000000;  

 2)落到哪個分區:我們根據binlog事件時間(executeTime) 判斷數據所屬哪個dt分區,mock的這部分數據 executeTime 爲用戶指定的一個值,默認爲${yesterday}。

3.3 Write2HDFS 

    
我們採用spark-streaming 將kafka消息持久化到HDFS,每5分鐘一個批次,一個批次的數據處理完成(持久化到HDFS)後再提交consumer offset,保證消息被at-least-once處理;同時也考慮了分庫分表問題、數據傾斜問題:

屏蔽分庫分表 :以訂單表爲例,mysql數據存儲在ordercenter_00 ... ordercenter_99 100個庫,每個庫下面又有orderinfo_00...orderinfo_99 100張表,庫前綴schemaNamePrefix=ordercenter,表前綴tableNamePrefix=orderinfo,統一映射到tableName=${schemaNamePrefix}_${tableNamePrefix}裏; 根據binlog executeTime字段生成對應的分區dt,確保同一個庫表同一天的數據落到同一個分區目錄裏:  base_path/ods_binlog_source.db/${database_prefix}_${table_prefix}/dt={binlogDt}/binlog-{timestamp}-{rdd.id}

防止數據傾斜 :  系統上線初期經常出現數據傾斜問題,排查發現某些時間段個別表由於 業務跑批等產 生的binlog量特別大,一張表一個批次的數據需要寫入同一個HDFS文件,單個HDFS文件的寫入速度成爲瓶頸。因此增加了一個環節(Step2),過濾出當前批次裏的“大表",將這些大表的數據分散寫入多個HDFS文件裏。 

base_path/ods_binlog_source.db/${database_prefix}_${table_prefix}/dt={binlogDt}/binlog-{timestamp}-{rdd.id}-[${randomInt}]


3.4 生成鏡像


3.4.1  數據就緒檢查
      
spark-streaming作業每5分鐘一個批次將kafka simple_binlog消息持久化到HDFS,merge任務是每天執行一次。每天0點15分,開始進行數據就緒檢查。我們對消息的全鏈路進行了監控,包括binlog採集延遲 t1 、kafka同步延遲 t2 、spark-streaming consumer 延遲 t3。假設當前時間爲凌晨0點30分,設爲t4,若t4>(t1+t2+t3) 說明 T-1日數據已全部落入HDFS,即可執行下游的ETL作業(merge)。


3.4.2  Merge
     
HDFS上的simple binlog數據就緒後,下一步就是對相應MySQL業務表數據進行還原。以下是Merge的執行流程,步驟如下:

1)加載T-1 分區的simple binlog數據

數據就緒檢查通過後,通過 MSCK REPAIR PARTITION 加載T-1分區的simple_binlog數據,注意:這個表是原始的simple binlog數據,並未平鋪具體mysql表的字段。如果是首次做mysql-hive鏡像,歷史數據重放的simple binlog也會落入T-1分區。

2)檢查Schema ,並抽取T-1增量

請求mirror後臺,獲取最新的mysql schema,如果發生了變更則更新mysql-hive鏡像表(snap),讓下游無感知;同時根據mysql schema 的field列表 、以及"hive主鍵" 等配置信息,從上述simple_binlog分區抽取出mysql表的T-1日明細數據 (delta)。

3)判斷業務庫是否發生了歸檔操作 ,以決定後續合 並時是否忽略DELETE事件。

業務DELETE數據有2種情況:業務修單等引起的正常DELETE,需要同步變更到Hive;業務庫歸檔歷史數據產生的DELETE,這類DELETE操作需要忽略掉。

系統上線初期,我們等待業務或DBA通知,然後手工處理,比較繁瑣,很多時候會有通知不到位的情況,導致Hive數據缺失歷史數據。爲了解決這個問題,在Merge之前進行程自動判斷,參考規則如下:

a)業務歸檔通常是大批量的DELETE(百萬+),因此可以設置一個閾值,比如500W或日增量的7倍。 

 b)業務歸檔的時間段通常比較久,比如設置閾值爲30天。如果滿足了條件1,且刪除的這些數據在30天以前,則屬於歸檔產生的DELETE。

4)對增量數據(delta)和當前快照(snap T-2)進行合併去重,得到最新snap T-1。


下面通過一個例子說明merge的過程,假設訂單order表共有id,order_no,amount三個字段,id是全局唯一建;  snap表t3 是mysql-hive鏡像,merge過程如圖展示。

1)加載目標(dt=T-1)分區裏的simple binlog數據,表格式如t1;
2)請求mirror後臺獲取mysql的最新schema,從t1 抽取數據到臨時表t2; 
3)snap表t3 與mysql schema進行適配(本例無變更);   
4)對增量表t2、存量snap t3 進行union(對t3自動增加type列,值爲INSERT),得到臨時表t4;
5) 對t4表按唯一鍵id進行row_number,分組按binlogOffset降序排序,序號爲1的即爲最新數據。


3.4.3  check
    
在數據merge完成後,爲了保證mysql-hive鏡像表中數據準確性,會對hive表和mysql表進行字段和數據量對比,做好最後一道防線。我們在配置mysql-hive鏡像時,會指定一個檢查條件,通常是按createTime字段對比7天的數據;mirror後臺每天凌晨會預先從mysql 統計出過去7日增量,離線任務通過腳本(http)獲取上述數據,和snap表進行校驗。實踐中遇到一些問題:

1)T-1的binlog落在T分區的情況

check服務根據createTime 生成查詢條件去check mysql和Hive數據,由於業務sql裏的createTime 和 binlog executeTime 不一致,分別爲凌晨時刻的前後1秒,會導致Hive裏漏掉這條數據,這種情況可以通過一起加載T日分區的binlog數據,重新merge。

2)業務表遷移,原錶停止更新,雖然mysql和hive數據量一致,但已經不符合要求了,這種情況可以通過波動率發現。

3.5 其他

    
在實踐中,可根據需要在binlog採集以及後續的消息流裏引入一些數據治理工作。比如:

1)明文檢測:binlog採集環節對核心庫表數據做實時明文檢測,可以避免敏感數據流入數倉;

2)標準化:一些字段的標準化操作,比如id映射、不同密文的映射;

3)元數據:mysql→hive鏡像是數倉ODS的核心,可以根據採集配置信息,實現二者映射關係的雙向檢索,便於數倉溯源。這塊是金融元數據管理的重要組成部分。

通過消費binlog實現mysql到實時數倉(kudu、es)、在線緩存(redis)的鏡像邏輯相對簡單,限於篇幅,本文不再贅述。

四、總結與展望



金融基於binlog的數據基礎層構建方案,順利完成了預期目標:

1)金融數據中心建設(ODS層):數千張mysql表到攜程DC的鏡像, 全部T+1 1:30產出;

2)金融實時數倉建設:金融核心mysql表到kudu的鏡像,支持實時分析、分表合併查詢等偏實時的運營場景;

3)金融在線緩存服務:異地多活,緩存近1000G業務數據;支撐整個消金入口、風控業務近100W/min的請求。

該方案已經成爲金融在線和離線服務的基石,並在持續擴充使用場景。未來會在自動化配置(整合mirror-admin和canal-admin,實現一鍵構造)、智能運維(數據check異常的識別與恢復)、元數據管理方面做更多的投入。

本文介紹了攜程金融構建大數據基礎層的技術方案,着重介紹了binlog採集和mysql-hive鏡像的設計,以及實踐中遇到的一些問題及解決辦法。希望能給大家帶來一些參考價值,也歡迎大家一起來交流。

本文分享自微信公衆號 - 一線碼農聊技術(dotnetfly)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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