canal源碼解析(2)—位點的實現(xiàn)

首先說一下我對canal中位點的理解。什么是位點?位點是 binlog事件在binlog文件中的位置。但是對于canal而言,canal server發(fā)送dump請求前需要確定mysql的同步位點,主要包括canal server啟動,mysql主備切換,canal server主備切換,dump異常后重啟等情況。 同時,在canal client不斷從canal server讀取數(shù)據(jù)的過程中, canal client需要告知 canal server自己消費成功的位點,這樣當(dāng)發(fā)生canal client崩潰或者canal server崩潰重啟后,都會考慮是否按照原來消費成功的位點之后繼續(xù)消費或dump。下面我將通過canal server dump前找mysql同步位點的過程分析我對canal中位點的理解。
對于HA模式的canal server,我們先看下有哪些位點管理器。

FailbackLogPositionManager:實現(xiàn)基于failover查找的機制完成meta的操作,應(yīng)用場景:比如針對內(nèi)存buffer,出現(xiàn)HA切換,先嘗試從內(nèi)存buffer區(qū)中找到lastest position,如果不存在才嘗試找一下meta里消費的信息。初始化過程為 :

//eventParser中的成員變量logPositionManager
<bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <constructor-arg>
                    <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
                </constructor-arg>
                <constructor-arg>
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <constructor-arg ref="metaManager"/>
                    </bean>
                </constructor-arg>
            </bean>
//上面logPositionManager中的成員變量secondary
<bean id="metaManager" class="com.alibaba.otter.canal.meta.PeriodMixedMetaManager">
        <property name="zooKeeperMetaManager">
            <bean class="com.alibaba.otter.canal.meta.ZooKeeperMetaManager">
                <property name="zkClientx" ref="zkClientx" />
            </bean>
        </property>
        <property name="period" value="${canal.zookeeper.flush.period:1000}" />
    </bean>

主要包括兩個成員變量:

//內(nèi)存
    private final CanalLogPositionManager primary;
    private final CanalLogPositionManager secondary;

其中primary是在內(nèi)存中記錄位點變化的管理器MemoryLogPositionManager,對應(yīng)canal server中dump下來的binlog event最新位點。
secondary是位點管理器MetaLogPositionManager,它有一個成員變量metaManagerPeriodMixedMetaManager

PeriodMixedMetaManager:canal client消費信息管理器,如上是定時刷新canal client消費位點信息到zk上的位點管理器。主要成員變量:

  //內(nèi)存中記錄的canal client ack position
    protected Map<ClientIdentity, Position>                  cursors;
    //定時刷新線程池
    private ScheduledExecutorService executor;
  //與zk交互的位點管理器,更新ack position等
    private ZooKeeperMetaManager     zooKeeperMetaManager;
    //定時刷新到zk ack  position的時間間隔
    private long                     period     = 1000;  // 單位ms
    private Set<ClientIdentity>      updateCursorTasks;

canal server dump前找mysql同步位點

在特定instance上激活的canal server變開始啟動自己的CanalInstanceWithSpring實例,其中會啟動自己的eventParser。如官方文檔所說,parser的啟動和解析過程為:



1.Connection獲取上一次解析成功的位置 (如果第一次啟動,則獲取初始指定的位置或者是當(dāng)前數(shù)據(jù)庫的binlog位點)
2.Connection建立鏈接,發(fā)送BINLOG_DUMP指令
3.Mysql開始推送Binaly Log
4.接收到的Binaly Log的通過Binlog parser進行協(xié)議解析,補充一些特定信息
// 補充字段名字,字段類型,主鍵信息,unsigned類型處理
傳遞給EventSink模塊進行數(shù)據(jù)存儲,是一個阻塞操作,直到存儲成功
5.存儲成功后,定時記錄Binaly Log位置

在此,我們聚焦在步驟一,即canal server dump前找mysql同步位點。代碼如下:

