canal解读(结合RocketMQ)

660 阅读7分钟

1.canal介绍:

是用于数据同步的中间件。 依靠主从同步的原理向MySql数据库拉取binlog日志(ROW格式,记录的是数据库变更前后的数据变化),解析后转发到下游。

**性能比较好,主要的性能消耗在binlog解析、io的消耗、操作数据时锁的竞争。 **

可以应用于异步的Mysql与Redis的数据缓存同步:一个比较重要的应用就是利用canal server解析将数据转发到消息队列上,用于消息的异步更新。

**以下介绍主要流程:
解析->(过滤)->put->get->send **

解析是将binlog日志解析为数据变更事件Event,之后需要放入存储区域中,使用Memory内存

package com.alibaba.otter.canal.store.memory 下的 MemoryEventStoreWithBuffer 封装了内存缓冲区及其操作

**包含put get ack三个位点:(三个操作安全的原子类)

  1. put 最后存放位点
  2. get 最后获取位点
  3. ack 最后确认成功发送位点**

B8E4287522E7C565928CDE17562AB71A.png

2.canal 中内存缓冲区Ringbuffer

image.png

1) put方法

put方法需要获取final ReentrantLock lock = this.lock 同步锁;(分为超时放弃和阻塞获取) 获取这个锁之后调用doPut

  private void doPut(List<Event> data) {
    long current = putSequence.get();
    long end = current + data.size();

    // 先写数据,再更新对应的cursor,并发度高的情况,putSequence会被get请求可见,拿出了ringbuffer中的老的Entry值
    for (long next = current + 1; next <= end; next++) {
        entries[getIndex(next)] = data.get((int) (next - current - 1));
    }

    putSequence.set(end);//记录最终的put位置
    // 记录一下gets memsize信息,方便快速检索
    if (batchMode.isMemSize()) {
        long size = 0;
        for (Event event : data) {
            size += calculateSize(event);
        }

        putMemSize.getAndAdd(size);
    }
    profiling(data, OP.PUT);
    // tell other threads that store is not empty
    notEmpty.signal();
}

其中向数据缓存区中先存入数据再 putSequence.set(end);再记录put位点;避免拿到循环队列中老的值

2)get方法

执行doGet之前依然需要获取final ReentrantLock lock = this.lock 同步锁,保证线程安全

private Events<Event> doGet(Position start, int batchSize) throws CanalStoreException {
    LogPosition startPosition = (LogPosition) start;

    long current = getSequence.get();
    long maxAbleSequence = putSequence.get();
    long next = current;
    long end = current;
    // 如果startPosition为null,说明是第一次,默认+1处理
    if (startPosition == null || !startPosition.getPostion().isIncluded()) { // 第一次订阅之后,需要包含一下start位置,防止丢失第一条记录
        next = next + 1;
    }

    if (current >= maxAbleSequence) {
        return new Events<>();
    }

    Events<Event> result = new Events<>();
    List<Event> entrys = result.getEvents();
    long memsize = 0;
    if (batchMode.isItemSize()) {
        end = (next + batchSize - 1) < maxAbleSequence ? (next + batchSize - 1) : maxAbleSequence;
        // 提取数据并返回
        for (; next <= end; next++) {
            Event event = entries[getIndex(next)];
            if (ddlIsolation && isDdl(event.getEventType())) {
                // 如果是ddl隔离,直接返回
                if (entrys.size() == 0) {
                    entrys.add(event);// 如果没有DML事件,加入当前的DDL事件
                    end = next; // 更新end为当前
                } else {
                    // 如果之前已经有DML事件,直接返回了,因为不包含当前next这记录,需要回退一个位置
                    end = next - 1; // next-1一定大于current,不需要判断
                }
                break;
            } else {
                entrys.add(event);
            }
        }
    } else {
        long maxMemSize = batchSize * bufferMemUnit;
        for (; memsize <= maxMemSize && next <= maxAbleSequence; next++) {
            // 永远保证可以取出第一条的记录,避免死锁
            Event event = entries[getIndex(next)];
            if (ddlIsolation && isDdl(event.getEventType())) {
                // 如果是ddl隔离,直接返回
                if (entrys.size() == 0) {
                    entrys.add(event);// 如果没有DML事件,加入当前的DDL事件
                    end = next; // 更新end为当前
                } else {
                    // 如果之前已经有DML事件,直接返回了,因为不包含当前next这记录,需要回退一个位置
                    end = next - 1; // next-1一定大于current,不需要判断
                }
                break;
            } else {
                entrys.add(event);
                memsize += calculateSize(event);
                end = next;// 记录end位点
            }
        }

    }

    PositionRange<LogPosition> range = new PositionRange<>();
    result.setPositionRange(range);

    range.setStart(CanalEventUtils.createPosition(entrys.get(0)));
    range.setEnd(CanalEventUtils.createPosition(entrys.get(result.getEvents().size() - 1)));
    range.setEndSeq(end);
    // 记录一下是否存在可以被ack的点

    for (int i = entrys.size() - 1; i >= 0; i--) {
        Event event = entrys.get(i);
        // GTID模式,ack的位点必须是事务结尾,因为下一次订阅的时候mysql会发送这个gtid之后的next,如果在事务头就记录了会丢这最后一个事务
        if ((CanalEntry.EntryType.TRANSACTIONBEGIN == event.getEntryType() && StringUtils.isEmpty(event.getGtid()))
            || CanalEntry.EntryType.TRANSACTIONEND == event.getEntryType() || isDdl(event.getEventType())) {
            // 将事务头/尾设置可被为ack的点
            range.setAck(CanalEventUtils.createPosition(event));
            break;
        }
    }

    if (getSequence.compareAndSet(current, end)) {
        getMemSize.addAndGet(memsize);
        notFull.signal();
        profiling(result.getEvents(), OP.GET);
        return result;
    } else {
        return new Events<>();
    }
}

