1. 程式人生 > 實用技巧 >阿里巴巴Canal常見問題:重複解析/Filter失效/消費落後

阿里巴巴Canal常見問題:重複解析/Filter失效/消費落後

前言

Canal是阿里巴巴開源的資料庫Binlog日誌解析框架,主要用途是基於 MySQL 資料庫增量日誌解析,提供增量資料訂閱和消費。

在之前我寫的文章阿里開源MySQL中介軟體Canal快速入門中,我已經介紹了Canal的基本原理和基礎使用。

在部署到生產環境的過程中,自己作為一個菜鳥,又踩了一些坑,期間做了記錄和總結,並再解決後分析了下原因,便有了此文。

本文重點內容

Canal常見的三大問題原因分析及解決方案

  • Binlog解析錯誤:重複解析/DML解析為QUERY
  • Filter失效:設定過濾器無效
  • 消費落後:消費延遲或卡死

Canal踩坑與原因分析

問題:Binlog解析錯誤 重複解析/DML解析為QUERY

這個問題主要由以下幾種典型情況:

  • INSERT/UPDATE/DELETE被解析為Query或DDL語句
  • Binlog重複解析,即一個操作又有QUERY訊息,又有對應的INSERT/UPDATE/DELETE訊息。

這兩個問題主要都是因為Binlog不是row模式導致的,先來複習下Binlog的三種模式。

複習 MySQL Binlog的三種執行模式

MySQL在進行主從同步時,會使用Binlog,從庫讀取Binlog來進行資料的同步。但是Binlog是有三種不同的執行模式的,分別是ROW模式、Statement模式和Mix模式。

1. ROW模式

Binlog日誌中僅記錄哪一條記錄被修改了,修改成什麼樣了,會非常清楚的記錄下每一行資料修改的細節,Master修改了哪些行,slave也直接修改對應行的資料

優點:row的日誌內容會非常清楚的記錄下每一行資料修改的細節,非常容易理解。而且不會出現某些特定情況下的儲存過程和function,以及trigger的呼叫和出發無法被正確複製問題。

缺點:在row模式下,所有的執行的語句當記錄到日誌中的時候,都將以每行記錄的修改來記錄,這樣可能會產生大量的日誌內容。

2. Statement模式

每一條會修改資料的sql都會記錄到master的binlog中,slave在複製的時候sql程序會解析成和原來master端執行相同的sql再執行。

優點:在statement模式下首先就是解決了row模式的缺點,不需要記錄每一行資料的變化減少了binlog日誌量,節省了I/O以及儲存資源,提高效能。因為他只需要記錄在master上所執行的語句的細節以及執行語句的上下文資訊。

缺點:在statement模式下,由於他是記錄的執行語句,所以,為了讓這些語句在slave端也能正確執行,那麼他還必須記錄每條語句在執行的時候的一些相關資訊,也就是上下文資訊,以保證所有語句在slave端被執行的時候能夠得到和在master端執行時候相同的結果。另外就是,由於mysql現在發展比較快,很多的新功能不斷的加入,使mysql的複製遇到了不小的挑戰,自然複製的時候涉及到越複雜的內容,bug也就越容易出現。在statement中,目前已經發現不少情況會造成Mysql的複製出現問題,主要是修改資料的時候使用了某些特定的函式或者功能的時候會出現,比如:sleep()函式在有些版本中就不能被正確複製,在儲存過程中使用了last_insert_id()函式,可能會使slave和master上得到不一致的id等等。由於row是基於每一行來記錄的變化,所以不會出現,類似的問題。

3. Mix模式

從官方文件中看到,之前的 MySQL 一直都只有基於 statement 的複製模式,直到 5.1.5 版本的 MySQL 才開始支援 row 複製。從 5.0 開始,MySQL 的複製已經解決了大量老版本中出現的無法正確複製的問題。但是由於儲存過程的出現,給 MySQL Replication 又帶來了更大的新挑戰。

另外,看到官方文件說,從 5.1.8 版本開始,MySQL 提供了除 Statement 和 Row 之外的第三種複製模式:Mixed,實際上就是前兩種模式的結合。

在 Mixed 模式下,MySQL 會根據執行的每一條具體的 SQL 語句來區分對待記錄的日誌形式,也就是在 statement 和 row 之間選擇一種。

新版本中的 statment 還是和以前一樣,僅僅記錄執行的語句。而新版本的 MySQL 中對 row 模式也被做了優化,並不是所有的修改都會以 row 模式來記錄,比如遇到表結構變更的時候就會以 statement 模式來記錄,如果 SQL 語句確實就是 update 或者 delete 等修改資料的語句,那麼還是會記錄所有行的變更。

說完了三種模式,下面就來看看在Canal中會帶來的影響,簡單來說就是會造成Canal解析Query出現問題

我的客戶端程式碼片段:

String tableName = header.getTableName();
String schemaName = header.getSchemaName();

RowChange rowChange = null;

try {
    rowChange = RowChange.parseFrom(entry.getStoreValue());
} catch (InvalidProtocolBufferException e) {
    LOGGER.error("解析資料變化出錯", e);
}

EventType eventType = rowChange.getEventType();

LOGGER.info("當前正在操作表 {}.{}  執行操作 = {}", schemaName, tableName, eventType);

執行後,可以看到輸出:

紅框標出的部分,可以看出其實是一次操作,但是應該由於是Mix模式,canal解析成了兩條訊息,一次是QUERY,一次是UPDATE。

官方文件其實給出瞭解釋:

https://github.com/alibaba/canal/wiki/常見問題解答

問1:INSERT/UPDATE/DELETE被解析為Query或DDL語句?

答1: 出現這類情況主要原因為收到的binlog就為Query事件,比如:

  1. binlog格式為非row模式,通過show variables like 'binlog_format'可以檢視. 針對statement/mixed模式,DML語句都會是以SQL語句存在
  2. mysql5.6+之後,在binlog為row模式下,針對DML語句通過一個開關(binlog-rows-query-log-events=true, show variables裡也可以看到該變數),記錄DML的原始SQL,對應binlog事件為RowsQueryLogEvent,同時也有對應的row記錄. ps. canal可以通過properties設定來過濾:canal.instance.filter.query.dml = true

懂了問題出在Binlog後,其實這個問題也就不是太大,只是一開始讓人很迷惑。

問題:Filter失效

Canal提供了filter可以過濾掉不需要監聽的表(黑名單),或者指定需要監聽的表(白名單)。

我們通常在canal-server端的conf/example/instance.properties檔案中進行設定:

# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=

設定規則方式為:

mysql 資料解析關注的表,Perl正則表示式.
多個正則之間以逗號(,)分隔,轉義符需要雙斜槓(\\) 
常見例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打頭的表:canal\\.canal.*
4.  canal schema下的一張表:canal.test1
5.  多個規則組合使用:canal\\..*,mysql.test1,mysql.test2 (逗號分隔)

也可以在客戶端與canal進行連線時,用客戶端的connector.subscribe("xxxxxxx");來覆蓋服務端初始化時的設定。

Canal官方可能是收到的filter設定不成功的反饋有點多了,在canal1.1.3+版本之後,會在日誌裡記錄最後使用的filter條件,可以對比使用的filter看看是否和自己期望的是一致:

c.a.o.canal.parse.inbound.mysql.dbsync.LogEventConvert - --> init table filter : ^.*\..*$
c.a.o.canal.parse.inbound.mysql.dbsync.LogEventConvert - --> init table black filter :

可能原因一:客戶端呼叫subscribe("xxx")

如果失效,首先看下自己在客戶端是不是呼叫過connector.subscribe("xxxxxxx");覆蓋了服務端初始化時的設定。

可能原因二:Binlog非ROW模式

Binlog如果不是row模式,filter會失效

過濾條件只針對row模式的資料有效(ps. mixed/statement因為不解析sql,所以無法準確提取tableName進行過濾)

我上面截圖中那種收到兩條訊息的情況,第一條訊息就是一個QURTY,並且沒法確定表名,所以沒法開啟過濾。

問題:消費落後

Canal現在的架構是單機消費,就算是高可用架構,為了保證binlog消費的順序,依然是單機高可用,也就是在一臺消費者掛了之後在其他待命的消費者中啟動一臺繼續消費。(這個是目前版本我的理解,以後或許會有併發消費的新版本出來。)可以看下圖:

這種情況下,在Binlog資料量極大時,消費程序就有可能處理不過來。最後就會體現在消費跟不上,進度滯後,甚至掛掉。在Canal開源倉庫的issues中你可以看到很多類似的問題報告:

https://github.com/alibaba/canal/issues/726

我在部署完Canal後,在遇到資料庫寫入高峰期,就遇到了資料延遲問題。資料延遲還是小事,但是一旦延遲到堆滿了記憶體緩衝區,不消費的話,新的訊息就進不來了。

進一步分析這個問題,Canal整體架構如下圖:

而在訊息的儲存設計中,Canal使用了RingBuffer,架構如下圖:

可以看到,現在Canal是在記憶體中來快取訊息的,並不會對資料進行持久化,而且快取空間大小肯定是固定的,所以就會存在一直不提交確認ACK,導致記憶體快取被佔滿的情況。

下面貼幾個看到的寫的比較好的對於Canal消費堆積分析的文字,並貼出原文連結:

https://zqhxuyuan.github.io/2017/10/10/Midd-canal/

這裡假設環形緩衝區的最大大小為15個(原始碼中是16MB),那麼上面兩批一共產生了15個元素,剛好填滿了環形緩衝區。
如果又有Put事件進來,由於環形緩衝區已經滿了,沒有可用的slot,則Put操作會被阻塞,直到被消費掉。

https://blog.csdn.net/zhanlanmg/article/details/51213631

檢視canal原始碼,為了尋找canal是否進行了檔案持久化,大致上是沒有的,只有一個發現就是會有臨時的儲存,儲存介面CanalEventStore,CanalServerWithEmbedded.getWithoutAck()方法。繼續~真正處理資料是在AbstractEventParser類,它會開啟執行緒持續向master提交複製請求,直到有資料流過時,會呼叫EventTransactionBuffer的add(CanalEntry.Entry entry)方法,然後是put,可以看到put方法裡面,會把資料放在記憶體快取起來,當快取滿了以後會flush,而這個flush會呼叫TransactionFlushCallback介面的flush實現,這個介面在AbstractEventParser類裡面有一個匿名實現,它會把資料處理掉,在consumeTheEventAndProfilingIfNecessary方法中會呼叫sink方法,它會一直呼叫到entryEventSink.doSink(List events)方法,這裡面證實了,如果快取區已經滿了,那麼會等待,等待,直到有空位放。所以當快取區滿了以後會阻塞。這就是為什麼canal的資料走了很多之後,如果一直不對它ack那麼就不會再有新的資料過來了的原因。另外,由於測試方法的問題,導致昨天的描述不正確,並不是插入和更新有區別,而是我的操作問題,因為我的操作是批量更新和單條插入,而快取的大小取決於獲取資料的條數(就是一次master到slave的dump是一條資料),而不是因為資料量的原因。

一個可行的解決辦法是,將訊息拉取後,寫入訊息佇列(如RabbitMQ/Kafka),用訊息佇列來堆積訊息處理,來保證大量訊息堆積後不會導致canal卡死,並且可以支援資料持久化。

我自己對Canal這樣做的的猜測:Canal應該想是讓專業的工具做專業的事,Canal就只是一個讀取Binlog的中介軟體,並不是專業的訊息佇列,訊息應該讓專業的訊息佇列來處理。

總結

Canal實際用起來,特別是好好讀他的文件後,能感覺到還有許多問題和坑,還需要自己多多實踐一下,調研一下,才知道什麼是適合自身業務的。之後如果遇到更多Canal的坑,我還會持續記錄下來。

參考

關注我

我是一名後端開發工程師。主要關注後端開發,資料安全,爬蟲,物聯網,邊緣計算等方向,歡迎交流。

各大平臺都可以找到我

原創文章主要內容

  • 後端開發
  • Java面試
  • 設計模式/資料結構/演算法題解
  • 爬蟲/邊緣計算/物聯網
  • 讀書筆記/逸聞趣事/程式人生

個人公眾號:後端技術漫談

如果文章對你有幫助,不妨收藏,轉發,在看起來~