//MysqlEventParser.findStartPosition
    protected EntryPosition findStartPosition(ErosaConnection connection) throws IOException {
        if (isGTIDMode()) {
//步驟一
            // GTID模式下,CanalLogPositionManager里取最后的gtid,沒有則取instanc配置中的
            LogPosition logPosition = getLogPositionManager().getLatestIndexBy(destination);
            if (logPosition != null) {
//1.3
                // 如果以前是非GTID模式,后來調(diào)整為了GTID模式,那么為了保持兼容,需要判斷gtid是否為空
                if (StringUtils.isNotEmpty(logPosition.getPostion().getGtid())) {
                    return logPosition.getPostion();
                }
            }else {
//1.4
                if (masterPosition != null && StringUtils.isNotEmpty(masterPosition.getGtid())) {
                    return masterPosition;
                }
            }
        }
//步驟二
        EntryPosition startPosition = findStartPositionInternal(connection);
        if (needTransactionPosition.get()) {
//步驟三
            logger.warn("prepare to find last position : {}", startPosition.toString());
            Long preTransactionStartPosition = findTransactionBeginPosition(connection, startPosition);
            if (!preTransactionStartPosition.equals(startPosition.getPosition())) {
                logger.warn("find new start Transaction Position , old : {} , new : {}",
                    startPosition.getPosition(),
                    preTransactionStartPosition);
                startPosition.setPosition(preTransactionStartPosition);
            }
            needTransactionPosition.compareAndSet(true, false);
        }
        return startPosition;
    }

代碼邏輯為:
步驟一.如果是GTID Mode,找到位點就直接返回。
步驟二.否則調(diào)用findStartPositionInternal(connection)繼續(xù)尋找位點。
步驟三.如果needTransactionPosition=true,則必須要求找到事務(wù)開啟使的binlog位點作為起始位點返回。

在具體分析上述步驟前,我們先看一下位點類的結(jié)構(gòu):

public class LogPosition extends Position {
//偽裝成slave的canal server信息
    private LogIdentity       identity;
//位點的包裝類
    private EntryPosition     postion;

}

public class EntryPosition extends TimePosition {
//
    private boolean           included              = false;
//binlog文件名稱
    private String            journalName;
//所在binlog文件中的位點位置
    private Long              position;
  // 記錄一下位點對應(yīng)的mysql serverId
    private Long              serverId              = null; 
//偽裝成slave的canal server的gtid集合           
    private String            gtid                  = null;

}
//基于時間的位置,position數(shù)據(jù)不唯一
public class TimePosition extends Position {

    private static final long serialVersionUID = 6185261261064226380L;
    protected Long            timestamp;

可以看出位點也是考慮了兩種dump方式,一種是 binlog filename + position,一種是GTID。下面我們先看步驟一中的GTID模式找起始位點。

//FailbackLogPositionManager. getLatestIndexBy
 public LogPosition getLatestIndexBy(String destination) {
//1.1
        LogPosition logPosition = primary.getLatestIndexBy(destination);
        if (logPosition != null) {
            return logPosition;
        }
//1.2
        return secondary.getLatestIndexBy(destination);
    }

1.1先從內(nèi)存位點管理器primary中尋找instance對應(yīng)的dump位點,能找到就返回。

//MemoryLogPositionManager.getLatestIndexBy
public LogPosition getLatestIndexBy(String destination) {
        return positions.get(destination);
    }

1.2如果內(nèi)存位點管理器中找不到,則到位點管理器secondary中尋找位點并返回。

//MetaLogPositionManager.getLatestIndexBy
 public LogPosition getLatestIndexBy(String destination) {
        List<ClientIdentity> clientIdentities = metaManager.listAllSubscribeInfo(destination);
        LogPosition result = null;
        if (!CollectionUtils.isEmpty(clientIdentities)) {
            // 嘗試找到一個最小的logPosition
            for (ClientIdentity clientIdentity : clientIdentities) {
//1.2.1
                LogPosition position = (LogPosition) metaManager.getCursor(clientIdentity);
                if (position == null) {
                    continue;
                }

                if (result == null) {
                    result = position;
                } else {
                    result = CanalEventUtils.min(result, position);
                }
            }
        }

        return result;
    }

1.2.1.主要邏輯是到metaManager,也就是PeriodMixedMetaManager中尋找內(nèi)存中記錄的canal client消費位點信息,找不到則到zk上cursor節(jié)點找。

//PeriodMixedMetaManager.getCursor
 public Position getCursor(ClientIdentity clientIdentity) throws CanalMetaManagerException {
//從內(nèi)存中獲取該client對應(yīng)的消費位點
        Position position = super.getCursor(clientIdentity);
        if (position == nullCursor) {
            return null;
        } else {
            return position;
        }
    }

// super.getCursor
    public Position getCursor(ClientIdentity clientIdentity) throws CanalMetaManagerException {
        return cursors.get(clientIdentity);
    }

cursors中找不到數(shù)據(jù)時將會調(diào)用apply方法初始化client對應(yīng)的數(shù)據(jù)。

