事务日志(Transaction log)是理解 Delta Lake 的一个关键点,不少 Delta Lake 的重要特性都是基于事务日志实现的,包括 ACID 事务性、可扩展元数据处理、时间回溯等等。本文将探讨什么是事务日志,如何在文件层面实现,以及怎样优雅地解决并发读写的问题。数据库
Delta Lake 的事务日志(简称 DeltaLog)是一种有序记录集,按序记录了 Delta Lake 表从生成伊始的全部事务操做。json
单一信息源缓存
Delta Lake 基于 Apache Spark 构建,用来支持多用户同时读写同一数据表。事务日志做为单一信息源——跟踪记录了用户全部的表操做,从而为用户提供了在任意时刻准确的数据视图。并发
当用户首次访问 Delta Lake 的表,或者对一张已打开的表提交新的查询但表中的数据在上一次访问以后已发生变化时,Spark 将会检查事务日志来肯定该表经历了哪些事务操做,并将更新结果反馈给用户。这样的流程保证了用户所看到的数据版本永远保持与主分支一致,不会对同一个表产生有冲突的修改。大数据
Delta Lake 原子性实现spa
原子性,做为 ACID 四个特性之一,保证了对数据湖的操做(例如 INSERT 或者 UPDATE)或者所有完成,或者所有不完成。若是没有原子性保证,那么很容易由于硬件或软件的错误致使表中的数据被部分修改,从而致使数据错乱。版本控制
事务日志提供了一种机制来保证 Delta Lake 的原子性。任何操做只要没有记录在事务日志中,都会被认为没有发生过。事务操做只有在彻底执行成功后才会被记录到事务日志中,而且将事务日志做为单一信息源,这二者保证了数据的可靠性,保证用户能够安心处理 PB 级的数据。调试
将事务分解为原子提交日志
每当用户提交一个修改表的操做时(例如 INSERT, UPDATE 或 DELETE),Delta Lake 将该操做分解为包括以下所示的一系列离散的步骤:blog
这些操做都会被记录在事务日志中,造成一系列原子的单元,称做提交。
例如,假设用户建立了一个事务,往表中新增一列,而且添加一些数据。Delta Lake 会将该事务分解成离散步骤,当事务完成后,将如下提交添加到事务日志中:
事务日志在文件层面的实现
当用户建立一个 Delta Lake 的表时,会在 _delta_log 子目录下自动建立该表的事务日志。后续对表的修改操做都将被记录为有序的原子提交,写入事务日志中。每一个提交都是一个 JSON 文件,序号从 000000.json 开始。以后的修改操做都将生成递增的文件序号,例如 000001.json、000002.json,以此类推。
举个例子,假如咱们须要往数据文件 1.parquet 和 2.parquet 中添加新的记录,该事务会被自动写入到事务日志中,保存成 000000.json 文件。而后,咱们又决定删除这些文件而且添加一个新的文件(3.parquet),这些操做将被记录成事务日志中的下一个新的提交 000001.json,以下图所示:
即便如今 1.parquet 和 2.parquet 已经再也不是 Delta Lake 表中的数据,对它们的添加删除操做仍会记录在事务日志中,由于即使增删操做的做用最后相互抵消,可是这些操做是确实发生过的。Delta Lake 仍会保留这些原子提交,来保证当咱们须要对事件进行审计,或者进行时间回溯查询表在某个历史时间点的视图时,咱们均可以得到精确的结果。
另外,Spark 也不会从磁盘上删除这些文件,即便咱们执行了删除了底层的数据文件的操做。用户能够经过 VACUMM 命令显示地删除再也不须要的文件。
从 Checkpoint 文件快速重构状态
每隔 10 个提交,Delta Lake 会在 _delta_log 子目录下自动生成一个 Parquet 格式的 checkpoint 文件。
这些 checkpoint 文件保存了表在该时间点上的全部状态,而原生 Parquet 格式对 Spark 读取也比较友好和高效。换句话说,checkpoint 文件给 Spark 提供了一种捷径来重构表状态,避免低效地处理可能上千条的 JSON 格式的小文件。
为了同步提交进度,Spark 能够执行 listFrom 操做查看全部事务日志的文件,快速跳转到最新的 checkpoint 文件,这样只需处理该 checkpoint 以后的 JSON 提交便可。
下面详细阐述一下该工做流程,假设咱们的提交一直建立到 000007.json,以下图所示,Spark 同步到该提交,也就是说已经将表的最新版本缓存在内存中。同时,其余提交者添加了新的提交一直建立到 000012.json。
为了包含这些新的事务而且更新咱们的表状态,Spark 会运行 listFrom verion 7 操做来查看新的修改。
Spark 将会直接跳转到最新的 checkpoint 文件,而不是逐条处理全部的 JSON 文件,由于 checkpoint 文件包含了 commit #10 以前的全部表状态。如今,Spark 只需增量执行 0000011.json 和 0000012.json,来构建表的当前状态,而后将版本12缓存在内存中。经过这样的流程,Delta Lake 可以利用 Spark 来高效地维护任意时刻的表状态。
咱们已经阐述了事务日志的大体工做原理,接下来咱们讨论一下如何处理并发。以上咱们的示例基本覆盖了用户顺序提交事务的场景,或者说是没有冲突的场景。但若是 Delta Lake 处理并发读写会发生什么?
这个问题很是简单,因为 Delta Lake 是基于 Apache Spark 实现的,多个用户同时修改一个表彻底是一种很是常见的场景,Delta Lake 使用了乐观锁来解决这个问题。
什么是乐观锁
乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法,它假设多用户并发的事务在处理时不会彼此互相影响。乐观锁很是高效,由于在处理 PB 级大数据时,有很大几率不一样用户处理的是数据的不一样部分,乐观锁使得各事务可以在不产生锁的状况下处理各自影响的那部分数据。
举个例子,假设你和我在一块儿合做玩拼图游戏,只要咱们负责拼不一样的部分,好比你负责拼角,我负责拼边,那么咱们彻底能够同时处理咱们各自负责的部分,最终可以以翻倍的速度完成拼图。只有当咱们同时须要拿同一块拼图时才会发生冲突。这就是乐观锁的原理。
相对而言,有一些数据库使用了悲观锁,悲观锁假设了最坏的状况,就是说即便咱们有 10,000 块拼图,也假设咱们会同时拿同一块拼图,从而会形成大量的冲突状况。悲观锁规定同时只能有一我的对拼图进行操做,其余人不能同时操做拼图,这并非一个完成拼图游戏的高效方式。
固然,即便使用乐观锁,仍是会存在不一样用户同时修改数据同一部分的场景,幸运的是,Delta Lake 有一套本身的协议来处理这个问题。
乐观处理冲突
为了提供 ACID 事务性,Delta Lake 有一套协议来规定 commit 如何排序(也就是数据库领域串行性 serializability 的概念),协议规定了如何处理同一时间点的有多个 commit。Delta Lake 经过互斥准则来处理这种场景,而且试图乐观处理冲突。协议容许 Delta Lake 实现 ACID 的隔离性准则(isolation),保证了通过多个并发提交后表的最终状态和单独顺序提交的结果是一致的。
一般来讲,整个流程以下所示:
下图具体阐述了 Delta Lake 如何处理冲突,假设两个用户从同一个表中读取数据,而后同时去尝试添加数据。
在大部分状况下,这种重试可以在后台无感知的完成,但也存在一些状况 Delta Lake 没法经过这种重试成功完成(例如 User 1 和 2 都同时删除同一个文件),在这种状况下将会返回异常给用户。
最后提一点,因为 Delta Lake 表的全部事务都直接保存在磁盘上,所以整个过程知足 ACID 的持久性(durability)特性,也就是说可以容忍诸如系统崩溃等错误状况。
时间回溯
每张表的状态都是由记录在事务日志中中的全部 commit 所决定的,事务日志至关于提供了每一步操做的历史记录,详细记录了表从初始状态到当前状态的全部操做步骤。所以,咱们能够经过遍历表从初始状态到某个时间点的全部 commit ,来重构出表在任意时间点的状态。这个强大的功能就是时间回溯,或者叫作数据版本控制。更多关于时间回溯的说明,能够参考 Introducing Delta Time Travel for Large Scale Data Lakes。
数据血缘和调试
事务日志确切地记录了 Delta Lake 表的全部修改,所以它能提供可信的数据血缘,这对治理、审计和合规目的颇有用处。它也能够用来跟踪一些无心或者有错误的修改,从而可以回退到指望的版本。用户能够执行 DESCRIBE HISTORY 来查看指定修改附近的元数据。
本文详细探讨了 Delta Lake 的事务日志,主要包含了如下几点:
本文为云栖社区原创内容,未经容许不得转载。