刘嘉承:从设计、实现和优化角度浅谈Alluxio元数据同步 DATE: 2024-04-28 18:22:39
导读:今天分享的刘嘉题目是Alluxio元数据和数据的同步,从设计实现和优化的设计实现角度进行讨论。主要包括以下几个方面的和优化角内容 :
- Alluxio简介
- Alluxio的数据挂载
- Alluxio和底层存储的一致性
- Alluxio和UFS的元数据/数据同步
- 元数据同步的实现原理和优化
- 对不同场景的推荐配置
01
Alluxio简介
Alluxio是云原生的数据编排平台,通过解耦计算和存储层,度浅在中间产生了一个数据编排层 ,元数负责对上层计算应用隐藏底层的据同时间细节 。Alluxio提供了统一的刘嘉存储命名空间 ,在中间层提供了缓存和其他数据管理功能。设计实现在下图可以看到有Spark、和优化角Hive、度浅Map reduce这一类传统的元数Hadoop大数据计算应用 、Presto 这种OLAP类型的据同数据分析 ,还有像Tensorflow 、刘嘉Pytorch这样的设计实现AI应用。存储层比较丰富 ,和优化角包括各种各样的存储 。
图1 Alluxio简介
下面是Alluxio用户列表,这些公司都公开展示了Alluxio的使用场景。通过粗略分类 ,看到非常多的行业 ,包括互联网、金融 、电子商务、娱乐、电信等。感兴趣的同学可以关注公众号,上面有相关文章的汇总 。
图2 Alluxio的用户展示
--
02
Alluxio数据挂载
这部分将首先回顾Alluxio如何通过数据挂载实现统一编排层;之后讨论Alluxio如何和底层存储保持一致;介绍元数据和数据同步功能;Alluxio的时间原理和优化;最后对不同场景的推荐配置给出建议 。
1. Alluxio统一的数据命名空间
首先介绍数据挂载这个功能 。Alluxio通过把底层存储挂载到Alluxio层上,实现了统一的数据命名空间 。
图3 Alluxio统一命名空间
上图的例子中Alluxio挂载了HDFS和对象存储 。Alluxio的文件系统树就是由左右两棵树合成 ,形成了一个虚拟文件系统的文件系统树 。它可以支持非常多的底层存储系统,统一把它们称作Under File System 。称为Under是因为它们都处于Alluxio的抽象层下 。Alluxio支持各种各样不同的底层存储系统,比如不同版本的HDFS ,支持NFS, Ceph, Amazon S3, Google Cloud之类不同的对象存储 。除此之外还支持非常多其他类型的对象存储,比如微软Azure 、阿里、华为、腾讯 ,也包括国内其他供应商,如七牛对象存储 。左下图中的例子是在自己的电脑上运行Alluxio ,可以挂载不同的存储 ,比如挂载HDFS,另外还可以挂载不同版本的HDFS ,挂载对象存储,挂载网盘 。
2. Alluxio挂载点
Alluxio的统一命名空间 ,实际就是把挂载合成了一个Alluxio的虚拟层。Alluxio的挂载点可以粗略分成两种:
- 根挂载点
- 嵌套挂载点
图4 Alluxio挂载点
根挂载点直接挂在根节点上 ,组成了Alluxio的根节点。如果没有根节点 ,无法产生,继续形成下面的结构。所以要求在配置文件里面定义根挂载点,系统启动的时候就进行挂载,不挂载就没有办法启动。
嵌套挂载点比较灵活 ,可以通过指令进行挂载。通过这个命令行 ,发出通知 ,做挂载的操作。同样地,可以挂载,也可以卸载,就是把Mount换成Unmount 。嵌套挂载点是嵌套在目录的下面 ,可以挂在某个部分下面,不一定挂载在根节点下面。这里有个要求,即两个嵌套点的树不能互相覆盖 ,这样带来的好处是比较灵活 。如果根挂载点将来需要更换,为了避免需要改配置和重启服务 ,可以使用一个dummy的根挂载点,比如就挂载在本地路径下面 ,不使用它 ,且不在它下面创建任何文件 ,它存在的唯一目的就是可以启动Alluxio服务 。然后在此基础上,把所有要管理的存储,都以嵌套挂载点的方式挂载上去。之后如果要改变,就直接卸载更换为其它挂载点 ,这样就很灵活。所有挂载和挂载操作 ,都会记录在日志里,重启系统 ,并重启服务之后,无需再手动操作。
3. Alluxio策略化数据管理
图5 Alluxio策略化数据管理
挂载操作有一个进阶版操作 ,目前只包含在商业版本里面。所做的事情就是让用户可以把两个存储挂载到同一个路径下 ,可以互相覆盖。同时通过配置读写策略,定义读写文件到哪个存储里,并给出操作的先后顺序。同时Alluxio有一个迁移策略,让文件可以自动在Alluxio的管理下 ,在多个存储之间进行迁移 。例如,把HDFS和对象存储同时挂载到同一路径下 ,上层用户只能看到这样一棵树,但是实际上背后有两个不同的存储 。通过配置,让Alluxio把HDFS的数据 ,根据一些规则 ,定期迁移进S3,例如规定将超过七天的数据 ,认定是不常用到的冷数据之后 ,把它从HDFS的集群拿出来 ,迁移到S3 ,节省HDFS的存储空间 。
--
03
Alluxio底层存储一致性
在把底层存储挂载到Alluxio的统一命名空间上之后,如何保持Alluxio和底层存储的一致性?我们在这一部分进行分析。
图6 Alluxio与一致性
Alluxio和底层存储的一致性,要从Alluxio命名空间中文件的来源说起。文件的操作分为两类:
- 一类是写 ,上层应用通过Alluxio创建一个文件,通过Alluxio写入UFS;
- 一类是读 ,上层应用通过Alluxio读一个文件 ,当发现自己没有这个文件的时候,Alluxio从UFS进行加载。
一致性可以分为两个部分 :
- Alluxio UFS元数据的一致性
- Alluxio UFS数据的一致性
下面先看写数据的一致性。
1. Alluxio写文件流程
首先Alluxio写文件的流程可以把它抽象成两步。第一步是客户端到Alluxio ,第二步是Alluxio到UFS。
图7 Alluxio写文件流程
其中的每一步都可以抽象成下面的三个步骤:
- 创建文件
- 写数据
- 提交文件
同样Alluxio到存储系统也可以同样地抽象提取 。
客户端和Alluxio之间 ,主要流程分三步 :
- 客户端向发请求创建一个文件;
- 找到Alluxio Worker写具体对应的数据;
- 在写完数据之后,提交这个文件 。
同样Alluxio到存储系统也抽象成三步。不同存储系统的抽象和具备的一致性,都不同,此处进行抽象只是为了便于理解 。比如要求强一致性保证,但是很多对象存储 ,给的一致性保证会弱很多,比如写进去之后不能马上读到这个数据。在这里 ,不考虑这种本身的不一致性的问题。假设Alluxio向存储提交了之后 ,就能保证存储端的文件就是需要的样子 。
Alluxio为了满足不同的需求 ,设计了几种不同的写策略,下面逐一分析写策略的流程以及带来的数据一致性保证。
2. Must-Catch写模式
图8 Alluxio:MUST_CACHE写模式
首先是常用的MUST_CACHE模式。在这种模式下 ,只会写Alluxio缓存不写UFS。这个模式分为三步:
首先客户端会向Alluxio 发出创建文件请求,创建的文件只是一个空文件,作为一个占位;
之后Alluxio Worker实现具体数据的写操作 ,具体数据会被分割成多个数据块 ,作为Block存在于Alluxio Storage里面。
在缓存写之后,客户端对Master做提交文件的请求,告诉Master写了这些数据块,写到Worker,然后更新对应的元数据,也知道了这些数据块和Worker所对应的位置。
在这样的流程之后,Alluxio不会向UFS创建这个文件,也不会写这个文件,所以Alluxio和UFS之间的元数据和数据都不一致。
3. Through写模式
图9 Alluxio:Through写模式
THROUGH的写模式有所不同,这里同样的 createfile() 发出一个请求 ,然后找Worker写数据 。Worker会做三件事:
- 创建文件
- 写文件
- 提交文件
在第二步结束后,客户端会向Alluxio 提交这个文件 。因为Alluxio 的提交是发生在文件写完了之后 ,所以 ,Alluxio和UFS此时的元数据是一致的 。因为没有数据缓存 ,所以也不存在数据一致性的问题。
Alluxio的缓存是在需要读之后才会产生 ,而这种THROUGH模式是比较适合用来写已知不再会被Alluxio读取的数据。所以在这种情况下,元数据是一致的 ,也不存在数据不一致的问题。
4. CACHE_THROUGH写模式
下面的CACHE_THROUGH模式就是前面两种模式的结合 。
图10 Alluxio :CATCH_THROUGH写模式
唯一的不同点是在第二步,写缓存的同时又写了UFS 。在这两个都成功之后,第二步才会成功,之后客户端才会做提交操作。同样的道理,因为Alluxio在UFS更新之后才更新,所以两者的元数据和数据都是一致的。
5. ASYNC_THROUGH写模式
最后是ASYNC_THROUGH异步写模式 ,和前面的模式唯一的区别是第二步中的UFS写变成了异步 ,放在了第四步 。
图11 Alluxio :ASYNC_THROUGH写模式
在Alluxio写缓存之后,首先创建了文件之后 ,在第二步写了Alluxio缓存;在第二步缓存写完之后 ,Worker就向客户端返回成功;然后由客户端向Master提交文件。注意在这个时候 ,Worker还没有去UFS创建这个文件,也没有向UFS写文件 。在Alluxio向客户端返回请求成功之后,在之后的某个时间,由Job Service把这个文件创建到时里面 ,并且持久化。
需要注意 :在异步的模式下 ,持久化由于某些原因失败了 ,比如Alluxio成功之后,突然有人直接向里面创建了一个同名的文件,在第四步的时候 ,由于缓存和之间产生了不一致,导致这个文件无法创建、无法写入 。这个时候 ,Alluxio会有不一致的问题 ,此时需要人工介入来解决这个冲突。
6. 读文件流程
前文介绍以上四种不同的写模式以及一致性保证,现在来看Alluxio的读文件流程。读文件也可以粗略分成两种 :冷读和热读 。
图12 Alluxio :读文件流程
简单来说 ,冷读情况下Alluxio不知道这个文件,需要从加载元数据和数据。热读的时候,缓存命中,不需要加载元数据和数据 。
① 冷读文件
图13 Alluxio:冷读文件
在冷读流程里 ,客户端向Alluxio请求元数据,此时Master还没有这个元数据,所以会向UFS发出一个请求,并从UFS加载这个元数据,这也称之为元数据同步。
在客户端具体读数据的时候,客户端找到Worker,Worker此时还没有缓存,于是Worker会向UFS做缓存的加载 ,这就是常说的缓存冷读。在做完这两个步骤之后 ,缓存是和元数据一致的 。
② 热读文件
图14 Alluxio:热读文件
在热读的情况下,元数据可以在缓存里面找到 ,数据可以在Worker里面找到;此时不会有对UFS的读请求。
在缓存命中的时候,如何保证缓存与和是一致的?这里包括元数据的一致和数据的一致 。这个简单的来说 ,就是通过Alluxio的元数据和数据的同步机制,也就是下一部分的内容 。
--
04
Alluxio和UFS元数据和数据同步
1. 检查Alluxio元数据/数据一致性
首先考虑这个问题:在什么时候需要检查Alluxio的元数据和数据的一致性?
首先在写数据的时候需要检查 。如果这个文件在已经存在了 ,作为缓存,除了放弃这个操作之外,也没有其他的选项。因为不能在用户不知情的情况下 ,覆盖掉用户在里面的数据 。
在读数据的时候,同样需要考虑如果文件在里面已经更新了 ,那缓存也需要对应进行更新,需要UFS考虑里面的文件是否发生了变化。
图15 检查Alluxio :元数据/数据一致性
2. 保证Alluxio元数据/数据一致性
元数据和数据的一致性分成两步来逐个讨论 。首先讨论如何保证Alluxio元数据和一致。Alluxio通过两种方式来保证:
图16 保证Alluxio:元数据/数据一致性
① 通过基于时间的假设
第一种是通过基于时间的假设,在里面的文件,在一段时间内它是不变的。判断方法是在每一次文件源信息 、元数据请求的时候 ,检查Alluxio的元数据是否足够新。
这个判断分成了两个不同的部分 :
- 在每一次请求的时候
- 检查这个元数据是否足够新
这种元数据的同步机制是惰性的 ,只有在请求的时候才会进行检查。这样设计的理念是尽量避免访问慢的操作、昂贵的操作 。这样的事情越少越好 、越懒越好。如果Alluxio所知的文件信息足够新 ,就假设Alluxio和UFS是一致的。如果不够新 ,就放弃这个假设再做一次同步;同时更新Alluxio里面的元数据。
② 基于通知
另外一个思路就是抛弃基于时间的假设 ,基于通知,依赖文件更改的告知 。这个不是假设,是一个确定的信息。如果没有通知文件有变化,就确定Alluxio和现在的文件是一致的。
其次是如何保证Alluxio和UFS的数据保持一致 。思路也非常简单 :保持数据的一致,只需要确定元数据及是否一致 。这里做出的假设是 :如果UFS的数据 ,文件内容有所改变 ,那这个改变一定会反映在文件的元数据上 。要么是文件的长度改变,要么是这个文件的Hash,也就是哈希值会发生改变。通过观察这个Alluxio和的元数据,可以发现这些变化点。
如果基于这个假设,Alluxio的元数据和UFS保持一致时缓存和UFS也会一致 。如果观察元数据发现内容有变化,那么就更新元数据并抛弃已有缓存 。在下一次读的时候 ,重新加载缓存 。如果发现文件的内容没有变化,只做必要的元数据更新 ,不抛弃数据缓存 。
3. 数据同步机制
Alluxio提供两种同步机制,这里先介绍时间戳机制,再介绍基于消息的同步机制。
① 基于元数据时间戳的同步机制
下面先看一下第一种机制,基于元数据时间戳的同步机制。
图17 基于元数据时间戳同步
时间戳主要是通过配置项alluxio.user.file.metadata.sync.interval,通常称之为sync.interval或者interval。比较同步数据上次同步的时间戳和配置项 。配置项中有几种不同的配置方式:
- 配置为-1,就只在第一次加载的时候进行同步,之后就永远不再和这个底层存储去做同步了;
- 配置为0 ,每一次访问的时候,都会进行同步,抛弃所有知识 ,不做任何假设 。
- 比较常用的一种是用户指定一个配置值,做一个假设,如果上一次同步的时间还没有超出这个时间 ,就假设原信息是新的。这个假设是基于对这个文件的了解 ,比如:知道文件的来源,知道文件由哪些业务流程产生的 。在此基础上,可以做一个合理的推断:只需每隔照一段时间去检查的就足够 。如何进行配置,请看下图。
图18 元数据同步的开销
- 这张图展示了不同的配置方式带来什么样的行为。这张图的纵轴是一个RPC完成所需时间。不同的环境、不同测试方式都会得到不一样的结果。主要看相互之间的大小关系 。如果把interval设成零,就是每一次都同步 ,那每一个RPC都会有元数据同步的开销 ,延迟会比较高;
- 如果interval设为-1,完全关掉同步 ,只要文件存在于Alluxio里面,完成第一次加载,之后的每一次 ,RPC都会有一个低的延迟 ,因为没有做的同步操作。
- 最常用的是设置成一个适中的值,实际上带来这样一个结果,就是如果还在interval之内 ,RPC能得到快速的返回;如果达到了interval ,触发一次同步,那在下一次RPC就会有一个比较高的开销 。通过这样的方式 ,实际上把这个元数据同步的开销,平均到每一次的RPC请求中。这也大家比较喜欢的方式,因为取得了一个平衡。
② 同步时间间隔配置
图19 同步时间间隔配置
这个时间间隔具体配置有三个 ,优先级是由低到高 ,后面的配置可以覆盖前面的配置。
- 第一种最基础是配置文件/环境变量方式,是最不灵活的方式 。因为整个集群里面文件都不同,假设也不同,很难使用同样间隔 ,所以一般都给一个默认值。
- 第二种方式是基于路径的设置做配置 。比如:通过管理员指令给这个Alluxio的/data_center1路径,添加property也就是sync.interval 。意思就是假设来自数据中心这个路径下的所有文件夹下面所有路径 ,都按照一个小时一次的时间间隔 ,在超过一个小时之后,才去再进行一次原文件的同步。这样的假设的根源就是:知道来自数据中心的文件,它的更新不是特别频繁,那可以做出这样的一个判断。
- 第三种方式是直接写在指令里面。ls是大家最常用的命令 。比如:对/data/tables下面的文件做ls操作的时候,给予一个大于零的值 。意思就是:如果因为不小心误操作或者多做了几次ls,不小心多敲了几次回车或者脚本写错 ,至少还有一个回旋的余地,不会一下子触发大量元数据同步的操作 。
Alluxio还提供一些语法糖指令 ,比如:loadMetadata指令就是专门为了触发元数据同步 。如果加上-F选项 ,实际上的意思就是把sync.interval设成0,相当于强制进行一次元数据的刷新。ls和metadata这两个指令的区别:ls把文件展示在面前,ls的RPC有网络开销 , 会把信息发给你,客户端要保存下来,并且展示出来。这个不是每一次都需要 ,假如只是想要触发一次元数据的同步,只需loadMetadata就可以,返回值只有成功或者失败,可以节约很多网络带宽和的内存开销 。
③ 基于消息的同步机制
以上是基于时间的同步机制,下面看一下另外一种思路,就是基于消息同步机制 。