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=ON 과 enforce-gtid-consistency=ON 을 함께 명시해야 한다.
만약 gtid-mode 만 ON 으로 설정되고 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 복제 구축 자체는 어렵지 않다. 하지만 실제 운영에서는
- 복제 지연 모니터링
- 페일오버 자동화
- 복제 깨졌을 때 복구
이런 부분들이 더 중요하다. 이번 실습으로 기본 개념을 잡았으니, 다음에는 복제 지연 시뮬레이션이나 페일오버 테스트를 해봐도 좋을 것 같다.










