bug_x

导航

 

对于 MySQL Online DDL 目前主流的有三种工具:

原生 Online DDL;

pt-osc(online-schema-change),

gh-ost

1.1 原理

1. 创建一个与原表结构相同的空表,表名是 _new 后缀;

2. 修改步骤 1 创建的空表的表结构;

3. 在原表上加三个触发器:delete/update/insert,用于 copy 数据过程中,将原表中要执行的语句在新表中执行;

4. 将原表数据以数据块(chunk)的形式 copy 到新表;

5. rename 原表为 old 表,并把新表 rename 为原表名,然后删除旧表;

6. 删除触发器。

1.2 限制

1. 原表上要有 primary key 或 unique index,因为当执行该工具时会创建一个 DELETE 触发器来更新新表;

注意:一个例外的情况是 --alter 指定的子句中是在原表中的列上创建 primary key 或 unique index,这种情况下将使用这些列用于 DELETE 触发器。

2. 不能使用 rename 子句来重命名表;

3. 列不能通过删除 + 添加的方式来重命名,这样将不会 copy 原有列的数据到新列;

4. 如果要添加的列是 not null,则必须指定默认值,否则会执行失败;

5. 删除外键约束(DROP FOREIGN KEY constraint_name),外键约束名前面必须添加一个下划线 '_',即需要指定名称 _constraint_name,而不是原始的 constraint_name;

例如:

CONSTRAINT `fk_foo` FOREIGN KEY (`foo_id`) REFERENCES `bar` (`foo_id`)

必须指定 --alter "DROP FOREIGN KEY _fk_foo"。

percona-toolkit中pt-online-schema-change工具安装和使用

pt-online-schema-change介绍

使用场景:在线修改大表结构


在数据库的维护中,总会涉及到生产环境上修改表结构的情况,修改一些小表影响很小,而修改大表时,往往影响业务的正常运转,如表数据量超过500W,1000W,甚至过亿时


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

工具介绍
pt-online-schema-change是Percona公司开发的一个工具,在percona-toolkit包里面可以找到这个功能,它可以在线修改表结构

原理:

 在使用之前需确定表table里必须带有主键或者唯一索引!!

(1)首先它会新建一张一模一样的表,表名一般是_new后缀
(2)然后在这个新表执行更改字段操作
(3)然后在原表上加三个触发器,DELETE/UPDATE/INSERT,将原表中要执行的语句也在新表中执行
(4)最后将原表的数据拷贝到新表中,然后替换掉原表


这个过程中有两个问题需要注意:
1. 触发器
因为整个过程是在线的,为了将改表过程中对原始表的更新同时更新到新表上,会创建相应的触发器,每当发生针对原始表的增删改操作,就会触发对新表的相应的操作。所以原始表上不能有其他触发器,即如果原始表上存有触发器,OSC会罢工的。

2. 外键
外键使改表操作变得更加复杂,如果原始表上有外键的话,自动rename原始表和新表的操作就不能顺利进行,必须要在数据拷贝完成后将外键更新到新表上,该工具有两种方法来支持这个操作,具体使用参数(--alter-foreign-keys-method)实现。

--alter-foreign-keys-method
外键改表前后必须持续的链接正确的表,当该工具rename原始表并用新表来取代原始表时,外键必须正确更新到新表上,并且原始表中的外键不再生效

外键会使该工具的操作变得复杂并引入额外风险。当有外键引用该表时,重命名原表和新表的原子操作将无法正常运行。在表结构修改完成后,该工具必须修改外键以指向新表。该工具支持两种方式实现该操作,具体可参考--alter-foreign-keys-method文档。外键也会造成一些副作用。最终的表将会有与原表相同的外键和索引(除非你在ALTER语句进行分别指定),但对象的名称可能会被略微修改以避免MySQL和InnoDB中的对象名称冲突。

出于安全考虑,该工具并不会真的修改表,除非你指定--execute选项,而该选项默认并未启用。该工具支持各种各样的其它措施以防止非预期的负载或其它问题,包括自动检测从节点,连接到它们,并使用如下安全检查:
(1)在大多情况下,除非表中有PRIMARY KEY或UNIQUE INDEX,该工具才会进行操作。详见--alter选项。
(2)如果检测到有replication filter,该工具会拒绝操作。详见--[no]check-replication-filters选项。
(3)该工具会停止数据拷贝操作,如果它观察到主从延迟大于--max-lag选项的值,默认为1s。
(4)该工具会停止或放弃操作,如果它检测到对服务器造成太多负载。见--max-load和--critical-load选项。
(5)该工具会设置innodb_lock_wait_timeout=1和(对于MySQL 5.5及更新的版本)lock_wait_timeout=60,因此它会更容易成为锁竞争的受害者,并更少破坏其它事务。这些值可以通过指定--set-vars来修改。
(6)该工具会拒绝修改表,如果外键约束有引用它,除非你指定了--alter-foreign-keys-method。