两个关键点:1)DDL隔离 也就是DDL独占一个Entrys(发送消息的时候会再次进行拆分)

2)getSequence.compareAndSet(current, end) 获取Entrys后更新get位点

3)rollBack方法

也是一样先获取同步锁;之后回滚getSequence......

public void rollback() throws CanalStoreException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        getSequence.set(ackSequence.get());
        getMemSize.set(ackMemSize.get());
    } finally {
        lock.unlock();
    }
}

可以看到回滚了get位点到ack位点上,没有回滚put位点。

github.com/alibaba/can…
本文在阿里开源代码中已有很多注释,本人做了一下总结,用于个人记录也用于分享。

4)支持流式批量的getBatch

5F92B47E9C4760101D3630DAEB770C81.png 认为出现发送到canal client(RocketMQ)失败的概率非常小,可以支持批量从环形队列中获取消息,减少加锁次数提高性能。按流的顺序ack,一旦一个前面的消息发送到重试上限,失败后会回滚后面发送的所有消息,认为后面发送的所有消息失败。 发送成功后更新position,文件消费位置。

3.问题:

  1. canal可能丢消息吗?
    canal默认依赖于文件的position位置记录消费位置,如果持久化存储的position不正确就可能丢信息。但如果position正确,最多只可能多发重复信息,RocketMQ消费端可以做一些幂等逻辑判断。
  2. canal异步更新缓存优势在于什么?
    一是将更新缓存的任务与用户线程解耦;二是异步更新缓存,方便合并写请求并支持批处理操作。
  3. canal崩溃后会丢消息吗?
    只要position位置没有置后,最多只可能多发送一些重复消息。
  4. canal优缺点?
    优点:对业务代码无侵入性、解析性能比较好。
    缺点:开启binlog会损耗Mysql数据库性能;canal内部的环形队列基于内存,ack后消息变为可覆盖,不支持历史数据的回溯。

4.一般的整体流程

整体流程

(BinLogEvent 解析为——> Canal的Entry即canal自定义的事件, message封装List send——> List 轻量级消息)

注意,支持List,说明canal支持将多个消息打包成批量消息发送到消息队列,也就是说明支持 消费者端批量处理消息。(这个List也可以支持让List里面的消息一条一条发送,批量则是一个整体)

5. canal性能提升的一些点

1)原生 使用线程池的方式多线程接收Binlog 、

2)解析效率-算法效率高

3)获取消息-RingBuffer队列支持异步批量获取数据 无需get位点被ack、

4)反序列化和batch开销(send什么?性能优化点)

flatMessage模式,性能普遍比不开启flatMessage模式慢,主要就是protoobject的反序列化开销和小batch的发送成本,也就是说flatMessage是解析好的,而非flatMessage模式是没有解析好的message,相当于节约了一个步骤,发送到client端。后续可以通过更多的topic+分区追回。 (Message = List)

5)可以依据业务提升网络IO:可以发送批量消息,通过合理提高get一个批次的数据量,增大一次网络IO的数量提升效率(但太大会导致MQ端响应缓慢,影响MQ的写入)。

6) 合理设置topic+分区;对于MQ来说增加性能。
下面是阿里官方测试:

canal的性能经过阿里测试,性能TPS,从解析到存储到转发,性能是越来越低的。官方建议自己开发时可以考虑避开存入内存缓冲区

但我认为可能会有大量的数据解析,通过缓冲区方便管理数据。

下面是阿里官方给的性能测试结果: 说明阶段越早结束,性能越高,比如不发送到消息队列,而是直接从canal内存缓冲区中获取消息去更新redis缓存,性能更高,但是随之而来的是数据管理问题及消息积压问题等。

7)binlog parser优化 并发解析模型 (针对瓶颈点采用了多线程的模式提升吞吐)