3、Canal的server模块

一、模块结构

在这里插入图片描述

server模块的核心接口是CanalServer,其有2个实现类CanalServerWithNetty、CanalServerWithEmbeded。关于CanalServer,官方文档中有有以下描述:

在这里插入图片描述

网上解读:

在这里插入图片描述

左边的图

表示的是Canal独立部署。不同的应用通过canal client与canal server进行通信,所有的canal client的请求统一由CanalServerWithNetty接受,之后CanalServerWithNetty会将客户端请求派给CanalServerWithEmbeded 进行真正的处理。CannalServerWithEmbeded内部维护了多个canal instance,每个canal instance伪装成不同的mysql实例的slave,而CanalServerWithEmbeded会根据客户端请求携带的destination参数确定要由哪一个canal instance为其提供服务。

右边的图

是直接在应用中嵌入CanalServerWithEmbeded,不需要独立部署canal。很明显,网络通信环节少了,同步binlog信息的效率肯定更高。但是对于使用者的技术要求比较高。在应用中,我们可以通过CanalServerWithEmbeded.instance()方法来获得CanalServerWithEmbeded实例,这一个单例。

翻看源码,看起来基于netty的实现代码虽然多一点,这其实只是幻觉,CanalServerWithNetty会将所有的请求委派给CanalServerWithEmbedded处理。

二、代码分析

1、CanalServer接口
CanalServer接口继承了CanalLifeCycle接口,主要是为了重新定义start和stop方法,抛出CanalServerException。

/**

  • 对应canal整个服务实例,一个jvm实例只有一份server

  • @author jianghang 2012-7-12 下午01:32:29

  • @version 1.0.0
    */
    public interface CanalServer extends CanalLifeCycle {

    void start() throws CanalServerException;

    void stop() throws CanalServerException;
    }

在这里插入图片描述

而内嵌的方式只有CanalServerWithEmbedded一个类, 是因为CanalServerWithEmbedded又要根据destination选择某个具体的CanalInstance来处理客户端请求,而CanalInstance的实现位于instance模块,我们将在之后分析。因此从canal server的角度来说,CanalServerWithEmbedded才是server模块真正的核心。

在之前的deployer的CanalController里面就是优先进行嵌入式的server启动:

在这里插入图片描述

CanalServerWithNetty和CanalServerWithEmbedded都是单例的,提供了一个静态方法instance()获取对应的实例。回顾前一节分析CanalController源码时,在CanalController构造方法中准备CanalServer的相关代码,就是通过这两个静态方法获取对应的实例的。

在这里插入图片描述

2、CanalServerWithNetty类

public class CanalServerWithNetty extends AbstractCanalLifeCycle implements CanalServer {

//监听的所有客户端请求都会为派给CanalServerWithEmbedded处理
private CanalServerWithEmbedded embeddedServer; // 嵌入式server
//监听的ip和port,client通过此ip和port与服务端通信
private String ip;
private int port;
//netty组件,这个很熟悉
private Channel serverChannel = null;
private ServerBootstrap bootstrap = null;
private ChannelGroup childGroups = null; // socket channel
// container, used to
// close sockets
// explicitly.

private static class SingletonHolder {

