基于MySQL內核的SQL限流設計與實現
一、引言
編寫目的
本文詳細描述了SQL限流特性的需求設計方案以及使用方式,開發、測試人員可根據本文實現功能的開發、測試,DBA可根據本文合理使用SQL限流功能。
需求概述
生產環境中可能出現由于業務量增長過快或者慢SQL等原因導致CPU使用率打滿,當CPU打滿時,會影響所有依賴該數據庫實例的業務,因此需要有手段限制影響CPU的業務執行,保證其他核心業務不受影響。
SQL限流的目標是在出現CPU使用率打滿影響所有業務時,通過限制非核心業務或者慢SQL的業務,實現核心業務的快速恢復。
軟件結構
MySQL軟件結構如下圖所示:
圖片
參考資料
MySQL官網:https://dev.mysql.com/doc/refman/5.7/en/
二、概要設計
架構設計
圖片
總體架構如圖所示,SQL限流的主要功能在執行器工作階段通過限流規則與查詢串的匹配來實現。
流程圖
圖片
三、詳細設計
功能設計
對外接口
針對SQL限流功能設計對外提供管理命令,如下:
- 開關控制命令(du_flow_control)
圖片
用于控制限流功能是否啟用。
- 大小寫敏感命令(du_flow_control_case_sensitive)
圖片
用于控制限流規則匹配時是否大小寫敏感。
- 預留用戶命令(du_flow_control_reserve_user)
圖片
限流功能對于預留用戶不生效。
預留用戶參數以字符串的形式接受輸入,如果存在多個預留用戶,使用 ',' 進行分隔。
在服務啟動時,需格式化該參數,后續在做限流判斷時,需要根據格式化之后參數識別是否是預留用戶,如果是預留用戶,則不進行限流操作,無需進行后續的限流規則匹配。
預留用戶參數接受NULL和空串 '',當該參數為NULL或空串時,表明所有用戶都不是預留用戶。
對于預留用戶字符串的處理,與MySQL社區對于用戶名的處理邏輯保持一致,即忽略每個用戶名前后的無意義字符(如空格、換行等),保留用戶名之間的無意義字符。
- 分隔符設置命令(du_flow_control_delimiter)
圖片
用于控制限流規則的分隔符。
分隔符不可為空,且長度小于等于1024。
修改分隔符之后,需要重新加載限流規則,對限流規則進行解析,會消耗系統資源,因此不建議在系統負載過高時修改分隔符。
對于預留用戶的處理,核心功能如下:
// 更新
static bool update_delimiter(sys_var *self, THD *thd, enum_var_type type)
{
reload_rules(thd);
}
// 校驗
static bool check_delimiter(sys_var *self, THD *thd, set_var *var)
{
judge(var->value);
judge(str);
judge(length);
}
功能模塊詳細設計
- 規則管理
SQL限流使用的場景為CPU負載過高,為了防止加重系統的負擔,限流規則應該事先加載到內存中。基于此,需要對內存中的限流規則進行管理,涉及的操作有:規則的讀取、移除、解析。
讀取
規則讀取的場景有:
- 主動執行自定義讀取命令,用于更新限流規則到內存。每當添加限流規則后,需手動執行該操作,更新限流規則到內存。
- 修改分隔符時。修改分隔符后,限流規則需要重新解析,因此也需要重新讀取。
- 數據庫實例啟動時。基于性能考慮,在實例啟動時,將限流規則加載到內存中。
移除
規則移除的場景有:
- 系統停止時移除限流規則時。
- 手動刪除限流規則時。
解析
用戶輸入的規則在物理表中的表現是一個字符串,在將其加載到內存時,需要根據限流規則的分隔符將其解析為對應的規則模式串,在模式串中包含的關鍵字全部被滿足時,需要進行限流。解析過程如下:
- 將物理表中的限流規則字符串讀取到內存字符串中。
- 根據分隔符將字符串解析為關鍵字組成的模式串鏈表。
- 流程控制
SQL限流的的流程控制添加在SQL執行階段,主要的內容有:
啟動時:
- 加載、解析限流規則到內存中。
- 解析預留用戶。
int mysqld_main(int argc, char **argv)
{
...
load_rules();
...
}
執行時:
- 在具體執行語句之前對查詢語句進行判斷,如果當前的執行線程是復制相關的系統線程、存儲過程和方法、用戶是預留用戶,則無需進行規則匹配;否則,根據規則匹配的結果來決定是否進行SQL限流。匹配的效率與限流規則的數量、大小、查詢串的大小都有關系,由于此時解析完成的限流規則都已在內存中,因此整個匹配過程消耗資源較少。但還是建議用戶設置的限流規則更加通用、長度更短、數量更少,這樣更能提高限流功能的執行效率。
- 查詢執行完成后,維護對應限流規則的當前并發度。具體實現為在thd中添加id字段,在進行限流時,id為非0值,如果在流程中判斷id非0,且限流功能已開啟,則在限流規則中查找,根據規則節點的id與thd->id進行匹配,如果存在匹配的限流規則,則將其當前并發度減一。
void dec_conc(THD *thd, int command)
{
// 根據查詢類型在對應鏈表找節點
node = find_by_id(list, thd->id);
// 并發數量減1
if (node) {
__sync_sub_and_fetch(&(node->concur), 1);
}
// 重置狀態
thd->id = 0;
}
關閉時:
關閉服務時,釋放限流規則占用的內存資源(遍歷規則鏈表,釋放其中每一個節點占用的內存)。
void clean_up()
{
cleanup();
}
- 限流匹配
SQL限流的基本功能的實現邏輯為查詢串與規則串的匹配,匹配主要的策略如下:
根據DB判斷是否是對系統表的查詢,如果是對系統表的查詢,不做限流。
/* The flow control does not take effect on system tables */
if (check_system_table(first_table->db)) {
return ret;
}
針對不同的操作類型,在相應的限流規則鏈表上做模式匹配。
- 獲取并解析鏈表上的節點,根據鏈表節點中保存的關鍵字串與查詢串匹配;
- 如果關鍵字串都匹配到,則匹配成功。
bool check_rule_matched(THD* thd, LIST* list)
{
while (滿足條件,無異常) {
// 根據大小寫開關是否打開,分別進行模式串匹配
it = find(query_str, item->key_array[nums]);
// 如果it為空,沒有匹配到,查看下一個list,否則繼續匹配當前限流規則節點
judge();
}
// 匹配成功,或者對下一個節點進行匹配
}
在匹配過程中維護原子變量cur_concur、cur_reject、total_reject,分別表示當前并發數、當前限流次數、總的限流次數,用以判斷是否需要進行限流以及在系統運行期間觀察SQL限流的執行狀態。
- 數據獲取
用戶輸入的限流規則保存在限流規則系統表中,在需要讀取物理表中的數據時,需要打開表、讀取數據、關閉表。
通過創建限流規則表讀取類來控制對表的操作,類的定義如下:
class Du_table_access {
public:
Du_table_access() : m_drop_thd_object(NULL) {}
virtual ~Du_table_access() {}
// 初始化打開表的環境、鎖表并且打開表
bool init(THD **thd, TABLE **table, bool is_write);
// 關閉表,清理環境
bool deinit(THD *thd, TABLE *table, bool error, bool need_commit);
// 設置打開表的策略
void before_open(THD *thd);
// 如果需要的話創建線程,大部分時候并不需要,因為手動執行讀取數據的時候已經在線程中了
THD *create_thd();
// 如果手動創建了 thd,則需要手動清理
void drop_thd(THD *thd);
};
讀取到的數據以節點的形式保存,并且掛在LIST中進行管理。
限流規則表設計
- 定義系統表保存限流規則,表格式如下:
圖片
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;
- 權限與格式控制
通過存儲過程實現權限與格式控制:
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 ;
性能設計
當開關關閉時,對于每一個查詢在內核中執行,增加了對開關是否打開的判斷的消耗,該操作對于整個語句的執行來說影響較小,可以忽略。
當開關打開時,需要將查詢字符串與限流規則進行匹配,整個過程是內存操作,且根據類型將限流規則分為不同的鏈表,加快匹配速度,整個過程對性能的影響與限流規則的數量以及復雜度有關(注:加入SQL限流特性對系統有影響,影響程度與限流規則的數量和復雜度有關,但實際使用場景是針對少部分業務進行限流,且不需要限流的時候不啟用該功能,因此該場景對性能的影響不決定最終實現)。
性能設計:
- 刷新限流規則到節點時,為了提升加載效率,節省不必要的遍歷,使用MySQL原生的鏈表插入方法,且只增刪限流規則,不允許修改限流規則。具體流程為,在限流規則節點增加ID字段,該字段與規則表中的自增主鍵ID對應,即該字段遞增。在從系統表中讀取數據后,根據ID可以快速判斷出該條記錄該插入的情況,此時對于該條記錄,可能有兩種場景:一是該條記錄已經加載并解析到內存(鏈表中存在該節點),無需再次插入;二是這條記錄還沒有加載到內存,找到對應位置插入即可。
- 在數據庫實例啟動時,從已有系統表中加載一次數據,提升后續限流效率。
- 使用方面:
由于分隔符的選擇決定了限流規則的不同形式,因此修改分隔符會導致限流規則全部重新加載解析一次,盡量不在業務高峰期修改分隔符。
SQL限流的性能取決于限流規則的數量、關鍵字數量、查詢的單詞數量,因此在使用時,應盡量使用較為通用的限流規則。
功能限制
- 當SQL語句匹配多條限流規則時,優先生效最新添加的規則;
- 在添加SQL限流規則之前,已經開始執行的SQL語句,不會被記入并發數;
- 存儲過程、觸發器、函數和對系統表的查詢不受SQL限流的限制;
- 當設置過多限流規則時,對性能有一定影響。
四、總結
本文詳細描述了基于MySQL內核的SQL限流功能的整體架構、主要功能模塊等關鍵要素。SQL限流的設計與實現基于原生MySQL的執行原理與字符串匹配規則實現。整體功能對原生MySQL嵌入修改較少,除了對是否進行限流的判斷以外,其他功能以獨立模塊呈現,安全可控。