ShardingSphere 的分佈式數據庫負載均衡架構

Apache ShardingSphere 是一個分佈式數據庫生態系統, 可將任何數據庫轉換爲分佈式數據庫, 併爲其提供數據分片、彈性擴展、加密等功能。在本文中, 我演示瞭如何基於 ShardingSphere 構建分佈式數據庫負載均衡架構, 並探討了引入負載均衡的影響。

架構

ShardingSphere 分佈式數據庫負載均衡架構由兩個產品組成: ShardingSphere-JDBC 和 ShardingSphere-Proxy, 它們可以獨立部署或採用混合架構。以下是混合部署架構:

(Wu Weijie, CC BY-SA 4.0)

ShardingSphere-JDBC 負載均衡解決方案

ShardingSphere-JDBC 是一個輕量級 Java 框架, 在 JDBC 層提供額外的服務。ShardingSphere-JDBC 在應用程序執行數據庫操作之前添加計算操作。應用程序進程仍然通過數據庫驅動程序直接連接到數據庫。

因此, 使用 ShardingSphere-JDBC, 用戶不必擔心負載均衡問題, 而是可以專注於如何對應用程序進行負載均衡。

ShardingSphere-Proxy 負載均衡解決方案

ShardingSphere-Proxy 是一個透明的數據庫代理, 通過數據庫協議爲客戶端提供服務。以下是 ShardingSphere-Proxy 作爲獨立部署進程, 並在其上進行負載均衡:

(Wu Weijie, CC BY-SA 4.0)

負載均衡解決方案的關鍵點

ShardingSphere-Proxy 集羣負載均衡的關鍵點在於, 數據庫協議本身被設計爲有狀態的 (連接認證狀態、事務狀態、預備語句等)。

如果在 ShardingSphere-Proxy 之上的負載均衡無法理解數據庫協議, 您唯一的選擇是選擇一個四層負載均衡代理 ShardingSphere-Proxy 集羣。在這種情況下, 特定的代理實例維護客戶端與 ShardingSphere-Proxy 之間的數據庫連接狀態。

由於代理實例維護連接狀態, 四層負載均衡只能實現連接級別的負載均衡。同一數據庫連接的多個請求無法輪詢到多個代理實例。無法實現請求級別的負載均衡。

本文不涉及四層和七層負載均衡的細節。

應用層建議

理論上, 客戶端直接連接到單個 ShardingSphere-Proxy 或通過負載均衡門戶連接到 ShardingSphere-Proxy 集羣之間沒有功能上的差異。但在技術實現和不同負載均衡器的配置方面存在一些差異。

例如, 在直接連接 ShardingSphere-Proxy 且沒有限制數據庫連接會話的總保持時間的情況下, 某些彈性負載均衡 (ELB) 產品在第 4 層有 60 分鐘的最大會話保持時間。如果負載均衡超時導致空閒數據庫連接被關閉, 但客戶端沒有意識到被動 TCP 連接關閉, 應用程序可能會報告錯誤。

因此, 除了在負載均衡層面的考慮外, 您可能還需要考慮客戶端措施, 以避免引入負載均衡的影響。

按需創建連接

如果連接實例是持續創建和使用的, 當執行一小時間隔的定時任務且執行時間較短時, 數據庫連接大部分時間都處於空閒狀態。當客戶端本身不知道連接狀態的變化時, 長時間的空閒會增加連接狀態的不確定性。對於執行間隔較長的場景, 可以考慮按需創建連接並在使用後釋放。

連接池

一般的數據庫連接池具有維護有效連接、拒絕失敗連接等能力。通過連接池管理數據庫連接可以降低自行維護連接的成本。

啓用 TCP KeepAlive

客戶端通常支持 TCP KeepAlive 配置:

但是, 啓用 TCP KeepAlive 也有一些侷限性:

用戶案例

最近, ShardingSphere 社區成員反饋, 他們的 ShardingSphere-Proxy 集羣正在爲公衆提供服務, 並使用上層負載均衡。在此過程中, 他們發現應用程序與 ShardingSphere-Proxy 之間的連接穩定性存在問題。

問題描述

假設用戶的生產環境使用三節點 ShardingSphere-Proxy 集羣, 通過雲供應商的 ELB 爲應用程序提供服務。

(Wu Weijie, CC BY-SA 4.0)

其中一個應用程序是一個常駐進程, 執行定時任務, 每小時執行一次, 任務邏輯中包含數據庫操作。用戶反饋每次定時任務觸發時, 應用程序日誌中都會報告錯誤:

send of 115 bytes failed with errno=104 Connection reset by peer
檢查ShardingSphere-Proxy日誌,沒有發現異常信息。

該問題只發生在每小時執行的定時任務中, 其他應用程序正常訪問 ShardingSphere-Proxy。由於任務邏輯有重試機制, 每次重試後任務都能成功執行, 不影響原有業務。

問題分析

應用程序顯示錯誤的原因很明確 - 客戶端向已關閉的 TCP 連接發送數據。故障排查的目標是確定 TCP 連接被關閉的原因。

如果遇到以下三種情況之一, 我建議在問題發生前後的幾分鐘內, 對應用程序和 ShardingSphere-Proxy 端進行網絡數據包捕獲:

數據包捕獲現象 1

ShardingSphere-Proxy 每 15 秒接收到來自客戶端的 TCP 連接建立請求。但客戶端在與代理完成三次握手後立即向代理發送 RST。客戶端在收到服務器問候語之後或甚至在代理發送服務器問候語之前, 就向代理發送 RST, 而不做任何響應。

(Wu Weijie, CC BY-SA 4.0)

但是, 在應用程序端的數據包捕獲結果中, 沒有發現與上述行爲相匹配的流量。

經諮詢社區成員的 ELB 文檔, 發現上述網絡交互是該 ELB 實現四層健康檢查機制的方式。因此, 這種現象與本案例的問題無關。

(Wu Weijie, CC BY-SA 4.0)

數據包捕獲現象 2

MySQL 連接在客戶端和 ShardingSphere-Proxy 之間建立, 在 TCP 連接斷開階段, 客戶端向代理發送 RST。

(Wu Weijie, CC BY-SA 4.0)

上述數據包捕獲結果顯示, 客戶端首先向 ShardingSphere-Proxy 發送了 COM_QUIT命令。客戶端基於以下可能的場景主動斷開了 MySQL 連接:

經過多輪數據包分析, 在問題出現前後的幾分鐘內, ShardingSphere-Proxy 都沒有向客戶端發送過 RST。

根據現有信息, 可能是客戶端與 ShardingSphere-Proxy 之間的連接在更早前就斷開了, 但數據包捕獲時間有限, 沒有捕獲到斷開連接的時刻。

由於 ShardingSphere-Proxy 本身沒有主動斷開客戶端連接的邏輯, 因此問題正在客戶端和 ELB 層面進行排查。

客戶端應用程序和 ELB 配置檢查

用戶反饋中包含以下補充信息: 以下是對您提供的內容的中文翻譯:

考慮到定時任務的執行頻率, 我建議用戶將 ELB 會話空閒超時修改爲大於定時任務執行間隔。在用戶將 ELB 超時改爲 66 分鐘後, 連接重置問題就不再發生。

如果用戶在故障排查過程中繼續進行數據包捕獲, 很可能會發現 ELB 在每小時的第 40 分鐘斷開 TCP 連接的流量。

問題結論

客戶端報告了一個 "Connection reset by peer" 的錯誤。

ELB 空閒超時小於定時任務執行間隔。客戶端在 ELB 會話保持超時時間內保持空閒, 導致客戶端與 ShardingSphere-Proxy 之間的連接被 ELB 超時斷開。

客戶端向已被 ELB 關閉的 TCP 連接發送數據, 導致 "Connection reset by peer" 錯誤。

超時模擬實驗

我決定進行一個簡單的實驗來驗證客戶端在負載均衡會話超時後的性能。我在實驗過程中進行了數據包捕獲, 分析網絡流量並觀察負載均衡的行爲。

構建基於 Nginx 的負載均衡 ShardingSphere-Proxy 集羣環境

理論上, 這篇文章可以涵蓋任何四層負載均衡的實現。我選擇了 Nginx。

我將 TCP 會話空閒超時設置爲 1 分鐘, 如下所示:

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
stream {
    upstream shardingsphere {
        hash $remote_addr consistent;
        server proxy0:3307;
        server proxy1:3307;
    }
    server {
        listen 3306;
        proxy_timeout 1m;
        proxy_pass shardingsphere;
    }
}

構建 Docker Compose 文件

以下是一個 Docker Compose 文件:

version: "3.9"
services:
  nginx:
    image: nginx:1.22.0
    ports:
      - 3306:3306
    volumes:
      - /path/to/nginx.conf:/etc/nginx/nginx.conf
  proxy0:
    image: apache/shardingsphere-proxy:5.3.0
    hostname: proxy0
    ports:
      - 3307
  proxy1:
    image: apache/shardingsphere-proxy:5.3.0
    hostname: proxy1
    ports:
      - 3307

啓動環境

