본문 바로가기

SNS Service

SNS 서비스 | 4. MySQL Master - Slave

API 서버는 단순히 트래픽을 처리할 수 있는 인프라를 증설하는 것으로 충분하지만, DB의 경우에는 모든 DB 서버가 동일한 데이터를 저장하고 조회 시에도 동일한 결과를 반환해야 하는 데이터 정합성이 핵심입니다.

이에 데이터가 저장되는 주체와, 저장한 데이터를 받아와 동기화시키는 Master- Slave 구조를 이용하기로 하였습니다. 쓰기 작업은 Master 서버에서 이루어지고, 읽기 작업은 Slave 서버에서 이루어지게 하여 부하를 효율적으로 분산시킬 수 있기 때문입니다.

 

복제 타입

복제 타입 중 바이너리 로그 파일 위치 기반 복제 방식은 복제에서 각각의 이벤트들이 바이너리 로그 파일명과 파일 내 위치 값의 조합으로 식별됩니다. 문제는 이 같은 식별이 바이너리 로그 파일이 저장돼 있는 마스터 서버에서만 유효하고, 동일한 이벤트가 슬레이브 서버에서도 동일한 파일명의 동일한 위치에 저장된다는 보장이 없기 때문에 서버마다 동일한 이벤트에 대해 서로 다른 식별 값을 갖게 되는 데이터 불일치 문제가 발생할 수 있습니다.

 

복제를 구성하는 서버들의 데이터 불일치 문제 때문에 복제 토폴로지 변경 작업이 어려워지고, 복제를 이용한 Failover가 어려워집니다. Orchestrator와 같은 고가용성 솔루션들은 바이너리 로그 파일 위치 계산을 포기해버리는 형태로 처리되기도 합니다.

 

 

Master 서버에서 발생한 각 이벤트들이 슬레이브 서버들에서 동일한 고유 식별값을 가지게 한다면, 복제 토폴로지 변경은 물론 FailOver에 소요되는 시간도 줄어들 것입니다. 이처럼 마스터 서버에서만 유효한 고유 식별 값이 아닌 복제에 참여한 전체 슬레이브 서버들에서 고유하도록 각 이벤트에 부여된 식별 값을 글로벌 트랜잭션 아이디(GTID)라고 하며, 이를 기반으로 복제가 진행되는 형태를 GTID 기반 복제라고 합니다.

GTID 기반 복제는 트랜잭션마다 고유 ID를 부여하여 데이터베이스 간 복제 상태를 정확하게 추적합니다. 마스터 서버 장애 발생으로 새로운 마스터 서버가 선정되었을 때, 각 슬레이브 서버는 자신이 어디까지 읽었는지 정확히 알고 있어 누락이나 중복 없이 동기화를 이어갈 수 있습니다. 이러한 명확한 트랜잭션 추적 기능은 자동 페일오버 도구인 Orchestrator와 결합했을 때 더욱 빛을 발했습니다. Orchestrator가 각 데이터베이스의 복제 상태를 정확하게 파악하고 장애 상황에서 자동으로 마스터 서버 승격 작업을 수행하여 복제 토폴로지 변경이 자유롭다는 장점이 있어 복제 타입으로 GTID 기반 복제 타입을 채택하였습니다.

 

 

복제 데이터 포맷

복제 데이터 포맷은 ROW 방식을 채택하였습니다. Statement 방식은 슬레이브 서버가 마스터 서버와 같은 쿼리를 실행해 처리하는 형태인 반면에 ROW 방식은 슬레이브 서버에 변경된 데이터 자체가 넘어가서 적용되므로, 동일한 데이터 변경일지라도 락을 더 적게 점유하고 처리 속도도 훨씬 빠릅니다. 같은 이유로 비확정적 쿼리에서도 마스터 서버와 동일한 결과를 보장하는 장점이 있습니다.

복제 동기화 방식

복제 동기화 방식으로서는 비동기 복제 방식을 택하였습니다. 반동기 복제 방식은 master 서버가 slave 서버로부터 트랜잭션 수신 확인을 받을 때까지 대기하여 데이터 정합성은 높일 수 있지만, 그만큼 master 서버의 성능이 저하될 수 있다는 단점이 있었습니다. 송금이나 결제 등 강력한 데이터 정합성을 요구하는 금융 서비스는 반동기 복제 방식이 적합할 수 있겠지만, SNS 서비스 특성상 실시간 상호작용이 빈번하게 발생하기 때문에 성능을 최우선으로 고려하여 비동기 방식을 선택하였습니다.

Orchestrator 

마스터 서버에 장애가 생길 경우 쓰기 작업을 수행하지 못하는 문제가 발생합니다. 이럴 경우 슬레이브 서버 중 하나가 마스터 서버로 승격하게 하여, 쓰기 작업을 대체하게 하는 구성이 필요합니다. 오픈소스이며 간편하게 구성할 수 있는 Orchestrator를 통하여 고가용성 솔루션을 구성하기로 하였습니다.

 

docker-compose

version: '3'

services:
  mysql-master:
    image: mysql:8.0
    container_name: mysql-master
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test_db
    ports:
      - "3306:3306"
    volumes:
      - ./master/config:/etc/mysql/conf.d
    cpuset: "0"
    deploy:
      resources:
        limits:
          memory: 2GB
    networks:
      - mybridge

  mysql-slave1:
    image: mysql:8.0
    container_name: mysql-slave1
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test_db
    ports:
      - "3307:3306"
    volumes:
      - ./slave1/config:/etc/mysql/conf.d
    cpuset: "1"
    deploy:
      resources:
        limits:
          memory: 2GB
    networks:
      - mybridge
  mysql-slave2:
    image: mysql:8.0
    container_name: mysql-slave2
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: test_db
    ports:
      - "3308:3306"
    volumes:
      - ./slave2/config:/etc/mysql/conf.d
    cpuset: "2"
    deploy:
      resources:
        limits:
          memory: 2GB
    networks:
      - mybridge

  orchestrator:
    image: openarkcode/orchestrator:latest
    container_name: orchestrator
    ports:
      - "3000:3000"
    networks:
      - mybridge

networks:
  mybridge:
    driver: bridge

 

my.cnf

# master의 MySQL 설정 예시
[mysqld]
log_bin                     = mysql-bin
binlog_format               = ROW
gtid_mode                   = ON
enforce-gtid-consistency    = true
server-id                   = 100
log_slave_updates
datadir                     = /var/lib/mysql
socket                      = /var/lib/mysql/mysql.sock

 

마스터 서버

CREATE USER 'repl'@'%' IDENTIFIED BY 'repl';

GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

FLUSH PRIVILEGES;

 

슬레이브 서버

CHANGE MASTER TO MASTER_HOST='mysql-master',

MASTER_USER='repl', MASTER_PASSWORD='repl',

MASTER_SSL=1,

MASTER_AUTO_POSITION=1;

 

START SLAVE;

SHOW SLAVE STATUS;

 

Orchestrator 구성

 

CREATE USER orc_client_user@'192.%' IDENTIFIED BY 'orc_client_password';

GRANT SUPER, PROCESS, REPLICATION SLAVE, RELOAD ON *.* TO orc_client_user@'192.%';

GRANT SELECT ON mysql.slave_master_info TO orc_client_user@'192.%';

 

 

마스터 서버를 포함하여 총 3대의 서버가 정상적으로 동작하고 있음을 알 수 있습니다. 이를 통해 앞서 설정한 내용이 제대로 적용되었으며, Orchestrator가 각 서버에 정상적으로 접속하여 상태 정보를 가져오고 있는 것을 확인할 수 있습니다.

 

다음은 Auto failover를 위한 기능 설정입니다.

 

  "RecoverMasterClusterFilters": [
    "*"
  ]

 

"RecoverMasterClusterFilters": ["*"]: 이 설정은 모든 클러스터에서 Master 서버 장애 복구를 시도하도록 합니다.

 

마스터 서버가 실행중인 docker 컨테이너를 임의로 중지한 결과, Failover가 정상적으로 수행되면서 Cluster는 mysql-slave1과 mysql-master로 분리되며,  mysql-slave2가 mysql-slave1의 슬레이브 서버로 설정 변경되는 모습을 볼 수 있습니다. 

DataSource 라우팅

AbstractRoutingDataSource을 상속해 커넥션 시 사용할 DataSource를 결정하는 방법과, 처음부터 master 서버와 slave 서버 DataSource를 분리하여 각각 해당하는 EntityManagerFactory, TransactionManager를 만들고, Repository 패키지를 분리하고 명시하여 사용하는 방법이 있습니다.

Repository 패키지 분리 방식은 컴파일 타임에 잘못된 DB 접근을 방지하고 코드만으로도 DB 접근 의도 파악이 명확하며 각 DB에 최적화된 설정을 적용할 수 있지만, Repository 코드 중복이 발생하고, 읽기 - 쓰기 DataSource 마다 EntityManagerFactory와 TransactionManager를 별도로 설정해야 했기에 빈 객체가 늘어나면서 메모리 사용량이 증가할 수 있었습니다.

반면 AbstractRoutingDataSource를 상속받고, TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 통해 DataSource를 결정하는 방법은 @Transactional을 명시해야 하므로 트랜잭션 속성 누락 시 의도치 않은 master 서버 DB 접근이 가능하고 코드만으로는 어떤 DB를 사용하는지 파악이 어려우며 런타임에서야 DB 접근 오류를 발견할 수 있다는 단점이 있었지만, 불필요한 코드 중복이 없고 적은 빈 객체를 관리하여 유지보수가 쉽고, @Transactional의 readonly 속성을 통한 JPA의 스냅샷 미생성, 더티 체킹 스킵으로 성능 개선 효과를 자연스럽게 활용할 수 있다는 장점이 명확하였습니다.

AbstractRoutingDataSource를 상속받아 ReplicationRoutingDataSource를 구현하고, AtomicInteger를 활용하여 Thread-Safe한 라운드 로빈 알고리즘으로 라우팅을 구현하였습니다. 또한, ReplicationRoutingDataSource를 매개변수로 사용하여 LazyConnectionDataSourceProxy를 최종 DataSource 객체로 반환함으로써 불필요한 커넥션 낭비를 방지하고, 시스템 자원을 효율적으로 관리할 수 있도록 설계했습니다.

서비스 클래스에 기본적으로 @Transactional 어노테이션을 설정하되, SELECT 쿼리를 사용하는 메서드에는 @Transactional(readonly = true) 속성을 부여하여 사용하는 DataSoruce를 분리하게 하였습니다.

 

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    private int slaveCount;
    private AtomicInteger counter = new AtomicInteger(0);


    public ReplicationRoutingDataSource(int slaveCount) {
        this.slaveCount = slaveCount;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

        if (isReadOnly && slaveCount > 0) {
            int idx = counter.getAndIncrement() % slaveCount;
            return "slave" + idx;
        }
        return "master";
    }


}

 

 

회원가입 요청을 보내면, 이렇게 복제가 정상적으로 성공하는 것을 볼 수 있습니다.

 

피드 조회 API 요청 시 슬레이브 서버 2대에게 피드 조회 쿼리가 정상적으로 라우팅 되는 것을 확인할 수 있습니다.

 

AbstractRoutingDataSource를 통한 트랜잭션 기반 DataSource 라우팅을 통해 쓰기 / 읽기 쿼리를 분산할 수 있었습니다. 이 방식은 애플리케이션 단에서 간단한 코드로 낮은 시스템 복잡도를 요구합니다. 하지만 다음과 같은 단점이 있습니다.

 

