mysql 5.6 原生Online DDL解析

作MySQL的都知道,數據庫操做裏面,DDL操做(好比CREATE,DROP,ALTER等)代價是很是高的,特別是在單表上千萬的狀況下,加個索引或改個列類型,就有可能堵塞整個表的讀寫。html

而後 mysql 5.6 開始,你們期待的Online DDL出現了,能夠實現修改表結構的同時,依然容許DML操做(select,insert,update,delete)。在這個特性出現之前,用的比較多的工具是pt-online-schema-change,比較請參考pt-online-schema-change使用說明、限制與比較ONLINE DDL VS PT-ONLINE-SCHEMA-CHANGEmysql

1. Online DDL

在 MySQL 5.1 (帶InnoDB Plugin)和5.5中,有個新特性叫 Fast Index Creation(下稱 FIC),就是在添加或者刪除二級索引的時候,能夠不用複製原表。對於以前的版本對於索引的添加刪除這類DDL操做,MySQL數據庫的操做過程爲以下:sql

  1. 首先新建Temp table,表結構是 ALTAR TABLE 新定義的結構數據庫

  2. 而後把原表中數據導入到這個Temp table緩存

  3. 刪除原表併發

  4. 最後把臨時表rename爲原來的表名app

爲了保持數據的一致性,中間複製數據(Copy Table)全程鎖表只讀,若是有寫請求進來將沒法提供服務,鏈接數爆張。less

引入FIC以後,建立二級索引時會對原表加上一個S鎖,建立過程不須要重建表(no-rebuild);刪除InnoDB二級索引只須要更新內部視圖,並標記這個索引的空間可用,去掉數據庫元數據上該索引的定義便可。這個過程也只容許讀操做,不能寫入,但大大加快了修改索引的速度(不含主鍵索引,InnoDB IOT的特性決定了修改主鍵依然須要 Copy Table )。函數

FIC只對索引的建立刪除有效,MySQL 5.6 Online DDL把這種特性擴展到了添加列、刪除列、修改列類型、列重命名、設置默認值等等,實際效果要看所使用的選項和操做類別來定。工具

1.1 Online DDL選項

MySQL 在線DDL分爲 INPLACECOPY 兩種方式,經過在ALTER語句的ALGORITHM參數指定。

  • ALGORITHM=INPLACE,能夠避免重建錶帶來的IO和CPU消耗,保證ddl期間依然有良好的性能和併發。

  • ALGORITHM=COPY,須要拷貝原始表,因此不容許併發DML寫操做,可讀。這種copy方式的效率仍是不如 inplace ,由於前者須要記錄undo和redo log,並且由於臨時佔用buffer pool引發短期內性能受影響。

上面只是 Online DDL 內部的實現方式,此外還有 LOCK 選項控制是否鎖表,根據不一樣的DDL操做類型有不一樣的表現:默認mysql儘量不去鎖表,可是像修改主鍵這樣的昂貴操做不得不選擇鎖表。

  • LOCK=NONE,即DDL期間容許併發讀寫涉及的表,好比爲了保證 ALTER TABLE 時不影響用戶註冊或支付,能夠明確指定,好處是若是不幸該 alter語句不支持對該表的繼續寫入,則會提示失敗,而不會直接發到庫上執行。ALGORITHM=COPY默認LOCK級別

  • LOCK=SHARED,即DDL期間表上的寫操做會被阻塞,但不影響讀取。

  • LOCK=DEFAULT,讓mysql本身去判斷lock的模式,原則是mysql儘量不去鎖表

  • LOCK=EXCLUSIVE,即DDL期間該表不可用,堵塞任何讀寫請求。若是你想alter操做在最短的時間內完成,或者表短期內不可用能接受,能夠手動指定。

可是有一點須要說明,不管任何模式下,online ddl開始以前都須要一個短期排它鎖(exclusive)來準備環境,因此alter命令發出後,會首先等待該表上的其它操做完成,在alter命令以後的請求會出現等待waiting meta data lock。一樣在ddl結束以前,也要等待alter期間全部的事務完成,也會堵塞一小段時間。因此儘可能在ALTER TABLE以前確保沒有大事務在執行,不然同樣出現連環鎖表。

1.2 考慮不一樣的DDL操做類別

從上面的介紹能夠看出,不是5.6支持在線ddl就能夠爲所欲爲的alter table,鎖不鎖表要看狀況:

<!-- more -->

