20260107.SeaTunnel MySQL-CDC 分片列不生效:明明主键是 id,却总用 seq 分片——一次从日志追到源码的排错复盘
背景
我在做 MySQL → Doris 的 CDC 同步,使用的是 Apache SeaTunnel 2.3.11 + connector-cdc-mysql-2.3.11.jar,模式为 STREAMING + startup.mode=initial(先快照后增量)。目标 Doris 用 UNIQUE KEY + 2PC,保证 Exactly-once。
问题表:erp_prd.amzn_cleaned_report_inventory_ledger_detail_view_data,结构里明确 PRIMARY KEY(id),并且所有表主键都是 id。
我希望全量快照阶段按 id 分片(split),并通过 snapshot.split.size 控制每片行数,防止下游写入慢导致上游数据堆积(此前确实遇到 OOM)。
问题现象
配置里已经写了:
table-names-config 指定:
primaryKeys=["id"]
snapshotSplitColumn="id"
同时还写了一堆“看起来像能指定分片列”的参数(后来发现很多是无效的)
但启动日志仍然显示 SeaTunnel 选用 seq 做分片列:
Splitting table ... split column: seq, min: 1, max: 814, chunk size: 5000 ...
The distribution factor ... approximate row count 5063023
Use unevenly-sized chunks ...
更关键的是这条:
Config snapshotSplitColumn not unique key for table erp_prd.amzn_cleaned_report_inventory_ledger_detail_view_data
也就是说:我配置了 snapshotSplitColumn=id,但系统直接判定 “不是唯一键”,然后忽略了配置,继续走自动选择逻辑,最终选成了 seq。
先看表结构:为什么 “seq” 会被认为更“适合”?
问题表(节选):
PRIMARY KEY (id BIGINT)
UNIQUE KEY uni_str (str_value_sha256, seq, ledger_date, is_delete)
seq 是 INT,且处在一个 UNIQUE KEY 里。很多系统在自动选分片列时,会偏好数值型(尤其是 INT)并考虑“是否唯一/是否可范围切分”。
但我的诉求很明确:即使自动策略喜欢 seq,也应当尊重显式配置的 snapshotSplitColumn=id。
排查路线:从日志追到 ChunkSplitter
日志里出现的类名很明确:
AbstractJdbcSourceChunkSplitter
JdbcSourceChunkSplitter has split ...
这说明:快照分片逻辑最终落在了 CDC 基础模块 cdc-base 的 chunk splitter 上。
于是直接在源码里定位:
github.com/apache/seat…
org.apache.seatunnel.connectors.cdc.base.source.enumerator.splitter.AbstractJdbcSourceChunkSplitter#getSplitColumn()
根因:getSplitColumn 的逻辑把 “PRIMARY KEY” 排除在“配置合法性校验”之外
核心逻辑是:
先看用户配置的 snapshotSplitColumn
但它会先判断:配置列是否属于 dialect.getUniqueKeys() 返回的 unique keys
如果不在 unique keys 里,就直接 warn 并忽略配置
然后进入自动选择:
先遍历 PrimaryKey 列(id)
再遍历 UniqueKey 列(seq 等)
最终按类型优先级挑最“优”的一个:TINYINT > SMALLINT > INT > BIGINT > ...
这就解释了整个现象链:
我的 snapshotSplitColumn=id 被判定 “not unique key”
进入自动选择后:
id 是 BIGINT
seq 是 INT
类型优先级 INT(3) < BIGINT(4),于是最终选中 seq
即便主键是 id,最后仍然用 seq 分片。
以为“配置强制生效”,但实际上被静默忽略,只能从日志里看出来;并且再加上官方的mysqlcdc文档, 都不知道如何下手去调整(因为已经按照文档去设置了指定的分片键了)
修复方案:把 PRIMARY KEY 也视为合法的 snapshotSplitColumn
最小修复点在“校验配置列是否可用”的那段。
原逻辑只认 unique key;我新增一个判断:
如果该列属于 PrimaryKey,也视为合法(等价于 unique)
修复后的关键片段(伪代码概括):
isUniqueKey = inUniqueKeys(tableSc);
if (!isUniqueKey) {
isUniqueKey = inPrimaryKey(tableSc);
}
if (isUniqueKey && isEvenlySplitColumn(tableSc)) return tableSc;
修复后再次启动,日志变成:
Splitting table ... split column: id, min: 6707..., max: 1523..., chunk size: 5000 ...
Use sampling sharding ...
这说明 snapshotSplitColumn=id 终于被执行到了分片逻辑里。
顺便一提:这次也暴露了一个“配置误区”
我最开始为了“保证兼容各种引擎”,写了很多类似:
key-column
split.key
chunk-key-column
snapshot.split.column
debezium.snapshot.row.column
实际上,对于 SeaTunnel MySQL-CDC 2.3.11 的有效参数集合,应该以官方 connector 文档为准,多余字段不会报错但会被忽略,它们会显著提高排查难度。
建议在排障时:
尽量把配置“收敛到最小集”
只保留 connector 文档明确支持的 key
特别是像 snapshotSplitColumn 这种关键参数,必须通过日志确认是否生效
打包与替换:shaded 包的价值
SeaTunnel 的插件体系非常适合这种“修复 → 编译 → 替换 jar → 快速验证”的工作流。
我使用的构建命令:
mvn -pl seatunnel-connectors-v2/connector-cdc/connector-cdc-mysql \
-am -DskipTests -DskipUT=true -DskipIT=true -D"skip.spotless"=true package
构建输出(shaded jar):
.../seatunnel-connectors-v2/connector-cdc/connector-cdc-mysql/target/
connector-cdc-mysql-2.3.12-SNAPSHOT-shaded.jar
替换线上 SeaTunnel 的:
${SEATUNNEL_HOME}/connectors/connector-cdc-mysql-2.3.11.jar
然后重启任务即可验证。
shaded 的好处:
依赖被打进一个 jar,避免在服务器上额外处理依赖冲突
插件级替换,不影响 SeaTunnel engine 主体
回滚也很简单:换回原 jar
这次排错体现的 SeaTunnel 设计点
1.Connector-V2 + 插件可插拔
问题在 connector 内部,修复也只改 connector 并替换 jar,不需要动 engine。
2.CDC-base 抽象层
MySQL-CDC 分片逻辑集中在 CDC-base 的通用 chunk splitter,上层 connector 更薄。优点是复用强,缺点是如果 base 层逻辑有 bug,会影响多个 JDBC/CDC 连接器。
3.日志可观测性很关键
能快速定位到 AbstractJdbcSourceChunkSplitter,并直接看到 “split column: xxx / distribution factor / sampling sharding”等信息,极大降低排查成本。
最终建议
遇到分片不均/OOM,先看日志确认 split column 是谁
配置 table-names-config.snapshotSplitColumn 后,必须确认日志真的使用了该列
如果出现 Config snapshotSplitColumn not unique key:
优先检查:代码是否把 PRIMARY KEY 算进“可用 split column”
或者临时 workaround:给 id 加一个 UNIQUE KEY(成本更高,但无需改源码)
降低试错成本的技巧:
只跑单表、最小配置
snapshot.split.size 调小(例如 5000/2000)
需要时触发采样分片(sample-sharding.threshold 调小,inverse-sampling.rate 调小)
.preview-wrapper pre::before { position: absolute; top: 0; right: 0; color: #ccc; text-align: center; font-size: 0.8em; padding: 5px 10px 0; line-height: 15px; height: 15px; font-weight: 600; } .hljs.code__pre > .mac-sign { display: flex; } .code__pre { padding: 0 !important; } .hljs.code__pre code { display: -webkit-box; padding: 0.5em 1em 1em; overflow-x: auto; text-indent: 0; } h2 strong { color: inherit !important; }
本文使用 文章同步助手 同步