먼저 슬레이브 서버가 늘어남에 따라 총 커넥션 풀 사이즈가 무한히 커지는 문제가 있었습니다. 마스터 서버 1대, 슬레이브 서버 2대로도 API 서버는 기본 설정이라는 가정 하에 커넥션 풀을 30개 가지고 있으며, 슬레이브 서버가 늘어날 수록 커넥션 풀은 그만큼 증가하게 됩니다. 이는 앞선 게시글에서 확인했듯이 컨텍스트 스위칭으로 인한 오버헤드가 발생할 수 있는 문제를 가지고 있습니다.

 

 

SNS 피드 시스템 설계 | 3. 서버의 수평적 확장

테스트 환경docker의 리소스 제한을 이용해 cpuset을 각 2개로 분리하고 memory를 4GB 할당한 API 서버와 DB로 테스트를 진행했습니다.사용자 생성:테스트 주체 사용자 1,000명인플루언서 50명일반 사용자

durian-1.tistory.com

 

더불어 Auto failover를 통해 새로운 슬레이브 서버를 마스터 서버로 승격하게 해도 API 서버는 DB에 대해 커넥션 오류를 일으킵니다. 이는 승격된 슬레이브 서버가 더 이상 read-only 상태가 아니며 쓰기 요청을 처리할 수 있음에도 불구하고, AbstractRoutingDataSource의 DataSourceMap에서 master 키에 매핑된 값이 여전히 중단된 기존 마스터 서버의 DataSource를 가리키고 있었기 때문입니다.

 

기존 AbstractRoutingDataSource를 통한 쿼리 라우팅 방식은 자동 장애 복구 시 장애가 발생한 기존 마스터 서버, 마스터 서버로 승격한 기존 슬레이브 서버의 정보를 파악하여 targetDataSources와 DefaultTargetDataSource를 변경해 줘야 한다는 점과, 각 DB에 대한 DataSoruce가 독립된 커넥션 풀을 가지고 있어 DB가 늘어날 때 마다 커넥션 풀이 늘어나 API 서버의 부담이 무한히 증가할 수 있다는 문제가 있었습니다.

이는 애플리케이션이 DB의 가용 상태를 헬스 체크 또는 이벤트 리스너를 통해 지속해서 추적해야 하며, 장애나 복구 상황에 따라 추가적인 로직을 관리해야 하는 부담으로 이어졌습니다. 이로 인해 코드 작성이 늘어나면서 시스템 복잡도가 증가하고, 장애 상황에서 신속하고 안정적인 복구가 어렵다는 문제가 있었습니다.

API 서버와 DB 서버 사이 프록시 서버를 둬, API 서버는 DB를 동적으로 연결하지 않고, 프록시 서버만 바라보게 한 다음, 프록시 서버에게 DB 연결에 대한 책임을 위임하고자 하였습니다. 프록시 서버가 트랜잭션 속성을 파악하고, failover를 감지하여 적절한 DB에 연결하게 한다면 애플리케이션 레벨에서 복잡한 로직을 관리하지 않아도 되어, 시스템의 유연성을 크게 향상할 수 있다고 보았습니다.

단점으로서는 프록시 서버를 경유하기 때문에 추가적인 네트워크 통신이 발생하여, 요청-응답 간 네트워크 오버헤드가 우려되었습니다. 또한, 단순히 네트워크 오버헤드뿐만 아니라, 프록시가 트랜잭션을 분석하고 라우팅 방식을 결정하는 과정에서도 오버헤드가 발생할 수 있으며, 이는 곧 응답 시간의 저하로 이어질 가능성이 있었습니다. 프록시 서버의 단일 장애 지점을 극복하기 위해 이중화를 설계하고 구현하는 추가적인 인프라 고려 사항도 필요했습니다.

기존에는 가장 단순하게 구현할 수 있는 라운드 로빈 알고리즘을 라우팅 알고리즘으로 택하였지만, DB 서버의 상태를 고려하지 않고 무조건 순차적으로 분배하고 있어 부하의 불균형이 발생할 수 있는 문제가 있었고, 다른 알고리즘을 구현하기 위해선 추가적인 로직이 필요하였습니다.

이런 상황에선 프록시 서버의 단점보다 애플리케이션 레벨에서 동적 DB 라우팅과 복잡한 애플리케이션의 failover 로직을 직접 구현할 필요가 없어진다는 장점이 매우 매력적으로 다가왔습니다.

실제로 검색을 해보고 나니 ProxySQL이라는 오픈소스 프록시가 존재했고, 제 요구사항을 충족하는 다양한 기능이 있어 이용해 보고자 하였습니다.

프록시 서버를 통한 쿼리 라우팅

앞서 만든 docker-compose.yml 파일에 proxysql 서비스를 추가해줍니다.

proxysql:
  image: proxysql/proxysql
  ports:
    - "16032:6032"
    - "16033:6033"
  volumes:
    - ./proxysql/data:/var/lib/proxysql
    - ./proxysql/conf/proxysql.cnf:/etc/proxysql.cnf
  networks:
    - mybridge

 

proxysql.cnf

datadir="/var/lib/proxysql"
admin_variables=
{
    admin_credentials="admin:admin;radmin:radmin"
    mysql_ifaces="0.0.0.0:6032"
}
mysql_variables=
{
    threads=4
    max_connections=2048
    default_query_delay=0
    default_query_timeout=36000000
    have_compress=true
    poll_timeout=2000
    interfaces="0.0.0.0:6033"
    default_schema="information_schema"
    stacksize=1048576
    server_version="8.0.39"
    connect_timeout_server=3000
    monitor_username="monitor"
    monitor_password="monitor"
    monitor_history=600000
    monitor_connect_interval=60000
    monitor_ping_interval=10000
    monitor_read_only_interval=1500
    monitor_read_only_timeout=500
    ping_interval_server_msec=120000
    ping_timeout_server=500
    commands_stats=true
    handle_unknown_charset=1
    set_query_cache_size=false
    sessions_sort=true
    connect_retries_on_failure=10
}

 

마스터 서버 유저 생성

-- 애플리케이션 커넥션 유저

CREATE USER appuser@'%' IDENTIFIED BY 'apppass';

 

GRANT SELECT, INSERT, UPDATE, DELETE ON test_db.* TO appuser@'%';

 

-- ProxySQL 모니터링 유저

CREATE USER 'monitor'@'%' IDENTIFIED BY 'monitor';

 

GRANT REPLICATION CLIENT ON *.* TO 'monitor'@'%';

 

FLUSH PRIVILEGES;

 

ProxySQL 서버 설정

-- Write 호스트 그룹

INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (10, 'mysql-master', 3306);

 

-- Read 호스트 그룹

INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'mysql-slave1', 3306);

INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'mysql-slave2', 3306);

 

-- Replication 관련 설정

INSERT INTO mysql_replication_hostgroups VALUES (10, 20, 'read_only', '');

 

LOAD MYSQL SERVERS TO RUNTIME;

SAVE MYSQL SERVERS TO DISK;

 

-- 어플리케이션 유저 정보 입력

INSERT INTO mysql_users(username, password, default_hostgroup, transaction_persistent)

VALUES ('appuser', 'apppass', 10, 0);

 

LOAD MYSQL USERS TO RUNTIME;

SAVE MYSQL USERS TO DISK;

 

-- 쿼리 룰 정보 입력

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup)

VALUES (1, 1, '^SELECT.*FOR UPDATE$', 10);

 

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup)

VALUES (2, 1, '^SELECT', 20);

 

LOAD MYSQL QUERY RULES TO RUNTIME;

SAVE MYSQL QUERY RULES TO DISK;

SELECT  *  FROM   mysql_servers;

SELECT  *  FROM   mysql_users;
SELECT   rule_id ,  active ,  match_pattern ,  destination_hostgroup ,  apply   FROM   mysql_query_rules;

 

mysql_query.sh

#!/bin/bash
while true;
do
  mysql -uappuser -papppass -h219.255.60.149 -P16033 -N -e "SELECT @@hostname, NOW()" 2>&1 | grep -v "Warning"
  sleep 1
done

 

 

슬레이브 서버를 통해 SELECT 쿼리가 라우팅 되는 것을 확인할 수 있습니다.

 

mysql_insert_query.sh

#!/bin/bash
while true;
do
  mysql -uappuser -papppass -h219.255.60.149 -P16033 -N -e "INSERT INTO test_db.Post (comment_count, content, created_at, like_count, user_id, user_name)
SELECT 0, @@hostname, NOW(), 0, 2, 'test_user';" 2>&1| grep -v "Warning"
  sleep 1
done

 

마스터 서버를 통해 INSERT 쿼리가 라우팅 되는 것을 확인할 수 있습니다.

 

다음은 장애 상황 시 마스터 서버로 승격된 슬레이브 서버에게 write 쿼리가 라우팅 되는 것을 확인해보겠습니다.

 

마스터 서버를 임의로 중지시킨 후, mysql-slave1 서버가 새로운 마스터 서버로 승격한 것을 확인할 수 있습니다.

 

mysql-master 서버에 대해 연결이 불가하고, 새로운 마스터 서버인 mysql-slave1 서버가 read_only 값이 0이 된 것을 확인해 쓰기 가능한 상태임을 인지한 것을 확인 후, writer hostgroup에 추가합니다. mysql-master 서버는 SHUNNED 상태가 되고, mysql-slave1 서버는 writer hostgroup에 포함된 채 상태를 업데이트 한 것을 확인할 수 있습니다.

 

mysql_insert_query.sh 스크립트를 다시 실행해보겠습니다.

 

정상적으로 INSERT 쿼리가 라우팅되는 것을 확인할 수 있습니다.

 

다음은 마스터 서버 1대일 때 피드 조회 API 요청 결과입니다.

 

다음은 슬레이브 서버 2대와 ProxySQL 서버 1대를 추가 후, 쿼리 라우팅을 통한 피드 조회 API 요청 결과입니다.

다음과 같이 API 서버에서 ProxySQL 서버에 대해 접속이 가능합니다.

# ProxySQL 연동
proxysql_url=jdbc:mysql://host.docker.internal:16033/test_db
proxysql_username=appuser
proxysql_password=apppass

 

평균 응답 시간은 319ms -> 140ms, 95% Line 응답 시간은 1282ms -> 426ms로 크게 줄어들었습니다.

Orchestrator 고가용성

만약 Orchestrator 서버가 장애가 발생할 경우, Auto Failover를 수행하지 못하게 됩니다. 이를 방지하기 위해 Orchestrator 서버의 고가용성을 보장해야 됩니다. 저는 Raft 방식을 사용하여  Orchestrator의 고가용성을 보장하기로 하였습니다.

 

Orchestrator의 Raft는 여러 Orchestrator 노드가 Raft 합의 프로토콜을 통해 서로 통신하여 리더를 선출하고, 리더가 Failover를 수행하며 팔로워들에게 변경한 내용을 전파하게 됩니다.

 

https://github.com/openark/orchestrator/blob/master/docs/high-availability.md

 

orchestrator/docs/high-availability.md at master · openark/orchestrator

MySQL replication topology management and HA. Contribute to openark/orchestrator development by creating an account on GitHub.

github.com

 

docker-compose.yml

version: '3'

services:
  orchestrator1:
    image: openarkcode/orchestrator:latest
    container_name: orchestrator1
    hostname: orchestrator1
    ports:
      - "3000:3000"
      - "10008:10008"
    volumes:
      - ./orchestrator1/conf/orchestrator.conf.json:/etc/orchestrator.conf.json
      - ./orchestrator1/data:/var/lib/orchestrator
    networks:
      mybridge:
        aliases:
          - orchestrator1

  orchestrator2:
    image: openarkcode/orchestrator:latest
    container_name: orchestrator2
    hostname: orchestrator2
    ports:
      - "3001:3000"
      - "10009:10008"
    volumes:
      - ./orchestrator2/conf/orchestrator.conf.json:/etc/orchestrator.conf.json
      - ./orchestrator2/data:/var/lib/orchestrator
    networks:
      mybridge:
        aliases:
          - orchestrator2

  orchestrator3:
    image: openarkcode/orchestrator:latest
    container_name: orchestrator3
    hostname: orchestrator3
    ports:
      - "3002:3000"
      - "10010:10008"
    volumes:
      - ./orchestrator3/conf/orchestrator.conf.json:/etc/orchestrator.conf.json
      - ./orchestrator3/data:/var/lib/orchestrator
    networks:
      mybridge:
        aliases:
          - orchestrator3

