批量插入回滾或插入失敗導致的MySQL表碎片


一般大家都知道,delete操作可以引起表碎片問題。但引起表碎片的並不僅僅只有delete操作。這里將演示一下由insert操作引起的表碎片。

 

在MySQL中,有兩種碎片:

1.表中的頁是完全空閑的

2.表中的頁沒有被完全填充,還有部分空閑空間

 

有三種情況下,insert操作會導致表碎片:

1.帶有rollback的insert

2.失敗了的insert操作

3.頁分裂導致的碎片

 

測試環境:Server version: 5.7.23-log MySQL Community Server (GPL)

表:frag、ins_frag、frag_page_spl

 

案例1:帶有rollback的insert

> create table ins_frag like frag;
Query OK, 0 rows affected (0.01 sec)

> begin;
Query OK, 0 rows affected (0.00 sec)

> insert into ins_frag select * from frag;
Query OK, 5964924 rows affected (2 min 23.04 sec)
Records: 5964924  Duplicates: 0  Warnings: 0

> 

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql  29K Nov  8 09:41 ins_frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:48 ins_frag.ibd

到此,已經執行了insert操作,但是還沒有提交或回滾insert操作。表占用了2.3G的磁盤空間。

現在,開始回滾上面的insert操作。

> select count(*) from ins_frag;
+----------+
| count(*) |
+----------+
|  5964924 |
+----------+
1 row in set (1.08 sec)

> rollback;
Query OK, 0 rows affected (2 min 27.15 sec)

> select count(*) from ins_frag;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

>

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql  29K Nov  8 09:41 ins_frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql 2.3G Nov  8 10:01 ins_frag.ibd

回滾結束后,表ins_frag仍然占用了2.3GB的磁盤空間。

