基于MySQL內(nèi)核的SQL限流設(shè)計(jì)與實(shí)現(xiàn)
一、引言
編寫目的
本文詳細(xì)描述了SQL限流特性的需求設(shè)計(jì)方案以及使用方式,開發(fā)、測(cè)試人員可根據(jù)本文實(shí)現(xiàn)功能的開發(fā)、測(cè)試,DBA可根據(jù)本文合理使用SQL限流功能。
需求概述
生產(chǎn)環(huán)境中可能出現(xiàn)由于業(yè)務(wù)量增長(zhǎng)過快或者慢SQL等原因?qū)е翪PU使用率打滿,當(dāng)CPU打滿時(shí),會(huì)影響所有依賴該數(shù)據(jù)庫(kù)實(shí)例的業(yè)務(wù),因此需要有手段限制影響CPU的業(yè)務(wù)執(zhí)行,保證其他核心業(yè)務(wù)不受影響。
SQL限流的目標(biāo)是在出現(xiàn)CPU使用率打滿影響所有業(yè)務(wù)時(shí),通過限制非核心業(yè)務(wù)或者慢SQL的業(yè)務(wù),實(shí)現(xiàn)核心業(yè)務(wù)的快速恢復(fù)。
軟件結(jié)構(gòu)
MySQL軟件結(jié)構(gòu)如下圖所示:
圖片
參考資料
MySQL官網(wǎng):https://dev.mysql.com/doc/refman/5.7/en/
二、概要設(shè)計(jì)
架構(gòu)設(shè)計(jì)
圖片
總體架構(gòu)如圖所示,SQL限流的主要功能在執(zhí)行器工作階段通過限流規(guī)則與查詢串的匹配來實(shí)現(xiàn)。
流程圖
圖片
三、詳細(xì)設(shè)計(jì)
功能設(shè)計(jì)
對(duì)外接口
針對(duì)SQL限流功能設(shè)計(jì)對(duì)外提供管理命令,如下:
- 開關(guān)控制命令(du_flow_control)
圖片
用于控制限流功能是否啟用。
- 大小寫敏感命令(du_flow_control_case_sensitive)
圖片
用于控制限流規(guī)則匹配時(shí)是否大小寫敏感。
- 預(yù)留用戶命令(du_flow_control_reserve_user)
圖片
限流功能對(duì)于預(yù)留用戶不生效。
預(yù)留用戶參數(shù)以字符串的形式接受輸入,如果存在多個(gè)預(yù)留用戶,使用 ',' 進(jìn)行分隔。
在服務(wù)啟動(dòng)時(shí),需格式化該參數(shù),后續(xù)在做限流判斷時(shí),需要根據(jù)格式化之后參數(shù)識(shí)別是否是預(yù)留用戶,如果是預(yù)留用戶,則不進(jìn)行限流操作,無需進(jìn)行后續(xù)的限流規(guī)則匹配。
預(yù)留用戶參數(shù)接受NULL和空串 '',當(dāng)該參數(shù)為NULL或空串時(shí),表明所有用戶都不是預(yù)留用戶。
對(duì)于預(yù)留用戶字符串的處理,與MySQL社區(qū)對(duì)于用戶名的處理邏輯保持一致,即忽略每個(gè)用戶名前后的無意義字符(如空格、換行等),保留用戶名之間的無意義字符。
- 分隔符設(shè)置命令(du_flow_control_delimiter)
圖片
用于控制限流規(guī)則的分隔符。
分隔符不可為空,且長(zhǎng)度小于等于1024。
修改分隔符之后,需要重新加載限流規(guī)則,對(duì)限流規(guī)則進(jìn)行解析,會(huì)消耗系統(tǒng)資源,因此不建議在系統(tǒng)負(fù)載過高時(shí)修改分隔符。
對(duì)于預(yù)留用戶的處理,核心功能如下:
// 更新
static bool update_delimiter(sys_var *self, THD *thd, enum_var_type type)
{
reload_rules(thd);
}
// 校驗(yàn)
static bool check_delimiter(sys_var *self, THD *thd, set_var *var)
{
judge(var->value);
judge(str);
judge(length);
}
功能模塊詳細(xì)設(shè)計(jì)
- 規(guī)則管理
SQL限流使用的場(chǎng)景為CPU負(fù)載過高,為了防止加重系統(tǒng)的負(fù)擔(dān),限流規(guī)則應(yīng)該事先加載到內(nèi)存中?;诖耍枰獙?duì)內(nèi)存中的限流規(guī)則進(jìn)行管理,涉及的操作有:規(guī)則的讀取、移除、解析。
讀取
規(guī)則讀取的場(chǎng)景有:
- 主動(dòng)執(zhí)行自定義讀取命令,用于更新限流規(guī)則到內(nèi)存。每當(dāng)添加限流規(guī)則后,需手動(dòng)執(zhí)行該操作,更新限流規(guī)則到內(nèi)存。
- 修改分隔符時(shí)。修改分隔符后,限流規(guī)則需要重新解析,因此也需要重新讀取。
- 數(shù)據(jù)庫(kù)實(shí)例啟動(dòng)時(shí)。基于性能考慮,在實(shí)例啟動(dòng)時(shí),將限流規(guī)則加載到內(nèi)存中。
移除
規(guī)則移除的場(chǎng)景有:
- 系統(tǒng)停止時(shí)移除限流規(guī)則時(shí)。
- 手動(dòng)刪除限流規(guī)則時(shí)。
解析
用戶輸入的規(guī)則在物理表中的表現(xiàn)是一個(gè)字符串,在將其加載到內(nèi)存時(shí),需要根據(jù)限流規(guī)則的分隔符將其解析為對(duì)應(yīng)的規(guī)則模式串,在模式串中包含的關(guān)鍵字全部被滿足時(shí),需要進(jìn)行限流。解析過程如下:
- 將物理表中的限流規(guī)則字符串讀取到內(nèi)存字符串中。
- 根據(jù)分隔符將字符串解析為關(guān)鍵字組成的模式串鏈表。
- 流程控制
SQL限流的的流程控制添加在SQL執(zhí)行階段,主要的內(nèi)容有:
啟動(dòng)時(shí):
- 加載、解析限流規(guī)則到內(nèi)存中。
- 解析預(yù)留用戶。
int mysqld_main(int argc, char **argv)
{
...
load_rules();
...
}
執(zhí)行時(shí):
- 在具體執(zhí)行語(yǔ)句之前對(duì)查詢語(yǔ)句進(jìn)行判斷,如果當(dāng)前的執(zhí)行線程是復(fù)制相關(guān)的系統(tǒng)線程、存儲(chǔ)過程和方法、用戶是預(yù)留用戶,則無需進(jìn)行規(guī)則匹配;否則,根據(jù)規(guī)則匹配的結(jié)果來決定是否進(jìn)行SQL限流。匹配的效率與限流規(guī)則的數(shù)量、大小、查詢串的大小都有關(guān)系,由于此時(shí)解析完成的限流規(guī)則都已在內(nèi)存中,因此整個(gè)匹配過程消耗資源較少。但還是建議用戶設(shè)置的限流規(guī)則更加通用、長(zhǎng)度更短、數(shù)量更少,這樣更能提高限流功能的執(zhí)行效率。
- 查詢執(zhí)行完成后,維護(hù)對(duì)應(yīng)限流規(guī)則的當(dāng)前并發(fā)度。具體實(shí)現(xiàn)為在thd中添加id字段,在進(jìn)行限流時(shí),id為非0值,如果在流程中判斷id非0,且限流功能已開啟,則在限流規(guī)則中查找,根據(jù)規(guī)則節(jié)點(diǎn)的id與thd->id進(jìn)行匹配,如果存在匹配的限流規(guī)則,則將其當(dāng)前并發(fā)度減一。
void dec_conc(THD *thd, int command)
{
// 根據(jù)查詢類型在對(duì)應(yīng)鏈表找節(jié)點(diǎn)
node = find_by_id(list, thd->id);
// 并發(fā)數(shù)量減1
if (node) {
__sync_sub_and_fetch(&(node->concur), 1);
}
// 重置狀態(tài)
thd->id = 0;
}
關(guān)閉時(shí):
關(guān)閉服務(wù)時(shí),釋放限流規(guī)則占用的內(nèi)存資源(遍歷規(guī)則鏈表,釋放其中每一個(gè)節(jié)點(diǎn)占用的內(nèi)存)。
void clean_up()
{
cleanup();
}
- 限流匹配
SQL限流的基本功能的實(shí)現(xiàn)邏輯為查詢串與規(guī)則串的匹配,匹配主要的策略如下:
根據(jù)DB判斷是否是對(duì)系統(tǒng)表的查詢,如果是對(duì)系統(tǒng)表的查詢,不做限流。
/* The flow control does not take effect on system tables */
if (check_system_table(first_table->db)) {
return ret;
}
針對(duì)不同的操作類型,在相應(yīng)的限流規(guī)則鏈表上做模式匹配。
- 獲取并解析鏈表上的節(jié)點(diǎn),根據(jù)鏈表節(jié)點(diǎn)中保存的關(guān)鍵字串與查詢串匹配;
- 如果關(guān)鍵字串都匹配到,則匹配成功。
bool check_rule_matched(THD* thd, LIST* list)
{
while (滿足條件,無異常) {
// 根據(jù)大小寫開關(guān)是否打開,分別進(jìn)行模式串匹配
it = find(query_str, item->key_array[nums]);
// 如果it為空,沒有匹配到,查看下一個(gè)list,否則繼續(xù)匹配當(dāng)前限流規(guī)則節(jié)點(diǎn)
judge();
}
// 匹配成功,或者對(duì)下一個(gè)節(jié)點(diǎn)進(jìn)行匹配
}
在匹配過程中維護(hù)原子變量cur_concur、cur_reject、total_reject,分別表示當(dāng)前并發(fā)數(shù)、當(dāng)前限流次數(shù)、總的限流次數(shù),用以判斷是否需要進(jìn)行限流以及在系統(tǒng)運(yùn)行期間觀察SQL限流的執(zhí)行狀態(tài)。
- 數(shù)據(jù)獲取
用戶輸入的限流規(guī)則保存在限流規(guī)則系統(tǒng)表中,在需要讀取物理表中的數(shù)據(jù)時(shí),需要打開表、讀取數(shù)據(jù)、關(guān)閉表。
通過創(chuàng)建限流規(guī)則表讀取類來控制對(duì)表的操作,類的定義如下:
class Du_table_access {
public:
Du_table_access() : m_drop_thd_object(NULL) {}
virtual ~Du_table_access() {}
// 初始化打開表的環(huán)境、鎖表并且打開表
bool init(THD **thd, TABLE **table, bool is_write);
// 關(guān)閉表,清理環(huán)境
bool deinit(THD *thd, TABLE *table, bool error, bool need_commit);
// 設(shè)置打開表的策略
void before_open(THD *thd);
// 如果需要的話創(chuàng)建線程,大部分時(shí)候并不需要,因?yàn)槭謩?dòng)執(zhí)行讀取數(shù)據(jù)的時(shí)候已經(jīng)在線程中了
THD *create_thd();
// 如果手動(dòng)創(chuàng)建了 thd,則需要手動(dòng)清理
void drop_thd(THD *thd);
};
讀取到的數(shù)據(jù)以節(jié)點(diǎn)的形式保存,并且掛在LIST中進(jìn)行管理。
限流規(guī)則表設(shè)計(jì)
- 定義系統(tǒng)表保存限流規(guī)則,表格式如下:
圖片
SET @cmd= "CREATE TABLE IF NOT EXISTS du_flow_control_rules (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Id of the flow control rules.',
type ENUM('SELECT', 'UPDATE', 'INSERT', 'DELETE') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Type of flow control rules.',
max_concur INT NOT NULL COMMENT 'Max concurrent of sql.',
orig_str VARCHAR(1024) CHARSET SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Original string of flow control rules.',
PRIMARY KEY(id)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT 'Flow control rules info.'";
SET @str=IF(@have_innodb <> 0, CONCAT(@cmd, ' ENGINE= INNODB;'), CONCAT(@cmd, ' ENGINE= MYISAM;'));
PREPARE stmt FROM @str;
EXECUTE stmt;
DROP PREPARE stmt;
- 權(quán)限與格式控制
通過存儲(chǔ)過程實(shí)現(xiàn)權(quán)限與格式控制:
DELIMITER $$
CREATE DEFINER='root'@'localhost' PROCEDURE mysql.add_flow_control ( IN sql_type INT, IN str VARCHAR(1024), IN max_num INT )
COMMENT '
Description
-----------
Basic functions for inserting rules.
It is not recommended to call it directly, but to call it through add_select_flow_control、
add_update_flow_control、add_update_flow_control and add_delete_flow_control.
'
SQL SECURITY INVOKER
BEGIN
IF (sql_type = 0) THEN
INSERT INTO mysql.du_flow_control_rules(type, max_concur, orig_str) VALUES('SELECT', max_num, str);
ELSEIF (sql_type = 1) THEN
INSERT INTO mysql.du_flow_control_rules(type, max_concur, orig_str) VALUES('UPDATE', max_num, str);
ELSEIF (sql_type = 2) THEN
INSERT INTO mysql.du_flow_control_rules(type, max_concur, orig_str) VALUES('INSERT', max_num, str);
ELSEIF (sql_type = 3) THEN
INSERT INTO mysql.du_flow_control_rules(type, max_concur, orig_str) VALUES('DELETE', max_num, str);
ELSE
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Sql type is error, please input correctly.';
END IF;
END$$
CREATE DEFINER='root'@'localhost' PROCEDURE mysql.add_select_flow_control (IN str VARCHAR(1024), IN max_num INT )
COMMENT '
Description
-----------
Used to add select type rules to the current rule table.
Parameters
-----------
str (VARCHAR(1024)):
The string of select rules entered by user.
max_num (INT):
The number of queries that can be executed concurrently.
Example
--------
mysql> SELECT * FROM du_flow_control_rules;
Empty set (0.00 sec)
mysql> CALL add_select_flow_control(''select~from~t1'', 100);
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM du_flow_control_rules;
+----+--------+------------+----------------+
| id | type | max_concur | orig_str |
+----+--------+------------+----------------+
| 1 | SELECT | 100 | select~from~t1 |
+----+--------+------------+----------------+
1 row in set (0.00 sec)
'
SQL SECURITY INVOKER
BEGIN
CALL add_flow_control(0, str, max_num);
END$$
DELIMITER ;
性能設(shè)計(jì)
當(dāng)開關(guān)關(guān)閉時(shí),對(duì)于每一個(gè)查詢?cè)趦?nèi)核中執(zhí)行,增加了對(duì)開關(guān)是否打開的判斷的消耗,該操作對(duì)于整個(gè)語(yǔ)句的執(zhí)行來說影響較小,可以忽略。
當(dāng)開關(guān)打開時(shí),需要將查詢字符串與限流規(guī)則進(jìn)行匹配,整個(gè)過程是內(nèi)存操作,且根據(jù)類型將限流規(guī)則分為不同的鏈表,加快匹配速度,整個(gè)過程對(duì)性能的影響與限流規(guī)則的數(shù)量以及復(fù)雜度有關(guān)(注:加入SQL限流特性對(duì)系統(tǒng)有影響,影響程度與限流規(guī)則的數(shù)量和復(fù)雜度有關(guān),但實(shí)際使用場(chǎng)景是針對(duì)少部分業(yè)務(wù)進(jìn)行限流,且不需要限流的時(shí)候不啟用該功能,因此該場(chǎng)景對(duì)性能的影響不決定最終實(shí)現(xiàn))。
性能設(shè)計(jì):
- 刷新限流規(guī)則到節(jié)點(diǎn)時(shí),為了提升加載效率,節(jié)省不必要的遍歷,使用MySQL原生的鏈表插入方法,且只增刪限流規(guī)則,不允許修改限流規(guī)則。具體流程為,在限流規(guī)則節(jié)點(diǎn)增加ID字段,該字段與規(guī)則表中的自增主鍵ID對(duì)應(yīng),即該字段遞增。在從系統(tǒng)表中讀取數(shù)據(jù)后,根據(jù)ID可以快速判斷出該條記錄該插入的情況,此時(shí)對(duì)于該條記錄,可能有兩種場(chǎng)景:一是該條記錄已經(jīng)加載并解析到內(nèi)存(鏈表中存在該節(jié)點(diǎn)),無需再次插入;二是這條記錄還沒有加載到內(nèi)存,找到對(duì)應(yīng)位置插入即可。
- 在數(shù)據(jù)庫(kù)實(shí)例啟動(dòng)時(shí),從已有系統(tǒng)表中加載一次數(shù)據(jù),提升后續(xù)限流效率。
- 使用方面:
由于分隔符的選擇決定了限流規(guī)則的不同形式,因此修改分隔符會(huì)導(dǎo)致限流規(guī)則全部重新加載解析一次,盡量不在業(yè)務(wù)高峰期修改分隔符。
SQL限流的性能取決于限流規(guī)則的數(shù)量、關(guān)鍵字?jǐn)?shù)量、查詢的單詞數(shù)量,因此在使用時(shí),應(yīng)盡量使用較為通用的限流規(guī)則。
功能限制
- 當(dāng)SQL語(yǔ)句匹配多條限流規(guī)則時(shí),優(yōu)先生效最新添加的規(guī)則;
- 在添加SQL限流規(guī)則之前,已經(jīng)開始執(zhí)行的SQL語(yǔ)句,不會(huì)被記入并發(fā)數(shù);
- 存儲(chǔ)過程、觸發(fā)器、函數(shù)和對(duì)系統(tǒng)表的查詢不受SQL限流的限制;
- 當(dāng)設(shè)置過多限流規(guī)則時(shí),對(duì)性能有一定影響。
四、總結(jié)
本文詳細(xì)描述了基于MySQL內(nèi)核的SQL限流功能的整體架構(gòu)、主要功能模塊等關(guān)鍵要素。SQL限流的設(shè)計(jì)與實(shí)現(xiàn)基于原生MySQL的執(zhí)行原理與字符串匹配規(guī)則實(shí)現(xiàn)。整體功能對(duì)原生MySQL嵌入修改較少,除了對(duì)是否進(jìn)行限流的判斷以外,其他功能以獨(dú)立模塊呈現(xiàn),安全可控。