networks:
  mybridge:
    driver: bridge

 

orchestrator.conf.json

"RaftEnabled": true,
"RaftDataDir": "/var/lib/orchestrator",
"RaftBind": "0.0.0.0:10008",
"RaftAdvertise": "orchestrator1:10008", // 호스트 이름
"DefaultRaftPort": 10008,
"RaftNodes": [
  "orchestrator1",
  "orchestrator2",
  "orchestrator3"

 

컨테이너마다 json 파일에 해당 내용을 추가합니다.

 

docker-compose 파일을 실행하면 192.168.80.4 ip 주소를 갖고 있는 orchestrator1 컨테이너가 Follower -> Candidate -> Leader 순으로 상태가 변경되는 것을 확인할 수 있습니다. 

 

  • Heartbeat timeout from "" reached, starting election: 이전 리더로부터 하트비트가 시간 초과되었고, 이로 인해 새로운 리더 선출이 시작됨.
  • Node at 192.168.80:10008 [Candidate] entering Candidate state: 192.168. 80.4 노드가 후보(Candidate) 상태로 전환됨.
  • Vote granted from 192.168.80.4:10008. Tally: 1, Vote granted from 192.168.80.3:10008. Tally: 2: 192.168.80.4 노드가 192.168.80.4와 192.168.80.3으로부터 투표를 받음.
  • Election won. Tally: 2: 선거에서 승리함.
  • Node at 192.168.80.4:10008 [Leader] entering Leader state: 192.168.80.4 노드가 리더로 선출됨.

 

localhost:3000번 포트를 통해 GUI로 확인할 수 있습니다.

 

mysql-master 서버에 대해 정상적으로 등록이 되는 것을 확인할 수 있습니다.

 

Follower 서버들에 대해 mysql-master 서버에 대한 클러스터 정보가 복제되는 것을 확인할 수 있습니다.

 

임의로 Leader 서버인 orchestrator1을 stop 시켜보겠습니다.

 

orchestrator1가 하트비트를 보내지 않아서 시간 초과가 발생했고, 이를 계기로 새로운 리더를 선출하기 위한 선거가 시작된 상황입니다. 아까와 동일한 방식으로 선거가 진행되어 192.168.80.3 노드가 Leader로 선출된 것을 확인할 수 있습니다.

 

MySQL 마스터 서버를 임의로 중단시켜 보겠습니다.

 

동일한 방식으로 Auto Failover가 수행되어 클러스터가 분리된 것을 확인할 수 있습니다

 

Follower 서버에 대해 클러스터 정보가 전파된 것을 확인할 수 있습니다.

ProxySQL 추가 설정

ProxySQL 서버가 어느 호스트에서 동작해야 하는가?

 

일반적으로 애플리케이션이 동작하는 호스트 내부에 ProxySQL 서버를 배치하는 것을 권장하고 있습니다.

 

이유는 다음과 같습니다.

 

- 네트워크 지연

 애플리케이션 서버와 ProxySQL이 같은 호스트에 있으면, 애플리케이션이 ProxySQL로 쿼리를 전달할 때 네트워크 홉이 발생하지 않습니다. 이로 인해 네트워크 지연이 줄어들고, 응답 시간이 빨라집니다.

 

- 고가용성

애플리케이션과 ProxySQL이 같은 호스트에 있으면, ProxySQL에 장애가 발생해도 해당 애플리케이션 서버만 영향을 받습니다. 클러스터 구성에서 다른 애플리케이션 서버와 ProxySQL 인스턴스가 여전히 정상적으로 작동하므로, 시스템의 전반적인 가용성을 유지할 수 있습니다.

 

- 수평 확장

애플리케이션 서버와 ProxySQL이 같은 호스트에 있으면, 수평 확장이 용이합니다. 새로운 애플리케이션 서버를 추가할 때 ProxySQL 인스턴스도 함께 추가되므로, 트래픽 분산과 부하 관리가 자동으로 이루어집니다.

 

https://proxysql.com/blog/scaling-with-proxysql/

 

Scaling from a Dark Ages of Big Data to Brighter Today - ProxySQL

Explore strategies for database infrastructure: single vs. multiple servers, MySQL replication, spare capacity, routing traffic, and ProxySQL benefits for efficient and reliable architecture.

proxysql.com

 

https://www.percona.com/blog/where-do-i-put-proxysql/

 

Where Do I Put ProxySQL?

In this blog post, we'll look at how to deploy ProxySQL.

www.percona.com

 

ProxySQL Cluster

 

ProxySQL을 애플리케이션과 같은 호스트에 설치해야 한다면, 인스턴스를 추가할 때 마다 ProxySQL의 설정을 관리해야 합니다. ProxySQL 인스턴스 그룹을 관리하려면 각 호스트를 개별적으로 구성하고 Ansible/Chef/Puppet/Salt와 같은 구성 관리 도구를 사용하거나 Consul/ZooKeeper와 같은 서비스 검색 도구를 사용해야 합니다. 따라서 외부 소프트웨어(구성 관리 소프트웨어 자체)가 필요하고 이에 의존합니다. 이러한 이유로 ProxySQL 1.4.x 구성 클러스터링부터 기본적으로 지원됩니다. ProxySQL Cluster를 이용하면 ProxySQL 인스턴스 간의 설정 및 상태를 동기화하여 관리 및 운영의 복잡성을 줄이고, 고가용성과 확장성을 강화할 수 있습니다.

 

https://proxysql.com/documentation/proxysql-cluster/

 

ProxySQL Cluster - ProxySQL

ProxySQL Cluster supports decentralized configuration management with native clustering, SSL connections, and adaptive query routing for high scalability and performance.

proxysql.com

https://www.percona.com/blog/proxysql-experimental-feature-native-clustering/

 

ProxySQL Experimental Feature: Native ProxySQL Clustering

ProxySQL 1.4.2 introduced native clustering, allowing several ProxySQL instances to communicate with and share configuration updates with each other.

www.percona.com

 

중앙 집중형 서버

 

분명 애플리케이션 서버와 ProxySQL 서버를 같은 호스트 내에 배치하는 것은 추가적인 네트워크 홉을 줄여줍니다. 하지만 호스트의 리소스를 공유하기 때문에 성능 저하를 유발할 수 있습니다. ProxySQL 클러스터를 이용해 인스턴스 그룹의 관리가 용이하지만, 수 백개의 인스턴스 그룹을 가질 경우 복잡도를 요구합니다. 또한 ProxySQL이 백엔드 서버(MySQL)를 모니터링하는 과정에서 데이터베이스를 바쁘게 유지하게되는 문제가 있습니다. 이에 호스트를 분리하고, 로드밸런서 또는 KeepAlived의 VIP 기능을 통해 연결을 하는 방식이 있습니다.

 

https://aws.amazon.com/ko/blogs/database/how-to-use-proxysql-with-open-source-platforms-to-split-sql-reads-and-writes-on-amazon-aurora-clusters/

 

How to use ProxySQL with open source platforms to split SQL reads and writes on Amazon Aurora clusters | Amazon Web Services

The blog post How to set up a single pgpool endpoint for reads and writes with Amazon Aurora PostgreSQL introduces an architecture that uses the read and write split capabilities of Amazon Aurora PostgreSQL endpoints. This type of architecture works great

aws.amazon.com

 

https://www.percona.com/blog/setup-proxysql-for-high-availability-not-single-point-failure/

 

Setup ProxySQL for High Availability (not a Single Point of Failure)

In this blog post, we'll look at how to set up ProxySQL for high availability.

www.percona.com