发布日期:2024-08-26 14:28 点击次数:185
文 / 陈宗志(暴跳)【ONSD-403】地上波では見られない‘本番’4時間
在阿里云仙境旗下的云原生数据库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 之间分享,因此可以只存储一份,从辛劳毕数据压缩;后缀部分由每个 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行,datasize大于128的插入400w行。罗致最大的相通率,即每个page里面独一几种rec。data size是二级索引字段的大小【ONSD-403】地上波では見られない‘本番’4時間,单元是utf8字符,data size为32绝顶于96Bytes,其未压缩的索引大小是压缩的2.92倍。留心,不同灌数据花式会导致不同的压缩率,这里测的是单线程立时插入,压缩着力优于多线程并发插入,因为并发插入可能导致毋庸要的page分离。