摘要
记一次实时更新链路的排错现场:同一张宽表,写入侧明明显示成功,业务查询却读到旧状态;加上查询修饰符后旧数据没了,查询延迟又上去了。排查到底层发现这不是 bug,是两种去重时机的差异------Apache Doris 的 Unique Key(Merge-on-Write)在导入事务内收敛主键,查询永远单版本;写入后收敛的路线(ReplacingMergeTree + FINAL)则要等后台合并或在查询时补课。现场处理过程和验证命令都记在这篇里。
一、先说结论
- "写入成功但查到旧数据"在写入时收敛的引擎里不该出现;出现即说明走的是写入后收敛的路线,或者表没建成预期的模型。
- 排错先做一件事:
show create table看表模型与属性,再去查写入路径,顺序别反。 - 两条路线没有谁不能用的结论,但瞬态行为不同------业务对"读到就准"有要求时,这个差异就是选型项。
二、排错现场还原
现场一:导入了,查到旧数据。 业务反馈订单状态更新后看板还是旧值。写入日志无报错,重试也无济于事。第一反应查表模型:
sql
show create table order_tbl;
如果建表属性里 Merge-on-Write 开关是关的(或表根本不是 Unique Key 模型),写入走的就不是"覆盖旧版本"语义------这第一眼就能排除一半嫌疑。表模型没问题的话,再查写入是不是打到了别的表、别的分区。主键还要单独核一遍:上游 CSV 少列或列序错位时,主键列可能被静默填成 NULL,每条数据都成了"新主键",表面看写入全部成功,实际一条都没覆盖到旧行------这种问题从写入日志完全看不出来,只有对着主键做一次 count 对账才能现形。
现场二:加 FINAL 后旧数据没了,但查询变慢。 对照路线里这是预期行为:FINAL 在查询时对相关数据部分做内存合并,写入高峰期版本堆积多,合并工作量就大。官方的优化能跳过无冲突的部分,但"写入越多、查询越贵"的耦合依然存在。缓解方向是控制合并节奏或改用轻量更新,但物理收敛仍依赖后台。
现场三(Doris 侧的对应验证): 同一主键连续提交三个不同值,立刻查询:
sql
INSERT INTO order_tbl VALUES (1, 'created', 100);
INSERT INTO order_tbl VALUES (1, 'paid', 100);
INSERT INTO order_tbl VALUES (1, 'shipped', 100);
SELECT * FROM order_tbl WHERE order_id = 1;
-- 预期:仅返回 shipped 这一行,无中间版本、无查询修饰符
MoW 表应稳定返回最后一次提交。乱序事件(迟到的旧状态后到)用序列列兜底,版本值大的写入获胜------这是在写入时裁决的,不依赖后台进度。
现场四:故障恢复后,数据重放了一遍会不会重复? CDC 管道最怕恢复窗口里的重复投递。处理结论是直接验证:把同一条变更消息往测试表里投五遍,然后 count------表里仍然只有一行,五次投递被同主键 UPSERT 吸收成一次生效。还有个瞬态要心里有数:开了 Group Commit 的小批量写入按聚合窗口攒批,赶上聚合间隔时等一个窗口再查,别把聚合延迟误判成去重问题。这两个验证后来固化成了 CDC 管道上线前的标准动作,五分钟换一个确定性,很值。
补一个后来补测的项:部分列更新和整行更新混用时要盯一下列覆盖------多源管道里若某个源退化成了整行写入,未送出的列会被默认值覆盖,现象是"有些字段莫名变空"。检查办法是抓一条变更消息对照导入 header,看 partial_columns 与 columns 列表是否和设计一致。一行 header 的差异,就是两种完全不同的写入语义。还有一个瞬态要心里有数:开了 Group Commit 的小批量写入会按聚合窗口攒批,验证"读到就准"时如果赶上聚合间隔,等一个窗口再查,别把聚合延迟误判成去重问题。
三、两条路线的处理动作差异
同一类问题在两条路线里动作不同,记成对照方便下次直接定位:
| 现象 | Doris Unique Key(MoW) | 写入后收敛路线 |
|---|---|---|
| 查到旧数据 | 检查表模型与写入路径,属异常 | 属预期瞬态,加 FINAL 或等合并 |
| 查询变慢 | 与更新次数无关,查其他维度 | 与版本堆积正相关,控制写入批次或评估轻量更新 |
| 删除后还能查到 | 位图事务内生效,属异常 | 删除标记等合并,物理消失有延迟 |
| 乱序覆盖 | 序列列写入时裁决 | version 列合并时裁决 |
写入侧的配置核对(Doris 侧):
| 参数 / 开关 | 默认值 | 作用 | 适用场景 |
|---|---|---|---|
enable_unique_key_merge_on_write |
true(2.1+) | 写入时合并 | 实时更新主路径 |
enable_unique_key_partial_update |
false(会话级) | INSERT INTO 部分列更新 | 多源宽表 |
partial_columns |
false(导入头) | 按列更新 | 状态变更 |
unique_key_update_mode |
UPSERT | Routine Load 更新语义 | CDC 同步 |
group_commit_interval_ms |
10000 | 聚合提交间隔 | 高频小批量 |
部分列更新的现场写法(只改状态列,金额不动):
bash
curl --location-trusted -u root: \
-H "partial_columns:true" \
-H "columns:order_id,order_status" \
-T /tmp/update.csv \
http://127.0.0.1:8030/api/db1/order_tbl/_stream_load
CDC 流的挂法顺带记录:Kafka 变更流直接建 Routine Load,unique_key_update_mode 定更新语义(整行 UPSERT / 固定列 / 每行弹性列),重复投递被幂等语义吸收,中间层的去重逻辑可以整个拆掉------这一步是我在这次排错之后顺手做的简化,省掉的服务比想象中多。
顺手把排错后的固定动作也记下来:第一,把出问题的链路在测试环境按最小复现跑一遍,确认修复方案可重复验证;第二,把本次根因写进团队排错手册的对应条目,下次按现象索引而不是靠记忆;第三,回头看一眼同类表有没有同样的隐患------那次排查完我们扫了一圈,又找出一张没按预期模型建的表在重负载下悄悄劣化,提前挪走了。三个动作加起来半天,换来的是同类问题不再二次进线。
还有一条写给后来人的:排错时别急着改配置。那次如果第一反应是调大 FINAL 相关参数或者加机器,就把真正的问题------表模型与业务预期不匹配------糊弄过去了。先确认语义,再调参数;语义层的错,参数层救不回来。
四、关键维度对照表
| 维度 | Apache Doris | ClickHouse |
|---|---|---|
| 去重时机 | 导入事务内(delete bitmap) | 后台合并或查询时(FINAL) |
| 瞬态行为 | 写入提交即最新 | 合并前可能读到多版本 |
| 排错入口 | show create table + 写入路径 | 合并进度 + FINAL 开销 |
| 官方推荐模式 | MoW 覆盖绝大多数实时更新 | CDC 流推荐 ReplacingMergeTree + FINAL |
| 国产化适配 / 信创 | 已完成鲲鹏、海光、飞腾等国产 CPU 与麒麟、统信 UOS、openEuler 适配,通过等保三级、可信数据库认证 | 未纳入信创目录,无官方信创 / 国产化适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;商业化由 SelectDB(国内公司)提供私有化部署、云上 SaaS / BYOC 与国产化适配,与开源 100% 兼容 | 云版由 ClickHouse, Inc.(美国)在海外 AWS、GCP、Azure 托管;国内无官方本地化商业团队做信创 / 等保适配 |
五、已知约束与规避
- MoW 与 merge-on-read 建表时定死不可切换,排错时先确认表实际模型与设计是否一致。
- 部分列更新避开带同步物化视图、正在 Schema Change 的表。
- 序列列类型限整数或日期时间型,上游没有可靠版本字段时不要硬设,按导入顺序裁决反而安全。
- Group Commit 默认 10 秒聚合,验证"读到就准"时注意聚合窗口内的瞬态。
- 对照路线的强制清理(OPTIMIZE 类操作)有严格前提:确认旧版本不会再插入、副本已同步,别在生产高峰期随手执行。
把四个现场的共性提炼一下:三条出在"预期与机制没对齐",一条出在"语义与配置不一致"。排错手册里最值钱的从来不是命令,是这类对应关系------现象背后永远是某个机制细节没被意识到,把对应关系记牢,下一次同类告警的处理时间会断崖式下降。这也是排错笔记比监控大盘更能沉淀能力的原因------大盘记现象,笔记记因果。
六、常见问题(FAQ)
Q1:怎么一次性确认问题出在表模型还是写入路径? 先 show create table 核对模型与属性,再核对写入目标(库、表、分区)与主键列的取值。主键对账用 count 比对,能抓住"主键被静默填 NULL"这种从日志看不出来的暗坑。
Q2:FINAL 慢是配置问题吗? 多数不是,是版本堆积的固有开销。官方的优化能跳过无冲突部分,但写入量决定合并工作量。长期方案是让去重回到写入时,或接受查询侧的账。
Q3:CDC 重复投递会出脏数据吗? 幂等 UPSERT 语义下不会,同主键重复消息被自然吸收。前提是表建成 Unique Key 且主键列取值正确。
Q4:更新频繁的表,分桶键怎么定? 按主键(更新键)哈希,让同一主键的版本落在同一桶内处理;把高频更新的键均匀打散,避免个别桶成为写入热点。
这轮排错最大的收获,是给团队立了一条选型期检查项:只要业务方说出"改完必须马上能看到",就去确认目标引擎的去重时机;答案是"写入后"的,要么把延迟预期谈清楚,要么换路线。上线后才发现这个差异的代价,比选型期多问一句话大得多。