前段时间使用percona的osc工具,遇到了死锁,今天简单回顾下造成死锁的原因,并思考下gh-ost替换pt-osc的可行性。
pt-online-schema-change过程
第一步:
首先使用帐号密码连接到mysql后,获取指定表的状态信息,检查是否有触发器,检查表是否有主键。
第二步:
接着按照修改表的表定义,新建一个名为'_tb_new'不可见的临时表,对这个表执行alter添加字段,并校验是否执行成功。
第三步:
然后针对源表创建三个触发器,分别如下:
create trigger db_tb_del after delete on db.tb for each row delete ignore from db._tb_new where db._tb_new.id <=> OLD.id #删掉新表中db._tb_new.id <=> OLD.id的数据,否则忽略操作
create trigger db_tb_del after update on db.tb for each row replace into db._tb_new(id,...) values(new.id,...) #源表执行update的时候,把对应的数据replace into的方式写入新表
create trigger db_tb_del after insert on db.tb for each row replace into db._tb_new(id,...) values(new.id,...) #源表执行iinsert操作的时候,把对应的数据replace into的方式写入新表
第四步:
触发器创建好之后会执行insert low_priority ignore into db._tb_new(id,..) select id,... from tb lock in share mode语句复制源表数据到新表。
第五步:
复制完成之后执行语句:rename table db.tb to db._tb_old,db._tb_new to db.tb同时把源表修改为_tb_old格式,把新表_tb_new修改为源表名字的原子修改。
第六步:
接着,如果没有加不删除old表的选项,那么就会删除Old表,然后删除三个触发器。到这里就完成了在线表结构的修改 。整个过程只在rename表的时间会锁一下表,其他时候不锁表。
pt-online-schema-change死锁
LATEST DETECTED DEADLOCK
2019-02-27 21:53:21 7ef572991700 *** (1) TRANSACTION: TRANSACTION 48587163201, ACTIVE 1 sec inserting mysql tables in use 3, locked 3 LOCK WAIT 949 lock struct(s), heap size 95784, 477 row lock(s), undo log entries 3 MySQL thread id 414993, OS thread handle 0x7ef5724ed700, query id 207464437 172.16.158.178 merchant1 update REPLACE INTO merchant
._mht_trade_request_new
(request_id
, trans_id
, ref_trans_id
,...) values (...)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 7325 page no 3348 n bits 328 index idx_uk_mht_trade_request
of table merchant
._mht_trade_request_new
trx id 48587163201 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
*** (2) TRANSACTION: TRANSACTION 48587157030, ACTIVE 20 sec inserting mysql tables in use 2, locked 2 15756 lock struct(s), heap size 1406504, 506420 row lock(s), undo log entries 490668 MySQL thread id 415019, OS thread handle 0x7ef572991700, query id 207464502 localhost root Sending data INSERT LOW_PRIORITY IGNORE INTO merchant
._mht_trade_request_new
(request_id
, trans_id
, ref_trans_id
,...) SELECT request_id
, trans_id
, ref_trans_id
*** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 7325 page no 3348 n bits 328 index idx_uk_mht_trade_request
of table merchant
._mht_trade_request_new
trx id 48587157030 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 7325 page no 18448 n bits 176 index idx_uk_mht_trade_request
of table merchant
._mht_trade_request_new
trx id 48587157030 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
*** WE ROLL BACK TRANSACTION (1)
分析
事务一:
REPLACE INTO merchant._mht_trade_request_new (request_id, trans_id,......) VALUES (NEW.request_id, NEW.trans_id, NEW.ref_trans_id, NE
业务更具条件更新,对mht_trade_request持有排他RECORD LOCKS;
更新后触发器被触发,再以replace的方式插入_mht_trade_request_new,需要对_mht_trade_request_new持有一个隐式的自增锁;
事务二:
INSERT LOW_PRIORITY IGNORE INTO merchant._mht_trade_request_new (request_id, trans_id, ref_trans_id, ......) SELECT request_id, trans_id, ref_trans_id,
insert into select from,会先对_mht_trade_request_new加上表级的自增锁;
新表加锁后,在更具条件中的范围去申请原表mht_trade_request的记录锁;
由于事务1先更新原表mht_trade_request,对更新的记录加上排它锁,触发器还没触发时,事务2开始执行,这个时候事务2现对新表加表锁,当它再去申请对原表加记录级别的共享锁时,发现部分记录被加上了排他锁,所以需要等待。这时事务1触发器触发了,需要对新表获取一个自增锁,造成了回环,产生死锁。
加锁情况
事务一:
持有:mht_trade_request表记录上的X锁;
等待:_mht_trade_request_new表上的auto_inc lock;
事务二:
持有:_mht_trade_request_new表上的auto_inc lock;
等待:mht_trade_request表记录上的S锁;
gh-ost简介
Online DDL一直是比较头疼的问题,尤其是大表的DDL,只能选在业务低峰期进行操作。业界常用的工具有percona的online-schema-change,facebook 的OSC等,他们本质上都是基于触发器,简单来讲就是通过数据库的触发器把作用在源表的操作在一个事务内同步到修改后的表中,这在业务高峰期时会极大的加重主库的负载。
gh-ost 是由 Github 开发的 Online DDL 工具,使用 binlog 代替触发器来做增量数据同步,这样可以降低主库的负载,异步的执行。
gh-ost原理
基于触发器的 Online DDL 工具
先来看下基于触发器的工具原理,可以大致分为以下步骤:
根据原来的表结构执行 alter 语句,新建一个更新表结构之后的表,通常称为幽灵表。对用户不可见;
把原来表的已有数据 copy 到幽灵表;
在 copy 的过程中,会有新的数据过来,这些数据要同步到幽灵表,也就是 “Online” 的精髓;
copy 和同步完成后,锁住源表,交换表名,幽灵表替换源表;
删除源表,完成 online DDL。
其中第三步是最为关键的。最开始办法就是使用触发器,在源表上增加几个触发器,例如当源表执行 INSERT,UPDATE,DELETE 语句,就把这些操作通过触发器同步到幽灵表上,这样在幽灵表上执行的语句和源表的语句就属于同一个事务,显然这样会影响主库的性能。
后面出现了异步的模式,使用触发器把对源表的操作保存到一个 Changelog 表中,不真正的去执行,专门有一个后台的线程从 Changelog 表读取数据应用到幽灵表上。这种方式一定程度上缓解了主库的压力,但是保存到 Changelog 表也同样是属于同一个事务中,对性能也有不小的影响。
网上和gh-ost官方文档列举了触发器模式的几个不足之处:
触发器是用存储过程的实现的,就无法避免存储过程本身需要的开销。
增大了同一个事务的执行步骤,更多的锁争抢。
整个过程无法暂停,假如发现影响主库性能,停止 Online DDL,那么下次就需要从头来过。
他们认为多个并行的操作是不安全的。
无法在生产环境做测试。
触发器和源操作还是在同一个事务空间。
gh-ost
触发器的作用是源表和幽灵表之间的增量数据同步,gh-ost 放弃了触发器,使用 binlog 来同步。gh-ost 作为一个伪装的备库,可以从主库/备库上拉取 binlog,过滤之后重新应用到主库上去,相当于主库上的增量操作通过 binlog 又应用回主库本身,不过是应用在幽灵表上。
官网介绍图:
gh-ost 首先连接到主库上,根据 alter 语句创建幽灵表,然后作为一个”备库“连接到其中一个真正的备库上,一边在主库上拷贝已有的数据到幽灵表,一边从备库上拉取增量数据的 binlog,然后不断的把 binlog 应用回主库。图中 cut-over 是最后一步,锁住主库的源表,等待 binlog 应用完毕,然后替换 gh-ost 表为源表。
这种架构带来以下好处:
整个流程异步执行,对于源表的增量数据操作没有额外的开销,高峰期变更业务对性能影响小。
降低写压力,触发器操作都在一个事务内,gh-ost 应用 binlog 是另外一个连接在做。
可停止,binlog 有位点记录,如果变更过程发现主库性能受影响,可以立刻停止拉binlog,停止应用 binlog,稳定之后继续应用。
可测试,gh-ost 提供了测试功能,可以连接到一个备库上直接做 Online DDL,在备库上观察变更结果是否正确,再对主库操作,心里更有底。
并行操作,对于 gh-ost 来说就是多个对主库的连接。
gh-ost限制
使用方式
在下一篇文章中补充gh-ost的测试及详细用法。
关于gh-ost的设计
之前也被percona的osc坑过,在了解gh-ost的设计原理后,觉得这应该是目前市面上最可靠的onlie ddl工具了。
Cut-over step
有关gh-ost最后的一步:
https://github.com/github/gh-ost/blob/master/doc/cut-over.md
以安全性为考量,原子性的切换步骤:
Connections C1..C9 operate on tbl with normal DML: INSERT, UPDATE, DELETE
Connection C10: CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
Connection C10: LOCK TABLES tbl WRITE, tbl_old WRITE
Connections C11..C19, newly incoming, issue queries on tbl but are blocked due to the LOCK
Connection C20: RENAME TABLE tbl TO tbl_old, ghost TO tbl
This is blocked due to the LOCK, but gets prioritized on top connections C11..C19 and on top C1..C9 or any other connection that attempts DML on tbl
Connections C21..C29, newly incoming, issue queries on tbl but are blocked due to the LOCK and due to the RENAME, waiting in queue
Connection C10: checks that C20's RENAME is applied (looks for the blocked RENAME in show processlist)
Connection 10: DROP TABLE tbl_old
Nothing happens yet; tbl is still locked. All other connections still blocked.
Connection 10: UNLOCK TABLES
BAM! The RENAME is first to execute, ghost table is swapped in place of tbl, then C1..C9, C11..C19, C21..C29 all get to operate on the new and shiny tbl
切换步骤非常巧妙,可以多多思考。
下次写一些关于gh-ost的使用方法。