Post

MySQL GTID Replication

MySQL GTID Replication

들어가며

데이터베이스를 운영하다 보면 복제(Replication)라는 단어를 자주 접하게 된다. 단순히 “백업용 아닌가?”라고 생각할 수 있지만, 실제로 복제는 다양한 목적으로 사용된다.

복제를 사용하는 이유:

  • 고가용성(High Availability) : Source 서버 장애 시 Replica로 페일오버
  • 읽기 부하 분산 : 쓰기는 Source, 읽기는 Replica로 분산
  • 백업 분리 : Replica에서 백업 수행 → Source 서버 성능 영향 없음
  • 용도별 분리 : 서비스용, 통계/분석용, 배치 작업용 Replica 분리

실제로 GitHub는 semi-synchronous replication으로 lossless failover를 구현하고, Shopify는 ProxySQL과 함께 읽기 부하 분산에 복제를 활용하고 있다.

이번 글에서는 Docker 환경에서 GTID 기반 MySQL 복제를 직접 구축해본다.


복제 방식 : 바이너리 로그 포지션 vs GTID

MySQL 복제 방식에는 크게 두 가지가 있다.

바이너리 로그 포지션 방식

1
2
3
4
5
6
7
8
CHANGE REPLICATION SOURCE TO
	SOURCE_HOST='source_server_host',
    SOURCE_PORT=3306,
    SOURCE_USER='repl_user',
    SOURCE_PASSWORD='repl_user_password',
    SOURCE_LOG_FILE='binary-log.000002',
    SOURCE_LOG_POS=2708,
   	GET_SOURCE_PUBLIC_KEY=1;

파일명과 오프셋을 직접 지정해야 한다. 페일오버 시 포지션을 정확히 맞춰야 해서 까다롭다.

GTID 방식

트랜잭션마다 고유한 ID(GTID)가 부여되어, “어디까지 실행했는지” 자동으로 추적한다. 페일오버나 토폴로지 변경이 훨씬 쉽다.

1
2
3
4
5
6
7
CHANGE REPLICATION SOURCE TO
	SOURCE_HOST='source_server_host',
    SOURCE_PORT=3306,
    SOURCE_USER='repl_user',
    SOURCE_PASSWORD='repl_user_password',
	SOURCE_AUTO_POSITION=1,
   	GET_SOURCE_PUBLIC_KEY=1;

위 바이너리 로그 방식과 SOURCE_AUTO_POSITION 옵션이 있다는 점이 다르다. 이 옵션으로 인해 레플리카 서버는 자신의 gtid_executed 값 (MySQL 서버에서 실행되어 바이너리 로그 파일에 기록된 모든 트랜잭션들의 GTID 셋을 나타냄) 을 참조해 해당 시점부터 소스 서버와 복제를 연결해서 데이터를 동기화하게 된다.

참고로 현재는 GTID가 표준이다. 바이너리 로그 포지션 방식은 레거시 시스템에서나 볼 수 있고, 새로 구축한다면 GTID를 쓰는 것이 맞다.


실습 환경 구성

Docker Compose로 Source와 Replica 서버를 구성한다.

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
version: '3.8'

services:
  mysql-source:
    image: mysql:8.0
    container_name: mysql-source
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: testdb
    ports:
      - "3306:3306"
    command:
      - --server-id=1
      - --log-bin=mysql-bin
      - --gtid-mode=ON
      - --enforce-gtid-consistency=ON
      - --binlog-format=ROW
    volumes:
      - source-data:/var/lib/mysql
    networks:
      - mysql-network

  mysql-replica:
    image: mysql:8.0
    container_name: mysql-replica
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: testdb
    ports:
      - "3307:3306"
    command:
      - --server-id=2
      - --log-bin=mysql-bin
      - --gtid-mode=ON
      - --enforce-gtid-consistency=ON
      - --binlog-format=ROW
      - --read-only=ON
    volumes:
      - replica-data:/var/lib/mysql
    networks:
      - mysql-network
    depends_on:
      - mysql-source

volumes:
  source-data:
  replica-data:

networks:
  mysql-network:
    driver: bridge

주요 옵션 설명

옵션 설명
server-id 복제에 참여하는 서버의 고유 식별자. 각 서버마다 달라야 함
log-bin 바이너리 로그 활성화
gtid-mode=ON GTID 모드 활성화
enforce-gtid-consistency=ON GTID 일관성 강제. gtid-mode와 함께 설정해야 함
binlog-format=ROW 바이너리 로그 포맷. ROW가 가장 안전
read-only=ON Replica에서 쓰기 방지

여기서 중요한 점은 설정파일에서 반드시 gtid-mode=ONenforce-gtid-consistency=ON 을 함께 명시해야 한다.

