MySQL Binlog 解析工具 Maxwell 詳解

小旋鋒發表於2019-03-11

MySQL Binlog 解析工具 Maxwell 詳解

maxwell 簡介

Maxwell是一個能實時讀取MySQL二進位制日誌binlog,並生成 JSON 格式的訊息,作為生產者傳送給 Kafka,Kinesis、RabbitMQ、Redis、Google Cloud Pub/Sub、檔案或其它平臺的應用程式。它的常見應用場景有ETL、維護快取、收集表級別的dml指標、增量到搜尋引擎、資料分割槽遷移、切庫binlog回滾方案等。官網(http://maxwells-daemon.io)、GitHub(https://github.com/zendesk/maxwell)

Maxwell主要提供了下列功能:

  • 支援 SELECT * FROM table 的方式進行全量資料初始化
  • 支援在主庫發生failover後,自動恢復binlog位置(GTID)
  • 可以對資料進行分割槽,解決資料傾斜問題,傳送到kafka的資料支援database、table、column等級別的資料分割槽
  • 工作方式是偽裝為Slave,接收binlog events,然後根據schemas資訊拼裝,可以接受ddl、xid、row等各種event

除了Maxwell外,目前常用的MySQL Binlog解析工具主要有阿里的canal、mysql_streamer,三個工具對比如下:

canal、maxwell、mysql_streamer對比

canal 由Java開發,分為服務端和客戶端,擁有眾多的衍生應用,效能穩定,功能強大;canal 需要自己編寫客戶端來消費canal解析到的資料。

maxwell相對於canal的優勢是使用簡單,它直接將資料變更輸出為json字串,不需要再編寫客戶端。

快速開始

首先MySQL需要先啟用binlog,關於什麼是MySQL binlog,可以參考文章《MySQL Binlog 介紹

$ vi my.cnf

[mysqld]
server_id=1
log-bin=master
binlog_format=row
複製程式碼

建立Maxwell使用者,並賦予 maxwell 庫的一些許可權

CREATE USER 'maxwell'@'%' IDENTIFIED BY '123456';
GRANT ALL ON maxwell.* TO 'maxwell'@'%';
GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE on *.* to 'maxwell'@'%'; 
複製程式碼

使用 maxwell 之前需要先啟動 kafka

wget http://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.1.0/kafka_2.11-2.1.0.tgz
tar -xzf kafka_2.11-2.1.0.tgz
cd kafka_2.11-2.1.0
# 啟動Zookeeper
bin/zookeeper-server-start.sh config/zookeeper.properties
複製程式碼

單機啟動 kafka 之前,需要修改一下配置檔案,開啟配置檔案 vi config/server.properties,在檔案最後加入 advertised.host.name 的配置,值為 kafka 所在機器的IP

advertised.host.name=10.100.97.246
複製程式碼

不然後面通過 docker 啟動 maxwell 將會報異常(其中的 hadoop2 是我的主機名)

17:45:21,446 DEBUG NetworkClient - [Producer clientId=producer-1] Error connecting to node hadoop2:9092 (id: 0 rack: null)
java.io.IOException: Can't resolve address: hadoop2:9092
        at org.apache.kafka.common.network.Selector.connect(Selector.java:217) ~[kafka-clients-1.0.0.jar:?]
        at org.apache.kafka.clients.NetworkClient.initiateConnect(NetworkClient.java:793) [kafka-clients-1.0.0.jar:?]
        at org.apache.kafka.clients.NetworkClient.ready(NetworkClient.java:230) [kafka-clients-1.0.0.jar:?]
        at org.apache.kafka.clients.producer.internals.Sender.sendProducerData(Sender.java:263) [kafka-clients-1.0.0.jar:?]
        at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:238) [kafka-clients-1.0.0.jar:?]
        at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:176) [kafka-clients-1.0.0.jar:?]
        at java.lang.Thread.run(Thread.java:748) [?:1.8.0_181]
Caused by: java.nio.channels.UnresolvedAddressException
        at sun.nio.ch.Net.checkAddress(Net.java:101) ~[?:1.8.0_181]
        at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:622) ~[?:1.8.0_181]
        at org.apache.kafka.common.network.Selector.connect(Selector.java:214) ~[kafka-clients-1.0.0.jar:?]
        ... 6 more
複製程式碼

接著可以啟動 kafka

bin/kafka-server-start.sh config/server.properties
複製程式碼

測試 kafka

# 建立一個 topic
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

# 列出所有 topic
bin/kafka-topics.sh --list --zookeeper localhost:2181

# 啟動一個生產者,然後隨意傳送一些訊息
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
This is a message
This is another message

# 在另一個終端啟動一下消費者,觀察所消費的訊息
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
This is a message
This is another message
複製程式碼

通過 docker 快速安裝並使用 Maxwell (當然之前需要自行安裝 docker)

# 拉取映象 
docker pull zendesk/maxwell

# 啟動maxwell,並將解析出的binlog輸出到控制檯
docker run -ti --rm zendesk/maxwell bin/maxwell --user='maxwell' --password='123456' --host='10.100.97.246' --producer=stdout
複製程式碼

測試Maxwell,首先建立一張簡單的表,然後增改刪資料

CREATE TABLE `test` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `age` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
insert into test values(1,22,"小旋鋒");
update test set name='whirly' where id=1;
delete from test where id=1;
複製程式碼

觀察docker控制檯的輸出,從輸出的日誌中可以看出Maxwell解析出的binlog的JSON字串的格式

{"database":"test","table":"test","type":"insert","ts":1552153502,"xid":832,"commit":true,"data":{"id":1,"age":22,"name":"小旋鋒"}}
{"database":"test","table":"test","type":"update","ts":1552153502,"xid":833,"commit":true,"data":{"id":1,"age":22,"name":"whirly"},"old":{"name":"小旋鋒"}}
{"database":"test","table":"test","type":"delete","ts":1552153502,"xid":834,"commit":true,"data":{"id":1,"age":22,"name":"whirly"}}
複製程式碼

輸出到 Kafka,關閉 docker,重新設定啟動引數

docker run -it --rm zendesk/maxwell bin/maxwell --user='maxwell' \
    --password='123456' --host='10.100.97.246' --producer=kafka \
    --kafka.bootstrap.servers='10.100.97.246:9092' --kafka_topic=maxwell --log_level=debug
複製程式碼

然後啟動一個消費者來消費 maxwell topic的訊息,觀察其輸出;再一次執行增改刪資料的SQL,仍然可以得到相同的輸出

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic maxwell
複製程式碼

輸出JSON字串的格式

  • data 最新的資料,修改後的資料
  • old 舊資料,修改前的資料
  • type 操作型別,有insert, update, delete, database-create, database-alter, database-drop, table-create, table-alter, table-drop,bootstrap-insert,int(未知型別)
  • xid 事務id
  • commit 同一個xid代表同一個事務,事務的最後一條語句會有commit,可以利用這個重現事務
  • server_id
  • thread_id
  • 執行程式時新增引數--output_ddl,可以捕捉到ddl語句
  • datetime列會輸出為"YYYY-MM-DD hh:mm:ss",如果遇到"0000-00-00 00:00:00"會原樣輸出
  • maxwell支援多種編碼,但僅輸出utf8編碼
  • maxwell的TIMESTAMP總是作為UTC處理,如果要調整為自己的時區,需要在後端邏輯上進行處理

與輸出格式相關的配置如下

選項 引數值 描述 預設值
output_binlog_position BOOLEAN 是否包含 binlog position false
output_gtid_position BOOLEAN 是否包含 gtid position false
output_commit_info BOOLEAN 是否包含 commit and xid true
output_xoffset BOOLEAN 是否包含 virtual tx-row offset false
output_nulls BOOLEAN 是否包含值為NULL的欄位 true
output_server_id BOOLEAN 是否包含 server_id false
output_thread_id BOOLEAN 是否包含 thread_id false
output_schema_id BOOLEAN 是否包含 schema_id false
output_row_query BOOLEAN 是否包含 INSERT/UPDATE/DELETE 語句. Mysql需要開啟 binlog_rows_query_log_events false
output_ddl BOOLEAN 是否包含 DDL (table-alter, table-create, etc) events false
output_null_zerodates BOOLEAN 是否將 '0000-00-00' 轉換為 null? false

進階使用

基本的配置

選項 引數值 描述 預設值
config 配置檔案 config.properties 的路徑
log_level [debug | info | warn | error] 日誌級別 info
daemon 指定Maxwell例項作為守護程式到後臺執行
env_config_prefix STRING 匹配該字首的環境變數將被視為配置值

可以把Maxwell的啟動引數寫到一個配置檔案 config.properties 中,然後通過 config 選項指定,bin/maxwell --config config.properties

user=maxwell
password=123456
host=10.100.97.246
producer=kafka
kafka.bootstrap.servers=10.100.97.246:9092
kafka_topic=maxwell
複製程式碼

mysql 配置選項

Maxwell 根據用途將 MySQL 劃分為3種角色:

  • host:主機,建maxwell庫表,儲存捕獲到的schema等資訊

    • 主要有六張表,bootstrap用於資料初始化,schemas記錄所有的binlog檔案資訊,databases記錄了所有的資料庫資訊,tables記錄了所有的表資訊,columns記錄了所有的欄位資訊,positions記錄了讀取binlog的位移資訊,heartbeats記錄了心跳資訊
  • replication_host:複製主機,Event監聽,讀取該主機binlog

    • hostreplication_host分開,可以避免 replication_user 往生產庫裡寫資料
  • schema_host:schema主機,捕獲表結構schema的主機

    • binlog裡面沒有欄位資訊,所以maxwell需要從資料庫查出schema,存起來。
    • schema_host一般用不到,但在binlog-proxy場景下就很實用。比如要將已經離線的binlog通過maxwell生成json流,於是自建一個mysql server裡面沒有結構,只用於傳送binlog,此時表機構就可以制動從 schema_host 獲取。

通常,這三個主機都是同一個,schema_host 只在有 replication_host 的時候使用。

與MySQL相關的有下列配置

選項 引數值 描述 預設值
host STRING mysql 地址 localhost
user STRING mysql 使用者名稱
password STRING mysql 密碼 (no password)
port INT mysql 埠 3306
jdbc_options STRING mysql jdbc connection options DEFAULT_JDBC_OPTS
ssl SSL_OPT SSL behavior for mysql cx DISABLED
schema_database STRING Maxwell用於維護的schema和position將使用的資料庫 maxwell
client_id STRING 用於標識Maxwell例項的唯一字串 maxwell
replica_server_id LONG 用於標識Maxwell例項的唯一數字 6379 (see notes)
master_recovery BOOLEAN enable experimental master recovery code false
gtid_mode BOOLEAN 是否開啟基於GTID的複製 false
recapture_schema BOOLEAN 重新捕獲最新的表結構(schema),不可在 config.properties中配置 false
replication_host STRING server to replicate from. See split server roles schema-store host
replication_password STRING password on replication server (none)
replication_port INT port on replication server 3306
replication_user STRING user on replication server
replication_ssl SSL_OPT SSL behavior for replication cx cx DISABLED
schema_host STRING server to capture schema from. See split server roles schema-store host
schema_password STRING password on schema-capture server (none)
schema_port INT port on schema-capture server 3306
schema_user STRING user on schema-capture server
schema_ssl SSL_OPT SSL behavior for schema-capture server DISABLED

生產者的配置

僅介紹kafka,其他的生產者的配置詳見官方文件。

kafka是maxwell支援最完善的一個生產者,並且內建了多個版本的kafka客戶端(0.8.2.2, 0.9.0.1, 0.10.0.1, 0.10.2.1 or 0.11.0.1, 1.0.0.),預設 kafka_version=1.0.0(當前Maxwell版本1.20.0)

Maxwell 會將訊息投遞到Kafka的Topic中,該Topic由 kafka_topic 選項指定,預設值為 maxwell,除了指定為靜態的Topic,還可以指定為動態的,譬如 namespace_%{database}_%{table}%{database}%{table} 將被具體的訊息的 database 和 table 替換。

Maxwell 讀取配置時,如果配置項是以 kafka. 開頭,那麼該配置將設定到 Kafka Producer 客戶端的連線引數中去,譬如

kafka.acks = 1
kafka.compression.type = snappy
kafka.retries=5
複製程式碼

下面是Maxwell通用生產者和Kafka生產者的配置引數

選項 引數值 描述 預設值
producer PRODUCER_TYPE 生產者型別 stdout
custom_producer.factory CLASS_NAME 自定義消費者的工廠類
producer_ack_timeout PRODUCER_ACK_TIMEOUT 非同步消費認為訊息丟失的超時時間(毫秒ms)
producer_partition_by PARTITION_BY 輸入到kafka/kinesis的分割槽函式 database
producer_partition_columns STRING 若按列分割槽,以逗號分隔的列名稱
producer_partition_by_fallback PARTITION_BY_FALLBACK producer_partition_by=column時需要,當列不存在是使用
ignore_producer_error BOOLEAN 為false時,在kafka/kinesis發生錯誤時退出程式;為true時,僅記錄日誌 See also dead_letter_topic true
kafka.bootstrap.servers STRING kafka 叢集列表,HOST:PORT[,HOST:PORT]
kafka_topic STRING kafka topic maxwell
dead_letter_topic STRING 詳見官方文件
kafka_version KAFKA_VERSION 指定maxwell的 kafka 生產者客戶端版本,不可在config.properties中配置 0.11.0.1
kafka_partition_hash [default | murmur3] 選擇kafka分割槽時使用的hash方法 default
kafka_key_format [array | hash] how maxwell outputs kafka keys, either a hash or an array of hashes hash
ddl_kafka_topic STRING output_ddl為true時, 所有DDL的訊息都將投遞到該topic kafka_topic

過濾器配置

Maxwell 可以通過 --filter 配置項來指定過濾規則,通過 exclude 排除,通過 include 包含,值可以為具體的資料庫、資料表、資料列,甚至用 Javascript 來定義複雜的過濾規則;可以用正規表示式描述,有幾個來自官網的例子

# 僅匹配foodb資料庫的tbl表和所有table_數字的表
--filter='exclude: foodb.*, include: foodb.tbl, include: foodb./table_\d+/'
# 排除所有庫所有表,僅匹配db1資料庫
--filter = 'exclude: *.*, include: db1.*'
# 排除含db.tbl.col列值為reject的所有更新
--filter = 'exclude: db.tbl.col = reject'
# 排除任何包含col_a列的更新
--filter = 'exclude: *.*.col_a = *'
# blacklist 黑名單,完全排除bad_db資料庫,若要恢復,必須刪除maxwell庫
--filter = 'blacklist: bad_db.*' 
複製程式碼

資料初始化

Maxwell 啟動後將從maxwell庫中獲取上一次停止時position,從該斷點處開始讀取binlog。如果binlog已經清除了,那麼怎樣可以通過maxwell把整張表都複製出來呢?也就是資料初始化該怎麼做?

對整張表進行操作,人為地產生binlog?譬如找一個不影響業務的欄位譬如update_time,然後加一秒,再減一秒?

update test set update_time = DATE_ADD(update_time,intever 1 second);
update test set update_time = DATE_ADD(update_time,intever -1 second);
複製程式碼

這樣明視訊記憶體在幾個大問題:

  • 不存在一個不重要的欄位怎麼辦?每個欄位都很重要,不能隨便地修改!
  • 如果整張表很大,修改的過程耗時很長,影響了業務!
  • 將產生大量非業務的binlog!

針對資料初始化的問題,Maxwell 提供了一個命令工具 maxwell-bootstrap 幫助我們完成資料初始化,maxwell-bootstrap 是基於 SELECT * FROM table 的方式進行全量資料初始化,不會產生多餘的binlog!

這個工具有下面這些引數:

引數 說明
--log_level LOG_LEVEL 日誌級別(DEBUG, INFO, WARN or ERROR)
--user USER mysql 使用者名稱
--password PASSWORD mysql 密碼
--host HOST mysql 地址
--port PORT mysql 埠
--database DATABASE 要bootstrap的表所在的資料庫
--table TABLE 要引導的表
--where WHERE_CLAUSE 設定過濾條件
--client_id CLIENT_ID 指定執行引導操作的Maxwell例項

實驗一番,下面將引導 test 資料庫中 test 表,首先是準備幾條測試用的資料

INSERT INTO `test` VALUES (1, 1, '1');
INSERT INTO `test` VALUES (2, 2, '2');
INSERT INTO `test` VALUES (3, 3, '3');
INSERT INTO `test` VALUES (4, 4, '4');
複製程式碼

然後 reset master; 清空binlog,刪除 maxwell 庫中的表。接著使用快速開始中的命令,啟動Kafka、Maxwell和Kafka消費者,然後啟動 maxwell-bootstrap

docker run -it --rm zendesk/maxwell bin/maxwell-bootstrap --user maxwell  \
    --password 123456 --host 10.100.97.246  --database test --table test --client_id maxwell
複製程式碼

