canal adapter远程配置方式

远端配置方式:

canal版本:1.1.4

配置方式:基于canal-admin的HA模式

1、修改bootstrap.yml

canal:
  manager:
    jdbc:
      url: jdbc:mysql://127.0.0.1:3306/canal_manager?useUnicode=true&characterEncoding=UTF-8
      username: root
      password: 123456

2、在canal_manager库中的canal_config表中插入一条id为2的记录(源码中写死为2),该记录填写application.yml的内容,在canal_adapter_config表中插入一条记录,内容为rdb表的配置文件内容

3、启动适配器即可

存在的问题及原因:

1、修改application.yml将导致适配器无限重启

原因:适配器启动时将从远端数据库加载配置文件到本地,修改数据库中application.yml内容后,触发文件更改事件,将调用contextRefresher.refresh(),该方法内部将重新构建一个springboot环境,该构建过程调用了BootstrapConfiguration类的loadRemoteConfig方法,该方法将再次触发文件更改事件,至此开始无限循环之旅。

public class BootstrapConfiguration {

    @Autowired
    private Environment        env;

    private RemoteConfigLoader remoteConfigLoader = null;

    @PostConstruct
    public void loadRemoteConfig() {
        remoteConfigLoader = RemoteConfigLoaderFactory.getRemoteConfigLoader(env);
        if (remoteConfigLoader != null) {
            remoteConfigLoader.loadRemoteConfig();// 此为加载远端数据库中application.yml的方法
            remoteConfigLoader.loadRemoteAdapterConfigs();
            remoteConfigLoader.startMonitor(); // 启动监听
        }
    }

    @PreDestroy
    public synchronized void destroy() {
        if (remoteConfigLoader != null) {
            remoteConfigLoader.destroy();
        }
    }
}
/**
 * 加载远程application.yml配置
 */
