返回ProxySQL系列文章:http://www.cnblogs.com/f-ck-need-u/p/7586194.htmlhtml
當ProxySQL收到前端app發送的SQL語句後,它須要將這個SQL語句(或者重寫後的SQL語句)發送給後端的MySQL Server,而後收到SQL語句的MySQL Server執行查詢,並將查詢結果返回給ProxySQL,再由ProxySQL將結果返回給客戶端(若是設置了查詢緩存,則先緩存查詢結果)。前端
ProxySQL能夠實現多種方式的路由:基於ip/port、username、schema、SQL語句。其中基於SQL語句的路由是按照規則進行匹配的,匹配方式有hash高效匹配、正則匹配,還支持更復雜的鏈式規則匹配。mysql
本文將簡單演示基於端口、用戶和schema的路由,而後再詳細介紹基於SQL語句的路由規則。不過須要說明的是,本文只是入門,爲後面ProxySQL的高級路由方法作鋪墊。git
在閱讀本文以前,請確保:github
若是想速成,可參考;ProxySQL初試讀寫分離正則表達式
本文涉及到的實驗環境以下:sql
角色 | 主機IP | server_id | 數據狀態 |
---|---|---|---|
Proxysql | 192.168.100.21 | null | 無 |
Master | 192.168.100.22 | 110 | 剛安裝的全新MySQL實例 |
Slave1 | 192.168.100.23 | 120 | 剛安裝的全新MySQL實例 |
Slave2 | 192.168.100.24 | 130 | 剛安裝的全新MySQL實例 |
該實驗環境已經在前面的文章中搭建好,本文再也不贅述一大堆的內容。環境的搭建請參考前面給出的一、二、3。數據庫
我前面寫了一篇經過MySQL Router實現MySQL讀寫分離的文章,MySQL Router實現讀寫分離的方式就是經過監聽不一樣端口實現的:一個端口負責讀操做,一個端口負責寫操做。這樣的路由邏輯很是簡單,配置起來也很方便。後端
雖然基於端口實現讀寫分離配置起來很是簡單,可是缺點也很明顯:必須在前端app的代碼中指定端口號碼。這意味着MySQL的一部分流量權限被開發人員掌控了,換句話說,DBA沒法全局控制MySQL的流量。此外,修改端口號時,app的代碼也必須作出相應的修改。緩存
雖然說有缺點,但爲了我這個ProxySQL系列文章的完整性,本文仍是要簡單演示ProxySQL如何基於端口實現讀寫分離。
首先修改ProxySQL監聽SQL流量的端口號,讓其監聽在不一樣端口上。
admin> set mysql-interfaces='0.0.0.0:6033;0.0.0.0:6034'; admin> save mysql variables to disk;
而後重啓ProxySQL。
[root@xuexi ~]# service proxysql stop [root@xuexi ~]# service proxysql start [root@xuexi ~]# netstat -tnlp | grep proxysql tcp 0 0 0.0.0.0:6032 0.0.0.0:* LISTEN 27572/proxysql tcp 0 0 0.0.0.0:6033 0.0.0.0:* LISTEN 27572/proxysql tcp 0 0 0.0.0.0:6034 0.0.0.0:* LISTEN 27572/proxysql
監聽到不一樣端口,再去修改mysql_query_rules
表。這個表是ProxySQL的路由規則定製表,後文會很是詳細地解釋該表。
例如,插入兩條規則,分別監聽在6033端口和6034端口,6033端口對應的hostgroup_id=10
是負責寫的組,6034對應的hostgroup_id=20
是負責讀的組。
insert into mysql_query_rules(rule_id,active,proxy_port,destination_hostgroup,apply) values(1,1,6033,10,1), (2,1,6034,20,1); load mysql query rules to runtime; save mysql query rules to disk;
這樣就配置結束了,是否很簡單?
其實除了基於端口進行分離,還能夠基於監聽地址(修改字段proxy_addr便可),甚至能夠基於客戶端地址(修改字段client_addr字段便可,該用法可用於採集數據、數據分析等)。
不管哪一種路由方式,其實都是在修改mysql_query_rules表,因此下面先解釋下這個表。
能夠經過show create table mysql_query_rules
語句查看定義該表的語句。
下面是我整理出來的字段屬性。
| COLUMN | TYPE | NULL? | DEFAULT | |-----------------------|---------|----------|------------| | rule_id (pk) | INTEGER | NOT NULL | | | active | INT | NOT NULL | 0 | | username | VARCHAR | | | | schemaname | VARCHAR | | | | flagIN | INT | NOT NULL | 0 | | client_addr | VARCHAR | | | | proxy_addr | VARCHAR | | | | proxy_port | INT | | | | digest | VARCHAR | | | | match_digest | VARCHAR | | | | match_pattern | VARCHAR | | | | negate_match_pattern | INT | NOT NULL | 0 | | re_modifiers | VARCHAR | | 'CASELESS' | | flagOUT | INT | | | | replace_pattern | VARCHAR | | | | destination_hostgroup | INT | | NULL | | cache_ttl | INT | | | | reconnect | INT | | NULL | | timeout | INT | | | | retries | INT | | | | delay | INT | | | | mirror_flagOU | INT | | | | mirror_hostgroup | INT | | | | error_msg | VARCHAR | | | | sticky_conn | INT | | | | multiplex | INT | | | | log | INT | | | | apply | INT | NOT NULL | 0 | | comment | VARCHAR | | |
各個字段的意義以下:有些字段不理解也無所謂,後面會分析一部分比較重要的。
schemaname
做爲默認schema時,該鏈接發出的查詢纔會被匹配。(在MariaDB/MySQL中,schemaname等價於databasename)。stats_mysql_query_digest.digest
中。match_digest
或match_pattern
匹配的纔算被成功匹配。也就是說,至關於在這兩個匹配動做前加了NOT操做符進行取反。CASELESS
後,將忽略大小寫。指定了GLOBAL
後,將替換全局(而不是第一個被匹配到的內容)。爲了向後兼容,默認只啓用了CASELESS
修飾符。transaction_persistent=1
(見mysql_users
表),且該用戶創建的鏈接開啓了一個事務,則這個事務內的全部語句都將路由到同一主機組,無視匹配規則。mysql-default_query_timeout
的值。mysql-query_retries_on_failure
的值。mysql-default_query_delay
全局變量中,因此它會應用於全部的查詢。未來的版本中將會提供一個更高級的限流機制。error_msg
指定的信息。NULL
,表示不會修改multiplexing的策略。mysql_query_rules_fast_routing
中的規則)。基於mysql user的配置方式和基於端口的配置是相似的。
須要注意,在插入mysql user到mysql_users
表中時,就已經指定了默認的路由目標組,這已經算是一個路由規則了(只不過是默認路由目標)。當成功匹配到mysql_query_rules
中的規則時,這個默認目標就再也不生效。因此,經過默認路由目標,也能簡單地實現讀寫分離。
例如,在後端MySQL Server上先建立好用於讀、寫分離的用戶。例如,root用戶用於寫操做,reader用戶用於讀操做。
# 在master節點上執行: grant all on *.* to root@'192.168.100.%' identified by 'P@ssword1!'; grant select,show databases,show view on *.* to reader@'192.168.100.%' identified by 'P@ssword1!';
而後將這兩個用戶添加到ProxySQL的mysql_users
表中,並建立兩條規則分別就有這兩個用戶進行匹配。
insert into mysql_users(username,password,default_hostgroup) values('root','P@ssword1!',10),('reader','P@ssword1!',20); load mysql users to runtime; save mysql users to disk; delete from mysql_query_rules; # 爲了測試,先清空已有規則 insert into mysql_query_rules(rule_id,active,username,destination_hostgroup,apply) values(1,1,'root',10,1),(2,1,'reader',20,1); load mysql query rules to runtime; save mysql query rules to disk;
固然,在上面演示的示例中,mysql_query_rules中基於username的規則和mysql_users中這兩個用戶的默認規則是重複了的。
ProxySQL支持基於schemaname進行路由。這在必定程度上實現了簡單的sharding功能。例如,將後端MySQL集羣中的節點A和節點B定義在不一樣主機組中,ProxySQL將全部對於DB1庫的查詢路由到節點A所在的主機組,將全部對DB2庫的查詢路由到節點B所在的主機組。
只需配置一個schemaname字段就夠了,好簡單,是否是感受很爽。但想太多了,ProxySQL的schemaname字段只是個雞肋,要實現分庫sharding,只能經過正則匹配、查詢重寫的方式來實現。
例如,原語句以下,用於找出浙江省的211大學。
select * from zhongguo.university where prov='Zhejiang' and high=211;
按省份分庫後,經過ProxySQL的正則替換,將語句改寫爲以下SQL語句:
select * from Zhejiang.university where 1=1 high=211;
而後還能夠將改寫後的SQL語句路由到指定的主機組中,實現真正的分庫。
這些內容比較複雜、也比較高級,在後面的文章中我會詳細解釋。
從這裏開始,開始介紹ProxySQL路由規則的核心:基於SQL語句的路由。
ProxySQL接收到前端發送的SQL語句後,首先分析語句,而後從mysql_query_rules
表中尋找是否有匹配該語句的規則。若是先被username或ip/port類的規則匹配並應用,則按這些規則路由給後端,若是是被基於SQL語句的規則匹配,則啓動正則引擎進行正則匹配,而後路由給對應的後端組,若是規則中指定了正則替換字段,則還會重寫SQL語句,而後再發送給後端。
ProxySQL支持兩種類型的SQL語句匹配方式:match_digest和match_pattern。在解釋這兩種匹配方式以前,有必要先解釋下SQL語句的參數化。
什麼是參數化?
select * from tbl where id=?
這裏將where條件語句中字段id的值進行了參數化,也就是上面的問號?
。
咱們在客戶端發起的SQL語句都是完整格式的語句,可是SQL優化引擎出於優化的目的須要考慮不少事情。例如,如何緩存查詢結果、如何匹配查詢緩存中的數據並取出,等等。將SQL語句參數化是優化引擎其中的一個行爲,對於那些參數相同但參數值不一樣的查詢語句,SQL語句認爲這些是同類查詢,同類查詢的SQL語句不會重複去編譯而增長額外的開銷。
例如,下面的兩個語句,就是同類SQL語句:
select * from tbl where id=10; select * from tbl where id=20;
將它們參數化後,結果以下:
select * from tbl where id=?;
通俗地講,這裏的"?"就是一個變量,任何知足這個語句類型的值均可以傳遞到這個變量中。
因此,對參數化進行一個通俗的定義:對於那些參數相同、參數值不一樣的SQL語句,使用問號"?"去替換參數值,替換後返回的語句就是參數化的結果。
不管是MySQL、SQL Server仍是Oracle(這個不肯定),優化引擎內部都會將語句進行參數化。例如,下面是SQL Server的執行計劃,其中"@1"就是所謂的問號"?"。
ProxySQL也支持參數化。當前端發送SQL語句到達ProxySQL後,ProxySQL會將其參數化並分類。例如,下面是sysbench測試過程當中,ProxySQL統計的參數化語句。
+----+----------+------------+-------------------------------------------------------------+ | hg | sum_time | count_star | digest_text | +----+----------+------------+-------------------------------------------------------------+ | 2 | 14520738 | 50041 | SELECT c FROM sbtest1 WHERE id=? | | 1 | 3142041 | 5001 | COMMIT | | 1 | 2270931 | 5001 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? ORDER BY c | | 1 | 2021320 | 5003 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? | | 1 | 1768748 | 5001 | UPDATE sbtest1 SET k=k+? WHERE id=? | | 1 | 1697175 | 5003 | SELECT SUM(K) FROM sbtest1 WHERE id BETWEEN ? AND ?+? | | 1 | 1346791 | 5001 | UPDATE sbtest1 SET c=? WHERE id=? | | 1 | 1263259 | 5001 | DELETE FROM sbtest1 WHERE id=? | | 1 | 1191760 | 5001 | INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?) | | 1 | 875343 | 5005 | BEGIN | +----+----------+------------+-------------------------------------------------------------+
ProxySQL的mysql_query_rules
表中有三個字段,能基於參數化後的SQL語句進行三種不一樣方式的匹配:
若是要進行SQL語句的重寫(即正則替換),或者對參數值匹配,則必須採用match_pattern。若是能夠,儘可能採用digest匹配方式,由於它的效率更高。
在ProxySQL的stats庫中,包含了幾個統計表。
admin> show tables from stats; +--------------------------------------+ | tables | +--------------------------------------+ | global_variables | | stats_memory_metrics | | stats_mysql_commands_counters | <--已執行查詢語句的統計信息 | stats_mysql_connection_pool | <--鏈接池信息 | stats_mysql_connection_pool_reset | <--重置鏈接池統計數據 | stats_mysql_global | <--全局統計數據 | stats_mysql_prepared_statements_info | | stats_mysql_processlist | <--模擬show processlist的結果 | stats_mysql_query_digest | <--本文解釋 | stats_mysql_query_digest_reset | <--本文解釋 | stats_mysql_query_rules | <--本文解釋 | stats_mysql_users | <--各mysql user前端和ProxySQL的鏈接數 | stats_proxysql_servers_checksums | <--ProxySQL集羣相關 | stats_proxysql_servers_metrics | <--ProxySQL集羣相關 | stats_proxysql_servers_status | <--ProxySQL集羣相關 +--------------------------------------+
這些表的內容、解釋我已經翻譯,參見:ProxySQL的stats庫。本文介紹其中3個和路由、規則相關的表。
這個表對於分析SQL語句相當重要,是分析語句性能、定製路由規則指標的最主要來源。
剛纔已經解釋過什麼是SQL語句的參數化,還說明了ProxySQL會將參數化後的語句進行hash計算獲得它的digest,這個統計表中記錄的就是每一個參數化分類後的語句對應的統計數據,包括該類語句的執行次數、所花總時間、所花最短、最長時間,還包括語句的文本以及它的digest。
以下圖:
如下是各個字段的意義:
注意,該表中的查詢所花時長是指ProxySQL從接收到客戶端查詢開始,到ProxySQL準備向客戶端發送查詢結果的時長。所以,這些時間更像是客戶端看到的發起、接收的時間間隔(儘管客戶端到服務端數據傳輸也須要時間)。更精確一點,在執行查詢以前,ProxySQL可能須要更改字符集或模式,可能當先後端不可用(當先後端執行語句失敗)而找一個新的後端,可能由於全部鏈接都繁忙而須要等待空閒鏈接,這些都不該該計算到查詢執行所花時間內。
其中hostgroup、digest、digest_text、count_start、{sum,min,max}_time這幾列最經常使用。
例如:
admin> select hostgroup hg,count_star,sum_time,digest,digest_text from stats_mysql_query_digest; +----+------------+----------+--------------------+------------------------+ | hg | count_star | sum_time | digest | digest_text | +----+------------+----------+--------------------+------------------------+ | 10 | 4 | 2412 | 0xADB885E1F3A7A5C2 | select * from test2.t1 | | 10 | 6 | 4715 | 0x57497F236587B138 | select * from test1.t1 | +----+------------+----------+--------------------+------------------------+
從中分析,兩個語句都路由到了hostgroup=10的組中,第一個語句執行了4次,這4次總共花費了2412微秒(即2.4毫秒),第二個語句執行了6次,總花費4.7毫秒。還給出了這兩個語句參數化後的digest值,以及參數化後的SQL文本。
這個表的表結構和stats_mysql_query_digest
是徹底同樣的,只不過每次從這個表中檢索數據(隨便檢索什麼,哪怕where 1=0
),都會重置stats_mysql_query_digest
表中已統計的數據。
這個表只有兩個字段:
rule_id
:對應的是規則號碼。hits
,對應的是每一個規則被命中了多少次。digest匹配規則是對digest進行精確匹配。
例如,從stats_mysql_query_digest
中獲取兩個對應的digest值。注意,如今它們的hostgroup_id=10。
admin> select hostgroup hg,count_star,sum_time,digest,digest_text from stats_mysql_query_digest; +----+------------+----------+--------------------+------------------------+ | hg | count_star | sum_time | digest | digest_text | +----+------------+----------+--------------------+------------------------+ | 10 | 4 | 2412 | 0xADB885E1F3A7A5C2 | select * from test2.t1 | | 10 | 6 | 4715 | 0x57497F236587B138 | select * from test1.t1 | +----+------------+----------+--------------------+------------------------+
插入兩條匹配這兩個digest的規則:
insert into mysql_query_rules(rule_id,active,digest,destination_hostgroup,apply) values(1,1,"0xADB885E1F3A7A5C2",20,1),(2,1,"0x57497F236587B138",10,1);
而後測試
mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test1.t1;" mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test2.t1;"
再去查看規則的路由命中狀況:
admin> select * from stats_mysql_query_rules; +---------+------+ | rule_id | hits | +---------+------+ | 1 | 1 | | 2 | 1 | +---------+------+
查看路由的目標:
admin> select hostgroup hg,count_star cs,digest,digest_text from stats_mysql_query_digest; +----+----+--------------------+------------------------+ | hg | cs | digest | digest_text | +----+----+--------------------+------------------------+ | 20 | 1 | 0xADB885E1F3A7A5C2 | select * from test2.t1 | | 10 | 1 | 0x57497F236587B138 | select * from test1.t1 | +----+----+--------------------+------------------------+
可見,基於digest的精確匹配規則已經生效。
match_digest是對digest作正則匹配,但注意match_pattern字段中給的規則不是hash值,而是SQL語句的文本匹配規則。
ProxySQL支持兩種正則引擎:
老版本中默認的正則引擎是RE2,如今默認的正則引擎是PCRE。可從變量mysql-query_processor_regex
獲知當前的正則引擎是RE2仍是PCRE:
Admin> select @@mysql-query_processor_regex; +-------------------------------+ | @@mysql-query_processor_regex | +-------------------------------+ | 1 | +-------------------------------+
其中1表明PCRE,2表明RE2。
在mysql_query_rules
表中有一個字段re_modifiers
,它用於定義正則引擎的修飾符,默認已經設置caseless
,表示正則匹配時忽略大小寫,因此select和SELECT都能匹配。此外,還能夠設置global修飾符,表示匹配全局,而非匹配第一個,這個在重寫SQL語句時有用。
(RE2引擎沒法同時設置caseless和global,即便它們都設置了也不會生效。因此,將默認的正則引擎改成了PCRE)
在進行下面的實驗以前,先把mysql_query_rules
表清空,並將規則的統計數據也清空。
delete from mysql_query_rules; select * from stats_mysql_query_digest_reset; insert into mysql_query_rules(rule_id,active,match_digest,destination_hostgroup,apply) values (1,1,"^select .* test2.*",20,1),(2,1,"^select .* test1.*",10,1); load mysql query rules to runtime; save mysql query rules to disk;
而後分別執行:
mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test1.t1;" mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test2.t1;"
查看規則匹配結果:
admin> select * from stats_mysql_query_rules; +---------+------+ | rule_id | hits | +---------+------+ | 1 | 1 | | 2 | 1 | +---------+------+ admin> select hostgroup hg,count_star cs,digest,digest_text dt from stats_mysql_query_digest; +----+----+--------------------+------------------------+ | hg | cs | digest | dt | +----+----+--------------------+------------------------+ | 10 | 1 | 0x57497F236587B138 | select * from test1.t1 | | 20 | 1 | 0xADB885E1F3A7A5C2 | select * from test2.t1 | +----+----+--------------------+------------------------+
顯然,命中規則,且按照指望進行路由。
若是想對match_digest取反,即不被正則匹配的SQL語句才命中規則,則設置mysql_query_rules
表中的字段negate_match_pattern=1
。一樣適用於下面的match_pattern匹配方式。
和match_digest的匹配方式相似,但match_pattern是基於原始SQL語句進行匹配的,包括參數值。有兩種狀況必須使用match_pattern:
若是想對match_pattern取反,即不被正則匹配的SQL語句才命中規則,則設置mysql_query_rules
表中的字段negate_match_pattern=1
。
例如:
## 清空規則以及規則的統計數據 delete from mysql_query_rules; select * from stats_mysql_query_digest_reset where 1=0; insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(1,1,"^select .* test2.*",20,1),(2,1,"^select .* test1.*",10,1); load mysql query rules to runtime; save mysql query rules to disk;
執行查詢:
mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test1.t1;" mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e "select * from test2.t1;"
而後查看匹配結果:
admin> select * from stats_mysql_query_rules; +---------+------+ | rule_id | hits | +---------+------+ | 1 | 1 | | 2 | 1 | +---------+------+ admin> select hostgroup hg,count_star cs,digest,digest_text dt from stats_mysql_query_digest; +----+----+--------------------+------------------------+ | hg | cs | digest | dt | +----+----+--------------------+------------------------+ | 20 | 1 | 0xADB885E1F3A7A5C2 | select * from test2.t1 | | 10 | 1 | 0x57497F236587B138 | select * from test1.t1 | +----+----+--------------------+------------------------+
再來看看匹配參數值(雖然幾乎不會這樣作)。這裏要測試的語句以下:
mysql -uroot -p123456 -h127.0.0.1 -P6033 -e "select * from test1.t1 where name like 'malong%';" mysql -uroot -p123456 -h127.0.0.1 -P6033 -e "select * from test2.t1 where name like 'xiaofang%';"
如今插入兩條規則,對參數"malong%"和"xiaofang"進行匹配。
## 清空規則以及規則的統計數據 delete from mysql_query_rules; select * from stats_mysql_query_digest_reset where 1=0; insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(1,1,"malong",20,1),(2,1,"xiaofang",10,1); load mysql query rules to runtime; save mysql query rules to disk;
執行上面的兩個查詢語句,而後查看匹配結果:
admin> select * from stats_mysql_query_rules; +---------+------+ | rule_id | hits | +---------+------+ | 1 | 1 | | 2 | 1 | +---------+------+ admin> select hostgroup hg,count_star cs,digest,digest_text dt from stats_mysql_query_digest; +----+----+--------------------+------------------------------------------+ | hg | cs | digest | dt | +----+----+--------------------+------------------------------------------+ | 20 | 1 | 0x0C624EDC186F0217 | select * from test1.t1 where name like ? | | 10 | 1 | 0xA38442E236D915A7 | select * from test2.t1 where name like ? | +----+----+--------------------+------------------------------------------+
已按預期進行路由。
一個極簡單卻大有用處的讀、寫分離功能:將默認路由組設置爲寫組,而後再插入下面兩個select語句的規則。
# 10爲寫組,20爲讀組 insert into mysql_query_rules(rule_id,active,match_digest,destination_hostgroup,apply) VALUES (1,1,'^SELECT.*FOR UPDATE$',10,1), (2,1,'^SELECT',20,1);
但須要注意的是,這樣的規則只適用於小環境下的讀寫分離,對於稍複雜的環境,須要對不一樣語句進行開銷分析,對於開銷大的語句須要制定專門的路由規則。在以後的文章中我會稍做分析。
ProxySQL能經過ip、port、client_ip、username、schemaname、digest、match_digest、match_pattern實現不一樣方式的路由,方式可謂繁多。特別是基於正則匹配的靈活性,使得ProxySQL能知足一些比較複雜的環境。
總的來講,ProxySQL主要是經過digest、match_digest和match_pattern進行規則匹配的。在本文中,只是介紹了匹配規則的基礎以及簡單的用法,爲進軍後面的文章作好鋪墊。