領導讓我
SQL
優化,我直接把服務干掛了...
前言
MySQL
大表加字段或者加索引,是有一定風險的。
大公司一般有DBA
,會幫助開發解決這個痛點,可是DBA
是怎么做的呢?
小公司沒有DBA
,作為開發我們的責任就更大了。那么我們怎么才能安全的加個索引呢?
今天,我們通過模擬案例以及原理分析,去弄清楚MySQL
中DDL
的風險,以及如何避免事故發生。
准備
軟件以及項目
- 安裝本地版本MySQL。
- 一個簡單的增刪改查項目。
- 使用JMeter進行並發請求測試。
創建表
# 如果存在user表則刪除
DROP TABLE IF EXISTS user;
# 創建user表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`name` varchar(10) DEFAULT NULL COMMENT '姓名',
`age` int(2) DEFAULT NULL COMMENT '年齡',
`address` varchar(30) DEFAULT NULL COMMENT '地址',
`description` varchar(100) DEFAULT NULL COMMENT '描述',
`test_id` bigint DEFAULT NULL COMMENT '測試 id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '創建時間',
`modify_time` timestamp NULL DEFAULT NULL COMMENT '修改時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='mysql ddl測試表';
創建存儲過程
# 如果存在test存儲過程則刪除
DROP PROCEDURE IF EXISTS `test`;
# 創建無參存儲過程,名稱為test
CREATE PROCEDURE test()
BEGIN
# 聲明變量
DECLARE i INT;
# 變量賦值
SET i = 0;
# 結束循環的條件: 當i等於100萬時跳出while循環
WHILE i < 1000000 DO
# 往t_test表添加數據
INSERT INTO `test`.user (`name`, `age`, `address`,
`description`, `test_id`, `create_time`, `modify_time`)
VALUES ('iisheng', 26, '北京', '如逆水行舟', LAST_INSERT_ID() + 1,
'2020-05-17 16:01:44', '2020-05-17 16:01:51');
# 循環一次, i加1
SET i = i + 1;
# 結束while循環
END WHILE;
END
下面的創建存儲過程語句,是在
IDE
內選擇代碼塊執行的,如果在Terminal
中執行,需要使用DELIMITER
關鍵字,更改語句結束標志。
調用存儲過程,生成百萬數據
CALL test();
開啟慢SQL日志
# 查看MySQL是否開啟慢日志記錄
SHOW VARIABLES LIKE 'slow_query_log';
# 開啟慢SQL日志記錄
SET GLOBAL slow_query_log = 'ON';
# 查看慢SQL日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
# 查看執行多久的SQL才算慢SQL
SHOW VARIABLES LIKE 'long_query_time';
# 設置慢SQL執行時間 只有新session才生效
SET GLOBAL long_query_time = 1;
通常情況下這些會在MySQL的配置文件中配置,啟動時生效。
幾個有用的SQL語句
# 展示哪些線程正在運行
SHOW PROCESSLIST;
# 查看正在執行的事務
SELECT * FROM information_schema.INNODB_TRX;
# 查看正在鎖的事務
SELECT * FROM information_schema.INNODB_LOCKS;
# 查看正在等待鎖的事務
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
# 顯示innodb存儲引擎狀態的大量信息,包含死鎖日志
SHOW ENGINE INNODB STATUS ;
# 展示數據庫最大連接數的配置
SHOW VARIABLES LIKE 'max_connections';
# 查看存在哪些觸發器
SELECT * FROM information_schema.TRIGGERS;
# 查看MySQL版本
SELECT VERSION();
后面我們會主要用前兩條。
事故現場
說明
- 我創建的
user
表除了主鍵是沒有其他索引的。 - 測試的
user
表數據量為一百萬。 - 測試
MySQL
版本為5.7.28
。 - 測試項目的邏輯:隨機get()、list()、update()、create(),每個操作都開啟事務,並且休眠500毫秒。
步驟
運行測試項目
這里我們可以看到,項目已經正常啟動了。
postman
調用一下接口
這里我們隨便測試一個接口,請求時間2秒左右。
執行JMeter的Test Plan,觀察項目日志
這里我們創建了四個線程組,每個線程組調用一個我們的接口。模擬10個人循環1000次的訪問。
這里我們看到該請求頻率下,日志無異常。
慢SQL日志
這里我們看到,百萬級的SQL,如果沒加索引SQL執行時間還是比較長的,有的已經達到了2s。
加個索引,再觀察項目日志
這里我們看到,項目已經開始報錯了,大量的Connection is not available, request timed out after 30001ms
。
SHOW PROCESSLIST
一下
這里我們看到,有大量的Waiting for table metadata lock
。
postman
再次調用一下接口
這個時候,調用接口已經報錯了,響應時間也比較久。此時,服務對用戶來說,已經基本不可用了。
為什么會這樣?
我就想加個索引,怎么就這么難?
看吧,就因為我加了個索引,服務就掛了,我沒加之前還是好好的。遇到問題,我們要冷靜,不是我們的鍋堅決不能背,真的是我們的問題,下次一定要記得改正。那么,此刻的服務為什么就不可用了呢?
首先我們要知道,在InnoDB
事務中,鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。
然后,在MySQL5.5
版本中引入了MDL(Metadata Lock)
,當對一個表做增刪改查操作的時候,加MDL
讀鎖;當要對表做結構變更操作的時候,加MDL
寫鎖。
我們可以簡單的嘗試一下下面的情況。
Session A
開啟一個事務,執行了一個簡單的查詢語句。此時,Session B
,執行另一個查詢語句,可以成功。接着,Session C
執行了一個DDL
操作,加了個字段,因為Session A
的事務沒有提交,而且Session A
持有MDL
讀鎖,Session C
獲取不到MDL
寫鎖,所以Session C
堵塞等待MDL
寫鎖。又由於MDL
寫鎖獲取優先級高於MDL
讀鎖,因此Session D
這個時候也獲取不到MDL
讀鎖,等待Session C
獲取到MDL
寫鎖之后它才能獲取到MDL
讀鎖。
我們發現,DDL操作之前如果存在長事務,一直不提交,DDL操作就會一直被堵塞,還會間接的影響后面其他的查詢,導致所有的查詢都被堵塞。
這也就是為什么我們把服務干掛的原因了。
目前主流解決方案
針對上面出現的情況,我們怎么解決呢?
MySQL5.6的Online DDL
MySQL
從5.6
開始,支持Online DDL
。類似於這種的語句ALTER TABLE user ADD INDEX idx_test_id (test_id), ALGORITHM=INPLACE, LOCK=NONE
在普通的ALTER TABLE
或者CREATE INDEX
語句后面添加ALGORITHM
參數和LOCK
參數。
實際上,
ALTERT TABLE
語句如果不加ALGORITHM
參數,默認就會選擇ALGORITHM=INPLACE
的形式,如果執行的語句支持INPLACE
,否則,會使用ALGORITHM=COPY
。
以前寫SQL
只會ALTER TABLE
不知道后面還可以加ALGORITHM
參數,后來知道了Online DDL
,知道了可以加ALGORITHM=INPLACE
,結果兩種寫法有的時候是一樣的...
這里順便提一句,學習的途徑有很多,但是官網,的確可以多看看。
使用pt-online-schema-change
簡單說一下怎么安裝這個東西
首先官網下載,然后校驗以及安裝,執行下面命令
perl Makefile.PL
make
make install
然后使用CPAN
安裝相關依賴(適用Unix
),CentOS
下直接yum
更簡單
perl -MCPAN -e shell
cpan> install DBI
cpan> install DBD::mysql
我自己Mac安裝沒啥問題,公司Mac安裝失敗了,然后升級了一下Perl版本就可以了。
語法
pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=user --password=pass --host=host_addr P=3306,D=database,t=table --alter "ADD INDEX idx_name(field_name)" --execute
我的腳本添加索引
pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=root --password=mGy6GAzdawFPTJ7R --host=127.0.0.1 P=3306,D=test,t=user --alter "add INDEX idx_test_id(test_id)" --execute
使用pt-osc
測試
這里我們看到,pt-osc
創建觸發器的時候卡在那了。實際上這里也是在等待鎖。
最終成功了,但是整個過程時間比較久。過程中我們也發現了一些死鎖的日志。
其實,這個跟我的代碼有一定的關系,我的測試代碼隨機數生成的范圍是[0, 20000]
,然后我根據生成的隨機數,去查詢數據庫,鎖的沖突會比較多。把范圍修改為[0, 1000000]
會好很多。
再看Online DDL
因為剛才我們發現了,自己代碼寫的有一些問題,所以我們剛才的結論也有一些影響。我們把隨機數的范圍改到100萬,重新測試一遍。
這次Online DDL
也成功了。但是也是有一些連接超時的日志。之前的測試如果一直執行下去,也會成功,只不過堵塞時間太長,對用戶影響太大,我就停止算執行失敗了。
實際效果跟機器性能也是有一些關系的,這里的關鍵點在於拿
MDL
寫鎖的等待時間,這個時間稍微久一些就會對用戶造成很大的影響。
pt-osc
執行過程
- 創建一個和原表表結構一樣的臨時表(
_tablename_new
),執行alter
修改臨時表表結構。 - 在原表上創建3個與
insert
delete
update
對應的觸發器,用於copy
數據的過程中,在原表的更新操作,更新到新表。 - 從原表拷貝數據到臨時表,拷貝過程中在原表進行的寫操作都會更新到新建的臨時表。
rename
原數據表為old
表,把新表rename
為原表名,並將old
表刪除。- 刪除觸發器。
這里面創建、刪除觸發器和rename
表的時候都會嘗試獲取DML
寫鎖,如果獲取不到會等待。就是我們看到的Waiting for table metadata lock
。
所以,這些時間段如果長時間獲取不到鎖,就會一直堵塞,還是會出現問題的。
Online DDL
執行過程
- 拿
MDL
寫鎖 - 降級成
MDL
讀鎖 - 真正做
DDL
- 升級成
MDL
寫鎖 - 釋放
MDL
鎖
1、4
如果沒有鎖沖突,執行時間非常短。第3步占用了DDL
絕大部分時間,這期間這個表可以正常讀寫數據,因此稱為online。
但是,如果拿鎖的時候沒拿到,或者升級MDL
寫鎖不能成功,就會等待,我們又會看到Waiting for table metadata lock
,然后就接着的一系列問題了。
總結
加個索引,說難也難,說不難也不難。如果數據量大,又存在長事務,加索引的過程又有用戶訪問,Online DDL
和pt-osc
都不能保證對業務沒有影響。但是如果我們SQL
的執行時間比較短,或者我們加索引的時候,對應的業務沒有多少請求。那么我們就可以很快的加完索引。
加字段也是類似的過程,但是如果我們能保證沒有慢SQL
,那么就不會存在長事務,那么執行時間就會很快,對用戶就可以做到幾乎沒有影響。至於選擇Online DDL
還是pt-osc
就要看他們的一些限制以及自己的場景需求了。感興趣的同學,自己嘗試一下。
最后想說
當萬丈高樓崩塌的時候,超人也不能將它復原。我們應該做的,是有一個好的規范,好的認知,好的監控,在問題沒有出現的時候,就將問題扼殺在搖籃中。而不是讓問題,日漸壯大,大到覆水難收...
參考文獻:
[1]:《MySQL實戰45講》
[2]: https://dev.mysql.com/doc/refman/5.7/en/
[3]: https://www.percona.com/doc/percona-toolkit/3.0/pt-online-schema-change.html
歡迎關注個人微信公眾號【如逆水行舟】,用心輸出基礎、算法、源碼系列文章。