pt-online-schema-change的实现原理

pt-online-schema-change用于MySQL的在线DDL。

下面结合官方文档和general log来分析其实现原理。

测试表

mysql> show create table t2G
*************************** 1. row ***************************
       Table: t2
Create Table: CREATE TABLE `t2` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1005764 DEFAULT CHARSET=utf8
1 row in set (0.19 sec)

该表中只有1列,id,自增主键。

其中,表中已经存在一部分数据

mysql> select count(*) from t2;
+----------+
| count(*) |
+----------+
|  1005763 |
+----------+
1 row in set (0.31 sec)

利用pt-online-schema-change对该表新增一列

# pt-online-schema-change --execute --alter "ADD COLUMN c1 DATETIME" D=test,t=t2

Found 2 slaves:
  test
  hbase
Will check slave lag on:
  test
  hbase
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 `test`.`t2`...
Creating new table...
CREATE TABLE `test`.`_t2_new` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1005764 DEFAULT CHARSET=utf8
Created new table test._t2_new OK.
Waiting forever for new table `test`.`_t2_new` to replicate to test...
Altering new table...
ALTER TABLE `test`.`_t2_new` ADD COLUMN c1 DATETIME
Altered `test`.`_t2_new` OK.
2016-11-21T12:49:18 Creating triggers...
CREATE TRIGGER `pt_osc_test_t2_del` AFTER DELETE ON `test`.`t2` FOR EACH ROW DELETE IGNORE FROM `test`.`_t2_new` WHERE `test`.`_t2_ne
w`.`id` <=> OLD.`id`CREATE TRIGGER `pt_osc_test_t2_upd` AFTER UPDATE ON `test`.`t2` FOR EACH ROW REPLACE INTO `test`.`_t2_new` (`id`) VALUES (NEW.`id`)
CREATE TRIGGER `pt_osc_test_t2_ins` AFTER INSERT ON `test`.`t2` FOR EACH ROW REPLACE INTO `test`.`_t2_new` (`id`) VALUES (NEW.`id`)
2016-11-21T12:49:18 Created triggers OK.
2016-11-21T12:49:18 Copying approximately 1005075 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_t2_new` (`id`) SELECT `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND (
(`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 2352 copy nibble*/SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chun
k boundary*/Copying `test`.`t2`:  40% 00:44 remain
Copying `test`.`t2`:  82% 00:12 remain
2016-11-21T12:50:31 Copied rows OK.
2016-11-21T12:50:31 Analyzing new table...
2016-11-21T12:50:32 Swapping tables...
RENAME TABLE `test`.`t2` TO `test`.`_t2_old`, `test`.`_t2_new` TO `test`.`t2`
2016-11-21T12:50:35 Swapped original and new tables OK.
2016-11-21T12:50:35 Dropping old table...
DROP TABLE IF EXISTS `test`.`_t2_old`
2016-11-21T12:50:36 Dropped old table `test`.`_t2_old` OK.
2016-11-21T12:50:36 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_del`;
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_upd`;
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_ins`;
2016-11-21T12:50:36 Dropped triggers OK.
Successfully altered `test`.`t2`.

查看general log中的输出

161017 11:22:56     1052 Connect    root@localhost on test
         1052 Query    set autocommit=1
         1052 Query    SHOW VARIABLES LIKE 'innodb\_lock_wait_timeout'
         1052 Query    SET SESSION innodb_lock_wait_timeout=1
         1052 Query    SHOW VARIABLES LIKE 'lock\_wait_timeout'
         1052 Query    SET SESSION lock_wait_timeout=60
         1052 Query    SHOW VARIABLES LIKE 'wait\_timeout'
         1052 Query    SET SESSION wait_timeout=10000
         1052 Query    SELECT @@SQL_MODE
         1052 Query    SET @@SQL_QUOTE_SHOW_CREATE = 1/*!40101, @@SQL_MODE='NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'*/
         1052 Query    SELECT @@server_id /*!50038 , @@hostname*/
1053 Connect root@localhost on test 1053 Query set autocommit=1 1053 Query SHOW VARIABLES LIKE 'innodb\_lock_wait_timeout' 1053 Query SET SESSION innodb_lock_wait_timeout=1 1053 Query SHOW VARIABLES LIKE 'lock\_wait_timeout' 1053 Query SET SESSION lock_wait_timeout=60 1053 Query SHOW VARIABLES LIKE 'wait\_timeout' 1053 Query SET SESSION wait_timeout=10000 1053 Query SELECT @@SQL_MODE 1053 Query SET @@SQL_QUOTE_SHOW_CREATE = 1/*!40101, @@SQL_MODE='NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'*/ 1053 Query SELECT @@server_id /*!50038 , @@hostname*/

上述主要是设置会话的变量信息,包括innodb_lock_wait_timeout,wait_timeout和SQL_QUOTE_SHOW_CREATE。

         1052 Query    SHOW VARIABLES LIKE 'wsrep_on'
         1052 Query    SHOW VARIABLES LIKE 'version%'
         1052 Query    SHOW ENGINES
         1052 Query    SHOW VARIABLES LIKE 'innodb_version'
         1052 Query    SHOW VARIABLES LIKE 'innodb_stats_persistent'
         1052 Query    SELECT @@SERVER_ID
         1052 Query    SHOW GRANTS FOR CURRENT_USER()
         1052 Query    SHOW FULL PROCESSLIST
         1052 Query    SHOW SLAVE HOSTS
         1052 Query    SHOW GLOBAL STATUS LIKE 'Threads_running'
         1052 Query    SHOW GLOBAL STATUS LIKE 'Threads_running'
         1052 Query    SELECT CONCAT(@@hostname, @@port)
         1052 Query    SHOW TABLES FROM `test` LIKE 't2'
         1052 Query    SHOW TRIGGERS FROM `test` LIKE 't2'
         1052 Query    /*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, @@SQL_MODE := '', @OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, @@SQL_QUOTE_SHOW_CREATE := 1 */
         1052 Query    USE `test`
         1052 Query    SHOW CREATE TABLE `test`.`t2`
         1052 Query    /*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, @@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */
         1052 Query    EXPLAIN SELECT * FROM `test`.`t2` WHERE 1=1
         1052 Query    SELECT table_schema, table_name FROM information_schema.key_column_usage WHERE referenced_table_schema='test' AND referenced_table_name='t2'
         1052 Query    SHOW VARIABLES LIKE 'wsrep_on'
         1052 Query    /*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, @@SQL_MODE := '', @OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, @@SQL_QUOTE_SHOW_CREATE := 1 */

解释: 

1. 查看参数变量,当前用户的权限,slave的信息,会话变量

2. 确认t2是否存在,t2上是否有触发器

3. 查看执行计划

4. 查看是否t2表是否被其它表外键关联。

           39 Query    USE `test`
           39 Query    SHOW CREATE TABLE `test`.`t2`
           39 Query    /*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, @@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */
           39 Query    CREATE TABLE `test`.`_t2_new` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1005764 DEFAULT CHARSET=utf8
161121 12:49:18       39 Query    ALTER TABLE `test`.`_t2_new` ADD COLUMN c1 DATETIME
           39 Query    /*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, @@SQL_MODE := '', @OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, @@SQL_QUOTE_SHOW_CREATE := 1 */
           39 Query    USE `test`
           39 Query    SHOW CREATE TABLE `test`.`_t2_new`
           39 Query    /*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, @@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */
           39 Query    CREATE TRIGGER `pt_osc_test_t2_del` AFTER DELETE ON `test`.`t2` FOR EACH ROW DELETE IGNORE FROM `test`.`_t2_new` WHERE `test`.`_t2_new`.`id` <=> OLD.`id`
           39 Query    CREATE TRIGGER `pt_osc_test_t2_upd` AFTER UPDATE ON `test`.`t2` FOR EACH ROW REPLACE INTO `test`.`_t2_new` (`id`) VALUES (NEW.`id`)
           39 Query    CREATE TRIGGER `pt_osc_test_t2_ins` AFTER INSERT ON `test`.`t2` FOR EACH ROW REPLACE INTO `test`.`_t2_new` (`id`) VALUES (NEW.`id`)

解释:

1. 根据目标表结构创建一张新表。

2. 对新表添加字段,可以看出pt-online-shema-change对表结构进行变更依赖的还是MySQL自身的Online DDL。

3. 针对目标表创建三个触发器,DELETE,UPDATE和INSERT,因为REPLACE操作只有在主键或唯一索引存在的情况下才有意义,这也就解释了为什么目标表上要有主键或唯一索引。

           39 Query    EXPLAIN SELECT * FROM `test`.`t2` WHERE 1=1
           39 Query    SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) ORDER BY `id` LIMIT 1 /*first lower boundary*/
           39 Query    SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX (`PRIMARY`) WHERE `id` IS NOT NULL ORDER BY `id` LIMIT 1 /*key_len*/
           39 Query    EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ * FROM `test`.`t2` FORCE INDEX (`PRIMARY`) WHERE `id` >= '1' /*key_len*/
           39 Query    EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) ORDER BY `id` LIMIT 999, 2 /*next chunk boundary*/
           39 Query    SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) ORDER BY `id` LIMIT 999, 2 /*next chunk boundary*/
           39 Query    EXPLAIN SELECT `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) AND ((`id` <= '1000')) LOCK IN SHARE MODE /*explain pt-online-schema-change 2352 copy nibble*/
           39 Query    INSERT LOW_PRIORITY IGNORE INTO `test`.`_t2_new` (`id`) SELECT `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) AND ((`id` <= '1000')) LOCK IN SHARE MODE /*pt-online-schema-change 2352 copy nibble*/
           39 Query    SHOW WARNINGS
           39 Query    SELECT @@SERVER_ID
           39 Query    SHOW GRANTS FOR CURRENT_USER()
           39 Query    SHOW FULL PROCESSLIST
           39 Query    SELECT @@SERVER_ID
           39 Query    SHOW GRANTS FOR CURRENT_USER()
           39 Query    SHOW FULL PROCESSLIST
