导读:本期分享一种新的存储方式。过去十几年大数据场景中存储层使用最多的是 HDFS,然而最近几年面对存算分离场景又无一例外选择了对象存储。为解决对象存储在大数据平台实际开发中遇到的问题,从 2017 年起我们启动了 JuiceFS 项目。
01 Data Lake 和 Lakehouse 从何而来?
1. 什么是 Data Lake
Wikipedia 上关于 Data Lake 的介绍是:A data lake is a system or repository of data stored in its natural/raw format, usually object blobs or files.
这段话的核心是 nature/raw format。Data Lake 和传统数据仓库的不同点是 Data Lake 能够存储更原始格式的数据,可以存储结构化、半结构化、非结构数据。而传统数仓需要将数据汇聚起来,并且在不同业务线上构建不同的数仓。
今天我们希望把更多的原始数据汇聚到一起,解决数据孤岛、多业务多格式数据统一等问题。为了实现高效存储,存储计算分离成为必然;为了更快地看到数据,需要将数据先入湖,将 ETL 环节后置。
2. 什么是 Lakehouse
2020 年,Databricks 公司提出了 Lakehouse 概念,意思是 Data Lake 不是想替代掉数仓,而是想成为一家人,所以数仓仍然存在,只是后置了。传统的数据仓库需要做 T+1 的 batch ETL 工作,导致数据仓库生产的数据具有滞后性。随着技术升级和业务发展,我们需要更实时地处理数据、更高效地分析数据。由于机器学习和深度学习所需的数据可以存放于数据仓库或者数据湖中,因此数据平台不仅要服务于 BI 和报表,还要实现大数据场景和 AI 场景的数据融合。Data bricks 公司针对 Lakehouse 引入了 ACID 事务、多版本数据、索引、零拷贝等特性,这些常出现在数据库领域里的特性在 Data Lake 阶段是没有提及的。所以 Lakehouse 对存储的要求更高了。
02 HDFS 与对象存储适合么
1. 存储系统比较
无论业务架构怎么构建,最底层的存储系统常见选项是 HDFS 和对象存储。针对这两种存储进行比较,左侧是比较的维度。
单个 namespace 的存储规模,HDFS 通常做到亿级,行业实践中单个 HDFS 集群 3 亿以内的文件运维是比较轻松的,5 亿以上需要考虑到 Federation(联邦)机制。但是对象存储在私有化部署中可以达到千亿级,公有云可以达到万亿级的对象规模。
在一致性方面,HDFS 是强一致性的文件系统,对象存储大部分则是最终一致性。最终一致性在 ETL 过程中会引入一些问题,这也是应用开发者会忽略的部分,后面会再具体解释。
容量管理方面,HDFS 是手工运维的,需要手工的容量规划、扩容;对象存储是弹性的,没有了容量规划的过程。
原子重命名能力,在文件系统中这是个基本功能,但对象存储里没有。后面会再讲到在对象存储里如何实现最简单的对象或者目录改名的功能,很多应用开发者并没有关注过这个差异。
List 的性能,在文件系统里执行 ls 命令是日常操作,在对象存储中执行该操作性能会差 100 倍。
在随机写上,HDFS 和对象存储都不支持随机写。
缓存加速,在这两个系统里也都默认没有带。
在运维复杂度上,对象存储在公有云上不需要运维;HDFS 的运维复杂度较高,经过十几年迭代后,虽然已经积累了很多经验和优秀实践,但是仍然需要专业的、经验丰富的运维人员来维护。
在 HDFS 兼容性上,HDFS 本身兼容性很好;对象存储其实在适配整个 Hadoop 生态时是有一些痛点的。今天我们看所有公有云上的半托管的大数据服务,比如各家提供的 EMR 或类似产品里能提供的计算组件是非常有限的。尽管 Hadoop 上层组件很丰富,有几十个,但是大部分公有云上提供的只有 Spark、Hive、Presto 等,Impala、Trino 等计算组件公有云大部分都没有提供适配,提供的 Spark、Hive 和 Presto 的版本也是有限的,这是因为这些云厂商在上层计算引擎和自己的对象存储之间的 Connector 及引擎之间做了深度的修改工作,导致云厂商在对接每一种引擎和对象存储时工作量较大,没法去对接所有引擎,无法跟进所有社区版本,这就会给上层组件的选择带来限制。使用社区的 Hadoop 的生态还是云厂商提供的生态,不同公司有不同的选择。
在结合机器学习场景时,POSIX 接口是一个必要的访问方式。机器学习和深度学习框架,如 PyTorch、TensorFlow 等对 HDFS 的 API 兼容并不好,对 S3 能够兼容,但性能上不能满足模型训练的需求,因此在训练时,通常需要将数据先拷贝到另外一个 POSIX 的文件系统里,再去跑训练,这增加了系统使用复杂度。
2. 存储系统的阿克琉斯之踵
比较了两个存储系统之后,再讲一下它们的共性问题--阿克琉斯之踵。这来源于一个希腊神话,阿克琉斯是一位非常厉害的战士,他唯一的“死穴”是脚后跟。和阿克琉斯一样,强大的 HDFS 和对象存储都存在各自的致命弱点。
3. HDFS 阿克琉斯之踵--NameNode
HDFS 最大的弱点是 NameNode。HDFS 最开始设计时并没有考虑到今天的数据规模,NameNode 一直以来都是垂直扩展方案。在 Scale Up 的扩展模式下,随着单个 HDFS 集群、单个 NameSpace 文件数量增加,NameNode 内存开销也在增大。由于单个节点内存是有上限的,因此常用的解决方法是增加内存,例如将内存扩容到 1TB。尽管听起来可行,但是 1T 内存的 NameNode 运维是很恐怖的,Failover 需要耗时两到三个小时。因此,在高可靠性、高可用性场景下,NameNode 是不会做到这么大实例的。
业界常用的解决办法是:联邦架构 1.0,ViewFs+多集群;联邦架构 2.0 ,Router-based Federation(RBF) 联邦方案。这两种方案表面上解决了横向扩展难题,对用户而言并不是完全透明的。针对路径感知、扩缩容感知等情况,仍然需要知道 Router 的细节。因此,并没有透明的、简单的横向扩展方案实现 HDFS 单个 NameSpace 对 10 亿、100 亿文件的管理。
4. 对象存储阿克琉斯之踵--元数据
对象存储的弱点是元数据管理。比如,如何实现文件目录改名?如果在硬盘上或者文件系统中执行 mv 命令则可以把目录改名,在文件系统执行这个命令的过程中是原子操作,只需要找到这个目录名字对应的 inode 改名即可。在对象存储中没有原生目录,只是把目录路径看作一个字符串作为对象的 key,所有对象都是平铺的,彼此之间没有任何关联的数据结构。在对象存储中保存对象要先创建一个 bucket,这个 bucket 的名字其实非常形象,意味着装进去的那些对象没有任何层次结构。
假设以 /foo 前缀的 key 有 100 万个对象,如果要想把 /foo 改个名,会对元数据做全量索引搜索,找到所有以 /foo 为前缀的对象,可能是 10 个、10 万个、100 万...取决于 key 的命名,找到全部符合 key 前缀的对象后做一次完整的 IO 拷贝,数据可能是 1G、100G、100T...取决于这些对象的数据量,使用新名字拷贝为一批新的对象完成后,再去删掉旧的对象。这就是在对象存储里面完成一个 rename 的过程。
Mv 这个命令在文件系统里面是个原子操作,在对象存储中显然不是原子操作,它有多个步骤,并且这些步骤的执行是没有事务保障的,实际上这个过程是分阶段完成的。因此可能会引起两个问题:第一,假如运行到一半出现宕机,重启后只能去重新执行 rename 操作,重新拷贝、重新删除,时间代价是很高的;第二,机器没挂,但是过程中消耗 IO 量比较大,需要几秒、几分钟、几小时、甚至更长的时间,在这个过程中若别的进程同时在访问数据,则会引起数据不一致的问题,这也是对象存储大多是最终一致性的原因。对象存储仅仅对最终一致性负责,过程中的耗时情况、开始时间、结束时间等对开发者也是不可见的。运行在这个过程中的问题是无法复现的,因为当我们想复现的时候,过程已经执行结束了。
什么时候要改名呢?举个 ETL 的例子。比如在执行 Spark 任务时,在进行 map reduce 计算时,有一些分支结束后会先提交到临时目录中,当所有的 worker 都结束时,最后要将临时目录改名。在 HDFS 里最后提交的过程是可以忽略不计的;在云上用对象存储运行同样的任务,commit 时可能会持续很长一段时间,就是 rename 过程的开销有很大不同。
执行 ls 命令的效率在不同存储系统上也有质的差别。大家会有意识的不在单一目录下放太多文件,比如某个目录下有 100 万文件运行 ls 时会卡、1000 万文件运行 ls 会卡更久。
在对象存储里是什么样的?在文件系统中,整个目录树对应了一个树形的数据结构,在对象存储中所有对象是平铺的,没有树型数据结构,会把所有的 key 的字符串前缀做个全文索引,上图展示了针对不同文件数量在对象存储 S3 中执行 listing 带来时延的差别。在 Hudi 文档测试中,100 个文件 50ms、100K 个文件 9s,在对象存储里面对大量对象做 listing 遍历时,性能代价是不能忽视的。为了解决这个问题,Hudi 提出了 Metadata Table 改进方案用于独立记录所有元数据,因此不需要在对象存储里直接调 ls 了,使用 Metadata Table 会更快。
在公有云对象存储上,所有访问都是通过 HTTP API 调用的,并有 QPS 限制,以 S3 举例,它在每个前缀上默认的 get 请求 QPS 是 5000,put 请求 QPS 是 3500,如果达到 QPS 上限则返回 400 错误码,但是它会在一段时间后使用前缀进行再分配,慢慢地增加这个 QPS,这可能是在系统没有达到一定负载程度下开发者难以注意的问题,等碰上了想解决这个问题时,就会发现已经很困难了,因为整个 prefix 前缀可能已经按照业务最理想的表达方式写过了。为了解决这个问题,在 Iceberg 里做了优化,设定一个参数声明一下,如果是对象存储,可以 enable 这个开关,后面也会进一步讲到。
5. Lakehouse 对文件系统的需要
Lakehouse 文档中,对文件系统提出三个具体的需求:一、原子语义,比如上文提到的对象存储上 rename 是没有原子语义的,但希望 Lakehouse 下面的存储系统是有原子语义的;二、并发写的能力;三、强一致性的能力,比如上文提到的在对大量对象改名的过程中做 listing 出现不一致的情况是不符合一致性要求的。
03 JuiceFS 的设计
2016 年,我们有了做 Juice FS 的想法。当时在 Databricks 内部的 S3 存储平台上跑批大量的大数据任务时碰到了很多痛点,当时还没有提出 Data Lake、Lakehouse 等概念,因此我们开始思考能否把文件系统的能力和云上 S3 的优势结合起来。比如文件系统里面有原生的目录树、有一套使用方便的 API、有强一致性保障等;S3 有弹性伸缩、免运维等好处。于是我们提出将这两种存储系统的优势在云上融合,为开发者提供更适合大数据使用的存储方案。
1. 什么是 JuiceFS
实际使用中,S3 虽然解决了一些规模上的瓶颈,但还有元数据性能差、没有原子 rename、没有强一致性保证、并发写入限制、API QPS 限制、API 调用成本高等痛点。比如,将一个 HDFS 的集群搭起来后,无论如何使用它,都不会为每一个 request 付钱,但是 S3 上的每一个 request 是要交钱的,并且费用是不同的,分两类计费。其中 Listing API 是 Get API 价格的十倍。在 ETL 的过程中需要大量使用 Listing API 做数据发现,成本极高。如何解决客户使用对象存储的痛点、难点是当时和今天仍然迫切的需求。
无论是 Data Lake,还是 Lakehouse,还是其他的概念,底层都需要有一个更好用、更适合的文件系统,才能解决今天这个数据规模上的各种痛点。
从 2017 年起,JuiceFS 项目启动,当时还不是开源的,以服务云上客户为核心,通过云服务方式做了 6 年。在这个过程中,我们发现不仅大数据上有文件存储需求,其他很多场景也有同样的需求,因此我们在 2021 年发布了开源社区版,为开发者提供了更开放的选择,在不同的场景中可以做一些自定义组合。不论是商业版还是社区版使用的用户都有很多,主要围绕着 AI 和大数据这两个场景。
2. JuiceFS 架构
社区版架构图如上图左所示。图中三个虚线框分别代表了文件系统中最主要的三个核心元素,左下角是元数据引擎、右下角是数据持久化引擎、上面是用来访问数据的客户端。这三个组件和 2005 年 Google 发表的第一篇 Google File System 论文里的顶层架构是一样的。那篇论文的第一个开源实现就是 HDFS,同样在这篇论文的方向的指导下,我们设计出了更适合云环境使用的 JuiceFS。
JuiceFS 与 HDFS、Ceph 等其他分布式文件系统的区别是什么?数据持久化这一层以前都是面向很多节点、很多磁盘的,需要先将这些节点和磁盘做一个管理服务,包括数据写入方式、副本配置方式、IO 读写路径等,类似 HDFS DataNode 的角色。JuiceFS 的实现并不是这样,它借助公有云已经提供的对象存储作为基础设施。
对象存储管理海量硬盘的能力已经很强大了。我们将 S3 或者其他对象存储看成一个无限容量、弹性伸缩的大硬盘,其中 Metadata Engine 相当于这块硬盘的分区表,比如当我们的电脑需要增加硬盘时,首先要在电脑插上一块硬盘,然后格式化一个分区格式。Metadata Engine 就相当于格式化出来的一个分区表,用于管理文件系统里面目录树、文件名、时间戳等信息。
JuiceFS 的 Metadata Engine 引擎与以往的分布式文件系统最大的不同是,JuiceFS 是一个插件式架构,支持十几种不同的开源存储引擎,比如 KV 存储、关系型数据库等。JuiceFS 的 Metadata Engine 这样设计可以借助已成熟的存储引擎为社区的开发者降低使用门槛,因为在社区中推广自研的分布式引擎并让用户理解、掌握、积累运维经验的过程是很漫长的,而如何运维好 Redis、MySQL 已经有非常丰富的经验,而且在云上有托管服务,不需要用户自己运维。
尽管这些引擎经过了十几年的验证、成熟稳定,但是它们作为文件系统使用不一定是最优的,要结合使用场景来看。JuiceFS 在开源之前已经做了三年多的商业化,在此期间我们发现市场上对于文件存储的应用有着非常广泛的需求,不只是大数据和 AI 场景。
在不同的场景上,大家对于文件规模、容量、性能、可靠性、可用性、成本这些维度的优先级排序是不同的。我们认为架构的选择上没有银弹,没有哪一款引擎能够完美解决所有场景需求,因此我们做一款更开放式的插件引擎,用户可以结合自己场景、经验,选择一个合适的存储引擎做 JuiceFS 的 Metadata Engine。
Data Storage 持久化也是插件式架构,兼容了现在市场上所有的对象存储服务。
Client 客户端与以前的文件系统的差别在于 JuiceFS 兼容了三种最主流的标准:首先,提供了 POSIX 的完整兼容,POSIX 从上个世纪 80 年代开始迭代到现在,是文件系统标准的最大集,因此完整地支持 POSIX 标准就意味着已经能够和过去 40 年的各种应用系统直接兼容;其次,提供了 Java SDK 完整兼容 HDFS API。在大数据生态里面中只有 POSIX 是不够的,JuiceFS 同时兼容 HDFS 的 2 和 3 版本;最后,提供了 S3 API 兼容,S3 从 2006 年发布到现在也已经积累了大量用 S3 API 写的代码。这样,JuiceFS 完美解决了更换存储系统需要改一遍代码的难题。
关于数据存储在 S3 上面的性能优化问题,JuiceFS 在客户端里面做了缓存层。意味着在大数据、AI 读场景下,所有读到的数据都可以在客户端建立缓存,下次再读同样数据的时候就可以在缓存中找到了,在缓存层可以用 SSD 做性能提升。因此,对于数仓查询、AI 模型训练等场景,读缓存发挥着非常重要的作用。
JuiceFS 内部的功能实现如上图右侧所示。很多用户会问 JuiceFS 和 Alluxio 对比有什么区别?
Alluxio 的定位是在现有的存储系统之上提供缓存加速层,在实际项目中存储系统大多是对象存储系统;JuiceFS 的定位是为云环境设计的分布式文件系统,可以通过缓存机制加速数据访问。在使用 JuiceFS 时,写入文件与 HDFS 类似,会做大文件拆分,将文件拆分后的数据块存储在对象存储中,一般按 4M 的 block 块存储在对象存储里。这样的架构设计提供了完整 POSIX 兼容、数据存储强一致性、覆盖写和追加写、缓存一致性保障等核心能力。JuiceFS 是一个文件存储的服务,而不是透明的缓存加速层。
3. 存储系统比较
对 HDFS、对象存储、JuiceFS 进行对比,如上图所示。
宏观上 JuiceFS 在单个 namespace 下可以存百亿级的文件。不过 JuiceFS 支持了十几种不同的元数据引擎,并不是每个引擎都能存百亿。比如,TiKV 的社区用户实践有很多过百亿的,我们自研的商业引擎也可以,但是 Redis 和 MySQL 不能。
一致性上实现了 Read-After-Close 强一致性保障。
容量管理上,基于 S3 的底层存储是弹性的。在完整的 POSIX 兼容情况下,原子重命名、List 的性能等都得到了保障。
在兼容性方面也做了很强的优化。通常兼容性是选择新系统时需要考虑的重要事情,即不用修改代码即可引入新系统。
4. JuiceFS 与 HDFS、对象存储元数据性能比较
JuiceFS 与 HDFS、对象存储元数据性能比较,如上图所示,比较点有两个:吞吐和时延。在吞吐量上,JuiceFS 比对象存储和 HDFS 更有优势,在单机情况下或者同配置情况下 JuiceFS 可以获得更高 QPS。在时延上,JuiceFS 执行 rename 操作时,时延优势是最明显的,对象存储和文件系统之间相差近 100 倍。
5. JuiceFS 缓存加速与 HDFS 性能比较
基于 TPC-DS 数据集,测试 HDFS 和 JuiceFS 查询性能之间的比较,如上图所示。HDFS 具备存算耦合、数据本地性的特征,实现查询性能加速。存算分离以后会下降多少呢?TPC-DS 前 10 个 query 重复执行,即数据预热到缓存中时,JuiceFS 与 HDFS 的表现是一样的。
6. Lakehouse 对文件系统的需要
在 QPS 限制上,S3 有自己的限制,其他公有云也有类似的限制。Iceberg 的做法是,在对象存储场景中,在 key 前面去加一个随机哈希值。JuiceFS 自动将文件切分成很多 block 后,形成多级的 prefix,这个多级的前缀也能够提升 API 的 QPS。
除此之外,因为有缓存的存在,在数据读取时,可以优先命中缓存,进而降低了后端对象存储在存储和 QPS 的压力。其次,listing、rename 等操作只发生在 JuiceFS 元数据引擎中,不用再请求到对象存储,因此,JuiceFS 对下面对象存储的 API 依赖就只有 get、put、delete 这三个最基础 API 了。
04 用户案例分享
1. 用户案例-某上市集团 K12 教育业务
某上市集团 K12 教育业务数据平台。在数据湖场景上,通过 Hudi + JuiceFS 方式收集上游不同数据源 CDC 的数据。以前在没有数据湖情况下,ETL 是几小时的时延;引入数据湖后,将原始数据 CDC 入湖,数据马上就可以被查询引擎使用,数据时延从几小时缩短到 10 分钟。
2. 用户案例-豆瓣
豆瓣在 2019 年完成了将所有作业从机房迁移上云的过程。在机房中使用的是 MooseFS ,它也是一款支持 POSIX 的文件系统。上云之后没有选择重建 MooseFS,而是用了云上托管的 JuiceFS 服务。
由于两个 POSIX 文件系统之间很容易平移,所以它把日志、CSV、数仓的列存文件、算法等这些各种各样的数据都迁移了上来,并在数仓上增加了 Iceberg,与上一个案例类似,也是引入了数据库 CDC 采集,为豆瓣的一些算法提供更实时的数据访问。今天用户所有收藏、打分、点击等数据,都可以更快地进入数据湖里供给算法使用。
05 从 BI 到 AI
前文中分析了不同存储系统之间的差异。从业务角度来看,在过去 6 年中我们发现越来越多的数据平台将数据提供给算法团队使用,包括机器学习和深度学习。从最开始使用数仓解决搜索、广告、推荐等业务,数据更偏向于数值型,比如用户画像;到今天除了数值型以外,还有了更多非结构化数据,比如声音、文字、图片等等,都融入进来。
如何把这些非结构化、半结构化的数据统一地管理在一起?如上图右所示,以前的解决方案是使用几套不同的存储系统。
在机器学习中,数据 pipeline 上分几个不同的阶段,从最开始的数据产生,到预处理,再到特征工程、训练,最后到模型出来后能够做推理,整个过程中每一个环节都依赖于上游应用,每个过程都有用起来更舒服的数据访问 API 偏好。
比如,数据生成团队主要是把产生的数据尽快存下来,选用 S3 是最简单的方案。数据做预处理时,很多人使用 Spark 或 Ray 分布式计算框架进行数据处理。特征工程部分,大多使用 Python 写代码,将图片、声音向量化。到了模型训练时,PyTorch、TensorFlow 等训练框架都对 POSIX 支持的很好。
在这个过程中,以前需要从对象存储、文件系统等不同系统来回搬运数据,数据交接效率比较低。而 JuiceFS 支持数仓数据、原始数据、非结构化数据存放到一起,每一个环节需要的访问接口都可以直接对接上。
06 总结
最后,再回顾一下本次分享的核心内容。
JuiceFS 在大数据的生态里面提供了 HDFS 2 & 3 完整的兼容性。
在数据平台里面接入时,可以不用改代码而是直接通过改配置的方式接入进来,支持现存的 HDFS 和对象存储体系共同使用。
在路径上,比如 HDFS 路径是 hdfs://,到 JuiceFS 上是 jfs://。
在数仓中可以按表、按列指定不同的 location,在实际使用中很多的用户会先保持现有的 HDFS 不动,然后将 JuiceFS 作为温冷数据存储,温冷的数据 partition 在元数据里改变 location 即可,这个过程对上层的使用者是完全无感的。
在数据湖方面,JuiceFS 已经支持了 Hudi、Iceberg、Deltalake。
前面介绍的案例中,用户都是在保留原有数仓架构的情况下引入了 Hudi、Iceberg 等新的组件支持某些业务,进而更快地处理、呈现实时业务流数据。
当 AI 业务要做更深的数据融合时,JuiceFS 能够给大家带来更多的便利。