注意:--bootstrapper=sync 時,在處理bootstrap時,會阻塞正常的binlog解析;--bootstrapper=async 時,不會阻塞。

也可以執行下面的SQL,在 maxwell.bootstrap 表中插入記錄,手動觸發

insert into maxwell.bootstrap (database_name, table_name) values ('test', 'test');
複製程式碼

就可以在 kafka 消費者端看見引導過來的資料了

{"database":"maxwell","table":"bootstrap","type":"insert","ts":1552199115,"xid":36738,"commit":true,"data":{"id":3,"database_name":"test","table_name":"test","where_clause":null,"is_complete":0,"inserted_rows":0,"total_rows":0,"created_at":null,"started_at":null,"completed_at":null,"binlog_file":null,"binlog_position":0,"client_id":"maxwell"}}
{"database":"test","table":"test","type":"bootstrap-start","ts":1552199115,"data":{}}
{"database":"test","table":"test","type":"bootstrap-insert","ts":1552199115,"data":{"id":1,"age":1,"name":"1"}}
{"database":"test","table":"test","type":"bootstrap-insert","ts":1552199115,"data":{"id":2,"age":2,"name":"2"}}
{"database":"test","table":"test","type":"bootstrap-insert","ts":1552199115,"data":{"id":3,"age":3,"name":"3"}}
{"database":"test","table":"test","type":"bootstrap-insert","ts":1552199115,"data":{"id":4,"age":4,"name":"4"}}
{"database":"maxwell","table":"bootstrap","type":"update","ts":1552199115,"xid":36756,"commit":true,"data":{"id":3,"database_name":"test","table_name":"test","where_clause":null,"is_complete":1,"inserted_rows":4,"total_rows":0,"created_at":null,"started_at":"2019-03-10 14:25:15","completed_at":"2019-03-10 14:25:15","binlog_file":"mysql-bin.000001","binlog_position":64446,"client_id":"maxwell"},"old":{"is_complete":0,"inserted_rows":1,"completed_at":null}}
{"database":"test","table":"test","type":"bootstrap-complete","ts":1552199115,"data":{}}
複製程式碼

中間的4條便是 test.test 的binlog資料了,注意這裡的 type 是 bootstrap-insert,而不是 insert

然後再一次檢視binlog,show binlog events;,會發現只有與 maxwell 相關的binlog,並沒有 test.test 相關的binlog,所以 maxwell-bootstrap 命令並不會產生多餘的 binlog,當資料表的數量很大時,這個好處會更加明顯

Bootstrap 的過程是 bootstrap-start -> bootstrap-insert -> bootstrap-complete,其中,start和complete的data欄位為空,不攜帶資料。

在進行bootstrap過程中,如果maxwell崩潰,重啟時,bootstrap會完全重新開始,不管之前進行到多少,若不希望這樣,可以到資料庫中設定 is_complete 欄位值為1(表示完成),或者刪除該行

Maxwell監控

Maxwell 提供了 base logging mechanism, JMX, HTTP or by push to Datadog 這四種監控方式,與監控相關的配置項有下列這些:

選項 引數值 描述 預設值
metrics_prefix STRING 指標的字首 MaxwellMetrics
metrics_type [slf4j | jmx | http | datadog] 釋出指標的方式
metrics_jvm BOOLEAN 是否收集JVM資訊 false
metrics_slf4j_interval SECONDS 將指標記錄到日誌的頻率,metrics_type須配置為slf4j 60
http_port INT metrics_type為http時,釋出指標繫結的埠 8080
http_path_prefix STRING http的路徑字首 /
http_bind_address STRING http釋出指標繫結的地址 all addresses
http_diagnostic BOOLEAN http是否開啟diagnostic字尾 false
http_diagnostic_timeout MILLISECONDS http diagnostic 響應超時時間 10000
metrics_datadog_type [udp | http] metrics_type為datadog時釋出指標的方式 udp
metrics_datadog_tags STRING 提供給 datadog 的標籤,如 tag1:value1,tag2:value2
metrics_datadog_interval INT 推指標到datadog的頻率,單位秒 60
metrics_datadog_apikey STRING metrics_datadog_type=http 時datadog用的api key
metrics_datadog_host STRING metrics_datadog_type=udp時推指標的目標地址 localhost
metrics_datadog_port INT metrics_datadog_type=udp 時推指標的埠 8125

具體可以得到哪些監控指標呢?有如下,注意所有指標都預先配置了指標字首 metrics_prefix

指標 型別 說明
messages.succeeded Counters 成功傳送到kafka的訊息數量
messages.failed Counters 傳送失敗的訊息數量
row.count Counters 已處理的binlog行數,注意並非所有binlog都發往kafka
messages.succeeded.meter Meters 訊息成功傳送到Kafka的速率
messages.failed.meter Meters 訊息傳送失敗到kafka的速率
row.meter Meters 行(row)從binlog聯結器到達maxwell的速率
replication.lag Gauges 從資料庫事務提交到Maxwell處理該事務之間所用的時間(毫秒)
inflightmessages.count Gauges 當前正在處理的訊息數(等待來自目的地的確認,或在訊息之前)
message.publish.time Timers 向kafka傳送record所用的時間(毫秒)
message.publish.age Timers 從資料庫產生事件到傳送到Kafka之間的時間(毫秒),精確度為+/-500ms
replication.queue.time Timers 將一個binlog事件送到處理佇列所用的時間(毫秒)

上述有些指標為kafka特有的,並不支援所有的生產者。

實驗一番,通過 http 方式獲取監控指標

docker run -p 8080:8080 -it --rm zendesk/maxwell bin/maxwell --user='maxwell' \
    --password='123456' --host='10.100.97.246' --producer=kafka \
    --kafka.bootstrap.servers='10.100.97.246:9092' --kafka_topic=maxwell --log_level=debug \
    --metrics_type=http  --metrics_jvm=true --http_port=8080
複製程式碼

上面的配置大部分與前面的相同,不同的有 -p 8080:8080 docker埠對映,以及 --metrics_type=http --metrics_jvm=true --http_port=8080,配置了通過http方式釋出指標,啟用收集JVM資訊,埠為8080,之後可以通過 http://10.100.97.246:8080/metrics 便可獲取所有的指標

Maxwell監控

http 方式有四種字尾,分別對應四種不同的格式

endpoint 說明
/metrics 所有指標以JSON格式返回
/prometheus 所有指標以Prometheus格式返回(Prometheus是一套開源的監控&報警&時間序列資料庫的組合)
/healthcheck 返回Maxwell過去15分鐘是否健康
/ping 簡單的測試,返回 pong

如果是通過 JMX 的方式收集Maxwell監控指標,可以 JAVA_OPTS 環境變數配置JMX訪問許可權

export JAVA_OPTS="-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.local.only=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=10.100.97.246"
複製程式碼

多個Maxwell例項

在不同的配置下,Maxwell可以在同一個主伺服器上執行多個例項。如果希望讓生產者以不同的配置執行,例如將來自不同組的表(table)的事件投遞到不同的Topic中,這將非常有用。Maxwell的每個例項都必須配置一個唯一的client_id,以便區分的binlog位置。

GTID 支援

Maxwell 從1.8.0版本開始支援基於GTID的複製(GTID-based replication),在GTID模式下,Maxwell將在主機更改後透明地選擇新的複製位置。

什麼是GTID Replication?

GTID (Global Transaction ID) 是對於一個已提交事務的編號,並且是一個全域性唯一的編號。

從 MySQL 5.6.5 開始新增了一種基於 GTID 的複製方式。通過 GTID 保證了每個在主庫上提交的事務在叢集中有一個唯一的ID。這種方式強化了資料庫的主備一致性,故障恢復以及容錯能力。

在原來基於二進位制日誌的複製中,從庫需要告知主庫要從哪個偏移量進行增量同步,如果指定錯誤會造成資料的遺漏,從而造成資料的不一致。藉助GTID,在發生主備切換的情況下,MySQL的其它從庫可以自動在新主庫上找到正確的複製位置,這大大簡化了複雜複製拓撲下叢集的維護,也減少了人為設定複製位置發生誤操作的風險。另外,基於GTID的複製可以忽略已經執行過的事務,減少了資料發生不一致的風險。

注意事項

timestamp column

maxwell對時間型別(datetime, timestamp, date)都是當做字串處理的,這也是為了保證資料一致(比如0000-00-00 00:00:00這樣的時間在timestamp裡是非法的,但mysql卻認,解析成java或者python型別就是null/None)。

如果MySQL表上的欄位是 timestamp 型別,是有時區的概念,binlog解析出來的是標準UTC時間,但使用者看到的是本地時間。比如 f_create_time timestamp 建立時間是北京時間 2018-01-05 21:01:01,那麼mysql實際儲存的是 2018-01-05 13:01:01,binlog裡面也是這個時間字串。如果不做消費者不做時區轉換,會少8個小時。

與其每個客戶端都要考慮這個問題,我覺得更合理的做法是提供時區引數,然後maxwell自動處理時區問題,否則要麼客戶端先需要知道哪些列是timestamp型別,或者連線上原庫快取上這些型別。

binary column

maxwell可以處理binary型別的列,如blob、varbinary,它的做法就是對二進位制列使用 base64_encode,當做字串輸出到json。消費者拿到這個列資料後,不能直接拼裝,需要 base64_decode

表結構不同步

如果是拿比較老的binlog,放到新的mysql server上去用maxwell拉去,有可能表結構已經發生了變化,比如binlog裡面欄位比 schema_host 裡面的欄位多一個。目前這種情況沒有發現異常,比如阿里RDS預設會為 無主鍵無唯一索引的表,增加一個__##alibaba_rds_rowid##__,在 show create tableschema 裡面都看不到這個隱藏主鍵,但binlog裡面會有,同步到從庫。

另外我們有通過git去管理結構版本,如果真有這種場景,也可以應對。

大事務binlog

當一個事物產生的binlog量非常大的時候,比如遷移日表資料,maxwell為了控制記憶體使用,會自動將處理不過來的binlog放到檔案系統

Using kafka version: 0.11.0.1
21:16:07,109 WARN  MaxwellMetrics - Metrics will not be exposed: metricsReportingType not configured.
21:16:07,380 INFO  SchemaStoreSchema - Creating maxwell database
21:16:07,540 INFO  Maxwell - Maxwell v?? is booting (RabbitmqProducer), starting at Position[BinlogPosition[mysql-bin.006235:24980714],
lastHeartbeat=0]
21:16:07,649 INFO  AbstractSchemaStore - Maxwell is capturing initial schema
21:16:08,267 INFO  BinlogConnectorReplicator - Setting initial binlog pos to: mysql-bin.006235:24980714
21:16:08,324 INFO  BinaryLogClient - Connected to rm-xxxxxxxxxxx.mysql.rds.aliyuncs.com:3306 at mysql-bin.006235/24980714 (sid:637
9, cid:9182598)
21:16:08,325 INFO  BinlogConnectorLifecycleListener - Binlog connected.
03:15:36,104 INFO  ListWithDiskBuffer - Overflowed in-memory buffer, spilling over into /tmp/maxwell7935334910787514257events
03:17:14,880 INFO  ListWithDiskBuffer - Overflowed in-memory buffer, spilling over into /tmp/maxwell3143086481692829045events
複製程式碼

但是遇到另外一個問題,overflow隨後就出現異常 EventDataDeserializationException: Failed to deserialize data of EventHeaderV4,當我另起一個maxwell指點之前的binlog postion開始解析,卻有沒有拋異常。事後的資料也表明並沒有資料丟失。

問題產生的原因還不明,Caused by: java.net.SocketException: Connection reset,感覺像讀取 binlog 流的時候還沒讀取到完整的event,異常關閉了連線。這個問題比較頑固,github上面類似問題都沒有達到明確的解決。(這也從側面告訴我們,大表資料遷移,也要批量進行,不要一個insert into .. select 搞定)

03:18:20,586 INFO  ListWithDiskBuffer - Overflowed in-memory buffer, spilling over into /tmp/maxwell5229190074667071141events
03:19:31,289 WARN  BinlogConnectorLifecycleListener - Communication failure.
com.github.shyiko.mysql.binlog.event.deserialization.EventDataDeserializationException: Failed to deserialize data of EventHeaderV4{time
stamp=1514920657000, eventType=WRITE_ROWS, serverId=2115082720, headerLength=19, dataLength=8155, nextPosition=520539918, flags=0}
        at com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer.deserializeEventData(EventDeserializer.java:216) ~[mys
ql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer.nextEvent(EventDeserializer.java:184) ~[mysql-binlog-c
onnector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:890) [mysql-binlog-connector-java-0
.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:559) [mysql-binlog-connector-java-0.13.0.jar:0.13
.0]
        at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:793) [mysql-binlog-connector-java-0.13.0.jar:0.13.0
]
        at java.lang.Thread.run(Thread.java:745) [?:1.8.0_121]
