直播回顾:如何使用 JuiceFS 优化 Kylin 4.0 的存储性能
本篇是 2021 年 1 月 30 日 Kylin Meetup 的直播回顾,主要介绍 JuiceFS 如何优化 Kylin 4.0 的存储性能。完整 slide 请点击这里查看。
大家好,我是来自 Juicedata 的架构师高昌健,今天给大家分享的主题是「如何使用 JuiceFS 优化 Kylin 4.0 的存储性能」。
首先看一下今天分享的提纲,主 要分为几个部分。首先对 Kylin 4.0 以及它在云上面临的挑战进行简单介绍,然后详细介绍一下 JuiceFS 是什么以及为什么我们要在 Kylin 中使用 JuiceFS,最后是一些 benchmark。
Kylin 4.0 架构简介
我们先来了解一下 Kylin 4.0 的架构。这是 4.0 的架构图,Kylin 4.0 摒弃了之前基于 HBase 的架构,改为使用 Spark 作为构建以及查询引擎,并且使用 Parquet 作为 Cube 的存储格式直接写到 HDFS 或者对象存储上。Kylin 4.0 的架构不仅性能上有大幅提升,也更加贴合现在云原生的部署方式。
这里想重点讲一下有关存储这块儿的改动,新的存储方式帮助 Kylin 真正实现了存储与计算分离的架构。传统大数据的架构都是存储与计算耦合的,也就是说没有办法单独对存储或者计算资源进行扩缩容。但是在云上使用对象存储替代 HDFS 作为大数据平台的底层存储已经越来越成为主流,也就需要上层计算组件的架构更加灵活。
Kylin on Parquet 在云上的挑战
但是我们也看到云上的对象存储或多或少都存在一些问题,也就为 Kylin 4.0 在云上部署和使用带来了一些挑战。这里我们来看一下对象存储的具体问题。首先很多时候大家会误以为对象存储是完全等价于 HDFS 的,但其实不是,在很多维度上对象存储和 HDFS 都是不一样的。
比如一致性模型,HDFS 是保证强一致性的文件系统,但是对象存储往往是最终一致性,也就是说当你往对象存储中写入新的数据以后并不能立即看到,有可能需要等待一段时间,并且这个等待时间对于用户来说是不可控的,最终一致性会给大数据平台的计算任务带来很多的不确定性,影响任务的稳定性。
然后是元数据操作性能。在大数据场景常见的一些元数据操作,比如 list 是列举某个目录有什么文件、rename 是重命名、delete 是删除,这些元数据操作在对象存储上性能都是不太好的,本质上是因为对象存储是一个 K/V 存储,没法高效实现刚才提到的这些元数据操作。这里比较明显的操作是 rename,因为很多对象存储都不支持重命名,因此真正底层实现的时候都是通过先拷贝原始数据到新的路径,再删除原始数据的方式来实现。这种实现在大数据这种大批量处理数据的场景对性能的影响会比较明显。
然后是数据本地性。Hadoop 因为是存储计算耦合的架构,因此当计算任务被调度时会尽量把计算任务分配到数据节点上,这样就可以提升数据读取的性能。但是对象存储并不提供这样的能力,所有数据都必须通过网络获取,这对于对象存储的带宽有很高的要求,也会对计算的性能造成影响。
对象存储还有一些隐性的 点可能不一定被大家关注。对于一个 bucket 中的每个路径其实都有最大的 API 请求频率限制,如果计算任务发送的请求超过了这个限制,就会报错,当然我们可以通过重试或者降低请求并发的方式来一定程度上缓解这个问题,但都是治标不治本,并且也会给业务带来很多不便。对象存储的 API 请求也不是免费的,大数据场景很容易产生大量的请求,这些都会带来一些成本。
Hadoop 兼容性也会成为一个问题,Hadoop 生态具有繁多的组件,不同云厂商的对象存储也可能都会有一些差异,导致上层组件在接入时不一定能完全兼容,甚至出现组件无法正常使用的情况。
JuiceFS 简介
针对刚才提到的这些问题,因此我们开发了 JuiceFS 这样一个项目。这里简单介绍一下 JuiceFS。JuiceFS 是一个开源的云原生分布式文件系统,从今年 1 月初开源以来已经在 GitHub 上积累了超过 2700 个 star。JuiceFS 创新性地基于 Redis 和对象存储构建,同时提供传统文件系统的诸多特性。
例如强一致性,利用独立的元数据管理以及 Redis 的事务特性,JuiceFS 能够确保数据的强一致性。目前 JuiceFS 已经支持市面上几乎所有的对象存储,不管是公有云上的,还是开源对象存储系统,因此对于机房用户来说也是非常友好的。
除了兼容 HDFS 接口以外,JuiceFS 还提供了标准的 POSIX 接口,这是目前 HDFS 以及对象存储都不支持或者支持得不好的一块儿。借助 FUSE 你可以像 使用本地文件系统一样把 JuiceFS 挂载到机器上,再通过标准的命令行工具读写数据。此外 JuiceFS 还支持 S3、NFS、Samba 等协议,多种协议的支持使得只用 JuiceFS 一个文件系统就可以同时满足多种业务场景,而不用在不同存储之间重复拷贝数据。
用户访问 JuiceFS 是通过特定的客户端,这个客户端本身也是支持多系统的,包括 Linux、macOS、Windows。在 CPU 架构支持上,除了 x86 以外也支持现在应用广泛的 ARM 平台。
JuiceFS 也提供 K8s CSI 驱动,也就是说在 K8s 平台上你可以很方便地把 JuiceFS 挂载到容器里,这个挂载的 volume 是支持多个容器同时读写的(ReadWriteMany)。同时你也不用担心挂载的 volume 它的一个生命周期,都是由 JuiceFS 的 CSI 驱动来管理,自动地创建、销毁。JuiceFS 的 CSI 驱动很好地满足了容器平台共享存储的需求。
JuiceFS 也具备数据缓存的能力,可以把远端的数据,也就是对象存储的数据缓存到本地,这个后面在介绍 benchmark 的时候会再详细说明数据缓存的能力。最下面是 JuiceFS 的 GitHub 链接。
这是 JuiceFS 的架构图。结合刚刚讲到的,JuiceFS 在最底层依赖的是对象存储来作为最基本的数据块存储。JuiceFS 不是原封不动地把数据存储到对象存储上,所有通过 JuiceFS 写入的数据默认会按照 4MiB 一个块来分块,例如写入一个 100MiB 或者 1GiB 的文件会按照 4MiB 这个粒度来切分,切分以后再存储到对象存储上。之所以这样设计很大程度上是因为希望对大文件进行小的分块可以提升读写的吞吐和性能,相 比直接一个大文件读取或者写入的话,拆分成小的块之后读写的性能提升还是蛮明显的。
然后在图的左边可以看到是一个 Redis,这里是把 Redis 作为元数据的存储,相当于所有文件系统的元数据都会存到 Redis 中。JuiceFS 依赖 Redis 的事务特性保证所有元数据的操作的原子性,也就是保证元数据的强一致性。
然后右边是 JuiceFS 的客户端。JuiceFS 已经提供了多种客户端,比如 FUSE 是通过挂载来提供 POSIX 接口;Java SDK 是在 Hadoop 环境中使用,我们近期也已经将 Hadoop SDK 开源出来;最右边的是 S3 Gateway,通过这个 gateway 可以对上层提供 S3 兼容的接口。可以看到 JuiceFS 提供了多协议的客户端,这些客户端底层都是共享了同一个实现,也就是图上的 Client Core。除了 Java SDK 以外,客户端整体上都是用 Go 语言实现,Java SDK 底层也是通过 JNI 去调用 Go 语言的接口。
前面提到了数据缓存,意思就是说当从对象存储读取数据时,client 端会自动地把数据缓存到本地配置的某个缓存路径,JuiceFS 也会自动地管理缓存空间,比如说当缓存盘满了之后应该怎么去失效、怎么去维护缓存数据的生命周期,以及保证缓存数据与对象存储之间的一致性。这些都是由 JuiceFS client 端提供的特性。
为什么 Kylin 和 JuiceFS 要一起使用?
下面一个问题就是为什么 Kylin 要和 JuiceFS 一起使用呢?经过前面的介绍,大家可能也或多或少能够想到一些,本质上希望通过 JuiceFS 来解决刚才提到的对象存储的各种问题。所以具体来说 JuiceFS 能给 Kylin 带来的收益有这么几个。
首先是强一致性,也就是相比对象存储的最终一致性来说 JuiceFS 是一个强一致性的文件系统。然后是高性能,JuiceFS 不管是在元数据还是数据的读写上都有很好的性能表现。元数据是基于 Redis 的,Redis 因为是全内存的 K/V 存储,所以元数据操作的性能是非常高的,同时 JuiceFS 在 Redis 之上也做了一些优化。数据的读写 JuiceFS 默认是按照 4MiB 的块来分块存储,在读写上也有蛮多的性能提升。
然后数据本地性也是 JuiceFS 能够带来的一个好处,通过缓存可以把数据缓存到计算节点上,虽然计算节点的缓存空间不一定特别大,但一定程度上也提供了数据本地性。这块儿的具体实现是通过 Java SDK 提供了缓存数据的 location 给上层的调度器(比如 YARN),使得调度器能够知道缓存数据当前是在哪一个计算节点上,可以在调度时把相应的计算任务调度到具有缓存数据的节点上,也就一定程度上实现了数据本地性。即使调度失败了,也有一些进一步的优化,例如在不同计算节点之间组成一个分布式缓存系统,也就是节点互相之间是能够读取对方的缓存数据,对数据读取进行优化,而不是完全依赖底层的对象存储的吞吐和性能。
JuiceFS 同时也是完整兼容 Hadoop 生态的,不管是对于 Hadoop 2.x 和 Hadoop 3.x 这种大版本的变化,还是对于某个 Hadoop 发行版的所有组件都是完全兼容的。因为 JuiceFS 本身提供的就是一个标准的 HDFS 接口,对于上层的组件来说基本上是透明的,也没有任何侵入,就可以当作是 HDFS 来使用。
TCO 低也是 JuiceFS 的一个好处。前面提到对象存储的 API 请求是收费的,这些 API 请求中涵盖了很多元数据的请求, 当使用 JuiceFS 之后这些请求会直接发给元数据服务(也就是 Redis),也就不存在元数据请求的费用。数据的请求依靠比如缓存这样的特性很大程度上减少对对象存储的依赖,整体上来说成本都能有一定优化。
JuiceFS 还支持一些 HDFS 或者对象存储可能不提供的功能。比如快照,快照的意思是可以针对某一个目录创建一个 snapshot,看起来好像是对数据进行了一次拷贝,但底层的实现其实是不存在拷贝的。本质上是一个 copy-on-write 的原理,在创建快照时只是对元数据进行了拷贝,底层是共享的同一份数据,并没有拷贝对象存储上数据。对你对快照进行修改时,就会将原始数据进行拷贝,然后存储修改后的数据。这个特性对于很多场景是非常有帮助的,比如测试、多版本管理。
符号链接在操作系统中是一个常见的功能,通过符号链接可以将一个文件指向另一个文件的路径。在 JuiceFS 上也实现了这个功能,不仅可以将 JuiceFS 的路径映射到另一个路径,也可以链接到任何 JuiceFS 支持的存储上(比如 HDFS、对象存储)。通过 JuiceFS 实现一个统一的文件系统命名空间管理,可以在这一个命名空间中看到多种存储的数据。这其实是一个蛮有用的特性,比如我们对数据进行迁移时是非常有帮助的(可以参考之前的一篇文章)。
本身 JuiceFS 是一个开源的项目,但同时我们也提供一个云上全托管的版本,会将元数据服务进行统一管理,也有一个 web 的控制台可以让大家很方便地使用。这是 JuiceFS 商业版提供的一个功能。
性能比较
然后分享一下之前做的一个 benchmark 结果。先看一下测试环境,这里用了标准的 10GB 大小的 TPC-H 作为数据集,用了 1 台 master,3 台 worker,大概是这样的配置。对比的是 Kylin on OSS 和 Kylin on JuiceFS 的性能。
这是最终 benchmark 的对比图,黄色的是 OSS,绿色的是 JuiceFS,图上展示的是 TPC-H 每一个查询的执行时间,这个时间越低越好。
总结一下刚刚提到的测试。因为 Kylin 需要提前构建 cube,我们在测试时遇到 Kylin on OSS 这个方案构建 cube 直接失败了,导致没有办法在 OSS 上构建 cube。最终是通过把 Kylin on JuiceFS 构建完的 cube 拷贝到 OSS 上完成的测试。总的查询时间上 JuiceFS 快 38% 左右,然后看单个查询的执行时间 JuiceFS 最多能够快 85%,平均下来也能快 46% 左右。
未来展望
最后想展望一下未来 JuiceFS 的一些新特性。JuiceFS 已经在性能上进行了很多优化,前面的 benchmark 也能看到,但其实也有很多细粒度的点我们可以优化。比如说查询预读,可以从 JucieFS 的角度预先知道某个查询接下来会读取的文件,也就可以自动地在后台进行一些预读的处理,进一步加速查询的效率。刚刚提到的 P2P 分布式缓存特性也会是接下来在开源版本的 Hadoop SDK 去优化的一个点。最后 JuiceFS 会提供一个 profiling 的工具,这个工具是为了帮助用户更快更简便地调优,发现性能热点,从而针对性地对读写进行优化。
今天的分享就到这里,感谢大家。