COW、MOR、MOW
COW
即copy on write,写时拷贝。在COW表中,只有数据文件/基本文件(.parquet), 没有增量文件(.log)
它是在数据写入的时候,复制一份原来的拷贝,在其基础上添加新数据,创建数据文件的新版本(新的FileSlice)。新版本文件包括旧版本文件的记录以及来自传入批次的记录(全量最新)。
正在读数据的请求,读取的是最近的完整副本,这类似Mysql 的MVCC的思想
流程示例:
设我们有 3 个文件组,其中包含如下数据文件。
我们进行一批新的写入,在索引后,我们发现这些记录与File group 1 和File group 2 匹配,然后有新的插入,我们将为其创建一个新的文件组(File group 4)。
MOR
即merge on read读时合并。在MOR表中,可能包含列式存储的基本文件(.parquet)和行存的增量日志文件(一定会有)(基于行的avro格式,.log.*)。
新插入的数据存储在delta log 中,定期再将delta log合并进行parquet数据文件。读取数据时,会将delta log跟老的数据文件做merge。
合并成本从写入端转移到读取端。因此在写入期间我们不会合并或创建较新的数据文件版本。标记/索引完成后,对于具有要更新记录的现有数据文件,Hudi 创建增量日志文件并适当命名它们,以便它们都属于一个文件组。
读取端将实时合并基本文件及其各自的增量日志文件。你可能会想到这种方式,每次的读取延迟都比较高(因为查询时进行合并),所 以 Hudi 使用压缩机制来将数据文件和日志文件合并在一起并创建更新版本的数据文件。(文件合并时产生parquet)
MOW
即merge on write写时合并。处理流程是:
- 对于每一条 Key,查找它在 Base 数据中的位置(rowsetid + segmentid + 行号)
- 如果 Key 存在,则将该行数据标记删除。标记删除的信息记录在 Delete Bitmap中,其中每个 Segment 都有一个对应的 Delete Bitmap
- 将更新的数据写入新的 Rowset 中,完成事务,让新数据可见(能够被查询到)
- 查询时,读取 Delete Bitmap,将被标记删除的行过滤掉,只返回有效的数据
- 查询流程:
当我们查询某⼀版本数据时, Doris 会从 LRU Cache Delete Bitmap 中查找该版本对应的缓存。
如果缓存不存在,再去 RowSet 中读取对应的 Bitmap。
使⽤ Delete Bitmap 对 RowSet 中的数据进⾏过滤,将结果返回。
引入了一个 LRU Cache,每个版本的 Bitmap 只需要做一次合并操作。
优缺点和应用场景
cow
- 优点:读取时,只读取对应分区的一个数据文件即可,较为高效;
- 缺点:数据写入的时候,需要复制一个先前的副本再在其基础上生成新的数据文件,这个过程比较耗时。
- 适用场景:对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
COW缺陷
- 数据一致性问题
cow这种实现只是保证数据的最终一致性,在添加到拷贝数据但还没进行替换的时候,读到的仍然是旧数据。 - 内存占用问题
如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。
mor
- 优点:由于写入数据先写delta log,且delta log较小,所以写入成本较低;
- 缺点:需要定期合并整理compact,否则碎片文件较多。读取性能较差,因为需要将delta log和老数据文件合并 。
Merge-on-Read 的特点是写⼊速度比较快,但是在数据读取过程中由于需要进⾏多路归并排序,存在着大量非必要的 CPU 计算资源消耗和 IO 开销。
mow
优点:兼顾了写入和查询性能。该模式不需要在读取的时候通过归并排序来对主键进行去重,这对于高频写入的场景来说,大大减少了查询执行时的额外消耗。此外还能够支持谓词下推,并能够很好利用 Doris 丰富的索引,在数据 IO 层面就能够进行充分的数据裁剪,大大减少数据的读取量和计算量,因此在很多场景的查询中都有非常明显的性能提升。
总结
hudi/delta lake:cow/mor
kudu: delta store
doris:mor/mow
redis: cow
starrocks: full row upsert/delete /cow/mor/mow
,Merge-On-Write 付出中等的写入代价,换取了较低的读取成本,对谓词下推、非 key 列索引过滤均能友好支持,并对查询性能优化有较好的效果。