MySQL 8.0 Online DDL和pt-osc、gh-ost深度对比分析
温正湖2020-03-25 11:27:14

本文基于MySQL 8.0测试常用的DDL工具,通过多个维度进行对比分析。基本结论是MySQL原生Online DDL在性能、存储开销和易用性等方面有较大优势,目前在加列等场景可取代pt-osc/gh-ost,在创建索引等场景还需要做进一步优化。

DDL工具实现分析

在MySQL使用过程中,根据业务的需求对表结构进行变更是个普遍的运维操作,这些称为DDL操作。常见的DDL操作有在表上增加新列,或给某个列添加索引。

目前可用的DDL操作工具包括pt-osc,github的gh-ost,以及MySQL提供的在线修改表结构命令Online DDL。pt-osc和gh-ost均采用拷表方式实现,即创建个空的新表,通过select+insert将旧表中的记录逐次读取并插入到新表中,不同之处在于处理DDL期间业务对表的DML操作(增删改)。

pt-osc

pt-osc是Percona运维工具包中提供的表结构变更工具,全称pt-online-schema-change。执行流程图如下:


从中可知其采用触发器来应用DDL执行期间对表所做的DML操作,每种DML操作均对应一个触发器,如下所示:

CREATE TRIGGER `pt_osc_sbtest_sbtest2_del` AFTER DELETE ON `sbtest`.`sbtest2` FOR EACH ROW DELETE IGNORE FROM `sbtest`.`_sbtest2_new` WHERE `sbtest`.`_sbtest2_new`.`id` <=> OLD.`id`
CREATE TRIGGER `pt_osc_sbtest_sbtest2_upd` AFTER UPDATE ON `sbtest`.`sbtest2` FOR EACH ROW REPLACE INTO `sbtest`.`_sbtest2_new` (`id`, `k`, `c`, `pad`, `ptosc`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`, NEW.`ptosc`)
CREATE TRIGGER `pt_osc_sbtest_sbtest2_ins` AFTER INSERT ON `sbtest`.`sbtest2` FOR EACH ROW REPLACE INTO `sbtest`.`_sbtest2_new` (`id`, `k`, `c`, `pad`, `ptosc`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`, NEW.`ptosc`)

根据定义可知,delete变为delete ignore,update和insert均转换为replace into。

pt-osc同时处理全量和增量数据,即一边拷表一边回放增量DML,其所用拷表语句如下:

INSERT LOW_PRIORITY IGNORE INTO `sbtest`.`_sbtest2_new` (`id`, `k`, `c`, `pad`, `ptosc`) SELECT `id`, `k`, `c`, `pad`, `ptosc` FROM `sbtest`.`sbtest2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 115039 copy nibble*/
SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `sbtest`.`sbtest2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDE

由于采用触发器方式,需要解决增量回放与全量拷贝乱序问题。pt-osc通过如下方式解决:

在拷表select时需要进行当前读(lock in shared mode)并与insert组成一个事务,避免快照读导致增量的delete操作丢失。场景如下:

 如果是快照读,且在读id=x之前,业务执行了delete操作,则触发器先执行了空操作,导致新表中仍存在需删除掉的id=x记录;
 如果与insert不是一个事务,存在相似问题;

触发器将增量update和insert均转换为replace into用于防止增量回放时因数据不存在而报错。

拷表insert时采用‘LOW_PRIORITY IGNORE’也是类似的考虑,防止全量旧数据覆盖增量新数据。

根据pt-osc上述实现,可以发现其存在的使用约束至少包括:

· 需要该表存在主键:因为进行每个select+insert事务拆分时用到了‘FORCE INDEX(PRIMARY)’;

· 需要确保表上没有触发器,否则会导致触发器冲突;

在使用过程中,会出现因存在触发器导致与业务事务冲突死锁的问题。

gh-ost

gh-ost是github针对pt-osc用触发器带来的问题而重新设计的一款较新的表结构变更工具。其使用binlog+回放线程来替换掉触发器。示意图如下:


 

gh-ost根据binlog来源不同支持2种模式,分别是读取主库binlog和读取从库binlog。除此之外,还外加一种迁移/测试模式。 下面以读取从库binlog模式为例做流程说明:


 

· 先连接到主库上,根据alter语句创建所需的新表;

· 作为一个“备库”连接到其中一个真正的备库上,一边在主库上拷贝已有的数据到新表,一边从备库上拉取增量数据的binlog;

· 然后不断的把 binlog 应用回主库;

· cut-over是最后一步,锁住主库的源表,等待binlog 应用完毕,然后替换gh-ost表为源表。

由于使用单线程回放binlog来替换触发器,所以增量DML回放效率不如触发器,因为pt-osc的增量回放并发度是与业务DML并发度相同的,是多线程的。

因此,从实现层面,gh-ost对业务负载敏感度会远高于pt-osc。

gh-ost虽然在抗业务负载能力不如pt-osc,但也有其优势,除了没有触发器相关问题之外,还有如下优点受DBA青睐:

· 对于DDL操作的灵活度掌控,可暂停,可动态修改参数;DBA可以根据执行情况来快速调整预设的参数,可快可慢,实现DDL操作性能和对业务影响的平衡;

· 更为稳健的切表控制:将-cut-over-lock-timeout-seconds和-default-retries 配合使用,可以对切表进行灵活的控制。避免pt-osc切表异常导致对业务造成严重影响;

Online DDL

Online DDL是MySQL官方原生的表结构修改工具。既然官方有原生的工具,为什么还会出现pt-osc和gh-ost呢。说来话长,一言难尽。

Online DDL在MySQL 5.6才开始支持的,在5.5及之前版本,使用alter table/create index等命令进行表结构修改操作均会锁表,这在生产环境上是不可接受的。

MySQL 5.7,Online DDL在性能和稳定性上不断得到优化,比如通过bulk load方式来去除表重建时的redo日志等。到了MySQL 8.0,Online DDL已经支持秒级加列特性,该特性来源于国内的腾讯互娱DBA团队。

概括来说,在MySQL 8.0上,Online DDL有2种划分维度,一是DDL期间运行的并发程度,二是DDL的实现方式。

先说DDL时的业务DML操作运行程度(Permits Concurrent DML),可以通过LOCK关键字来指定DDL期间加锁程度,可选:

· LOCK=NONE:允许查询和DML操作;

· LOCK=SHARED:允许查询,不允许DML操作;

· LOCK=DEFAULT:由系统决定,选择最宽松的模式;

· LOCK=EXCLUSIVE:不允许查询和DML操作。

对于耗时的DDL操作,目前MySQL 8.0/5.7均已支持在DDL期间进行查询和DML。

另一种划分方式为是否拷贝数据,可分为如下几种:

· 仅修改元数据:包括修改表名,字段名等;

· ALGORITHM=COPY:采用拷表方式进行表变更,与pt-osc/gh-ost类似;

· ALGORITHM=INPLACE:仅需要进行引擎层数据改动,不涉及Server层;

· ALGORITHM=INSTANT:与第一种方式类似,仅修改元数据。目前仅支持在表最后增加新列;

· ALGORITHM=DEFAULT:有系统决定,选择最优的算法执行DDL。 用户可以选用上述算法来执行,但本身收到DDL类型限制,如果指定的算法无法执行DDL,则ALTER操作会报错。

从流程上看,Online DDL可分为3个阶段:

· 初始化阶段,确定DDL操作支持的最优LOCK和ALGORITHM设置,并与用户指定的设置相比,若无法办到则报错;

· 执行阶段,如果需要拷表或修改引擎层数据,则该阶段是最耗时的阶段;

· 提交阶段,该阶段会加锁进行新旧表切换;

pt-osc/gh-ost不同,Online DDL的执行阶段又可以分为前后2个步骤,首先是拷贝全量数据,然后才回放增量DML日志。在全量拷贝期间,增量DML日志被保存在日志文件中,由innodb_online_alter_log_max_size 参数确定文件最大阈值,若积累的DML日志超过该阈值,则DDL操作返回失败。

Online DDL也是采用单线程回放DML日志,因此对负载应该也比较敏感,而且由于拷贝和增量回放时先后进行的,负载高时对性能影响会更加明显。但DML日志回放行为是在引擎层进行的,不是SQL回放,因此在效率上会更高。

DDL工具性能对比测试

测试时,我们使用sysbench来模拟有无业务负载的场景。DDL操作考虑加列和加索引这两种。测试数据量为1k万条记录。测试结果如下。

无负载测试

 

从图中可以看出,Online DDL的性能明显好于2种第三方工具。

有负载测试

负载配置:

oltp.lua   --oltp_tables_count=4 --oltp-table-size=10000000 --rand-init=on --num-threads=32

负载tps值约6k:


测试时,pt-osc和Online DDL均顺利完成,但gh-ost无法结束,将并发数调整为5后才达到与pt-osc相似的性能表现。 gh-ost负载配置:

oltp.lua   --oltp_tables_count=4 --oltp-table-size=10000000 --rand-init=on --num-threads=5

此时负载tps值约为1.3k:

性能测试结果如下:


 

综合考虑有无负载情况,Online DDL的性能均大大优于2种第三方工具,进一步对比在这2种工具。可以看到pt-osc在性能和高负载情况下的稳定性均好于gh-ost。

DDL操作对业务负载的影响

Online DDL执行期间对业务负载影响较小,从6k降为5.8k左右。但在切换阶段会出现秒级地停写情况,如下所示:


 pt-osc对业务负载的影响相比Online DDL和gh-ost都大,操作期间,性能从6k降为5.2k左右。


gh-ost虽然在高业务负载情况下无法完成DDL操作,但对性能的影响较小,与Online DDL相近,但其也不会出现停写情况,这与pt-osc相似。

Online DDL存在的问题和优化

从上面的性能测试来看,Online DDL是碾压另外2种工具的,而且走拷贝模式进行表变更会产生大量的binlog,可能出现严重的主从复制延迟。那么为什么DBA们不使用Online DDL呢,个人分析存在以下几种原因:

· pt-osc已经用了很久,成熟而灵活:pt-osc在MySQL 5.5就已经大规模使用,有什么坑都已清楚,用得顺手,而且pt-osc提供了丰富的配置参数,尤其是流控相关参数,可以尽可能减低对线上业务的影响;牺牲点性能求稳定是稳妥的;

· Online DDL持续进一步不为人知:MySQL被大规模使用应该是从5.5甚至更高5.1版本开始的,此时还没有Online DDL,5.6版本发布后,虽具备该功能,可还存在较多问题,关注度不高,到了5.7版本,Online DDL有了长足进步,但大家都不愿意第一个吃螃蟹;

· Online DDL有复制阻塞问题:在MySQL多线程并行复制框架下,从库回放DDL操作时排他性的,也就是说DDL操作独立为一个group,只有该DDL操作执行完才能回放后续的DML,如果DDL操作需花费2小时,那么复制延迟至少为2小时;

· Online DDL在切表时会停写:在上面测试结果中提到会短暂停写,那么如果执行DDL的表较大,且业务负载也不小,是否停写时间也会变成呢?

最后2个原因是可验证的,且第三个原因较为明确,我们放在最后分析。下面先确认最后一个问题是否存在。

**2020-3-23 补充:**

有些评论说:Online DDL执行时会有一定概率报主键冲突,导致DDL操作失败;

主键冲突问题在重建表场景可能出现,但在创建二级索引场景应该是不会的,通过分析源码发现,创建索引场景下DML是采用delete+insert形式记录的。源码分析在下一篇文章进行。

大表Online DDL的停写时间是否与负载正相关

根据MySQL官方文档对参数innodb_online_alter_log_max_size的说明,有理由相信停写时间受积累的增量DML数量影响。

Thus, a large value for this option allows more DML to happen during an online DDL operation, but also extends the period of time at the end of the DDL operation when the table is locked to apply the data from the log.

该段话的字面意思是在回放增量DML时会锁表,从而导致业务DML无法执行。但目前网上对MySQL 5.6 Online DDL的分析都说仅在应用文件中最后一个日志块时才加锁:

Row Log以Block的方式存储,若DML较多,那么Row Logs可能会占用多个Blocks。row_log_t结构中包含两个指针:head与tail head指针用于读取Row Log,tail指针用于追加写新的Row Log;
3.在重用Row Log时,算法遵循一个原则:尽量减少索引树加锁的时间(索引树加X锁,也意味着表上禁止了新的DML操作)
索引树需要加锁的场景:
(一) 在重用Row Log跨越新的Block时,需要短暂加锁;
(二) 若应用的Row Log Block是最后一个Block,那么一直加锁 应用最后一个Block,由于禁止了新的DML操作,因此此Block应用完毕,新索引记录与聚簇索引达到一致状态,重用阶段结束;
(三) 在应用中间Row Log Block上的row log时,无需加锁,新的DML操作仍旧可以进行,产生的row log记录到最后一个Row Log Block之上;