好处:

降低主从延时的风险

可以限速、限资源,避免操作时MySQL负载过高

建议:

在业务低峰期做,将影响降到最低

percona-toolkit安装

1.去官网下载对应的版本,官网下载地址:https://www.percona.com/downloads/percona-toolkit/LATEST/

wget https://www.percona.com/downloads/percona-toolkit/3.1/binary/redhat/7/x86_64/percona-toolkit-3.1-1.el7.x86_64.rpm

2.安装依赖

yum -y install perl-DBI perl-DBD-MySQL perl-Digest-MD5 perl-IO-Socket-SSL perl-TermReadKey

3.安装

rpm -ivh percona-toolkit-3.1-1.el7.x86_64.rpm

或者直接使用yum安装
使用yum会自动安装依赖包,比较简单

yum install -y percona-toolkit-3.0.12-1.el6.x86_64.rpm

验证

pt-online-schema-change --help

 

pt-online-schema-change使用

修改表结构而不阻塞读写操作
./bin/pt-online-schema-change --help 可以查看参数的使用

复制代码
DNS选项常用的有:
--user= 连接mysql的用户名 --password= 连接mysql的密码 --host= 连接mysql的地址 P=3306 连接mysql的端口号 D= 连接mysql的库名 t= 连接mysql的表名 --alter 修改表结构的语句 --execute 执行修改表结构 --charset=utf8 使用utf8编码,避免中文乱码 --no-version-check 不检查和更新percona toolkit版本,也不检查mysql版本等。
复制代码

案例:

添加表字段SQL语句为:ALTER TABLE test.liu_test ADD COLUMN liu int(10) DEFAULT NULL;

pt-online-schema-change   --user=root --password=xxx  --host=172.16.xx.xx  P=3306,D=test,t=liu_test  --charset=utf8 --no-version-check --execute --alter "ADD COLUMN  liu int(10) DEFAULT NULL"

  在使用之前需确定表table里必须带有主键或者唯一索引!!

 

 

 

参考官网:https://www.percona.com/doc/percona-toolkit/LATEST/pt-online-schema-change.html

 

使用二:

pt-online-schema-change在对表进行表结构变更时,会创建三个触发器。

如下文测试案例中的t2表,表结构如下:

复制代码
mysql> show create table t2\G
*************************** 1. row ***************************
       Table: t2
Create Table: CREATE TABLE `t2` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.07 sec)
复制代码

只有一个自增列字段id。

创建的触发器如下:

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`
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`)

 

DELETE触发器和INSERT触发器逻辑上没有任何问题。

 

但对于UPDATE触发器来说,如果某条记录已经拷贝到中间表中,此时,有针对该记录的UPDATE操作,且修改的是主键,此时,针对中间表触发的“REPLACE INTO `test`.`__t2_new` (`id`) VALUES (NEW.`id`)”操作只会插入一条新的记录,而不会删除原来的记录。

 

下面重现该场景

创建触发器构造测试数据

复制代码
delimiter //
create procedure p1()
begin
  declare v1 int default 1;
  set autocommit=0;
  while v1 <=10000000 do
    insert into test.t2(id) values(null);
    set v1=v1+1;
    if v1%1000 =0 then
      commit;
    end if;
  end while;
end //
delimiter ;
call p1;
复制代码

 

此时,会生成1千万的数据

复制代码
mysql> select count(*),min(id),max(id) from t2;
+----------+---------+----------+
| count(*) | min(id) | max(id)  |
+----------+---------+----------+
| 10000000 |       1 | 10000000 |
+----------+---------+----------+
1 row in set (4.29 sec)
复制代码

 

利用pt-online-schema-change对t2表添加一列

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

