在第 1 部分中,我们构建了一个逻辑模型,用于说明写入时复制表在 Apache Hudi 中的工作方式,并提出了许多关于并发控制类型、时间戳单调性等方面的一致性问题。在第 2 部分中,我们研究了时间戳冲突、它们的概率以及如何避免它们(并符合 Hudi 规范)。在第 3 部分中,我们将重点介绍模型检查 TLA+ 规范的结果,并回答这些问题。
此 TLA+ 规范仅对我到目前为止解释的逻辑进行建模:
该规范具有以下参数:
该规范有一个重要的不变量 ConsistentRead,它检查每个提交的 KV 对操作(插入/更新/删除)是否永远可读,其值与该提交相关联(在兼容的时间戳处)。将结果总结为两类:
Hudi 规范明确指出时间戳必须是单调的,因此下面的所有配置都使用单调时间戳。对于多写入器方案,建议使用锁定,因此配置包括乐观和悲观的并发控制。最后避免重复的主键冲突检测是可选的,因此有带和不带它的配置。
Id Locking Timestamps PK conflict check Consistent
---- ------------- ------------ ------------------- ----------------
1 Optimistic Monotonic YES OK
2 Optimistic Monotonic NO Duplicate rows
3 Pessimistic Monotonic YES OK
4 Pessimistic Monotonic NO Duplicate rows
这些结果与 Hudi 文档中列出的保证相关:
正如我们在第 1 部分中介绍的那样,原子性和持久性是微不足道的。模型检查现在为我们提供了结果,以确定 Hudi 是否也支持一致性和隔离性。
当实现并启用可选的主键冲突检测时,将提供完整的 ACID 保证。但是,如果没有主键冲突检测,我们会遇到隔离失败,从而导致跨文件组的主键重复。仅当两个或多个并发操作在不同的文件组中插入相同的主键时,才会发生这种情况。对主键到文件组映射索引的最后一次写入获胜。在 OLTP 系统中,这种隔离问题可能只会导致写入/更新丢失,但在 Hudi 中,它会导致一致性问题,因为孤立的行仍然可以在错误的文件组中读取。在多写入器方案中使用主键冲突检查可解决问题。
以下配置不符合 Hudi v5 规范。但是存在一些可以使不符合要求的配置安全的对策,即 PutIfAbsent 存储支持以及在即时文件名和文件切片文件名中使用盐。为了完整起见,我们将查看安全和不安全的不符合项配置。
Id PutIfAbsent Salt CC Timestamps PK check Consistent
---- ------------- ------ ------------- --------------- ---------- ----------------------------------
5 Any Any No locking Any Any Fail (lost write)
6 NO NO Optimistic Non-monotonic YES Fail (lost write - ts collision)
7 NO NO Pessimistic Non-monotonic YES Fail (lost write - ts collision)
8 YES NO Optimistic Non-monotonic YES OK
9 YES NO Pessimistic Non-monotonic YES OK
10 NO YES Optimistic Non-monotonic YES OK
11 NO YES Pessimistic Non-monotonic YES OK
数据丢失的唯一情况与不符合要求的配置有关。我们还看到如果使用支持 PutIfAbsent 的存储或使用盐,我们可以摆脱非单调时间戳。但是,不对多个写入器进行并发控制从来都不安全。让我们深入了解其中的一些场景,以了解为什么每种场景都是安全的或不安全的。
参数:
此配置可保证避免时间戳冲突,但会遇到写入丢失的情况。
图 1.问题在于,不同主键的并发操作映射到同一个文件组,并且两个写入器同时读取时间线,找不到任何现有的文件切片。这导致第二个操作没有合并第一个操作的内容,从而导致主键 k1 的写入丢失。
参数:
这与情况 1 相同,只是我们使用乐观并发控制。这一次按键操作被放在锁中,导致第二个操作无法通过其 OCC 检查。
图 2.w2 的并发控制检查扫描了时间线,发现了 w1 的完成瞬间,与 w2 的操作触及了同一个文件组。编写器 w2 的更新器没有合并目标,因此使用时间戳 0 进行检查。w1 的已完成时刻的时间戳高于 0,因此检测到冲突。
参数
如果没有 PK 冲突检测,不同写入器对同一密钥的两个并发插入可能会导致同一密钥被写入两个单独的文件组,尽管有 OCC。
图 3.如果使用了 PK 冲突检测,w2 将看到键 k1 现在存在映射,这与它自己的赋值冲突,并且它将无法通过检查并中止。因为它没有这样做,所以它覆盖了 w1 的映射,并孤立了文件组 1 中的行。
当主键的副本存在于与索引不对应的文件组中时,只要其文件切片仍从时间线引用,它仍然是可读的。有趣的是这样一个仍然可读的孤立行最终是如何被过滤掉的?据推测,将文件切片合并到新的文件切片中将保留该行。
参数:
在 TLA+ 规范中,非单调时间戳是非确定性地发出的,其任何值介于 1 和单调值之间(包括会发生冲突的重复时间戳)。在进行暴力检查时,模型检查器实际上会探索每个操作的 1 和最低单调值之间的所有时间戳值。
图 4.两位写入端都选择了时间戳 ts=1。虽然 OCC 检查阻止了第二个操作的完成,但它并没有阻止第一个操作的文件切片被第二个操作的文件切片覆盖(因为文件名完全相同)。
参数:
对 1.commit.requested 的第二次写入失败,因为它已经存在,并且 w2 提前中止。
图 5.写入端 w2 在即时 1.commit.requested 的 put-if-absent 上中止。
回到第 1 部分分析的开头,不确定 v5 Hudi 规范谈论单调时间戳是否意味着插入时间或发布时间。在经历了在 TLA+ 中对 Hudi 进行建模的过程后,从正确的角度来看,最重要的是时间戳不应该发生冲突,至少在使用不支持 PutIfAbsent 的存储服务时是这样。但是,如果两个写入器获得的时间戳在发出时是单调的,但操作是无序执行的,会发生什么情况?答案是只要选择了一种合规、安全的配置,一切都没问题。
示例:乱序,相同的主键 进行以下操作,注意插入顺序与ts顺序不匹配:
行为:
如果两个重叠的操作不按时间戳顺序执行,则只有一个操作成功。使用 OCC 时,文件切片只能按时间戳顺序提交。从性能角度来看,这意味着以单调时间戳顺序执行的操作由于冲突较少,将具有更好的性能。
示例:乱序,不同的主键映射到不同的文件组
首先,op 1 和 op 2 执行 upserts:
然后执行 op3 和 op 4。
如果两个不相交的操作不按顺序执行,则两个操作都成功。但是,跨键的一致性呢?如果客户端在 ts=3 或 ts=4 时一直重复检索所有键,结果是否一致?在 ts=3 时,读取器在一遍又一遍地重复其查询时会看到以下结果:
在 ts=4 时,读取器在一遍又一遍地重复其查询时会看到以下结果:
在 ts=4 的情况下,读者在 k1=B 之前看到 k2=Y。这没关系,因为这两个操作是重叠的,因此任何选择的实现这些操作的总顺序都是有效的(这就是我们在这里看到的)。多个客户端在同一时间戳上读取将看到相同的总订单。
这种分析的范围有限,但到目前为止,模型检查 TLA+ 规范的结果与 Apache Hudi 文档并发控制的多写入器部分中讨论的保证相对应。符合 Hudi 规范的配置以及在多写入器方案中使用主键冲突检测时,都支持 ACID。在这个分析中非常关注多写入器场景。然而单写入器设置是更常见的情况。
关于多写入器方案,Apache Hudi v5 规范明确指出时间戳应该是单调的。根据我的分析,最重要的是时间戳不应该发生冲突,并且有多种选择可以做到这一点。如果使用支持 PutIfAbsent 的存储服务,则这是一个已解决的问题。否则如果使用的是 S3,则需要单调时间戳的来源。鉴于分布式锁定对于多写入器设置的正确性肯定是必需的,因此像 DynamoDB 或 ZooKeeper 之类的东西可以执行锁和单调计数器。使用这种系统进行时间戳和锁定对性能的影响应该是最小的,因为每秒的操作数应该比 Kafka 主题或 OLTP 数据库表低得多。加载时间线、读取和写入 Parquet 文件的成本应大大超过获取时间戳和获取/释放锁的成本。
Delta Lake 和 Apache Hudi 在这一点上非常相似,它们都采用预写日志 (WAL) 方法,并且都要求 WAL 条目使用单调标识符。Databricks 在使用 S3 时使用轻量级协调(可能是锁定)服务来确保不会发生 id 冲突(因为它缺乏 PutIfAbsent 支持)。Databricks 指出,由于湖仓一体表的写入速率相对较低,因此此协调服务的负载较低。
如果花更多时间分析,接下来的步骤将是建模读后合并 (MOR) 表和表服务(压缩、聚簇、清理等)。关于这些附加功能如何安全工作肯定有更多的数据一致性问题。到目前为止我的结果与 Hudi 文档中的保证相关,因此没有理由会发现问题。即便如此,进一步了解 Hudi 内部结构将是一个有用的练习。