作者:无沐
识货是上海识致信息科技有限责任公司旗下的年轻人潮流运动消费的购物决策平台,成立于2012年6月,成立初期主要为用户提供折扣、正品运动鞋商品导购,把真实的运动鞋体验传递给爱好者。通过不断发展,当前识货已经覆盖鞋服、数码、美妆、家电、食品酒水、潮玩、包表配件等多类目商品,帮助用户解决消费决策、产品评估、渠道挑选等难题。
为了应对业务的高速发展,识货使用 PolarDB-X 进行了分布式改造。经过大促实战验证,数据处理能力提升6倍,价格变更场景性能提升4倍,从小时级别缩短到分钟级别。详见:《客户说|从4小时到15分钟,一次分布式数据库的丝滑体验》。
在数据库运维过程中,对字段类型做变更是令人非常头痛的一件事,传统MySQL中,这是一个锁表的操作。识货也遇到了这样的问题,本文将描述识货是如何使用PolarDB-X原生的无锁变更能力,简单、稳定的对价格字段进行了精度扩展。
价格展示
识货在 PolarDB-X 中有着许许多多价格相关的字段,例如历史价格、市场价格、当前价格等,其中大多价格相关字段为 decimal(8,2),还有个别字段为 float(8,2),总的来说都是整数部分保留 6 位,小数部分保留 2 位。
随着业务范围扩展,识货需要覆盖更加广泛的商品,然而部分商品的价格已经超出了原先设计的价格范围,导致这部分商品暂时无法展示,价格达到百万以上的商品数据将无法进行存储。
因此,需要对数据库的表进行改造以支持更高的价格。
从 FLOAT 到 DECIMAL
商品表中个别字段类型为 float(8,2),使用 ALTER TABLE
语句可以非常快地将其变更为 float(12,2),这是因为该操作不涉及修改该列的存储方式,仅修改元元数据即可完成,可以做到“秒级”变更。
但在插入测试数据时,发现较高价格的商品存在丢失精度的现象,并且误差已经影响到了整数部分,显然这是无法接受的,因此需要升级为精确的数值类型 DECIMAL。
FLOAT 类型浮点数在计算机中是以二进制形式存储的,许多十进制小数不能被精确表示为有限长度的二进制小数,这导致了所谓的“浮点误差”。通常,浮点数的精度随着数值的增加或减小而逐渐降低,特别是当数值达到指数部分所能表示的极限时,浮点数会出现较大的误差。
DECIMAL 类型在内部以字符串形式存储,确保了数值的完全精确性。
有些价格字段本身就是 DECIMAL 类型,但是由于整数部分的位数不够,不足以存储更大的数值,也要从原来的 decimal(8,2) 变更到 decimal(12,2)。与浮点类型不同的是,这种变更需要修改了内部的存储方式,无法使用 INSTANT 或者 INPLACE 算法,需要使用会锁表的 COPY 算法。
目前商品表的数据量已经达到了10亿级别,如果使用原生 MySQL 的 DDL 变更,那么执行时间至少需要几个小时,更糟糕的是这个变更是需要锁表的,变更期间会阻塞商品表上的所有 DML。
识货作为典型的 7 * 24 服务的业务方,需要做变更的商品货品表是识货核心的业务表,也是流量最大的表,考虑到这张表承载着高频的更新操作,任何停机或锁表操作都可能对业务造成不可估量的影响,几个小时的锁表变更显然是不现实的。
因此,用户需要一种靠谱的无锁变更功能,来确保数据类型变更过程的平滑与高效。
传统单机数据库无锁变更方案
尽管 MySQL 现已支持 Online DDL,但并非所有操作都能采用高效的 INPLACE 策略执行,部分变更只能采用 COPY 方法,并且这一过程会禁止 DML 执行。针对那些仅能通过 COPY 策略执行的DDL操作,如何有效避免全表锁,以减少对业务影响,成为了一个关键问题。
目前,有许多开源的无锁变更工具为用户提供了平滑进行列类型变更的解决方案,有效规避了锁表问题。其中较为知名的工具包括 GitHub 开源项目 gh-ost 以及 Percona Toolkit 中的 pt-online-schema-change(简称 pt-osc)工具,它们的工作流程类似,都包含了以下几个步骤:
- 创建一张与原表结构一样的临时表
- 将具体变更操作应用到临时表上
- 将存量数据拷贝到临时表中
- 同步增量修改数据到临时表中
- 交换原表和临时表,完成变更
在增量数据同步方面,gh-ost 与 pt-osc 实现的方式有所不同,gh-ost 采用 binlog 订阅进行回放,而 pt-osc 采用的是利用触发器进行增量双写。增量数据同步方式的不同,使得这两个工具有着各自的优缺点:
- gh-ost
- 增量回放和存量数据拷贝公用一个线程,速度慢,但侵入性小,对业务影响小,稳定性较好
- 存量数据拷贝时,insert select lock in share mode 有额外的行锁
- 有锁表操作,存在短暂跌零现象
- 流量较大时,binlog 回放追不上,会导致永远无法完成
- 无触发器,可暂停,可在备库测试
- 无数据校验功能
- pt-ost
- 存量数据拷贝单线程,拷贝时 insert select lock in share mode 有额外的行锁
- 增量数据同步使用触发器(保证事务),可并发回放,速度相对 gh-ost 更快,但增加死锁风险
- 没有断点续传功能,且存在触发器残留问题,触发器无法暂停
- 流量较大时变更也可以完成
- 无数据校验功能
尽管 gh-ost 作为广泛应用的无锁变更工具,以其对业务影响低的特点广受好评,但在处理极高频率更新的表时,单线程 binlog 回放会产生滞后问题,可能导致变更任务无法顺利完成,因此,gh-ost 方案不适用于识货商品表变更场景。
相较于 gh-ost,pt-osc 不会出现变更永远无法完成的问题,但其采用触发器实现增量数据同步的机制也并不完美。在系统负载高、主从复制延迟或遭遇死锁等异常状况时,此方法的局限性尤为凸显:一旦变更流程启动,即使遇到紧急问题中断了 pt-osc 进程,已经创建的触发器也仍在工作,需手动进行删除,从而引入安全隐患。此外,pt-osc 也不支持数据校验功能,存在数据丢失风险。因此,pt-osc 在某些复杂场景下也不是较好的选择。
细说无锁变更过程中的锁
在存量数据拷贝阶段,gh-ost 和 pt-osc 均采用了 INSERT IGNORE SELECT 语句来高效地分批导入数据,并且都会通过预设的 chunk-size 将数据分批次处理。为了保证目标表数据的正确性,该语句还会伴随使用 LOCK IN SHARE MODE,为原表即将迁移的数据施加共享行锁,每次加锁仅针对 chunk-size 指定数量的行。尽管这种细粒度加锁策略已尽力减小锁的范围和影响,但对于极度敏感于锁冲突的业务场景而言,仍旧可能对业务产生影响,需审慎评估。另外 gh-ost 在进行原表和目标表切换时,还会对原表短暂进行 LOCK TABLE,这会导致这张表的写流量短暂跌零。
必须添加 LOCK IN SHARE MODE 的原因分析:
pt-osc 为即将迁移的数据加共享行锁比较容易理解,因为触发器增量同步与存量数据回填是并发执行的,假如不添加行锁,使用快照读进行数据导入,那么可能会出现下面这种情况,导致目标表出现脏数据。
gh-ost 的存量数据拷贝和增量数据回放公用一个线程,似乎不存在并发操作,为什么也需要施加行锁呢?关键在于事务提交机制的内在逻辑:一个事务在提交时,其流程是先记录 binlog,随后才是 InnoDB 存储引擎层的提交(详细说明见《无处不在的 MySQL XA 事务》)。这意味着,即便 binlog 中看到已记录的事务变更,可能尚未在 InnoDB 层面完成提交,此刻若执行快照读,则无法感知这些事务的更改。
假如 gh-ost 在存量数据拷贝时,不施加共享行锁,那么可能会出现以下场景,导致目标表出现脏数据:
PolarDB-X 无锁变更
PolarDB-X 针对上述的这种场景,提供了无锁变更功能,可以做到数据类型变更全程不会锁表,并且保证可以高效完成。
此前,在 《PolarDB-X 拆分规则变更》和《PolarDB-X 分区键列类型变更》文章中,我们详细阐述了 PolarDB-X Online Schema Change 的流程,鉴于 PolarDB-X 中不同 DDL 无锁变更的核心流程大体一致,为避免重复,此处将不再展开详述,感兴趣的读者可以参考上述提及的文章。
PolarDB-无锁变更充分利用了分布式数据库特性,相比与传统的开源无锁变更工具,拥有着以下几方面优势:
- 变更速度
- 不会出现增量数据追不上,导致变更始终无法完成的情况
- 并发度可以调整,满足不同用户对变更速度的需求
- 对于分区表,可充分利用分布式特性,在不同 DN 节点并行做存量数据拷贝,提升变更速度
- 对业务的影响
- 双版本元数据机制保证全程无需锁表
- 数据拷贝时存在限速,并且限速可以调整
- 拷贝存量数据时无需加锁,减少锁竞争
- 安全性
- 基于 TSO 的分布式事务双写技术,保证增量数据双写的一致性
- 存在数据校验,避免数据丢失问题,安全性有保障
- 拥有完备的 DDL 控制指令,支持 DDL 任务的即时暂停、恢复执行及回滚操作,确保了高度灵活性与可控性
无锁变更列类型
在 PolarDB-X 的无锁变更范畴内,无锁变更列类型(Online Modify Column,简称 OMC)是一种特殊情形,与拆分规则变更相比,其独特之处在于变更前后不影响表的拓扑,即临时表与原表的物理分片分布完全相同。
得益于此特性,执行列类型变更时,存量数据拷贝无需经过 CN 路由,只要将 INSERT SELECT 语句下推到各个 DN 节点即可。这一优化策略极大的削减了数据传输的中间环节,有效降低了数据冗余处理带来的性能损耗。显然,相比于拆分规则变更,利用这一策略的无锁列类型变更不仅执行速度更快,而且在资源消耗上也会更低,对业务的影响也会更小。
PolarDB-X 无锁变更列类型的具体语法如下,使用无锁变更列类型时,支持同时变更多个列的列类型,并且支持在变更列的同时进行加列和减列操作,详细说明见官网。
ALTER TABLE tbl_name alter_option [, alter_option] ... ALGORITHM = OMC alter_option: { table_options | ADD [COLUMN] col_name column_definition [FIRST | AFTER col_name] | CHANGE [COLUMN] old_col_name new_col_name column_definition [FIRST | AFTER col_name] | DROP [COLUMN] col_name | MODIFY [COLUMN] col_name column_definition [FIRST | AFTER col_name] }
借助 PolarDB-X 无锁变更列类型功能,可以完美解决识货商品表的无锁列类型变更需求。
无锁变更列类型的效果
针对上述识货商品表列类型变更的场景,可以使用以下语句进行无锁变更列类型:
ALTER TABLE products MODIFY COLUMN `price` decimal(12, 2) UNSIGNED NOT NULL DEFAULT '0.00', MODIFY COLUMN `price2` decimal(12,2) UNSIGNED NOT NULL DEFAULT '0.00', MODIFY COLUMN `price3` decimal(12,2) DEFAULT '0.00', MODIFY COLUMN `ids` varchar(310) not null default '', MODIFY COLUMN `discount` varchar(700) not null default '' COMMENT '折扣信息', ALGORITHM=OMC
此外,考虑到用户实例规格较高,加之用户对变更速度有诉求,要求最好尽快完成,我们可以在上述 SQL 前添加并发控制相关的 HINT 来调高数据拷贝的并发度,例如,可以将该 DDL 的存量数据拷贝线程数调成 8,需要添加的具体 HINT 如下:
/*+TDDL:cmd_extra(PHYSICAL_TABLE_BACKFILL_PARALLELISM=8)*/
商品货品表接近10亿行数据,最终无锁列类型变更耗时1小时55分,下图清晰地描绘了用户进行无锁列类型变更过程中的 QPS 变化曲线。从中可以观察到,用户业务的 QPS 始终保持在一个较高的水平,且 QPS 也未曾降至零点,仅从 QPS 变化曲线来看,难以直接辨认出无锁变更操作的确切时间段。
变更过程中的响应时间 RT 的变化如下图所示,特别值得注意的是,有段时间的 RT 要略微高于平均值,这一时间区间(从00:52:50至02:46:54)正是用户进行无锁列类型变更的具体时间段。无锁变更操作期间RT的小幅增长,其原因是存量数据拷贝的并发较高,对 DN 的压力会增大,导致 CN 到 DN 的物理 RT 上升,进而影响到用户 DML 的逻辑 RT。
对比测试
测试环境
实例配置
单表采用 16C 64G 单 DN 实例进行测试,分区表采用 4C 16G 4 CN 4DN 实例进行测试:
CN 规格 |
DN 规格 |
|
单表 |
16C 64G * 1 |
16C 64G * 1 |
分区表 |
4C 16G * 4 |
4C 16G * 4 |
为了减少误差,开源无锁变更工具安装在 CN leader 节点机器上。
测试参数
max-chunk = 1024
max-load=Threads_running=1500
critical-load=Threads_running=500
PolarDB-X 存量数据拷贝单表并发度:4
sysbench 参数
背景流量:oltp_read_write
表数量:1
表大小:4千万行数据
变更语句
ALTER TABLE MODIFY COLUMN `sbtest1` MODIFY COLUMN k bigint;
测试结果
测试结果如下图所示,可以得出以下几点结论:
- 随着背景流量并发量的攀升,使用 gh-ost 工具的变更耗时显著增长,且当并发量达到 50 时,其增量回放已经没办法追上流量的修改,无法顺利完成变更;
- 即便在面临 50 并发的背景流量,pt-osc 工具依旧能够保证变更任务成功完成,但是变更时间会有所增加;
- PolarDB-X 对单表执行无锁列类型变更操作时,尽管其变更时长同样会随背景流量并发量的提升而有所增加,但相比与 pt-osc 与 gh-ost,依然展现出的较高的变更效率,耗时大幅缩减;
- PolarDB-X 对分区表执行无锁列类型变更操作时,展现出了较高的稳定性与效率,其变更时长几乎不受背景流量波动的影响,并且所耗费的时间仅为 pt-osc 工具所需时间的三分之一;
说明:OMC 为 Online Modify Column 的缩写,即代表 PolarDB-X 无锁变更列类型功能
在变更操作期间,由于涉及一些资源的竞争,sysbench oltp_read_write 工作负载会受到一定的影响,导致 TPS 会有所下滑,具体下降比率详情请参见下表:
gh-ost |
pt-osc |
omc 单表 |
omc 分区表 |
|
10并发 |
3% |
11% |
15% |
4% |
25并发 |
3% |
3% |
7% |
3% |
50并发 |
无成绩 |
5% |
11% |
6% |
在单表场景下,尽管 PolarDB-X 的无锁列类型变更相较于 gh-ost 和 pt-osc 工具表现出更快的执行速度,但它对背景流量的暂时性影响要略显显著,这也反映了单个 DN 高并发操作下的一定局限性。
而在分区表场景下,PolarDB-X 的无锁列类型变更展现了极佳的性能:不仅变更过程迅速无比,而且其对背景流量的影响控制得与 gh-ost 工具相当,实现了速度与稳定性兼顾的优越成效。
总结
无论是单机数据库还是分布式数据库,无锁变更都是非常重要的能力。PolarDB-X 无锁变更技术能够极大提升数据库在线操作的灵活性与安全性,它允许在不影响业务连续性的情况下,对表结构进行修改,如增加列、变更列类型等,这对于全天候无间断服务的业务方来说是至关重要的。