TCP 로드 밸런싱 및 Galera로 MySQL 스케일링

NGINX Plus R5부터 TCP 로드 밸런싱을 도입하였고, 이후 릴리스에서 지속해 기능을 추가하였습니다. 또한 UDP 로드 밸런싱 기능도 제공하고 있습니다. 이 글에서는 TCP 로드 밸런싱의 핵심 요구사항과 NGINX Plus가 이를 어떻게 해결하는지 살펴보겠습니다.

NGINX Plus의 기능을 살펴보기 위해 데이터베이스 백엔드를 스케일링한 애플리케이션의 핵심 구성 요소를 나타내는 간단한 테스트 환경을 사용하겠습니다. 테스트 환경 구축에 대한 자세한 안내는 테스트 환경 구축을 참조하세요.

로드 밸런싱 MySQL 서버를 위한 테스트 환경은 MySQL 클라이언트와 Galera 클러스터 사이에 NGINX Plus를 배치합니다.

로드 밸런싱 MySQL 노드를 위한 테스트 환경

이 환경에서 NGINX Plus는 데이터베이스 서버의 리버스 프록시로 작동하며, 기본 MySQL 포트인 3306에서 수신합니다. 이를 통해 클라이언트에 간단한 인터페이스를 제공하면서, 백엔드 MySQL 노드는 클라이언트에 영향을 주지 않고 확장되거나 오프라인으로 전환될 수 있습니다. 테스트 환경에서 프론트엔드 애플리케이션을 나타내는 MySQL command-line tool 을 클라이언트로 사용합니다.

이 포스트에서 설명하는 많은 기능은 NGINX 오픈소스와 NGINX Plus 모두에 적용됩니다. 간결함을 위해 NGINX Plus를 전반적으로 언급하며, NGINX 오픈소스에서 사용할 수 없는 기능은 명시적으로 언급하도록 하겠습니다.

목차

1. TCP Load Balancing
2. High Availability와 Health Checks
3. Logging과 Diagnostics
3-1. NGINX JavaScript 모듈을 사용한 고급 로깅
3-2. NGINX Plus 대시보드
4. 동시 쓰기에 대한 고려 사항
5. 테스트 환경 구축
5-1. NGINX Plus 설치
5-2. MySQL용 Galera Cluster 설치
6. 요약

1. TCP Load Balancing

어떤 애플리케이션에 대한 로드 밸런싱을 구성하기 전에 데이터베이스와 어떻게 연결되는지 이해하는 것이 중요합니다. 대부분의 테스트에서는 MySQL command-line tool인 mysql(1)을 사용하여 Galera Cluster에 연결하고, 쿼리를 실행한 다음 연결을 종료합니다. 그러나 많은 애플리케이션 프레임워크에서는 데이터베이스 서버 자원을 효율적으로 활용하고 지연 시간을 최소화하기 위해 연결 풀을 사용합니다.

TCP 로드 밸런싱은 스트림 설정 컨텍스트에서 구성되므로, 기본 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 기본
    proxy_pass galera_cluster;
}

먼저 Galera Cluster에서 사용하는 세 개의 노드가 포함된 galera_cluster라는 upstream 그룹을 정의합니다. 테스트 환경에서는 각 노드가 고유한 포트 번호로 localhost에서 접근 가능합니다. zone 지시문은 로드 밸런싱 상태를 유지하기 위해 NGINX Plus의 모든 worker process 간에 공유되는 메모리의 양을 정의합니다. server{} 블록은 NGINX Plus가 클라이언트와 상호작용하는 방법을 구성합니다. NGINX Plus는 기본 MySQL 포트인 3306에서 수신하고, 모든 트래픽을 upstream 블록에서 정의한 Galera Cluster로 전달합니다.

이 기본 구성이 작동하는지 테스트하기 위해 MySQL 클라이언트를 사용하여 연결된 Galera Cluster에 있는 노드의 호스트명을 반환할 수 있습니다.

$ 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_conn 지시문을 사용하여 구성되는 최소 연결(Least Connections)입니다.

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 Plus는 현재 연결 수가 가장 적은 클러스터 노드를 선택합니다.

2. High Availability와 Health Checks

클러스터 전체에서 데이터베이스 작업량을 공유할 때의 가장 큰 장점은 high availability를 제공한다는 것입니다. 위에서 논의한 구성으로 인해, NGINX Plus는 새로운 TCP 연결을 설정할 수 없을 때 서버를 “down” 상태로 표시하고 TCP 패킷을 보내지 않습니다.

이러한 방식으로 다운된 서버를 처리하는 것 외에도, NGINX Plus는 자동으로 미리 사전 health checks를 수행하도록 구성할 수 있어 클라이언트 요청이 해당 서버로 전송되기 전에 사용할 수 없는 서버를 감지할 수 있습니다(이는 NGINX Plus 전용 기능입니다). 또한, 서버의 가용성은 애플리케이션 수준의 상태 확인을 통해 테스트할 수 있습니다. 즉, 각 서버에 요청을 보내고 양호한 상태를 나타내는 응답을 받는지 확인할 수 있습니다. 이에 따라 우리의 구성은 다음과 같이 확장됩니다.

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 ~* \x00\x00; # 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 Plus가 실제로 새로운 연결을 수락할 수 있는 서버에만 MySQL 연결을 전달하도록 합니다. 이 경우, 우리는 20초마다 health check를 수행하며, 단일 실패 후에는 서버를 TCP 로드 밸런싱 풀에서 제외하고, 2개의 연속된 성공적인 health checks 확인 후에는 다시 로드 밸런싱에 포함시킵니다.