161121 12:49:20       39 Query    SELECT 'pt-online-schema-change keepalive'
161121 12:49:21       39 Query    SELECT @@SERVER_ID
           39 Query    SHOW GRANTS FOR CURRENT_USER()
           39 Query    SHOW FULL PROCESSLIST
39 Query SHOW GLOBAL STATUS LIKE 'Threads_running' 39 Query EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1001')) ORDER BY `id` LIMIT 28516, 2 /*next chunk boundary*/ 39 Query SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1001')) ORDER BY `id` LIMIT 28516, 2 /*next chunk boundary*/ 39 Query EXPLAIN SELECT `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1001')) AND ((`id` <= '29517')) LOCK IN SHARE MODE /*explain pt-online-schema-change 2352 copy nibble*/ 39 Query INSERT LOW_PRIORITY IGNORE INTO `test`.`_t2_new` (`id`) SELECT `id` FROM `test`.`t2` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1001')) AND ((`id` <= '29517')) LOCK IN SHARE MODE /*pt-online-schema-change 2352 copy nibble*/

解释:

上述输出只包含两个chunk的选择。其它chunk的选择基本相同。

1. SHOW GLOBAL STATUS LIKE 'Threads_running'用于监控当前的系统负载。

2. 可以看出pt-online-schema-change是以chunk为单位进行目标表数据的拷贝。

3. 在拷贝的过程中,对目标表的相关记录加了共享锁,此时,会堵塞客户端对这些记录的DML操作。

           39 Query    ANALYZE TABLE `test`.`_t2_new` /* pt-online-schema-change */
161121 12:50:32       39 Query    RENAME TABLE `test`.`t2` TO `test`.`_t2_old`, `test`.`_t2_new` TO `test`.`t2`
161121 12:50:35       39 Query    DROP TABLE IF EXISTS `test`.`_t2_old`
161121 12:50:36       39 Query    DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_del`
           39 Query    DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_upd`
           39 Query    DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t2_ins`
           39 Query    SHOW TABLES FROM `test` LIKE '\_t2\_new'
161121 12:50:37       40 Quit    
           39 Quit    

解释:

1. 在完成数据的拷贝后,会对新表执行ANALYZE操作,这样,可及时更新新表的统计信息。

官档的解释如下:

This circumvents a potentially serious issue related to InnoDB optimizer statistics. If the table being alerted is
busy and the tool completes quickly, the new table will not have optimizer statistics after being swapped. This
can cause fast, index-using queries to do full table scans until optimizer statistics are updated (usually after 10
seconds). If the table is large and the server very busy, this can cause an outage.

2. 对目标表和新表进行RENAME操作。

3. 删除原来的目标表

4. 删除触发器。

原文地址:https://www.cnblogs.com/ivictor/p/5969026.html