    private static final CanalServerWithNetty CANAL_SERVER_WITH_NETTY = new CanalServerWithNetty();
}

private CanalServerWithNetty(){
    this.embeddedServer = CanalServerWithEmbedded.instance();
    this.childGroups = new DefaultChannelGroup();
}

embeddedServer:因为CanalServerWithNetty需要将请求委派给CanalServerWithEmbeded处理,因此其维护了embeddedServer对象。

ip、port:这是netty监听的网络ip和端口,client通过这个ip和端口与server通信

serverChannel、bootstrap:这是netty的API。其中ServerBootstrap用于启动服务端,通过调用其bind方法,返回一个类型为Channel的serverChannel对象,代表服务端通道。

核心的start方法

public void start() {
super.start();
//优先启动内嵌的canal server,因为基于netty的实现需要将请求委派给其处理
if (!embeddedServer.isStart()) {
embeddedServer.start();
}
/* 创建bootstrap实例,参数NioServerSocketChannelFactory也是Netty的API,其接受2个线程池参数
其中第一个线程池是Accept线程池,第二个线程池是woker线程池,
Accept线程池接收到client连接请求后,会将代表client的对象转发给worker线程池处理。
这里属于netty的知识,不熟悉的用户暂时不必深究,简单认为netty使用线程来处理客户端的高并发请求即可。/
this.bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
/

* enable keep-alive mechanism, handle abnormal network connection
* scenarios on OS level. the threshold parameters are depended on OS.
* e.g. On Linux: net.ipv4.tcp_keepalive_time = 300
* net.ipv4.tcp_keepalive_probes = 2 net.ipv4.tcp_keepalive_intvl = 30
/
bootstrap.setOption(“child.keepAlive”, true);
/

* optional parameter.
*/
bootstrap.setOption(“child.tcpNoDelay”, true);

    // 构造对应的pipeline

/pipeline实际上就是netty对客户端请求的处理器链,
可以类比JAVA EE编程中Filter的责任链模式,上一个filter处理完成之后交给下一个filter处理,
只不过在netty中,不再是filter,而是ChannelHandler。
/
bootstrap.setPipelineFactory(() -> {
ChannelPipeline pipelines = Channels.pipeline();
//主要是处理编码、解码。因为网路传输的传入的都是二进制流,FixedHeaderFrameDecoder的作用就是对其进行解析
pipelines.addLast(FixedHeaderFrameDecoder.class.getName(), new FixedHeaderFrameDecoder());
// support to maintain child socket channel.
//处理client与server握手
pipelines.addLast(HandshakeInitializationHandler.class.getName(),
new HandshakeInitializationHandler(childGroups));
//client身份验证
pipelines.addLast(ClientAuthenticationHandler.class.getName(),
new ClientAuthenticationHandler(embeddedServer));
//SessionHandler用于真正的处理客户端请求
SessionHandler sessionHandler = new SessionHandler(embeddedServer);
pipelines.addLast(SessionHandler.class.getName(), sessionHandler);
return pipelines;
});

    // 启动,当bind方法被调用时,netty开始真正的监控某个端口,此时客户端对这个端口的请求可以被接受到
    if (StringUtils.isNotEmpty(ip)) {
        this.serverChannel = bootstrap.bind(new InetSocketAddress(this.ip, this.port));
    } else {
        this.serverChannel = bootstrap.bind(new InetSocketAddress(this.port));
    }
}

3、SessionHandler类

canal处理client请求的核心逻辑都在SessionHandler这个处理器中。注意其在实例化时,传入了embeddedServer对象,前面提过,CanalServerWithNetty要将请求委派给CanalServerWithEmbedded处理,显然SessionHandler也要维护embeddedServer实例。

这里我们主要分析SessionHandler的 messageReceived方法,这个方法表示接受到了一个客户端请求,我们主要看的是SessionHandler如何对客户端请求进行解析,然后委派给CanalServerWithEmbedded处理的。为了体现其转发请求处理的核心逻辑,如下

public class SessionHandler extends SimpleChannelHandler {

//messageReceived方法表示收到客户端请求
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {

//根据客户端发送的网路通信包请求类型type,将请求委派embeddedServer处理
switch (packet.getType()) {
case SUBSCRIPTION://订阅请求

embeddedServer.subscribe(clientIdentity);

break;
case UNSUBSCRIPTION://取消订阅请求

embeddedServer.unsubscribe(clientIdentity);

break;
case GET://获取binlog请求

if (get.getTimeout() == -1) {// 根据客户端是否指定了请求超时时间调用embeddedServer不同方法获取binlog
message = embeddedServer.getWithoutAck(clientIdentity, get.getFetchSize());
} else {

message = embeddedServer.getWithoutAck(clientIdentity,
get.getFetchSize(),
get.getTimeout(),
unit);
}

break;
case CLIENTACK://客户端消费成功ack请求

embeddedServer.ack(clientIdentity, ack.getBatchId());

break;
case CLIENTROLLBACK://客户端消费失败回滚请求

if (rollback.getBatchId() == 0L) {
embeddedServer.rollback(clientIdentity);// 回滚所有批次
} else {
embeddedServer.rollback(clientIdentity, rollback.getBatchId()); // 只回滚单个批次
}

break;
default://无法判断请求类型
NettyUtils.error(400, MessageFormatter.format(“packet type={} is NOT supported!”, packet.getType())
.getMessage(), ctx.getChannel(), null);
break;
}

}

}
SessionHandler对client请求进行解析后,根据请求类型,委派给CanalServerWithEmbedded的相应方法进行处理。因此核心逻辑都在CanalServerWithEmbedded中。

4、CannalServerWithEmbeded类

CanalServerWithEmbedded实现了CanalServer和CanalServiceCan两个接口。其内部维护了一个Map,key为destination,value为对应的CanalInstance,根据客户端请求携带的destination参数将其转发到对应的CanalInstance上去处理。

public class CanalServerWithEmbedded extends AbstractCanalLifeCycle implements CanalServer, CanalService {

private static final Logger        logger  = LoggerFactory.getLogger(CanalServerWithEmbedded.class);
//key为destination,value为对应的CanalInstance。
private Map<String, CanalInstance> canalInstances;
// private Map<ClientIdentity, Position> lastRollbackPostions;
private CanalInstanceGenerator     canalInstanceGenerator;
private int                        metricsPort;
private CanalMetricsService        metrics = NopCanalMetricsService.NOP;
private String                     user;
private String                     passwd;

private static class SingletonHolder {

    private static final CanalServerWithEmbedded CANAL_SERVER_WITH_EMBEDDED = new CanalServerWithEmbedded();
}

public CanalServerWithEmbedded(){
    // 希望也保留用户new单独实例的需求,兼容历史
}

public static CanalServerWithEmbedded instance() {
    return SingletonHolder.CANAL_SERVER_WITH_EMBEDDED;
}

CanalService接口:

public interface CanalService {
//订阅
void subscribe(ClientIdentity clientIdentity) throws CanalServerException;
//取消订阅
void unsubscribe(ClientIdentity clientIdentity) throws CanalServerException;
//比例获取数据,并自动自行ack
Message get(ClientIdentity clientIdentity, int batchSize) throws CanalServerException;
//超时时间内批量获取数据,并自动进行ack
Message get(ClientIdentity clientIdentity, int batchSize, Long timeout, TimeUnit unit) throws CanalServerException;
//批量获取数据,不进行ack
Message getWithoutAck(ClientIdentity clientIdentity, int batchSize) throws CanalServerException;
//超时时间内批量获取数据,不进行ack
Message getWithoutAck(ClientIdentity clientIdentity, int batchSize, Long timeout, TimeUnit unit) throws CanalServerException;
//ack某个批次的数据
void ack(ClientIdentity clientIdentity, long batchId) throws CanalServerException;
//回滚所有没有ack的批次的数据
void rollback(ClientIdentity clientIdentity) throws CanalServerException;
//回滚某个批次的数据
void rollback(ClientIdentity clientIdentity, Long batchId) throws CanalServerException;
}
其中每个方法中都包含了一个ClientIdentity类型参数,这就是客户端身份的标识。

subscribe方法:

/**

  • 客户端订阅,重复订阅时会更新对应的filter信息
    */
    @Override
    public void subscribe(ClientIdentity clientIdentity) throws CanalServerException {
    checkStart(clientIdentity.getDestination());
    //1、根据客户端要订阅的destination,找到对应的CanalInstance
    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    if (!canalInstance.getMetaManager().isStart()) {
    canalInstance.getMetaManager().start();
    }
    //2、通过CanalInstance的CanalMetaManager组件进行元数据管理,记录一下当前这个CanalInstance有客户端在订阅
    canalInstance.getMetaManager().subscribe(clientIdentity); // 执行一下meta订阅
    //3、获取客户端当前订阅的binlog位置(Position),首先尝试从CanalMetaManager中获取
    Position position = canalInstance.getMetaManager().getCursor(clientIdentity);
    if (position == null) {
    //3.1 如果是第一次订阅,尝试从CanalEventStore中获取第一个binlog的位置,作为客户端订阅开始的位置。
    position = canalInstance.getEventStore().getFirstPosition();// 获取一下store中的第一条
    if (position != null) {
    canalInstance.getMetaManager().updateCursor(clientIdentity, position); // 更新一下cursor
    }
    logger.info("subscribe successfully, {} with first position:{} ", clientIdentity, position);
    } else {
    logger.info("subscribe successfully, use last cursor position:{} ", clientIdentity, position);
    }
    //4 通知下订阅关系变化
    canalInstance.subscribeChange(clientIdentity);
    }
    subscribe主要用于处理客户端的订阅请求,目前情况下,一个CanalInstance只能由一个客户端订阅,不过可以重复订阅。订阅主要的处理步骤如下:

1、根据客户端要订阅的destination,找到对应的CanalInstance

2、通过这个CanalInstance的CanalMetaManager组件记录下有客户端订阅。

3、获取客户端当前订阅位置(Position)。首先尝试从CanalMetaManager中获取,CanalMetaManager 中记录了某个client当前订阅binlog的位置信息。如果是第一次订阅,肯定无法获取到这个位置,则尝试从CanalEventStore中获取第一个binlog的位置。从CanalEventStore中获取binlog位置信息的逻辑是:CanalInstance一旦启动,就会立刻去拉取binlog,存储到CanalEventStore中,在第一次订阅的情况下,CanalEventStore中的第一条binlog的位置,就是当前客户端当前消费的开始位置。

4、通知CanalInstance订阅关系变化

unsubscribe方法:

/**

  • 取消订阅
    */
    @Override
    public void unsubscribe(ClientIdentity clientIdentity) throws CanalServerException {
    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    canalInstance.getMetaManager().unsubscribe(clientIdentity); // 执行一下取消meta订阅
    logger.info(“unsubscribe successfully, {}”, clientIdentity);
    }
    unsubscribe方法主要用于取消订阅关系。在下面的代码中,我们可以看到,其实就是找到CanalInstance对应的CanalMetaManager,调用其unsubscribe取消这个订阅记录。需要注意的是,取消订阅并不意味着停止CanalInstance。当某个客户端取消了订阅,还会有新的client来订阅这个CanalInstance,所以不能停。

listAllSubscribe方法:

/**

  • 查询所有的订阅信息
    */
    public List listAllSubscribe(String destination) throws CanalServerException {
    CanalInstance canalInstance = canalInstances.get(destination);
    return canalInstance.getMetaManager().listAllSubscribeInfo(destination);
    }
    这一个管理方法,其作用是列出订阅某个destination的所有client。这里返回的是一个List,目前一个destination只能由一个client订阅。这里之所以返回一个list,是canal原先计划要支持多个client订阅同一个destination。不过,这个功能一直没有实现。所以List中,实际上只会包含一个ClientIdentity。

get方法:

/**
* 获取数据
*
*


* 注意: meta获取和数据的获取需要保证顺序性,优先拿到meta的,一定也会是优先拿到数据,所以需要加同步. (不能出现先拿到meta,拿到第二批数据,这样就会导致数据顺序性出现问题)
*

*/
@Override
public Message get(ClientIdentity clientIdentity, int batchSize) throws CanalServerException {
return get(clientIdentity, batchSize, null, null);
}

/**
* 获取数据,可以指定超时时间.
*
*


* 几种case:
* a. 如果timeout为null,则采用tryGet方式,即时获取
* b. 如果timeout不为null
* 1. timeout为0,则采用get阻塞方式,获取数据,不设置超时,直到有足够的batchSize数据才返回
* 2. timeout不为0,则采用get+timeout方式,获取数据,超时还没有batchSize足够的数据,有多少返回多少
*
* 注意: meta获取和数据的获取需要保证顺序性,优先拿到meta的,一定也会是优先拿到数据,所以需要加同步. (不能出现先拿到meta,拿到第二批数据,这样就会导致数据顺序性出现问题)
*

*/
@Override
public Message get(ClientIdentity clientIdentity, int batchSize, Long timeout, TimeUnit unit)
throws CanalServerException {
checkStart(clientIdentity.getDestination());
checkSubscribe(clientIdentity);
CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
synchronized (canalInstance) {
// 获取到流式数据中的最后一批获取的位置
PositionRange positionRanges = canalInstance.getMetaManager().getLastestBatch(clientIdentity);

        if (positionRanges != null) {
            throw new CanalServerException(String.format("clientId:%s has last batch:[%s] isn't ack , maybe loss data",
                clientIdentity.getClientId(),
                positionRanges));
        }

        Events<Event> events = null;
        Position start = canalInstance.getMetaManager().getCursor(clientIdentity);
        events = getEvents(canalInstance.getEventStore(), start, batchSize, timeout, unit);

        if (CollectionUtils.isEmpty(events.getEvents())) {
            logger.debug("get successfully, clientId:{} batchSize:{} but result is null",
                clientIdentity.getClientId(),
                batchSize);
            return new Message(-1, true, new ArrayList()); // 返回空包,避免生成batchId,浪费性能
        } else {
            // 记录到流式信息
            Long batchId = canalInstance.getMetaManager().addBatch(clientIdentity, events.getPositionRange());
            boolean raw = isRaw(canalInstance.getEventStore());
            List entrys = null;
            if (raw) {
                entrys = Lists.transform(events.getEvents(), Event::getRawEntry);
            } else {
                entrys = Lists.transform(events.getEvents(), Event::getEntry);
            }
            if (logger.isInfoEnabled()) {
                logger.info("get successfully, clientId:{} batchSize:{} real size is {} and result is [batchId:{} , position:{}]",
                    clientIdentity.getClientId(),
                    batchSize,
                    entrys.size(),
                    batchId,
                    events.getPositionRange());
            }
            // 直接提交ack
            ack(clientIdentity, batchId);
            return new Message(batchId, raw, entrys);
        }
    }
}

与getWithoutAck主要流程完全相同,唯一不同的是,在返回数据给用户前,直接进行了ack,而不管客户端消费是否成功。

getWithoutAck方法:

/**
* 不指定 position 获取事件。canal 会记住此 client 最新的 position。

* 如果是第一次 fetch,则会从 canal 中保存的最老一条数据开始输出。
*
*


* 注意: meta获取和数据的获取需要保证顺序性,优先拿到meta的,一定也会是优先拿到数据,所以需要加同步. (不能出现先拿到meta,拿到第二批数据,这样就会导致数据顺序性出现问题)
*

*/
@Override
public Message getWithoutAck(ClientIdentity clientIdentity, int batchSize) throws CanalServerException {
return getWithoutAck(clientIdentity, batchSize, null, null);
}

/**
* 不指定 position 获取事件。canal 会记住此 client 最新的 position。

* 如果是第一次 fetch,则会从 canal 中保存的最老一条数据开始输出。
*
*


* 几种case:
* a. 如果timeout为null,则采用tryGet方式,即时获取
* b. 如果timeout不为null
* 1. timeout为0,则采用get阻塞方式,获取数据,不设置超时,直到有足够的batchSize数据才返回
* 2. timeout不为0,则采用get+timeout方式,获取数据,超时还没有batchSize足够的数据,有多少返回多少
*
* 注意: meta获取和数据的获取需要保证顺序性,优先拿到meta的,一定也会是优先拿到数据,所以需要加同步. (不能出现先拿到meta,拿到第二批数据,这样就会导致数据顺序性出现问题)
*

*/
@Override
public Message getWithoutAck(ClientIdentity clientIdentity, int batchSize, Long timeout, TimeUnit unit)
throws CanalServerException {
checkStart(clientIdentity.getDestination());
checkSubscribe(clientIdentity);

    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    synchronized (canalInstance) {
        // 获取到流式数据中的最后一批获取的位置
        PositionRange<LogPosition> positionRanges = canalInstance.getMetaManager().getLastestBatch(clientIdentity);

        Events<Event> events = null;
        if (positionRanges != null) { // 存在流数据
            events = getEvents(canalInstance.getEventStore(), positionRanges.getStart(), batchSize, timeout, unit);
        } else {// ack后第一次获取
            Position start = canalInstance.getMetaManager().getCursor(clientIdentity);
            if (start == null) { // 第一次,还没有过ack记录,则获取当前store中的第一条
                start = canalInstance.getEventStore().getFirstPosition();
            }

            events = getEvents(canalInstance.getEventStore(), start, batchSize, timeout, unit);
        }

        if (CollectionUtils.isEmpty(events.getEvents())) {
            // logger.debug("getWithoutAck successfully, clientId:{}
            // batchSize:{} but result
            // is null",
            // clientIdentity.getClientId(),
            // batchSize);
            return new Message(-1, true, new ArrayList()); // 返回空包,避免生成batchId,浪费性能
        } else {
            // 记录到流式信息
            Long batchId = canalInstance.getMetaManager().addBatch(clientIdentity, events.getPositionRange());
            boolean raw = isRaw(canalInstance.getEventStore());
            List entrys = null;
            if (raw) {
                entrys = Lists.transform(events.getEvents(), Event::getRawEntry);
            } else {
                entrys = Lists.transform(events.getEvents(), Event::getEntry);
            }
            if (logger.isInfoEnabled()) {
                logger.info("getWithoutAck successfully, clientId:{} batchSize:{}  real size is {} and result is [batchId:{} , position:{}]",
                    clientIdentity.getClientId(),
                    batchSize,
                    entrys.size(),
                    batchId,
                    events.getPositionRange());
            }
            return new Message(batchId, raw, entrys);
        }

    }
}

/**

  • 根据不同的参数,选择不同的方式获取数据
    */
    private Events getEvents(CanalEventStore eventStore, Position start, int batchSize, Long timeout,
    TimeUnit unit) {
    if (timeout == null) {
    return eventStore.tryGet(start, batchSize);
    } else {
    try {
    if (timeout <= 0) {
    return eventStore.get(start, batchSize);
    } else {
    return eventStore.get(start, batchSize, timeout, unit);
    }
    } catch (Exception e) {
    throw new CanalServerException(e);
    }
    }
    }
    getWithoutAck方法用于客户端获取binlog消息 ,一个获取一批(batch)的binlog,canal会为这批binlog生成一个唯一的batchId。客户端如果消费成功,则调用ack方法对这个批次进行确认。如果失败的话,可以调用rollback方法进行回滚。客户端可以连续多次调用getWithoutAck方法来获取binlog,在ack的时候,需要按照获取到binlog的先后顺序进行ack。如果后面获取的binlog被ack了,那么之前没有ack的binlog消息也会自动被ack。

getWithoutAck方法大致工作步骤如下所示:

根据destination找到要从哪一个CanalInstance中获取binlog消息。

确定从哪一个位置(Position)开始继续消费binlog。通常情况下,这个信息是存储在CanalMetaManager中。特别的,在第一次获取的时候,CanalMetaManager 中还没有存储任何binlog位置信息。此时CanalEventStore中存储的第一条binlog位置,则应该client开始消费的位置。

根据Position从CanalEventStore中获取binlog。为了尽量提高效率,一般一次获取一批binlog,而不是获取一条。这个批次的大小(batchSize)由客户端指定。同时客户端可以指定超时时间,在超时时间内,如果获取到了batchSize的binlog,会立即返回。 如果超时了还没有获取到batchSize指定的binlog个数,也会立即返回。特别的,如果没有设置超时时间,如果没有获取到binlog也立即返回。

在CanalMetaManager中记录这个批次的binlog消息。CanalMetaManager会为获取到的这个批次的binlog生成一个唯一的batchId,batchId是递增的。如果binlog信息为空,则直接把batchId设置为-1。

ack方法:

/**
* 进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。
*
*


* 注意:进行反馈时必须按照batchId的顺序进行ack(需有客户端保证)
*

*/
@Override
public void ack(ClientIdentity clientIdentity, long batchId) throws CanalServerException {
checkStart(clientIdentity.getDestination());
checkSubscribe(clientIdentity);

    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    PositionRange<LogPosition> positionRanges = null;

//1 从CanalMetaManager中,移除这个批次的信息
positionRanges = canalInstance.getMetaManager().removeBatch(clientIdentity, batchId); // 更新位置
if (positionRanges == null) { // 说明是重复的ack/rollback
throw new CanalServerException(String.format(“ack error , clientId:%s batchId:%d is not exist , please check”,
clientIdentity.getClientId(),
batchId));
}

    // 更新cursor最好严格判断下位置是否有跳跃更新
    // Position position = lastRollbackPostions.get(clientIdentity);
    // if (position != null) {
    // // Position position =
    // canalInstance.getMetaManager().getCursor(clientIdentity);
    // LogPosition minPosition =
    // CanalEventUtils.min(positionRanges.getStart(), (LogPosition)
    // position);
    // if (minPosition == position) {// ack的position要晚于该最后ack的位置,可能有丢数据
    // throw new CanalServerException(
    // String.format(
    // "ack error , clientId:%s batchId:%d %s is jump ack , last ack:%s",
    // clientIdentity.getClientId(), batchId, positionRanges,
    // position));
    // }
    // }

    // 更新cursor
//2、记录已经成功消费到的binlog位置,以便下一次获取的时候可以从这个位置开始,这是通过CanalMetaManager记录的
    if (positionRanges.getAck() != null) {
        canalInstance.getMetaManager().updateCursor(clientIdentity, positionRanges.getAck());
        if (logger.isInfoEnabled()) {
            logger.info("ack successfully, clientId:{} batchId:{} position:{}",
                clientIdentity.getClientId(),
                batchId,
                positionRanges);
        }
    }

    // 可定时清理数据
    canalInstance.getEventStore().ack(positionRanges.getEnd(), positionRanges.getEndSeq());
}

ack方法时客户端用户确认某个批次的binlog消费成功。进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。注意:进行反馈时必须按照batchId的顺序进行ack(需有客户端保证)

ack时需要做以下几件事情:

从CanalMetaManager中,移除这个批次的信息。在getWithoutAck方法中,将批次的信息记录到了CanalMetaManager中,ack时移除。

记录已经成功消费到的binlog位置,以便下一次获取的时候可以从这个位置开始,这是通过CanalMetaManager记录的。

从CanalEventStore中,将这个批次的binlog内容移除。因为已经消费成功,继续保存这些已经消费过的binlog没有任何意义,只会白白占用内存。

rollback方法:

/**

  • 回滚到未进行 {@link #ack} 的地方,下次fetch的时候,可以从最后一个没有 {@link #ack} 的地方开始拿
    /
    @Override
    public void rollback(ClientIdentity clientIdentity) throws CanalServerException {
    checkStart(clientIdentity.getDestination());
    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    // 因为存在第一次链接时自动rollback的情况,所以需要忽略未订阅
    boolean hasSubscribe = canalInstance.getMetaManager().hasSubscribe(clientIdentity);
    if (!hasSubscribe) {
    return;
    }
    synchronized (canalInstance) {
    // 清除batch信息
    canalInstance.getMetaManager().clearAllBatchs(clientIdentity);
    // rollback eventStore中的状态信息
    canalInstance.getEventStore().rollback();
    logger.info(“rollback successfully, clientId:{}”, new Object[] { clientIdentity.getClientId() });
    }
    }
    /
    *
  • 回滚到未进行 {@link #ack} 的地方,下次fetch的时候,可以从最后一个没有 {@link #ack} 的地方开始拿
    */
    @Override
    public void rollback(ClientIdentity clientIdentity, Long batchId) throws CanalServerException {
    checkStart(clientIdentity.getDestination());
    CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
    // 因为存在第一次链接时自动rollback的情况,所以需要忽略未订阅
    boolean hasSubscribe = canalInstance.getMetaManager().hasSubscribe(clientIdentity);
    if (!hasSubscribe) {
    return;
    }
    synchronized (canalInstance) {
    // 清除batch信息
    PositionRange positionRanges = canalInstance.getMetaManager().removeBatch(clientIdentity,
    batchId);
    if (positionRanges == null) { // 说明是重复的ack/rollback
    throw new CanalServerException(String.format(“rollback error, clientId:%s batchId:%d is not exist , please check”,
    clientIdentity.getClientId(),
    batchId));
    }
    // lastRollbackPostions.put(clientIdentity,
    // positionRanges.getEnd());// 记录一下最后rollback的位置
    // TODO 后续rollback到指定的batchId位置
    canalInstance.getEventStore().rollback();// rollback
    // eventStore中的状态信息
    logger.info(“rollback successfully, clientId:{} batchId:{} position:{}”,
    clientIdentity.getClientId(),
    batchId,
    positionRanges);
    }
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Kubernetes (K8s) 上部署 Canal Server,你可以使用 Deployment 和 Service 来创建和管理它。 首先,你需要创建一个 Canal Server 的配置文件,例如 `canal-server.yaml`,其内容类似于以下示例: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: canal-server spec: replicas: 1 selector: matchLabels: app: canal-server template: metadata: labels: app: canal-server spec: containers: - name: canal-server image: canal/canal-server:v1.1.4 ports: - containerPort: 11111 env: - name: canal.destinations value: example --- apiVersion: v1 kind: Service metadata: name: canal-server spec: selector: app: canal-server ports: - port: 11111 targetPort: 11111 ``` 上述配置文件定义了一个名为 `canal-server` 的 Deployment 和一个名为 `canal-server` 的 Service。Deployment 中的容器使用 `canal/canal-server:v1.1.4` 镜像,并将容器的端口设置为 11111。Service 将流量转发到 Pod 的容器端口 11111。 执行以下命令来部署 Canal Server: ```shell kubectl apply -f canal-server.yaml ``` 这将创建一个名为 `canal-server` 的 Deployment 和一个名为 `canal-server` 的 Service。 你可以使用以下命令来检查 Deployment 和 Service 的状态: ```shell kubectl get deployments kubectl get services ``` 一旦 Deployment 和 Service 创建成功,你就可以通过 Service 的 Cluster IP 或 NodePort 来访问 Canal Server。 请注意,上述示例仅供参考,你需要根据自己的实际需求进行修改和调整。另外,还需要确保你的 Kubernetes 集群已正确配置,并且能够拉取指定的 Canal Server 镜像。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值