@Override
public void loadRemoteConfig() {
    try {
        // 加载远程adapter配置
        ConfigItem configItem = getRemoteAdapterConfig();
        if (configItem != null) {
            if (configItem.getModifiedTime() != currentConfigTimestamp) {
                currentConfigTimestamp = configItem.getModifiedTime();
                overrideLocalCanalConfig(configItem.getContent());// 加载远端配置文件到本地,本质上就是覆写文件
                logger.info("## Loaded remote adapter config: application.yml");
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}

此方法将触发文件更改事件

/**
     * 覆盖本地application.yml文件
     *
     * @param content 文件内容
     */
    private void overrideLocalCanalConfig(String content) {

        try (OutputStreamWriter writer = new OutputStreamWriter(
            new FileOutputStream(CommonUtils.getConfPath() + "application.yml"),
            StandardCharsets.UTF_8)) {
            writer.write(content);
            writer.flush();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

文件更改事件如下:

@Override
public void onFileChange(File file) {
    super.onFileChange(file);
    try {
        // 检查yml格式
        new Yaml().loadAs(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8), Map.class);

        canalAdapterService.destroy();

        // refresh context
        contextRefresher.refresh();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // ignore
        }
        canalAdapterService.init();
        logger.info("## adapter application config reloaded.");
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}

解决方案:

修改 com.alibaba.otter.canal.adapter.launcher.config.BootstrapConfiguration 类,改为如下:

public class BootstrapConfiguration {

    @Autowired
    private Environment env;

    private RemoteConfigLoader remoteConfigLoader = null;

    // 通过此变量控制此类的loadRemoteConfig方法只被执行一次
    // 防止远程配置环境下执行contextRefresher.refresh()时调用到loadRemoteConfig再次触发文件更改事件,造成死循环
    private volatile boolean inited = false;

    @PostConstruct
    public void loadRemoteConfig() {
        if (!inited) {
            remoteConfigLoader = RemoteConfigLoaderFactory.getRemoteConfigLoader(env);
            if (remoteConfigLoader != null) {
                remoteConfigLoader.loadRemoteConfig();
                remoteConfigLoader.loadRemoteAdapterConfigs();
                remoteConfigLoader.startMonitor(); // 启动监听
                inited = true;
            }
        }
    }

    @PreDestroy
    public synchronized void destroy() {
        if (remoteConfigLoader != null) {
            remoteConfigLoader.destroy();
        }
    }
}

 

2、修改rdb配置文件需重启才能生效

rdb适配器运行流程:

1、加载本地/远端rdb配置文件存入mappingConfigCache 变量,此变量的键与使用的同步模式有关,如果使用消息队列,则键为:destination+ "-"+ groupId+ ""+ database + "-" + table ;如果使用tcp,则键为:destination+ ""+ database + "-" + table 。

2、接收到消息后使用mappingConfigCache 中的配置信息组装成SQL在目标库中执行

部分相关源码如下:

com.alibaba.otter.canal.client.adapter.rdb.RdbAdapter

private Map<String, MappingConfig> rdbMapping = new ConcurrentHashMap<>();                // 文件名对应配置
private Map<String, Map<String, MappingConfig>> mappingConfigCache = new ConcurrentHashMap<>();                // 库名-表名对应配置
   /**
     * 初始化方法
     *
     * @param configuration 外部适配器配置信息
     */
    @Override
    public void init(OuterAdapterConfig configuration, Properties envProperties) {
        this.envProperties = envProperties;
        Map<String, MappingConfig> rdbMappingTmp = ConfigLoader.load(envProperties);
        // 过滤不匹配的key的配置
        rdbMappingTmp.forEach((key, mappingConfig) -> {
            if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
                    || (mappingConfig.getOuterAdapterKey() != null && mappingConfig.getOuterAdapterKey()
                    .equalsIgnoreCase(configuration.getKey()))) {
                rdbMapping.put(key, mappingConfig);
            }
        });

        if (rdbMapping.isEmpty()) {
            throw new RuntimeException("No rdb adapter found for config key: " + configuration.getKey());
        }

        for (Map.Entry<String, MappingConfig> entry : rdbMapping.entrySet()) {
            String configName = entry.getKey();
            MappingConfig mappingConfig = entry.getValue();
            if (!mappingConfig.getDbMapping().getMirrorDb()) {
								// mappingConfigCache 的键的生成规则如下:
                String key;
                if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
                    key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
                            + StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
                            + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
                } else {
                    key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
                            + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
                }
                Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(key,
                        k1 -> new ConcurrentHashMap<>());
                configMap.put(configName, mappingConfig);
            } else {
                // mirrorDB
                String key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
                        + mappingConfig.getDbMapping().getDatabase();
                mirrorDbConfigCache.put(key, MirrorDbConfig.create(configName, mappingConfig));
            }
        }

        // 以下省略若干不关注的代码
    }

com.alibaba.otter.canal.client.adapter.rdb.RdbAdapter

/**
     * 同步方法
     *
     * @param dmls 数据包
     */
    @Override
    public void sync(List<Dml> dmls) {
        if (dmls == null || dmls.isEmpty()) {
            return;
        }
        try {
            rdbSyncService.sync(mappingConfigCache, dmls, envProperties); // 使用mappingConfigCache对象中存储的配置文件信息进行SQL组装
            rdbMirrorDbSyncService.sync(dmls);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

com.alibaba.otter.canal.client.adapter.rdb.service.RdbSyncService

public void sync(Map<String, Map<String, MappingConfig>> mappingConfig, List<Dml> dmls, Properties envProperties) {
        sync(dmls, dml -> {
            if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
                // DDL
                columnsTypeCache.remove(dml.getDestination() + "." + dml.getDatabase() + "." + dml.getTable());
                return false;
            } else {
                // DML
                String destination = StringUtils.trimToEmpty(dml.getDestination());
                String groupId = StringUtils.trimToEmpty(dml.getGroupId());
                String database = dml.getDatabase();
                String table = dml.getTable();
                Map<String, MappingConfig> configMap;
								// 重点在此,此处根据同步模式生成键,然后从mappingConfigCache 对象中获取配置信息,可以看到此处键的生成方式与RdbAdapter中的init方法保持了一致,没有问题
                if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
                    configMap = mappingConfig.get(destination + "-" + groupId + "_" + database + "-" + table);
                } else {
                    configMap = mappingConfig.get(destination + "_" + database + "-" + table);
                }

                if (configMap == null) {
                    return false;
                }

                if (configMap.values().isEmpty()) {
                    return false;
                }

                for (MappingConfig config : configMap.values()) {
                    if (config.getConcurrent()) {
                        List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
                        singleDmls.forEach(singleDml -> {
                            int hash = pkHash(config.getDbMapping(), singleDml.getData());
                            SyncItem syncItem = new SyncItem(config, singleDml);
                            dmlsPartition[hash].add(syncItem);
                        });
                    } else {
                        int hash = 0;
                        List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml);
                        singleDmls.forEach(singleDml -> {
                            SyncItem syncItem = new SyncItem(config, singleDml);
                            dmlsPartition[hash].add(syncItem);
                        });
                    }
                }
                return true;
            }
        });
    }

com.alibaba.otter.canal.client.adapter.rdb.monitor.RdbConfigMonitor

@Override
public void onFileChange(File file) {
    super.onFileChange(file);
    try {
        if (rdbAdapter.getRdbMapping().containsKey(file.getName())) {
            // 加载配置文件
            String configContent = MappingConfigsLoader
                .loadConfig(adapterName + File.separator + file.getName());
            if (configContent == null) {
                onFileDelete(file);
                return;
            }
            MappingConfig config = YmlConfigBinder
                .bindYmlToObj(null, configContent, MappingConfig.class, null, envProperties);
            if (config == null) {
                return;
            }
            config.validate();
            if ((key == null && config.getOuterAdapterKey() == null)
                || (key != null && key.equals(config.getOuterAdapterKey()))) {
                if (rdbAdapter.getRdbMapping().containsKey(file.getName())) {
                    deleteConfigFromCache(file);
                }
                addConfigToCache(file, config);// 重点是此方法
            } else {
                // 不能修改outerAdapterKey
                throw new RuntimeException("Outer adapter key not allowed modify");
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}

com.alibaba.otter.canal.client.adapter.rdb.monitor.RdbConfigMonitor

private void addConfigToCache(File file, MappingConfig mappingConfig) {
    if (mappingConfig == null || mappingConfig.getDbMapping() == null) {
        return;
    }
    rdbAdapter.getRdbMapping().put(file.getName(), mappingConfig);
    if (!mappingConfig.getDbMapping().getMirrorDb()) {
				// 坑就在这里,此处生成键的规则直接默认为tcp模式了,如果使用的是MQ模式,就会导致生成一个新键(RdbSyncService中匹配不上)存入配置信息,而原来的键的值已经在deleteConfigFromCache方法中删除了
        Map<String, MappingConfig> configMap = rdbAdapter.getMappingConfigCache()
            .computeIfAbsent(StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
                             + mappingConfig.getDbMapping().getDatabase() + "-"
                             + mappingConfig.getDbMapping().getTable(),
                k1 -> new HashMap<>());
        configMap.put(file.getName(), mappingConfig);
    } else {
        Map<String, MirrorDbConfig> mirrorDbConfigCache = rdbAdapter.getMirrorDbConfigCache();
        mirrorDbConfigCache.put(StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
                                + mappingConfig.getDbMapping().getDatabase(),
            MirrorDbConfig.create(file.getName(), mappingConfig));
    }
}

配置文件修改不能及时生效的原因即修改配置文件后,重新加载时未正确覆盖原键值对,导致RdbSyncService中获取不到配置信息而跳过。

解决方案:

修改com.alibaba.otter.canal.client.adapter.rdb.monitor.RdbConfigMonitor中的addConfigToCache方法如下:

private void addConfigToCache(File file, MappingConfig mappingConfig) {
    if (mappingConfig == null || mappingConfig.getDbMapping() == null) {
        return;
    }
    rdbAdapter.getRdbMapping().put(file.getName(), mappingConfig);
    if (!mappingConfig.getDbMapping().getMirrorDb()) {
        String key;
        if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
            key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
                    + StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
                    + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
        } else {
            key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
                    + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
        }
        Map<String, MappingConfig> configMap = rdbAdapter.getMappingConfigCache()
            .computeIfAbsent(key, k1 -> new HashMap<>());
        configMap.put(file.getName(), mappingConfig);
    } else {
        Map<String, MirrorDbConfig> mirrorDbConfigCache = rdbAdapter.getMirrorDbConfigCache();
        mirrorDbConfigCache.put(StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
                                + mappingConfig.getDbMapping().getDatabase(),
            MirrorDbConfig.create(file.getName(), mappingConfig));
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值