https://www.infoq.cn/article/QxFxSOt5UuH1-WqJwMQg
一、引子
有贊是提供商家 SAAS 服務,隨着越來越多的商家使用有贊,搜索或詳情的需求也日益增長,針對需求及場景,之前提到過的訂單管理架構演變及 AKF 架構等在這兩篇文章里已經有所體現,而這些數據的查詢來自於不同的 Nosql,怎么同步這些非實時存儲系統將是一個很有趣的事情。
1.1 同步現狀
當前有贊訂單同步流程及業務現狀如圖所示,采用了 ES+HBase(tip1)架構體系去解決搜索和詳情的需求,利用 canal(tip2)將數據庫變更寫入到 mq 中,然后利用同步系統來解決相關數據同步問題,而后續下文中將敘述有贊訂單同步面臨的問題及應對方案。
二、同步
2.1 實現同步基礎 - 單表同步
2.1.1 亂序問題
單表同步如圖所示:
業務場景中,在這任何一個序號鏈路中,如果並發出現同一主鍵的兩條消息,同時在 Nosql 中沒有做版本控制,都有可能造成消息亂序問題,而相反,如果通過順序解析 binlog 的同時,為每條 statement sql 執行的結果分配一個順序 SeqNo,該 SeqNo 保證有序(tip3),再通過 Nosql 中控制樂觀鎖就可以解決順序性問題。
2.1.2 HBase 同步
Hbase 同步相對來說比較簡單,Hbase 內部擁有 timestamp 協助控制每個 qualify 的版本,只要讓 timestamp 傳入上文所說的順序 SeqNo,那么就可以保證每個字段讀取出來的數據是最終一致性的。
2.1.3 ES 同步
ES 同步針對單表場景可以通過 index 的操作來進行寫入,index 可以采用 exteneral 版本也稱作外部版本號來進行控制,同樣適用上述的 SeqNo 來做樂觀鎖去解決該問題。
2.1.4 同步過程圖
2.2 實現同步進階 - 多表同步
2.2.1 亂序問題
多表同步基於單表同步的基礎上做相關的同步,如圖所示:
當數據庫的兩張表(不關心是否在一個實例上,如果不在,還更慘,SeqNo 還不一定能夠保證有序)觸發了更新操作,假設 t1 生成 binlog 的 SeqNo 小於 t2 生成 binlog 的 SeqNo,若 t1 這條消息因序號鏈路中的網絡抖動或其它原因造成消費晚於 t2,也就是 t2 的 binlogSeq 先寫入 Nosql 中,那么就會造成一個 t1 的數據無法寫入到 Nosql 中。
2.2.2 HBase 同步
其實上述多表同步亂序的問題並不是絕對的,針對 Hbase 這種自帶列版本號的將會自動處理或丟棄低版本數據,同時針對這種情況,設計成每個 table 表中的字段都會列入到 Hbase 中。舉個例子,針對訂單的情形存入訂單主表和訂單商品表
場景 1:1
針對訂單主表,我們寫入的數據以訂單號做 hash,然后以 hash 值: 訂單號作為主鍵降低熱點問題,同時定義單 column family,qualifiy 格式為 表: 字段 value 為對應的 value 值,timestamp 為 SeqNo,如圖所示:
場景 1:n
針對訂單商品表,我們寫入的數據同樣以訂單號生成相應的 rowkey,同時定義單 column family,qualifiy 格式為 表: 字段: 對應記錄的 id 值 value 為對應的 value 值,timestamp 為 SeqNo,如圖所示:
通過上述寫入,能夠針對具體到某個字段都有對應的 timestamp 值的更新,為后期寫入更新數據能夠更新到具體字段級別。
2.2.3 ES 同步
針對上面的同步亂序問題,ES 沒有 HBase 這種列版本號,ES 只有 doc 級別的 version,如果上述真的出現 SeqNo2>SeqNo1, 且 SeqNo2 早於 SeqNo1 寫入到 ES 中,則就會出現 SeqNo1 的內容無法寫入,也就會造成順序不一致的情況。那如何去解決這個多表同步問題呢?
既然會亂序,那讓它有序就好了,數據保證有序不就能夠解決這個事情嘛,讓整個鏈路有序也就代表 canal 消費 binlog 數據保證有序且丟到 MQ 中有序,MQ 然后保證順序投遞到 Sync 消費處理程序中,通過消費一條消息然后 ack 告訴 MQ 是否成功,已達到保證所有數據全部有序(若多線程或多機器處理 MQ 中的多個分區都是會存在問題)。如圖所示:
如此只要保證 t1 表的數據和 t2 表中的數據在 ES 不互相關聯,每個數據寫入的時候按照 update 方式寫入(如果不存在需要做一次 create 操作), 這樣就能保證所有數據按照順序執行。
2.3 配置化同步
上文已經講到數據同步是由一個數據源(這個數據源可以來自於 MQ、Mysql 等)同步到另外一個數據源(Mysql、ES、HBase、Alert 等),也就是一個管道的過程。借鑒了一下 logstash 官網,同樣處理流程分為 input、filter、output 組件,這些流程稱之為 task 任務,如圖所示:
通過這些組件,抽象化出每個組件都有對應的配置,由這些配置來進行初始化組件,驅動組件去執行流程。簡單來說,只需要在頁面中配置一些組件,無需開發任何一行代碼就能實現同步任務。如圖所示:
通過一系列的配置,就能配置出一個任務,針對業務邏輯,可以采用動態語言 groovy 來實行腳本化處理(復雜業務場景可以通過 UDF 函數來做支持),針對 mqinput 拿到的字段然后經過處理,經過過濾 filter 等,可以直接拿到相關的數據進行組裝,然后配置化的寫入到 ES 中,無需開發任何一行 java 代碼即可實現流程自動配置化,針對復雜的需求也能夠高效率支持。配置界面如圖所示:
2.3.1 性能瓶頸
上述就能解決 ES 多表同步的問題,但是同樣會存在一些問題:
- 性能瓶頸問題
- 失敗堆積問題
性能瓶頸:比如寫入量超級大的場景情況下,而 Sync 消費程序只能針對 MQ 中的分區(kafka 的 partition 概念)消費,每個分區只能有一個線程去執行,消費速率與消費分區成正比,與消費 RT 成反比,尤其是大促場景下就會造成數據消費不過來,數據堆積嚴重問題。
失敗堆積:因為是順序消費,只要某個分區的某條消息消費失敗,后續消息就會全部堆積,造成數據延遲率超高。所以建議用順序隊列的場景除非是業務量沒有性能瓶頸的情況下可以采取使用,而怎么去解決順序隊列或者去掉順序隊列呢?
用順序隊列無非就是保證有序,因為 ES 沒有 HBase 的字段級別版本號,目前訂單采用的是用 HBase 做一層中間處理層,解決該問題,如圖所示:
通過借助 HBase 字段級別版本號幫助每個表保證表內部字段有序,同時 put 寫入完數據之后,通過額外字段 version 做 increment 操作,當這兩個寫入動作完成之后立馬 get 操作拿到 HBase 的數據寫入到 ES 中,無論並發程度如何,最終至少有一次的 get 請求拿到的版本 version 字段是最大的,用該 version 作為 ES 的外部版本號解決 ES 版本號問題。
用此方案會有好處:
- HBase 協助管理內部字段版本,同時根據內部操作,協助 ES 拿到對應的版本,且數據能拿到最新數據;
- 去掉了順序隊列,HBase 具有良好的吞吐,相對於順序隊列擁有更大的吞吐量;
- 橫向拓展增大消費速率;
- ES 可以采用 index 操作,性能更好。
當然也有弊端:HBase 存在抖動的情況,以及主備切換問題。
因為存在抖動或者准備切換問題,會造成數據不一致,我們該怎么去解決這個事情呢?
2.4 未來擴展
目前訂單同步是通過加載配置文件形式來做的,也就是橫向拓展的機器都會去加載同一份配置文件,各個任務通過異常解耦,理論上不會有影響,但是會存在加載任務的重要度的問題。
舉個例子:
- 我需要一台機器臨時去消費數據解決線上問題;
- 有個量級很大但又不是很重要的任務,想不影響其他任務的進行;
- 要做對比,增量延遲對比或全量對比數據,但又不希望影響其他數據;
- 查詢日志需要所有機器查看查詢(當然,公司有內部日志系統,可直接上去查看) 如此,可以讓同步系統無狀態化,每個任務的配置加載有任務配置平台來進行配置,指定相關的機器做相關的處理,擴容也可以動態申請擴容,如圖所示,可以自由分配機器處理不同的任務。
三、一致性保障
上文講了有贊在處理訂單的時候怎么講數據同步到 ES 或 HBase,數據來源於 binlog,寫入到 MQ,也就是說處理的來源來自於 MQ。簡單一句話來講:我們不生產消息,我們是消息的搬運工。“搬運工”的角色可以做一些事情,同樣有贊在處理數據對比也是如此,這章講講“搬運工”可以做什么:
3.1 數據對比
上述一般情況下不會出問題,那如果出問題了怎么辦,需要做數據對比,而數據來源就是我們剛剛拋棄的順序隊列,順序隊列有個缺點就是堆積,同樣我們也可以利用堆積的特性,讓其第一條消息堆積十分鍾,那么后續消息基本上也會堆積十分鍾,然后就可以消費這個消息進行數據拉取,拿到最新的數據進行數據對比,如圖所示:
通過對比結果發送到 alert 中,就可以知道哪些數據不一致,頻率多少,這也是一種同步(mq->filter->alert)!
3.2 全量對比 / 數據刷入
上述我們講到數據同步到 Nosql 中,但是只是講了增量的一個過程,涉及到歷史數據,就需要對歷史數據進行遷移,同樣,這也是一種數據同步,后面將會出相關博文怎么去做全量數據同步。
四、Tips
Tip-1:為什么采用 ES + HBase 處理搜索和詳情?
一般情況下,公司達到一定規模,有類似全文檢索的需求或者高頻 key:value 的時候,大家會推薦 ES+HBase 的架構體系去完成搜索和詳情的需求,而現實中,絕大多數情況下生產環境不會將數據直接寫入到 ES 或者 Hbase,大家都會優先寫入數據庫,不進行雙寫的操作是因為增加鏈路影響業務。當然 Hbase 可能還好一點,ES 本身就是非實時查詢系統(為什么是非實時,有興趣的可以去看看 ES 讀寫流程),這種情況下也造就了 ES 和 HBase 的一個准實時系統。針對業務來說,准實時是可以滿足相關需求的,比如商家搜索訂單並不要求實時。
Tip-2:為什么有贊選 canal 解析 binlog,而不是采用業務消息進行數據同步?
- 數據表被更改,比如修數據情況,業務消息不會觸發,導致無法寫入到對應的 Nosql 中造成。數據不一致
- 順序性問題無法得到相關保障;
- 業務消息並不能拿到所有相關的數據進行寫入到 nosql 中。
Tip-3:SeqNo 實現方式,為什么不用 binlogoffset?
因為 cana 實例與 mysql 實例是 1:N(推薦 1:1), 而大部分業務場景同一種數據一般會落在同一個實例上,canal 就可以通過該台實例的時間與每秒處理的個數相結合。如:timestamp*10000+counter++,而不用 binlogoffset 的原因是 mysql 的實例掛了話,binlogoffset 可能會亂序。
五、結語
有贊交易訂單管理承接了億級流量的同步任務,面臨着眾多的需求挑戰,從最開始的 mysql 到如今的產品化的同步任務,從單表同步到多表同步,從單索引到多索引,從增量到全量,都有不同的解決之道,如今新興搜索中台更是承接億級搜索和同步流量,如有興趣,歡迎加入,我們一起共同探討。