深度剖析 StarRocks 读取 ORC 加密文件背后的技术
作者:vivo 互联网大数据团队 - Zheng Xiaofeng
本文介绍了 StarRocks 数据库如何读取 ORC 加密文件,包括基础概念以及具体实现方案。深入探讨了利用 ORC 文件的四层结构和三层索引机制,实现高效查询加密数据。希望通过本文对 ORC 加密文件读取功能的实现细节的剖析,让读者更加深刻理解 ORC 文件,同时了解 StarRocks 支持加解密数据分析的方案。
一、背景
为了提升对敏感数据的保护,需要对 Hive 表一些敏感数据进行加密存储。
Spark 组件已经通过引入了 Apache ORC 项目(Java 版本)对 ORC 格式的 Hive 表的数据进行加解密。
StarRocks 也使用了 Apache ORC 项目的 C++版本读写 ORC 文件,但是 C++版本没有实现加解密功能,在使用 StarRocks 对 Hive 表进行即席分析时,无法对具有加密列的 Hive 表进行查询,因此,需要对 StarRocks 的 Apache ORC 模块进行改造,使其支持对 ORC 格式的 Hive 加密表数据读取功能,数据架构图如下图所示:

希望通过本文对 ORC 加密文件读取功能的实现细节的剖析,让读者更加深刻理解 ORC 文件,同时了解 StarRocks 支持加解密数据分析的方案。
二、问题引入
在正式开启全文的阅读之前,我们首先引入几个问题,然后带着这些问题去阅读后面的内容,将会更有针对性与启发性,通过深入解答这些问题,我们不仅能够更好地理解相关的概念和技术,还能提升分析和解决问题的能力。问题如下:
程序解压某个文件时,是否需要一次性读取整个文件后再进行解压操作?
ORC 文件究竟是如何做到在不扫描全文件的情况下就能精准查询到想要的数据?
当 SQL 查询条件不符合最左前缀原则时,ORC 文件中的索引是否就会失效?
数据加密、解密、解压以及压缩之间的关联关系到底是怎样的?
在写 ORC 文件时为什么是先压缩后加密,而不是先加密后解压?
三、ORC 文件介绍
ORC(Optimized Row Columnar)文件格式是一种高度优化的列式存储格式,它主要用于 Hadoop 生态系统中的大数据处理和分析。ORC 文件结构的设计旨在提高 I/O 效率、减少数据读取时间,并支持复杂的数据类型和压缩算法。
3.1 四层结构 File ,Stripe,Stream,Group
一个 File 中包含多个 Stripe,一个 Stripe 包含多个 Steam,一个 Stream 包含多个 Group,每个 Group 默认存储 1 万行数据,如下图所示:

3.2 三层索引
FileStat :文件级别各列的统计信息,用于判断 SQL 条件是否下推。
StripeStat:Stripe 级别各列的统计信息,用于判断 SQL 条件是否下推。
IndexData:每个 Stripe 内部各列的索引信息,用于判断 SQL 条件是否下推。
在读取文件中数据之前,会先读取以上 3 类索引数据,根据 SQL 条件逐层进行比对,来决定是否跳过某些数据的读取,减少数据扫描量,从而提升 SQL 查询效率。
下表是只包含 id 和 name 两列的 ORC 文件的各层统计信息的案例:
FileStat

StripeStat

IndexData

3.3 ORC 文件内部详细结构
前面已经大体介绍了 ORC 文件的结构,下面详细介绍其内部结构,ORC 文件由多个逻辑层次组成,每个层次都有特定的作用和结构,下图具体描述了包含 2 列(id,name)的 ORC 文件结构图:

Tail:存储文件的元数据,如列的压缩信息、统计信息、版本等,包含了三个部分:PostScript、Footer、MetaData。
Body:实际存储数据的部分,由多个 Stripe 组成。
下面分别介绍 Tail 和 Body 内部包含哪些结构:
Tail 文件尾部是读取 ORC 文件的起点,它包含了文件关键信息:
PostScript:存储文件的压缩类型、压缩块大小、版本信息,Footer 和 MetaData 的长度等,这部分数据不会被压缩。
Footer:记录了整个文件所有列的统计信息(FileStat),所有 Stripe 的元数据信息(stripesList),加密信息(encryption)以及文件 body 长度。
MetaData:存储该文件所有 Stripe 的统计信息(StripeStat)。
Body 实际存储数据的部分,由多个 Stripe 组成,每个 Stripe 包含多个 Stream,先存储索引相关的 Stream(index-Stream),后面存储实际数据相关的 Stream(data-Stream),每一列包含多个 index-Stream 和 data-Stream,Stripe 是 ORC 文件中数据存储的基本单元,每个 Stripe 数据大小一般不超过 200M,主要包含下面几块内容:
Stripe Footer:包含所有 Stream 的元数据(streamsList)和加密信息(encryption)等。
Index-Stream:存储索引相关数据的 Stream,按列存储。
Data-Stream:储实际数据相关的 Stream,按列存储。
ORC 文件的读取是从尾部最后一个字节开始的,得到 PostScript 的长度,读取 PostScript,然后根据 PostScript 中的 FooteLength,MetaDataLength 信息读取 MetaData 和 Footer,最后根据 Footer 中的 Stripe 信息读取具体的数据 Stripe,上面的文字介绍可能不是很直观,如果想更细节了解 ORC 文件结构内容可以参考(ORC文件结构思维导图,ORC文件官网介绍)。
四、相关概念的理解
4.1 对称加解密

对称加解密的要素包括密钥、明文、密文和加密算法。以下是对这些要素关系的描述:
密钥:密钥是加密和解密过程中的关键元素,它是由随机数生成的,通常是固定长度的一串二进制数。
明文:明文是指原始的信息,可以是文本、图片、音频等各种形式的数据。
密文:密文是经过加密算法处理后的数据,只有知道密钥的人才能解密还原成明文。
加密算法:加密算法是将明文转换成密文的过程,这个过程通常涉及到一系列的数学运算,比如 AEC,RSA 等。
注意:对称加密的加密密钥 和 解密密钥是一样的。
4.2 文件的压缩和解压缩
4.2.1 压缩算法
压缩算法是用于减小文件大小的数学方法。它通过各种技术,如替换、重新编码、差分编码、运行长度编码、字典编码、变换编码等,来减少数据的冗余和实现数据的体积缩小。压缩算法可以是无损的或有损的:
无损压缩:意味着原始数据可以完全从压缩文件中恢复,常用于文本和某些类型的数据文件。
有损压缩:为了获得更高的压缩率,允许丢失一些数据,常用于图像、音频和视频文件。
4.2.2 解压算法
解压算法是压缩算法的逆过程,它用于将压缩文件恢复到其原始状态。无损压缩的解压算法能够完全恢复原始数据,而有损压缩的解压算法则可能无法完全恢复所有原始数据。
文件压缩和解压缩简单流程图如下:

注意:数据的压缩算法和解压算法要一样
4.2.3 压缩块
文件压缩块是指对文件进行压缩处理后生成的一组连续的数据块。在文件压缩过程中,文件被分割成多个块,每个块都经过压缩算法处理。一般来说,文件压缩块的大小可配置。例如,ZIP 压缩的每个压缩块的大小可以达到 64KB 或更大,而在其他压缩格式如 7z 中,压缩块的大小可以更大,通常为数 MB。这些大小可以根据文件的特性和压缩算法的性能进行调整,以达到更好的压缩比和解压性能。
注意:在解压文件的过程中会从文件中读取整个压缩块数据到内存之后再使用解压算法进行解压处理,所以压缩块越大每次解压读取到内存里的数据会越大。
4.3 加密压缩文件读写大致流程
在掌握了数据加密和压缩的基础知识之后,让我们从宏观的角度了解一下 ORC 加密文件读写流程,如下图所示:在写入时,内存中的数据首先被序列化,然后压缩以减少体积,最后对数据加密。在读取时,数据首先被解密以恢复原始格式,然后解压数据得到原始数据,最后通过反序列化原始数据转换为内存对象。

详细说明写入和读取过程中的各个步骤:
(1)写入过程(序列化、压缩、加密)
序列化:在数据写入存储系统之前,首先需要将内存中的对象转换成可以存储或传输的格式,这个过程称为序列化。序列化后的数据通常是一个二进制格式,便于后续的处理。
压缩:序列化后的数据可能会占用较大的空间。为了减少存储需求和提升后续数据加密处理效率,接下来对数据进行压缩。压缩算法会尝试去除数据中的冗余,从而减少数据的体积。
加密:压缩后的数据需要进行加密,以确保数据的安全性。加密算法会使用密钥对数据进行加密,生成密文。
存入文件中:加密后的密文被存储在文件中,等待后续的读取或传输。
(2)读取过程(解密、解压、反序列化)
解密:当需要读取文件中的数据时,首先需要使用正确的密钥和加密算法对密文进行解密,恢复为压缩前的数据。
解压:解密后,应用解压算法对数据进行解压,恢复到序列化前的状态。
反序列化:解压后的数据是一个二进制格式,需要进行反序列化,将其转换为内存中的对象。反序列化是序列化的逆过程,它将二进制数据转换为可读可操作的数据结构。
内存对象:经过解密、解压和反序列化之后,数据最终以内存对象的形式被程序处理。
五、StarRocks 读取 ORC 加密文件实现方案
5.1 ORC 文件内部数据加密关系
首先,介绍几个密钥的含义:
statKey:用于解密加密列的 FileStat,StripeStat 的密钥,每个列一个,加密存储在文件 Footer 里。
dataKey:用于解密加密列的 IndexData 和 RowData,每个 Stripe 的每一列都有一个,加密存储在 Stripe 的 Footer 里。
masterKey:文件的根密钥,用于解密 ORC 文件中被加密的 statKey 和 dataKey,该密钥没有存储在文件中,一般存储在 Hive 表属性上。要解密 ORC 文件中的数据,首先需要获取这个 masterKey。然而,masterKey 本身也是加密的,因此在读取 Hive 表之前,必须先从表属性中提取出加密的 masterKey,访问密钥管理服务(Key Management Service, KMS),对加密的 masterKey 进行解密,从而获得可用于实际解密操作的明文 masterKey 密钥,一旦获得了 masterKey 的明文形式,就可以用它来解密 ORC 文件中的 dataKey 和 statKey。

下图是描述了 masterKey、statKey,dataKey 之间的关系,灰色部分代表是存储在文件中被加密的数据,绿色部分则是解密之后的数据,包括我们解密后的 statKey,dataKey。获得这两个密钥之后分别用于解密统计信息和文件中的真实数据。
5.2 StarRocks 读取 ORC 加密文件流程
在深入掌握了 ORC 文件中密钥的相互关系和功能后,我们现在转向探讨 StarRocks 是如何读取 ORC 加密表的数据。这个过程如下图所示:

1)提交 SQL 查询:用户首先通过 SQL 客户端向 StarRocks FE 节点提交查询请求。这通常涉及到对 Hive 表下存储的 ORC 加密文件进行读取操作。
2)获取解密的 masterKey:查询提交后,系统首要根据 SQL 获取 Hive 表中的 ORC 文件所需的 masterKey。这个 masterKey 一般存储在表属性里,并且是加密存储的,必须调用 KMS 服务来解密,得到密钥明文。
3)传递 masterKey 明文:解密后的 masterKey,以明文形式传递给 StarRocks BE 节点。
4)读取并解密密钥:BE 拿到已解密的 masterKey 之后 读取并解密 ORC 文件中的 statKey 和 dataKey,这两个密钥分别用于解密统计信息(FileStat,StripeStat)和实际数据内容,为接下来的统计信息和数据解密做准备。
5)使用 statKey 和 dataKey 解密数据:BE 使用 statKey 来解密文件的统计信息(fileStat 和 StripeStat)同时使用 dataKey 来解密实际的数据内容。
5.3 读取 ORC 加密文件的关键实现细节
通过了解前文 StarRocks 读取 ORC 加密文件流程,我们将深入探讨读取 ORC 加密文件的数据关键实现细节。首先,我们提出一个问题:在物理存储中,文件存储的是什么内容?答案是二进制数据。这些二进制数据通常会经过压缩处理。
ORC 文件的读取流程是自外向内的,类似于剥洋葱的过程,逐步深入到我们需要读取的目标数据。读取流程可以概括为:首先读取文件元数据,通过元数据获取目标数据的偏移量(offset)和数据长度如下图所示,然后通过流的方式读取目标数据。

具体到 ORC 加密文件的读取实现代码,主要采用了设计模式中的装饰模式方式来组织代码的。在这个模式中,原始的文件流(SeekableFileInputStream)首先被解密流(DecryptionInputStream)所包装,如果是非加密文件就没有这一层,然后解密流又被解压缩流(DecompressionStream)所包装。每一层流都只负责向其包装的流请求数据,并在接收到一定量数据后开始处理自己的逻辑。如下图所示:

这种分层的方法保证每一层都专注于自己的职责,共同协作完成 ORC 文件的读取任务。通过这种方式,我们不仅能够高效地读取 ORC 文件,还能确保数据的安全性和完整性。综上所述,ORC 文件的读取流程是一个从文件元数据到具体数据内容的逐步深入过程。
5.4 加密字段跳读机制
为了提升数据的查询效率,查询数据时会根据索引数据跳过不必要的数据读取,下面我们介绍加密列跳读机制,理解了这部分的内容,就能非常清晰的知道,读取加密字段时,对数据解密与解压是怎样协作的。
5.4.1 加密块与压缩块的关系
加密列的数据划分了多个加密块与压缩块,一个压缩块 包含多个加密块,读取数据时,先对每个加密块进行解密,解密多个加密块之后,把这些解密后的数据块合并成一个完整的压缩块,然后对这个压缩块进行解压得到原始数据下图是加密块与压缩块的关系图:

5.4.2 ORC 文件使用的加解密算法和模式
下图描述了具体的数据加解密过程中以及设计到整个过程中各种元素输入输出的关系:

注意:同一个数据块(16 字节)加密过程和解密过程中的 密钥、IV 值、加密算法和加密模式必须相同。
明文块:我们对 ORC 文件加密使用的加密算法是 AES-128-CTR/NoPadding,该算法加密数据时 ,会把明文按照 16 个字节划分多个块,每个块加密之后得到的数据就是加密块。
加密块:每个明文数据块加密之后得到的数据就是加密块。
初始向量 IV:初始向量 IV 的作用是使加密更加安全可靠(加盐),我们使用 AES 加密时需要主动提供这个初始向量 IV,而且只需要提供一个初始向量就够了,后面每个数据块的加密向量由加密模式决定,所以每个数据块的加密向量都不一样。初始向量 IV 的长度规定为 128 位 16 个字节,ORC 文件解密参数 IV 的描述如下:总共 16 个字节,前面 8 个字节分别存储:列 ID,Stream 类型,Stripe 的 ID ,后面 8 个字节用于填充 min_count,由于我们使用的是 CTR 加密模式,所以这个 min_count 就是加密块在整个加密数据中的计数,iv 各个内容长度定义如下图:

密钥:AES 要求密钥的长度可以是 128 位 16 个字节、192 位或者 256 位,位数越高,加密强度自然越大,但是加密的效率自然会低一些,因此要做好权衡。我们开发通常采用 128 位 16 个字节的密钥,我们使用 AES 加密时需要主动提供密钥,而且只需要提供一个密钥就够了,每个数据块加解密使用的都是同一个密钥。
加密模式:有 5 种加密模式,这些加密模式的主要目的是为了不让重复的明文加密之后得到的密文一样,提升数据安全性,我们使用的是 CTR 模式(计数器模式)对数据加密,那解密的时候也需要 CTR 模式对数据解密,计数器模式介绍请参考链接,CTR 模式 的 iv 参数 包含了 加密块计数(min_count),所以 每次对一个加密块解密时 需要知道 当前加密块的初始计数值。
5.4.3 举例说明跳读流程
学习了前面读取加密数据的关键细节之后,举个例子说明跳读 ORC 文件流程,假设根据索引数据和查询条件确定需要读取某个文件中第 1 个 Strip 中第 1 列的第 5 个 group 的数据,那么我们知道 group5 数据的偏移量 offset,文件结构如下图所示:

具体逻辑大体流程如下:

注意:解压数据块时,必须把当前解压块的所有数据读出来才能使用对应的解压算法解压数据。
1)group5 数据的偏移量 group_offset 计算出 group5 数据在哪个压缩块里,计算公式为:block_index = group_offset/zipBlockSize(压缩块大小),并得到该压缩块的起始位置 zip_head_offset 公式为:zip_head_offset = block_index*zipBlockSize。
2)获取 zip_head_offset 位置对应的加密块计数,加密块计数值计算公式为:min_count = zip_head_offset/encrypted-size(加密块大小) 更新 iv 向量的 min_count 值。
3)文件读指针定位到 zip_head_offset,开始读取压缩块的数据,这个压缩块的数据全部读出之后,使用解压算法进行解压。
4)通过 group5 在解压的数据上偏移量和长度,读取 group5 数据,然后再对数据进行解码。
六、问题解答
通过前面对相关内容的讲解,下面我们来解答前文提出的问题:
1)文件解压是否意味着一定是对整个文件进行解压操作?
答:不需要,文件是按照一定大小划分出若干个压缩块,只要读出相应的压缩块进行解压就行。
2)ORC 文件究竟是如何做到在不扫描全文件的情况下就能精准查询到想要的数据?
答:ORC 文件有三层索引,在读取文件数据之前先读取各层级的索引信息,根据过滤条件过滤掉不必要的数据扫描,从而提升数据查询效率。
3)当 SQL 查询条件不符合最左前缀原则时,其索引效果是否就会失效呢?
答:不会失效,ORC 文件是列式存储的,各列信息都是相互独立的,有自己的索引信息,与行式数据库的索引最左前缀规则不同。
4)数据加密、解密、解压以及压缩之间的关联关系到底是怎样的?
答:请参考本文:5.1 ORC 文件内部数据加密关系 内容。
5)在写加密列数据时,为什么不是先加密数据再压缩,而是先压缩后加密?
答:主要是为了提升加密效率,数据被压缩处理之后,数据量变少了,加密效率就提升了。
七、总结
本文介绍了 StarRocks 数据库如何读取 ORC 文件的加密数据,包括相关概念理解、ORC 文件介绍、以及 StarRocks 读取加密 ORC 文件的具体实现方案。阐述了出于数据安全的需要,对 Hive 表中的敏感数据进行加密存储的必要性,介绍了对称加密、文件压缩与解压、加密压缩文件读写流程等概念,深入探讨了 ORC 文件的三层结构和索引机制,以及如何利用这些特性实现高效查询加密数据。还详细描述了 StarRocks 读取加密 ORC 文件的流程,包括获取解密的 masterKey、使用 masterKey 解密 ORC 文件中的密钥、以及使用这些密钥解密数据。
希望通过本文对 ORC 加密文件读取功能的实现细节,让读者对 ORC 文件的理解更深刻。最后如果想从代码层面了解 ORC 文件解密过程可以参考开源PR。
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/c70c19bd3cdbd7ac06164a301】。文章转载请联系作者。
评论