pt-online-schema-change

因为使用delete删除数据的时候,MySQL并不会把数据文件真实删除,而只是将数据文件的标识位删除,也没有整理数据文件,因此不会彻底释放表空间。换句话说,每当我们从表中删除数据时,这段被删除数据的空间就会被留出来,如果又赶上某段时间内该表进行大量的delete操作,则这部分被删除数据的空间就会越来越大。当有新数据写入时,MySQL会再次利用这些被删除的区域,但也无法彻底占用。

//安装依赖
yum install -y perl-TremR perl-DBI perl-DBD-mysql perl-Time-HiRes perl-IO-Socket-SSL perl-TermReadKey perl-Digest-MD5
//下载
wget https://downloads.percona.com/downloads/percona-toolkit/3.5.2/binary/redhat/7/x86_64/percona-toolkit-3.5.2-2.el7.x86_64.rpm
//安装
rpm -ivh percona-toolkit-3.5.2-2.el7.x86_64.rpm

工具自身环境检查:
说明:工具在执行时也会进行检查,如果遇到不能执行的情况,则报错,建议在执行前先进行 dry-run。

  • 检查要变更的表上是否有主键或非空唯一键。
  • 检查是否有其他表外键引用该表。
  • 检查表上是否有触发器。
  • 检查从库是否设置 change filter。如果设置了 change filter,则不会执行,除非指定 --no-check-replication-filters。

执行之前测试:

pt-online-schema-change h=localhost,u=root,p=11111,P=3306,D=userblink,t=copy1 --alter "ENGINE=InnoDB"  --recursion-method=none --no-check-replication-filters --alter-foreign-keys-method auto --print --dry-run
pt-online-schema-change h=localhost,u=root,p=11111,P=3306,D=userblink,t=copy1 --alter "ENGINE=InnoDB"  --recursion-method=none --no-check-replication-filters --alter-foreign-keys-method auto --print --execute

清理数据库磁盘碎片和在线更改表结构:

  1. 锁表方式:alter table table_name engine=innodb、optimize table table_name。
  2. 推荐方式:pt-online-schema-change。

1.工作原理 

  1. 创建影子表:创建 t1 表的副本_t1_new。此时 _t1_new 里没有任何数据,表结构和 t1 完全相同。
  2. 在影子表上执行变更:在 _t1_new 表上添加列 c4 ,执行语句为 ALTER 语句,因为此时 _t1_new 里没有数据,且业务上不会使用到该表,不会有阻塞发生,所以变更很快就能完成。
  3. 创建触发器:在表 t1上创建触发器,分别对应 INSERT,DELETE 和 UPDATE 操作。创建触发的目的就是为了在变更期间发生在 t1 上的 DML 操作同步到 _t1_new 上,保证数据的一致性。
  4. 同步数据:循环将数据库从 T1 拷贝到 _t1_new,主要执行的就是 INSERT … SELECT … LOCK IN SHARE MODE 。注意此时正在同步的数据是无法进行 DML 操作的;另外根据选项设置,每次循环时都会监控主从延迟情况或着数据库负载情况。
  5. 分析表:确认数据拷贝完成后执行ANALYZE TABLE 操作,这一步主要是为了防止执行完第六步以后,相关的SQL无法选择正确的执行计划。
  6. 更改表名:RENAME TABLE t1 TO _t1_old, _t1_new TO t1。
  7. 删除原始表:删除原始表 _t1_old 和触发器。