        cursors = MigrateMap.makeComputingMap(new Function<ClientIdentity, Position>() {

            public Position apply(ClientIdentity clientIdentity) {
                Position position = zooKeeperMetaManager.getCursor(clientIdentity);
                if (position == null) {
                    return nullCursor; // 返回一個空對象標(biāo)識,避免出現(xiàn)異常
                } else {
                    return position;
                }
            }
        });

1.3.如果上述找到的位點不為空 ,且位點中的gtid也不為空,說明以前就是gtid模式,可以使用這個gtid set作為gtid dump的位點,也就是找到了gtid模式的位點。
1.4.如果上述找到的位點為空,則判斷masterPosition是否不為空且gtid也不為空,如果滿足則使用masterPosition作為gtid模式的位點。其中 masterPosition是在該instance配置文件instance.properties 中配置的mysql dump位點。

# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=binlog.000008
canal.instance.master.position=2993
canal.instance.master.timestamp=99999999999999
canal.instance.master.gtid=c6704a02-8354-11e9-a0c3-f054d220296f:1-16

總結(jié)一下:步驟一找GTID位點的邏輯為:先到primary中尋找canal server中dump下來的binlog event最新位點(內(nèi)存中 ),找不到就到secondary中尋找canal client成功消費位點。如果找到了就判斷位點的gtid是否為空,如果為空則說明以前不是gtid模式,則不支持gtid模式,繼續(xù)步驟二。如果上述找不到位點,則判斷masterPosition是否不為空,且gtid也不為空,如果滿足則使用masterPosition作為gtid模式的位點。否則繼續(xù)步驟二。

接下來看步驟二是如何尋找非GTID模式下的起始位點的。

//MysqlEventParser.findStartPositionInternal
 protected EntryPosition findStartPositionInternal(ErosaConnection connection) {
        MysqlConnection mysqlConnection = (MysqlConnection) connection;
//2.1
        LogPosition logPosition = logPositionManager.getLatestIndexBy(destination);
        if (logPosition == null) {// 找不到歷史成功記錄
            EntryPosition entryPosition = null;
            if (masterInfo != null && mysqlConnection.getConnector().getAddress().equals(masterInfo.getAddress())) {
//2.2.1
                entryPosition = masterPosition;
            } else if (standbyInfo != null
                       && mysqlConnection.getConnector().getAddress().equals(standbyInfo.getAddress())) {
//2.2.2
                entryPosition = standbyPosition;
            }

            if (entryPosition == null) {
//2.2.3
                entryPosition = findEndPositionWithMasterIdAndTimestamp(mysqlConnection); // 默認從當(dāng)前最后一個位置進行消費
            }

            // 判斷一下是否需要按時間訂閱

            if (StringUtils.isEmpty(entryPosition.getJournalName())) {
//2.2.4
                // 如果沒有指定binlogName,嘗試按照timestamp進行查找
                if (entryPosition.getTimestamp() != null && entryPosition.getTimestamp() > 0L) {
                    logger.warn("prepare to find start position {}:{}:{}",
                        new Object[] { "", "", entryPosition.getTimestamp() });
                    return findByStartTimeStamp(mysqlConnection, entryPosition.getTimestamp());
                } else {
                    logger.warn("prepare to find start position just show master status");
                    return findEndPositionWithMasterIdAndTimestamp(mysqlConnection); // 默認從當(dāng)前最后一個位置進行消費
                }
            } else {
//2.2.5
                if (entryPosition.getPosition() != null && entryPosition.getPosition() > 0L) {
                    // 如果指定binlogName + offest,直接返回
                    entryPosition = findPositionWithMasterIdAndTimestamp(mysqlConnection, entryPosition);
                    logger.warn("prepare to find start position {}:{}:{}",
                        new Object[] { entryPosition.getJournalName(), entryPosition.getPosition(),
                                entryPosition.getTimestamp() });
                    return entryPosition;
                } else {
//2.2.6
                    EntryPosition specificLogFilePosition = null;
                    if (entryPosition.getTimestamp() != null && entryPosition.getTimestamp() > 0L) {
                        // 如果指定binlogName +
                        // timestamp,但沒有指定對應(yīng)的offest,嘗試根據(jù)時間找一下offest
                        EntryPosition endPosition = findEndPosition(mysqlConnection);
                        if (endPosition != null) {
                            logger.warn("prepare to find start position {}:{}:{}",
                                new Object[] { entryPosition.getJournalName(), "", entryPosition.getTimestamp() });
                            specificLogFilePosition = findAsPerTimestampInSpecificLogFile(mysqlConnection,
                                entryPosition.getTimestamp(),
                                endPosition,
                                entryPosition.getJournalName(),
                                true);
                        }
                    }
//2.2.7
                    if (specificLogFilePosition == null) {
                        // position不存在,從文件頭開始
                        entryPosition.setPosition(BINLOG_START_OFFEST);
                        return entryPosition;
                    } else {

                        return specificLogFilePosition;
                    }
                }
            }
        } else {
//2.1.1
            if (logPosition.getIdentity().getSourceAddress().equals(mysqlConnection.getConnector().getAddress())) {

                if (dumpErrorCountThreshold >= 0 && dumpErrorCount > dumpErrorCountThreshold) {
//2.1.1.1
                    // binlog定位位點失敗,可能有兩個原因:
                    // 1. binlog位點被刪除
                    // 2.vip模式的mysql,發(fā)生了主備切換,判斷一下serverId是否變化,針對這種模式可以發(fā)起一次基于時間戳查找合適的binlog位點
                    boolean case2 = (standbyInfo == null || standbyInfo.getAddress() == null)
                                    && logPosition.getPostion().getServerId() != null
                                    && !logPosition.getPostion().getServerId().equals(findServerId(mysqlConnection));
                    if (case2) {
                        long timestamp = logPosition.getPostion().getTimestamp();
                        long newStartTimestamp = timestamp - fallbackIntervalInSeconds * 1000;
                        logger.warn("prepare to find start position by last position {}:{}:{}", new Object[] { "", "",
                                logPosition.getPostion().getTimestamp() });
                        EntryPosition findPosition = findByStartTimeStamp(mysqlConnection, newStartTimestamp);
                        // 重新置為一下
                        dumpErrorCount = 0;
                        return findPosition;
                    }

                    Long timestamp = logPosition.getPostion().getTimestamp();
                    if (isRdsOssMode() && (timestamp != null && timestamp > 0)) {
                        // 如果binlog位點不存在,并且屬于timestamp不為空,可以返回null走到oss binlog處理
                        return null;
                    }
                }
                // 其余情況
//2.1.1.2
                logger.warn("prepare to find start position just last position\n {}",
                    JsonUtils.marshalToString(logPosition));
                return logPosition.getPostion();
            } else {
//2.1.2
                // 針對切換的情況,考慮回退時間
                long newStartTimestamp = logPosition.getPostion().getTimestamp() - fallbackIntervalInSeconds * 1000;
                logger.warn("prepare to find start position by switch {}:{}:{}", new Object[] { "", "",
                        logPosition.getPostion().getTimestamp() });
                return findByStartTimeStamp(mysqlConnection, newStartTimestamp);
            }
        }
    }

這是一段蠻長的代碼。。。其實我們需要思考的問題是:如何根據(jù)能夠查到的LogPosition的信息(數(shù)據(jù)庫信息,binlog信息)中提煉出最精確的dump位點。廢話不多說,看代碼。

2.1.從logPositionManager中尋找內(nèi)存中最精確的dump位點(先尋找canal server中dump下來的binlog event最新位點,找不到就尋找canal client成功消費位),前面分析過。分為找到位點和找不到位點兩種情況。我們先看找不到的情況 。
2.2.如果從logPositionManager中找不到位點,說明canal server是第一次從該instance dump。那么就要從instance.properties文件中找可能位點。該文件是可以配置一主一從mysql的,這樣可以實現(xiàn)主備切換的mysql dump。上面介紹了masterPosition的配置選項,standbyPosition也是同理。

# mysql多節(jié)點解析配置
#canal.instance.standby.address = 10.20.144.29:3306
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =

2.2.1.如果mysql主庫信息masterInfo不為null并且當(dāng)前mysqlConnection連接的是主庫,則entryPosition = masterPosition。
2.2.2.否則如果mysql從庫信息masterInfo不為null并且當(dāng)前mysqlConnection連接是從庫,則entryPosition = standbyPosition。
2.2.3.如果此時entryPosition為空,表明canal server沒有指定任何位點,則將當(dāng)前mysqlConnection連接的數(shù)據(jù)庫的binlog最后一個位置作為dump位點并返回。

如果entryPosition能夠從instance.properties中獲得位點信息,則說明entryPosition中journalName,position,timestamp不為空,可通過這些值進一步確定精確的dump位點。
2.2.4.如果entryPosition中journalName為空但是timestamp不為空,則嘗試按照timestamp進行查找小于timestamp最接近的binlog事件。findByStartTimeStamp的邏輯就是返回從最后一個binlog文件往前依次找滿足條件的事務(wù)開始的位點。如果能找到,就返回該位點,找不到則將當(dāng)前mysqlConnection連接的數(shù)據(jù)庫的binlog最后一個位置作為dump位點并返回。
2.2.5.如果entryPosition中journalName不為空并且position不為空,則直接返回該位點。
2.2.6.如果entryPosition中journalName不為空但是position不為空,則只會在該binlog文件中根據(jù)timestamp查找小于timestamp最接近的binlog事件。找到就返回該位點。
2.2.7.上述找不到則將當(dāng)前mysqlConnection連接的數(shù)據(jù)庫的binlog最后一個位置作為dump位點并返回。

至此,根據(jù)instance.properties文件中配置的位點信息確定最終dump的位點分析完畢,主旨就是如果binlog filename+position存在,則直接作為dump位點,否則根據(jù)timestamp確定。如果最終根據(jù)binlog filename,position,timestamp確定不了位點,則使用當(dāng)前mysqlConnection連接的數(shù)據(jù)庫的binlog最后一個位置作為dump位點

回到2.1,如果從logPositionManager中從內(nèi)存中找到位點logPosition,則說明之前這個instance是從mysql中 dump過數(shù)據(jù)的,需要結(jié)合連接狀態(tài)確定當(dāng)前canal server的狀態(tài)是怎么樣的,然后才能進一步確定dump位點。

2.1.1.logPosition中mysql數(shù)據(jù)庫信息和當(dāng)前mysqlConnection連接的數(shù)據(jù)庫信息一致,說明logPosition就是當(dāng)前mysqlConnection連接的數(shù)據(jù)庫的位點信息。那么為什么又會再次確定dump位點呢?可能原因是eventParser解析過程中發(fā)生dump異常,導(dǎo)致關(guān)閉整個parser過程,再重新啟動parser解析過程。發(fā)生異常的原因可能有:binlog定位位點失敗,mysql的連接臨時斷開又重連上,canal server HA切換等

2.1.1.1. dumpErrorCount默認超過2次,則說明是binlog定位位點失敗導(dǎo)致的。可能原因:
binlog位點被刪除:此時canal server會不停打印“errno = 1236.....”的錯誤日志,代表該位點的binlog在mysql中已經(jīng)找不到了。
vip模式的mysql,發(fā)生了主備切換:判斷一下serverId是否變化,針對這種模式可以發(fā)起一次基于時間戳查找合適的binlog位點。(應(yīng)該是和阿里云mysql rds vip的模式相關(guān)的,沒用過表示不懂這種場景。。)
2.1.1.2.可能是mysql的連接臨時斷開又重連上,canal server HA切換等導(dǎo)致的重新parser過程,確定了dump位點后接著用就行。
2.1.2.如果logPosition中mysql數(shù)據(jù)庫信息和當(dāng)前mysqlConnection連接的數(shù)據(jù)庫信息不一致,說明發(fā)生了mysql dump主備切換。此時需要根據(jù)timestamp確定在新的mysql上的dump位點,而不能依賴原來連接的數(shù)據(jù)庫的binlog filename 和position。

步驟三:在步驟一和步驟二中解析出來的位點不一定是事務(wù)起始事件處的位點,此時在dump過程中可能找不到binlog事件所在table信息,會拋出TableIdNotFoundException異常,同時將needTransactionPosition設(shè)置為true。到了步驟三會根據(jù)步驟一和步驟二中解析出來的位點確定小于它的最近的事務(wù)起始事件處的位點,作為最終的dump位點。

至此,關(guān)于canal server dump前找mysql同步位點的代碼分析完了,總結(jié)一下:

步驟一:如果是GTID Mode,則根據(jù) logPositionManager的 primary或者secondary位點管理器的從內(nèi)存中找位點,找不到從配置文件instance.properties中找。
步驟二:如果步驟一找不到,說明是根據(jù)binlog filename + position確定dump位點。同理先根據(jù) logPositionManager的 primary或者secondary位點管理器的從內(nèi)存中找位點,找不到從配置文件instance.properties中找。當(dāng)然會考慮canal server HA切換,dump異常,mysql連接切換對確定位點作出一些調(diào)整。
步驟三:如果將needTransactionPosition設(shè)置為true,會根據(jù)步驟一和步驟二中解析出來的位點確定小于它的最近的事務(wù)起始事件處的位點,作為最終的dump位點。

如果你看懂了上面的碎碎念,不知道是否會疑惑 logPositionManager的 primary或者secondary位點管理器管理的內(nèi)存位點是如何第一次創(chuàng)建的?之后是如何更新的?下面我們再捋一捋這些過程。

primary的位點管理
上文我們提到primary的初始化過程,primary是在內(nèi)存中記錄位點變化的管理器MemoryLogPositionManager,對應(yīng)canal server中dump下來的binlog event最新位點。所以每當(dāng)dump到binlog數(shù)據(jù)時便會更新primary位點信息。如何定義canal server dump到binlog數(shù)據(jù)呢?在文章開頭的parser過程步驟5中,binlog數(shù)據(jù)經(jīng)過Binlog parser—>EventSink傳遞—>存儲到EventStore后,會記錄Binaly Log位點。代碼中會回調(diào)TransactionFlushCallback.flush方法刷新位點。