3. Logging과 Diagnostics

NGINX Plus는 모든 TCP/UDP 처리 내용을 디버깅이나 오프라인 분석을 위해 기록할 수 있는 유연한 로깅 기능을 제공합니다. MySQL과 같은 TCP 프로토콜의 경우, NGINX Plus는 연결이 종료될 때 로그 항목을 작성합니다. log_format 지시문은 로그에 나타날 값들을 정의합니다. stream 모듈에서 사용 가능한 변수 중에서 선택할 수 있습니다. 우리는 log 형식을 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

3-1. NGINX JavaScript 모듈을 사용한 고급 로깅

NGINX JavaScript는 “NGINX native” 프로그래밍 구성 언어입니다. 이는 NGINX 및 NGINX Plus를 위한 독특한 JavaScript 구현체로, 서버 측 사용 사례와 각 요청에 대한 처리를 위해 특별히 설계되었습니다.

[참조 – 다음 사용 사례는 NGINX JavaScript 모듈의 많은 사용 사례 중 하나에 불과합니다. 모든 사용 사례 목록은 NGINX JavaScript 모듈의 사용 사례를 참조하십시오.]

NGINX JavaScript를 사용하면 TCP/UDP 로드 밸런싱을 위한 Stream 모듈 내에서 요청 및 응답 패킷의 내용에 접근할 수 있습니다. 이는 SQL 쿼리에 해당하는 클라이언트 요청을 검사하고 SELECT 또는 UPDATE와 같은 SQL 메서드와 같은 유용한 요소를 추출할 수 있다는 것을 의미합니다. NGINX JavaScript는 이러한 값을 일반 NGINX 변수로 사용할 수 있도록 만들어줍니다. 이 예시에서는 JavaScript 코드를 /etc/nginx/sql_method.js에 넣습니다.

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();
    });
}

function setSqlMethod() {
    return method;
}

getSqlMethod() 함수에는 현재 패킷을 나타내는 JavaScript 객체가 전달됩니다. fromUpstream 및 버퍼와 같은 이 객체의 속성은 패킷 및 해당 컨텍스트에 대해 필요한 정보를 제공합니다.

먼저, 우리는 패킷이 클라이언트로부터 온 것인지 확인합니다. 상위 MySQL 서버로부터 온 패킷을 검사할 필요가 없기 때문입니다. 여기서는 세 번째 클라이언트 패킷에 관심이 있습니다. 처음 두 개의 패킷에는 핸드셰이크 및 인증 정보가 포함되어 있습니다. 세 번째 클라이언트 패킷에는 SQL 쿼리가 포함되어 있습니다. 이 문자열의 시작 부분은 methods 배열에 정의된 SQL 메서드 중 하나와 비교됩니다. 일치하는 항목을 찾으면 결과의 전역 변수 $method에 저장하고 Error Log에 항목을 작성합니다. NGINX JavaScript logging은 info 수준의 error log에 작성되므로 기본적으로 표시되지 않습니다.

setSqlMethod() 함수는 동일한 이름의 NGINX 변수가 평가될 때 호출됩니다. 이때, 해당 변수는 getSqlMethod() 함수 호출을 통해 얻은 NGINX JavaScript 전역 변수 $method로 채워집니다.

이 NGINX JavaScript 코드는 단일 쿼리가 실행되는 MySQL command-line 클라이언트를 대상으로 설계되었습니다. 복잡한 쿼리나 장기적으로 지속되는 연결에서 여러 쿼리를 정확하게 캡처하지는 못하지만, 해당 코드를 해당 사용 사례에 맞게 수정할 수 있습니다. NGINX JavaScript 모듈을 설치하고 활성화하기 위한 지침은 NGINX Plus 가이드를 참조하십시오.

로그에 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 JavaScript 코드를 실행하는 방법과 시기를 NGINX Plus에 알리도록 구성을 확장해야 합니다.

js_import  /etc/nginx/sql_method.js;
js_set     $sql_method sql_method.setSqlMethod;