提示:下表根據官方 Summary of Online Status for DDL Operations 整理挑選的經常使用操做。

  • In-Place爲Yes是優選項,說明該操做支持INPLACE

  • Copies Table爲No是優選項,由於爲Yes須要重建表。大部分狀況與In-Place是相反的

  • Allows Concurrent DML?爲Yes是優選項,說明ddl期間表依然可讀寫,能夠指定 LOCK=NONE(若是操做容許的話mysql自動就是NONE)

  • Allows Concurrent Query?默認全部DDL操做期間都容許查詢請求,放在這只是便於參考

  • Notes會對前面幾列Yes/No帶*號的限制說明

Operation In-Place? Copies Table? Allows Concurrent DML? Allows Concurrent Query? Notes
添加索引 Yes* No* Yes Yes 對全文索引的一些限制
刪除索引 Yes No Yes Yes 僅修改表的元數據
OPTIMIZE TABLE Yes Yes Yes Yes 從 5.6.17開始使用ALGORITHM=INPLACE,固然若是指定了old_alter_table=1或mysqld啓動帶--skip-new則將仍是COPY模式。若是表上有全文索引只支持COPY
對一列設置默認值 Yes No Yes Yes 僅修改表的元數據
對一列修改auto-increment 的值 Yes No Yes Yes 僅修改表的元數據
添加 foreign key constraint Yes* No* Yes Yes 爲了不拷貝表,在約束建立時會禁用foreign_key_checks
刪除 foreign key constraint Yes No Yes Yes foreign_key_checks 不影響
改變列名 Yes* No* Yes* Yes 爲了容許DML併發, 若是保持相同數據類型,僅改變列名
添加列 Yes* Yes* Yes* Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做。當添加列是auto-increment,不容許DML併發
刪除列 Yes Yes* Yes Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做
修改列數據類型 No Yes* No Yes 修改類型或添加長度,都會拷貝表,並且不容許更新操做
更改列順序 Yes Yes Yes Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做
修改ROW_FORMAT <br/> 和KEY_BLOCK_SIZE Yes Yes Yes Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做
設置列屬性NULL<br/>或NOT NULL Yes Yes Yes Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做
添加主鍵 Yes* Yes Yes Yes 儘管容許 ALGORITHM=INPLACE ,但數據大幅重組,因此它仍然是一項昂貴的操做。<br/> 若是列定義必須轉化NOT NULL,則不容許INPLACE
刪除並添加主鍵 Yes Yes Yes Yes 在同一個 ALTER TABLE 語句刪除就主鍵、添加新主鍵時,才容許inplace;數據大幅重組,因此它仍然是一項昂貴的操做。
刪除主鍵 No Yes No Yes 不容許併發DML,要拷貝表,並且若是沒有在同一 ATLER TABLE 語句裏同時添加主鍵則會收到限制
變動表字符集 No Yes No Yes 若是新的字符集編碼不一樣,重建表

從表看出,In-Place爲No,DML必定是No,說明ALGORITHM=COPY必定會發生拷貝表,只讀。但ALGORITHM=INPLACEE也要可能發生拷貝表,但能夠併發DML:

  • 添加、刪除列,改變列順序

  • 添加或刪除主鍵

  • 改變行格式ROW_FORMAT和壓縮塊大小KEY_BLOCK_SIZE

  • 改變列NULL或NOT NULL

  • 優化表OPTIMIZE TABLE

  • 強制 rebuild 該表

不容許併發DML的狀況有:修改列數據類型、刪除主鍵、變動表字符集,即這些類型操做ddl是不能online的。

另外,更改主鍵索引與普通索引處理方式是不同的,主鍵即彙集索引,體現了表數據在物理磁盤上的排列,包含了數據行自己,須要拷貝表;而普通索引經過包含主鍵列來定位數據,因此普通索引的建立只須要一次掃描主鍵便可,並且是在已有數據的表上創建二級索引,更緊湊,未來查詢效率更高。

修改主鍵也就意味着要重建全部的普通索引。刪除二級索引更簡單,修改InnoDB系統表信息和數據字典,標記該因此不存在,標記所佔用的表空間能夠被新索引或數據行從新利用。