 public AbstractEventParser(){
        // 初始化一下
        transactionBuffer = new EventTransactionBuffer(new TransactionFlushCallback() {

            public void flush(List<CanalEntry.Entry> transaction) throws InterruptedException {
                boolean successed = consumeTheEventAndProfilingIfNecessary(transaction);
                if (!running) {
                    return;
                }

                if (!successed) {
                    throw new CanalParseException("consume failed!");
                }
//返回的是最大的TRANSACTIONEND類型的事件位點,盡量記錄一個事務(wù)做為position
                LogPosition position = buildLastTransactionPosition(transaction);
                if (position != null) { // 可能position為空
                    logPositionManager.persistLogPosition(AbstractEventParser.this.destination, position);
                }
            }
        });
    }

EventParser初始化時會初始化一個TransactionFlushCallback匿名類對象,其中flush方法中logPositionManager.persistLogPosition便會刷新primary中位點。

//FailbackLogPositionManager.persistLogPosition
 public void persistLogPosition(String destination, LogPosition logPosition) throws CanalParseException {
        try {
            primary.persistLogPosition(destination, logPosition);
        } catch (CanalParseException e) {
            logger.warn("persistLogPosition use primary log position manager exception. destination: {}, logPosition: {}",
                destination,
                logPosition,
                e);
            secondary.persistLogPosition(destination, logPosition);
        }
    }

secondary位點管理
上文我們提到secondary的初始化過程,secondary是位點管理器MetaLogPositionManager,它有一個成員變量metaManager是PeriodMixedMetaManager,是canal client消費信息管理器,如上是定時刷新canal client消費位點信息到zk上的位點管理器。所以每當(dāng)canal client ack時會更新該位點。

//CanalServerWithEmbedded.ack
//客戶端ack時canal server的處理
    public void ack(ClientIdentity clientIdentity, long batchId) throws CanalServerException {
        checkStart(clientIdentity.getDestination());
        checkSubscribe(clientIdentity);

        CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());
        PositionRange<LogPosition> positionRanges = null;
//注意,客戶端是按批次去binlog 數(shù)據(jù),一個batchId可能對應(yīng)多條binlog數(shù)據(jù),由服務(wù)端根據(jù)batchId確定最終ack的位點是什么
        positionRanges = canalInstance.getMetaManager().removeBatch(clientIdentity, batchId); // 更新位置
        if (positionRanges == null) { // 說明是重復(fù)的ack/rollback
            throw new CanalServerException(String.format("ack error , clientId:%s batchId:%d is not exist , please check",
                clientIdentity.getClientId(),
                batchId));
        }
        // 更新cursor
        if (positionRanges.getAck() != null) {
            canalInstance.getMetaManager().updateCursor(clientIdentity, positionRanges.getAck());
            if (logger.isInfoEnabled()) {
                logger.info("ack successfully, clientId:{} batchId:{} position:{}",
                    clientIdentity.getClientId(),
                    batchId,
                    positionRanges);
            }
        }

        // 可定時清理數(shù)據(jù)
        canalInstance.getEventStore().ack(positionRanges.getEnd(), positionRanges.getEndSeq());
    }

更新cursor時會更新secondary的位點信息

PeriodMixedMetaManager. updateCursor
    public void updateCursor(ClientIdentity clientIdentity, Position position) throws CanalMetaManagerException {
//更新成員變量cursors中的位點
        super.updateCursor(clientIdentity, position);
// 添加到任務(wù)隊列中,定時觸發(fā)zk上該instance的位點更新,/otter/canal/destinations/{destination}/{clientId}/cursor節(jié)點
        updateCursorTasks.add(clientIdentity);
    }

此外,我們知道/otter/canal/destinations/{destination}/{clientId}/cursor節(jié)點是zk上的持久節(jié)點,不知道的參考canal源碼解析-HA模式的實現(xiàn)。那么當(dāng)發(fā)生canal server HA切換時,canal server應(yīng)該從zk上初始化原來的client ack位點信息,避免無用的dump。上面canal server dump前解析位點時步驟1.2.1便會從zk上拉取原來的client ack位點信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容