pt-osc工作的提前是表有主键或者唯一键,如果没有主键或者唯一键,为何不行采用pt-osc来进行DDL操作,
这还得从它的工作原理说起。pt-osc的原理是新建一个更改后的表结构,再在原表上创建三个触发器,分别
对应update,delete,insert操作,以保证新的DML操作能同步到新表,再按chunk拆分copy原表的数据到新表,
最后最一次rename操作,因此整个过程都是几乎不堵塞的,rename 操作会堵塞,但是rename操作时间很短,
因此也可以认为是不堵塞的。下面将其中主要流程分开说明:
三个触发器,分别对应update,delete,insert。其中update和insert都对应replace into操作到新表,replace into
操作才是保证原表和新表记录一致性的关键,reaplce into操作是依赖主键或者唯一键来工作的,如果没有主键或者
唯一键,那么repalce into操作就是简单的insert into操作,那么当update原表一条记录,触发器则replace这条记录
到新表,由于没有主键或者唯一键,则直接insert into update之后的记录到新表,然后在copy阶段,再次读到该条
记录,采用insert ignore操作,但是没有主键和唯一键,又是直接insert into 记录到新表,这样就导致新表多出来
一条记录,若该记录更新较多,则会在新表插入多条记录,从而导致数据不一致。对于insert 一条记录到原表,
也是先通过触发器insert到新表一条记录,然后在copy阶段,则可能再次被insert 一次,导致数据不一致。
对于delete操作,不管是先copy还是先delete再copy,都不会导致数据不一致。
如果有主键或者唯一键,情况会如何?对于update原表一条记录,replace into一条记录到新表,由于有主键或者唯一键,
如果新表以及copy了这条数据,则通过replace into更新这条记录和原表一致,如果还没copy这套记录,那么则insert into
这条记录到新表保持数据的一致,当copy阶段,操作到这条记录时,由于是采用的insert ignore,因此对于已经存在的
记录,则直接忽略,数据还是一致的,如果没有该记录,则insert 该条记录,保持一致。因此在有主键和唯一键的情况下,
update操作不会导致数据不一致。
对于insert 原表一条记录,原表上的触发器replace into到新表,由于之前新表没有这条记录,因此replace into直接将这条
记录insert into新表,在copy阶段,此时新表已经有这条记录了,但是采用的是insert ignore操作,因此会忽略掉该条记录,
因此原表和新表数据还是一致的。
对于delete语句,不管是先copy还是先delete再copy,都不会导致数据不一致。
也许你会说,如果在cop阶段,执行了DML操作,会不会导致数据不一致呢,答案是不会。在copy阶段,pt-osc会根据
主键或者唯一键来拆分chunk来执行copy操作,对于每一个chunk,采用的是insert ignore xxxx select xxx lock in share mode。
lock in share mode,会堵塞该chunk的其他dml操作,保证该chunk上记录操作的串行性,因此此时如果用户操作该chunk的记录,
其实是会堵塞的,只有copy完成了,用户操作才能继续,则又回到上面流程上了,这样保证每一个chunk操作的一致性。
因此copy阶段,也不会导致数据不一致。
这就是为何pt-osc一定要有主键或者唯一键的原因,为了保证原表和新表数据的一致性。
下面说说 pt-osc chunk拆分方式:
默认chunk-size 1000,如果小于一个chunk ,则整表按照一个chunk来处理,否则则按chunk size来拆分,如果没有指定chunk-size 默认为1000,
但是并不是每个chunk按照该大小来拆分,pt-osc会根据每个上次chunk的处理时间和chunk大小来计算下一个chunk的大小,以保证处理的速度,
如果指定了chunk-size 则按照指定的chunk size来拆分。
# Explicit --chunk-size disable auto chunk sizing.
$o->set('chunk-time', 0) if $o->got('chunk-size'); 如果指定了chunk-size参数大小,则将chunk-time设置为0,后续再计算chunk size的时候,会用该chunk-time
my $chunk_time = $o->get('chunk-time'); # brevity 这里获取到chunk-time 为0
# Adjust chunk size. This affects the next chunk. 计算下一次操作的chunk size
if ( $chunk_time ) { 如果指定chunk-size,则chunk_time为0,则每个chunk的大小有chunk-size决定,否则则是根据上次操作的时间和chunk size来计算下次chunk 的大小
# Calcuate a new chunk-size based on the rate of rows/s.
$tbl->{chunk_size} = $tbl->{rate}->update(
$cnt, # processed this many rows
$tbl->{nibble_time}, # is this amount of time
);
if ( $tbl->{chunk_size} < 1 ) {
# This shouldn't happen. WeightedAvgRate::update() may
# return a value < 1, but minimum chunk size is 1.
$tbl->{chunk_size} = 1;
# This warning is printed once per table.
if ( !$tbl->{warned_slow} ) {
warn ts("Rows are copying very slowly. "
. "--chunk-size has been automatically reduced to 1. "
. "Check that the server is not being overloaded, "
. "or increase --chunk-time. The last chunk "
. "selected $cnt rows and took "
. sprintf('%.3f', $tbl->{nibble_time})
. " seconds to execute.\n");
$tbl->{warned_slow} = 1;
}
}
# Update chunk-size based on the rate of rows/s.
$nibble_iter->set_chunk_size($tbl->{chunk_size});
}
# pt_online_schema_change:10809 118966 SHOW WARNINGS
# Retry:3762 118966 Try code succeeded
# pt_online_schema_change:9335 118966 Nibble time: 0.526419878005981 上个chunk的处理时间
# NibbleIterator:5538 118966 0 rows in nibble 8
# NibbleIterator:5550 118966 No rows in nibble or nibble skipped
# pt_online_schema_change:9358 118966 Average copy rate (rows/s): 13159
# WeightedAvgRate:5256 118966 Master op time: 6728 n / 0.526419878005981 s
# WeightedAvgRate:5262 118966 Weighted avg rate: 13249.0307387304 n/s 计算平均速度
# WeightedAvgRate:5272 118966 Adjust n to 6624 设置限制为平均速度的 / 2
# NibbleIterator:5634 118966 Set new chunk size (LIMIT): 6624
# ReplicaLagWaiter:4897 118966 All slaves caught up
# MySQLStatusWaiter:5145 118966 Checking status variables
# pt_online_schema_change:8502 118966 SHOW GLOBAL STATUS LIKE ? Threads_running
# MySQLStatusWaiter:5148 118966 Threads_running = 1
# MySQLStatusWaiter:5175 118966 All var vals are low enough
# pt_online_schema_change:10872 118966 EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`online_test` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/ params: 024b8b98a545fc23cf6b832155644c1e 6623
如果指定了chunk-size的值,则按照每个chunk size来拆分,每个chunk 数量size - 1,而不再上每次计算下次chunk size大小
# Retry:3745 3029 Try 1 of 10
# pt_online_schema_change:10790 3029 INSERT LOW_PRIORITY IGNORE INTO `test`.`_______online_test_new` (`id`, `c1`, `c2`, `c3`, `c4`, `c5`, `c8`) SELECT `id`, `c1`, `c2`, `c3`, `c4`, `c5`, `c8` FROM `test`.`online_test` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 3029 copy nibble*/ lower boundary: 444501 upper boundary: 445000
# pt_online_schema_change:10809 3029 SHOW WARNINGS
# Retry:3762 3029 Try code succeeded
# pt_online_schema_change:9335 3029 Nibble time: 0.0557880401611328
# NibbleIterator:5538 3029 0 rows in nibble 890
# NibbleIterator:5550 3029 No rows in nibble or nibble skipped
# pt_online_schema_change:9358 3029 Average copy rate (rows/s): 8484
# ReplicaLagWaiter:4897 3029 All slaves caught up
# MySQLStatusWaiter:5145 3029 Checking status variables
# pt_online_schema_change:8502 3029 SHOW GLOBAL STATUS LIKE ? Threads_running
# MySQLStatusWaiter:5148 3029 Threads_running = 1
# MySQLStatusWaiter:5175 3029 All var vals are low enough
# pt_online_schema_change:10872 3029 EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `test`.`online_test` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/ params: 445001 499
有了chunk size,pt-osc采用的是 select id from xxx where id >= ? order by id limit (chunk_size -1),2 获取到每次操作chunk的最大id范围,注意这里的id可以是主键,也可以是唯一键。
这里得到两个值,第一个值是此次chunk操作的最大值,第二个值则是下一个chunk的最小值,对应下次的where id >=?
得到一个具体的id值,然后采用INSERT LOW_PRIORITY IGNORE xxx FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE 来copy数据,
where条件最该该chunk的最大值和最小值,最大值是前面select获取到的,而最小值是每次chunk操作完设置的,也是上面select查询得到的。按照这样迭代,根据主键或者唯一键升顺一个一个chunk的来操作。
另外不只是pt-osc chunk的拆分是这样方式,实际上pt-table-checksum也是按照这种方式来拆分的。