Percona的pt-osc工具算是DBA的一個福利工具。想想一個數據量有些大的表,在上面做DDL操作真是一種煎熬,我們也基本理解了這是一種空間換時間的策略,儘可能保證一些準備和同步工作能夠離線進行,而正式的切換是一個最小粒度的rename操作。
但是這樣一個很柔性的操作,其實有一些問題還需要我們更深層次的分析和理解,否則我們使用pt-osc就是一個執行者而已,還沒有掌握這種思路的核心。
比如有一個表newtest,我們需要給它加上一個索引,可以使用pt-osc的dry-run選項和print組合來得到執行的一些細節信息。
DDL語句類似這樣:
alter table newtest add index idx_newtest_name(name),使用pt-online-schema-change,命令如下:
[root@localhost bin]# ./pt-online-schema-change --host=127.0.0.1 -u pt_osc -p xxxx -P3306 --alter='add index idx_newtest_name(name)' --print D=test,t=newtest --dry-run
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
Starting a dry run. `test`.`newtest` will not be altered. Specify --execute instead of --dry-run to alter the table.
Creating new table...
CREATE TABLE `test`.`_newtest_new` (
`id` int(11) NOT NULL,
`name` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._newtest_new OK.
Altering new table...
ALTER TABLE `test`.`_newtest_new` add index idx_newtest_name(name)
Altered `test`.`_newtest_new` OK.
Not creating triggers because this is a dry run.
Not copying rows because this is a dry run.
INSERT LOW_PRIORITY IGNORE INTO `test`.`_newtest_new` (`id`, `name`) SELECT `id`, `name` FROM `test`.`newtest` LOCK IN SHARE MODE /*pt-online-schema-change 4358 copy table*/
Not swapping tables because this is a dry run.
Not dropping old table because this is a dry run.
Not dropping triggers because this is a dry run.
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_ins`
2018-06-24T23:30:52 Dropping new table...
DROP TABLE IF EXISTS `test`.`_newtest_new`;
2018-06-24T23:30:52 Dropped new table OK.
Dry run complete. `test`.`newtest` was not altered.
通過這種方式我們可以很清晰的看到一個變更的思路,是創建一個影子表_newtest_new,然後新的DDL變更部署在這個上面,因為這個時候表裡還沒有數據,所以這個過程很快。
接下來會在原表上添加三個觸發器,然後開始數據的複製,基本原理就是insert into _newtest_new select *from newtest這種形式。
然後數據複製完成之後,開啟rename模式,這個過程會分為兩個步驟,把表newtest改名為一個別名 _newtest_old,同時把_newtest_new修改為newtest
最後來清理戰場,刪除原來的舊錶,刪除原來的觸發器。
這個過程我相信做過pt-osc的同學,簡單看下日誌也能夠明白這個原理和過程,但是顯然上面的信息是很粗略的,而且有些信息是經不起推敲的。我們需要了解更深層次的細節來看看觸發器的方式是否可行。
如果用觸發器的方式可以直接變更,我們直接手工觸發整個變更是否可行,有什麼瓶頸?帶著這個問題我們來逐個分析一下。
首先創建的三個觸發器,delete,insert,update他們是怎麼把增量數據寫入到新表中的。因為新表的數據複製是一個離線的過程,目標是insert,delete,update操作不應該被阻塞,我們來逐個分析一下。打開代碼來看一下:
先來看一下insert trigger,整個過程的思路就是replace into,如果在數據複製期間,有insert請求進來,那麼replace into就類似於insert,如果複製流程已經完成,那麼insert請求進來,就會是一個replace into實現的類似update的過程。
my $insert_trigger
= "CREATE TRIGGER `${prefix}_ins` AFTER INSERT ON $orig_tbl->{name} "
. "FOR EACH ROW "
. "REPLACE INTO $new_tbl->{name} ($qcols) VALUES ($new_vals)";
而update trigger的作用和上面的類似,如果數據複製的還沒有完成,那麼也會轉換為一個replace into的insert 操作,如果複製已經完成,那麼就會是一個update操作。這裡需要注意的一點是,如果複製還沒有完成的時候,處理update請求,我們直接insert,那麼稍後表裡就會生成兩條記錄,顯然這是不合理的(實際上確實不可行),所以我們需要保證一個delete操作能夠避免這種尷尬的數據衝突出現。
my $update_trigger
= "CREATE TRIGGER `${prefix}_upd` AFTER UPDATE ON $orig_tbl->{name} "
. "FOR EACH ROW "
. "BEGIN "
. "DELETE IGNORE FROM $new_tbl->{name} WHERE !($upd_index_cols) AND $del_index_cols;"
. "REPLACE INTO $new_tbl->{name} ($qcols) VALUES ($new_vals);"
. "END ";
然後就是delete操作,這個過程相比前面的過程會略微簡單一些,使用了delete ignore的方式,基本能夠杜絕潛在的性能問題。
my $delete_trigger
= "CREATE TRIGGER `${prefix}_del` AFTER DELETE ON $orig_tbl->{name} "
. "FOR EACH ROW "
. "DELETE IGNORE FROM $new_tbl->{name} "
. "WHERE $del_index_cols";
所以如此看來觸發器的過程是一系列隱式的操作組成,但是實際上這個表很大的情況下,這個操作的代價就很高了。如果存在1000萬數據,整個阻塞的過程會把這個時間無限拉長,顯然也不合理,所以這裡做到了小步快走的方式,把一個表的數據拆分成多份,也叫chunk,然後逐個擊破。這樣一來數據做了切分,粒度小了,阻塞的影響也會大大降低。
所以pt-osc工具實現了一個切分的思路,這個是原本的觸發器不可替代的。整個數據的複製中增量DML的replace into處理很巧妙,加上數據的粒度拆分,讓這個事情變得可控可用。
當然實際的pt-osc工具的邏輯遠比這個複雜,裡面考慮了很多額外的因素,比如對於外鍵,或者是表中的約束的信息等。
最後來一個基本完整的變更日誌。
[root@localhost bin]# ./pt-online-schema-change --host=127.0.0.1 -u pt_osc -p pt_osc -P33091 --alter='add index idx_newtest_name(name)' --print D=test,t=newtest --execute
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`.`newtest`...
Creating new table...
CREATE TABLE `test`.`_newtest_new` (
`id` int(11) NOT NULL,
`name` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._newtest_new OK.
Altering new table...
ALTER TABLE `test`.`_newtest_new` add index idx_newtest_name(name)
Altered `test`.`_newtest_new` OK.
2018-06-24T23:35:54 Creating triggers...
2018-06-24T23:35:54 Created triggers OK.
2018-06-24T23:35:54 Copying approximately 4 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_newtest_new` (`id`, `name`) SELECT `id`, `name` FROM `test`.`newtest` LOCK IN SHARE MODE /*pt-online-schema-change 4424 copy table*/
2018-06-24T23:35:54 Copied rows OK.
2018-06-24T23:35:54 Analyzing new table...
2018-06-24T23:35:54 Swapping tables...
RENAME TABLE `test`.`newtest` TO `test`.`_newtest_old`, `test`.`_newtest_new` TO `test`.`newtest`
2018-06-24T23:35:54 Swapped original and new tables OK.
2018-06-24T23:35:54 Dropping old table...
DROP TABLE IF EXISTS `test`.`_newtest_old`
2018-06-24T23:35:54 Dropped old table `test`.`_newtest_old` OK.
2018-06-24T23:35:54 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_newtest_ins`
2018-06-24T23:35:54 Dropped triggers OK.
Successfully altered `test`.`newtest`.
閱讀更多 楊建榮的學習筆記 的文章