canal写kafka,zookeeper中cursor更新异常

一.相关组件

  • 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的配置进行更新。

四.解决过程

  1. 当canal.instance.gtidon=false,此时server直接写kafka是可以正常更新offset的
  2. 当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);
        }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值