DataX 同步 StarRocks VARCHAR 超长导致数据丢失
才疏学浅,若有不对之处烦请指正
前言
最近线上一个 DataX 同步任务翻车了——从 PostgreSQL 往 StarRocks 搬数据,8116 条记录里有 6 条莫名其妙地“消失”了。报错信息写的是 NULL value in non-nullable column,但源库里这些字段明明有值啊?
排查了一圈才发现,两个数据库对 VARCHAR(255) 的理解压根不是一回事。一个按字符算,一个按字节算,中文一进来直接超长,数据就这么无声无息地丢了。
这种 Bug 特别阴险——大部分数据正常,只有极少数行出问题,任务还显示成功。要不是这次刚好没配 max_filter_ratio 导致任务直接失败,可能到现在都没人发现数据少了。
参考链接
1. 现场还原
线上有一个数据接入任务,通过 Airflow 调度 DataX 将 PostgreSQL 的某张主数据表全量同步到 StarRocks。
某天任务突然失败,Airflow 日志报错:
1 | java.io.IOException: Failed to flush data to StarRocks. |
- 报错发生在 StarRocks Stream Load 写入阶段,不是 DataX reader 读取阶段
- 报错的列(
name、short_name、biz_scope)在 StarRocks 目标表中定义为NOT NULL - 源库 PostgreSQL 中这些列 确实有值,不是 NULL
- 只有少量行(6 条)受影响,其余 8110 条正常写入
嗯……源库有值,目标库说是 NULL,这就很离谱了。
2. 排查过程
2.1 第一反应:源数据是 NULL?
看到 NULL value in non-nullable column,第一反应肯定是——源库数据就是 NULL 呗。
直接查源库:
1 | SELECT id, name, length(name) as name_len |
结果:三条数据的 name 都有值,长度为 363 个字符,内容是一段包含中文、特殊字符的长字符串。
源数据不是 NULL,问题不在源端。
2.2 第二反应:分隔符冲突?
name 的值包含大量特殊字符(<>?:"{}|+_)(*&^%$#@!~ 等),而 StarRocks Stream Load 使用的是 CSV 模式,配置了自定义分隔符:
1 | "loadProps": { |
怀疑 name 值中包含 \x01 或 \x02,导致 Stream Load 解析时列错位。
验证方式:将 loadProps 改为 JSON 格式,彻底避免分隔符问题:
1 | "loadProps": { |
结果:换成 JSON 格式后,报错完全一样。同样的行、同样的列、同样的 NULL 错误。
不是分隔符冲突的问题。CSV 和 JSON 两种格式都报同样的错,说明问题在更上游。
2.3 第三反应:DataX reader 读出来就是 NULL?
既然源库有值,Stream Load 收到的却是 NULL,那是不是 DataX 的 PostgreSQL reader 在读取时把值丢了?
看一眼 DataX reader 的处理逻辑
1 | // 伪代码:DataX reader 读取 VARCHAR 字段 |
resultSet.getString() 对于正常的 VARCHAR 数据不会返回 NULL(除非源数据本身就是 NULL)。而且 DataX 的统计信息显示 读出记录总数 8116,读写失败总数 0,说明 reader 端读取完全正常。
DataX reader 正常读到了所有数据,包括那 6 条”问题行”。数据在 reader 端没有丢失。
2.4 关键转折:对比源表和目标表的列定义
前面三个方向都排除了,回到最基本的问题:StarRocks 说收到了 NULL,但数据确实发过去了,那只有一种可能——StarRocks 在写入时主动丢弃了这个值。
什么情况下 StarRocks 会把一个非 NULL 的值变成 NULL?
查 StarRocks 目标表的列定义:
1 | SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE |
| 列名 | 类型 | 最大长度 | 允许 NULL |
|---|---|---|---|
| name | VARCHAR | 255 | NO |
| short_name | VARCHAR | 255 | NO |
再看源数据的实际长度:
| id | name 长度(字符数) |
|---|---|
| 10001 | 363 |
| 10002 | 363 |
| 10003 | 363 |
源数据 name 有 363 个字符,StarRocks 的 name 列定义为 VARCHAR(255)。363 > 255,超长了。
等等,两边不都是 VARCHAR(255) 吗?PostgreSQL 能存 363 个字符,StarRocks 怎么就存不下了?
3. 根因分析
VARCHAR(n) 的语义差异
StarRocks 目标表是根据 PostgreSQL 源表的 DDL 自动生成的,两边都是 VARCHAR(255)。但问题在于:
PostgreSQL 和 StarRocks 对 VARCHAR(n) 中 n 的定义不同:
- PostgreSQL:n 表示 字符数,一个中文算 1 个字符
- StarRocks:n 表示 字节数,UTF-8 下一个中文占 3 个字节
所以同样是 VARCHAR(255):
| 数据库 | 最多存储中文字符数 | 最多存储字节数 |
|---|---|---|
| PostgreSQL | 255 个 | ~765 字节 |
| StarRocks | ~85 个 | 255 字节 |
差了整整 3 倍,这还怎么玩。
那源库里怎么会有 363 个字符的数据?
PostgreSQL 对 VARCHAR(n) 的长度校验是严格的,正常情况下不允许插入超过 n 个字符的数据。源表中出现 363 字符的历史数据,可能的原因:
- 该列曾通过
ALTER TABLE缩短过长度,但旧数据未清理 - 源列实际类型是
TEXT,在自动建表逻辑中被误映射为VARCHAR(255)
无论哪种情况,这些数据同步到 StarRocks 时,UTF-8 编码后远超 255 字节,StarRocks 不会截断,而是直接将值置为 NULL,再加上 NOT NULL 约束,就报错了。
同样的问题也影响了 short_name 和 biz_scope 列。
4. 问题代码
后端项目中,根据源表 DDL 自动生成 StarRocks 建表语句的逻辑,直接将源库的 VARCHAR(n) 原样映射为 SR 的 VARCHAR(n),没有考虑两个数据库对长度单位的差异。
1 | // 伪代码:自动建表的类型映射逻辑 |
这个问题不仅影响 PostgreSQL,MySQL 的 VARCHAR(n) 同样是字符数语义(MySQL 5.0+ 之后),直接映射到 StarRocks 也会有同样的问题。
5. 修复方案
修改自动建表的类型映射逻辑
源库(PG / MySQL)的 VARCHAR(n) 映射到 StarRocks 时,长度应乘以 3,即 VARCHAR(n) → VARCHAR(n * 3)。
n * 3 覆盖了绝大多数中文和拉丁字符。但 Emoji 和部分生僻字在 UTF-8 下占 4 字节,如果业务数据中 Emoji 较多,建议改为 n * 4,或对长文本列直接使用 STRING。
1 | // 伪代码:修复后的类型映射逻辑 |
完整映射规则:
| 源库类型 | 条件 | StarRocks 类型 |
|---|---|---|
| VARCHAR(n) | n * 3 ≤ 1048576 | VARCHAR(n * 3) |
| VARCHAR(n) | n * 3 > 1048576 | STRING |
| CHAR(n) | n * 3 ≤ 1048576 | VARCHAR(n * 3) |
| CHAR(n) | n * 3 > 1048576 | STRING |
| TEXT | — | STRING |
| MEDIUMTEXT(MySQL) | — | STRING |
| LONGTEXT(MySQL) | — | STRING |
| TINYTEXT(MySQL) | — | STRING |
1048576(1 MB)是 StarRocks 2.1+ VARCHAR 的最大字节长度限制。STRING 类型等价于 VARCHAR(65533)。
6. 为什么这个 Bug 特别隐蔽?
这个 Bug 有几个特点让它很难被发现,聊一聊:
6.1 大部分数据正常
8116 条数据中只有 6 条超长,占比不到 0.1%。如果 DataX 配置了 errorLimit 或 max_filter_ratio,这些行会被 静默丢弃,任务显示成功,但数据已经丢了。
max_filter_ratio 在 DataX starrockswriter 中默认值为 0,即不容忍任何错误行。只有手动设为非 0 值(如 0.001)时,才会出现”任务成功但数据丢失”的现象。
6.2 DataX 统计信息会误导
当配置了 max_filter_ratio 时,DataX 的任务统计会显示:
1 | 读出记录总数: 8116 |
看起来完全成功,但实际上 StarRocks 只写入了 8110 条。DataX 认为自己成功了,因为 Stream Load 在 max_filter_ratio 范围内返回了成功。
6.3 报错信息有误导性
NULL value in non-nullable column 'name' 这个错误信息会让人以为源数据是 NULL,或者 DataX 传输过程中丢了数据。实际上数据完整传过去了,是 StarRocks 在写入时因为超长主动丢弃了值。
6.4 源表定义和目标表定义”看起来一样”
两边都是 VARCHAR(255),表面上完全一致,但语义不同(字符 vs 字节)。如果不了解这个差异,很难想到是长度问题。
7. 总结
根因链
1 | 源库 VARCHAR(n) 的 n = 字符数 |
经验教训
- 跨库同步 VARCHAR 必须换算长度。PG/MySQL 的 n 是字符数,StarRocks 的 n 是字节数,建议映射时统一乘以 3。
- StarRocks 超长不截断,直接置 NULL。这个行为比报错更危险,因为配了
max_filter_ratio后数据会静默丢失,DataX 还显示”读写失败总数: 0”。生产环境max_filter_ratio建议设为 0。
VARCHAR(n) 的 n 不是通用概念,字符数 ≠ 字节数。


