percona-toolkit大表操作DDL

yuyu888 于 2024-11-01 发布

前言

很多互联网业务都面临着无法停机,需要在线变更数据库结构的情况。但是在线修改数据量较大的表,可能对线上业务产生较大影响,比如:

  1. 在线修改大表的表结构执行时间往往不可预估,一般时间较长。
  2. 由于修改表结构是表级锁,因此在修改表结构时,影响表写入操作。
  3. 如果长时间的修改表结构,中途修改失败,由于修改表结构是一个事务,因此失败后会还原表结构,在这个过程中表都是锁着不可写入。
  4. 修改大表结构容易导致数据库 CPU、IO 等性能消耗,使 MySQL 服务器性能降低。
  5. 在线修改大表结构容易导致主从延时,从而影响业务读取。

MySQL 5.6.7 之后支持online DDL之后,可以直接在线修改表结构

Percona-Toolkit

Percona-Toolkit 源自 Maatkit 和 Aspersa 工具,这两个工具是管理 MySQL 的最有名的工具,但 Maatkit 已经不维护了,全部归并到 Percona-Toolkit。Percona Toolkit 是一组高级的命令行工具,用来管理 MySQL 和系统任务,主要包括以下功能:

  1. 验证主节点和复制数据的一致性
  2. 有效的对记录行进行归档
  3. 找出重复的索引
  4. 总结 MySQL 服务器
  5. 从日志和 tcpdump 中分析查询
  6. 问题发生时收集重要的系统信息
  7. 在线修改表结构

安装

官网:https://github.com/percona/percona-toolkit

git clone https://github.com/percona/percona-toolkit.git

cd your_path/percona-toolkit

perl Makefile.PL
make
make test
make install

实际上还需要安装mysql, DBD::MySQL; 安装教程:https://segmentfault.com/a/1190000008664272

安装过程中会遇到各种问题,软件下载不下来,或者文件缺失等, 解决起来特别麻烦 索性就使用docker安装

docker pull docker.io/perconalab/percona-toolkit

docker run -it perconalab/percona-toolkit 

/usr/bin/pt-online-schema-change  {yourParams} 

percona-toolkit工具介绍

工具类别 工具命令 工具作用 备注
开发类 pt-duplicate-key-checker 列出并删除重复的索引和外键  
开发类 pt-online-schema-change 在线修改表结构  
开发类 pt-query-advisor 分析查询语句,并给出建议,有bug 已废弃
开发类 pt-show-grants 规范化和打印权限  
开发类 pt-upgrade 在多个服务器上执行查询,并比较不同  
性能类 pt-index-usage 分析日志中索引使用情况,并出报告  
性能类 pt-pmp 为查询结果跟踪,并汇总跟踪结果  
性能类 pt-visual-explain 格式化执行计划  
性能类 pt-table-usage 分析日志中查询并分析表使用情况  
配置类 pt-config-diff 比较配置文件和参数  
配置类 pt-mysql-summary 对mysql配置和status进行汇总  
配置类 pt-variable-advisor 分析参数,并提出建议  
监控类 pt-deadlock-logger 提取和记录mysql死锁信息  
监控类 pt-fk-error-logger 提取和记录外键信息  
监控类 pt-mext 并行查看status样本信息  
监控类 pt-query-digest 分析查询日志,并产生报告 常用命令
监控类 pt-trend 按照时间段读取slow日志信息 已废弃
复制类 pt-heartbeat 监控mysql复制延迟  
复制类 pt-slave-delay 设定从落后主的时间  
复制类 pt-slave-find 查找和打印所有mysql复制层级关系  
复制类 pt-slave-restart 监控salve错误,并尝试重启salve  
复制类 pt-table-checksum 校验主从复制一致性  
复制类 pt-table-sync 高效同步表数据  
系统类 pt-diskstats 查看系统磁盘状态  
系统类 pt-fifo-split 模拟切割文件并输出  
系统类 pt-summary 收集和显示系统概况  
系统类 pt-stalk 出现问题时,收集诊断数据  
系统类 pt-sift 浏览由pt-stalk创建的文件  
系统类 pt-ioprofile 查询进程IO并打印一个IO活动表  
实用类 pt-archiver 将表数据归档到另一个表或文件中  
实用类 pt-find 查找表并执行命令  
实用类 pt-kill Kill掉符合条件的sql 常用命令
实用类 pt-align 对齐其他工具的输出  
实用类 pt-fingerprint 将查询转成密文  

pt-online-schema-change 介绍

