很多团队接触 Hudi,第一关就卡在表类型上。建表时会遇见 COPY_ON_WRITE 和 MERGE_ON_READ 两个选项,官方文档讲清楚了它们的基本差异,但真正到了生产环境,问题通常会变成一句很实际的话:我手头的写入压力、查询压力和更新频率摆在面前,选哪个才不会在半年后头疼。Hudi 的 MOR 与 COW 两种写入模式,本质是在写入延迟、写入吞吐和查询性能三者之间做置换,理解不了这个三角权衡,后面调参数也容易被动。

先搞清楚 COW 和 MOR 到底在更新数据时发生了什么
COW 的名字很直白,copy on write。更新一条记录时,Hudi 会把这条记录所在的基础数据文件读出来,应用变更后再把整个文件重新写回。写入过程中会产生新的文件版本,旧版本由后续的清理机制删除。也就是说,一次更新操作哪怕只改了文件里的几行,也要等完整的读改写流程结束,提交才会完成。
MOR 则换了一个思路。更新和删除不直接碰基础 Parquet 文件,而是先把变更追加到对应文件组的日志块里,也就是 Avro 格式的 delta log。读取的时候再把基础文件和这些日志合并,得到最新数据。因为写入侧变成了纯追加,延迟和吞吐都变得很舒服,但代价并没有消失,只是被搬到了读取侧和后台压缩任务里。
还有一个容易被忽略的机制是 file group。Hudi 会把同一主键范围的数据组织进一个文件组,无论是 COW 还是 MOR。COW 更新时,文件组里受影响的基础文件会被替换成新版本;MOR 更新时,变更会追加到文件组对应的日志文件中。这个差异决定了两种模式对提交频率的容忍度:COW 一次提交要做若干文件的原子替换,MOR 一次提交只是落盘少量日志。提交频率越高、单次变更越碎,两者的吞吐差距就越明显。
理解到这一层,就能看出来这其实是数据系统里常见的写换读、时间换空间的搬移。Hudi 把它做成了一个显式的表结构选择,代价也因此变得可见、可控。
三角权衡:为什么快写和快读很难同时成立
最核心的原因很简单:列式文件没法做真正意义上的原地更新。Parquet 是批量编码的存储格式,单行改动无法直接落盘。所以每一次更新,本质上都逃不开“读旧文件、生成新文件”这个过程。COW 选择在写入时完成这个过程,MOR 选择把它延后到读取和压缩时完成。于是就有了下面这组对应关系:
COW 的延迟高,但换来的是干净稳定的文件布局和读取路径;MOR 的吞吐高,但它的查询性能有一个前提——delta log 不能无限制增长。
写入放大是 COW 最直接的代价
假设一张订单表里单个 Parquet 文件有 200MB,某次 upsert 只命中这个文件里的 10 条记录,COW 依然会把整个文件重新编码写一遍。当更新频繁且散落在大量文件里时,一次提交要对几十个文件做读改写,写延迟自然下不来。不少团队把更新占比高的表从 COW 迁到 MOR 后,写入链路立刻稳定下来,原因就是写入端不再关心文件内部的记录分布。
但这里有一个常见误区:如果写入以 INSERT 为主,COW 并不存在那么严重的放大问题。新增数据直接生成新文件,没有读改写环节,COW 和 MOR 的写入性能差距并不大。只有更新占比高的负载才会放大 COW 的劣势,没必要在任何场景下都换 MOR。
读放大是 MOR 的隐性成本
MOR 的读放大体现在每次查询都要做基础文件与日志的合并。合并过程对查询引擎透明,但成本并不隐形。日志文件越多、跨的文件组越广,合并开销越大。极端情况下,一个文件组积累了几十个日志块,点查可能从毫秒级变成秒级。更麻烦的是,这种慢查询很难靠堆并发解决,因为瓶颈在单个文件组内部的拼接计算。
所以说,MOR 本质上是用查询稳定性换写入稳定性,这个交换划不划算,完全取决于你的业务更在意哪一侧。
MOR 的一个关键细节:查询与日志深度强绑定
如果你翻过 Hudi 早期版本的文档,会发现 MOR 表其实支持两种查询视角:读优化查询只读基础文件,不包含最新日志变更;实时查询要合并日志,返回最新数据。后来这种双轨设计在版本演进中逐渐收敛,但历史设计说明了一件事——MOR 在诞生之初就预设了“读取侧可以牺牲新鲜度来换性能”这个选项。
这个背景在实际选型里很有用。比如上游经常对近几天的数据进行修正,而对下游报表来说这些修正不需要秒级可见,那么 MOR 配合合理的压缩调度,就可以同时承受高频改动和相对可控的查询成本。反过来,如果一张表要被实时分析平台高频查询,日志深度控制不住的话,MOR 的查询侧会先扛不住。
生产场景里的典型样本
先看一个 CDC 入湖的场景。某个团队做数据库变更同步,每秒几百到上千条变更,更新占比很高。他们一开始用 COW,提交延迟越拖越长,因为每批变更都在反复重写基础文件。切成 MOR 之后写入立刻顺畅,但紧接着巡检用的 SQL 开始变慢,点查和聚合都出现偶发长尾。最后他们不得不引入定时压缩,把日志深度控制在若干次提交以内,查询端才恢复可接受的水平。
再看一个离线数仓回刷的场景。另一个团队每天写入几千万条新增数据,偶尔对历史分区做一次全量修正。这种负载对 COW 非常友好,历史修正虽然慢一点,但一天只跑一两次完全能接受。如果他们换成 MOR,不但要额外维护压缩调度,查询侧还要白白承担日志合并的成本。COW 在这里反而是更优解。
还有一个在 Flink 入湖场景里很典型的现象。流式任务通常保持较小的批间隔,追求秒级到分钟级的时效。如果表是 COW,每次 checkpoint 都要评估哪些基础文件被命中,更新密集时 checkpoint 时长会明显上涨,进而拖累整个作业的吞吐。MOR 在这种负载下的优势几乎是决定性的,这也是很多实时入湖流水线默认选 MOR 的原因之一。
综合这几个样本,常见的错误判断大致有三类:
- 以写入吞吐作为唯一标准,认为 MOR 全面优于 COW。实际上插入占比高的负载下两者差距很小,COW 的查询稳定性反而更有价值。
- 忽略读取侧的合并成本,上线后才发现慢查询。MOR 的写入优势是显性的,读取劣势是隐性的,通常要等日志堆起来才暴露。
- 把 Compaction 当作可以无限延后的后台任务。压缩确实可以异步,但不调度、不监控,就有日志无界增长和任务堆积双重风险。
COW 与 MOR 的多维度对比
| 对比维度 | COW | MOR |
|---|---|---|
| 更新写入方式 | 读改写整个基础文件 | 追加 Avro delta log |
| 写入放大 | 高 | 低 |
| 读放大 | 低 | 高,取决于日志深度 |
| 写入延迟 | 偏高 | 低 |
| 查询稳定性 | 稳定 | 受日志长度影响明显 |
| 文件数管理 | 较好,关注小文件问题 | 依赖压缩收敛日志 |
| 典型负载 | INSERT 为主、低频更新 | 高频 UPSERT、DELETE |
| 运维复杂度 | 低 | 中高,重点是压缩调度 |
落地时的配置与避坑建议
如果决定在某张表上用 MOR,Hudi 的配置本身并不复杂。以 Spark 批式写入为例,核心就是表类型和压缩策略两个位置:
df.write.format("hudi")
.option("hoodie.table.name", "dwd_trade_match")
.option("hoodie.table.type", "MERGE_ON_READ")
.option("hoodie.datasource.write.operation", "upsert")
.option("hoodie.compact.inline", "true")
.option("hoodie.compact.inline.max.delta.commits", "5")
.mode("append")
.save("s3://warehouse/dwd_trade_match")
这里把压缩设置为每 5 次提交触发一次,目的是把日志深度限制在可控范围。max.delta.commits 设得越小,查询侧越稳定,但压缩频率越高,对资源的占用也越明显。这个值没有通用最优解,必须结合提交频率和单次提交的数据规模来调。
COW 表的配置重点则完全不同。它更关心小文件治理,比如 hoodie.parquet.small.file.limit、hoodie.copyonwrite.record.size.estimate 这类参数。COW 的每次更新都会产生新版本文件,如果分区更新频率高而数据量小,文件数会快速膨胀,查询性能同样会垮。听上去 COW 很省心,小文件问题才是它真正的运维暗面。
经验提醒:MOR 表如果查询压力大,优先保证压缩调度的稳定性,而不是在查询引擎侧硬调参数。压缩不及时造成的日志堆积,是 MOR 慢查询最常见的根因。
落到具体行动上,建议按下面几步走:
- 先量化现状:观察提交时长、单次提交重写的文件数量、目标查询的 P95 延迟,再决定是否动表类型。
- 不要全表迁移,挑一张更新频率最高的真实表灰度,切换前后各记录一段时间写入吞吐和查询延迟。
- 更新频繁但很少被查询的中间过程表,选 MOR 收益最大;被实时报表高频查询的宽表,则要谨慎评估日志深度和压缩窗口。
- 把压缩调度写进监控体系,观察压缩延迟和积压数量,而不是上线后就不再过问。
Hudi 的 COW 和 MOR 没有哪一个是默认答案。它们只是把“更新数据”这件事的成本,放在了时间轴的不同位置。COW 把代价提前到写入那一刻,用写入延迟换查询干净;MOR 把代价延后到读取和压缩那一刻,用运维复杂度换写入吞吐。选型的关键,是想清楚自己的更新频率、查询压力,以及团队愿意承担多少压缩调度的工作。把这三件事量化了再建表,大概率不会走弯路。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/942/