> SELECT
    -> table_schema AS 'DATABASE',
    -> TABLE_NAME AS 'TABLE',
    -> CONCAT ( ROUND( ( data_length + index_length ) / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'TOTAL',
    -> CONCAT ( ROUND( data_free / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'DATAFREE' 
    -> FROM
    -> information_schema.TABLES 
    -> WHERE
    -> TABLE_NAME = 'ins_frag';
+----------+----------+-------+----------+
| DATABASE | TABLE    | TOTAL | DATAFREE |
+----------+----------+-------+----------+
| ysoap    | ins_frag | 2.18G | 2.23G    |
+----------+----------+-------+----------+
1 row in set (0.00 sec)

> 

說明insert回滾后,產生了碎片。
現在我們重構這個表,釋放空間。

> alter table ins_frag engine=innodb;
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

> 

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql  29K Nov  8 10:16 ins_frag.frm
-rw-r----- 1 mysql mysql 128K Nov  8 10:16 ins_frag.ibd

  

案例2:失敗的insert操作
在會話1中,開啟一個事務執行insert操作。但是會在會話2中kill掉會話1。

會話1:

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql  29K Nov  8 10:16 ins_frag.frm
-rw-r----- 1 mysql mysql 128K Nov  8 10:16 ins_frag.ibd

> begin;
Query OK, 0 rows affected (0.00 sec)

> insert into ins_frag select * from frag; #運行過程中

會話2將會話1殺掉:

> pager grep -i insert ; show processlist;
PAGER set to 'grep -i insert'
| 1603454 | root    | localhost          | ysoap | Query            |       5 | Sending data         | insert into ins_frag select * from frag |
16 rows in set (0.00 sec)

> kill 1603454;
Query OK, 0 rows affected (0.00 sec)

> 

  

回到會話1

> insert into ins_frag select * from frag; 
ERROR 2013 (HY000): Lost connection to MySQL server during query

> select count(*) from ins_frag;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

> 

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql  29K Nov  8 10:16 ins_frag.frm
-rw-r----- 1 mysql mysql 1.2G Nov  8 10:28 ins_frag.ibd

insert操作被中途kill了。表中沒有數據。但是,磁盤上還是占用了1.2GB的物理空間。

> SELECT
    -> table_schema AS 'DATABASE',
    -> TABLE_NAME AS 'TABLE',
    -> CONCAT ( ROUND( ( data_length + index_length ) / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'TOTAL',
    -> CONCAT ( ROUND( data_free / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'DATAFREE' 
    -> FROM
    -> information_schema.TABLES 
    -> WHERE
    -> TABLE_NAME = 'ins_frag';
+----------+----------+-------+----------+
| DATABASE | TABLE    | TOTAL | DATAFREE |
+----------+----------+-------+----------+
| ysoap    | ins_frag | 1.04G | 1.15G    |
+----------+----------+-------+----------+
1 row in set (0.01 sec)

> 

  

現在我們再次重構這個表,釋放空間。

> alter table ins_frag engine='innodb';
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

>

# ll -ltrh |grep frag
-rw-r----- 1 mysql mysql  29K Nov  8 09:38 frag.frm
-rw-r----- 1 mysql mysql 2.3G Nov  8 09:46 frag.ibd
-rw-r----- 1 mysql mysql  29K Nov  8 10:34 ins_frag.frm
-rw-r----- 1 mysql mysql 128K Nov  8 10:34 ins_frag.ibd

 

案例3:頁分裂導致的碎片
在內部,InnoDB記錄存儲在InnoDB頁中。默認情況下,每個頁大小是16K,但是可以選擇更改頁面大小。

如果InnoDB頁沒有足夠的空間容納新的記錄或索引條目,它將被分成兩個頁面,每個頁大約有50%的空間是滿的。這意味着,即使對於只有插入的工作負載,沒有回滾或刪除,最終也可能只有75%的平均頁面利用率——因此這種內部頁面碎片的損失為25%。

索引是通過排序構建的,如果表有很多插入到索引中的隨機位置,就會導致頁分割。

為了做一個實驗,我創建了一個具有排序索引(降序)的表:

> show create table frag_page_spl\G
*************************** 1. row ***************************
       Table: frag_page_spl
Create Table: CREATE TABLE `frag_page_spl` (
  `ID` varchar(64) NOT NULL,
  `TYPE` varchar(255) DEFAULT NULL,
  `TIME` datetime(3) NOT NULL,
  `USER_ID` varchar(255) DEFAULT NULL,
  `TASK_ID` varchar(64) DEFAULT NULL,
  `PINST_ID` varchar(64) DEFAULT NULL,
  `ACTION` varchar(255) DEFAULT NULL,
  `MESSAGE` varchar(4000) DEFAULT NULL,
  `FULL_MSG` longblob,
  PRIMARY KEY (`ID`),
  KEY `idx_pinstid` (`PINST_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

可以通過INFORMATION_SCHEMA.INNODB_METRICS監控表的頁分裂活動。不過需要開啟innodb monitor。

> set global innodb_monitor_enable=all;

創建6個並發線程,隨機插入數據,完成后查看:

> select name,count,type,status,comment from information_schema.innodb_metrics where name like '%index_page_spl%'\G
*************************** 1. row ***************************
name: index_page_splits
count: 52186
type: counter
status: enabled
comment: Number of index page splits
1 row in set (0.05 sec)

mysql> SELECT
-> table_schema as 'DATABASE',
-> table_name as 'TABLE',
-> CONCAT(ROUND(( data_length + index_length ) / ( 1024 * 1024 ), 2), 'M') 'TOTAL',
-> CONCAT(ROUND(data_free / ( 1024 * 1024 ), 2), 'M') 'DATAFREE'
-> FROM information_schema.TABLES
-> where table_name='frag_page_spl';
+----------+---------------+----------+----------+
| DATABASE | TABLE.        | TOTAL    | DATAFREE |
+----------+---------------+----------+----------+
| percona  | frag_page_spl | 2667.55M | 127.92M  |
+----------+---------------+----------+----------+
1 row in set (0.00 sec)

  

從innodb_metrics來看,我們可以看到頁面分裂計數器增加了。輸出顯示發生了52186個頁分割操作,創建了127.92 MB的碎片。

一旦創建了頁分裂,惟一的方法就是將創建的頁面降至合並閾值以下。當這種情況發生時,InnoDB通過合並操作將數據從分裂頁面中移出。MERGE_THRESHOLD對於表和特定的索引是可配置的。

另一種重新組織數據的方法是optimize表。這可能是一個代價昂貴和漫長的過程,但通常也處理太多頁面位於稀疏區域的情況中唯一方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM