一.相关组件
- canal 1.1.4
- mysql 5.7.19
- zookeeper 3.4.5
- kafka 2.9.2-0.8.1.1
二.canal的关键配置
# 在server层直接写kafka需要配置这个
canal.serverMode = kafka
# 高可用server需要使用default-instance.xml配置
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
三.问题描述
- 在测试两个server宕机的情况时,发现instance每次重启都从最开始的gtid开始同步,而不是从instance最后的消费的gtid的点开始同步。如此,主备切换就存在极大的问题。
- 排查后发现,是instance的cursor未及时更新,很久才更新,毫无规律可言。而不是按照canal.zookeeper.flush.period的配置进行更新。
四.解决过程
- 当canal.instance.gtidon=false,此时server直接写kafka是可以正常更新offset的
- 当canal.instance.gtidon=true时,server写kafka无法正常更新offset。
原因如下:
1. CanalKafkaProducer类消费完,会commit对应的消息。CanalServerWithEmbedded的ack函数,会判断positionRanges的ack是否为空,空则过滤。问题就是出在这个,positionRanges的ack一直是空的。
1. 根据PositionRange.setAck(),可定位到MemoryEventStoreWithBuffer,MemoryEventStoreWithBuffer判断是gtid模式,就必须有CanalEntry.EntryType.TRANSACTIONEND事件,才会setAck。我这边测试时,一直显示无法拿到CanalEntry.EntryType.TRANSACTIONEND事件。
1. 通过b,我怀疑是部分空事务被过滤,导致MemoryEventStoreWithBuffer无法正确拿到CanalEntry.EntryType.TRANSACTIONEND事件。所以往前的parse\sink模块排查。最终定位到EntryEventSink类的sinkData函数,有一些策略将空事务过滤了。
EntryEventSink的sinkData过滤逻辑如下:
事务>=8192时或距上一个发送到下游的空事务头/尾的超过5s,就会发送这个空事务。
问题如下:
1. 一般第一个发送的是空事务是EntryType.TRANSACTIONBEGIN,根据事务>=8192这条规则,下次发送的还是EntryType.TRANSACTIONBEGIN。也就是说这条规则无法发送EntryType.TRANSACTIONEND。
2. 根据5s发送一次的规则,若每次都是小事务,事务耗时不超过5s,此时这种情况下,也是拿不到EntryType.TRANSACTIONEND。
此时就会出现,一直无法获取到EntryType.TRANSACTIONEND的情况。结合b看,就无法满足更新offset的条件。
EntryEvent的sinkData函数关键源代码
if ((filterTransactionEntry
&& (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND))) {
long currentTimestamp = entry.getHeader().getExecuteTime();
// 基于一定的策略控制,放过空的事务头和尾,便于及时更新数据库位点,表明工作正常
if (lastTransactionCount.incrementAndGet() <= emptyTransctionThresold
&& Math.abs(currentTimestamp - lastTransactionTimestamp) <= emptyTransactionInterval) {
continue;
} else {
lastTransactionCount.set(0L);
lastTransactionTimestamp = currentTimestamp;
}
}
下面代码,在遵循原来代码逻辑的情况下,可以保证,同一个事务的头尾可以同时被发送。如此,就可以正常更新offset了。将打包好的sink包替换到canal的lib目录中即可。
boolean hasRowData = false;
boolean hasHeartBeat = false;
String commitGtid = "";
List<Event> events = new ArrayList<Event>();
for (CanalEntry.Entry entry : entrys) {
if (!doFilter(entry)) {
continue;
}
if (filterTransactionEntry
&& (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND)) {
long currentTimestamp = entry.getHeader().getExecuteTime();
String gtid = entry.getHeader().getGtid();
if (lastTransactionCount.incrementAndGet() <= emptyTransctionThresold
&& Math.abs(currentTimestamp - lastTransactionTimestamp) <= emptyTransactionInterval &&
(StringUtils.isEmpty(gtid) || !commitGtid.equals(gtid))) {
continue;
} else {
lastTransactionCount.set(0L);
lastTransactionTimestamp = currentTimestamp;
commitGtid = gtid;
}
}
hasRowData |= (entry.getEntryType() == EntryType.ROWDATA);
hasHeartBeat |= (entry.getEntryType() == EntryType.HEARTBEAT);
Event event = new Event(new LogIdentity(remoteAddress, -1L), entry, raw);
events.add(event);
}