server {
    # ...
    js_filter  sql_method.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 Plus에게 $sql_method 변수를 평가해야 할 때 setSqlMethod() 함수를 호출하도록 지시합니다. 그런 다음, server{} 블록 내에서 js_filter 지시문을 사용하여 각 패킷이 처리될 때마다 호출될 함수를 지정합니다. 선택적으로 error_log 지시문을 추가하여 info 옵션을 사용하여 NGINX JavaScript logging을 활성화할 수도 있습니다.

이 추가 구성을 적용하면 이제 Access Log가 다음과 같이 표시됩니다.

$ 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

3-2. NGINX Plus 대시보드

[참고 – 이 섹션은 NGINX Plus API를 참조하도록 업데이트되었습니다. 이 API는 원래 여기에서 논의된 별도의 확장된 상태 모듈을 대체하고 더 이상 사용하지 않습니다.]

MySQL의 활동을 자세히 기록하는 것 외에도, NGINX Plus live activity 모니터링 대시보드에서 실시간 지표와 상위 MySQL 서버의 상태를 관찰할 수 있습니다 (NGINX 오픈 소스는 더 적은 지표를 제공하며 Stub Status API를 통해서만 확인할 수 있습니다).

NGINX Plus 대시보드는 NGINX Plus R7부터 도입되었으며, NGINX Plus API에 대한 웹 인터페이스를 제공합니다. 이를 활성화하기 위해 별도의 /etc/nginx/conf.d/dashboard.conf 파일에서 http 컨텍스트에 새 server{} 블록을 추가하여 이를 활성화합니다.

server {
    listen 8080;
    location /api { # Enable JSON status 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
}

또한 MySQL 서비스에 대해 모니터링 데이터를 수집할 수 있도록 status_zone 지시문을 사용하여 stream.conf의 server{} 블록을 업데이트해야 합니다.

server {
    # ...
    status_zone galera_cluster;
}

이 구성이 완료되면 NGINX Plus 대시보드는 8080 포트에서 사용할 수 있습니다. 다음 스크린샷에서는 세 개의 MySQL 서버를 볼 수 있으며, 각 서버는 수많은 진행 중인 연결과 현재 상태에 대한 세부 정보를 보여줍니다. 33062 포트에서 수신 대기 중인 노드는 이전에 18.97초 동안 단기간 중단이 있었음을 확인할 수 있습니다(DT 열에 보고됨).

NGINX Plus 라이브 활동 모니터링 대시보드를 사용하면 MySQL 노드를 로드 밸런싱할 때 서버 상태를 추적할 수 있습니다.

4. 동시 쓰기에 대한 고려 사항

Galera Cluster는 각 MySQL 서버 노드를 읽기 및 쓰기를 수행하는 마스터 데이터베이스로 제공합니다. 많은 애플리케이션에서는 읽기와 쓰기의 비율이 매우 높아 동시에 여러 클라이언트가 동일한 행을 업데이트하는 위험이 연성에 비해 완전히 허용될 정도입니다. 동시 쓰기가 발생할 가능성이 높은 상황에서는 두 가지 옵션이 있습니다.

  1. 두 개의 개별 upstream 그룹을 생성합니다. 하나는 읽기용이고 다른 하나는 쓰기용이며 각각 다른 포트에서 수신 대기합니다. 읽기 그룹에 포함된 모든 노드를 쓰기 전용으로 지정합니다. 읽기 및 쓰기 작업에 적합한 포트를 선택하려면 클라이언트 코드를 업데이트해야 합니다. 이 접근 방식은 블로그의 NGINX Plus를 사용한 MySQL Load Balancing 에서 설명하며 많은 MySQL 서버 노드가 있는 고도로 확장된 환경을 선호합니다.
  2. 단일 upstream 그룹을 유지하고 쓰기 오류를 감지하도록 클라이언트 코드를 수정합니다. 쓰기 오류가 감지되면 코드는 동시성이 종료된 후 다시 시도하기 전에 기하급수적으로 백오프됩니다. 클러스터 노드를 쓰기 전용으로 지정하면 고가용성이 손상되는 소규모 클러스터를 선호합니다.

5. 테스트 환경 구축

테스트 환경은 격리되고 반복이 가능하도록 가상 머신에 설치합니다. 그러나 물리적인 “베어 메탈(bare metal)” 서버에 설치할 수 없는 이유는 없습니다.

5-1. NGINX Plus 설치

NGINX Plus 설치 가이드를 참조하세요.

5-2. MySQL용 Galera Cluster 설치

이 예시에서는 각 노드에 Docker 컨테이너를 사용하여 단일 호스트에 Galera Cluster를 설치합니다. 아래의 지침은 “Getting Started Galera with Docker” 가이드를 기반으로 하며, Docker EngineMySQL command-line tool이 이미 설치되어 있다고 가정합니다.

1. Docker 이미지로 각 Galera 컨테이너에 복사할 기본 MySQL 구성 파일(my.cnf)을 만듭니다.

[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 노드(node3)를 만듭니다. 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 Cluster에 연결할 수 있는지 확인합니다.

$ 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 |
+--------------------+

6. 요약

이 포스트에서는 MySQL과 같은 TCP/UDP 애플리케이션의 로드밸런싱에 대한 여러 핵심적인 측면을 살펴보았습니다. NGINX Plus는 트래픽의 종류에 관계없이 성능, 신뢰성, 보안 및 확장성을 갖춘 애플리케이션을 제공하기 위해 환전한 기능을 갖춘 TCP/UDP 로드 밸런서를 제공합니다.

NGINX Plus를 사용해 보려면 지금 무료 30일 평가판을 시작하거나 NGINX STORE에 연락하여 사용 사례에 대해 문의하십시오.

사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.