Hudi Upsert原理

1. 前言

如果要深入了解Apache Hudi技术的应用或是性能调优,那么明白源码中的原理对我们会有很大的帮助。Upsert是Apache Hudi的核心功能之一,主要完成增量数据在HDFS/对象存储上的修改,并可以支持事务。而在Hive中修改数据需要重新分区或重新整个表,但是对于Hudi而言,更新可以是文件级别的重写或是数据先进行追加后续再重写,对比Hive大大提高了更新性能。upsert支持两种模式的写入Copy On WriteMerge On Read ,下面本文将介绍Apache Hudi 在Spark中Upsert的内核原理。

2. Upsert场景执行流程介绍

对于Hudi Upsert 操作整理了比较核心的几个操作如下图所示(检查上次事务失败后进行回滚操作)

1)开始提交:判断上次任务是否失败,如果失败会触发回滚操作。 然后会根据当前时间生成一个事务开始的请求标识元数据。

2)构造HoodieRecord Rdd对象:Hudi 会根据元数据信息构造HoodieRecord Rdd 对象,方便后续数据去重和数据合并。

3)数据去重:一批增量数据中可能会有重复的数据,Hudi会根据主键对数据进行去重避免重复数据写入Hudi 表。

4)数据fileId位置信息获取:在修改记录中可以根据索引获取当前记录所属文件的fileid,在数据合并时需要知道数据update操作向那个fileId文件写入新的快照文件。

5)数据合并:Hudi 有两种模式cow和mor。在cow模式中会重写索引命中的fileId快照文件;在mor 模式中根据fileId 追加到分区中的log 文件。

6)完成提交:在元数据中生成xxxx.commit文件,只有生成commit 元数据文件,查询引擎才能根据元数据查询到刚刚upsert 后的数据。

7)compaction压缩:主要是mor 模式中才会有,他会将mor模式中的xxx.log 数据合并到xxx.parquet 快照文件中去。

8)hive元数据同步:hive 的元素数据同步这个步骤需要配置非必需操作,主要是对于hive 和presto 等查询引擎,需要依赖hive 元数据才能进行查询,所以hive元数据同步就是构造外表提供查询。

介绍完Hudi的upsert运行流程,再来看下Hudi如何进行存储并且保证事务,在每次upsert完成后都会产生commit 文件记录每次重新的快照文件。

例如上图

时间1初始化写入三个分区文件分别是xxx-1.parquet;

时间3场景如果会修改分区1和分区2xxx-1.parquet的数据,那么写入完成后会生成新的快照文件分别是分区1和分区2xxx-3.parquet文件。(上述是COW模式的过程,而对于MOR模式的更新会生成log文件,如果log文件存在并且可追加则继续追加数据)。

时间5修改分区1的数据那么同理会生成分区1下的新快照文件。

可以看出对于Hudi 每次修改都是会在文件级别重新写入数据快照。查询的时候就会根据最后一次快照元数据加载每个分区小于等于当前的元数据的parquet文件。Hudi事务的原理就是通过元数据mvcc多版本控制写入新的快照文件,在每个时间阶段根据最近的元数据查找快照文件。因为是重写数据所以同一时间只能保证一个事务去重写parquet 文件。不过当前Hudi版本加入了并发写机制,原理是Zookeeper分布锁控制或者HMS提供锁的方式, 会保证同一个文件的修改只有一个事务会写入成功。

下面将根据Spark 调用write方法深入剖析upsert操作每个步骤的执行流程。

2.1 开始提交&数据回滚

在构造好spark 的rdd 后会调用 df.write.format("hudi") 方法执行数据的写入,实际会调用Hudi源码中的HoodieSparkSqlWriter#write方法实现。在执行任务前Hudi 会创建HoodieWriteClient 对象,并构造HoodieTableMetaClient调用startCommitWithTime方法开始一次事务。在开始提交前会获取hoodie 目录下的元数据信息,判断上一次写入操作是否成功,判断的标准是上次任务的快照元数据有xxx.commit后缀的元数据文件。如果不存在那么Hudi 会触发回滚机制,回滚是将不完整的事务元数据文件删除,并新建xxx.rollback元数据文件。如果有数据写入到快照parquet 文件中也会一起删除。

2.2 构造HoodieRecord Rdd 对象

HoodieRecord Rdd 对象的构造先是通过map 算子提取spark dataframe中的schema和数据,构造avro的GenericRecords Rdd,然后Hudi会在使用map算子封装为HoodierRecord Rdd。对于HoodileRecord Rdd 主要由currentLocation,newLocation,hoodieKey,data 组成。HoodileRecord数据结构是为后续数据去重和数据合并时提供基础。

currentLocation 当前数据位置信息:只有数据在当前Hudi表中存在才会有,主要存放parquet文件的fileId,构造时默认为空,在查找索引位置信息时被赋予数据。

newLocation 数据新位置信息:与currentLocation不同不管是否存在都会被赋值,newLocation是存放当前数据需要被写入到那个fileID文件中的位置信息,构造时默认为空,在merge阶段会被赋予位置信息。

HoodieKey 主键信息:主要包含recordKey 和patitionPath 。recordkey 是由hoodie.datasource.write.recordkey.field 配置项根据列名从记录中获取的主键值。patitionPath 是分区路径。Hudi 会根据hoodie.datasource.write.partitionpath.field 配置项的列名从记录中获取的值作为分区路径。

data 数据:data是一个泛型对象,泛型对象需要实现HoodieRecordPayload类,主要是实现合并方法和比较方法。默认实现OverwriteWithLatestAvroPayload类,需要配置hoodie.datasource.write.precombine.field配置项获取记录中列的值用于比较数据大小,去重和合并都是需要保留值最大的数据。

2.3 数据去重

在upsert 场景中数据去重是默认要做的操作,如果不进行去重会导致数据重复写入parquet文件中。当然upsert 数据中如果没有重复数据是可以关闭去重操作。配置是否去重参数为hoodie.combine.before.upsert,默认为true开启。

在Spark client调用upsert 操作是Hudi会创建HoodieTable对象,并且调用upsert 方法。对于HooideTable 的实现分别有COW和MOR两种模式的实现。但是在数据去重阶段和索引查找阶段的操作都是一样的。调用HoodieTable#upsert方法底层的实现都是SparkWriteHelper

在去重操作中,会先使用map 算子提取HoodieRecord中的HoodieatestAvroPayload的实现是保留时间戳最大的记录。这里要注意如果我们配置的是全局类型的索引,map 中的key 值是 HoodieKey 对象中的recordKey。因为全局索引是需要保证所有分区中的主键都是唯一的,避免不同分区数据重复。当然如果是非分区表,没有必要使用全局索引。

2.4 数据位置信息索引查找

对于Hudi 索引主要分为两大类:

非全局索引:索引在查找数据位置信息时,只会检索当前分区的索引,索引只保证当前分区内数据做upsert。如果记录的分区值发生变更就会导致数据重复。

全局索引:顾名思义在查找索引时会加载所有分区的索引,用于定位数据位置信息,即使发生分区值变更也能定位数据位置信息。这种方式因为要加载所有分区文件的索引,对查找性能会有影响(HBase索引除外)。

Spark 索引实现主要有如下几种

布隆索引(BloomIndex)

全局布隆索引(GlobalBloomIndex)

简易索引(SimpleIndex)

简易全局索引(GlobalSimpleIndex)

全局HBase 索引(HbaseIndex)

内存索引(InMemoryHashIndex)。

下面会对索引的实现方式做一一介绍。

2.4.1 布隆索引(BloomIndex)

Spark 布隆索引的实现类是SparkHoodieBloomIndex,要想知道布隆索引需要了解下布隆算法一般用于判断数据是否存在的场景,在Hudi中用来判断数据在parquet 文件中是否存在。其原理是计算RecordKey的hash值然后将其存储到bitmap中去,key值做hash可能出现hash 碰撞的问题,为了较少hash 值的碰撞使用多个hash算法进行计算后将hash值存入BitMap,一般三次hash最佳,但是索引任然有hash 碰撞的问题,但是误判率极低可以保证99%以上的数据判断是正确的。即使有个别数据发生了误判也没有关系在合并操作中如果匹配不到也会被丢弃。

索引实现类调用tagLocation开始查找索引记录存在哪个parquet 文件中,步骤如下

提取所有的分区路径和主键值,然后计算每个分区路径中需要根据主键查找的索引的数量。

有了需要加载的分区后,调用LoadInvolvedFiles 方法加载分区下所有的parquet 文件。在加载paquet文件只是加载文件中的页脚信息,页脚存放的有布隆过滤器、记录最小值、记录最大值。对于布隆过滤器其实是存放的是bitmap序列化的对象。

加载好parquet 的页脚信息后会根据最大值和最小值构造线段树。

根据Rdd 中RecordKey 进行数据匹配查找数据属于那个parqeut 文件中,对于RecordKey查找只有符合最大值和最小值范围才会去查找布隆过滤器中的bitmap ,RecordKey小于最小值找左子树,RecordKey大于最大值的key找右子树。递归查询后如果查找到节点为空说明RecordKey在当前分区中不存在,当前Recordkey是新增数据。查找索引时spark会自定义分区避免大量数据在一个分区查找导致分区数据倾斜。查找到RecordKey位置信息后会构造 Rdd 对象。

线段树的数据结构如下:

以 Rdd 为左表和Rdd 做左关联,提取HoodieRecordLocation位置信息赋值到HoodieRecord 的currentLocation变量上,最后得到新的HoodieRecord Rdd。在HoodieRecordLocation对象中包含文件fileID 和快照时间。

说明:Parquet 文件名称组成主要是36位fileId、文件编号、spark 任务自定义分区编号、spark 任务stage 编号、spark 任务attempt id、commit时间。

2.4.2 全局布隆索引(GlobalBloomIndex)

全局布隆索引底层算法和布隆索引一样,只是全局布隆索引在查找每个RecordKey 属于那个parquet 文件中,会加载所有parquet文件的页脚信息构造线段树,然后在去查询索引。

因为Hudi需要加载所有的parquet文件和线段树节点变多对于查找性能会比普通的布隆索引要差。但是对于分区字段的值发生了修改,如果还是使用普通的布隆索引会导致在当前分区查询不到当成新增数据写入Hudi表。这样我们的数据就重复了,在很多业务场景是不被允许的。所以在选择那个字段做分区列时,尽量选择列值永远不会发生变更的,这样我们使用普通布隆索引就可以了。

2.5 数据合并

COW模式和MOR模式在前面的操作都是一样的,不过在合并的时候Hudi构造的执行器不同。对于COW会根据位置信息中fileId 重写parquet文件,在重写中如果数据是更新会比较parquet文件的数据和当前的数据的大小进行更新,完成更新数据和插入数据。而MOR模式会根据fileId 生成一个log 文件,将数据直接写入到log文件中,如果fileID的log文件已经存在,追加数据写入到log 文件中。与COW 模式相比少了数据比较的工作所以性能要好,但是在log 文件中可能保存多次写有重复数据在读log数据时候就不如cow模式了。还有在mor模式中log文件和parquet 文件都是存在的,log 文件的数据会达到一定条件和parqeut 文件合并。所以mor有两个视图,ro后缀的视图是读优化视图(read-optimized)只查询parquet 文件的数据。rt后缀的视图是实时视图(real-time)查询parquet 和log 日志中的内容。

2.5.1 copy on write模式

COW模式数据合并实现逻辑调用BaseSparkCommitActionExecutor#excute方法,实现步骤如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/884391.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ctfshow web入门文件上传总结

1.web151 前端验证 前端验证&#xff0c;修改html代码&#xff0c;上传还有一句话木马的php文件,之后用蚁剑连接即可找到flag <?php eval($_POST[1])?>2.web152 后端验证&#xff0c;修改mime类型(content-type) burp抓包&#xff0c;修改content-type为image/png …

基于Spring Boot+Vue的助农销售平台(协同过滤算法、限流算法、支付宝沙盒支付、实时聊天、图形化分析)

&#x1f388;系统亮点&#xff1a;协同过滤算法、节流算法、支付宝沙盒支付、图形化分析、实时聊天&#xff1b; 一.系统开发工具与环境搭建 1.系统设计开发工具 后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk1…

手把手写Linux第一个小程序 - 进度条(5种版本)

本专栏内容为&#xff1a;Linux学习专栏&#xff0c;分为系统和网络两部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握Linux。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;linux &#x1f69a;代码仓库&#xff1a;小小unicorn的代…

dhcp池没有空闲ip导致手机无法获得ip

得到用户反馈&#xff0c;一个高速项目部的wifi无法接入&#xff0c;让排查原因。 反馈有的手机能接入&#xff0c;有的接入不了。查看ac界面发现有个终端获得的ip是169.254.xxx.xxx。 ip地址是169.254.96.17显然是手机打开wlan开关后&#xff0c;鉴权通过后dhcp过程&#xff0…

《高频电子线路》—— 振荡器稳定性问题

文章内容来源于【中国大学MOOC 华中科技大学通信&#xff08;高频&#xff09;电子线路精品公开课】&#xff0c;此篇文章仅作为笔记分享。 振荡器稳定性问题 频率准确度 & 频率稳定度 希望频率稳定度越小越好。 频率稳定度分类 影响振荡频率稳定度的参数 振荡频率是和电…

HTMLCSS: 打造跳一跳加载器,点燃用户等待热情

效果演示 这段 HTML 代码创建了一个简单的网页&#xff0c;其中包含一个动画效果&#xff0c;用来模拟一个加载器loading HTML <div class"loader"></div>div创建了一个动画效果的加载器 CSS html, body {width: 100vw;height: 100vh;display: flex…

Nginx 的 Http 模块介绍(上)

Nginx 的 Http 模块介绍&#xff08;上&#xff09; 1. http 请求 11 个处理阶段介绍 Nginx 将一个 Http 请求分成多个阶段&#xff0c;以模块为单位进行处理。其将 Http请求的处理过程分成了 11 个阶段&#xff0c;各个阶段可以包含任意多个 Http 的模块并以流水线的方式处理…

网络:ARP的具体过程和ARP欺骗

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》《网络》 《redis学习笔记》 文章目录 前言ARP具体过程ARP欺骗原理总结 前言 本文仅作为ARP具体过程和ARP欺骗的知识总结 硬件类型 &#xff1a;指定发送和接受ARP包的硬件类型&am…

[0260].第25节:锁的不同角度分类

MySQL学习大纲 我的数据库学习大纲 从不同维度对锁的分类&#xff1a; 1.对数据操作的类型划分:读锁和写锁 1.1.读锁 与 写锁概述&#xff1a; 1.对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题&#xff0c;需要使用…

数据结构之链式结构二叉树的实现(初级版)

本文内容将主会多次用到函数递归知识&#xff01;&#xff01;&#xff01; 本节内容需要借助画图才能更好理解&#xff01;&#xff01;&#xff01; 和往常一样&#xff0c;还是创建三个文件 这是tree.h #pragma once #include<stdio.h> #include<stdlib.h> …

Data+AI━━人群圈选,你被圈中了吗?

DataAI━━人群圈选&#xff0c;你被圈中了吗&#xff1f; 前言我们是否正在失去选择的自主权&#xff1f;AI人群圈选流程AI人群圈选技术发展趋势 前言 智能时代的清晨&#xff0c;打开手机&#xff0c;各类APP精准推送的信息扑面而来。菜市场买过一次三文鱼&#xff0c;生鲜A…

手撕快排的三种方法:让面试官对你刮目相看

快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 目录 &#x1f4af;前言 &#x1f4af;快速排序基础概念 &#x1f4af;Hoare 版本 1.算法思路 2.代码示例 3.有关该代码的问题 3.1&#x1f62e;为什么…

51单片机教程(五)- LED灯闪烁

1 项目分析 让输入/输出口的P1.0或P1.0~P1.7连接的LED灯闪烁。 2 技术准备 1、C语言知识点 1 运算符 1 算术运算符 #include <stdio.h>int main(){// 算术运算符int a 13;int b 6;printf("%d\n", ab); printf("%d\n", a-b); printf("%…

ceph补充介绍

SDS-ceph ceph介绍 crushmap 1、crush算法通过计算数据存储位置来确定如何存储和检索&#xff0c;授权客户端直接连接osd 2、对象通过算法被切分成数据片&#xff0c;分布在不同的osd上 3、提供很多种的bucket&#xff0c;最小的节点是osd # 结构 osd (or device) host #主…

集成ruoyi-it管理系统,遇到代码Bug

前言&#xff1a;这次ruoyi框架开发it管理系统&#xff0c;出现很多问题&#xff0c;也有学到很多东西&#xff0c;出现几个问题&#xff0c;希望下次项目不会出现或者少出现问题&#xff1b;其中还是有很多基础知识有些忘记&#xff0c;得多多复习 1&#xff1a;当写的代码没…

大模型面试-Layer normalization篇

1. Layer Norm 的计算公式写一下? 2. RMS Norm 的计算公式写一下? 3. RMS Norm 相比于 Layer Norm 有什么特点? 4. Deep Norm 思路? 5. 写一下 Deep Norm 代码实现? 6.Deep Norm 有什么优点? 7.LN 在 LLMs 中的不同位置 有什么区别么?如果有,能介绍一下区别么? 8. LLM…

【Linux第七课--基础IO】内存级文件、重定向、缓冲区、文件系统、动态库静态库

目录 引入内存级文件重新使用C文件接口 -- 对比重定向写文件读文件文件流 认识文件操作的系统接口open参数 -- flagflag的内容宏的传参方式 open关闭文件写文件读文件结论 引入文件描述符fd、对文件的理解理解一切皆文件方法集文件fd的分配规则 重定向代码的重定向输入重定向输…

手写实现call,apply,和bind方法

手写实现call&#xff0c;apply和bind方法 call&#xff0c;apply和bind方法均是改变this指向的硬绑定方法&#xff0c;要想手写实现此三方法&#xff0c;都要用到一个知识点&#xff0c;即对象调用函数时&#xff0c;this会指向这个对象&#xff08;谁调用this就指向谁&#…

Redis全系列学习基础篇之位图(bitmap)常用命令的解析

文章目录 描述常用命令及解析常用命令解析 应用场景统计不确定时间周期内用户登录情况思路分析实现 统计某一特定时间内活跃用户(登录一次即算活跃)的数量思路分析与实现 描述 bitmap是redis封装的用于针对位(bit)的操作,其特点是计算效率高&#xff0c;占用空间少,常被用来统计…

Java | Leetcode Java题解之第518题零钱兑换II

题目&#xff1a; 题解&#xff1a; class Solution {public int change(int amount, int[] coins) {int[] dp new int[amount 1];boolean[] valid new boolean[amount 1];dp[0] 1;valid[0] true;for (int coin : coins) {for (int i coin; i < amount; i) {valid[i…