复制代码
No slaves found.  See --recursion-method if host localhost.localdomain 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 `test`.`t2`...
Creating new table...
CREATE TABLE `test`.`___t2_new` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10000001 DEFAULT CHARSET=utf8
Created new table test.___t2_new OK.
Altering new table...
ALTER TABLE `test`.`___t2_new` ADD COLUMN c1 DATETIME
Altered `test`.`___t2_new` OK.
2016-10-23T20:24:13 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`.`___t
2_new`.`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-10-23T20:24:13 Created triggers OK.
2016-10-23T20:24:13 Copying approximately 9429750 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 2456 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`: 29% 01:12 remain Copying `test`.`t2`: 52% 00:54 remain Copying `test`.`t2`: 76% 00:27 remain 2016-10-23T20:26:22 Copied rows OK. 2016-10-23T20:26:22 Analyzing new table... 2016-10-23T20:26:23 Swapping tables... RENAME TABLE `test`.`t2` TO `test`.`_t2_old`, `test`.`___t2_new` TO `test`.`t2` 2016-10-23T20:26:24 Swapped original and new tables OK. 2016-10-23T20:26:24 Dropping old table... DROP TABLE IF EXISTS `test`.`_t2_old` 2016-10-23T20:26:24 Dropped old table `test`.`_t2_old` OK. 2016-10-23T20:26:24 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-10-23T20:26:24 Dropped triggers OK. Successfully altered `test`.`t2`.
复制代码

当输出到上述红色信息时,打开另外一个终端窗口,执行如下命令

复制代码
 mysql -e 'update test.t2 set id=-1 where id=1'
 mysql -e 'update test.t2 set id=-2 where id=2'
 mysql -e 'update test.t2 set id=-3 where id=3'
 mysql -e 'update test.t2 set id=-4 where id=4'
 mysql -e 'update test.t2 set id=-5 where id=5'
 mysql -e 'update test.t2 set id=-6 where id=6'
 mysql -e 'update test.t2 set id=-7 where id=7'
 mysql -e 'update test.t2 set id=-8 where id=8'
 mysql -e 'update test.t2 set id=-9 where id=9'
 mysql -e 'update test.t2 set id=-10 where id=10'
复制代码

 

查看t2表修改完表结构后的数据情况

复制代码
mysql> select count(*),min(id),max(id) from t2;
+----------+---------+----------+
| count(*) | min(id) | max(id)  |
+----------+---------+----------+
| 10000010 |     -10 | 10000000 |
+----------+---------+----------+
1 row in set (3.00 sec)

mysql> select * from t2 order by id limit 20;
+-----+------+
| id  | c1   |
+-----+------+
| -10 | NULL |
|  -9 | NULL |
|  -8 | NULL |
|  -7 | NULL |
|  -6 | NULL |
|  -5 | NULL |
|  -4 | NULL |
|  -3 | NULL |
|  -2 | NULL |
|  -1 | NULL |
|   1 | NULL |
|   2 | NULL |
|   3 | NULL |
|   4 | NULL |
|   5 | NULL |
|   6 | NULL |
|   7 | NULL |
|   8 | NULL |
|   9 | NULL |
|  10 | NULL |
+-----+------+
20 rows in set (0.08 sec)
复制代码

 

可见,在执行pt-online-schema-change命令的过程中,针对原表执行的update操作并没有理所当然的反应到中间表上。

 

总结

1. 上述测试使用的pt-online-schema-change是2.2.19版本。

2. 欲进行表结构变更的表中必须存在主键或者唯一索引。

   体现在以下方面:

   1> 针对DELETE触发器

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`

        DELETE触发器是基于主键或者唯一索引进行删除的。如果id是普通索引,则原表中可能只有一行记录的删除(譬如delete from t where id=1 and name='victor'),导致中间表中所有id为1的记录的删除。

   2> 针对UPDATE触发器

         如果原表中不存在主键或者唯一索引,则replace操作会直接插入,而不会进行替换。

复制代码
mysql> create table t3(id int,name varchar(10));
Query OK, 0 rows affected (0.08 sec)

mysql> insert into t3 values(1,'a');
Query OK, 1 row affected (0.05 sec)

mysql> replace into t3 values(1,'b');
Query OK, 1 row affected (0.06 sec)

mysql> select * from t3;
+------+------+
| id   | name |
+------+------+
|    1 | a    |
|    1 | b    |
+------+------+
2 rows in set (0.00 sec)

mysql> alter table t3 modify id int primary key;
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
mysql> delete from t3 where id=1 and name='b';
Query OK, 1 row affected (0.07 sec)

mysql> alter table t3 modify id int primary key;
Query OK, 0 rows affected (0.24 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> select * from t3;
+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+
1 row in set (0.00 sec)

mysql> replace into t3 values(1,'b');
Query OK, 2 rows affected (0.01 sec)

mysql> select * from t3;
+----+------+
| id | name |
+----+------+
|  1 | b    |
+----+------+
1 row in set (0.01 sec)
复制代码

3. 即便欲进行表结构变更的表中存在主键或者唯一索引,如果在利用pt-online-schema-change进行online ddl过程中,有针对主键的更新操作,则会导致记录的新增。这点需引起注意。

 

 

 

posted on 2022-05-06 17:53  bug_x  阅读(37)  评论(0编辑  收藏  举报