Delta 表定时Compact设计

Delta 表定时Compact设计

目前我们的数仓中delta表实时跟离线的都有,离线的表主要是每天的定时任务的运行,并根据日期字段进行分区的存储。离线任务正常是读取前一天的数据计算,并写入当天的时间分区,或者是重跑数据,进行N天的数据计算,会读取N的数据计算,最后根据数据日期分区落盘。

正常来说这种方式产生的小文件不会太多,小文件多的delta表大多是实时job写的表,流每次对delta表进行写入的时候,由于delta lake的time travel特性,每次写都会产生小文件,时间长了之后,目录下的小文件就会特别多,造成的影响就是下游的流在读上游的delta表的速度特别慢,导致下游job的处理时间变长,所以对流式job的delta表进行compact变得尤为重要。

我们目前的流式job会对delta表进行CURD操作,所以会出现数据insert的时候compact、数据update的时候compact、数据delete的时候compact。

1.Demo测试

demo测试采用一个进程循环不断对delta表进行操作,另一个进程进行compact

​ 1.delta表insert数据时进行compact操作

​ 总共进行了5次测试,5次compact操作全部成功。通过查看delta_log中的操作json文件可以看到每次delta table进行write操作并且是append类型也就是insert操作的时候,都会产生新的parquet文件。

json 复制代码
{"commitInfo":{"timestamp":1592381504085,"operation":"WRITE","operationParameters":{"mode":"Append","partitionBy":"[]"},"readVersion":26,"isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputBytes":"1077","numOutputRows":"1240"}}}
{"add":{"path":"part-00000-57630465-82f1-4538-980f-0e33d5abf860-c000.snappy.parquet","partitionValues":{},"size":1077,"modificationTime":1592381504000,"dataChange":true}}

​ 而进行compact操作的时候是把数据捞出来,重新repartition之后再overwrite写入delta表的时候,commitInfo的操作是另一种。