pt-online-schema-change 是 Percona-Toolkit 工具集中的一个组件,很多 DBA 在使用 Percona-Toolkit 时第一个使用的工具就是它,同时也是使用最频繁的一个工具。它可以做到在修改表结构的同时(即进行 DDL 操作)不阻塞数据库表 DML 的进行,这样降低了对生产环境数据库的影响。 在 MySQL 5.6.7 之前是不支持 Online DDL 特性的,即使在添加二级索引的时候有 FIC 特性,但是在修改表字段的时候还是会有锁表并阻止表的 DML 操作。这样对于 DBA 来说是非常痛苦的,好在有 pt-online-schema-change 工具在没有 Online DDL 时解决了这一问题,pt-online-schema-change 其主要特点就是在数据库结构修改过程中不会造成读写阻塞。

原理对比

原生DDL操作

在MySQL5.6之前,在alter这个时间段里面,表是被加了锁的(写锁),加写锁时其他用户只能select表不能update、insert表。表数据量越大,耗时越长。 mysql在线ddl(加字段、加索引等修改表结构之类的操作)过程如下: 1、对表加锁(表此时只读) 2、复制原表物理结构 3、修改表的物理结构 4、把原表数据导入中间表中,数据同步完后,锁定中间表,并删除原表 5、rename中间表为原表 6、刷新数据字典,并释放锁

使用pt-osc工具修改表结构

pt-osc工具是PT工具包里面的一种,它的全称是pt-online-schema-change,看这个名字,不难猜出来,它是为了在线修改表结构来才创建出来的,所谓的在线修改表,也就是不影响线上业务从而实现修改表结构的效果。

pt-osc工作流程:
1、检查更改表是否有主键或唯一索引,是否有触发器
2、检查修改表的表结构,创建一个临时表,在新表上执行ALTER TABLE语句
3、在源表上创建三个触发器分别对于INSERT UPDATE DELETE操作
4、从源表拷贝数据到临时表,在拷贝过程中,对源表的更新操作会写入到新建表中
5、将临时表和源表rename(需要元数据修改锁,需要短时间锁表)
6、删除源表和触发器,完成表结构的修改。

##=====================================================##
pt-osc工具限制
1、源表必须有主键或唯一索引,如果没有工具将停止工作
2、如果线上的复制环境过滤器操作过于复杂,工具将无法工作
3、如果开启复制延迟检查,但主从延迟时,工具将暂停数据拷贝工作
4、如果开启主服务器负载检查,但主服务器负载较高时,工具将暂停操作
5、当表使用外键时,如果未使用--alter-foreign-keys-method参数,工具将无法执行
6、只支持Innodb存储引擎表,且要求服务器上有该表1倍以上的空闲空间。

pt-osc之alter语句限制
1、不需要包含alter table关键字,可以包含多个修改操作,使用逗号分开,如"drop clolumn c1, add column c2 int"
2、不支持rename语句来对表进行重命名操作
3、不支持对索引进行重命名操作
4、如果删除外键,需要对外键名加下划线,如删除外键fk_uid, 修改语句为"DROP FOREIGN KEY _fk_uid"

pt-online-schema-change 常用参数

--alter# 结构变更语句,不需要alter table关键字。可以指定多个更改,用逗号分隔
--alter-foreign-keys-method#    这个参数是用来处理需要修改的表上具有外键的情况的,如果表上有外键,则需要使用该参数来处理,该参数有4个值,分别是auto、rebuild_constraints、drop_swap、none,一般情况下,选用auto即可,默认值也是auto
--execute 
#  确定修改表,则指定该参数。真正执行。
--charset=utf8
#使用utf8编码,避免中文乱码
--chunk-size
# 对每次导入行数进行控制,已减少对原表的锁定时间。
--print 
# 打印SQL语句到标准输出。指定此选项可以让你看到该工具所执行的语句
--user=    
# 连接mysql的用户名
--password=   
# 连接mysql的密码
--host=     
# 连接mysql的地址
P=            
# 连接mysql的端口号
D=       
#  连接mysql的库名
t=     
# 连接mysql的表名
--recursion-method  
# 发现从的方法,  默认是show processlist,可以指定none来不检查Slave

坑点:
实际实验中由于没有 设置–charset=utf8, 导致COMMENT乱码;
没有设置–recursion-method=none;导致从库连接失败

查看从库信息

SHOW SLAVE HOSTS;

实践

原生执行

ALTER TABLE attendance_20240115 MODIFY COLUMN right_name CHAR(60) NOT NULL;

然后再执行 INSERT 一直锁着 无法执行成功

使用 pt-online-schema-change

/usr/bin/pt-online-schema-change –host=10.130.130.87 –user=root –password=xxxxxx –alter “MODIFY COLUMN right_name CHAR(60) NOT NULL” D=hec_attendance,t=attendance_20240115 –print –execute –recursion-method=none

执行完毕,过一会等触发器建立完毕,插入一条数据能执行成功, 等全部执行完毕,数据依然在

