通過 ProxySQL 在 TiDB 上實現 SQL 的規則化路由

PingCAP發表於2021-11-04

回顧,以最佳實踐為起點

作為一款 HTAP 資料庫,TiDB 能同時處理來自使用者端的 OLTP 線上業務與 OLAP 分析業務。針對分析類需求,優化器會自動將請求路由到列存的 TiFlash 節點;而對於線上請求,優化器會自動路由到行存 TiKV 請求。對於 HTAP 資料庫,我們最關心的莫過於佔用大量資源的分析類查詢是否會影響到線上的 OLTP 業務,針對這個問題,TiDB 在物理層上對 TiKV 與 TiFlash 進行了隔離,很好的避免了這種情況。

反思,最佳實踐結構之痛

通過資源隔離的方式,我們解決了業務之間的相互影響。然而要想實現更靈活、高效的應用,上面的架構中仍然存在一定的問題:

  • HAProxy 暫時沒有高可用功能
  • 對於 TiDB Server 來說,沒有做到 TP 業務與 AP 業務的隔離

對於以上的兩個問題,我們可以採用以下的兩種方案規避上面的風險。

HAProxy 的高可用方案

在生產環境中,一般我們是不會單獨使用 HaProxy 做 DNSRoundRobin 的。任何一個單點非高可用結構都會導致系統的不可用。HaProxy 本身是一個無狀態的服務, 對於無狀態服務,我們可以通過多個服務來來規避單節點的可用性風險。另外,在 HaProxy 之上,我們可以通過 Keepalived 的探活指令碼將 VIP 飄到一個可用的節點上,以完成單入口的高可用結構。

TP 與 AP 的隔離方案

在 HTAP 場景中,我們已經通過將資料在物理層面上存放在 TiKV 與 TiFlash 上來隔離 OLTP 和 OLAP 查詢請求,真正實現了儲存引擎級別的隔離。在計算引擎上,也可以通過 TiDB 例項級別設定 isolation-read 引數來實現 engine 的隔離。配置 isolation-read 變數來指定所有的查詢均使用指定 engine 的副本,可選 engine 為 “TiKV”、“TiDB” 和 “TiFlash”(其中 “TiDB” 表示 TiDB 內部的記憶體表區,主要用於儲存一些 TiDB 系統表,使用者不能主動使用)。

無論前端是否做 HAProxy 的高可用,在 roundrobin endpoint 的時候,HAProxy 無法判斷 TiDB Server 的 isolation-read engine 隔離機制是什麼樣的。這樣就可能造成一個尷尬的局面,HAProxy 可能將 OLTP 的查詢請求路由到了 isolation-read 設定為 TiFlash 的節點上,使得我們無法以最佳的姿態來處理請求。亦或是說,某些我們強制使用了 hint 走 TiFlash 的分析類查詢,可能會被路由到 isolation-read 設定為 TiKV 的結點上,SQL 請求丟擲異常。
從功能點出發,我們需要重新定義一下 HTAP 資料庫:

  • 我希望儲存層資料是分離的,OLTP 和 OLAP 業務互不影響
  • 我希望計算層的請求是分離的,OLTP 和 OLAP 請求互不影響

變更,需求驅動架構轉型

基於 HAProxy 的改造

為了解決計算層 TiDB Server 的路由,我們可以使用兩套 HAProxy 將 TiDB Server 叢集進行物理上的區分。一套 HAProxy 叢集用來管理 isolation-read 為 TiKV 的 TiDB Server,另一套 HAProxy 叢集用來管理 isolation-read 為 Tiflash 的 TiDB Server。出於高可用的考慮,我們仍然需要在 HaProxy 叢集上做高可用,這樣一來,可以抽象出如下的架構:

從整體架構上來看,這樣的一套架構設計基本滿足了我們的需求,計算層 TiDB Server 被物理隔離開,前端的 Proxy 也做了高可用。但這樣的結構還是存在缺陷的:

  • 結構較為複雜,以致為了保證系統的高可用性,花費的相對物理結點較高
  • Proxy 的出口不統一,需要兩套 Keepalived 維護兩個 VIP,在業務邏輯中需要進行編碼操作

如果採用這樣一套架構,從削減成本的角度考慮,我們可以進行結點混部。兩套 keepalived 叢集我們可以考慮部署在一套三節點的機器上,通過 virtual_router_id 進行物理隔離。或者直接部署一套 keepalived 叢集,不使用 keepalived 中自帶的 VIP,在一套 keepalived 分別部署兩套 vrrp script,各自的探活指令碼中維護獨立的 VIP。HAProxy 我們也可以使用 keepalived 的機器進行部署,做成一套 2 (3 Keepalived + 3 * Haproxy) 的結構。如此改進的叢集架構,雖然可以將機器成本壓縮到和維護普通叢集相同,但仍然無法從架構上削減複雜性,也無法更改兩個入口帶來的不變。

使用 ProxySQL 實現 SQL 的路由

現在來看,我們需要的是一款 TP/AP 分離的 Proxy。從需求上來看是比較匹配 MySQL 讀寫分離的,或者明確的說,我們的需求就是需要一款 SQL 路由的工具。
想必接觸過 MySQL 的同學都會了解 ProxySQL 這款產品。ProxySQL 是一款基於 MySQL 的開源中介軟體產品,是一個靈活的 MySQL 代理工具。作為一款強大的規則引擎中介軟體,ProxySQL 為我們提供了很多特性:

  • 靈活強大的 SQL 路由規則,可以智慧的負載 SQL 請求。
  • 無狀態服務,方便的高可用管理方案。
  • 自動感知結點的監控狀態,快速剔除異常結點。
  • 方便的 SQL 監控分析統計。
  • 配置庫基於 SQLite 儲存,可以線上修改配置並且動態載入。
  • 相比於 MySQL query cache 更靈活的 cache 功能,可以在配置表中多維度的控制語句快取。


在我看來,ProxySQL 是一款強大到沒有什麼多餘功能的產品,他的每一個特性都能切實的命中使用者的痛點,滿足使用者的需求。如果硬要說有什麼不足的話,我能想到的就是由於路由功能帶來的效能衰退,但這樣的衰退在其他的 Proxy 工具中依然存在甚至更甚。
作為一款 “大尺碼” 的 MySQL,TiDB 是否可以很好的適配 ProxySQL 呢?答案是肯定的。我們可以簡單的複製 ProxySQL 在 MySQL 讀寫分離的方案,進行 TP/AP SQL 請求的路由操作。甚至來說,以上介紹的種種強大的功能,在 TiDB 中仍然適用,在某種程度上,彌補了 TiDB 生態的不足。

全鏈路的高可用

對於一套資料庫系統,任何一個環節都可能成為故障點,所以任何服務都不能以單點的形式存在。TiDB Cluster 的任何元件都是有高可用並且可擴充套件的。ProxySQL 也可以配置高可用叢集。對於 ProxySQL 的高可用,目前流行的主要有兩種方案:

  • 多個相互獨立的 ProxySQL
  • 使用 ProxySQL 的高可用叢集

ProxySQL 本身是無狀態的服務,所以前端多個相互獨立的 ProxySQL 本身就是對可用性的一種保障。但由於多個 ProxySQL 是獨立的,相關的配置檔案無法互聯。對任何配置進行改動無法自動同步,這對管理來說是存在風險的。如果使用叢集版的 ProxySQL 高可用,為了保證叢集狀態的 watchdog 程式可能本身對於叢集就是一種負載。

正如前面所處,對於一套叢集,我們期望能有一個入口。而前端的 ProxySQL 本身是有多個入口的。我們可以採用 Keepalived + haproxy 的方式進行一個 endpoint 的負載均衡,或者說擔心多級 proxy(HAProxy + ProxySQL)帶來的效能大量衰退,我們可以自己維護 Keepalived 的探活指令碼控制 VIP。對於網路監管比較嚴格的公司,可能關閉了 VRRP 協議,那麼可以選擇 Zookeeper 服務註冊與發現來維護 ProxySQL 的狀態,在 Zookeeper 中管理叢集的 VIP。

針對於多 endpoint 的統一入口高可用方案,每個公司都有自己的解決架構。就我而言,相比於 Keepalived + HAProxy 或者在 Keepalived 的指令碼中做負載均衡,我更傾向於使用 zookeeper 來管理叢集的狀態。我們需要仔細的去規劃 Keepalived 的算分制度,為了減少 HAProxy 對效能的衰減,可能又要在指令碼中管理另一套 VIP 或者關閉失敗結點上的 Keepalived 服務。當然,使用什麼方案還要配合我們自己的技術棧,只有適合自己的才是最佳實踐。

在上面的架構中,TP 與 AP 的請求通過 APP 程式接入到後臺的 TiDB Cluster。作為程式的唯一入口,Keepalived 的探活程式會選擇一臺可用的 ProxySQL,在之上建立一個 VIP。這個 VIP 將作為應用程式與 TiDB Cluster 對接的唯一入口。在 ProxySQL 叢集中,根據 Router Table(mysql_query_rules)中配置的 TP 和 AP 的 pattern 配置,將 TP 與 AP 的 查詢請求自動的路由配置好的 TP_GROUP 與 AP_GROUP 中。
綜上所述,這樣的架構能夠解決我們之前的痛點問題:

  • 應用程式與資料庫叢集使用唯一的介面。
  • 簡單的高可用結構,通過一套 keepalived 與一套 Proxy 叢集實現高可用性。
  • TP/AP 的請求能夠自動的路由到對應的計算節點 TiDB Server 中。

踐行,從案例入手求結果

部署了一個 demo 系統,簡單的展示一下整套架構的執行流程與結果。

以下為結點上的元件列表:

安裝 TiDB

略,可參考官方文件(TiDB 資料庫快速上手指南)。

安裝 ProxySQL

可以選擇使用 rpm 的方式安裝 ProxySQL。但為了統一安裝的位置,一般我會習慣使用原始碼進行編譯安裝,然後使用 rpmbuild 打成安裝包部署到其他結點上。編譯安裝可以參考 INSTALL.md 文件。
修改 proxy.cfg 檔案,修改 datadir="/opt/tidb-c1/proxysql-6033/data"。
使用以下命令可以啟動 ProxySQL,或是配置 systemd 檔案進行啟動。
/opt/tidb-c1/proxysql-6033/proxysql -c /opt/tidb-c1/proxysql-6033/proxysql.cfg

配置 ProxySQL

由於在本例中,我使用了三臺獨立的 ProxySQL 做高可用負載,需要在這三臺機器上做相同的配置。如果選擇了 ProxySQL 自帶的高可用,那麼只需要在一臺機器上進行配置。

[root@r31 proxysql-6033]# mysql -uadmin -padmin -h127.0.0.1 -P6032 --prompt 'admin>'## set server infoinsert into mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.232.31',14000);insert into mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.232.32',14000);insert into mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.232.33',14000);insert into mysql_servers(hostgroup_id,hostname,port) values(20,'192.168.232.34',14000);insert into mysql_servers(hostgroup_id,hostname,port) values(20,'192.168.232.35',14000);load mysql servers to runtime;save mysql servers to disk;## set userinsert into mysql_users(username,password,default_hostgroup) values('root','mysql',10);load mysql users to runtime;save mysql users to disk;## set monitoring userset mysql-monitor_username='monitor';set mysql-monitor_password='monitor';load mysql variables to runtime;save mysql variables to disk;## set sql router rule## this is just a demoinsert into mysql_query_rules(rule_id,active,match_digest,destination_hostgroup,apply)values(1,1,'^select.*tikv.*',10,1),(2,1,'^select.*tiflash.*',20,1);load mysql query rules to runtime;save mysql query rules to disk;

配置 Keepalived

Keepalived 的安裝參考 keeaplived-install,與 ProxySQL 的安裝相同,推薦編譯安裝後達成 rpm 包或者直接 copy keepalived 的 binary。
Keepalived 的配置檔案指令碼如下:

global_defs {   notification_email {     acassen@firewall.loc     failover@firewall.loc     sysadmin@firewall.loc   }   vrrp_skip_check_adv_addr   vrrp_strict   vrrp_garp_interval 0   vrrp_gna_interval 0}vrrp_script check_proxysql {    script 'killall -0 proxysql || systemctl stop keepalived'    interval 2    weight 10}vrrp_script test_script {    script 'echo `date` >> /tmp/aaa'    interval 1    weight 1}vrrp_instance proxysql_kp {    state MASTER    interface ens33    virtual_router_id 51    priority 150    advert_int 1    authentication {        auth_type PASS        auth_pass 1888    }    virtual_ipaddress {        192.168.232.88    }    track_script{        check_proxysql        ##test_script    }}

驗證 ProxySQL

我開啟了五臺 TiDB Server 上的 general log 用來記錄 SQL語句。
在 TiDB Cluster 中建立了兩張表:

  • test.t_tikv(idi int),資料從 1 - 1000
  • test.t_tiflash(idi int),資料從 1 - 1000

在前端使用簡單的迴圈進行壓測:

for i in `seq 1000`; do mysql -uroot -P6033 -h192.168.232.88 -pmysql -e "select * from test.t_tikv where idi = $i"; donefor i in `seq 1000`; do mysql -uroot -P6033 -h192.168.232.88 -pmysql -e "select * from test.t_tiflash where idi = $i"; done

TiDB Server log 過濾關鍵字 “select * from test.t_tikv where idi =” 的條數。可以看出針按照路由表中配置的 TiKV SQL,1000 條較為分散的路由到了 TiDB-1,TiDB-2,TiDB-3 結點上。

彩蛋,你想要的審計功能

資料庫審計是對資料庫的訪問行為進行監管的系統,他能夠在發生資料庫安全事件之後為事件的罪責定責提供依據。將審計日誌抽取到實時數倉中進行風控處理,能夠及時的發現風險,最大程度的挽回損失。審計日誌在一些重要的金融、訂單交易系統中至關重要。

如何捕獲 audit log

與現在很多使用者一樣,曾經我也遇到過 audit 的需求。像 MongoDB 這樣的開源資料庫,很多都是不提供免費的審計功能的。審計功能對於很多金融類的場景是尤其的重要,為了完成審計功能,我們通常有兩種方式:

  • 在原始碼中解析語義
  • 資料的流量採集

所謂的原始碼語義解析,其實就是我們在原始碼中手動的新增 audit 的功能。我們可以修改原始碼,將一些希望捕獲的變數資訊落盤到本地檔案中。但是這種方式可能會造成大量的等待,影響資料庫的效能。通過將這種寫操作非同步執行,可以稍微緩解效能的下降。
另一種資料流量採集的方式,相比於變數落盤這種方式,要稍微好一些。流量監控的思路是搭建一套與資料庫相對獨立的旁路系統,通過抓包或者探針等工具截獲流量,將針對於資料庫的請求列印到本地的檔案中。這種方法本身與資料庫不掛鉤,非同步的獲取審計日誌。

在 TiDB 中捕獲 audit log

TiDB 上的審計目前來看主要有兩種,一種是購買原廠提供的審計外掛,另一種是開啟 General log 功能,在 TiDB log 中可以檢視到 SQL 語句。需要注意的是,由於前端我們使用了 HAProxy,我們需要配置 forwardfor 引數以捕獲客戶端的 IP。General log 會將所有的包括 select 在內的請求都記錄在 TiDB log 中,根據以往的測試來看,會有大概 10%-20% 的效能損失。記錄的 SQL 語句包括時間或 IP 等其他的資訊可能不能滿足我們的需求,並且從 TiDB log 中整理出 audit 也是一個較大的工程。

在 ProxySQL 中獲取 audit log

Audit 的需求是非常常見的。如 MongoDB,開源資料庫的社群版本不提供 audit 功能也是較為普遍的。從整條鏈路來看,能獲取到完整 audit 的結點有兩個,一個是資料庫端,一個是 Proxy 端。ProxySQL 可以為我們提供了審計的功能。當我們指定了 MySQL_enventslog_filename 引數,即設定開啟審計功能。在審計檔案中,我們可以捕捉到 ProxySQL 入口的所有 SQL audit。
在我的環境中,可以捕捉到以下格式的 audit log,基本滿足了使用者的大部分需求:

通過探針截獲 audit

可以通過 systemtap 做成 probe 掛在 proxySQL上,根據一些 ProxySQL 關鍵字,比如說,run、execute、query、init、parse、MySQL、connection 等嘗試追蹤到這些函式的呼叫棧與引數。列印這些引數可以獲取到處理請求時的 IP 與 Statement。如果這些函式沒有辦法追蹤到 audit 資訊,那麼可以考慮使用暴力破解的思路,追蹤 ProxySQL 的所有函式(通過 function("*") 來匹配)。根據結果定位到指定的函式。但這種方法在開發時需要一臺較為強大的伺服器。

目前通過虛幻能夠追蹤到的審計日誌如下:

>>>>>>>>>>>>>>>>>>>[ function >> ZN10Query_Info25query_parser_command_typeEv ]  [ time >> 1622953221 ]  this={.QueryParserArgs={.buf="select ?", .digest=2164311325566300770, .digest_total=17115818073721422293, .digest_text="select ?", .first_comment=0x0, .query_prefix=0x0}, .sess=0x7f1961a3a300, .QueryPointer="select 1113 192.168.232.36", .start_time=2329486915, .end_time=2329486535, .mysql_stmt=0x0, .stmt_meta=0x0, .stmt_global_id=0, .stmt_info=0x0, .QueryLength=11, .MyComQueryCmd=54, .bool_is_select_NOT_for_update=0, .bool_is_select_NOT_for_update_computed=0, .have_affected_rows=0, .affected_rows=0, .rows_s   ######

其中可以抓到 query point,從中可以獲取到 query 的文字,使用者的 client IP,而 function name 與 time 是我通過 systemtap 的指令碼直接本底寫入的。

使用外掛探針這種方式,能夠很好的減輕 Proxy 或者 Database 的寫日誌等待,從而最小程度的減少對資料庫效能的影響,基本可以忽略因為審計帶來的效能損失。

相關文章