1.3 在線DDL的限制

  • 在alter table時,若是涉及到table copy操做,要確保datadir目錄有足夠的磁盤空間,可以放的下整張表,由於拷貝表的的操做是直接在數據目錄下進行的。

  • 添加索引無需table copy,但要確保tmpdir目錄足夠存下索引一列的數據(若是是組合索引,當前臨時排序文件一合併到原表上就會刪除)

  • 在主從環境下,主庫執行alter命令在完成以前是不會進入binlog記錄事件,若是容許dml操做則不影響記錄時間,因此期間不會致使延遲。然而,因爲從庫是單個SQL Thread按順序應用relay log,輪到ALTER語句時直到執行完才能下一條,因此從庫會在master ddl完成後開始產生延遲。(pt-osc能夠控制延遲時間,因此這種場景下它更合適)

  • During each online DDL ALTER TABLE statement, regardless of the LOCK clause, there are brief periods at the beginning and end requiring an exclusive lock on the table (the same kind of lock specified by the LOCK=EXCLUSIVE clause). Thus, an online DDL operation might wait before starting if there is a long-running transaction performing inserts, updates, deletes, or SELECT ... FOR UPDATE on that table; and an online DDL operation might wait before finishing if a similar long-running transaction was started while the ALTER TABLE was in progress.

  • 在執行一個容許併發DML在線 ALTER TABLE時,結束以前這個線程會應用 online log 記錄的增量修改,而這些修改是其它thread裏產生的,因此有可能會遇到重複鍵值錯誤(ERROR 1062 (23000): Duplicate entry)

  • 涉及到table copy時,目前尚未機制限制暫停ddl,或者限制IO閥值
    在MySQL 5.7.6開始可以經過 performance_schema 觀察alter table的進度

  • 通常來講,建議把多個alter語句合併在一塊兒進行,避免屢次table rebuild帶來的消耗。可是也要注意分組,好比須要copy table和只需inplace就能完成的,應該分兩個alter語句。

  • 若是DDL執行時間很長,期間又產生了大量的dml操做,以致於超過了innodb_online_alter_log_max_size變量所指定的大小,會引發DB_ONLINE_LOG_TOO_BIG 錯誤。默認爲 128M,特別對於須要拷貝大表的alter操做,考慮臨時加大該值,以此得到更大的日誌緩存空間

  • 執行完 ALTER TABLE 以後,最好 ANALYZE TABLE tb1 去更新索引統計信息

2. 實現過程

online ddl主要包括3個階段,prepare階段,ddl執行階段,commit階段,rebuild方式比no-rebuild方式實質多了一個ddl執行階段,prepare階段和commit階段相似。下面將主要介紹ddl執行過程當中三個階段的流程。

  • Prepare階段:

    1. 建立新的臨時frm文件(與InnoDB無關)

    2. 持有EXCLUSIVE-MDL鎖,禁止讀寫

    3. 根據alter類型,肯定執行方式(copy,online-rebuild,online-norebuild)
      假如是Add Index,則選擇online-norebuild即INPLACE方式

    4. 更新數據字典的內存對象

    5. 分配row_log對象記錄增量(僅rebuild類型須要)

    6. 生成新的臨時ibd文件(僅rebuild類型須要)

  • ddl執行階段:

    1. 降級EXCLUSIVE-MDL鎖,容許讀寫

    2. 掃描old_table的彙集索引每一條記錄rec

    3. 遍歷新表的彙集索引和二級索引,逐一處理

    4. 根據rec構造對應的索引項

    5. 將構造索引項插入sort_buffer塊排序

    6. 將sort_buffer塊更新到新的索引上

    7. 記錄ddl執行過程當中產生的增量(僅rebuild類型須要)

    8. 重放row_log中的操做到新索引上(no-rebuild數據是在原表上更新的)

    9. 重放row_log間產生dml操做append到row_log最後一個Block

  • commit階段:

    1. 當前Block爲row_log最後一個時,禁止讀寫,升級到EXCLUSIVE-MDL鎖

    2. 重作row_log中最後一部分增量

    3. 更新innodb的數據字典表

    4. 提交事務(刷事務的redo日誌)

    5. 修改統計信息

    6. rename臨時idb文件,frm文件

    7. 變動完成

這有一直導圖挺直觀的:http://blog.itpub.net/22664653/viewspace-2056953
添加列 時因爲須要copy table,row_log會重放到新表上(臨時ibd文件),直到最後一個block,鎖住原表禁止更新。

row_log記錄了ddl變動過程當中新產生的dml操做,並在ddl執行的最後將其應用到新的表中,保證數據完整性

3. 對比實驗

3.1 添加二級索引

我這裏使用sysbench產生的表測試(500w數據):

mysql> select version();
+------------+
| version()  |
+------------+
| 5.6.30-log |
+------------+
1 row in set (0.00 sec)

