canal adapter 1.1.4使用中的问题

canal adapter 是canal 中的一个组件,canal通过拉取mysql binlog的方式提供对binlog的增量消费,canal adapter提供了es,mysql,hbase等增量消费mysql数据的能力,现使用canal adapter 1.1.4 发行版本用于mysql->es 的增量与全量同步

  • 全量同步
    Canal 本身不提供全量同步的能力,canal adapter自身通过select 全表的方式进行全量同步,在全量同步开始前,记下增量同步的位点,全量同步完成后,通过基于row模式的binlog进行覆盖以保证数据的一致性
  • 增量同步
    canal adapter通过canal 拉取binlog进行过滤并消费,通过解析mysql到es的一条sql来确定其对应关系
    具体流程源码可以查看 https://github.com/alibaba/canal

现讨论使用过程中遇到的一些问题

1.如何保证高可用

通过两个canal adapter服务同时去订阅一个canal 的方式来保证高可用,同一时间,只会有一个 canal adapter订阅成功,其它canal adapter只会处在开始订阅状态,当成功订阅canal adapter的机器宕机时,另一台机器便会成功订阅。可以在zk的canal-adapter节点查看当前destination成功订阅的机器ip
在这里插入图片描述

2.产品化支持

Canal adapter本身是支持远程数据库中读取配置的,藏的比较隐蔽,通过在bootsrap.yml文件中指定远程数据库进行读取配置, 配置文件有两种

  • application.yml: canal_config 表中id=2的content字段存储,主要包括全量同步的datasource以及增量任务的配置
