一、MaxCompute 平台存储引擎背景
阿里云大数据计算服务( MaxCompute,原名 ODPS )是阿里云提供的一种安全可靠、高效能、低成本、从 GB 到 EB 级别按需弹性伸缩的在线大数据计算服务。MaxCompute 不仅仅是一个计算平台,也承担着大数据的存储。阿里巴巴集团 99%的数据存储都基于 MaxCompute,总数量达 EB 级。MaxCompute 存储引擎处于 MaxCompute Tasks 和底层盘古分布式文件系统之间,提供一个统一的逻辑数据模型给各种各样的计算任务。
存储层最核心的部分就是文件格式。对于文件存储而言,有两种主流的方式,即按行存储以及按列存储。所谓按行存储就是把每一行数据依次存储在一起,即先存储第一行的数据再存储第二行的数据,以此类推。按列存储就是把表中的数据按照列存储在一起,先存储第一列的数据,再存储第二列的数据。而在大数据场景之下,往往只需要获取部分列的数据,那么使用列存就可以只读取少量数据,这样可以节省大量磁盘和网络 I/O 的消耗。此外,因为相同列的数据属性非常相似,冗余度非常高,列式存储可以增大数据压缩率,进而大大节省磁盘空间。MaxCompute 的存储格式演化,从最早的行存格式 CFile1,到第一个列存储格式 CFile2,再到新一代的列存格式 AliORC,经历了从行存到列存的转换。
二、什么是 Apache ORC 开源项目
AliORC 是基于开源的 Apache ORC 打造的列存文件格式,那什么是 Apache ORC?Apache ORC 是专为 Hadoop 生态系统打造的一款读写快、体积小的列式存储文件格式。它支持 ACID 事务,内置轻量级索引,以及支持各种复杂类型。有很多的开源系统采用了 ORC,比如 Spark、Presto、Hive、Hadoop、Impala 和 Arrow 等。
简单介绍一下 Apache ORC 项目的发展历程。在 2013 年初的时候,Hortonworks 和 Facebook 一起开发出 ORC 用来替代 Hive 中的 RCFile 文件格式。经过两个版本的迭代,ORC 孵化成为了 Apache 顶级开源项目,并且顺利地从 Hive 中脱离出来成为一个单独的项目。阿里云 MaxCompute 技术团队在 2017 年初参与到 Apache ORC 社区中,基于 ORC 打造了 AliORC 作为 MaxCompute 内置的文件存储格式之一。
阿里巴巴对 ORC 社区的贡献
阿里巴巴 MaxCompute 技术团队为 Apache ORC 项目做出了大量贡献,共提交了 30 多个 patch,总计 1 万 5 千多行代码。包括一个完整的 C++实现的 ORC Writer,以及一些重要的 bug fix 和性能优化。团队中一共有 3 名 contributor。在 2017 年的 Hadoop Summit 上,ORC 的创始人 Owen O`Malley 也特别提到了阿里巴巴的贡献。
开源 ORC 文件格式介绍
ORC 在类型系统上的建模是一个树形的结构。对于一些诸如 Struct 这样的复杂类型会有一个或者多个孩子节点;Map 类型有两个孩子节点,即 key 和 value;List 类型只有一个孩子节点;其他的普通类型就是一个叶子节点。如下图所示,左侧的表结构就能够被形象地转化成右侧的树型结构,简单、直观。
ORC 主有两个优化指标,查询速度和存储效率,先来谈谈查询速度优化。ORC 将文件切分成大小相近的块,在块内部使用列式存储,也就是将相同列的数据存储到一起。针对这些数据,ORC 提供轻量的索引支持,记录每个数据块的最小值、最大值、非空值计数等。基于这些统计信息,可以在读取文件的时候非常方便地过滤掉不满足查询语句的数据,减少数据的读取量和网络传输消耗。此外,ORC 还支持列裁剪,如果查询中只需要读取部分列,那么 Reader 只需要返回所需列的数据,进一步减小了需要读取的数据量。
关于存储效率优化目标,ORC 采用了通用的压缩算法,比如开源的 zStandard、zlib、snappy、LZO 等来提高文件压缩率。同时,也使用了轻量级的编码算法,比如游程编码、字典编码等来进一步提高压缩比。
三、为何选择 ORC?
在对下一代文件格式进行技术选型的时候,为了更好的生态和开放性,我们决定把眼光投向开源社区。在大数据领域,除了 Apache ORC 之外,就是是由 Cloudera 和 Twitter 共同开发的 Apache Parquet,其灵感来源于 Google 发表的 Dremel 的论文。Parquet 的思想和 ORC 非常相近,使用了列式存储和通用的压缩以及编码算法,也能够提供轻量级索引以及统计信息。
相比 ORC,Parquet 有两个优点。第一点就是 Parquet 能够更好地支持嵌套类型,Parquet 能够通过使用 definition level 和 repetition level 来标识复杂类型的层数等信息。不过这样的设计就连 Google 的论文都使用整整一页来介绍这个算法,实现起来比较复杂。此外,当前 Parquet 的编码类型比 ORC 也更多一些,其支持 plain、bit-packing 以及浮点数等编码方式,所以 Parquet 在某些数据类型的压缩率上比 ORC 更高。
ORC 和 Parquet 性能对比
虽然 Parquet 相对 ORC 有自己的优势,但我们最终还是看性能说话。MaxCompute 团队内部和开源社区都进行了性能测试,这里以 Hadoop Summit 上公开的一次性能测试进行阐述。Hortonworks 的联合创始人 Owen 基于 Github 日志数据和纽约市出租车数据这两个开源数据集,测试了各种文件格式(ORC、Parquet、Avro 和 JSON)的存储效率和读表性能,由于 Avro 和 JSON 都是行存格式,我们在此略过。
关于存储效率的对比,Taxi 和 Github 两张表在相同压缩算法下,Parquet 和 ORC 存储性能非常相近。Github 项目数据集下 ORC 比 Parquet 的压缩率更高一些,压缩后数据量变得更小。
关于读表性能的对比,相同压缩算法的 ORC 文件读起来比 Parquet 要更快一些。
在数据压缩效率差不多的前提下,大数据场景我们更关心表的读取性能,因此 ORC 更契合我们的需求。同时由于 ORC 的设计简洁,社区有更强的领导力,最终我们决定选择 ORC 作为 MaxCompute 存储格式更新换代的起点。
四、AliORC 技术揭秘
AliORC 是基于开源 Apache ORC 深度优化的文件格式,它的首要目标是和开源的 ORC 保持兼容,这样才能保证易用性和开放性。AliORC 主要从两个方面对于开源的 ORC 进行了优化,一方面,AliORC 提供了更多的扩展特性,比如对于 Clustered Index 和 C++ Arrow 的支持,实现谓词下推等等。另一方面,AliORC 进行了大量性能优化,实现了异步预读、智能的 I/O 管理以及自适应字典编码等。
异步预读
传统读文件的方式一般是从底层文件系统(这里是阿里自研的的分布式文件系统盘古)先拿到原始数据,然后进行解压和解码。这两步操作分别是 I/O 密集型和 CPU 密集型的任务,并且两者没有任何并行性,因此就加长了整体的端到端时间,并且造成了资源浪费。AliORC 这样就将所有的读盘操作变成了异步的操作,实现了从文件系统读数据和解压解码操作的并行处理,。Reader 在读数据前,提前将读取数据的请求全部发送出去,当真正需要数据的时候就去检查之前的异步请求是否已经返回了;如果数据已经返回,则可以立即进行解压和解码操作而不需要等待读盘,这样就可以极大地提高并行度,降低读取文件的所需时间。
如下图所示的就是打开了异步预读优化前后的性能对比。开启异步预读之前,读取一个文件需要 14 秒,而在打开异步预读之后则只需要 3 秒,读取速度提升了数倍。当异步请求返回较慢时还是会变成同步请求,从右侧饼图可以看出,实际情况下 80%以上的异步请求都是有效的。
消除小 I/O
在 ORC 文件中,不同列的文件大小是完全不同的,而每次读取都是以列为单位进行的。这样对于数据量比较小的列而言,读取时的网络 I/O 开销非常大。而 ORC 文件中有许多这样数据量很小的列,从而造成了大量小 I/O 的产生。小 I/O 不仅造成整体 latency 变慢,还会造成 I/O 次数增多降低并发度。为了消除这些小 I/O 开销,AliORC 在 Writer 写数据时,针对不同列的数据压缩后大小进行了排序,将数据量少的列放在一起写。而在 reader 端,用一次大 I/O 块即可将排列在一起的小数据块全部读出来,大大减小了小 I/O 的次数。
如下图所示的是 AliORC 优化前后的对比情况。蓝色部分表示的就是消除小 I/O 之前的 I/O 分布情况,橙色的部分则是表示消除之后的 I/O 分布情况。可以看到,消除小 I/O 之前,小于 64K 的 I/O 有 26 个,而在消除小 I/O 之后,小于 64K 的 I/O 为零,优化效果还是非常显著的。
内存管理
在开源版本的 ORC 实现中,Writer 的每列数据都使用了一个很大的 Buffer 去保存压缩后的数据,默认大小为 1M。Buffer 设置得越大,压缩率越高。但是不同列的数据量不同,某些列根本用不到 1M 大小的 Buffer,因此就会造成极大的内存浪费。避免内存浪费的简单方法就是在一开始的时候只给很小的数据块作为 Buffer,并且按需分配,如果需要写的数据更多,那么就通过类似 C++ std::vector 的 resize 方式提供更大的数据块。原本实现方式中,resize 一次就需要进行一次 O(N)的复制操作,将原始数据从老的 Buffer 拷贝到新的 Buffer 中去,这样性能损失非常大。因此,AliORC 开发了新的内存管理结构,初始值为 64K 大小,按需每次分配一个连续 64K 大小的 Block,但是 Block 与 Block 之间内存不是连续的。这种方式打破了内存连续的假设,会造成很多代码的改动,但是这一改动却是值得的。因为在很多场景下,原来的 resize 方式需要消耗很多内存,有可能造成内存耗尽,尤其是在动态分区写入的场景下,进而导致任务因为 OOM 而失败,而新的方式可以在这种场景下大大降低内存的峰值,效果非常明显。
Seek 读取优化
Seek 原来的问题在于压缩块比较大,每个压缩块中包含很多个 Row Group。在下图中,每一万行数据叫做一个 Row Group。在 Seek 的场景下,可能会 Seek 到文件中间的任意一处,可能刚好是在某一个压缩块中间,比如图中第 7 个 Row Group 被包含在第 2 个压缩块中。通常 Seek 的操作就是先跳转第 2 个 Block 的头部,然后进行解压,将第 7 个 Row Group 之前的数据先解压出来,再真正地跳转到第 7 个 Row Group 处。但是图中绿色的部分数据我们并不需要读,因此这一段的数据量就被白白解压了,浪费掉很多计算资源。因此,AliORC 就是在写文件的时候把压缩块和 Row Group 的边界进行对齐,这样 Seek 到任何的 Row Group 都是一个单独的压缩块,不需要额外解压缩操作。
如图所示的是进行 Seek 优化前后的效果对比。蓝色部分是优化之前的情况,橙色部分代表优化之后的情况。可以发现,有了对于 Seek 的优化,解压所需的时间和数据量都降低了 5 倍左右。
自适应字典编码
字典编码就是针对重复度比较高的字段,首先整理出来一个字典,然后使用字典中的序号来代替原来的数据进行编码。对字符串而言,相当于把字符串类型数据的编码转化成整型数据的编码,这样可以大大减少数据量。但是 ORC 字典编码存在一些问题。首先,不是所有的字符串都适合字典编码,而在开源 ORC 的实现里,每一列先默认打开字典编码,当写文件结束时再判断每一列是否适合字典编码,如果不适合,再回退到非字典编码。由于回退操作相当于需要重写字符串类型数据,因此开销会非常大。AliORC 所做的优化就是通过一个自适应的算法提早决定某一列是否需要使用字典编码,这样就可以节省很多的计算资源。另外,开源的 ORC 中通过标准库中的 std::unordered_map 来实现字典编码,但是它的实现方式并不适合大数据场景下的数据特征,而 Google 开源的 dense_hash_map 库可以带来 10%的写性能提升,因此 AliORC 采用了这种实现方式。最后,开源的 ORC 标准中要求对于字典类型进行排序,但实际上这是没有必要的,剔除掉该限制可以使得 Writer 端的性能提高 3%。
Range 分区表的 Range 对齐读取优化
这部分主要是对于 Range Partition 的优化。如下图右侧的 DDL 所示,Range Partition 就是将一张表按照某些定义好的列进行范围聚集,并对这些列的数据进行排序。比如图中的例子将这些数据存储到 4 个桶中,每个桶分别存储 0 到 1、2 到 3、4 到 8 以及 9 到无穷大的数据。在具体实现过程中,每个桶都使用了一个独立的 AliORC 文件,在文件尾部存储了一个类似于 B+Tree 的索引。当需要进行查询的时候,如果查询的 Filter 和 Range Key 相关,就可以直接利用该索引来排除不需要读取的文件和数据,进而大大减少所需要处理的数据量。
对于 Range Partition 而言,AliORC 具有一个很强大的功能,叫做 Range 对齐。假设需要 Join 两张 Range Partition 的表,它们的 Join Key 就是 Range Partition Key。如下图所示,表 A 有三个 Range,表 B 有两个 Range。在普通表的情况下,这两个表进行 Join 会产生大量的 Shuffle,因为需要将相同的 Join Key 数据 Shuffle 到同一个 Worker 上进行 Join 操作,而我们知道 Join 操作又是非常消耗计算资源的。有了 Range Partition 之后,就可以根据 Range 的信息进行对齐,在这里就是将 A 表的三个桶和 B 表的两个桶进行对齐,产生如下图所示的三个蓝色区间,可以确定蓝色区间之外的数据是肯定不可能产生 Join 结果,因此 Worker 上的 Reader 根本不需要读取那些数据。
完成优化之后,每个 Worker 只需要打开蓝色区域的数据进行 Join 操作即可。这样就可以使得 Join 操作能够在本地 Worker 中完成,不需要进行 Shuffle,进而大大降低了数据传输量,提高了端到端的效率。
AliORC 效果
如下图所示的是在阿里巴巴内部测试中 AliORC 和开源的 C++版本 ORC 以及 Java 版本 ORC 的读取时间比较。从图中可以看出 AliORC 的读取速度比开源 ORC 要快一倍。
截止 2019 年 5 月在阿里巴巴内部也迭代了 3 个版本,从下图可以看出,每个版本之间也有接近 30%的性能提升,并且还在持续优化当中。目前,AliORC 已经在阿里集团内部 MaxCompute 生产环境中大规模使用,尚未在公有云上进行发布。后续我们也会将 AliORC 开放出来,让大家共享技术红利。
五、个人成长
为何选择加入 MaxCompute 团队
从个人角度而言,我更加看好大数据领域。虽然对于一项技术而言,黄金期往往最多只有 10 年。对于大数据技术而言,它已经经历了 10 年,但我相信大数据技术并不会衰落。尤其是在人工智能技术的加持下,大数据技术仍然有很多需要解决的问题,有各种各样的海量数据需要去分析、去处理、去储存。此外,阿里的 MaxCompute 团队更是人才济济,北京、杭州、西雅图等团队都具有强大的技术实力,从优秀的同事身边能够学习到很多东西。最后一点,开源大数据产品基本上都是国外的天下,而 MaxCompute 是完全国产自研的平台,加入 MaxCompute 团队让自己能够有机会为国产软件尽一份力量。
如何走上大数据技术之路的
我走上大数据技术这条路也是各种机缘巧合,之前在学校里面所学习的内容与大数据完全没有关系。在美国找工作的时候基本拿的都是视频编码相关的 offer,只有 Uber 是给的大数据相关的岗位,由于比较好奇互联网独角兽的文化就加入了。在加入 Uber Hadoop Platform 组的时候,该组还处于组建的早期,平台内基本没有数据,公司内大家都是各玩各的,自己搭建一些 Hadoop 服务来执行任务。当时跟着团队从 0 到 1 地学习了 Scala 和 Spark,从最开始了解如何使用 Spark 到阅读 Spark 源码,然后基于 Spark 慢慢地搭建起大数据平台、扎根大数据领域。而加入阿里巴巴 MaxCompute 团队之后,能够从需求、设计、开发、测试、优化和跟进用户等角度来接触打造大数据产品的各个阶段,这也是比较有趣和宝贵的经历。
在阿里巴巴美国办公室的工作体验
阿里的美国部门其实和国内的部门差别并不大,大家工作都是相似的节奏。虽然西雅图的办公室人数并不是很多,也就一百多人,但是各个团队的同事都有,比如集团、蚂蚁、阿里云、达摩院、量子实验室等等,很容易和不同技术方向的同事交流新点子和有趣的事情。另外,在阿里巴巴的美国办公室,可以近水楼台参加这边举办的行业会议和开源社区 meetup,我们自己也会经常去分享和交流经验。
如何一步步成为 ORC 社区 PMC
MaxCompute 平台存储引擎升级需要引入新的文件格式,而 ORC 当时开源的 C++版本只有 Reader 却没有 Writer,因此刚好有这个机会去开发 C++版本的 ORC Writer。完成之后为了能够服务开源社区,并且集合社区的力量共同把 C++版本 ORC 做好,因此我们将代码贡献回了开源社区,质量和数量都得到了开源社区的认可。成为 Committer 之后,责任也就更大了,不仅自己需要贡献代码,还需要和社区一起成长,review 其他成员的代码,并讨论短期和长期的需求和设计。在获取 Committer 一年后,由于活跃度较高,因此被授予了 PMC。这个过程中最大的感受就是,取之开源,回馈开源,高质量的开源社区需要大家一起来投入,开源精神才能长久、健康的发展下去。
作者介绍:
作者简介:吴刚,阿里云智能计算平台事业部高级技术专家,主要负责 MaxCompute 平台存储引擎的开发和优化,同时是 Apache 顶级开源项目 ORC 的 PMC。
入职阿里前就职于 Uber 总部负责内部的 Spark 计算平台,先后毕业于中国科学技术大学和卡内基梅隆大学。
本文转载自公众号阿里技术(ID:ali_tech)。
原文链接:
https://mp.weixin.qq.com/s/aVLBXS-eiWVpvIbeE2JVIw
评论