一、問題描述
搭建的canal是高可用模式,在IDEA裏面進行消費的,但是在服務端進行切換時,出現了數據重複被消費的問題。salve1:11111開啓服務時,往數據庫裏面插入了一條數據,然後又刪除了這條數據,這是Mysql的bin-log會產生兩條日誌,客戶端也獲取到了這個兩條數據。當我把salve1的服務stop.sh關掉之後,salve2:11111開啓了服務,但是在客戶端又重新獲取到了這兩條數據,也就是說在我切換之後數據被重新消費了一遍。之後我又將salve1的服務開啓,將salve2的服務關閉,這個兩條數據又被我的客戶端消費了一遍,也就這兩條數據被消費了6次了,,,,,,總之每次切換服務端數據都會再次被客戶端消費。
網上:Canal會導致消息重複嗎?
答:會,這從兩個大的方面談起。
1)Canal instance初始化時,根據“消費者的Cursor”來確定binlog的起始位置,但是Cursor在ZK中的保存是滯後的(間歇性刷新),所以Canal instance獲得的起始position一定不會大於消費者真實已見的position。
2)Consumer端,因爲某種原因的rollback,也可能導致一個batch內的所有消息重發,此時可能導致重複消費。
我們建議,Consumer端需要保持冪等,對於重複數據可以進行校驗或者replace。對於非冪等操作,比如累加、計費,需要慎重。
二、高可用搭建(HA)
可參考:https://github.com/alibaba/canal/wiki/AdminGuide#user-content-ha%E6%A8%A1%E5%BC%8F%E9%85%8D%E7%BD%AE
1、配置文件修改(兩臺機器都要下載canal,配置都一樣)
① 修改canal.properties,加上zookeeper配置,spring配置選擇default-instance.xml
1 canal.zkServers=hadoop:2181 ##zookeeper的ip和端口號
2 canal.instance.global.spring.xml = classpath:spring/default-instance.xml ##開啓這個(默認被註釋了)
② 創建example目錄,並修改instance.properties
1 canal.instance.mysql.slaveId = 1234 ##另外一臺機器改成1235,保證slaveId不重複即可
2 canal.instance.master.address = hadoop:3306 ##數據庫的ip和端口
3 canal.instance.dbUsername=canal ##Mysql的用戶名 (這個可以沒有)
4 canal.instance.dbPassword=Canal2019! ##Mysql的密碼(這個可以沒有)
2、開啓服務(兩臺都開啓)
到安裝目錄下
./bin/startup.sh (開啓服務)
./bin/stop.sh (關閉服務)
tail -F logs/canal/canal.log (查看日誌)
有以下三句話表示啓動成功
INFO com.alibaba.otter.canal.deployer.CanalStater - ## start the canal server.
INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[*.*.*.*:11111]
INFO com.alibaba.otter.canal.deployer.CanalStater - ## the canal server is running now ...
3、到zookeeper的客戶端查看
查看在活躍的服務端
get /otter/canal/destinations/example/running
{"active":true,"address":"*.*.*.*:11111","cid":1}
查看在活躍的客戶端
get /otter/canal/destinations/example/1001/running
{"active":true,"address":"*.*.*.*:55285","clientId":1001}
查看zookeeper同步的元數據
get /otter/canal/destinations/example/1001/cursor
{"@type":"com.alibaba.otter.canal.protocol.position.LogPosition","identity":{"slaveId":-1,"sourceAddress":{"address":"hadoop","port":3306}},"postion":{"gtid":"","included":false,"journalName":"mysql-bin.000005","position":28477,"serverId":1,"timestamp":1558665144000}}
刪除
rmr /otter/canal/destinations/example
在zookeeper保存 的數據目錄,推薦工具:http://www.onlinedown.net/soft/1222234.htm
三、IDEA客戶端消費
在idea端編寫client消費數據,來回的切換server的時候數據就會不斷的重新消費。需要的maven依賴直接來:
https://mvnrepository.com/ 搜索:canal
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.Message;
import java.net.InetSocketAddress;
import java.util.List;
/**
* @ClassName: NetTest
* @Description: TODO
* @Author: *******
* @Data: 2019/5/15 10:16
* @Version: 1.0
**/
public class NetTest {
public static void main(String args[]) {
String destination = "example";
String username = "canal";
String password = "Canal2019!";
InetSocketAddress inetSocketAddress = new InetSocketAddress("hadoop", 11111);
// 創建鏈接
CanalConnector connector = CanalConnectors.newClusterConnector("hadoop:2181",destination, username, password);
int batchSize = 1000;
int emptyCount = 0;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
int totalEmptyCount = 120;
while (emptyCount < totalEmptyCount) {//120*2秒還沒有數據就斷開
Message message = connector.getWithoutAck(batchSize); // 獲取指定數量的數據
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
emptyCount++;
System.out.println("empty count : " + emptyCount);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
} else {
emptyCount = 0;
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交確認
}
} finally {
connector.disconnect();
}
}
//輸出數據
private static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s , ",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List<Column> columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}
四、問題解決
問題1、數據重複消費
上文提到數據重複消費,不斷的切換server數據就不斷地重新被消費,之所以被會這樣就是zookeeper沒有同步client的meta數據,兩個服務器在zookeeper中保存的元數據不一樣,所以在互相切換的時候,服務端不認爲是一個客戶端在消費(其實每個server有且只能有一個client)。問題的根源找到了,就是在配置高可用的時候,有配置問題。
原來是一個服務器,我直接將所有的文件拷貝過去,所有的配置都是一樣的,後來拷貝的連接數據庫連接不上,查看發現數據的配置canal.instance.master.address 是localhost拷貝過來的文件,後來將localhost改成了數據的真實的ip:*.*.*.*,改完之後高可用就可以用,在使用的過程中出現了數據重複。
排查找到了zookeeper元數據問題,覈對發現get /otter/canal/destinations/example/1001/cursor ,一個服務端的元數據中"address":"hadoop",而另一個的address 是localhost,就是因爲這兩個切換,導致數據重複消費。
解決:
修改conf/example/instance.properties文件,將canal.instance.master.address的值都改成真實的ip,不要用主機映射的名字
例:canal.instance.master.address =192.168.123.45:3306
問題2、canal的服務開啓了,但是連接不上
canal的服務開啓了,查看日誌文件,裏面也確實是啓動了,但是就是連接不上
解決:修改conf/canal.properties 文件,將canal.ip改成本機真實的ip,如果用主機的映射開啓的服務就是:主機名:11111,而你在代碼裏面連接的時候輸入的主機名在解析的時候會被解析成真實的ip,根據這個ip去找相關的服務。而服務端並非ip+端口
建議:canal裏面的所有關於ip的配置最好不要用ip映射的名字(例如:127.0.0.1,localhost,映射名等等),最好都用真實的ip