如果正在看這篇文章,相信你已經知道自己的需求了。
在 mysql 5.5 版本以前,修改表結構如添加索引、修改列,需要鎖表,期間不能寫入,對於大表這簡直是災難。從5.5特別是5.6里,情況有了好轉,支持Online DDL,相關介紹見 這篇文章,而我在實際alter table過程中還是會引起 data meta lock 問題。pt-online-schema-change是Percona-toolkit一員,通過改進原生ddl的方式,達到不鎖表在線修改表結構。
1. pt-osc工作過程
- 創建一個和要執行 alter 操作的表一樣的新的空表結構(是alter之前的結構)
- 在新表執行alter table 語句(速度應該很快)
- 在原表中創建觸發器3個觸發器分別對應insert,update,delete操作
- 以一定塊大小從原表拷貝數據到臨時表,拷貝過程中通過原表上的觸發器在原表進行的寫操作都會更新到新建的臨時表
- Rename 原表到old表中,在把臨時表Rename為原表
- 如果有參考該表的外鍵,根據alter-foreign-keys-method參數的值,檢測外鍵相關的表,做相應設置的處理
- 默認最后將舊原表刪除
2. 常用選項說明
只介紹部分常用的選項
--host=xxx --user=xxx --password=xxx
連接實例信息,縮寫-h xxx -u xxx -p xxx
,密碼可以使用參數--ask-pass
手動輸入。--alter
結構變更語句,不需要ALTER TABLE
關鍵字。與原始ddl一樣可以指定多個更改,用逗號分隔。- 絕大部分情況下表上需要有主鍵或唯一索引,因為工具在運行當中為了保證新表也是最新的,需要舊表上創建 DELETE和UPDATE 觸發器,同步到新表的時候有主鍵會更快。個別情況是,當alter操作就是在c1列上建立主鍵時,DELETE觸發器將基於c1列。
- 子句不支持 rename 去給表重命名。
- alter命令原表就不支持給索引重命名,需要先drop再add,在pt-osc也一樣。(mysql 5.7 支持 RENAME INDEX old_index_name TO new_index_name)
但給字段重命名,千萬不要drop-add,整列數據會丟失,使用change col1 col1_new type constraint
(保持類型和約束一致,否則相當於修改 column type,不能online) - 子句如果是add column並且定義了not null,那么必須指定default值,否則會失敗。
- 如果要刪除外鍵(名 fk_foo),使用工具的時候外鍵名要加下划線,比如
--alter "DROP FOREIGN KEY _fk_foo"
-
D=db_name,t=table_name
指定要ddl的數據庫名和表名 -
--max-load
默認為Threads_running=25
。每個chunk拷貝完后,會檢查 SHOW GLOBAL STATUS 的內容,檢查指標是否超過了指定的閾值。如果超過,則先暫停。這里可以用逗號分隔,指定多個條件,每個條件格式:status指標=MAX_VALUE
或者status指標:MAX_VALUE
。如果不指定MAX_VALUE,那么工具會這只其為當前值的120%。
因為拷貝行有可能會給部分行上鎖,Threads_running 是判斷當前數據庫負載的絕佳指標。 -
--max-lag
默認1s。每個chunk拷貝完成后,會查看所有復制Slave的延遲情況(Seconds_Behind_Master
)。要是延遲大於該值,則暫停復制數據,直到所有從的滯后小於這個值。--check-interval
配合使用,指定出現從庫滯后超過 max-lag,則該工具將睡眠多長時間,默認1s,再檢查。如--max-lag=5 --check-interval=2
。
熟悉percona-toolkit的人都知道--recursion-method
可以用來指定從庫dsn記錄。另外,如果從庫被停止,將會永遠等待,直到從開始同步,並且延遲小於該值。 -
--chunk-time
默認0.5s,即拷貝數據行的時候,為了盡量保證0.5s內拷完一個chunk,動態調整chunk-size的大小,以適應服務器性能的變化。
也可以通過另外一個選項--chunk-size
禁止動態調整,即每次固定拷貝 1k 行,如果指定則默認1000行,且比 chunk-time 優先生效 -
--set-vars
使用pt-osc進行ddl要開一個session去操作,set-vars
可以在執行alter之前設定這些變量,比如默認會設置--set-vars "wait_timeout=10000,innodb_lock_wait_timeout=1,lock_wait_timeout=60"
。
因為使用pt-osc之后ddl的速度會變慢,所以預計2.5h只能還不能改完,記得加大wait_timeout
。 -
--dry-run
創建和修改新表,但不會創建觸發器、復制數據、和替換原表。並不真正執行,可以看到生成的執行語句,了解其執行步驟與細節,和--print
配合最佳。。 -
--execute
確定修改表,則指定該參數。真正執行alter。–dry-run與–execute必須指定一個,二者相互排斥
3. 使用疑惑(限制)
3.1 原表上不能有觸發器存在
這個很容易理解,pt-osc會在原表上創建3個觸發器,而一個表上不能同時有2個相同類型的觸發器,為簡單通用起見,只能一棍子打死。
所以如果要讓它支持有觸發器存在的表也是可以實現的,思路就是:先找到原表觸發器定義;重寫原表觸發器;最后階段將原表觸發器定義應用到新表。
3.2 通過觸發器寫數據到臨時新表,會不會出現數據不一致或異常
這其實是我的一個顧慮,因為如果update t1,觸發update t2,但這條數據還沒copy到t2,不就有異常了嗎?后台通過打開general_log,看到它創建的觸發器:
1
2
3
4
5
6
7
8
9
10
11
12
|
6165 Query
CREATE TRIGGER `pt_osc_confluence_sbtest3_del` AFTER DELETE ON `confluence`.`sbtest3`
FOR EACH ROW DELETE IGNORE FROM `confluence`.`_sbtest3_new` WHERE `confluence`.`_sbtest3_new`.`id` <=> OLD.`id`
6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_upd` AFTER UPDATE ON `confluence`.`sbtest3`
FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_ins` AFTER INSERT ON `confluence`.`sbtest3`
FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
並且copy操作是:
6165 Query INSERT LOW_PRIORITY IGNORE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`)
SELECT `id`, `k`, `c`, `pad` FROM `confluence`.`sbtest3` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '4692805')) AND ((`id` <= '4718680'))
LOCK IN SHARE MODE /*pt-online-schema-change 46459 copy nibble*/
|
在原表上update,新臨時表上是replace into整行數據,所以達到有則更新,無則插入。同時配合后面的 insert ignore,保證這條數據不會因為重復而失敗。
3.3 為什么外鍵那么特殊
假設 t1 是要修改的表,t2 有外鍵依賴於 t1,_t1_new 是 alter t1 產生的新臨時表。
這里的外鍵不是看t1上是否存在外鍵,而是作為子表的 t2。主要問題在 rename t1 時,t1“不存在”導致t2的外鍵認為參考失敗,不允許rename。
pt-osc提供--alter-foreign-keys-method
選項來決定怎么處理這種情況:
rebuild_constraints
,優先采用這種方式- 它先通過 alter table t2 drop fk1,add _fk1 重建外鍵參考,指向新表
- 再 rename t1 t1_old, _t1_new t1 ,交換表名,不影響客戶端
- 刪除舊表 t1_old
但如果字表t2太大,以致alter操作可能耗時過長,有可能會強制選擇 drop_swap。
涉及的主要方法在pt-online-schema-change
文件的 determine_alter_fk_method, rebuild_constraints,swap_tables三個函數中。
drop_swap
,- 禁用t2表外鍵約束檢查
FOREIGN_KEY_CHECKS=0
- 然后 drop t1 原表
- 再 rename _t1_new t1
這種方式速度更快,也不會阻塞請求。但有風險,第一,drop表的瞬間到rename過程,原表t1是不存在的,遇到請求會報錯;第二,如果因為bug或某種原因,舊表已刪,新表rename失敗,那就太晚了,但這種情況很少見。
我們的開發規范決定,即使表間存在外鍵參考關系,也不通過表定義強制約束。
- 禁用t2表外鍵約束檢查
3.4 在使用之前需要對磁盤容量進行評估
使用OSC會使增加一倍的空間,包括索引
而且在 Row Based Replication 下,還會寫一份binlog。不要想當然使用--set-vars
去設置 sql_log_bin=0,因為在這個session級別,alter語句也要在從庫上執行,除非你對從庫另有打算。
4. 使用 pt-osc原生 5.6 online ddl相比,如何選擇
- online ddl在必須copy table時成本較高,不宜采用
- pt-osc工具在存在觸發器時,不適用
- 修改索引、外鍵、列名時,優先采用online ddl,並指定 ALGORITHM=INPLACE
- 其它情況使用pt-osc,雖然存在copy data
- pt-osc比online ddl要慢一倍左右,因為它是根據負載調整的
- 無論哪種方式都選擇的業務低峰期執行
- 特殊情況需要利用主從特性,先alter從庫,主備切換,再改原主庫
借助percona博客一張圖說明一下:
5. 示例
-
添加新列
完整輸出過程1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253[root@ssd- 34 sysbench]# pt-online-schema-change --user=user --password=password --host=10.0.201.34 --alter "ADD COLUMN f_id int default 0" D=confluence,t=sbtest3 --print --executeNo slaves found. See --recursion-method if host ssd-34 has slaves.Not checking slave lag because no slaves were found and --check-slave-lag was not specified.Operation, tries, wait:analyze_table, 10, 1copy_rows, 10, 0.25create_triggers, 10, 1drop_triggers, 10, 1swap_tables, 10, 1update_foreign_keys, 10, 1Altering `confluence`.`sbtest3`...Creating new table... ==> 創建新表CREATE TABLE `confluence`.`_sbtest3_new` (`id` int( 10) unsigned NOT NULL AUTO_INCREMENT,`k` int( 10) unsigned NOT NULL DEFAULT '0',`c` char( 120) COLLATE utf8_bin NOT NULL DEFAULT '',`pad` char( 60) COLLATE utf8_bin NOT NULL DEFAULT '',PRIMARY KEY (`id`),KEY `k_3` (`k`)) ENGINE=InnoDB AUTO_INCREMENT= 5000001 DEFAULT CHARSET=utf8 COLLATE=utf8_bin MAX_ROWS=1000000Created new table confluence._sbtest3_new OK.Altering new table... ==> 使用ddl修改新表結構ALTER TABLE `confluence`.`_sbtest3_new` ADD COLUMN f_id int default 0Altered `confluence`.`_sbtest3_new` OK.2016-05-24T20:54:23 Creating triggers... ==> 在舊表上創建3個觸發器CREATE TRIGGER `pt_osc_confluence_sbtest3_del` AFTER DELETE ON `confluence`.`sbtest3` FOR EACH ROWDELETE IGNORE FROM `confluence`.`_sbtest3_new` WHERE `confluence`.`_sbtest3_new`.`id` <=> OLD.`id`CREATE TRIGGER `pt_osc_confluence_sbtest3_upd` AFTER UPDATE ON `confluence`.`sbtest3` FOR EACH ROWREPLACE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)CREATE TRIGGER `pt_osc_confluence_sbtest3_ins` AFTER INSERT ON `confluence`.`sbtest3` FOR EACH ROWREPLACE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)2016-05-24T20:54:23 Created triggers OK.2016-05-24T20:54:23 Copying approximately 4485573 rows... ==> 分塊拷貝數據到新表INSERT LOW_PRIORITY IGNORE INTO `confluence`.`_sbtest3_new` (`id`, `k`, `c`, `pad`)SELECT `id`, `k`, `c`, `pad` FROM `confluence`.`sbtest3` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?))LOCK IN SHARE MODE /*pt-online-schema-change 44155 copy nibble*/SELECT /*! 40001 SQL_NO_CACHE */ `id` FROM `confluence`.`sbtest3` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/Copying `confluence`.`sbtest3`: 36% 00:52 remainCopying `confluence`.`sbtest3`: 69% 00:26 remain2016-05-24T20:56:01 Copied rows OK.2016-05-24T20:56:01 Analyzing new table...2016-05-24T20:56:01 Swapping tables... ==> 交換新舊表RENAME TABLE `confluence`.`sbtest3` TO `confluence`.`_sbtest3_old`, `confluence`.`_sbtest3_new` TO `confluence`.`sbtest3`2016-05-24T20:56:01 Swapped original and new tables OK.2016-05-24T20:56:01 Dropping old table... ==> 刪除舊表DROP TABLE IF EXISTS `confluence`.`_sbtest3_old`2016-05-24T20:56:02 Dropped old table `confluence`.`_sbtest3_old` OK.2016-05-24T20:56:02 Dropping triggers...DROP TRIGGER IF EXISTS `confluence`.`pt_osc_confluence_sbtest3_del`;DROP TRIGGER IF EXISTS `confluence`.`pt_osc_confluence_sbtest3_upd`;DROP TRIGGER IF EXISTS `confluence`.`pt_osc_confluence_sbtest3_ins`;2016-05-24T20:56:02 Dropped triggers OK.Successfully altered `confluence`.`sbtest3`. -
修改列類型
1234567pt-online-schema-change h=10.0.201.34,P=3306,u=jacky,p=xxx,D=confluence,t=sbtest3 \- -alter "CHANGE pad f_pad varchar(60) NOT NULL DEFAULT '' " \- -print --dry-runpt-online-schema-change -ujacky -p xxx -h "10.0.201.34" D=confluence,t=sbtest3 \- -alter "CHANGE pad f_pad varchar(60) NOT NULL DEFAULT '' " \- -execute -
添加刪除索引
放后台執行123pt-online-schema-change --user=user --ask-pass --host=10.0.201.34 \--alter "DROP KEY cid, ADD KEY idx_corpid_userid(f_corp_id,f_user_id) " \D=confluence,t=sbtest3 --print --execute -
修改主鍵
在我的環境里有不少表設計之初沒有自增id,而是采用復合主鍵,pt-osc 對刪除、添加主鍵會特殊處理,詳見 這里。
6. 錯誤處理
1. 存在trigger
1
2
3
|
[zx@mysql-5 ~]$ pt-online-schema-
change -u user -p password -h 10.0.200.195 \
--alter="MODIFY COLUMN f_receiver varchar(128) NOT NULL DEFAULT '' AFTER f_user_id" --dry-run D=db_name,t=table_name
The
table `db_name`.`table_name` has triggers. This tool needs to create its own triggers, so the table cannot already have triggers.
|
表上存在觸發器,不適用。
2. no-version-check
1
2
3
|
$ pt-online-schema-change -uuser -ppassword --alter
"add key id_provice(f_provice)" \
D=db_name,t=tb_name -h rdsxxxxxx.mysql.rds.aliyuncs.com
Can't
use an undefined value as an ARRAY reference at /usr/bin/pt-online-schema-change line 7335.
|
這個錯誤在阿里雲RDS上執行時出現的,我以為是我哪里語法寫錯了,但拿到原生5.6的版本上就沒問題了,加上--no-version-check
選項就好了,見 https://help.aliyun.com/knowledge_detail/13098164.html ,沒深究,應該是pt去驗證mysql server版本的時候從rds拿到的信息不對,導致格式出錯。
參考
- refman pt-online-schema-change
- RDS MySQL 如何使用 Percona Toolkit
- percona-toolkit 之 【pt-online-schema-change】說明
- Avoiding MySQL ALTER table downtime
- MySQL Online DDL和NoSQL Schemaless Design
原文鏈接地址:http://seanlook.com/2016/05/27/mysql-pt-online-schema-change/