引言
PolarDB-X 是由阿里云自主研发的云原生分布式数据库,专注解决海量数据存储、超高并发吞吐、大表瓶颈以及复杂计算效率等数据库瓶颈难题,真正经历了各届天猫双11及阿里云各行业客户业务的考验。
在已经发布的PolarDB-X 5.4.14版本中,我们推出了冷热数据分离存储(下文简称PolarDB-X on OSS)这一新功能,以极低的成本来存储海量规模的冷数据,并支持高效的主键与索引点查、复杂分析型查询,满足高可用、MySQL兼容性和任意时间点闪回等特性。
PolarDB-X on OSS 将低成本做到了极致。除了OSS对象存储服务本身的存储成本优势外,还有一个重要的原因就是PolarDB-X极致的数据压缩能力。本文将对多款数据库产品的压缩能力进行测评对比,并展示其背后的设计原理。
压缩能力测评
我们测试所需的数据,以及HBase、MySQL、MongoDB相关的压缩能力测试数据,都来源于文章:文章链接
在这篇文章的基础上,我们使用相同测试集对PolarDB-X on OSS的压缩能力进行检验。
数据准备
- TPCH
参考文章导入TPCH数据集
CREATE TABLE `orders` ( `o_orderkey` int(11) NOT NULL, `o_custkey` int(11) NOT NULL, `o_orderstatus` varchar(1) NOT NULL, `o_totalprice` decimal(15, 2) NOT NULL, `o_orderdate` date NOT NULL, `o_orderpriority` varchar(15) NOT NULL, `o_clerk` varchar(15) NOT NULL, `o_shippriority` int(11) NOT NULL, `o_comment` varchar(79) NOT NULL, PRIMARY KEY (`o_orderdate`, `o_orderkey`), KEY `auto_shard_key_o_orderkey` USING BTREE (`o_orderkey`) ) ENGINE = InnoDB DEFAULT CHARSET = latin1 PARTITION BY KEY(`o_orderkey`) PARTITIONS 4
执行冷数据一键迁移归档:
create table orders like sizetest.orders engine = 'oss' archive_mode = 'loading';
执行结果如下:
- NGSIM
CREATE TABLE `ngsim` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `Vehicle_ID` int(11) NOT NULL, `Frame_ID` int(11) NOT NULL, `Total_Frames` int(11) NOT NULL, `Global_Time` bigint(20) NOT NULL, `Local_X` decimal(10, 3) NOT NULL, `Local_Y` decimal(10, 3) NOT NULL, `Global_X` decimal(15, 3) NOT NULL, `Global_Y` decimal(15, 3) NOT NULL, `v_length` decimal(10, 3) NOT NULL, `v_Width` decimal(10, 3) NOT NULL, `v_Class` int(11) NOT NULL, `v_Vel` decimal(10, 3) NOT NULL, `v_Acc` decimal(10, 3) NOT NULL, `Lane_ID` int(11) NOT NULL, `O_Zone` char(10) DEFAULT NULL, `D_Zone` char(10) DEFAULT NULL, `Int_ID` char(10) DEFAULT NULL, `Section_ID` char(10) DEFAULT NULL, `Direction` char(10) DEFAULT NULL, `Movement` char(10) DEFAULT NULL, `Preceding` int(11) NOT NULL, `Following` int(11) NOT NULL, `Space_Headway` decimal(10, 3) NOT NULL, `Time_Headway` decimal(10, 3) NOT NULL, `Location` char(10) NOT NULL, PRIMARY KEY (`ID`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 PARTITION BY KEY(`ID`) PARTITIONS 4 /*+TDDL:load_data_auto_fill_auto_increment_column=true*/ load data local infile './dataset/Next_Generation_Simulation__NGSIM__Vehicle_Trajectories_and_Supporting_Data.csv' into table ngsim FIELDS TERMINATED BY ',' IGNORE 1 LINES (Vehicle_ID,Frame_ID,Total_Frames,Global_Time,Local_X,Local_Y,Global_X,Global_Y,v_length,v_Width,v_Class,v_Vel,v_Acc,Lane_ID,O_Zone,D_Zone,Int_ID,Section_ID,Direction,Movement,Preceding,Following,Space_Headway,Time_Headway,Location)
执行冷数据一键迁移归档:
create table ngsim like sizetest.ngsim engine = 'oss' archive_mode = 'loading';
执行结果如下:
- ACCESS_LOG
CREATE TABLE `ACCESS_LOG` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `CONTENT` varchar(10000) DEFAULT NULL, PRIMARY KEY (`ID`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 PARTITION BY KEY(`ID`) PARTITIONS 4 /*+TDDL:load_data_auto_fill_auto_increment_column=true*/ load data local infile './dataset/access.log' into table ACCESS_LOG FIELDS TERMINATED BY '\n' IGNORE 1 LINES (CONTENT)
执行冷数据一键迁移归档:
create table ACCESS_LOG like sizetest.ACCESS_LOG engine = 'oss' archive_mode = 'loading';
执行结果如下:
- IJCAI-15
CREATE TABLE `USER_LOG` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `USER_ID` int(11) NOT NULL, `ITEM_ID` int(11) NOT NULL, `CAT_ID` int(11) NOT NULL, `SELLER_ID` int(11) NOT NULL, `BRAND_ID` int(11) DEFAULT NULL, `TIME_STAMP` char(4) NOT NULL, `ACTION_TYPE` char(1) NOT NULL, PRIMARY KEY (`ID`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 PARTITION BY KEY(`ID`) PARTITIONS 4 /*+TDDL:load_data_auto_fill_auto_increment_column=true*/ load data local infile './dataset/user_log_format1.csv' into table user_log FIELDS TERMINATED BY ',' IGNORE 1 LINES (user_id,item_id,cat_id,seller_id,brand_id,time_stamp,action_type)
执行冷数据一键迁移归档:
create table USER_LOG like sizetest.USER_LOG engine = 'oss' archive_mode = 'loading';
执行结果如下:
数据校验
通过PolarDB-X内置的校验聚合函数check_sum对归档后的冷数据表进行数据校验,确保数据的完整性、正确性:
空间占用统计
通过information_schema.files视图来查看表的空间占用大小:
select logical_table_name, sum(table_rows),sum(extent_size)/1024/1024 as `size(MB)` from information_schema.files where logical_schema_name = 'sizetest_oss' and remove_ts is null and life_cycle = 1 and logical_table_name in ('ACCESS_LOG', 'ngsim', 'orders', 'USER_LOG') group by logical_table_name;
查询结果如下:
测试结果
根据以上测试,和引用文章中已有的数据库测评结果进行对比,得到以下结果,PolarDB-X 冷数据在存储空间上,整体比较有优势。
存储成本分析
说明:
- 基于LSM Tree的HBase,通常基于compaction做通用场景的优化,具备普适性,这也是业界NewSQL数据库基于LSM Tree做存储引擎所具备的能力,通用在3~4倍的压缩率。
- PolarDB-X的热数据,默认采用MySQL InnoDB,整体的空间基本和开源MySQL对齐,PolarDB-X的冷数据,相比于PolarDB-X的热数据场景,平均有3~4倍的压缩,基本对齐LSM Tree的压缩场景,另外,面向字符和数值类型居多的场景最多有10倍的压缩效果。
- PolarDB-X基于存储介质的选型和优化,PolarDB-X On OSS的冷数据归档,结合高压缩率以及HDD存储成本的优势,成本仅为PolarDB-X热数据的1/20,具体计算公式 = 1 / (3~4倍压缩率 * 6倍的OSS存储成本优势)。
原理
冷热架构分离
参考文档:
《PolarDB-X on OSS: 冷热数据分离存储》
《PolarDB-X OSS冷数据备份恢复》
核心特性:
- 极低的冷数据存储成本,仅为在线热数据的1/20成本。
- 一个数据库实例,一个SQL引擎可以同时访问热数据和冷数据,具备跨数据的join/union/subquery等。在数据库运维上,冷热数据具备一体化的备份恢复能力,支持任意时间点恢复。
- 数据入湖,冷数据基于开源ORCFile通用格式,可以轻松对接Spark/Flink等开源生态,后续我们会开放对应Connector扩展接入大数据生态。
ORCFile
PolarDB-X 冷数据以开源格式ORC作为表文件的存储格式。ORC采用列式存储,并采取run-length encoding、dictionary encoding等算法对数值类型和字符类型进行高效编码。在此基础上,再选取LZ4压缩算法对编码后的数据进行通用压缩。由于PolarDB-X是MySQL生态下的分布式数据库,对MySQL数据类型完全兼容,因此还需要对MySQL主要数据类型进行不同的序列化设计,来适配ORC本身的压缩能力,从而将最终的存储空间优化到极致。
列存高效压缩
- run-length encoding
run-length encoding是用于整数类型的一套编码算法,其思路来源于Protobuf 提出的varint数值压缩。主流语言都使用32bit或64bit的固定长度来存储数值,但是在实际场景中,绝大多数的数值是比较小的,有效数据只集中在低位字节。varint压缩算法将数值中每7个bit进行一次分隔,将分隔后的比特串按小端序排列;再用最高位的1bit来标识编码是否继续:比特位为为1代表编码继续,为0代表编码终止。以Java long类型数字551为例,原始数据占据64bit,二进制串为:00000000 00000000 00000000 00000000 00000000 00000000 00000010 00100111 varint算法先按7bit进行有效位的拆分,并以小端序排列,得到:0100111 0000100 再补齐最高的比特位:10100111 00000100
这样我们变得到了最终的压缩数据:0xA7 0x04。相比于原来8字节的固定长度,编码后的数据只占据2字节的空间。对于数值x,其压缩后的空间占用为:
例如将上述例子中的数值551带入,可以得到空间占用为2字节。
- dictionary encoding
dictionary encoding(字典编码)用于压缩字符串数据。它的基本思想是,先从一组字符串中统计出不重复的值,称为“字典”;再存储对字典值的引用。不重复值的数目在数据库领域中称为NDV,是"number of distinct value"的缩写。显然,NDV越小,则字典值越少,字典压缩的效率就越高。例如我们要存储颜色这列字符,其字典值为“green”、“blue”、“red”,引用值分别为0、1、2,那么最终只需要存储012这三种数字即可。相比于直接存储字符串,字典压缩大大减少了存储空间的占用。
数据类型序列化
PolarDB-X为不同MySQL数据类型设计了相应的序列化方式,从而将复杂数据类型转化为数字或字节序列,用于适配ORC的字典编码和run-length编码。序列化过程要保证数据精度不会损失,并且保证序列化后的数字顺序或字节顺序能够反映不同类型下数据的真实大小关系。
数值类型
MySQL的数值类型中,bigint等整数类型可以直接应用于ORC;需要重点解决的是decimal类型的序列化设计。PolarDB-X采取多项式的形式来表示任意精度、任意小数的decimal;每个多项式系数使用integer整型存储,系数取值范围在10的9次方以内:
我们可以在此表示形式的基础上,将每个多项式的系数以更紧密的形式进行字节排列。例如对于数字1234567890.1234,可以拆分成多项式系数 1-234567890-123400000。将系数转化为16进制形式,再截取系数两端的0值,形成更紧密的字节排列,得到最终序列化结果0x01 0x0D 0xFB 0x38 0xD2 0x04 0xD2:
Decimal精度、小数位数与最终序列化后的大小关系如下,其中i代表整数部分长度,f代表小数部分长度:
例如对于decimal(15,2),其序列化得到的字节长度为7。
时间类型
PolarDB-X 针对各个时间类型的特征进行了不同的序列化方式:针对时区无关的类型datetime、date、time,依据年月日、时分秒、微秒进行拆分;针对时区相关的类型timestamp,依据 epoch second和微秒进行拆分。以datetime类型的时间值为例,先压缩时分秒部分得到hms值,再压缩年月日部分得到ymd值。将微秒值和以上两部分组装在一起,最后取正负号:
各个时间类型最终的空间占用如下图所示:
总结
PolarDB-X冷热分离存储架构,引入数据分区的Time-to-Live(TTL)策略,随着时间推进,可以将历史不常访问的冷数据进行定期归档,PolarDB-X结合云对象存储OSS作为冷存储介质,采用编码压缩、以及OSS本身的成本优势,冷数据仅有MySQL InnoDB在线数据1/20的存储成本,同时提供单个数据库实例的透明使用体验,统一SQL访问、备份操作等。
参考文献
Apache ORC
https://orc.apache.org/specification/ORCv0/
ProtoBuf Varint
https://developers.google.com/protocol-buffers/docs/encoding
TPCH
NGSIM数据集
server log数据集
https://www.kaggle.com/datasets/eliasdabbas/web-server-access-logs
IJCAI-15数据集
https://tianchi.aliyun.com/dataset/dataDetail?spm=5176.12281978.0.0.1e4268d7WHdFvm&dataId=47
/ End /