json 复制代码
{"commitInfo":{"timestamp":1592381500755,"operation":"WRITE","operationParameters":{"mode":"Overwrite","partitionBy":"[]"},"readVersion":23,"isBlindAppend":false,"operationMetrics":{"numFiles":"1","numOutputBytes":"2562","numOutputRows":"31000"}}}
{"add":{"path":"part-00000-c6bc7931-8b1d-4a11-821e-1b1ef92f194a-c000.snappy.parquet","partitionValues":{},"size":2562,"modificationTime":1592381499000,"dataChange":false}}
{"remove":{"path":"part-00000-3f61376e-dbe4-4e1b-b85e-6c95c3268fc6-c000.snappy.parquet","deletionTimestamp":1592381500743,"dataChange":false}}
{"remove":{"path":"part-00000-63545349-904e-4fbb-b26e-a5d1c4b663b5-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-c75cf921-244e-407a-b6b7-7d3e644a0507-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-5b3ba389-9ee1-49d7-b42c-be1f0be22c8b-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-46ea068e-f161-43d8-8847-21e9b626a9a3-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-35e263d2-04a6-4c4b-954b-4b009b0d3060-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-a5c0ef4c-9cbe-4564-bb17-1cf1182585aa-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-c4552227-9715-4ad6-8937-6b2333c911b3-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-fd425b0c-ebaf-44d3-8650-dd88e1a6c284-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-0b61e35c-4344-47a3-b1cd-b8a0430e6547-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-ca70928a-9f83-4bb7-84be-057ceea64de6-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-0365beec-affb-4f77-8698-f055bc911ee7-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-1b8d4867-b727-4341-ab7c-f7e5cfcf0dc8-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-6b81bb66-3dfd-4042-9025-28d91189b328-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-e3ddb0d0-3291-4fc8-9435-0edccaefb75a-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-03d6cb66-d9f1-4c0d-a34c-253c0320d264-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-58c024a9-526c-4702-820e-6890d0a6a5b1-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-7ce7a3d6-fa0f-4f8f-9439-a555ad716174-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-a6c63319-52e2-4030-a34e-e4053c533f5d-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-3bb25cb6-841e-4675-8c53-a00ed76aed42-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-3cf55ad4-2a57-4258-a15f-ed19ae9b44ee-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-56bf6162-ca12-44b4-bc39-fdba995328cd-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-da6fc919-7b84-44e1-b4ee-63c3e9ab460a-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}
{"remove":{"path":"part-00000-757459b2-936d-4b08-98ea-4d4b9b125e31-c000.snappy.parquet","deletionTimestamp":1592381500744,"dataChange":false}}

​ 从commiInfo上看,是把旧的parquet文件标记删除,然后产生新的parquet文件,旧文件不会被物理删除,而是被打上了删除标记,也就是time travel这个特性,通过在表上运行vacuum命令,可以删除不再由Delta表引用的文件,文件的默认保留期限是7天。测试没报错不代表没问题,正常来说如果两次操作在写数据时,都是生成一个新版本的数据文件(文件名不重复),在提交commit时生成下一个版本的日志文件,因为日志版本号是连续递增的,如果检测到了同名的文件已存在,则说明有其他用户执行了新的commit,此时进行冲突检测,如果检测通过,则更新当前的snapshot,然后继续提交commit,如果未通过冲突检测,则报错。

​ 2.delta表update/delete数据时进行compact操作

​ 进行了1次测试直接报错了,具体错误如下,意思是update操作在compact操作执行过程中就完成了,然后把需要被compact的parquet的文件直接标记删除了,compact操作在commit的时候发现文件被删除了,冲突检测未通过。

org.apache.spark.sql.delta.ConcurrentDeleteReadException: This transaction attempted to read one or more files that were deleted (for example part-00062-ab6b093a-4b85-4aae-8dcf-36214420cc58-c000.snappy.parquet in the root of the table) by a concurrent update. Please try the operation again.
Conflicting commit: {"version":52,"timestamp":1592382511785,"operation":"MERGE","operationParameters":{"predicate":(deltaTable.`name` = newData.`name`)},"readVersion":51,"isBlindAppend":false,"operationMetrics":{"numTargetRowsCopied":"0","numTargetRowsDeleted":"0","numTargetFilesAdded":"2","numTargetRowsInserted":"0","numTargetRowsUpdated":"9672","numOutputRows":"9672","numSourceRows":"1","numTargetFilesRemoved":"1"}}

update操作的commitInfo

json 复制代码
{"commitInfo":{"timestamp":1592382526252,"operation":"MERGE","operationParameters":{"predicate":"(deltaTable.`name` = newData.`name`)"},"readVersion":54,"isBlindAppend":false,"operationMetrics":{"numTargetRowsCopied":"0","numTargetRowsDeleted":"0","numTargetFilesAdded":"2","numTargetRowsInserted":"0","numTargetRowsUpdated":"9672","numOutputRows":"9672","numSourceRows":"1","numTargetFilesRemoved":"1"}}}
{"remove":{"path":"part-00062-1ee1e2e1-ee67-4c7b-b14c-8dddc6171f94-c000.snappy.parquet","deletionTimestamp":1592382526252,"dataChange":true}}
{"add":{"path":"part-00000-9521a2bf-f048-43cd-9b6d-865e14f24409-c000.snappy.parquet","partitionValues":{},"size":426,"modificationTime":1592382525000,"dataChange":true}}
{"add":{"path":"part-00062-6a78badd-36d2-4bd5-b7dc-d3af17360fb0-c000.snappy.parquet","partitionValues":{},"size":948,"modificationTime":1592382526000,"dataChange":true}}

2.Delta Table同时修改表的乐观并发控制

一般来说,过程是这样进行的

  • 记录起始表的版本;
  • 记录读和写操作;
  • 尝试提交;
  • 如果有人已经提交了,检查一下你读到的内容是否有变化;
  • 重复上面的步骤。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l1GIC4sq-1690265585469)(/Users/apple/Documents/yangxin/博客图片/image4-1.png)]

  • Delta Lake 记录在进行任何更改之前读取的表的起始表版本(版本0);
  • 用户1和2都试图同时向表添加一些数据。在这里,我们遇到了一个冲突,因为接下来只有一个提交可以被记录为 000001.json;
  • Delta Lake使用"互斥"概念处理这种冲突,这意味着只有一个用户能够成功提交 000001.json。用户1的提交被接受,而用户2的提交被拒绝;
  • Delta Lake 更倾向于乐观地处理这种冲突,而不是为用户2抛出错误。它检查是否对表进行了任何新的提交,并悄悄地更新表以反映这些更改,然后在新更新的表上重试用户2的提交(不进行任何数据处理),最后成功提交 000002.json。

