系列文章
- 第一篇:选型、架构与前置准备
- 第二篇:部署 canal-server 与 adapter
- 第三篇:踩坑与注意事项 ← 本篇
按实际踩到的顺序排列,也是推荐的排查顺序:
| # | 现象 | 根因 | 解法 |
|---|---|---|---|
| 0 | Plugin 'mysql_native_password' is not loaded | MySQL 8.4 默认禁用该插件 | 改用 caching_sha2_password |
| 1 | command 'show master status' has an error! | MySQL 8.4 移除了该语句 | 固定位点启动 |
| 2 | 认证失败 / 驱动不兼容 | adapter 自带 5.1.48 驱动 | 换 8.0.33 驱动 |
| 3 | None of the configured nodes are available | es7 适配器默认走 TransportClient | mode: rest |
| 4 | Illegal character in scheme name at index 0 | hosts 缺协议头 | 改成 http://ip:9200 |
| 5 | 启动 NullPointerException | application.yml 缺 accessKey | 补上空键 |
| 6 | 全量同步 NPE / 导入 0 条 | 索引没建或没有 mapping | 先建带 mapping 的索引 |
| 7 | ETL 成功但 count=0 | ES 未 refresh | _refresh 或等 1 秒 |
| 8 | CentOS 7 装不上 JDK | 系统 EOL,yum 源失效 | 用二进制包 |
坑 0 严格说是 MySQL 侧的坑,但它决定了后面所有配置的方向,所以放第一位。
mysql_native_password现象
在 MySQL 8.4 上按教程创建 Canal 账号:
sqlCREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'xxx';
ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded
按网上说的先装插件:
sqlINSTALL PLUGIN mysql_native_password SONAME 'mysql_native_password.so';
ERROR 1125 (HY000): Function 'mysql_native_password' already exists
再按另一篇教程改系统变量:
sqlSET PERSIST mysql_native_password = ON;
ERROR 1193 (HY000): Unknown system variable 'mysql_native_password'
三条路全堵死。
根因
MySQL 8.4 把 mysql_native_password 改成了内置插件,但默认关闭:
INSTALL PLUGIN 会报 "already exists"——它确实存在,只是没启用SET PERSIST 报 "Unknown system variable"官方给的启用方式是启动参数:
bashmysqld --mysql-native-password=ON
Docker 环境两种改法:
bash# 方式 A:改 my.cnf 后重启容器(不丢数据)
docker exec <容器名> sh -c "sed -i '/^\[mysqld\]/a mysql_native_password=ON' /etc/my.cnf"
docker restart <容器名>
# 方式 B:重建容器时加启动参数
docker run -d ... mysql:8.4 --mysql-native-password=ON
但本文没有走这条路,而是直接使用默认的 caching_sha2_password:
sqlCREATE USER 'canal'@'%' IDENTIFIED BY '<CANAL_DB_PASSWORD>';
为什么这么选?
| 方案 | 影响面 | 风险 |
|---|---|---|
开 mysql_native_password | 全库所有客户端 | 改动的是数据库全局配置,其他服务可能受影响 |
用 caching_sha2 + 换驱动 | 只有 Canal 一个组件 | 换一个 jar 包,可回滚 |
在别人的库上动全局配置,永远比在自己的组件里换 jar 风险大。
延伸:MySQL 9.x 怎么办
mysql_native_password 在 MySQL 9.0 被彻底移除,没有开关能打开。如果业务库已经上了 9.x,只能走 caching_sha2 路线(换驱动),或者降级到 8.0。
SHOW MASTER STATUS 了现象
Canal 启动后实例日志刷屏:
ERROR com.alibaba.otter.canal.common.alarm.LogAlarmHandler - destination:example [com.alibaba.otter.canal.parse.exception.CanalParseException: command : 'show master status' has an error! Caused by: java.io.IOException: ErrorPacket [errorNumber=1064, fieldCount=-1, message=You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'master status' at line 1, sqlState=42000, sqlStateMarker=#] with command: show master status
根因
MySQL 从 8.0.22 开始废弃 SHOW MASTER STATUS,8.4 直接移除,替代品是:
sqlSHOW BINARY LOG STATUS;
而 Canal 1.1.7 的 MysqlConnection.query("show master status") 是硬编码在 jar 里的,改配置没有任何用。
解法 A:固定位点(本文采用,零代码改动)
先查出当前位点:
sqlSHOW BINARY LOG STATUS;
+----------------+----------+ | File | Position | +----------------+----------+ | bin-log.000048 | 4059448 | +----------------+----------+
写进 instance.properties:
propertiescanal.instance.master.journal.name = bin-log.000048 canal.instance.master.position = 4059448
Canal 看到位点已指定,就跳过 SHOW MASTER STATUS 直接开始 dump。
⚠️ 副作用必须知道:
位点是写死的。以下情况会让 Canal 起不来:
PURGE BINARY LOGS报错长这样:
Could not find first log file name in binary log index file
处理办法是重新查位点、改配置、删掉 conf/example/meta.dat、重启。
所以生产环境如果要用这个方案,必须做两件事:
binlog_expire_logs_seconds(默认 30 天),确保保留时间大于最长停机时间解法 B:升级 Canal
官方在后续版本修复了 MySQL 8.4 兼容。但 1.1.7 是大量教程和配套配置的基准版本,升级可能引入其他差异,学习阶段不建议折腾。
我们的选择理由:业务库是 8.4 且不能动,Canal 版本是教程指定的 1.1.7。两座山都搬不动,只能在中间找条路——固定位点是成本最低的那条。
caching_sha2_password现象
adapter 启动时数据库连接失败,日志里是认证相关的异常(Access denied 或密钥交换失败)。
根因
canal.adapter-1.1.7.tar.gz 的 lib/ 里带的是 mysql-connector-java-5.1.48.jar——2015 年的版本。
这个版本只支持 mysql_native_password,而 MySQL 8.x 默认是 caching_sha2_password。
解法
换成 8.x 驱动,并且把旧驱动移出 lib 目录:
bashcd /opt/canal/adapter/lib
curl -fsSL -o mysql-connector-java-8.0.33.jar \
https://maven.aliyun.com/repository/public/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar
mv mysql-connector-java-5.1.48.jar /opt/canal/
JDBC URL 同时加上:
&allowPublicKeyRetrieval=true&useSSL=false
两个额外踩到的细节:
① Maven 坐标变了
8.0.31 之后,坐标从 mysql:mysql-connector-java 改成了 com:mysql-connector-j:
❌ mysql/mysql-connector-java/8.0.33/... ✅ com/mysql/mysql-connector-j/8.0.33/...
按旧路径下载 8.0.33 会 404。我一开始以为是镜像没同步,换了三个源都是 404,才意识到是坐标变了。
② 旧驱动必须"移走",不能改名
startup.sh 是这样拼 CLASSPATH 的:
bashfor i in "$base"/lib/*.jar ; do ... done
*.jar 会匹配到 mysql-connector-java-5.1.48.jar.disabled 吗?
不会——但 *.jar 匹配不到 .jar.disabled,问题是脚本里用的是 lib/*,或者有些版本用 -cp "$base/lib/*" 由 JVM 展开,JVM 的 * 是匹配所有文件。
我第一次把旧驱动改成 .disabled,结果 Spring Boot 启动时两个驱动都在 classpath 里,出现了驱动类冲突。移出目录才是干净的做法。
验证:
bashls -l /opt/canal/adapter/lib/ | grep mysql
# 只应该剩 mysql-connector-java-8.0.33.jar
es7 适配器默认居然走 TransportClient现象
全量同步接口返回:
json{
"succeeded": false,
"resultMessage": "导入ES 数据:0 条",
"errorMessage": "nfturbo_collection etl failed! ==>
None of the configured nodes are available:
[{#transport#-1}{9J2-VM7wRZCdaZhH8WjiSg}{127.0.0.1}{127.0.0.1:9200}]"
}
关键线索:报错里的 #transport#-1。
这是 TransportClient 的典型特征,不是 REST 客户端。而且注意——它把 9200 当成了 transport 端口去连。
根因
client-adapter.es7x 虽然名字叫 es7,但默认模式是 transport。
解开 jar 看 classes,里面确实打包了:
org/elasticsearch/client/transport/TransportClient.class org/elasticsearch/client/transport/TransportClientNodesService.class ...
适配器的 ESConnection 类同时持有两个字段:
javaprivate ESClientMode mode;
private TransportClient transportClient;
private RestHighLevelClient restHighLevelClient;
默认 mode = TRANSPORT,所以你写 hosts: 127.0.0.1:9200,它就拿 9200 当传输端口去连,自然连不上。
解法
显式声明走 REST:
yaml - name: es7
hosts: http://127.0.0.1:9200
properties:
mode: rest
为什么会这样设计?
Canal 的 ES 适配器最早是为 ES 5/6 写的,那时候 TransportClient 是主流(性能好、支持批量、能拿元数据)。 ES 7 虽然废弃了 TransportClient,但为了兼容老用户,适配器保留了它并设为默认。
结果就是:ES 7 用户必须手动改,而官方文档里这个点写得非常隐蔽。
hosts 必须带协议头现象
切到 mode: rest 后重启,直接启动失败:
java.lang.RuntimeException: java.lang.IllegalArgumentException: Illegal character in scheme name at index 0: 127.0.0.1:9200
根因
REST 模式下,配置会传给 HttpHost 构造,它要求是完整 URL(带 scheme)。纯 ip:port 会被当成 scheme 解析,第一个字符 1 不合法。
解法
yamlhosts: http://127.0.0.1:9200
多节点逗号分隔:
yamlhosts: http://10.0.0.1:9200,http://10.0.0.2:9200
HTTPS 同理:https://es.example.com:9200。
这个坑和坑 3 是一对:改了
mode: rest之后,hosts的格式也得跟着改。 很多人改了一半就重启,结果从一个错误跳到另一个错误。
application.yml 少写两行 → 启动 NPE现象
启动 adapter 直接崩:
ERROR c.a.o.canal.adapter.launcher.loader.CanalAdapterService - ## something goes wrong when starting up the canal client adapters: java.lang.NullPointerException: null at java.util.Hashtable.put(Hashtable.java:460) at com.alibaba.otter.canal.adapter.launcher.loader.AdapterProcessor.<init>(AdapterProcessor.java:72) at com.alibaba.otter.canal.adapter.launcher.loader.CanalAdapterLoader.lambda$init$0(CanalAdapterLoader.java:85) at java.util.HashMap.computeIfAbsent(HashMap.java:1127) ...
排查过程
堆栈只显示 NPE,没有任何业务信息。日志里前后文也没有别的线索。
于是我反编译了 AdapterProcessor,定位到第 72 行:
javaProperties.put("canal.aliyun.accessKey", canalClientConfig.getAccessKey());
java.util.Properties 继承自 Hashtable,而 Hashtable 不允许 null 值。
配置里没写 accessKey 时,getAccessKey() 返回 null,put 直接抛 NPE。
解法
application.yml 里这些键必须存在,哪怕值为空:
yamlcanal.conf:
mode: tcp
flatMessage: true
zookeeperHosts: # ← 保留
syncBatchSize: 1000
retries: -1
timeout: # ← 保留
accessKey: # ← 保留(空值可以,键不能没有)
secretKey: # ← 保留
为什么会踩?
我最初为了"配置文件干净",把用不到的 key 全删了——zookeeperHosts、timeout、accessKey、secretKey 一个没留。
结果启动就崩。
结论:Canal 的配置文件,不用的键留空,但别删。
这条经验适用于所有用 Spring Boot + Properties 的老中间件——很多项目都有类似的"隐式必填项"。
顺带说下反编译命令,遇到这类无头 NPE 很有用:
bashunzip -o -q client-adapter.launcher-1.1.7.jar \ "com/alibaba/otter/canal/adapter/launcher/loader/AdapterProcessor.class" javap -p -c -l com/alibaba/otter/canal/adapter/launcher/loader/AdapterProcessor.class
-l会输出 LineNumberTable,能把行号和字节码对应起来。
现象
第一次跑 ETL:
json{"succeeded":false,"errorMessage":"nfturbo_collection etl failed! ==>
Elasticsearch exception [type=index_not_found_exception,
reason=no such index [nfturbo_collection]]"}
手工建了个空索引再跑:
json{"succeeded":false,"errorMessage":"nfturbo_collection etl failed! ==>null"}
日志里:
java.lang.NullPointerException: null at ...ES7xTemplate.getEsType(ES7xTemplate.java:405) at ...ES7xTemplate.getValFromRS(ES7xTemplate.java:186) at ...ESEtlService.lambda$executeSqlImport$1(ESEtlService.java:82)
根因
adapter 写入前会读索引的 mapping,用字段类型决定如何转换值(日期格式、数字类型等)。
index_not_found_exceptiongetEsType() 返回 null → NPE注意这是两个不同阶段的问题。别以为"我建了索引啊"就好了——建了空索引只是把错误从 A 变成了 B。
解法
建索引时把 mapping 一起写进去(完整示例见第二篇 4.4):
bashcurl -u 'elastic:<ES_PASSWORD>' -X PUT "http://127.0.0.1:9200/nfturbo_collection" \
-H 'Content-Type: application/json' -d '{
"settings": { "number_of_shards": 1, "number_of_replicas": 0 },
"mappings": {
"properties": {
"name": { "type": "text" },
"price": { "type": "double" },
"create_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" }
}
}
}'
字段一个都不能漏吗?
不是。adapter 只对SQL 里 select 出来的字段查类型。少写字段会导致该字段同步失败或类型不符,但不会全部 NPE。
保险起见,把映射 SQL 里的字段都写进 mapping。
现象
json{"succeeded":true,"resultMessage":"导入ES 数据:8 条"}
紧接着查:
json{"count":0,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}
根因
ES 的 refresh_interval 默认 1 秒。写入的数据先进内存 buffer,1 秒后才刷新成可搜索的段。你查得太快了。
这不是同步失败,是时序问题。
解法
bash# 强制刷新
curl -u 'elastic:<ES_PASSWORD>' -X POST "http://127.0.0.1:9200/nfturbo_collection/_refresh"
# 或查询时带参数
curl -u 'elastic:<ES_PASSWORD>' "http://127.0.0.1:9200/nfturbo_collection/_search?refresh=true"
# 或干脆 sleep 1
延伸:什么时候该调 refresh_interval?
| 场景 | 建议 |
|---|---|
| 日常同步 | 保持默认 1s,查不到就多等一秒 |
| 大批量导入 | 临时设 "refresh_interval": "-1",导完再改回来,能显著提升吞吐 |
| 要求强实时 | 设 1s 已经是最小值,再小会严重影响写入性能 |
现象
bashyum install -y java-1.8.0-openjdk
各种 404、Cannot find a valid baseurl for repo: base/7/x86_64。
根因
CentOS 7 已于 2024-06-30 EOL,官方镜像源下线,mirrorlist.centos.org 不再解析。
解法
别折腾 yum 源,直接下 JDK 8 二进制包解压(详见第一篇第五节):
bashmkdir -p /usr/local/java
tar -xzf jdk-8u202-linux-x64.tar.gz -C /usr/local/java
要修复 yum 也可以把源换成 vault.centos.org:
bashsed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-*.repo
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*.repo
yum clean all && yum makecache
但为了装个 JDK 做这些,不如直接解压——二进制包不依赖系统的包管理器,还不会污染全局环境。
本文因为 MySQL 8.4 的兼容问题用了固定位点,这是权宜之计。
生产环境要关注:
binlog_expire_logs_seconds 别调太小。SHOW MASTER STATUS 和 meta.dat 里的位点做比对。能用位点自增就别写死。 如果 MySQL 版本允许,把 journal.name 和 position 留空,让 Canal 自己管理。
Canal 会把 DDL 事件传给 adapter,但 adapter 不会自动修改 ES 的 mapping。
加字段、改类型时:
PUT /index/_mapping)否则新字段不会进索引,或者动态 mapping 猜错类型(比如把日期猜成 text)。
_id 的设计决定能否正确更新删除单表 → id 分表 → concat('order_', id) 多表 join → concat(a.id, '_', b.id)
没有正确的 _id,UPDATE 会变成新增,DELETE 完全不生效,最终 ES 里全是重复数据。
一个自查方法:改一条数据,看 ES 里的文档数是不变(正确)还是**+1**(错误)。
sqlselect a.id, a.title, b.name as category_name
from article a left join category b on b.id = a.category_id
这条 SQL 能查出 category_name,但只有 article 表变更才会触发同步。
改了 category.name,ES 里的文章文档不会更新。
两种处理方式:
category 单独配一条映射,同步到独立索引吞吐量:
yamlcanal.conf:
syncBatchSize: 1000 # adapter 每批处理条数
esMapping:
commitBatch: 1000 # 每批提交 ES 的条数
调大能提升吞吐,但会增加单批延迟和内存占用。
ES 侧:
number_of_replicas 临时设为 0,导完再改回来refresh_interval 在批量导入期间设为 -1number_of_shards 按数据量规划,建好后不能改(只能 reindex)整套东西跑通后回头看,配置量其实不大——两个 properties、一个 yml、一个映射文件,加起来不到 50 行。
真正的成本全在版本兼容上。
Canal 1.1.7 是 2022 年的产物,MySQL 8.4 是 2024 年的。中间隔了两年,三处破坏性变更全撞上了:
SHOW MASTER STATUS 被移除mysql_native_password 默认关闭如果你也在做类似的"老版本中间件 + 新版本数据库"的组合,几个建议:
Caused by 里往往才是真相Binlog Dump 线程、Subscribe succeed 日志、_count 结果),别靠"看起来对了"这套东西现在跑在一台 CentOS 7 上,内存占用 1.5G 左右(两个 JVM 各 512m~1g),支持全量 + 秒级增量同步。
如果哪天业务上来了要同步正式表,只需要加一个映射文件 + 建一个索引 + POST 一次 ETL,不用动任何服务。
按顺序检查,任何一步不过就别往下走:
MySQL 侧
SELECT @@log_bin, @@binlog_format, @@binlog_row_image; → 1 / ROW / FULLSELECT user, host, plugin FROM mysql.user WHERE user='canal'; → 有 REPLICATION SLAVE 权限SHOW BINARY LOG STATUS;(8.4)或 SHOW MASTER STATUS;(8.0)能返回位点canal-server 侧
systemctl is-active canal-server → activestart successful 和 find start position successfullySHOW PROCESSLIST; 有 Binlog Dump 线程canal-adapter 侧
systemctl is-active canal-adapter → activeLoad canal adapter: es7 succeedSubscribe destination: example succeedES 侧
GET /<index>/_mappingPOST /etl/es7/xxx.yml 返回 "succeeded":trueGET /<index>/_count 数量与 MySQL count(*) 一致_id 配置正确)开机自启
systemctl is-enabled canal-server canal-adapter 都是 enabled

本文作者:章鱼哥
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!