mysql> show create table sbtest1;
CREATE TABLE `sbtest1` (
  `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_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=5000001 DEFAULT CHARSET=utf8 COLLATE=utf8_bin MAX_ROWS=1000000


mysql> show variables like "old_alter_table";
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| old_alter_table | OFF   |
+-----------------+-------+
1 row in set (0.00 sec)

舊模式下,建立刪除普通索引:

**SESSION1:**
mysql> set old_alter_table=1;
Query OK, 0 rows affected (0.00 sec)

mysql> alter table sbtest1 drop index idx_k_1;
Query OK, 5000000 rows affected (44.79 sec)
Records: 5000000  Duplicates: 0  Warnings: 0

mysql> alter table sbtest1 add index idx_k_1(k);
Query OK, 5000000 rows affected (1 min 11.29 sec)
Records: 5000000  Duplicates: 0  Warnings: 0


**SESSION2:**
mysql> select * from sbtest1 limit 1;
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| id | k       | c                                                                                                                       | pad                                                         |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
|  1 | 2481886 | 08566691963-88624...106334-50535565977 | 63188288836-9235114...351-49282961843 |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> update sbtest1 set k=2481885 where id=1;
Query OK, 1 row affected (45.16 sec)
Rows matched: 1  Changed: 1  Warnings: 0


**SESSION3:**
mysql> show processlist;
+--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+
| Id     | User            | Host      | db         | Command | Time   | State                           | Info                                    |
+--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+
| 118652 | root            | localhost | confluence | Query   |     19 | copy to tmp table               | alter table sbtest1 add index k_1(k)    |
| 118666 | root            | localhost | confluence | Query   |      3 | Waiting for table metadata lock | update sbtest1 set k=2481885 where id=1 |
| 118847 | root            | localhost | NULL       | Query   |      0 | init                            | show processlist                        |
+--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+
4 rows in set (0.00 sec)

同時在datadir目錄下能夠看到
-rw-rw---- 1 mysql mysql 8.5K May 23 21:24 sbtest1.frm
-rw-rw---- 1 mysql mysql 1.2G May 23 21:24 sbtest1.ibd
-rw-rw---- 1 mysql mysql 8.5K May 23 20:48 #sql-1c6a_1cf7c.frm
-rw-rw---- 1 mysql mysql 638M May 23 20:48 #sql-1c6a_1cf7c.ibd

傳統ddl方式有 copy to tmp table 過程,dml更新操做期間被堵住45s:Waiting for table metadata lock

下面改爲Online DDL方式

**SESSION1**
mysql> set old_alter_table=0;

mysql> alter table sbtest1 drop index k_1;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0
索引秒刪

mysql> alter table sbtest1 add index k_1(k);
Query OK, 0 rows affected (13.99 sec)
Records: 0  Duplicates: 0  Warnings: 0

**SESSION2**
mysql> update sbtest1 set k=2481887 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0


**SESSION3**
mysql> show processlist;
+--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+
| Id     | User            | Host      | db         | Command | Time   | State                  | Info                                 |
+--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+
| 118652 | root            | localhost | confluence | Query   |     10 | altering table         | alter table sbtest1 add index k_1(k) |
| 118666 | root            | localhost | confluence | Sleep   |      9 |                        | NULL                                 |
| 118847 | root            | localhost | NULL       | Query   |      0 | init                   | show processlist                     |
+--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+
4 rows in set (0.00 sec)

添加普通索引,並未出現阻塞update操做,並且速度更快。從 rows affected 能夠看出有沒有copy table。

但若是在alter以前有大事務在執行,會阻塞ddl以及後續的全部請求:

**SESSION1**
mysql> select * from sbtest1 where c='long select before alter';
Empty set (4.36 sec)

**SESSION2**
mysql> alter table sbtest1 add index k_1(k);
Query OK, 0 rows affected (16.28 sec)
Records: 0  Duplicates: 0  Warnings: 0

**SESSION3**
mysql> select * from sbtest1 where c='long select after alter execution but not complete';
Empty set (5.89 sec)

**SESSION4**
mysql> show processlist;
+----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+
| Id | User            | Host      | db         | Command | Time | State                           | Info                                                                               |
+----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+
|  5 | root            | localhost | confluence | Query   |    3 | Sending data                    | select * from sbtest1 where c='long select before alter'                           |
|  7 | root            | localhost | NULL       | Query   |    0 | init                            | show processlist                                                                   |
| 13 | root            | localhost | confluence | Query   |    2 | Waiting for table metadata lock | alter table sbtest1 add index k_1(k)                                               |
| 14 | root            | localhost | confluence | Query   |    1 | Waiting for table metadata lock | select * from sbtest1 where c='long select after alter execution but not complete' |
+----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+
5 rows in set (0.00 sec)

3.2 添加列示例

添加新列是ddl操做裏面相對較多的一類操做。從上文表中能夠看到

**SESSION1**
mysql> ALTER TABLE `sbtest2` \
       ADD COLUMN `f_new_col1` int(11) NULL DEFAULT 0, \
       ADD COLUMN `f_new_col2` varchar(32) NULL DEFAULT '' AFTER `f_new_col1`;
Query OK, 0 rows affected (1 min 57.86 sec)
Records: 0  Duplicates: 0  Warnings: 0

**SESSION2**
mysql> update sbtest2 set c="update when add colomun ddl start" where c='33333';
Query OK, 0 rows affected (4.41 sec)
Rows matched: 0  Changed: 0  Warnings: 0

**SESSION3**
mysql> select * from sbtest2 where c='select when add colomun ddl start';
Empty set (3.44 sec)

**SESSION4**
mysql> show processlist;
+-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+
| Id  | User            | Host      | db         | Command | Time | State                     | Info                                                                                                 |
+-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+
|   5 | root            | localhost | confluence | Query   |    4 | altering table            | ALTER TABLE `sbtest2`  ADD COLUMN `f_new_col1` int(11) NULL DEFAULT 0, ADD COLUMN `f_new_col2` varch |
|   7 | root            | localhost | NULL       | Query   |    0 | init                      | show processlist                                                                                     |
| 161 | root            | localhost | confluence | Query   |    2 | Searching rows for update | update sbtest2 set c="update when add colomun ddl start" where c='33333'                             |
| 187 | root            | localhost | confluence | Query   |    1 | Sending data              | select * from sbtest2 where c='select when add colomun ddl start'                                    |
+-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+
5 rows in set (0.00 sec)

看到,默認不加 ALGORITHM=INPLACE 就已經容許ddl期間併發DML操做。可是會有一個小臨時文件產生:

-rw-rw---- 1 mysql mysql 8.6K May 23 21:42 #sql-7055_5.frm
-rw-rw---- 1 mysql mysql 112K May 23 21:42 #sql-ib21-16847116.ibd

當指定copy時,就會鎖表了(通常你不想這樣作):

ALTER TABLE `sbtest2` 
    DROIP COLUMN `f_new_col1`, algorithm=copy;

3.3 修改字段類型

修改列類型與添加新列不同,修改類型須要rebuild整個表:
(select ok, update waiting)

**SESSION1**
mysql> ALTER TABLE sbtest2 
       CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT '', algorithm=inplace ;
ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY.
不支持INPLACE

mysql> ALTER TABLE sbtest2 
       CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT '';

**SESSION2**
mysql> update sbtest2 set c="update when add colomun ddl start" where c='33333';

mysql> select * from sbtest2 where c='select when add colomun ddl start';
Empty set (3.79 sec)

mysql> show processlist;
+-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+
| Id  | User            | Host      | db         | Command | Time | State                           | Info                                                                             |
+-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+
|   5 | root            | localhost | confluence | Query   |    5 | copy to tmp table               | ALTER TABLE sbtest2 
   CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT '' |
|   7 | root            | localhost | NULL       | Query   |    0 | init                            | show processlist                                                                 |
| 161 | root            | localhost | confluence | Query   |    4 | Waiting for table metadata lock | update sbtest2 set c="update when add colomun ddl start" where c='33333'         |
| 187 | root            | localhost | confluence | Query   |    3 | Sending data                    | select * from sbtest2 where c='select when add colomun ddl start'                |
+-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+
5 rows in set (0.00 sec)

3.4 Waiting for table metadata lock

Online DDL看起來很美好,實驗測試也正如預期,但幾回在生產環境修改索引時(5000w的表),仍是沒法避免出現大量 Waiting for table metadata lock 鎖等待,線程數持續增長並告警,致使長達十多分鐘不可寫。後來發現原來是版本升級的問題致使的,見[這裏]()。關於metadata lock介紹參考[這篇文章]。

4. 參考


原文連接地址:http://seanlook.com/2016/05/24/mysql-online-ddl-concept/

相關文章
相關標籤/搜索