在绝大多数情况下,这种和解是悄无声息地、天衣无缝地、成功地进行的。但是,如果 Delta Lake 无法乐观地解决不可调和的问题(例如,如果用户1删除了用户2也删除的文件),那么惟一的选择就是抛出一个错误。

【冲突检测(并发控制)】

在源码中commit失败之后重试会进行冲突检测具体代码在 OptimisticTransaction.checkAndRetry

  1. 如果后续commit升级了protocol版本,则不通过;

  2. 如果后续commit更改了metadata,则不通过;

  3. 如果后续commit更改了文件:

    通过三个隔离等级 Serializable,WriteSerializable,SnapshotIsolation 控制;(

    1. Serializable最严格的,要求绝对的串行化,设置了这个级别,只要出现并发冲突,且后续commit log存在AddFile操作,就会报错;
    2. WriteSerializable允许其他commit isBlindAppend时通过冲突检测(即后续的commit仅AddFile,不RemoveFile),此种情况下最终结果和串行的结果可能不同;
    3. SnapshotIsolation最宽松,基本都可以通过这部分的冲突检测,但是可能无法通过其他模块的检测。
  4. 如果后续commit删除了本次读取的文件,则不通过;

  5. 如果后续commit和本次commit删除了同一个文件,则不通过;

  6. 如果幂等的事务发生了冲突(SetTransaction部分有相同的appId),则不通过。

3.Compact设计

设计review之后采用在凌晨数据较少的时候进行compact操作,直接对delta表进行compact操作。

根据我们目前的delta表来看,delta表有两类,一类的写出的流表每天会写到当天的分区,另一类只有一个分区不按天分区,这两种情况要分开处理。

  1. 脚本逻辑:

    脚本输入两个参数,起始日期跟结束日期,日期用于分区表合并时的日期分区,需要合并的表的信息用json封装,其中包含表名、路径、是否分区、分区字段,然后通过base64加密之后作为提交任务的参数传给job。

  2. JOB逻辑:

    job输入参数包括起始日期跟结束日期、delta表的相关信息、compact操作的partition数。delta表的信息先base64解密,然后再解析json生成List,并进行表路径过滤,把不存在的表路径数据过滤掉。后续遍历List,通过是否分区的字段判断是执行分区compact还是不分区compact,分区合并操作将job的起始结束日期的数据并按照分区列进行合并。不分区的表直接将读取所有数据然后compact。添加了try catch操作,保证在发生异常操作时不会影响别的正在执行的compact操作失败退出。

相关推荐
乌龟跌倒44 分钟前
磁盘结构、访问时间、调度算法
大数据
神秘打工猴2 小时前
Spark任务的执⾏流程
大数据·分布式·spark
努力的布布4 小时前
Elasticsearch-索引的批量操作
大数据·elasticsearch·搜索引擎·全文检索
RodrickOMG4 小时前
【大数据】Hadoop三节点集群搭建
大数据·hadoop·分布式
智慧化智能化数字化方案4 小时前
工业金融政务数据分类分级体系建设解读
大数据·金融·数据分类分级·政务·政务数据分类·工业数据分类·金融数据分类分级
DashVector4 小时前
如何通过HTTP API插入或更新Doc
大数据·数据库·数据仓库·人工智能·http·数据库架构·向量检索
斑驳竹影4 小时前
ElasticSearch存储引擎
大数据·elasticsearch·搜索引擎
Aloudata5 小时前
NoETL 自动化指标平台如何保障数据质量和口径一致性?
大数据·数据分析·数据质量·noetl
SelectDB技术团队5 小时前
Apache Doris 创始人:何为“现代化”的数据仓库?
大数据·数据库·数据仓库·数据分析·doris
好记性+烂笔头6 小时前
踏踏实实练SQLday1-1连续登录
数据仓库