藉助 TCP 負載均衡和 Galera 叢集擴充套件 MySQL

NGINX開源社群發表於2022-11-23
原文作者: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負載均衡

在為任何應用配置負載均衡之前,我們最好先了解應用是如何連線到資料庫的。我們的大多數測試都使用 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 伺服器節點呈現為執行讀寫操作的主資料庫。許多應用的讀寫比都非常高,因此與來自多主資料庫叢集的靈活性相比,同時由多個客戶端更新的相同錶行的風險完全可以接受。在併發寫入風險較高的情況下,我們提供了兩個選項。

  1. 建立兩個單獨的上游組,一個用於讀取,另一個用於寫入,且每個上游組都監聽不同的埠。將叢集中的一個或多個節點用於寫入,其中所有節點都包含在讀取組中。必須更新客戶端程式碼,以便為讀寫操作選擇適當的埠。
  2. 使用一個上游組,並修改客戶端程式碼以檢測寫入錯誤。當檢測到寫入錯誤時,程式碼在併發結束後、再次嘗試之前呈指數級遞減。

結語

我們在本文中探討了負載均衡 TCP(或 UDP)應用(例如 MySQL)的幾個基本方面。NGINX 提供了一個功能齊全的 TCP/UDP 負載均衡器,無論流量型別如何,都可幫助您交付具有出色效能、可靠性、安全性及可擴充套件性的應用。


附錄:建立測試環境

測試環境是安裝在虛擬機器上的,方便隔離和重複使用。但這並不代表不能將其安裝到物理“裸機”伺服器上。

安裝 NGINX Plus

請參閱“NGINX Plus 管理指南”。

為 MySQL 安裝 Galera 叢集

在此示例中,我們使用每個節點的 Docker 容器在單個主機上安裝 Galera 叢集。以下操作說明改編自“透過 Docker 開啟 Galera 入門之旅”,並假設 Docker 引擎MySQL 命令列工具都已安裝。

  1. 建立一個基本的 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
  2. 拉取 Galera 的基本 Docker 映象。

    $ sudo docker pull erkules/galera:basic
  3. 建立第一個 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://
  4. 建立第二個 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
  5. 使用與 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
  6. 建立一個名為 nginx 的使用者賬戶,用於從主機遠端訪問叢集。這是透過執行 Docker 容器內的 mysql(1) 命令來執行的。

    $ sudo docker exec -ti node1 mysql -e
    "GRANT ALL PRIVILEGES ON *.* TO 'nginx'@'172.17.0.1' IDENTIFIED BY 'plus'"
  7. 使用 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 |
    +--------------------+
  8. 最後,對另一個叢集節點執行相同的命令,以確認 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...

相關文章