Caused by: java.net.SocketException: Connection reset
        at java.net.SocketInputStream.read(SocketInputStream.java:210) ~[?:1.8.0_121]
        at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[?:1.8.0_121]
        at com.github.shyiko.mysql.binlog.io.BufferedSocketInputStream.read(BufferedSocketInputStream.java:51) ~[mysql-binlog-connector-
java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.io.ByteArrayInputStream.readWithinBlockBoundaries(ByteArrayInputStream.java:202) ~[mysql-binlo
g-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.io.ByteArrayInputStream.read(ByteArrayInputStream.java:184) ~[mysql-binlog-connector-java-0.13
.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.io.ByteArrayInputStream.readInteger(ByteArrayInputStream.java:46) ~[mysql-binlog-connector-jav
a-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.AbstractRowsEventDataDeserializer.deserializeLong(AbstractRowsEventDataD
eserializer.java:212) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.AbstractRowsEventDataDeserializer.deserializeCell(AbstractRowsEventDataD
eserializer.java:150) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.AbstractRowsEventDataDeserializer.deserializeRow(AbstractRowsEventDataDeserializer.java:132) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.WriteRowsEventDataDeserializer.deserializeRows(WriteRowsEventDataDeserializer.java:64) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.WriteRowsEventDataDeserializer.deserialize(WriteRowsEventDataDeserializer.java:56) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.WriteRowsEventDataDeserializer.deserialize(WriteRowsEventDataDeserializer.java:32) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        at com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer.deserializeEventData(EventDeserializer.java:210) ~[mysql-binlog-connector-java-0.13.0.jar:0.13.0]
        ... 5 more
03:19:31,514 INFO  BinlogConnectorLifecycleListener - Binlog disconnected.
03:19:31,590 WARN  BinlogConnectorReplicator - replicator stopped at position: mysql-bin.006236:520531744 -- restarting
03:19:31,595 INFO  BinaryLogClient - Connected to rm-xxxxxx.mysql.rds.aliyuncs.com:3306 at mysql-bin.006236/520531744 (sid:6379, cid:9220521)
複製程式碼

tableMapCache

前面講過,如果我只想獲取某幾個表的binlog變更,需要用 include_tables 來過濾,但如果mysql server上現在刪了一個表t1,但我的binlog是從昨天開始讀取,被刪的那個表t1在maxwell啟動的時候是拉取不到表結構的。然後昨天的binlog裡面有 t1 的變更,因為找不到表結構給來組裝成json,會拋異常。

手動在 maxwell.tables/columns 裡面插入記錄是可行的。但這個問題的根本是,maxwell在binlog過濾的時候,只在處理row_event的時候,而對 tableMapCache 要求binlog裡面的所有表都要有。

自己(seanlook)提交了一個commit,可以在做 tableMapCache 的時候也僅要求快取 include_dbs/tables 這些表: https://github.com/seanlook/maxwell/commit/2618b70303078bf910a1981b69943cca75ee04fb

提高消費效能

在用rabbitmq時,routing_key%db%.%table%,但某些表產生的binlog增量非常大,就會導致各佇列訊息量很不平均,目前因為還沒做到事務xid或者thread_id級別的併發回放,所以最小佇列粒度也是表,儘量單獨放一個佇列,其它資料量小的合在一起。

binlog

Maxwell 在 maxwell 庫中維護了 binlog 的位移等資訊,由於一些原因譬如 reset master;,導致 maxwell 庫中的記錄與實際的binlog對不上,這時將報異常,這是可以手動修正binlog位移或者直接清空/刪除 maxwell 庫重建

com.github.shyiko.mysql.binlog.network.ServerException: Could not find first log file name in binary log index file
        at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:885)
        at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:564)
        at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:796)
        at java.lang.Thread.run(Thread.java:748)
複製程式碼

以及

com.github.shyiko.mysql.binlog.network.ServerException: A slave with the same server_uuid/server_id as this slave has connected to the master; the first event 'mysql-bin.000001' at 760357, the last event read from './mysql-bin.000001' at 1888540, the last byte read from './mysql-bin.000001' at 1888540.
        at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:885)
        at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:564)
        at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:796)
        at java.lang.Thread.run(Thread.java:748)
複製程式碼

參考文件

關注_小旋鋒_微信公眾號

相關文章