本文記錄在Canal實戰中遇到的問題和重點
一、關于高可用 HA
canal的高可用,有幾個維度
1.數據庫高可用
目前canal支持 數據庫的master和standby,但需要修改默認配置打開
在zk的cursor中可以看到數據庫信息,是否正常切換
數據庫HA 實現分析
由于我們知道canal是訂閱binlog的,那么當主宕機后,canal去連接standby,如何找到新的position呢,我們進行簡單分析
首先了解mysql主從復制的基本流程,主維護一個binlog,從會維護一個relay log,進行回放。
通常情況下,從并不記錄自己的binlog,因為要避免多個從引起的多次寫入。但如果只有一個從的情況下,可以開啟--log-slave-updates
這樣從也會維護一份binlog,但這些都不重要,你需要知道確認的一點是 不管從維護不維護binlog,主從的binlog都是不一樣的,你單獨連數據庫,查看binlog名稱就可以看出來!
回過來分析canal的數據庫HA
關鍵的類為 HeartBeatHAController
這個類反向追蹤,會在instance模塊的初始化doInitEventParser中創建。
心跳檢測
MysqlDetectingTimeTask
首先會有一個核心的心跳任務,默認每隔3s執行一次心跳,配置為detectingIntervalInSeconds
timer.schedule(heartBeatTimerTask, interval * 1000L, interval * 1000L);
這個task的任務就是去檢測心跳,默認我們通過select 1語句去進行檢測
public void run() {
try {
if (reconnect) {
reconnect = false;
mysqlConnection.reconnect();
} else if (!mysqlConnection.isConnected()) {
mysqlConnection.connect();
}
Long startTime = System.currentTimeMillis();
// 可能心跳sql為select 1
if (StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "select")
|| StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "show")
|| StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "explain")
|| StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "desc")) {
mysqlConnection.query(detectingSQL);
} else {
mysqlConnection.update(detectingSQL);
}
Long costTime = System.currentTimeMillis() - startTime;
if (haController != null && haController instanceof HeartBeatCallback) {
((HeartBeatCallback) haController).onSuccess(costTime);
}
} catch (SocketTimeoutException e) {
if (haController != null && haController instanceof HeartBeatCallback) {
((HeartBeatCallback) haController).onFailed(e);
}
reconnect = true;
logger.warn("connect failed by ", e);
} catch (IOException e) {
if (haController != null && haController instanceof HeartBeatCallback) {
((HeartBeatCallback) haController).onFailed(e);
}
reconnect = true;
logger.warn("connect failed by ", e);
} catch (Throwable e) {
if (haController != null && haController instanceof HeartBeatCallback) {
((HeartBeatCallback) haController).onFailed(e);
}
reconnect = true;
logger.warn("connect failed by ", e);
}
}
當3次檢測失敗后,執行切換doSwitch方法
public void onFailed(Throwable e) {
failedTimes++;
// 檢查一下是否超過失敗次數
synchronized (this) {
if (failedTimes > detectingRetryTimes) {
if (switchEnable) {
eventParser.doSwitch();// 通知執行一次切換
failedTimes = 0;
} else {
logger.warn("HeartBeat failed Times:{} , should auto switch ?", failedTimes);
}
}
}
}
切換時的業務邏輯
// 處理主備切換的邏輯
public void doSwitch() {
AuthenticationInfo newRunningInfo = (runningInfo.equals(masterInfo) ? standbyInfo : masterInfo);
this.doSwitch(newRunningInfo);
}
public void doSwitch(AuthenticationInfo newRunningInfo) {
// 1. 需要停止當前正在復制的過程
// 2. 找到新的position點
// 3. 重新建立鏈接,開始復制數據
// 切換ip
String alarmMessage = null;
if (this.runningInfo.equals(newRunningInfo)) {
alarmMessage = "same runingInfo switch again : " + runningInfo.getAddress().toString();
logger.warn(alarmMessage);
return;
}
if (newRunningInfo == null) {
alarmMessage = "no standby config, just do nothing, will continue try:"
+ runningInfo.getAddress().toString();
logger.warn(alarmMessage);
sendAlarm(destination, alarmMessage);
return;
} else {
stop();
alarmMessage = "try to ha switch, old:" + runningInfo.getAddress().toString() + ", new:"
+ newRunningInfo.getAddress().toString();
logger.warn(alarmMessage);
sendAlarm(destination, alarmMessage);
runningInfo = newRunningInfo;
start();
}
}
首先是判斷正在運行的是主還是從,支持來回切換。
接下來可以清晰的看到,先執行stop,停止目前的復制,再是輸出日志,最后重新start,此時start的running 已經采用新的切換后的Info。
重新啟動start后,這時候主要就要去搞清楚,是如何找到新的position
// 4. 獲取最后的位置信息
long start = System.currentTimeMillis();
logger.warn("---> begin to find start position, it will be long time for reset or first position");
EntryPosition position = findStartPosition(erosaConnection);
protected EntryPosition findStartPosition(ErosaConnection connection) throws IOException {
if (isGTIDMode()) {
// GTID模式下,CanalLogPositionManager里取最后的gtid,沒有則取instanc配置中的
LogPosition logPosition = getLogPositionManager().getLatestIndexBy(destination);
if (logPosition != null) {
return logPosition.getPostion();
}
if (masterPosition != null && StringUtils.isNotEmpty(masterPosition.getGtid())) {
return masterPosition;
}
}
EntryPosition startPosition = findStartPositionInternal(connection);
if (needTransactionPosition.get()) {
logger.warn("prepare to find last position : {}", startPosition.toString());
Long preTransactionStartPosition = findTransactionBeginPosition(connection, startPosition);
if (!preTransactionStartPosition.equals(startPosition.getPosition())) {
logger.warn("find new start Transaction Position , old : {} , new : {}",
startPosition.getPosition(),
preTransactionStartPosition);
startPosition.setPosition(preTransactionStartPosition);
}
needTransactionPosition.compareAndSet(true, false);
}
return startPosition;
}
- 當使用gtid時,可以看到是通過gtid來進行新的postion定位的
- 不使用gtid時
protected EntryPosition findStartPositionInternal(ErosaConnection connection) {
MysqlConnection mysqlConnection = (MysqlConnection) connection;
LogPosition logPosition = logPositionManager.getLatestIndexBy(destination);
if (logPosition == null) {// 找不到歷史成功記錄
.....
} else {
if (logPosition.getIdentity().getSourceAddress().equals(mysqlConnection.getConnector().getAddress())) {
....
} else {
// 針對切換的情況,考慮回退時間
long newStartTimestamp = logPosition.getPostion().getTimestamp() - fallbackIntervalInSeconds * 1000;
logger.warn("prepare to find start position by switch {}:{}:{}", new Object[] { "", "",
logPosition.getPostion().getTimestamp() });
return findByStartTimeStamp(mysqlConnection, newStartTimestamp);
}
}
}
一般生產我們使用zk進行position的保存,可以看到針對切換的情況,可以看到是通過時間戳進行定位的
canal.instance.fallbackIntervalInSeconds
canal發生mysql切換時,在新的mysql庫上查找binlog時需要往前查找的時間,單位秒
說明:mysql主備庫可能存在解析延遲或者時鐘不統一,需要回退一段時間,保證數據不丟
默認60
然后就在binlog里面根據時間找到startPosition
- 其他:我們公司采用了proxy的策略,作為數據庫的路由,經測試發現,在主庫宕機時,切換過程30s,無法提供服務,所以仍然可以心跳失敗,重新尋找位點,這里有個坑,重新尋找位點后,雖然不影響業務,但zk中的serverid和binlogName對應不起來
2.canal Server的高可用
canal的高可用,需要通過zookeeper實現,這個也簡單,就是啟動2個server,會進行搶占,在zk上可以看到,destinations節點中的信息,有利于幫助理解實現。雖然在destinations會顯示多個 server,但是在running那里可以看到,最終執行的server,掛掉一個,會自動切換
3.client的高可用
這個我得試試,首先在官方example中,有2種connector,一直是SimpleCanalConnector,另一種是ClusterCanalConnector
- SimpleCanalConnector
2個同時打開,由于每次connector都會釋放,所以2個程序一直while循環時,建立連接的會得到數據,每次都不固定 - clusterCanalConnector
打開第一個后,在zk上1001下可以看到running的client信息,打開第二個只會和zk保持ping的信息
當關閉第一個后,第二個會開始工作,zk上的running信息改變
二.數據一致性問題
方案評估的時候,被否定,在mysql主從結構中,復制的機制,如果mysql沒有問題,但是復制機制出了問題,出現主主或者主從數據不一致的情況 。是否有報警,是否能恢復?
1.mysql 的復制機制
復制如何工作
整體上來說,復制有3個步驟:
(1) master將改變記錄到二進制日志(binary log)中(這些記錄叫做二進制日志事件,binary log events);
(2) slave將master的binary log events拷貝到它的中繼日志(relay log);
(3) slave重做中繼日志中的事件,將改變反映它自己的數據。
將event的讀取和執行分開,避免被執行時拖慢 ,這時是分為了IO線程和SQL線程
2.canal的工作機制
原理相對比較簡單:
canal模擬mysql slave的交互協議,偽裝自己為mysql slave,向mysql master發送dump協議
mysql master收到dump請求,開始推送binary log給slave(也就是canal)
canal解析binary log對象(原始為byte流)
3.數據庫的異步、半同步、全同步
參考文檔:http://www.lxweimin.com/writer#/notebooks/15190526/notes/30341321
- 有了以上的基礎知識,進行分析,當canal在數據庫服務器高可用時,假設服務器是主主半同步A,B,由于proxy高可用,存在情況,寫入的是A,但canal當時監聽的是B
此時,A的binlog需要先到B的中繼日志,再回放寫到B中,半同步只能保證A的binlog到了某一個從的relaylog,其余無法保證,如果B回放過程出現問題,但沒有報警機制,確實可能會出現問題。
三、binlog的查看
3.1binlog的基礎知識
binlog主要用于復制和復原,日志由一組二進制日志文件和一個索引文件組成如
HOSTNAME-bin.0000101
HOSTNAME-bin.0000102
HOSTNAME-bin.0000103
HOSTNAME-bin.index
1.每個日志文件包含一個4字節的幻數,后跟一組描述數據修改的事件:
幻數字節是0xfe 0x62 0x69 0x6e = 0xfe'b''i''n'(這是BINLOG_MAGIC 常數log_event.h)
每個事件都包含頭字節,后跟數據字節:
標頭字節提供有關事件類型,生成時間,服務器等的信息。
數據字節提供特定于事件類型的信息,例如特定的數據修改。
2.第一個事件是描述符事件,它描述文件的格式版本(用于在文件中寫入事件的格式)。
3.其余事件根據版本進行解釋。
4.最后一個事件是一個日志輪換事件,它指定下一個二進制日志文件名。
5.索引文件是一個文本文件,列出了當前的二進制日志文件。
所有的事件類型可以參照官網:https://dev.mysql.com/doc/internals/en/event-classes-and-types.html
事件意義為https://dev.mysql.com/doc/internals/en/event-meanings.html
canal對此有封裝為內部的類型
3.2 binlog的常見命令
1.查看所有binlog日志列表
mysql> show master logs;
2.查看master狀態,即最后(最新)一個binlog日志的編號名稱,及其最后一個操作事件pos結束點(Position)值
mysql> show master status;
3.刷新log日志,自此刻開始產生一個新編號的binlog日志文件
mysql> flush logs;
注:每當mysqld服務重啟時,會自動執行此命令,刷新binlog日志;在mysqldump備份數據時加 -F 選項也會刷新binlog日志;
4.重置(清空)所有binlog日志
5
show variables like 'log_%'; 查看binlog是否開啟
show variables like 'binlog_%' 查看format
- binlog的2中模式
Statement-based logging: Events contain SQL statements that produce data changes (inserts, updates, deletes)
Row-based logging: Events describe changes to individual rows
所以當選擇row的format時,可能一個sql語句產生很多的記錄,如假設對全表某個字段加1
1.查看具體的binlog
SHOW BINLOG EVENTS
[IN 'log_name']
[FROM pos]
[LIMIT [offset,] row_count]
不允許直接不加任何條件查一個mysql文件,很容易oom
2.登錄數據庫主機,bin/mysqlbinlog目錄下查看
1.指定時間段
mysqlbinlog --start-datetime="2017-01-09 17:50:00" --stop-datetime="2017-01-09 18:00:00" bin.000025
注意時間段的選擇,別超過10s,不然根本看不清
2.指定position
mysqlbinlog --start-postion=107 --stop-position=1000 bin.000025