原文作者:Liam Crilly of F5
原文鏈接:藉助 TCP 負載均衡和 Galera 集羣擴展 MySQL
轉載來源:NGINX 官方網站
(編者按——本文最初發表於 2016 年,現已進行更新,改為使用更新之後修改過的 NGINX 功能。有關詳細信息,請參閲下文“藉助 NGINX JavaScript 模塊進行高級日誌記錄”和“NGINX Plus 儀表盤”兩節。)
我們在 NGINX Plus R5 中引入了 TCP 負載均衡,並不斷在後續版本中添加新功能以及 UDP 負載均衡支持。本文探討了 TCP 負載均衡的關鍵要求以及 NGINX Plus 如何滿足這些要求。
為了探討 NGINX 的功能,我們將使用一個簡單的測試環境,來代表具有可擴展數據庫後端的應用的關鍵組件。有關構建該測試環境的完整説明,請參閲附錄。
$$*負載均衡 MySQL 節點的測試環境*$$
在該環境中,NGINX 充當數據庫服務器的反向代理,監聽 MySQL 默認端口 3306。這為客户端提供了一個簡單的接口,同時後端 MySQL 節點可以向外擴展(甚至脱機),且不會對客户端產生任何影響。我們將 MySQL 命令行工具用作客户端,代表測試環境中的前端應用。
本文描述的許多功能都適用於 NGINX 開源版和 NGINX Plus。為簡單起見,我們全文只提 NGINX,並明確指出 NGINX 開源版不具備的功能。
我們將探討以下用例:
- TCP 負載均衡
- 高可用性和健康檢查
- 日誌記錄和診斷
- 併發寫入的考慮因素
TCP負載均衡
在為任何應用配置負載均衡之前,我們最好先了解應用是如何連接到數據庫的。我們的大多數測試都使用 mysql(1) 命令行工具連接 Galera 集羣、運行查詢,然後關閉連接。然而,許多應用框架都使用連接池來最大限度地減少延遲並高效利用數據庫服務器資源。
TCP 負載均衡在 stream 配置上下文中進行配置,因此我們在 nginx.conf 主文件中添加了一個 stream 塊來創建基本的 MySQL 負載均衡配置。
stream {
include stream.conf;
}
這可以將我們的 TCP 負載均衡配置與主配置文件分隔開來。然後我們在與 nginx.conf 相同的目錄下創建 stream.conf。請注意,默認情況下,conf.d 目錄是留給 http 配置上下文的,因此無法向該目錄添加 stream 配置文件。
upstream galera_cluster {
server 127.0.0.1:33061; # node1
server 127.0.0.1:33062; # node2
server 127.0.0.1:33063; # node3
zone tcp_mem 64k;
}
server {
listen 3306; # MySQL default
proxy_pass galera_cluster;
}
首先,我們定義一個名為 galera_cluster 的上游組,其中包含 Galera 集羣中的三個 MySQL 節點。在我們的測試環境中,它們都支持在本地主機上通過唯一的端口號進行訪問。zone 指令定義了所有 NGINX worker 進程共享的內存容量,以維持負載均衡狀態。Server{} 塊配置了 NGINX 處理客户端的方式。NGINX 監聽 MySQL 默認端口 3306,並將所有流量轉發到在上游塊中定義的 Galera 集羣。
要測試此基本配置是否正常工作,我們可以使用 MySQL 客户端返回我們所連接的 Galera 集羣中的節點的主機名。
$ echo "SHOW VARIABLES WHERE Variable_name = 'hostname'" | mysql --protocol=tcp --user=nginx --password=plus -N 2> /dev/null
hostname node1
要檢查負載均衡是否正常工作,我們可以重複執行以下命令。
$ !!;!!;!!
hostname node2
hostname node3
hostname node1
這表明默認的輪詢負載均衡算法運行正確。然而,如果我們的應用使用連接池訪問數據庫(如上文所述),那麼以輪詢方式打開集羣連接可能會導致每個節點上的連接數量不均衡。此外,我們不能將連接視為給定的工作負載,因為連接可能處於空閒狀態(等待應用查詢)或正在處理查詢。因此,對於 TCP 長連接,更合適的負載均衡算法是 Least Connections,配置 least_conn 指令:
upstream galera_cluster {
server 127.0.0.1:33061; # node1
server 127.0.0.1:33062; # node2
server 127.0.0.1:33063; # node3
zone tcp_mem 64k;
least_conn;
}
現在,當客户端打開到數據庫的新連接時,NGINX 會選擇當前連接數最少的集羣節點。
高可用性和健康檢查
跨集羣共享數據庫工作負載的一大優勢在於它還可提供高可用性。進行上述配置後,NGINX 將服務器標記為“不可用”,如果無法建立新的 TCP 連接則停止向其發送 TCP 數據包。
除了以這種方式處理不可用的服務器之外,NGINX 還可以配置為執行自動、主動的健康檢查,以便在發送客户端請求之前就檢測到不可用的服務器。此外,我們還可以使用應用級別的健康檢查來測試服務器的可用性,也就是説我們可以向每個服務器發送請求,然後從得到的響應來看服務器是否運行狀況良好。這將使我們的配置擴展如下。
upstream galera_cluster {
server 127.0.0.1:33061; # node1
server 127.0.0.1:33062; # node2
server 127.0.0.1:33063; # node3
zone tcp_mem 64k;
least_conn;
}
match mysql_handshake {
send x00;
expect ~* x00x00; # NullNull "filler" in handshake response packet
}
server {
listen 3306; # MySQL default
proxy_pass galera_cluster;
proxy_timeout 2s;
health_check match=mysql_handshake interval=20 fails=1 passes=2;
}
在此示例中,match 塊定義了啓動 MySQL 協議版本 10 握手所需的請求和響應數據。server{} 塊中的 health_check 指令應用此模式,並確保 NGINX 僅將 MySQL 連接轉發給實際能夠接受新連接的服務器。在這種情況下,我們每 20 秒執行一次健康檢查,每發生一次故障便從 TCP 負載均衡池中排除一個服務器,並在連續兩次成功的健康檢查後恢復負載均衡。
日誌記錄和診斷
NGINX 支持靈活地記錄日誌,因此它所有的 TCP/UDP 處理進程都可以被記錄下來,以便進行調試或離線分析。對於 TCP 協議(例如 MySQL),NGINX 會在連接關閉時寫入日誌條目。log_format 指令定義了該日誌中出現的值。我們可以從 Stream 模塊中的任何變量中進行選擇。我們在 stream.conf 文件頂部的 stream 上下文中定義了日誌格式。
log_format mysql '$remote_addr [$time_local] $protocol $status $bytes_received '
'$bytes_sent $upstream_addr $upstream_connect_time '
'$upstream_first_byte_time $upstream_session_time $session_time';
在 server{} 塊中添加 access_log 指令以啓用日誌記錄,並指定日誌文件的路徑以及上個代碼段中定義的日誌格式的名稱。
server {
# ...
access_log /var/log/nginx/galera_access.log mysql;
}
這會生成日誌條目,示例見下。
$ tail -3 /var/log/nginx/galera_access.log
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 1611 127.0.0.1:33063 0.000 0.003 12.614 12.614
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 8337 127.0.0.1:33061 0.001 0.001 11.181 11.181
192.168.91.1 [23/Jul/2021:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460
藉助 NGINX JavaScript 模塊實現高級日誌記錄
(編者按——NGINX JavaScript 模塊的用例有很多,以下用例只是其中之一。
如下所示,這部分的代碼已更新,以反映 NGINX JavaScript 的實現自博客最初發布以來的變化。
- 使用 NGINX JavaScript 0.2.4 中引入的 Stream 模塊的重構會話對象
- 使用 NGINX JavaScript 0.4.0 中引入的 js_import 指令。在 NGINX R23及以後版本中,它取代了已棄用的js_include指令。 要了解更多信息,請參閲 NGINX JavaScript 模塊的參考文檔-配置示例部分顯示了 NGINX 配置和 JavaScript 文件的正確語法。)
在 TCP/UDP 負載均衡的 Stream 模塊中,NGINX JavaScript 支持訪問請求和響應包的內容。這意味着我們可以檢查與 SQL 查詢對應的客户端請求,並提取有用元素(例如 SQL 方法:比如 SELECT 和UPDATE)。然後,NGINX JavaScript 可以將此類值設為常規 NGINX 變量。在下面的示例中,我們在 /etc/nginx/sql_method.js 中插入了我們的 JavaScript 代碼。
var method = "-"; // Global variable
var client_messages = 0;
function getSqlMethod(s) {
s.on('upload', function (data, flags) {
client_messages++;
if ( client_messages == 3 ) { // SQL query appears in 3rd client packet
var query_text = data.substr(1,10).toUpperCase();
var methods = ["SELECT", "UPDATE""INSERT""SHOW""CREATE""DROP"];
var i = 0;
for (; i < methods.length; i++ ) {
if ( query_text.search(methods[i]) > 0 ) {
s.log("SQL method: " + methods[i]);// To error_log [info]
method = methods[i];
s.allow(); // Stop searching
}
}
}
s.allow();
});
}
unction setSqlMethod() {
return method;
}
我們向 getSqlMethod() 函數傳遞了一個表示當前數據包的 JavaScript 對象。該對象的屬性(例如 fromUpstream 和 buffer)為我們提供了我們需要的關於數據包及其上下文的信息。
我們首先檢查 TCP 數據包是否來自客户端,因為我們不需要檢查來自上游 MySQL 服務器的數據包。此處,我們感興趣的是第三個客户端數據包,因為前兩個數據包包含了握手和身份驗證信息。第三個客户端數據包包含了 SQL 查詢。然後,將此字符串的開頭與 methods 數組中定義的其中一個 SQL 方法進行比較。當我們發現一個匹配時,我們將結果存儲在全局變量 $method 中並在錯誤日誌裏寫入一個條目。NGINX JavaScript 日誌記錄被寫入錯誤日誌中關於嚴重性的 info,因此默認情況下不會出現。
當對同名的 NGINX 變量求值時,會調用 setSqlMethod() 函數。在這種情況下,變量由 NGINX JavaScript 全局變量 $method(通過調用 getSqlMethod() 函數獲得)填充。
請注意,該 NGINX JavaScript 代碼專為 MySQL 命令行客户端設計,用於執行單個查詢。它不能準確捕獲複雜的查詢或者長期連接上的多個查詢 —— 儘管代碼可以適應這些用例。
為了在日誌中加入 SQL 方法,我們在 log_format 指令中添加了 $sql_method 變量。
log_format mysql '$remote_addr [$time_local] $protocol $status $bytes_received '
'$bytes_sent $upstream_addr $upstream_connect_time '
'$upstream_first_byte_time $upstream_session_time $session_time '
'$sql_method'; # Set by NGINX JavaScript
我們還需要擴展我們的配置,以告知 NGINX 如何以及何時執行 NGINX JavaScript 代碼。
js_import /etc/nginx/sql_method.js;
js_set $sql_method sql_method.setSqlMethod;
server {
# ...
js_filter getSqlMethod;
error_log /var/log/nginx/galera_error.log info; #For NGINX JavaScript s.log() calls
access_log /var/log/nginx/galera_access.log mysql;
}
首先,我們使用 js_import 指令指定 NGINX JavaScript 代碼的位置,並使用 js_set 指令告知 NGINX 在需要計算 $sql_method 變量時調用 setSqlMethod() 函數。在 server{} 塊中,我們使用 js_filter 指令指定每次處理數據包時調用的函數。我們還可以為 error_log 指令添加 info 選項,以啓用 NGINX JavaScript 日誌記錄。
添加這些附加配置後,我們的訪問日誌現在如下所示。
$ tail -3 /var/log/nginx/galera_access.log
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 1611 127.0.0.1:33063 0.000 0.003 12.614 12.614 UPDATE
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 8337 127.0.0.1:33061 0.001 0.001 11.181 11.181 SELECT
192.168.91.1 [23/Jul/2021:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460 UPDATE
NGINX Plus 儀表盤
(編者按 – 本節進行了更新,改為引用 NGINX Plus API,它取代並棄用了本文最初討論的單獨的擴展 Status 模塊。)
除了詳細記錄 MySQL 的活動,我們還可以通過 NGINX Plus 實時活動監控儀表盤觀察實時指標和上游 MySQL 服務器的健康狀況(NGINX 開源版提供了一組比較微觀的指標,且僅通過 Stub Status API 提供)。
NGINX Plus 儀表盤從 NGINX Plus R7 開始被引入,它提供了 NGINX Plus API 的 Web 接口。為了實現這一點,我們在一個單獨的 /etc/nginx/conf.d/dashboard.conf 文件中的 http 上下文中添加了一個新的 server{} 塊:
server {
listen 8080;
location /api { # Enable NGINX Plus API
write=on;
}
location = /dashboard.html {
root /usr/share/nginx/html;
}
# Redirect requests made to the old dashboard
location = /status.html {
return 301 /dashboard.html;
}
#deny all; # Protect from remote access in production
#allow 192.168.0.0/16; # Allow access from private networks only
}
我們還必須使用 status_zone 指令更新 stream.conf 中的 server{} 塊,以便為 MySQL 服務收集監控數據。
server {
# ...
status_zone galera_cluster;
}
進行此配置後,NGINX Plus 儀表盤就可以在端口 8080 上使用了。在下面的屏幕截圖中,我們可以看到三個 MySQL 服務器,每個服務器都顯示了許多進行中連接的詳細信息和當前的健康狀況。可以看出,監聽 33062 端口的節點以前曾短暫中斷過 18.97 秒(在 DT 列列出)。
NGINX Plus 實時活動監控儀表盤支持您跟蹤 MySQL 服務器的健康狀況。
併發寫入的考慮因素
Galera 集羣將每個 MySQL 服務器節點呈現為執行讀寫操作的主數據庫。許多應用的讀寫比都非常高,因此與來自多主數據庫集羣的靈活性相比,同時由多個客户端更新的相同錶行的風險完全可以接受。在併發寫入風險較高的情況下,我們提供了兩個選項。
- 創建兩個單獨的上游組,一個用於讀取,另一個用於寫入,且每個上游組都監聽不同的端口。將集羣中的一個或多個節點用於寫入,其中所有節點都包含在讀取組中。必須更新客户端代碼,以便為讀寫操作選擇適當的端口。
- 使用一個上游組,並修改客户端代碼以檢測寫入錯誤。當檢測到寫入錯誤時,代碼在併發結束後、再次嘗試之前呈指數級遞減。
結語
我們在本文中探討了負載均衡 TCP(或 UDP)應用(例如 MySQL)的幾個基本方面。NGINX 提供了一個功能齊全的 TCP/UDP 負載均衡器,無論流量類型如何,都可幫助您交付具有出色性能、可靠性、安全性及可擴展性的應用。
附錄:創建測試環境
測試環境是安裝在虛擬機上的,方便隔離和重複使用。但這並不代表不能將其安裝到物理“裸機”服務器上。
安裝 NGINX Plus
請參閲“NGINX Plus 管理指南”。
為 MySQL 安裝 Galera 集羣
在此示例中,我們使用每個節點的 Docker 容器在單個主機上安裝 Galera 集羣。以下操作説明改編自“通過 Docker 開啓 Galera 入門之旅”,並假設 Docker 引擎和 MySQL 命令行工具都已安裝。
-
創建一個基本的 MySQL 配置文件 (my.cnf),並由 Docker 鏡像複製到每個 Galera 容器中。
[mysqld] user = mysql bind-address = 0.0.0.0 wsrep_provider = /usr/lib/galera/libgalera_smm.so wsrep_sst_method = rsync default_storage_engine = innodb binlog_format = row innodb_autoinc_lock_mode = 2 innodb_flush_log_at_trx_commit = 0 query_cache_size = 0 query_cache_type = 0 -
拉取 Galera 的基本 Docker 鏡像。
$ sudo docker pull erkules/galera:basic -
創建第一個 Galera 節點 (node1),並將默認的 MySQL 端口顯示為 33061。
$ sudo docker run -p 33061:3306 --detach=true --name node1 -h node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm:// -
創建第二個 Galera 節點 (node2)。將 MySQL 端口顯示為 33062,並鏈接到 node1,用於集羣間通信。
$ sudo docker run -p 33062:3306 --detach=true --name node2 -h node2 --link node1:node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm://node1 -
使用與 node2 相同的方式創建第三個、也是最後一個 Galera 節點。將 MySQL 端口顯示為 33063。
$ sudo docker run -p 33063:3306 --detach=true --name node3 -h node3 --link node1:node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm://node1 -
創建一個名為 nginx 的用户賬户,用於從主機遠程訪問集羣。這是通過運行 Docker 容器內的 mysql(1) 命令來執行的。
$ sudo docker exec -ti node1 mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'nginx'@'172.17.0.1' IDENTIFIED BY 'plus'" -
使用 TCP 協議驗證您是否可以從主機連接到 Galera 集羣。
$ mysql --protocol=tcp -P 33061 --user=nginx --password=plus -e "SHOW DATABASES" mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | +--------------------+ -
最後,對另一個集羣節點運行相同的命令,以確認 nginx 用户賬户已被複制,且集羣運行正常。
$ mysql --protocol=tcp -P 33062 --user=nginx --password=plus -e "SHOW DATABASES" mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | +--------------------+
更多資源
想要更及時全面地獲取 NGINX 相關的技術乾貨、互動問答、系列課程、活動資源?
請前往 NGINX 開源社區:
官網:https://www.nginx.org.cn/
微信公眾號:https://mp.weixin.qq.com/s/XV...
微信羣:https://www.nginx.org.cn/stat...
B 站:https://space.bilibili.com/62...