만약 gtid-modeON 으로 설정되고 enforce-gtid-consistency 가 설정되지 않으면 에러가 발생한다.

컨테이너 실행

1
docker-compose up -d

1
docker ps

두 컨테이너가 모두 떴고 성공이다.


Source 서버 설정

Source 접속

1
docker exec -it mysql-source mysql -uroot -proot

복제 계정 생성

1
2
3
CREATE USER 'repl_user'@'%' IDENTIFIED WITH mysql_native_password BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'%';
FLUSH PRIVILEGES;

MySQL 8.0의 기본 인증 플러그인은 caching_sha2_password인데, 복제 연결 시 SSL 없이는 인증 에러가 발생할 수 있다. mysql_native_password를 명시적으로 지정하면 이 문제를 피할 수 있다.

GTID 모드 확인

1
SHOW VARIABLES LIKE 'gtid_mode';


Replica 서버 설정

이제 새 터미널에서 Replica에 접속해보자.

1
docker exec -it mysql-replica mysql -uroot -proot

복제 설정

1
2
3
4
5
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST='mysql-source',
    SOURCE_USER='repl_user',
    SOURCE_PASSWORD='repl_password',
    SOURCE_AUTO_POSITION=1;

SOURCE_AUTO_POSITION=1이 GTID 기반 자동 포지셔닝의 핵심이다. 바이너리 로그 파일명이나 포지션을 지정할 필요가 없다.

복제 시작

1
START REPLICA;

로그는 다음과 같이 나왔다.

1
2
2026-01-04 10:51:52 mysql-replica  | 2026-01-04T01:51:52.763802Z 9 
[System] [MY-014002] [Repl] Replica receiver thread for channel '': connected to source 'repl_user@mysql-source:3306' with server_uuid=661772cd-e90f-11f0-ba5c-0242ac130002, server_id=1. Starting GTID-based replication.

상태 확인

1
SHOW REPLICA STATUS\G
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
mysql> SHOW REPLICA STATUS\G
*************************** 1. row *************************** 
             Replica_IO_State: Waiting for source to send event
                  Source_Host: mysql-source
                  Source_User: repl_user
                  Source_Port: 3306
                Connect_Retry: 60
              Source_Log_File: mysql-bin.000003
          Read_Source_Log_Pos: 877
               Relay_Log_File: b8ad115814d2-relay-bin.000003
                Relay_Log_Pos: 1093
        Relay_Source_Log_File: mysql-bin.000003
           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Source_Log_Pos: 877
              Relay_Log_Space: 2996112
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Source_SSL_Allowed: No
           Source_SSL_CA_File:
           Source_SSL_CA_Path:
              Source_SSL_Cert:
            Source_SSL_Cipher:
               Source_SSL_Key:
        Seconds_Behind_Source: 0
Source_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Source_Server_Id: 1
                  Source_UUID: 661772cd-e90f-11f0-ba5c-0242ac130002
             Source_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
    Replica_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Source_Retry_Count: 86400
                  Source_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Source_SSL_Crl:
           Source_SSL_Crlpath:
           Retrieved_Gtid_Set: 661772cd-e90f-11f0-ba5c-0242ac130002:1-9
            Executed_Gtid_Set: 661772cd-e90f-11f0-ba5c-0242ac130002:1-9,
664037aa-e90f-11f0-8055-0242ac130003:1-6
                Auto_Position: 1
         Replicate_Rewrite_DB:
                 Channel_Name:
           Source_TLS_Version:
       Source_public_key_path:
        Get_Source_public_key: 0
            Network_Namespace:
1 row in set (0.00 sec)

여기서 확인해 볼 것은

  • Replica_IO_Running: Yes - Source에서 이벤트를 가져오는 스레드
  • Replica_SQL_Running: Yes - 가져온 이벤트를 실행하는 스레드
  • Seconds_Behind_Source: 0 - 복제 지연 시간

둘 다 Yes면 복제가 정상 동작 중이다.

Read-Only 설정

복제가 정상 동작하는 것을 확인했으면, Replica에 쓰기를 방지하기 위해 read-only 를 설정한다. (.yml 파일에서 옵션을 추가해뒀지만 잘 반영이 되지 않아 명시적으로 다시 설정하자)

1
2
SET GLOBAL read_only = ON;
SET GLOBAL super_read_only = ON;
1
2
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';

read_only는 일반 유저의 쓰기만 차단하고 root는 쓰기 가능하다. super_read_only까지 설정해야 root도 차단된다.

Docker Compose의 command에 --super-read-only=ON을 넣으면 초기화 단계에서 실패한다. 복제 설정 완료 후 수동으로 설정하는 것이 안전하다.


복제 테스트

Source에서 데이터 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
USE testdb;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name) VALUES ('test1');
INSERT INTO users (name) VALUES ('test2');
INSERT INTO users (name) VALUES ('test3');

SELECT * FROM users;
1
2
3
4
5
6
7
+----+-------+---------------------+
| id | name  | created_at          |
+----+-------+---------------------+
|  1 | test1 | 2026-01-04 00:41:37 |
|  2 | test2 | 2026-01-04 00:41:44 |
|  3 | test3 | 2026-01-04 00:41:46 |
+----+-------+---------------------+

Replica에서 확인

1
2
3
USE testdb;

SELECT * FROM users;
1
2
3
4
5
6
7
+----+-------+---------------------+
| id | name  | created_at          |
+----+-------+---------------------+
|  1 | test1 | 2026-01-04 00:41:37 |
|  2 | test2 | 2026-01-04 00:41:44 |
|  3 | test3 | 2026-01-04 00:41:46 |
+----+-------+---------------------+

Source에서 넣은 데이터가 Replica에서 즉시 확인된다.

실시간으로 데이터를 하나 더 추가해 동기화가 즉시 됐는지 검증해보자.

Source에서 다음과 같이 current_test 를 추가했고

바로 Replica에서 확인해보면

일치하는 결과가 나오는 것을 볼 수 있다.

GTID 확인

양쪽에서 실행:

1
SHOW MASTER STATUS\G

결과는 다음과 같다.

Source:

1
2
3
4
5
6
7
8
mysql> SHOW MASTER STATUS\G
*************************** 1. row ***************************
             File: mysql-bin.000003
         Position: 2396
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set: 661772cd-e90f-11f0-ba5c-0242ac130002:1-14
1 row in set (0.00 sec)

Replica:

1
2
3
4
5
6
7
8
9
mysql> SHOW MASTER STATUS\G
*************************** 1. row ***************************
             File: mysql-bin.000003
         Position: 2997046
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set: 661772cd-e90f-11f0-ba5c-0242ac130002:1-14,
664037aa-e90f-11f0-8055-0242ac130003:1-6
1 row in set (0.00 sec)

Executed_Gtid_Set 값이 동일하고 완벽하게 동기화된 상태다!

Source의 GTID 1번부터 14번이 일치하는것을 볼 수 있고, MySQL 초기화 과정에서 일부 추가 트랜잭션 1-6이 있는 걸 볼 수 있다. 컨테이너 시작할 때 자동으로 데이터베이스를 생성하는데, 이 과정에서 트랜잭션이 발생해서 GTID가 생긴다고 볼 수 있다.

이렇게 Replica를 새로 구축하면 초기화 과정에서 자체 GTID가 생길 수 있으니 염두해두자.

중요한 점은 Source의 GTID가 Replica에 모두 포함되어있는가 이다.


주의사항

1. Replica에는 반드시 read-only 설정

Replica에 직접 쓰기를 하면 Source와 데이터 불일치가 발생한다. 게다가 Replica에서 새로운 트랜잭션이 생기는 형태이므로 GTID도 꼬여서 복제가 깨질 수 있다.

1
2
command:
  - --read-only=ON

실수로 쓰기를 시도하면:

1
2
mysql> INSERT INTO users (name) VALUES ('readonly_test');
ERROR 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement

2. 인증 플러그인 문제

MySQL 8.0에서 복제 계정 생성 시 mysql_native_password를 명시하지 않으면 다음 에러가 발생할 수 있다.

1
2
Authentication plugin 'caching_sha2_password' reported error: 
Authentication requires secure connection.

3. 복제 지연

Replica는 Source의 변경사항을 비동기로 가져온다. 트래픽이 많으면 지연이 발생할 수 있다.

1
2
SHOW REPLICA STATUS\G
-- Seconds_Behind_Source: 10  <- 10초 뒤처짐

실시간성이 중요한 읽기는 Source에서 처리해야 한다.

4. mysql_native_password deprecated

실습에서 mysql_native_password를 사용했지만, 이는 deprecated 되었고 향후 제거될 예정이다.

1
2
[Warning] Plugin mysql_native_password reported: 'mysql_native_password' is deprecated 
and will be removed in a future release. Please use caching_sha2_password instead'

운영 환경에서는 SSL을 설정하고 caching_sha2_password를 사용하는 것이 권장된다.


마치며

MySQL 복제 구축 자체는 어렵지 않다. 하지만 실제 운영에서는

  • 복제 지연 모니터링
  • 페일오버 자동화
  • 복제 깨졌을 때 복구

이런 부분들이 더 중요하다. 이번 실습으로 기본 개념을 잡았으니, 다음에는 복제 지연 시뮬레이션이나 페일오버 테스트를 해봐도 좋을 것 같다.


References

This post is licensed under CC BY 4.0 by the author.