远端配置方式:
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));
}
}