是文档说明不正确还是新版本MySQL退化了? 这需要进行验证:

sysbench表记录数增加10倍,达到1亿条。单表32线程负载,tps约4k。


 

加列操作:

node1-sbtest>alter table sbtest1 ADD COLUMN online varchar(10) default null comment 'test';

Query OK, 0 rows affected (3.02 sec)

Records: 0  Duplicates: 0  Warnings: 0

加索引操作:

node1-sbtest>alter table sbtest1 add index idx_c(online);

Query OK, 0 rows affected (29 min 38.97 sec)

Records: 0  Duplicates: 0  Warnings: 0

加索引期间性能未明显下降。数据目录下没有新的ibd文件产生,说明加索引不需要重建表。

 虽然执行了近半个小时,但在切表过程中,仅3s时间停写,与加列操作的停写时间不相上下。这么短的停写时间应该是可以接受的。从这里可以看出,应该仅在回放增量DML的最后阶段才加锁。

通过上述证明,大表DDL的停写时间可控,不会明显恶化。

Online DDL的优缺点与后续优化

优点

综合上面的分析,目前Online DDL的优势包括: - 性能有显著优势,且对业务负载影响小,停写时间可控; - 操作简单,直接执行alter table或类似命令即可,无需安装第三方工具和依赖包; - 所需存储空间少,pt-osc/gh-ost均需要创建新表并拷贝数据,因此需要有不小于表大小的空闲存储空间。但对于加列和加索引等支持Inplace算法的Online DDL,无需拷表,空间大大节省; - 不会产生大量binlog,pt-osc/gh-ost拷表时会产生大量binlog文件,这即增大了最多表空间大小一倍的存储空间,而且也容易造成复制延迟,降低了从库的可读性,放大了服务不可用风险。

缺点

似乎Online DDL仅有的缺点是会导致复制阻塞,造成另一种形式的从库复制延迟。其示意图如下:

 主库上ddl和dml可以并发执行,在group commit阶段依次写入到binlog文件中,这些binlog传到从库上,由并行复制的worker线程回放。

通俗来说,由于并行复制是以group为单位进行回放的,ddl作为单独一个group,只有执行完才能进行下一个group回放,这意味在主库上与ddl差不多时间提交但binlog写在ddl之后的那些dml,其复制延迟会大于ddl的执行时间。

其他数据库做法

MySQL Online DDL存在复制阻塞问题,那么其他数据库是否存在呢?

MongoDB

MongoDB中,由于没有强制的schema限制,所以也就不存在加列,修改列类型等操作。主要的DDL方式就是为某个字段建二级索引,其复制采用oplog,类似于MySQL binlog,是否存在相同问题?

根据MongoDB 4.2官方文档,官方建议在MongoDB ReplicaSet(复制集)上,采用rolling方式创建索引。

To minimize the impact of building an index on replica set deployments, use the following procedure to build indexes in a rolling fashion.
The following procedure for replica set deployments does take one member out of the replica set at a time. However, this procedure will only affect one member of the set at a time rather than all secondaries at the same time.

大意就是逐个把secondary节点从复制集上摘掉,单独创建索引,重新加入复制集。最后再将primary节点降级为secondary重复上述处理。

详细说明可参考手册build-indexes-on-replica-sets

MySQL也可以这么做,但这是没办法的办法。缺点很明显:

· 降低了集群服务可用性保障。如果此时另一个secondary出现故障,则服务停写;

· 造成复制延迟。虽然仍在线的secondary不受影响,但当前节点加入复制集后,会有较大复制延迟;

Oracle

咨询了我们网易的Oracle DBA专家,Oracle也不存在问题,举Oracle DataGuard为例,其基于物理复制,在进行表结构变更时会记录数据页更改的redo日志,做个不恰当比喻,其类似拷表,该方式会导致redo日志剧增。

结论是并没有可以参考的优化方案。

解决思路

其实方法总比困难多。MySQL 8.0已经支持秒级加列(instant add column),目前主要矛盾是如何解决加索引问题。通过分析和交流,已形成了可行的优化方案。具体方案将在后续进行详细阐述。

下一篇将从源码分析MySQL 8.0版本处理Online DDL增量DML的方式。

 

0
0
写文章
戳我,来吐槽~