private ConfigItem getRemoteAdapterConfig() {
        String sql = "select name, content, modified_time from canal_config where id=2";
        try (Connection conn = dataSource.getConnection();
                Statement stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery(sql)) {
            if (rs.next()) {
                ConfigItem configItem = new ConfigItem();
                configItem.setId(2L);
                configItem.setName(rs.getString("name"));
                configItem.setContent(rs.getString("content"));
                configItem.setModifiedTime(rs.getTimestamp("modified_time").getTime());
                return configItem;
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
  • xxx.yml:canal_adapter_config表中的每行记录,每一行都是一个mysql->es同步的映射文件
private void loadModifiedAdapterConfigs() {
        Map<String, ConfigItem> remoteConfigStatus = new HashMap<>();
        String sql = "select id, category, name, modified_time from canal_adapter_config";
        try (Connection conn = dataSource.getConnection();
                Statement stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery(sql)) {
            while (rs.next()) {
                ConfigItem configItem = new ConfigItem();
                configItem.setId(rs.getLong("id"));
                configItem.setCategory(rs.getString("category"));
                configItem.setName(rs.getString("name"));
                configItem.setModifiedTime(rs.getTimestamp("modified_time").getTime());
                remoteConfigStatus.put(configItem.getCategory() + "/" + configItem.getName(), configItem);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        ...
        }

确定了配置文件读取方式后,如下文各种坑就开始了

3.多任务跑串问题

通过CommonRequest 类中的etl开关进行多任务的控制,发现多任务增量开关正常使用,log也没有异常,但是增量开关打开后,并没有增量同步能力。一顿debug后发现开关中控制的destination跑串了,多任务开关打开关闭会通过outerAdapterKey这个字段来进行管控,需要在两种配置文件中分别进行配置,不看源码甚至连这个配置项的存在都不知道。。

public class ESSyncConfig implements AdapterConfig {

    private String    dataSourceKey;   // 数据源key

    private String    outerAdapterKey; // adapter key

    private String    groupId;         // group id

    private String    destination;     // canal destination

    private ESMapping esMapping;
                   ...
}

4.任务暂停状态下,产品化的应用如何找到成功订阅的canal adapter 地址

由于1的解决,adapter产品化的服务需要通过etl api来对任务进行管控,一旦任务停止过长,zk就会丢失当前订阅成功canal adapter 的ip,就无法使用etl进行管控,这样就需要存在一个默认的持续开启的任务来找到多台canal adapter中至少一台机器的ip

5.修改远程配置数据库后,canal adapter会不断重复订阅任务、取消这两个流程

首先确定配置的修改顺序是先application.yml,再修改映射文件。重复订阅任务、取消显然是存在死循环,adapter读取远程配置是通过2s一读数据库,对于数据库的modifyTime字段与本地存储的currentConfigTimestamp进行比对来确定是否更新。debug发现每次读取远程数据库写入adapter都会refresh之前的配置类,重新生成一个currentConfigTimestamp 造成死循环。在DbRemoteConfigLoader类中新增一个单例的adapterConfigHolder来获取该时间标示,成功解决

public class DbRemoteConfigLoader implements RemoteConfigLoader {

    private static final Logger      logger                 = LoggerFactory.getLogger(DbRemoteConfigLoader.class);

    private DruidDataSource          dataSource;

    private volatile long            currentConfigTimestamp = 0;//就是它
   
    private Map<String, ConfigItem>  remoteAdapterConfigs   = new MapMaker().makeMap();

    private ScheduledExecutorService executor               = Executors.newScheduledThreadPool(2,
        new NamedThreadFactory("remote-adapter-config-scan"));

    private RemoteAdapterMonitor     remoteAdapterMonitor   = new RemoteAdapterMonitorImpl();
                        ...
}

6.全量同步时抛出大量es连接关闭异常

如题,主要是由于int threadCount = Runtime.getRuntime().availableProcessors(); 获取当前cpu数量来决定线程数,对于一个2C的k8s应用可能得到几十个线程

 public class AbstractEtlService{
 ...
 if (cnt >= 10000) {
              int threadCount = Runtime.getRuntime().availableProcessors();
               
                long offset;
                long size = CNT_PER_TASK;
                long workerCnt = cnt / size + (cnt % size == 0 ? 0 : 1);

                if (logger.isDebugEnabled()) {
                    logger.debug("workerCnt {} for cnt {} threadCount {}", workerCnt, cnt, threadCount);
                }
                }

7.全量同步连接池问题

需要在application.yml中指定全量同步的数据库连接数量 maxActive,同问题3,配置项隐藏比较深。。

8.全量同步数据库连接超时

数据库全量同步通过select全表方式拉数据,流式查询只能保护客户端,服务端会产生深度分页问题,将其改造成使用主键索引的方式去select全表大大提升速度,但是要要求主键必须是自增的,不然全量同步时会丢失数据。
`try {
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());

        List<Object> values = new ArrayList<>();
        // 拼接条件
        if (config.getMapping().getEtlCondition() != null && params != null) {
            String etlCondition = config.getMapping().getEtlCondition();
            for (String param : params) {
                etlCondition = etlCondition.replace("{}", "?");
                values.add(param);
            }

            sql += " " + etlCondition;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("etl sql : {}", sql);
        }

        // 获取总数
        String countSql = "SELECT COUNT(1) FROM ( " + sql + ") _CNT ";
        long cnt = (Long) Util.sqlRS(dataSource, countSql, values, rs -> {
            Long count = null;
            try {
                if (rs.next()) {
                    count = ((Number) rs.getObject(1)).longValue();
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
            return count == null ? 0L : count;
        });

        // 当大于1万条记录时开启多线程
        if (cnt >= 10000) {
            int threadCount = Runtime.getRuntime().availableProcessors();
            long offset;
            long size = CNT_PER_TASK;
            long workerCnt = cnt / size + (cnt % size == 0 ? 0 : 1);

            if (logger.isDebugEnabled()) {
                logger.debug("workerCnt {} for cnt {} threadCount {}", workerCnt, cnt, threadCount);
            }

            ExecutorService executor = Util.newFixedThreadPool(threadCount, 5000L);
            List<Future<Boolean>> futures = new ArrayList<>();
            for (long i = 0; i < workerCnt; i++) {
                offset = size * i;
                String sqlFinal = sql + " LIMIT " + offset + "," + size;
                Future<Boolean> future = executor.submit(() -> executeSqlImport(dataSource,
                    sqlFinal,
                    values,
                    config.getMapping(),
                    impCount,
                    errMsg));
                futures.add(future);
            }

            for (Future<Boolean> future : futures) {
                future.get();
            }
            executor.shutdown();
        } else {
            executeSqlImport(dataSource, sql, values, config.getMapping(), impCount, errMsg);
        }

        logger.info("数据全量导入完成, 一共导入 {} 条数据, 耗时: {}", impCount.get(), System.currentTimeMillis() - start);
        etlResult.setResultMessage("导入" + type + " 数据:" + impCount.get() + " 条");
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        errMsg.add(type + " 数据导入异常 =>" + e.getMessage());
    }
    if (errMsg.isEmpty()) {
        etlResult.setSucceeded(true);
    } else {
        etlResult.setErrorMessage(Joiner.on("\n").join(errMsg));
    }
    return etlResult;`
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 要下载 canal.deployer 1.1.4,您可以按照以下步骤进行操作: 1. 打开您的互联网浏览器,并在搜索引擎输入 "canal.deployer 1.1.4下载"。 2. 在搜索结果找到可靠的站点,例如 Apache 官方网站、GitHub 或其他受信任的软件下载站点。 3. 确保您正在访问正版和安全的下载渠道,以防止下载到病毒或恶意软件。 4. 在下载页面上,查找适用于您的操作系统的正确版本的 canal.deployer 1.1.4。通常,会提供适用于 Windows、Mac、Linux 和其他一些流行操作系统的版本。 5. 点击 "下载" 或类似按钮,启动下载过程。根据您的互联网连接速度和文件大小,下载时间可能有所不同。 6. 下载完成后,切勿随意运行未经验证的软件。请务必对下载的文件进行安全性扫描,以确保其没有病毒或恶意代码。 7. 如果下载的是压缩文件(通常是 .zip 或 .tar.gz 格式),请解压缩文件到您选择的目录。 8. 您现在可以在您的计算机上使用 canal.deployer 1.1.4 了。根据软件的特定安装要求,可以使用终端命令或图形界面来启动或安装它。 请注意,以上步骤可能会根据您的操作系统和下载渠道稍有不同。重要的是要选择可信任的来源,并始终确保您下载的软件是正版和安全的。 ### 回答2: 为了下载canal.deployer 1.1.4,你可以按照以下步骤进行操作: 1. 打开网页浏览器,进入canal的官方仓库或者可靠的第三方下载平台。 2. 在搜索栏输入“canal.deployer 1.1.4”的关键字。 3. 单击搜索按钮开始搜索。 4. 在结果列表找到适用于你系统的版本,确保下载的是最新且稳定版本。 5. 点击选择要下载的版本。 6. 查看下载页面的说明和版本兼容性,确保该版本适用于你的操作系统和相关环境。 7. 确认无误后,点击下载按钮开始下载。 8. 在弹出的保存文件对话框选择保存文件的位置,并点击“保存”按钮。 9. 等待下载完成,这可能需要一些时间,具体时间取决于你的网络速度。 10. 下载完成后,可以通过文件管理器打开下载的文件所在位置,或根据你的浏览器设置查找下载的文件。 希望以上回答能对你有所帮助! ### 回答3: 要下载 canal.deployer 1.1.4,您可以按照以下步骤操作: 1. 打开您的浏览器,访问 Canal GitHub 仓库的 Releases 页面。 2. 在页面上找到版本号为 1.1.4 的 canal.deployer 发布版本。您可以使用页面上的筛选功能或者滚动查找来定位。 3. 点击对应版本的 canal.deployer 下载链接。通常,下载链接会指向一个压缩文件(如 ZIP 或 TAR.GZ)。 4. 下载文件后,解压缩压缩文件。您可以使用操作系统提供的解压缩工具,或者使用第三方工具(如 WinRAR、7-Zip 等)。 5. 解压缩后,您可以在相应的文件夹找到 canal.deployer 1.1.4 的文件。 注意事项: 1. 在下载和使用任何软件之前,请确保您具备足够的权限和合法性。 2. 如果您在 GitHub 仓库上找不到 canal.deployer 1.1.4 版本,可能它已被更新或删除。您可以尝试查找其他版本或者联系开发者获取更多信息。 希望这些信息能够帮助您成功下载 canal.deployer 1.1.4。如果您还有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值