No slaves found.  See --recursion-method if host host-192-168-25-212 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
No foreign keys reference `passport`.`userinfo`; ignoring --alter-foreign-keys-method.
Altering `passport`.`userinfo`...
Creating new table...
CREATE TABLE `passport`.`_userinfo_new` (
  `UserId` int(11) NOT NULL DEFAULT '0',
  `Industry` varchar(64) DEFAULT NULL,
  `City` varchar(64) DEFAULT NULL,
  `Job` varchar(64) DEFAULT NULL,
  `WorkYears` varchar(64) DEFAULT NULL,
  `Gender` tinyint(2) DEFAULT '1',
  `NickName` varchar(64) DEFAULT NULL,
  `Website` varchar(128) DEFAULT NULL,
  `Description` varchar(512) DEFAULT NULL,
  `QQ` varchar(32) DEFAULT NULL,
  `MSN` varchar(64) DEFAULT NULL,
  `Birthday` datetime DEFAULT NULL,
  `Mobile` varchar(64) DEFAULT NULL,
  `RealName` varchar(64) DEFAULT NULL,
  `IsLocked` bit(1) DEFAULT b'0',
  PRIMARY KEY (`UserId`),
  KEY `ix_NickName` (`NickName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Created new table passport._userinfo_new OK.
Altering new table...
ALTER TABLE `passport`.`_userinfo_new` ENGINE=InnoDB
Altered `passport`.`_userinfo_new` OK.
2023-11-08T11:00:30 Creating triggers...
-----------------------------------------------------------
Event : DELETE 
Name  : pt_osc_passport_userinfo_del 
SQL   : CREATE TRIGGER `pt_osc_passport_userinfo_del` AFTER DELETE ON `passport`.`userinfo` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `passport`.`_userinfo_new` WHERE `passport`.`_userinfo_new`.`userid` <=> OLD.`userid`; END  
Suffix: del 
Time  : AFTER 
-----------------------------------------------------------
-----------------------------------------------------------
Event : UPDATE 
Name  : pt_osc_passport_userinfo_upd 
SQL   : CREATE TRIGGER `pt_osc_passport_userinfo_upd` AFTER UPDATE ON `passport`.`userinfo` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `passport`.`_userinfo_new` WHERE !(OLD.`userid` <=> NEW.`userid`) AND `passport`.`_userinfo_new`.`userid` <=> OLD.`userid`; REPLACE INTO `passport`.`_userinfo_new` (`userid`, `industry`, `city`, `job`, `workyears`, `gender`, `nickname`, `website`, `description`, `qq`, `msn`, `birthday`, `mobile`, `realname`, `islocked`) VALUES (NEW.`userid`, NEW.`industry`, NEW.`city`, NEW.`job`, NEW.`workyears`, NEW.`gender`, NEW.`nickname`, NEW.`website`, NEW.`description`, NEW.`qq`, NEW.`msn`, NEW.`birthday`, NEW.`mobile`, NEW.`realname`, NEW.`islocked`); END  
Suffix: upd 
Time  : AFTER 
-----------------------------------------------------------
-----------------------------------------------------------
Event : INSERT 
Name  : pt_osc_passport_userinfo_ins 
SQL   : CREATE TRIGGER `pt_osc_passport_userinfo_ins` AFTER INSERT ON `passport`.`userinfo` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; REPLACE INTO `passport`.`_userinfo_new` (`userid`, `industry`, `city`, `job`, `workyears`, `gender`, `nickname`, `website`, `description`, `qq`, `msn`, `birthday`, `mobile`, `realname`, `islocked`) VALUES (NEW.`userid`, NEW.`industry`, NEW.`city`, NEW.`job`, NEW.`workyears`, NEW.`gender`, NEW.`nickname`, NEW.`website`, NEW.`description`, NEW.`qq`, NEW.`msn`, NEW.`birthday`, NEW.`mobile`, NEW.`realname`, NEW.`islocked`);END  
Suffix: ins 
Time  : AFTER 
-----------------------------------------------------------
2023-11-08T11:00:30 Created triggers OK.
2023-11-08T11:00:30 Copying approximately 30978638 rows...
INSERT LOW_PRIORITY IGNORE INTO `passport`.`_userinfo_new` (`userid`, `industry`, `city`, `job`, `workyears`, `gender`, `nickname`, `website`, `description`, `qq`, `msn`, `birthday`, `mobile`, `realname`, `islocked`) SELECT `userid`, `industry`, `city`, `job`, `workyears`, `gender`, `nickname`, `website`, `description`, `qq`, `msn`, `birthday`, `mobile`, `realname`, `islocked` FROM `passport`.`userinfo` FORCE INDEX(`PRIMARY`) WHERE ((`userid` >= ?)) AND ((`userid` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 29249 copy nibble*/
SELECT /*!40001 SQL_NO_CACHE */ `userid` FROM `passport`.`userinfo` FORCE INDEX(`PRIMARY`) WHERE ((`userid` >= ?)) ORDER BY `userid` LIMIT ?, 2 /*next chunk boundary*/
Copying `passport`.`userinfo`:   9% 04:35 remain
Copying `passport`.`userinfo`:  20% 03:58 remain
Copying `passport`.`userinfo`:  30% 03:27 remain
Copying `passport`.`userinfo`:  40% 02:56 remain
Copying `passport`.`userinfo`:  51% 02:21 remain
2023-11-08T11:03:00 Copied rows OK.
2023-11-08T11:03:00 Analyzing new table...
2023-11-08T11:03:00 Swapping tables...
RENAME TABLE `passport`.`userinfo` TO `passport`.`_userinfo_old`, `passport`.`_userinfo_new` TO `passport`.`userinfo`
2023-11-08T11:03:00 Swapped original and new tables OK.
2023-11-08T11:03:00 Dropping old table...
DROP TABLE IF EXISTS `passport`.`_userinfo_old`
2023-11-08T11:03:00 Dropped old table `passport`.`_userinfo_old` OK.
2023-11-08T11:03:00 Dropping triggers...
DROP TRIGGER IF EXISTS `passport`.`pt_osc_passport_userinfo_del`
DROP TRIGGER IF EXISTS `passport`.`pt_osc_passport_userinfo_upd`
DROP TRIGGER IF EXISTS `passport`.`pt_osc_passport_userinfo_ins`
2023-11-08T11:03:00 Dropped triggers OK.
Successfully altered `passport`.`userinfo`.

 2.命令参数

–print:将工具执行的 SQL 语句打印到 STDOUT,可以和 --dry-run 同时使用。
–progress:在复制行时,将进度报告打印到 STDERR。该值是一个逗号分隔的列表,由两部分组成。第一部分可以是 percentage, time, iterations(每秒打印次数);第二部分指定对应的数值,表示打印的频率。
–statistics:打印统计信息。
–alter-foreign-keys-method :指定修改外键以使引用新表。当该工具重命名原始表以让新表取而代之时,外键跟随被重命名的表,因此必须更改外键以引用新表。支持两种方式:rebuild_constraints 和 drop_swap 。

  • auto:自动决定那种方式是最好的。如果可以使用 rebuild_constraints 则使用,否则使用 drop_swap。
  • rebuild_constraints:此方法使用 ALTER TABLE 删除并重新添加引用新表的外键约束。这是首选的方式,除非子表(引用 DDL 表中列的表)太大,更改会花费太长时间。通过比较子表的行数和将行从旧表复制到新表的速度来确定是否使用该方式。如果估计可以在比 --chunk-time 更短的时间内修改子表,那么它将使用这种方式。估计修改子表(引用被修改表)所需的时间方法:行复制率乘以 --chunk-size-limit,因为 MySQL alter table 通常比复制行过程快得多。备注:由于 MySQL 中的限制,外键在更改后不能与之前的名称相同。该工具在重新定义外键时必须重命名外键,通常在名称中添加一个前导下划线 ‘_’ 。在某些情况下,MySQL 还会自动重命名外键所需的索引。
  • drop_swap:禁用外键检查(FOREIGH_KEY_CHECKS=0),先删除原始表,然后将新表重命名到原来的位置。这与交换新旧表的方法不同,后者使用的是客户端应用程序无法检测到的原子 RENAME。风险:在 drop 原表和 rename 临时表之间的一段时间,DDL 的表不存在,查询这个表的语句将会返回报错。如果 rename 执行失败,没有修改成原表名称,但是原表已经被永久删除。

3.使用限制和风险

由于表上创建有触发器,若表的更新此时比较频繁很可能遇见锁争用问题。之前在给线上表增加索引时就遇见过这种问题,应用端频繁的报死锁错误,在停止 pt-osc 并删除触发器后死锁问题解决。

3.1.使用限制

由于 pt-osc 需要使用触发器来同步表上的变更,所以在使用时也有一些相应的限制:

  1. 原始表上必须有主键或者唯一键,因为创建的 DELETE 触发器依赖主键或者唯一键进行数据同步;不过,若原始表上没有主键或者唯一键,但是即将执行的变更包含创建主键或唯一键的操作也可以。
  2. 原始表上不能存在触发器。
  3. pt-online-schema-change 适用于 Percona XtraDB Cluster (PXC) 5.5.28-23.7 及更高版本,但有两个限制:只能更改 InnoDB 表,并且 wsrep_OSU_method 必须设置为 TOI。如果主机是集群节点并且表是 MyISAM 或正在转换为 MyISAM (ENGINE=MyISAM),或者wsrep_OSU_method 不是 TOI,则该工具将退出并报错。

 3.2.风险

3.2.1.更改有外键引用的表的结构或者属性

更改有外键引用的表会让操作比较负载,目前 pt-osc 提供三种方式处理外键:

  • rebuild_constraints:该方式会在第六步更改表名后,执行一个原子操作先删除外键再重新添加外键。
  • drop_swap:该方式会在第六步更改表名前删除原始表,然后将影子表重命名。这会导致表短暂的不存在,若是对表的查询很频繁会导致错误。
  • none:该方式执行的操作和处理无外键引用的表是相同的,但是外键实际上是引用了已经删除的表。

3.2.2.锁争用问题

表上创建有触发器,若表的更新此时比较频繁很可能遇见锁争用问题。之前在给线上表增加索引时就遇见过这种问题,应用端频繁的报死锁错误,在停止 pt-osc 并删除触发器后死锁问题解决。

// 查看存在的触发器
SHOW TRIGGERS FROM passport;
//删除掉TRIGGERS
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name;

3.2.3.创建唯一索引或者主键

由于 pt-osc 使用 INSERT LOW_PRIORITY IGNORE 方式同步原始表和影子表之间的数据,所以若新建唯一索引的列上有重复数据将会导致数据的丢失。若是需要创建唯一索引或主键需要提前确认数据是否重复,是否允许缺失等,需要指定选项 --no-check-alter。

你可能感兴趣的:(Mysql,mysql)