
com.huaweicloud.dws.client.binlog.reader.BinlogReader Maven / Gradle / Ivy
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
*/
package com.huaweicloud.dws.client.binlog.reader;
import com.huaweicloud.dws.client.DwsConfig;
import com.huaweicloud.dws.client.TableConfig;
import com.huaweicloud.dws.client.binlog.collector.BinlogApi;
import com.huaweicloud.dws.client.binlog.collector.BinlogCollector;
import com.huaweicloud.dws.client.binlog.collector.BinlogFullSyncCollector;
import com.huaweicloud.dws.client.binlog.model.BinlogRecord;
import com.huaweicloud.dws.client.binlog.model.FullSyncSlot;
import com.huaweicloud.dws.client.binlog.model.Slot;
import com.huaweicloud.dws.client.exception.DwsBinlogException;
import com.huaweicloud.dws.client.exception.DwsClientException;
import com.huaweicloud.dws.client.exception.ExceptionCode;
import com.huaweicloud.dws.client.exception.InvalidException;
import com.huaweicloud.dws.client.model.Constants;
import com.huaweicloud.dws.client.util.AssertUtil;
import com.huaweicloud.dws.client.util.LogUtil;
import com.huaweicloud.dws.client.worker.DwsConnectionPool;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @ProjectName: dws-connector
* @Description: binlog处理类总入口,负责启动读取binlog线程以及提交同步点
* 大体流程:
* 1. 在主线程中获取同步点信息(要兼容GTM和GTM-free的场景,前者返回统一的同步点信息,后者分别返回各个DN上的同步点信息)
* 1.1 按照步长分批消费同步点间的数据(如果开始的同步点+步长 < 结束的同步点,则无需再次从内核中获取同步点信息)
* 2. 开启多线程来增量同步binlog信息或全量同步binlog信息
* 3. 等待第二步所有线程读取完毕后,回到主线程并提交同步点
* @Date: 2023/07/11 15:40
* @Version 1.0
**/
@Slf4j
public class BinlogReader {
private static final long AWAIT_TIME = 1000L;
private static final int SLEEP_RANDOM_MS = 100;
private final SecureRandom sleepRandom;
private final DwsConfig dwsConfig;
@Getter
private final TableConfig tableConfig;
private final ExecutorService binlogCollectExecutorService;
private final ExecutorService binlogGetRecordExecutorService;
private final AtomicBoolean started = new AtomicBoolean(false);
// 是否需要全量同步(只要有一个同步点的startCsn为-1则进行全量同步)
private final AtomicBoolean needFullSync = new AtomicBoolean(false);
private final AtomicLong totalRecord = new AtomicLong(0);
private final List> completableFutureList = new ArrayList<>();
// 全量读取binlog的数据行数
private final int fullSyncBinlogBatchReadSize;
private final AtomicBoolean running = new AtomicBoolean(false);
private final List binlogCollectors = new ArrayList<>();
private final List binlogFullSyncCollectors = new ArrayList<>();
private final int binlogParallelNum;
// 存放读取binlog数据的队列
private final BlockingQueue queue;
// 指定的同步点信息
private final List specifySlots;
@Getter
private Exception exception;
private final List columnNames;
// 保存当前所需消费的同步点信息
@Setter
private List currentSlots = new ArrayList<>();
@Getter
private final String tableName;
@Getter
private final String slotName;
@Getter
private final DwsConnectionPool dwsConnectionPool;
@Getter
@Setter
private List nodeIds = new CopyOnWriteArrayList<>();
@Getter
private final boolean isNeedRedistribution;
@Getter
private final String errorMessage;
private int noDataTimes = 0;
@Setter
private String binlogWorkerThreadPrefix = "binlog-worker";
@Setter
private String binlogGetRecordThreadPrefix = "binlog-get-record";
public BinlogReader(DwsConfig dwsConfig, BlockingQueue queue, List specifySlots,
List columnNames, DwsConnectionPool dwsConnectionPool) {
checkParams(dwsConfig);
this.dwsConfig = dwsConfig;
this.queue = queue;
this.specifySlots = specifySlots;
this.columnNames = columnNames;
this.tableName = dwsConfig.getBinlogTableName();
this.dwsConnectionPool = dwsConnectionPool;
// 获取表级设置
this.tableConfig = dwsConfig.getTableConfig(tableName);
this.isNeedRedistribution = tableConfig.isNeedRedistribution();
this.errorMessage = tableConfig.getErrorMessage();
this.slotName = tableConfig.getBinlogSlotName();
this.fullSyncBinlogBatchReadSize = tableConfig.getFullSyncBinlogBatchReadSize();
this.binlogParallelNum = tableConfig.getBinlogParallelNum();
// 初始化binlog-worker线程相关信息
ThreadFactory binlogCollectThreadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(binlogWorkerThreadPrefix + "-" + tableName + "-" + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
}
};
// 初始化binlog-reader相关线程池
this.binlogCollectExecutorService =
new ThreadPoolExecutor(binlogParallelNum, binlogParallelNum, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10), binlogCollectThreadFactory, new ThreadPoolExecutor.AbortPolicy());
this.binlogGetRecordExecutorService = Executors.newFixedThreadPool(1, r -> {
Thread t = new Thread(r);
t.setName(binlogGetRecordThreadPrefix + "-" + tableName);
t.setDaemon(true);
return t;
});
try {
this.sleepRandom = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new InvalidException(e);
}
}
public BinlogReader(DwsConfig dwsConfig, BlockingQueue queue, List columnNames,
DwsConnectionPool dwsConnectionPool) {
this(dwsConfig, queue, null, columnNames, dwsConnectionPool);
}
/**
* 读取binlog信息入口方法: 读取到的数据实时往queue中写入
*
* @throws Exception 异常信息
*/
public synchronized void getRecords() throws Exception {
if (!started.get()) {
log.error("binlogReader not started...");
throw new DwsClientException(ExceptionCode.BINLOG_READER_NOT_STARTED, "binlogReader not started...");
}
if (Objects.nonNull(exception)) {
throw new DwsBinlogException(exception.getMessage(), exception);
}
if (running.get()) {
return;
}
// 开始获取binlog,另起一个线程,保证有数据就消费
binlogGetRecordExecutorService.execute(() -> {
try {
if (running.compareAndSet(false, true)) {
while (started.get()) {
if (Objects.nonNull(exception)) {
throw DwsClientException.fromException(exception);
}
doCollect();
}
}
} catch (Exception e) {
log.error("get binlog records has error", e);
exception = e;
}
});
}
public void checkException() throws DwsBinlogException {
if (Objects.nonNull(exception)) {
throw new DwsBinlogException(exception.getMessage(), exception);
}
}
public boolean hasException() {
return Objects.nonNull(exception);
}
/**
* 开启读取binlog流程, 大致如下:
* 获取同步点 -> 读取同步点内的binlog数据 -> 提交同步点
*
* @throws Exception 异常信息
*/
public synchronized void doCollect() throws Exception {
int binlogMaxRetryTimes = tableConfig.getBinlogMaxRetryTimes();
for (int retryCount = 1; retryCount <= binlogMaxRetryTimes; retryCount++) {
try {
long start = System.currentTimeMillis();
// 第一步: 获取binlog数据的前置动作(获取slot信息、初始化BinlogCollectors)
prepareToCollect();
// 第二步: 开启线程全量或增量读取binlog信息
collectBinlog();
// 第三步: 向内核注册同步点信息
registerSlot();
// 重置标志位
reset();
if (totalRecord.get() == 0) {
long sleepTime =
Math.min(tableConfig.getBinlogSleepTime() * (++noDataTimes), Constants.BINLOG_MAX_SLEEP_TIME);
LogUtil.withLogSwitch(dwsConfig,
() -> log.warn("there is no new data in currentSlots, sleepTime: {}", sleepTime));
// 如果从GTM上拿数据的话,即使表没进行DML操作,同步点也会推进,所以在这里判断是否读取到了数据,没有读到则sleep
TimeUnit.MILLISECONDS.sleep(sleepTime);
} else {
noDataTimes = 0;
log.info("read binlog info cost time: {}, slots: {},", (System.currentTimeMillis() - start),
currentSlots);
}
return;
} catch (Exception e) {
log.error("[retry]collectBinlog has error: ", e);
stopCollectors();
if (handleRedistribution(e)) {
log.info("no need retry, begin re-consume...");
throw DwsClientException.fromException(e);
}
// 重试
if (retryCount < binlogMaxRetryTimes) {
long sleepTime =
tableConfig.getBinlogRetryInterval() * retryCount + sleepRandom.nextInt(SLEEP_RANDOM_MS);
log.info("reader binlog fail, start retry, retryCount: {}, sleepTime: {}", retryCount, sleepTime);
try {
TimeUnit.MILLISECONDS.sleep(sleepTime);
} catch (InterruptedException ex) {
log.error("sleep has error: ", ex);
throw DwsClientException.fromException(ex);
}
} else {
log.info("reach to MaxRetryTimes: " + retryCount);
throw DwsClientException.fromException(e);
}
}
}
}
/**
* 处理扩缩容数据重分布逻辑
*
* @param e 异常信息,根据该异常信息来判断是否处于扩缩容阶段
* @return 是否需要处理
*/
public boolean handleRedistribution(Exception e) {
if (Objects.isNull(e.getMessage())) {
return false;
}
boolean isRedistribution = e.getMessage().contains(errorMessage);
if (!isRedistribution) {
return false;
}
log.info("start handle redistribution...");
return true;
}
/**
* 同步binlog的前置动作:
* 1. 从内核侧获取同步点信息
* 2. 按照消费步长,来初始化当前的同步点信息
* 3. 使用同步点信息来初始化BinlogCollector
*
* @throws DwsClientException 异常信息
*/
private void prepareToCollect() throws Exception {
if (nextCycle()) {
initCurrentSlots();
}
// 使用指定的slot信息来初始化当前的slot(故障恢复场景)
mergeCurrentSlotList();
if (currentSlots.stream().anyMatch(slot -> slot.getCurrentStartCsn() == Constants.FULL_SYNC_SLOT_CSN)) {
// 设置全量同步标识, 增量同步和全量同步调用内核接口不同
needFullSync.set(true);
}
// 每次使用新获取的同步点信息来初始化BinlogCollectors
initBinlogCollectors();
}
protected void initCurrentSlots() throws SQLException {
try (Connection connection = dwsConnectionPool.getConnection()) {
if (isNeedRedistribution && BinlogApi.NODE_COUNT.get() == 0) {
BinlogApi.NODE_COUNT.set(BinlogApi.getNodeIds(connection, getTableName()).size());
}
currentSlots = BinlogApi.getSyncPoint(connection, tableName, slotName, 0, false, isNeedRedistribution);
LogUtil.withLogSwitch(dwsConfig, () -> log.info("get slot info from db currentSlots: {}", currentSlots));
}
}
/**
* 根据内核返回的点位信息和当前保存的点位信息进行比较,判断是否有新数据
* 注意:由于每次获取同步点都会推进,所以暂时无需该判断方法。
*
* @return 是否有新数据
* @throws Exception 异常信息
*/
private boolean hasNewData() throws Exception {
if (currentSlots.isEmpty()) {
return true;
}
List slotsInDB = getSlotsInDb();
// 判断是否获取到新的点位
Map slotMap = slotsInDB.stream().collect(Collectors.toMap(Slot::getDnNodeId, Function.identity()));
for (Slot slot : currentSlots) {
int dnNodeId = slot.getDnNodeId();
if (slotMap.containsKey(dnNodeId) && slotMap.get(dnNodeId).getEndCsn() > slot.getCurrentStartCsn()) {
return true;
}
}
// 没有新数据,休眠一段时间(等待同步点推进)
log.warn("there is no new data, currentSlots: {}, slotsInDB: {}", currentSlots, slotsInDB);
return false;
}
protected List getSlotsInDb() throws SQLException {
try (Connection connection = dwsConnectionPool.getConnection()) {
List slotsInDB =
BinlogApi.getSyncPoint(connection, tableName, slotName, 0, false, isNeedRedistribution);
LogUtil.withLogSwitch(dwsConfig, () -> log.info("get slot info from db slotsInDB: {}", slotsInDB));
return slotsInDB;
}
}
public List getNodesFromDB() throws SQLException {
try (Connection connection = getDwsConnectionPool().getConnection()) {
return BinlogApi.getNodeIdsWithTableName(connection, getTableName());
}
}
/**
* 判断currentStartCsn是否小于endCsn
* 1.小于表示继续消费当前批次的数据
* 2.否则表示需要重新从内核获取同步点信息
*
* @return 当前批次的数据是否完全消费完
*/
private boolean nextCycle() {
return currentSlots.stream().allMatch(Slot::isEnd);
}
/**
* 使用传入的点位信息(specifySlots)来替换当前的点位信息
*/
private void mergeCurrentSlotList() {
if (Objects.isNull(specifySlots) || specifySlots.size() == 0) {
return;
}
if (specifySlots.size() == 1 && specifySlots.get(0) instanceof FullSyncSlot) {
currentSlots.forEach(slot -> {
slot.setStartCsn(Constants.FULL_SYNC_SLOT_CSN);
slot.setCurrentStartCsn(Constants.FULL_SYNC_SLOT_CSN);
});
} else {
// 使用指定slot的currentStartCsn来替代内核返回的(增量场景)
Map slotMap =
specifySlots.stream().collect(Collectors.toMap(Slot::getDnNodeId, Function.identity()));
for (Slot slot : currentSlots) {
int dnNodeId = slot.getDnNodeId();
if (slotMap.containsKey(dnNodeId)) {
Slot specifySlot = slotMap.get(dnNodeId);
if (specifySlot.getCurrentStartCsn() >= slot.getCurrentStartCsn()) {
// 属于异常清空,直接忽略
log.warn("specify slot is large than current slot, ignore...");
continue;
}
// 使用指定同步点来更新当前消费点位,之后从specifySlots中删除
slot.setCurrentStartCsn(specifySlot.getCurrentStartCsn());
log.info("use specify slot. specifySlot: {} and remove...", specifySlot);
specifySlots.removeIf(toBeRemoveSlot -> toBeRemoveSlot.getDnNodeId() == dnNodeId);
log.info("replaced current slot. currentSlots: {}, specifySlots: {}", currentSlots, specifySlots);
}
}
}
}
/**
* 调用内核接口全量或增量读取binlog信息
*/
private void collectBinlog() {
if (needFullSync.get()) {
binlogFullSyncCollectors.forEach(binlogFullSyncCollector -> completableFutureList
.add(CompletableFuture.runAsync(binlogFullSyncCollector, binlogCollectExecutorService)));
} else {
binlogCollectors.forEach(binlogCollector -> completableFutureList
.add(CompletableFuture.runAsync(binlogCollector, binlogCollectExecutorService)));
}
}
/**
* 初始化BinlogCollectors,需要根据同步点数量以及用户传入的并发度来初始化
*/
private void initBinlogCollectors() {
binlogCollectors.clear();
binlogFullSyncCollectors.clear();
totalRecord.set(0);
int slotNum = currentSlots.size();
// 如果用户传入的并发度小于slotNum,则一个线程需要读取多个DN上的数据
// 如果用户传入的并发度大于等于slotNum,则只需要起slotNum个线程
Map> map = new HashMap<>();
for (int i = 0; i < slotNum; i++) {
int index = i % binlogParallelNum;
map.computeIfAbsent(index, r -> new ArrayList<>()).add(currentSlots.get(i));
}
map.forEach((key, slotList) -> {
boolean isEnd = slotList.stream().allMatch(Slot::isEnd);
if (isEnd) {
// 表示该dn上的数据在此批次已经消费完毕
return;
}
if (needFullSync.get()) {
BinlogFullSyncCollector binlogFullSyncCollector = new BinlogFullSyncCollector(queue, slotList, started,
fullSyncBinlogBatchReadSize, dwsConnectionPool, columnNames, tableName, totalRecord);
binlogFullSyncCollectors.add(binlogFullSyncCollector);
} else {
BinlogCollector binlogCollector = new BinlogCollector(queue, slotList, started, tableConfig,
dwsConnectionPool, columnNames, totalRecord);
binlogCollectors.add(binlogCollector);
}
});
}
/**
* 向内核注册同步点信息
*
* @throws Exception 异常信息
*/
private void registerSlot() throws Exception {
CompletableFuture completableFuture =
CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[0])).whenComplete((v, err) -> {
completableFutureList.clear();
});
long timeout =
needFullSync.get() ? tableConfig.getFullSyncBinlogReadTimeout() : tableConfig.getBinlogReadTimeout();
completableFuture.get(timeout, TimeUnit.MILLISECONDS);
if (needFullSync.get() || nextCycle()) {
// 需要往每个dn上提交
for (Slot slot : currentSlots) {
try (Connection connection = dwsConnectionPool.getConnection()) {
BinlogApi.updateSyncPoint(connection, tableName, slotName, slot.getEndCsn(), slot.getDnNodeId(),
slot.getXmin(), false, tableConfig.getUpdateSyncPointTimeOut());
}
}
}
}
/**
* 重置标志位
*/
private void reset() {
needFullSync.set(false);
}
/**
* 启动BinlogReader
*/
public void start() {
if (!started.compareAndSet(false, true)) {
log.warn("BinlogReader has started...");
return;
}
log.info("start BinlogReader...");
// 系统停止时执行关闭
Runtime.getRuntime().addShutdownHook(new Thread(this::close));
}
/**
* binlog相关参数校验
*
* @param dwsConfig 配置参数
*/
private void checkParams(DwsConfig dwsConfig) {
String binlogTableName = dwsConfig.getBinlogTableName();
AssertUtil.nonNull(binlogTableName, new InvalidException("tableName not set"));
TableConfig binlogTableConfig = dwsConfig.getTableConfig(binlogTableName);
AssertUtil.nonNull(binlogTableConfig, new InvalidException("tableConfig not set"));
boolean binlog = binlogTableConfig.isBinlog();
AssertUtil.isTrue(binlog, new InvalidException("binlog is false"));
String binlogSlotName = binlogTableConfig.getBinlogSlotName();
if (Objects.isNull(binlogSlotName) || "".equals(binlogSlotName)) {
// 如果slotName为空则默认用tableName来设置slotName
binlogTableConfig.withBinlogSlotName(binlogTableName);
}
}
public void stop() {
started.set(false);
}
public boolean isStart() {
return started.get();
}
private void stopCollectors() {
log.info("stop collectors...");
// 清理资源
binlogFullSyncCollectors.forEach(BinlogFullSyncCollector::stop);
binlogCollectors.forEach(BinlogCollector::stop);
}
public void close() {
try {
if (!started.compareAndSet(true, false)) {
log.warn("BinlogReader has closed...");
return;
}
log.info("BinlogReader close start");
BinlogApi.NODE_COUNT.remove();
stopCollectors();
binlogCollectExecutorService.shutdownNow();
while (!binlogCollectExecutorService.awaitTermination(AWAIT_TIME, TimeUnit.MILLISECONDS)) {
log.info("wait binlog collect executorService termination");
}
binlogGetRecordExecutorService.shutdownNow();
while (!binlogGetRecordExecutorService.awaitTermination(AWAIT_TIME, TimeUnit.MILLISECONDS)) {
log.info("wait binlog get record executorService termination");
}
dwsConnectionPool.close();
log.info("BinlogReader close end");
} catch (Exception e) {
log.error("shutdown binlog executorService error.", e);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy