当数据库用户的关键业务系统具有庞大数据量时,IT 成本、数据成本等都会居高不下,此时,通过压缩来降低存储成本是再自然不过的选择。但对于很多数据库的用户而言,压缩并不能一劳永逸:选择高压缩比,压缩、解压往往需要耗费大量时间,通常也会降低内存、硬盘的读写性能,显然对延迟敏感的关键业务场景不适用;选择低压缩比,压缩文件依然会占据不少硬盘空间,这又与压缩的初衷背道而驰。
数据压缩最终目的是降本增效,降本不能牺牲效率,因此实现高压缩比的前提一定是先保证高性能,这也是更适合用户关键业务系统的压缩方案。在今年 5 月举行的 2022 中国国际大数据产业博览会上,OceanBase 的“高压缩比分布式存储引擎”荣获了“领先科技成果奖”。OceanBase 获奖的关键原因是这一自研的引擎创造性地解决了传统数据库无法平衡“性能”和“压缩比”的难题,本文也将分享我们对破解这一难题的思考。
关于作者
赵赛铜(乘骥)
OceanBase 高级开发工程师,TPC-H 项目组成员。 目前在OceanBase 存储-分析处理组,工作方向是存储结构和分析处理功能的维护与开发。
作为完全自主研发的原生分布式数据库,OceanBase 在存储架构、数据编码、数据日志等方面不断挑战技术极限,进一步拓展了高级压缩技术能力的边界。其中,基于数据编码的存储压缩技术是 OceanBase 实现高压缩比的核心技术之一,OceanBase 自研了一套对数据库进行行列混存编码的压缩方法 (encoding),使用列级别的字典、差值、前缀等编码算法,在通用压缩算法之前对数据进行编码压缩,可以在不影响数据读写性能的同时,显著提高数据压缩率,帮助用户大大降低存储成本。本篇文章将分享 OceanBase 对数据库压缩技术的思考,介绍我们在数据编码领域的技术创新思路与方案:
数据压缩究竟在解决什么问题;
如何做出更适合实际业务场景的数据压缩;
数据编码压缩的效果测试。
数据压缩究竟在解决什么问题?
通常而言,数据压缩是计算机按照一定的算法对数据进行处理,变换成占用更小空间格式的过程,能提高数据的传输、存储和处理效率。以常见的摩斯电码为例,它将文本中出现概率高的字母 (e, t, i) 借助用时比较短的编码来表示。这样做可以提高发送电报的效率,在相同的时间传输更多字符,此种方法就可理解为一种数据压缩。
与此同时,从摩斯电码的设计中我们也不难看出压缩算法的效果是和数据的分布与特征相关的,不同的系统也会针对自己的数据特征和应用场景采用不同的压缩算法。在数据库领域,存储的数据会受到 schema、类型、值域等限制,相似度会比较高,存储的数据也会存在一些冗余,这些特征使得数据库中的数据可以被更好地编码与压缩。
保证高性能的压缩,才是用户需要的压缩
目前市面上的数据库产品基本都提供了压缩功能,但是由于存储引擎架构和数据库应用场景的不同,产品之间的数据库压缩设计和能力也会产生一些差别。
传统的事务型数据库通常使用定长块存储数据,这样可以保障读写性能,但会带来额外开销和空间浪费。面向 OLTP 场景的数据库要在数据的写入和更新场景支持更高的 TPS,通常会使用行存及B+Tree 类的存储引擎,对于数据的压缩会比较保守。这样的存储引擎通常会将定长的内存数据页与持久化数据块对应起来管理数据,而且有些情况下需要将更新数据实时写到数据块中,会导致对页内少量行进行 dml 操作也需要对整个数据页进行重新压缩,带来更多的 overhead;而且定长数据块在进行压缩前难以确定压缩后的数据块大小,也会带来一些空间浪费等问题。
另一方面,分析型数据库天然更适合高压缩比,但压缩会降低查询和更新性能。面向 OLAP 场景的数据仓库等系统的数据,通常是批量导入的,增量数据相对较少。因此分析型数据库通常使用列存,增量数据写日志,定期重整基线数据的存储引擎。这样的存储引擎通常在批量导入和后台数据重整时进行数据压缩,并采用压缩比更高的压缩策略。如以更大的数据块为单位进行压缩,将更多数据压缩到同一个数据块中,将同一列的数据存储在相邻的数据中,并针对这一列数据的特征对数据进行压缩率更高的编码。但压缩后,可能会明显降低数据的点查性能,减小数据更新的 tps。
总结来说,使用压缩率越高的压缩算法,压缩和解压数据的 overhead (开销)就越大,对读写性能影响也越大。同时,在我们和用户的交流中也发现,相较于数据压缩率,用户往往更在意数据库性能,尤其是在关键业务场景中。如果在读写路径上对数据进行压缩和解压,会不可避免地消耗计算资源,也会对事务处理性能带来影响。因此,在传统数据库处理业务的场景中,出于性能考虑,普遍只会对归档或备份等访问不频繁的数据开启压缩功能,而对查询和更新比较频繁的数据只能放弃压缩,以满足业务的性能要求。
只有在保证高性能的前提下实现高压缩,才是真正对用户有价值的高压缩,也能实现真正的降本增效。OceanBase 基于 LSM-Tree 架构自研的存储引擎,实现了对 OLTP 与 OLAP 负载的同时支持,也能在提供高效的事务处理能力的同时,根据数据存储的特征进行自适应编码压缩,提供高效的数据压缩能力。在过去服务用户的经验中,OceanBase 的存储空间甚至可以降低到用户原有数据库系统存储空间的十分之一,能为企业降本增效发挥重要作用,帮助企业更好地打造核心竞争力。
OceanBase 如何做好数据压缩
LSM-Tree存储架构为数据库压缩提供了更多的可能
尽管对 B+ 树类的存储引擎进行更高效的压缩也有很多研究,例如在 FAST 2022 上,《Closing the B+-tree vs. LSM-tree Write Amplification Gap on Modern Storage Hardware with Built-in Transparent Compression》这篇论文通过可计算存储硬件(Computational storage device),利用存储硬件内部的透明压缩能力对 B 树类存储引擎的数据压缩进行优化,使其写放大达到了接近 LSM-Tree 架构存储引擎的效果。但 LSM-tree 中内存数据页更新与数据块落盘解耦,和 sstable 数据紧凑排布的特点,使得 LSM-tree 相对 B 树类存储引擎,仍然更适合在对查询/更新带来更少负面影响的前提下实现更高效的数据压缩。
利用好批量数据落盘的特性,才能实现更高效的压缩
由于 LSM-Tree 的结构特性,OceanBase的持久化数据是批量落盘的,将变更数据保存在内存中,并通过后台任务批量写入到磁盘上,这样做可以让数据块管理更简单:
- 消除了传统 B+Tree 的磁盘随机写瓶颈和存储空间碎片化问题,使得数据写入性能比传统的实时更新数据块的方式更高;
- 可以将数据更新(增删改)与压缩动作解耦,数据更新路径上没有压缩动作,对性能影响更小;
- 在批量落盘的过程中,数据库可以自适应地调整数据块中的数据数量,并在连续对数据块进行压缩的过程中,利用上一个块的压缩率等先验知识对下一个块进行更好的压缩。
OceanBase 充分利用批量数据落盘的特性,实现了更高的数据压缩比。在 OceanBase 中数据的更新会写入到 clog 和 memtable 中,OceanBase 的 memtable 是内存中的 B+ 树索引,提供高效的事务处理能力。memtable 会定期通过 compaction 生成硬盘持久化数据 sstable,多层 sstable 会采用 leveled compaction 策略进行增量数据重整。sstable 中数据块的存储分为两层,其中 2M 定长的数据块(宏块)作为 sstable 写入 IO 的最小单元,存储在宏块中的变长数据块(微块)作为数据块压缩和读 IO 的最小单元。
数据编码不应该成为手动挡,它应该成为自动挡
我们认为让数据库来选择数据的编码方式,有利于用户更好地使用编码功能。OceanBase 的自适应编码技术会自动为每个数据块选择合适的编码算法,充分降低用户的学习成本。OceanBase 从 2.0 版本开始引入了行列混存的微块存储格式(PAX),充分利用了同一列数据的局部性和类型特征,在微块内部对一组行以列存的方式存储,并针对数据特征按列进行编码。变长的数据块和连续批量压缩的数据也可以让我们通过同一个 sstable 中已经完成压缩的数据块的先验知识来对下一个数据块的压缩进行指导,在数据块中压缩尽量多的数据行,并选择更优的编码算法。也正是 LSM-Tree 的存储架构使得 OceanBase 能在事务性能和压缩率上取得更好的平衡。
如何做出更适合实际业务场景的数据压缩
实现更高压缩率的关键:通用压缩+数据编码
OceanBase 中同时支持不感知数据特征的通用压缩 (compression) 和感知数据特征并按列进行压缩的数据编码 (encoding)。这两种压缩方式是正交的,也就是说我们可以对一个数据块先进行编码,然后再进行通用压缩,来实现更高的压缩率。
OceanBase 中的通用压缩是在不感知微块内部数据格式的前提下,将整个微块通过通用压缩算法进行压缩,依赖通用压缩算法来检测并消除微块中的数据冗余。目前 OceanBase 支持用户选择 zlib, snappy, zstd, lz4 算法进行通用压缩。通常 snappy 和 lz4 压缩速度比较快,但压缩率比较低,其中 lz4 压缩与解压的速度会更快一些。zlib 和 zstd 压缩率比较高但是压缩速度相对更慢,其中 zstd 解压速度会更快一些。用户可以根据表的应用场景,通过 DDL 对指定表的通用压缩算法进行配置和变更。
由于通用压缩后的数据块在读取进行扫描前需要对整个微块进行解压,会消耗一定 CPU 并带来 overhead。为了降低解压数据块对于查询性能的影响,OceanBase 将解压数据的动作交给异步 IO 线程来进行,当数据块的 IO 结束后,会调用回调函数对数据块进行解压,并按需将解压后的数据块放在 block cache 中。这样结合查询时对预读 (prefetching) 技术的应用,可以为查询处理线程提供数据块的流水线,消除掉解压带来的额外开销。
通用压缩的优点是对被压缩的数据没有任何假设,任何数据都可能找到模式并压缩。但对于关系型数据库来说系统对数据库内存储的结构化数据有着更多的先验知识,OceanBase 认为利用这些先验知识可以对数据进行更高效的压缩。
OceanBase的数据编码算法
上文提到在关系型数据库中,由于 schema 和数据类型的限制,同一列的数据类型,精度,值域往往都是相同的。而且在实际应用中,同一列中相邻的数据也通常会有自己的特征。如下面两个场景:
- 业务中通过一列数据存储城市,性别,产品分类等具有类型属性的值时,这些列数据块内部数据的基数(cardinality)也会比较小,这时数据库可以直接在用户数据字段上建立字典,来实现更高的压缩率;
- 在有的业务场景中数据是按时序插入到数据库中的,那么这些插入的数据行中的时间相关字段,自增序列等数据的值域会相对比较小,也会有单调递增等特性,那么利用这些特性数据库也可以更方便地为这些数据做 bit-packing,差值等编码。
为了实现更高的压缩比,帮助用户大幅降低存储成本,OceanBase 设计了多种编码算法,最终在 OceanBase 的负载上实现了很好的压缩效果。OceanBase根据实际业务场景需求实现了单列数据的 bit-packing 编码、字符串 HEX 编码、字典编码、RLE 编码、常量编码、数值差值编码、定长字符串差值编码,同时也创新地引入了列间等值编码和列间子串编码,能够分别对数据库中一列数据或几列数据间可能产生的不同类型数据冗余进行压缩。
Bit-packing 和 HEX 编码:降低存储的位宽
Bit-packing 和 HEX 编码类似,都是在压缩数据的基数较小时,通过更小位宽的编码来表示原数据。比如对一列 int64 类型的数据,数据的值域在 [0, 7] 之间,这时我们可以通过存储低 3 位数据来表示元数据,减小不必要的全'0'高位数据的存储。 或者对一列字符串类型的数据,如果所有字符的基数小于 17,那么可以将出现过的每个字符映射到[0x0, 0xF]内的一个 16 进制数上,这样可以用一个 4 位的 16 进制数来表示原字符,减小每个字符编码后的存储空间。而且这两种编码可以与其他编码叠加,对于其他编码产生的数值或字符串数据,都可以再通过 bit-packing 或 HEX 编码进一步去除冗余。
bit-packing
HEX 编码
字典编码和RLE编码:单列数据去重
字典编码则可以通过在数据块内建立字典,来对低基数的数据进行压缩。当低基数的数据在微块内的分布也具有局部性,也就是列内相同数据会分布在相邻的行中时,我们也可以使用游程 (RLE) 编码的方式对存储的字典的引用值进行压缩。更进一步地,当低基数列的数据块中大部分行都是相同的值,我们还会采用常量编码,将出现频率最高的数据存储为常量值,其他非常量值数据和对应的行下标存储为 exception list,进行更进一步的压缩。
字典编码/RLE 编码
差值编码:利用数据的值域压缩
差值编码也是常用的编码方法,OceanBase 中的差值编码分为数值差值编码和定长字符串差值编码。数值差值编码主要用来对值域较小的数值类数据类型进行压缩。对于日期,时间戳等数据,或其他临近数据差值较小的数值类数据,可以只存储最小值,每行存储原数据与最小值的差值,这些差值通常也可以通过 bit-packing 压缩。定长字符串编码则可以比较好地对人工生成的 ID,如订单号/身份证号,或 url 等有一定模式的字符串进行压缩,对一个微块的数据存储一个模式串,每行额外存储与模式串不同的子串差值,来达到更好的压缩效果。
整形差值
字符串差值
列间编码:减小多列数据冗余
为了利用不同列间数据的相似性增强压缩效果,OceanBase 还引入了列间编码。通常情况下,列存数据库只会对数据在列内部进行编码,但在实际应用中有很多表除了同一列数据之间存在相似性,不同列的数据之间也可能有一定的关系。
- 当两列数据大部分值相同时,使用列间等值编码,这样一整列都是另外一列的引用,可以只存储与引用列的行不同的数据;
- 当一列数据是另外一列数据的前缀时,也可以使用列间子串编码,只存储完整的一列和一列的后缀。
这种列间编码可以对复合列、系统生成的一些数据做出更好的压缩,也能够降低在数据表设计范式上的问题导致的数据冗余。
自适应压缩技术:让数据库选择编码算法
数据编码的压缩效果不仅与表的 schema 相关,同时还与数据的分布,微块内数据值域等数据本身的特征相关,这也就意味着比较难以在用户设计表数据模型时指定列编码来实现最好的压缩效果。为了减轻用户的使用负担,也为了实现更好的压缩效果,OceanBase 支持在合并过程中自适应地探测合适的编码方式,对同一列在不同数据块中支持使用不同的算法来进行编码。
而对每个微块的每一列数据探测并选择最合适的编码需要大量的计算,会给 compaction 过程带来更多的 CPU 计算压力,所以 OceanBase 会通过分析数据类型,值域,NDV 等特征,结合 compaction 任务中上一个微块对应列选择的编码算法和压缩率,通过一个启发式算法来完成编码选择,在探测到相对更适合的编码算法的同时保证了合并时数据编码带来的计算开销在可以接受的区间内。
最后一步:编码数据的查询优化
为了能够更好地平衡压缩效果和查询的性能,我们在设计数据编码格式时也考虑到了对查询性能带来的影响。
行级粒度数据随机访问
通用压缩中如果要访问一个压缩块中的一部分数据通常需要将整个数据块解压后访问;某些分析型系统的数据编码中面向的多是扫描的场景,点查的场景比较少,所以采用了在访问某一行数据时需要对相邻数据行或数据块内读取行之前所有行进行解码计算的数据编码的格式(如 PFor 等差值编码)。
OceanBase 需要更好地支持事务型负载,就意味着要支持相对更高效的点查,因此 OceanBase 在设计数据编码格式时保证了编码后的数据是可以以行为粒度随机访问的。也就是在对某一行进行点查时只需要对这一行相关的元数据进行访问并解码,减小了随机点查时的计算放大。同时对于编码格式的微块,解码数据所需要的所有元数据都存储在微块内,让数据微块有自解释的能力,也在解码时提供了更好的内存局部性。
缓存解码器
在 OceanBase 目前的数据解码实现中,每一列数据都需要初始化一个解码器对象来解析数据,构造解码器时会需要进行一些计算和内存分配,为了进一步减小访问编码数据时的 RT,OceanBase 会将数据的解码器和数据块一起缓存在 block cache 中,访问 cache 中数据块时可以直接通过缓存的解码器解析数据,而解码器的构造与缓存也是由异步 IO 的回调线程来完成的,以降低初始化解码器的 overhead。当不能命中block cache中缓存的解码器时,OceanBase 还会为解码器用到的元数据内存和对象构建缓存池,在不同查询间复用这些内存和对象。
通过上述细节上的优化,行列混存格式的 sstable 编码数据也可以很好地支持事务型负载。
由于编码数据行列混存的格式,使得在分析型查询的处理上,编码数据有着和列存数据相似的特性,数据分布更紧凑,对 CPU cache 更加友好。这些特性也让我们可以应用列存常用的一些优化手段对分析型查询进行优化,充分利用 SIMD 等方法来提供更高效的分析型负载处理。
同时由于编码数据中我们会存储字典、null bitmap、常量等可以描述数据分布的元数据,在扫描数据时我们也可以利用这些数据对于部分过滤,聚合算子的执行过程进行优化,实现在压缩数据上直接进行计算。许多数据仓库也会用类似的方法来优化查询执行,在 SIGMOD 2022 中,《CompressDB: Enabling Efficient Compressed Data Direct Processing for Various Databases》这篇论文也系统地采用了类似的思想,通过将部分计算下推到存储层,直接在未解压的数据上执行来提高系统的效率,得到了很不错的性能。
OceanBase 在 3.2 版本中对分析处理能力进行了大幅的优化,其中就包括聚合与过滤计算下推到存储层执行,和在向量化引擎中利用编码数据的列存特征进行向量化的批量解码。在查询时充分利用了编码元数据和编码数据的列存储局部性,在编码数据上直接进行计算,大幅提高了下推算子的执行效率和向量化引擎中的数据解码效率。基于数据编码的计算下推和向量化解码也成为了支持 OceanBase 高效处理分析型负载,在 TPC-H benchmark 中达到优秀性能指标的重要功能。
数据编码压缩的效果测试
不同的压缩方式如何影响 OceanBase 的压缩效果,以下我们会通过一个简单的测试进行观察。
我们使用 OceanBase 4.0 版本,分别在交易场景的 TPC-H 10g 的数据模型和用户行为日志场景的 IJCAI-16 Brick-and-Mortar Store Recommendation Dataset 数据集上对 OceanBase 的压缩率进行测试:
- TPC-H 是对订单,交易场景的建模,我们对TPC-H模型中数据量比较大的两张表:存储订单的 ORDERS 表,和存储商品信息的 LINEITEM 表的压缩率进行统计。在 OceanBase 默认配置(zstd + encoding)下,这两张表的压缩率可以达到 4.6 左右,相较只开启 encoding 或 zstd 压缩时提升明显。
- IJCAI-16 taobao user log 则是淘宝脱敏后的真实业务数据,存储了用户浏览商品时的行为日志。在 OceanBase 默认配置(zstd + encoding)下压缩率可以达到 9.9,只开启 encoding 压缩率可以达到 8.3,只开启 zstd 压缩率为 6.0。
可以看到 OceanBase 在面对真实的业务数据时会有更出色的数据压缩效果,在 TPC-H 这种数据冗余相对更少的数据集上也有着优秀的数据压缩能力。
写在最后
本文介绍了 OceanBase 对于数据库中数据压缩的思考和实现方案。目前 OceanBase 已基于 LSM-Tree 存储引擎,在存储成本与查询性能之间实现平衡,并完成了系统的工程实践。
我们很高兴地看到,OceanBase 自研存储引擎的压缩能力已经在金融、政府公共服务、能源、交通、运营商等多个行业得到充分验证,显著提升了客户业务系统的稳定性、安全性,能够有效降低存储成本 70%-90%,在同一业务场景下,OceanBase 的数据存储量仅为 MySQL/Oracle 数据库的 1/4-1/3。
随着 OceanBase 系统架构的演进,我们会支持越来越多的数据类型和应用场景,提供更高效的数据管理与访问能力。OceaBase 的存储引擎仍在不断迭代升级,我们也会对数据编码压缩功能在不同场景、不同存储架构下的优化做出更多尝试,在更丰富的数据模型上,利用现代硬件的特性对数据的编码压缩效果、以及编码数据的访问做出更多的优化,不断地为用户带来更好的使用体验。