MySQL中使用pt-osc的一些小結

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


分享到:


相關文章: