20260107.SeaTunnel MySQL-CDC 分片列不生效:明明主键是 id,却总用 seq 分片——一次从日志追到源码的排错复盘

19 阅读5分钟

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, min1max814, chunk size5000 ...
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, min6707..., max1523..., chunk size5000 ...
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; }

本文使用 文章同步助手 同步