文 / 陈宗志(暴跳)麻生希ed2k
在阿里云仙境旗下的云原生数据库PolarDB中,通过轻量级压缩的竣事,可以在减少数据大小的同期,在一定进程上普及性能。这是何如竣事的呢?
针对这一问题,本文将主要先容PolarDB MySQL引擎层的索引前缀压缩才能(Index Prefix Compression)的时代竣事和着力。
布景
近几年,数据压缩、分级存储等时代成为了数据库家具(在时代层面上)竣事降本的中枢技能。动作一款云原生数据库,PolarDB面向浩荡行业、场景、需求不同的云用户,相似有必要且照旧竣事了这些才能。PolarDB在全链路多个层级上照旧竣事并正在渐渐交易化数据压缩才能,如整形、字符串、BLOB等数据阵势类型的压缩,数据列字典压缩、二级索引前缀压缩,存储层的数据块软/硬件压缩等。
来源要提到的势必是MySQL官方原生的两种压缩才能:表压缩和透明页压缩。这两种压缩才能由于各式原因,在实践线上业务中并莫得被鄙俗认同和使用。举例,前者在 Buffer pool 中存在两个版块数据且有较为复杂的和会逻辑,后者需要文献系统复古 punch hole 只可对带宽压缩而莫得优化 IOPS,两者只采取甘休的、相对支出较高的通用块压缩算法等。它们的实测弘扬也导致传统 MySQL 用户浩荡合计压缩会放手不少性能,并带来较多复杂度。
事实上,在通用块压缩基础上,淌若可以引入更空洞的轻量级压缩,以至在压缩后的数据上平直进行计较,那么可以在竣事数据压缩的同期又保证以至普及性能。况且,在计存分离架构下,费力 I/O 时延更长,淌若可以通过压缩减少数据大小,从而减少 I/O,压缩带来的收益比拟于土产货皮就愈加显然。
由MySQL向外扩展来看,针对:(1)动态(update-in-place)或静态(append-only)数据;(2)行存或列存组织组织(数据同质性不同);(3)有序链或无序堆组织的数据(数据局部性不同)等不恻隐况,能适用的压缩技艺亦然不同的,况且压缩能得回的着力会有很大相反。因此,关于PolarDB MySQL,除了官方的两种原生压缩才能,通过轻量级压缩技艺竣事页内/行级压缩,亦然进犯发展旅途。
PolarDB前缀压缩
通过建造索引结构可以普及数据检索的性能,代价是特别的写放大和诊治索引结构的存储空间。OLTP中为了复古多种探听旅途,比较常见的情况是在一个表上建造相配多的索引,这就导致索引在数据库举座存储空间中占了很大比例。索引存储占到 50% 以上的实例并不有数,这些实例时常在单表会有几十个二级索引。
由于索引的 key 部分数据存在有序性,因此对索引 key 部分进行前缀压缩时时可以取得可以的压缩着力。淌若用户的数据表中存在较多的索引(如一些作念saas的用户),索引数据量相对举座数据量的占比不低,此时前缀压缩的收益其实十分可不雅。
咱们先陋劣了解一下 InnoDB 的索引结构,关于主键 record,来源是统统主键 key 的字段列、再短长key数据的字段列;而二级索引 record,则先是对应二级索引 key 的字段列、再是主键key的字段列。
值得一提的是,部分交易数据库在竣事 non-unique index 时,一般会将疏导的二级索引对应的主键索引鸠集存放,这么二级索引 key 部分的数据只需要存一份(Duplicate Key Removal)。而在 InnoDB 中的竣事较为陋劣,每个二级索引 record 为叠加的二级索引 key 字段加不同主键 key,这加重了 InnoDB 索引数据彭胀的问题。
前缀压缩计议旨趣
前缀压缩其实有多种具体竣事,比如同个Page的record前缀叠加出现部分的平直压缩,如前缀为 "aaaaa" 平直压缩为 "a5";或相对前一纪录叠加部分的压缩,又或相对具体元素的前缀叠加部分,提真金不怕火 "aaaaa" 到全球区域动作前缀。
咱们采取的技艺是将record分为两个部分:前缀部分在多个 record 之间分享,因此可以只存储一份麻生希ed2k,从而竣事数据压缩;后缀部分由每个 record 单独存储。因此压缩后的 record 中只存储了前缀部分的指针 + 后缀部分的数据。
对数据页内的 record 进行前缀压缩着力:
采取前缀压缩,可以灵验减少 btree 索引的节点数目:
数据的压缩
以 insert 为例,当经过事务解决、纪录构建、索引定位等等操作后,最终会走到 btree 操作的底层函数中。这里会将获取的 dtuple_t 改造成 rec_t 采用(乐不雅/悲不雅)模式后插入数据,这时候应该要有计划压缩逻辑了。咱们此时照旧拿了所需的 page 锁,因此可以保证 page 内相干信息的独占性,统统需要 page 中压缩接济信息内容的行压缩可以在这一步竣事。在这一过程中进行压缩使得 rec_t 中的数据为压缩数据,同期需要在 rec_t 保留相干的元信息。
关于前缀压缩,咱们采取压缩的时机是 lazy 的,即新插入的 record 在 page 上保执非压缩情景,比及 page 容量触发阈值时,再对 page 举座进行压缩,这么保证压缩支出被均派到屡次 DML 操作上,而不会每次操作齐有压缩支出。
而触发 encode 阈值是在 optimistic 旅途的 page 满且判断 reorganize 也无法腾出空间时触发。原来会放锁干涉 SMO 历程,咱们这里先尝试 page 级别举座的encode。
不在 SMO 时作念压缩是因为其执有 index latch 和多个 page latch,对并发操作的影响领域太大,其次 page 里面的压缩不需要依赖其他信息。page 级别的压缩会尝试对统统纪录进行最优化中式前缀压缩元信息,并判断对应生成的新 symbol table 是否会有实足收益,有则压缩数据并更新。
咱们在 record 的 Info bits 上拓展了一个 bit 来表征此纪录是否是压缩阵势,老版块纪录对应象征不会被建立从而统统兼容原有操作旅途。在一个 page 页内可以同期存在压缩和非压缩两种类型的纪录,字据对应象征位判断解决模式。
数据的解压
来源需要保证在统统 record 使用旅途上,解压逻辑省略全面遮掩,让用户拿到原始纪录。
其次,InnoDB 里面也存在 dtuple_t(内存纪录阵势)和 rec_t(页上物理纪录阵势)两种 record 阵势类型的改造与比较。当数据前缀压缩后可能失去列属性,因此 rec_get_offsets 等函数无法对压缩后的 rec_t 平直领略,需要对应的纠正相应函数获取 rec_t 中的物理数据偏移。
另外,InnoDB 纪录的比较是基于列的,offsets 实质是接济领略 rec_t 至各列的结构,只须保证相应信息能将压缩部分数据也能领略出来,就可以用压缩 rec_t、压缩元信息以及对应的 offsets,去和 dtuple_t 改造或比较。
总的来说,关于压缩的record,要么先统统解压构建原来的 rec_t 数据走原来比较逻辑,要么用纠正过的 offsets 或 dtuple_t 以及对应的列比较践诺函数来作念比较(可施展压缩计较)。PolarDB当今在不同旅途上会字据环境条目从两种格式中给与之一。
前缀压缩的典型哄骗
关于如SaaS/电市集景等一些用户,其数据表中存在较多的索引可以通过前缀压缩的格式捏造存储本钱。况且咱们了解到,在许多实践场景下,表数据中有浩荡的冗余叠加数据,天然单表中悉数有 1 亿行,可是某一转,比如品类唯有 200 种傍边,这种是最常见的场景。
这种场景在sysbench-toolkit里面是saas_multi_index场景:https://github.com/baotiao/sysbench-toolkit
从底下的测试数据可以看到,在SaaS/电商等典型场景里,前缀压缩可以得回比较高的压缩率,同期又能普及举座读写性能。
IO Bound 场景
表结构如下:
CREATE TABLE `prefix_off_saas_log_10w%d` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `saas_type` varchar(64) DEFAULT NULL, `saas_currency_code` varchar(3) DEFAULT NULL, `saas_amount` bigint(20) DEFAULT '0', `saas_direction` varchar(2) DEFAULT 'NA', `saas_status` varchar(64) DEFAULT NULL, `ewallet_ref` varchar(64) DEFAULT NULL, `merchant_ref` varchar(64) DEFAULT NULL, `third_party_ref` varchar(64) DEFAULT NULL, `created_date_time` datetime DEFAULT NULL, `updated_date_time` datetime DEFAULT NULL, `version` int(11) DEFAULT NULL, `saas_date_time` datetime DEFAULT NULL, `original_saas_ref` varchar(64) DEFAULT NULL, `source_of_fund` varchar(64) DEFAULT NULL, `external_saas_type` varchar(64) DEFAULT NULL, `user_id` varchar(64) DEFAULT NULL, `merchant_id` varchar(64) DEFAULT NULL, `merchant_id_ext` varchar(64) DEFAULT NULL, `mfg_no` varchar(64) DEFAULT NULL, `rfid_tag_no` varchar(64) DEFAULT NULL, `admin_fee` bigint(20) DEFAULT NULL, `ppu_type` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`), KEY `saas_log_idx01` (`user_id`) USING BTREE, KEY `saas_log_idx02` (`saas_type`) USING BTREE, KEY `saas_log_idx03` (`saas_status`) USING BTREE, KEY `saas_log_idx04` (`merchant_ref`) USING BTREE, KEY `saas_log_idx05` (`third_party_ref`) USING BTREE, KEY `saas_log_idx08` (`mfg_no`) USING BTREE, KEY `saas_log_idx09` (`rfid_tag_no`) USING BTREE, KEY `saas_log_idx10` (`merchant_id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8IO Bound场景就地读,采取就地point read,128线程,4G Buffer Pool,二级索引大小20G,压缩后 3.5G。测得压缩后QPS是49w,非压缩是26w,压裁汰长压缩的1.88倍。
可以看到,在开启压缩之后,性能并莫得下跌,而是有一定进程的普及,原因如下:
压缩可以减少btree叶子节点的数目,在IO bound场景增多了 buffer pool 对叶子节点的遮掩率,遮掩更多的 page 意味着就地读场景更少的 page 换入换出,对 bp 的 hash table 和 lru list 探听频率更小,hash 锁和 lru list 锁竞争更少,此外,对文献系统的 IO 次数更少,用户线程平直掷中 BP 即可复返。
CPU Bound 场景
CPU Bound场景就地写(index锁突破),256 线程,100G Buffer Pool实足大,单表,一个二级索引,为了着力愈加显然,将二级索引的行长建立为500,insert场景,测得压缩10w QPS,非压缩8w QPS,压裁汰长压缩的1.25倍。
page中 record 密度更大,减少了 page 分辩频率,缓解了分辩对 index SX 锁的争抢,而且减少了正在分辩节点的父节点拿的 X 锁数目,缓解了对其叶子节点的插入。此外,为了减少开启压缩后 SMO 时拿 index 锁时代,压缩旅途不遮掩 SMO 过程。
CPU Bound场景就地读,压缩和非压缩性能差未几。
四房色播在 bp 实足大时进行就地读取,那么压缩并不会带来性能普及,但探听压缩 record 会带来一定解压支出,但解压支出很小(内存的就地探听),因此读取性能差未几。
压缩率
还有一个比较矜恤的问题是压缩着力,当今每个 page 有 symbol table,纪录了全球前缀,且一个 record 压缩到临了是有一部分元数据的。是以并不是 record 越大,压缩率就一定会更好的。假定全球前缀部分基本占据了通盘 record,那么经过演算得到压缩率随 record size 的变化弧线是抛物线,由于默许 row 阵势时 dynamic,其index key长度限定是 3072Bytes,终点于 1024 个 utf8 字符,这个值小于压缩率取到极值的点。
测试收场
测试二级索引大小对压缩率的影响,探讨压缩的极限压缩率。单线程规矩insert 400w~800w条数据,datasize不来源128的插入800w行麻生希ed2k,datasize大于128的插入400w行。采取最大的叠加率,即每个page里面唯有几种rec。data size是二级索引字段的大小,单元是utf8字符,data size为32终点于96Bytes,其未压缩的索引大小是压缩的2.92倍。瞩目,不同灌数据格式会导致不同的压缩率,这里测的是单线程就地插入,压缩着力优于多线程并发插入,因为并发插入可能导致无须要的page分辩。