先说结论
COW:Copy-on-Write这是一种较为普遍和通用的存储优化策略在Linux和中都有使用,也叫写时拷贝
- 写入时合并,重写整个数据文件,必定存在写入吞吐量低、写入延迟高,写入效率低
- 读取时,直接读最新的全量数据,读取性能高
MOR:Merge-on-Read,读时合并
- 写入时,追加增量数据,不合并,写入效率低
- 读取时,需要合并数据,读取性能低
MOW:Merge-on-Write,Doris2.0后推出的功能
- 写入时,会判断是否有该key,若有则先delete记录到Delete Bitmap再insert 实现合并的效果,写入效率高
- 读取时,先读取Delete Bitmap,将被标记删除的行过滤掉,只返回有效数据,读取性能高
- 高度依赖主键,需要额外存储Delete Bitmap
一.COW
1.先说定义
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
举个例子 :当一条
UPDATE
或DELETE
操作到来时,Flink 作业(如 Flink CDC 或使用 Hudi CoW 表的作业)会:
- 定位到这条记录所在的原始数据文件(parquet文件) 。
- 读取整个原始文件到内存。
- 在内存中合并更改:应用更新或删除标记。
- 将整个合并后的新数据写回一个新的文件。
- 更新元数据,将查询指向新文件,并异步删除旧文件。
2.适用场景
- 读多写少的维表,对查询性能和延迟有极致要求的场景
- 对写入要求不高
3.优缺点
优点
- 极佳的查询性能:读取路径简单高效,直接读取合并好的列式文件。
- 数据立即可见:数据一旦写入就是最终状态,没有状态延迟。
- 简化数据模型:对查询引擎透明,任何支持 Parquet/ORC 的引擎都可以直接高效查询。
缺点
- 写入放大 (Write Amplification) :重写整个文件导致极高的 I/O 成本和写入延迟。
- 写入吞吐量低:不适合高频更新的场景。
- 存储成本较高:在旧文件被清理前,同一数据会有多个副本存储。
二.MOR
1.先说定义
举个例子 :当一条
UPDATE
或DELETE
操作到来时,Flink 作业会:
- 将这条变更记录(通常是完整的行) 直接以行格式(如Avro) 快速追加写入到一个专门的增量日志文件中。
- 原有的基础文件(Base File,Parquet格式)保持不变。
- 表的元数据中会记录哪些增量日志文件对应哪个基础文件。
- 当有查询到来时:
- 读优化查询(Read Optimized) :仅读取基础文件(Parquet),查询不到最新的更新,只能看到上次Compaction前的状态。
- 快照查询(Snapshot Query) :查询引擎需要同时读取基础文件(Parquet)和增量日志文件(Avro) ,然后在内存中进行合并,得到最新状态的数据。这个过程有计算开销。
2.适用场景
- 写多读少的事实表,低延迟写入,高吞吐写入
- 允许查询有一定延迟
3.优缺点
优点
- 极高的写入吞吐量和低延迟:写入是顺序追加,速度极快。
- 支持真正的流式更新:非常适合处理无界的实时数据流。
缺点
- 查询性能开销大且不稳定:快照查询需要在读取时合并数据,消耗大量 CPU 和内存,延迟高。
- 架构复杂 :必须引入并精心调优后台压缩 (Compaction) 作业。如果压缩跟不上写入,日志会膨胀,查询性能会持续恶化。
- 数据延迟性:"读优化查询"看到的数据不是最新的,需要合并。
三.MOW---Doris2.0后推出
1.先说定义
基本工作原理:MoW的核心思想是在数据写入时就完成新旧数据的合并操作,而非等到查询时才进行合并,从而显著提升查询性能。
- 对于每一条待写入的数据,系统会通过主键索引查找该键在基础数据(Base Data)中的位置
- 如果该键已存在,则将原数据标记为删除(记录在Delete Bitmap中)
- 将新数据写入新的Rowset中,使新数据立即可见
- 查询时只需过滤掉被标记删除的行,即可获取最新数据
Doris的MOW
(Merge-on-Write)模式配合Delete Bitmap + Primary Index
技术
2.Delete + Insert机制
Doris的MoW实现参考了微软SQL Server在2015年VLDB上提出的方案,采用"Delete + Insert"的方式处理更新:
- 主键查找:对于每条待写入的Key,通过主键索引查找其在Base数据中的位置(rowsetid + segmentid + 行号)
- 标记删除:如果Key存在,则将该行数据标记删除,删除信息记录在Delete Bitmap中(每个Segment对应一个Delete Bitmap)
- 写入新数据:将更新的数据写入新的Rowset中,完成事务使新数据可见
- 查询过滤:查询时读取Delete Bitmap,将被标记删除的行过滤掉,只返回有效数据
这种设计的优势在于:
- 任何有效的主键只存在于一个地方(要么在Base Data中,要么在Delta Store中)
- 避免了查询时的大量归并排序消耗
- Base数据中的各种列存索引仍然有效
3.适用场景
- 需要高性能upsert、高吞吐写入、低延迟查询
- 高度依赖主键,对存储空间无极致要求
4.优缺点
优点
- 优秀的读写平衡:既保证了接近 MOR 的高写入吞吐,又提供了接近 COW 的低延迟查询。
- 高效的 Upsert:通过"Delete + Insert"机制,完美支持主键更新。
- 避免读时合并:查询引擎无需进行昂贵的合并计算,直接读取即可。
- 索引有效:Base 数据中的前缀索引、Bloom Filter 等仍然有效,因为数据本身没有改变,只是被标记了。
缺点
- 依赖主键索引:性能和效率高度依赖于主键索引的设计和查找速度。如果主键是随机分布且数据量极大,索引查找可能成为瓶颈。
- 存储放大:虽然避免了文件重写,但需要额外的存储空间来维护 Delete Bitmap。
- Compaction 依然需要