啓動容器:

 $ docker compose up -d 
[+] Running 4/4
 ⠿ Network lb_default     Created                                                                               0.0s
 ⠿ Container lb-proxy1-1  Started                                                                               0.5s
 ⠿ Container lb-proxy0-1  Started                                                                               0.6s
 ⠿ Container lb-nginx-1   Started

客戶端同一連接的定時任務模擬

首先, 構建一個客戶端延遲 SQL 執行。這裏, 通過 Java 訪問 ShardingSphere-Proxy, 並使用 MySQL Connector/J。

邏輯:

  1. 建立與 ShardingSphere-Proxy 的連接, 並執行一個查詢。

  2. 等待 55 秒, 然後執行另一個查詢。

  3. 等待 65 秒, 然後執行另一個查詢。

public static void main(String[] args) {
    try (Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306?useSSL=false", "root", "root"); Statement statement = connection.createStatement()) {
        log.info(getProxyVersion(statement));
        TimeUnit.SECONDS.sleep(55);
        log.info(getProxyVersion(statement));
        TimeUnit.SECONDS.sleep(65);
        log.info(getProxyVersion(statement));
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}
private static String getProxyVersion(Statement statement) throws SQLException {
    try (ResultSet resultSet = statement.executeQuery("select version()")) {
        if (resultSet.next()) {
            return resultSet.getString(1);
        }
    }
    throw new UnsupportedOperationException();
}

預期和客戶端運行結果:

  1. 客戶端連接到 ShardingSphere-Proxy, 第一個查詢成功。

  2. 客戶端的第二個查詢成功。

  3. 客戶端的第三個查詢由於 TCP 連接中斷而出錯, 因爲 Nginx 的空閒超時設置爲一分鐘。

執行結果符合預期。由於編程語言和數據庫驅動程序的差異, 錯誤消息的行爲有所不同, 但根本原因是相同的: 兩個 TCP 連接都已斷開。

日誌如下所示:

15:29:12.734 [main] INFO icu.wwj.hello.jdbc.ConnectToLBProxy - 5.7.22-ShardingSphere-Proxy 5.1.1
15:30:07.745 [main] INFO icu.wwj.hello.jdbc.ConnectToLBProxy - 5.7.22-ShardingSphere-Proxy 5.1.1
15:31:12.764 [main] ERROR icu.wwj.hello.jdbc.ConnectToLBProxy - Communications link failure
The last packet successfully received from the server was 65,016 milliseconds ago. The last packet sent successfully to the server was 65,024 milliseconds ago.
        at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
        at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1201)
        at icu.wwj.hello.jdbc.ConnectToLBProxy.getProxyVersion(ConnectToLBProxy.java:28)
        at icu.wwj.hello.jdbc.ConnectToLBProxy.main(ConnectToLBProxy.java:21)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
The last packet successfully received from the server was 65,016 milliseconds ago. The last packet sent successfully to the server was 65,024 milliseconds ago.
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
        at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:581)
        at com.mysql.cj.protocol.a.NativeProtocol.checkErrorMessage(NativeProtocol.java:761)
        at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:700)
        at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:1051)
        at com.mysql.cj.protocol.a.NativeProtocol.sendQueryString(NativeProtocol.java:997)
        at com.mysql.cj.NativeSession.execSQL(NativeSession.java:663)
        at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1169)
        ... 2 common frames omitted
Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
        at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeaderLocal(SimplePacketReader.java:81)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:63)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:45)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:52)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:41)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:54)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:44)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:575)
        ... 8 common frames omitted

數據包捕獲結果分析

數據包捕獲結果顯示, 在連接空閒超時後, Nginx 同時斷開了與客戶端和代理的 TCP 連接。但是, 客戶端並不知道這一點, 所以 Nginx 在發送命令後返回了 RST。

在 Nginx 連接空閒超時後, 與代理的 TCP 斷開連接過程正常完成。代理不知道客戶端使用已斷開的連接發送後續請求。

分析以下數據包捕獲結果:

(Wu Weijie, CC BY-SA 4.0)

總結

排查斷開連接問題需要檢查 ShardingSphere-Proxy 設置和雲服務提供商的 ELB 強制執行的配置。捕獲數據包有助於瞭解特定事件 (特別是 DST 消息) 發生的時間與空閒時間和超時設置的關係。

上述實現和故障排查場景基於特定的 ShardingSphere-Proxy 部署。有關雲端選項的討論, 請參閱我的後續文章。ShardingSphere on Cloud 爲各種雲服務提供商環境提供了額外的管理選項和配置。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vplNjC1UrW4RMJZZujWBqg