期间可以执行 SHOW TRIGGERS 查看 触发器

执行 show tables 可以看到新增了一个表 _attendance_20240115_new 执行明细:

bash-5.1$ /usr/bin/pt-online-schema-change --host=10.130.130.87 --user=root --password=123456 --alter "MODIFY COLUMN right_name CHAR(60) NOT NULL" D=hec_attendance,t=attendance_20240115 --print --execute --recursion-method=none
No slaves found.  See --recursion-method if host test.contract11.xxxxxx.net has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `hec_attendance`.`attendance_20240115`...
Creating new table...
CREATE TABLE `hec_attendance`.`_attendance_20240115_new` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `unique_id` char(64) NOT NULL,
  `day` date NOT NULL,
  `begin` datetime NOT NULL,
  `end` datetime NOT NULL,
  `work_hour` decimal(4,2) NOT NULL,
  `week` char(32) NOT NULL,
  `is_holiday` tinyint(4) DEFAULT NULL,
  `official_holiday` tinyint(4) NOT NULL DEFAULT '0' COMMENT '????????0:???1??',
  `late` tinyint(4) NOT NULL,
  `leave_early` tinyint(4) NOT NULL,
  `work_time_enough` tinyint(4) NOT NULL,
  `right_name` char(128) NOT NULL,
  `right_flag` tinyint(4) DEFAULT NULL,
  `overtime_totaltime` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT '?????',
  `overtime_list` text COMMENT '????',
  `application_totaltime` decimal(11,2) NOT NULL DEFAULT '0.00',
  `application_list` text COMMENT '????',
  `is_valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '???????1????0???',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '????',
  `period_name` varchar(10) NOT NULL DEFAULT '' COMMENT '??????',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_id_day` (`unique_id`,`day`) USING BTREE,
  KEY `day` (`day`),
  KEY `is_holiday_index` (`is_holiday`),
  KEY `official_holiday_index` (`official_holiday`),
  KEY `unique_id_late` (`unique_id`,`late`),
  KEY `unique_id_right_flag` (`unique_id`,`right_flag`),
  KEY `unique_id_is_holiday` (`unique_id`,`is_holiday`),
  KEY `unique_id` (`unique_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3240226 DEFAULT CHARSET=utf8
Created new table hec_attendance._attendance_20240115_new OK.
Altering new table...
ALTER TABLE `hec_attendance`.`_attendance_20240115_new` MODIFY COLUMN right_name CHAR(60) NOT NULL
Altered `hec_attendance`.`_attendance_20240115_new` OK.
2024-11-01T02:07:42 Creating triggers...
-----------------------------------------------------------
Event : DELETE
Name  : pt_osc_hec_attendance_attendance_20240115_del
SQL   : CREATE TRIGGER `pt_osc_hec_attendance_attendance_20240115_del` AFTER DELETE ON `hec_attendance`.`attendance_20240115` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `hec_attendance`.`_attendance_20240115_new` WHERE `hec_attendance`.`_attendance_20240115_new`.`id` <=> OLD.`id`; END
Suffix: del
Time  : AFTER
-----------------------------------------------------------
-----------------------------------------------------------
Event : UPDATE
Name  : pt_osc_hec_attendance_attendance_20240115_upd
SQL   : CREATE TRIGGER `pt_osc_hec_attendance_attendance_20240115_upd` AFTER UPDATE ON `hec_attendance`.`attendance_20240115` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `hec_attendance`.`_attendance_20240115_new` WHERE !(OLD.`id` <=> NEW.`id`) AND `hec_attendance`.`_attendance_20240115_new`.`id` <=> OLD.`id`; REPLACE INTO `hec_attendance`.`_attendance_20240115_new` (`id`, `unique_id`, `day`, `begin`, `end`, `work_hour`, `week`, `is_holiday`, `official_holiday`, `late`, `leave_early`, `work_time_enough`, `right_name`, `right_flag`, `overtime_totaltime`, `overtime_list`, `application_totaltime`, `application_list`, `is_valid`, `update_time`, `period_name`) VALUES (NEW.`id`, NEW.`unique_id`, NEW.`day`, NEW.`begin`, NEW.`end`, NEW.`work_hour`, NEW.`week`, NEW.`is_holiday`, NEW.`official_holiday`, NEW.`late`, NEW.`leave_early`, NEW.`work_time_enough`, NEW.`right_name`, NEW.`right_flag`, NEW.`overtime_totaltime`, NEW.`overtime_list`, NEW.`application_totaltime`, NEW.`application_list`, NEW.`is_valid`, NEW.`update_time`, NEW.`period_name`); END
Suffix: upd
Time  : AFTER
-----------------------------------------------------------
-----------------------------------------------------------
Event : INSERT
Name  : pt_osc_hec_attendance_attendance_20240115_ins
SQL   : CREATE TRIGGER `pt_osc_hec_attendance_attendance_20240115_ins` AFTER INSERT ON `hec_attendance`.`attendance_20240115` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; REPLACE INTO `hec_attendance`.`_attendance_20240115_new` (`id`, `unique_id`, `day`, `begin`, `end`, `work_hour`, `week`, `is_holiday`, `official_holiday`, `late`, `leave_early`, `work_time_enough`, `right_name`, `right_flag`, `overtime_totaltime`, `overtime_list`, `application_totaltime`, `application_list`, `is_valid`, `update_time`, `period_name`) VALUES (NEW.`id`, NEW.`unique_id`, NEW.`day`, NEW.`begin`, NEW.`end`, NEW.`work_hour`, NEW.`week`, NEW.`is_holiday`, NEW.`official_holiday`, NEW.`late`, NEW.`leave_early`, NEW.`work_time_enough`, NEW.`right_name`, NEW.`right_flag`, NEW.`overtime_totaltime`, NEW.`overtime_list`, NEW.`application_totaltime`, NEW.`application_list`, NEW.`is_valid`, NEW.`update_time`, NEW.`period_name`);END
Suffix: ins
Time  : AFTER
-----------------------------------------------------------
2024-11-01T02:07:42 Created triggers OK.
2024-11-01T02:07:42 Copying approximately 3135963 rows...
INSERT LOW_PRIORITY IGNORE INTO `hec_attendance`.`_attendance_20240115_new` (`id`, `unique_id`, `day`, `begin`, `end`, `work_hour`, `week`, `is_holiday`, `official_holiday`, `late`, `leave_early`, `work_time_enough`, `right_name`, `right_flag`, `overtime_totaltime`, `overtime_list`, `application_totaltime`, `application_list`, `is_valid`, `update_time`, `period_name`) SELECT `id`, `unique_id`, `day`, `begin`, `end`, `work_hour`, `week`, `is_holiday`, `official_holiday`, `late`, `leave_early`, `work_time_enough`, `right_name`, `right_flag`, `overtime_totaltime`, `overtime_list`, `application_totaltime`, `application_list`, `is_valid`, `update_time`, `period_name` FROM `hec_attendance`.`attendance_20240115` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 8 copy nibble*/
SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `hec_attendance`.`attendance_20240115` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/
Copying `hec_attendance`.`attendance_20240115`:   2% 24:19 remain
Copying `hec_attendance`.`attendance_20240115`:   2% 32:24 remain
Copying `hec_attendance`.`attendance_20240115`:   3% 38:47 remain
Copying `hec_attendance`.`attendance_20240115`:   4% 42:32 remain
.......
........
........
Copying `hec_attendance`.`attendance_20240115`:  98% 01:10 remain
Copying `hec_attendance`.`attendance_20240115`:  99% 00:44 remain
Copying `hec_attendance`.`attendance_20240115`:  99% 00:18 remain
2024-11-01T03:37:13 Copied rows OK.
2024-11-01T03:37:13 Swapping tables...
RENAME TABLE `hec_attendance`.`attendance_20240115` TO `hec_attendance`.`_attendance_20240115_old`, `hec_attendance`.`_attendance_20240115_new` TO `hec_attendance`.`attendance_20240115`
2024-11-01T03:37:13 Swapped original and new tables OK.
2024-11-01T03:37:13 Dropping old table...
DROP TABLE IF EXISTS `hec_attendance`.`_attendance_20240115_old`
2024-11-01T03:37:14 Dropped old table `hec_attendance`.`_attendance_20240115_old` OK.
2024-11-01T03:37:14 Dropping triggers...
DROP TRIGGER IF EXISTS `hec_attendance`.`pt_osc_hec_attendance_attendance_20240115_del`
DROP TRIGGER IF EXISTS `hec_attendance`.`pt_osc_hec_attendance_attendance_20240115_upd`
DROP TRIGGER IF EXISTS `hec_attendance`.`pt_osc_hec_attendance_attendance_20240115_ins`
2024-11-01T03:37:14 Dropped triggers OK.
Successfully altered `hec_attendance`.`attendance_20240115`.

通过日志可以清晰的看到,整个创建新表,创建触发器,同步数据,RENAME TABLE, 删除触发器等一系列操作过程

参考资料

https://www.jianshu.com/p/d623835f4ce6 https://zhuanlan.zhihu.com/p/700433432
https://segmentfault.com/a/1190000008664272
https://cloud.tencent.com/developer/article/1068214
https://cloud.tencent.com/developer/article/1520624
https://cloud.tencent.com/developer/article/2145230
https://cloud.tencent.com/developer/article/1717850
https://www.cnblogs.com/zishengY/p/6852280.html