Published on

MySQL 8 복제 지연 - GTID·병렬복제 튜닝

Authors

서론

MySQL 8 복제(Replication)에서 “Seconds_Behind_Source가 계속 늘어난다”는 증상은 결과(지연)이지 원인이 아닙니다. 원인은 크게 두 부류로 나뉩니다.

  • 소스(Primary)에서 생성되는 바이너리 로그(binlog) 이벤트가 너무 빠르다: 쓰기 폭주, 큰 트랜잭션, 핫스팟 업데이트
  • 레플리카(Replica)가 이벤트를 적용(apply)하는 속도가 느리다: 단일 스레드 적용, 디스크/IO 병목, 잠금 경합, 커밋 순서 제약

MySQL 8은 GTID 기반 복제와 병렬 복제 기능이 성숙해져 “적용 병목”을 상당 부분 해소할 수 있지만, 설정만 올린다고 무조건 빨라지지는 않습니다. 특히 GTID, 병렬 적용 방식(논리적 병렬성), 커밋 순서 보장, 그리고 워크로드의 트랜잭션 패턴이 맞물려 성능이 결정됩니다.

이 글에서는 MySQL 8에서 복제 지연을 진단하는 실전 쿼리부터, GTID·병렬복제 튜닝 포인트, 그리고 흔한 함정(오히려 지연이 커지는 케이스)까지 순서대로 정리합니다.

> 성능 이슈를 “증상→원인→측정→가설→검증” 흐름으로 푸는 접근은 DB뿐 아니라 다른 시스템에도 동일하게 적용됩니다. 유사한 관점의 사례로는 AWS RDS PostgreSQL IOPS 폭증과 Slow Query 해결 글도 참고할 만합니다.


복제 지연의 기본 구조: IO 스레드 vs SQL(Apply) 스레드

MySQL 8(정확히는 8.0.22 이후 용어)에서는 SHOW SLAVE STATUS 대신 SHOW REPLICA STATUS를 사용합니다. 복제는 크게 두 단계입니다.

  1. Receiver(IO) 스레드: 소스에서 binlog를 받아 레플리카의 relay log에 기록
  2. Applier(SQL) 스레드(들): relay log 이벤트를 읽어 실제 데이터에 반영

지연이 커질 때 먼저 확인해야 할 것은 “받는 단계가 느린가, 적용 단계가 느린가”입니다.

핵심 상태 확인 쿼리

SHOW REPLICA STATUS\G

주요 필드 체크:

  • Replica_IO_Running, Replica_SQL_Running: 둘 다 Yes인지
  • Seconds_Behind_Source: 지연(단, 네트워크/타임스탬프/멀티소스 등 조건에 따라 절대값 신뢰는 금물)
  • Relay_Log_Space: relay log가 누적되는지(적용이 못 따라가면 증가)
  • Last_SQL_Errno, Last_SQL_Error: 적용 중 에러로 멈춘 건 아닌지

추가로 MySQL 8에서는 Performance Schema로 훨씬 구체적인 병목을 볼 수 있습니다.

-- 복제 관련 워커 상태
SELECT *
FROM performance_schema.replication_applier_status_by_worker\G

-- 적용 채널 단위 상태
SELECT *
FROM performance_schema.replication_applier_status\G

-- receiver 상태
SELECT *
FROM performance_schema.replication_connection_status\G

여기서 워커별 SERVICE_STATE, LAST_ERROR_*, 그리고 “어떤 워커가 바쁜지/놀고 있는지”를 보면 병렬화가 실제로 먹고 있는지 감이 옵니다.


GTID가 복제 지연과 무슨 상관인가

GTID의 역할

GTID(Global Transaction ID)는 “이 트랜잭션을 전 세계에서 유일하게 식별”하는 ID입니다. 레플리카는 GTID 집합을 통해 어디까지 적용했는지를 정확히 추적하고, 장애/재구성 시에도 안전하게 이어받을 수 있습니다.

하지만 지연 관점에서 더 중요한 점은:

  • MySQL의 병렬 복제는 결국 “서로 독립적인 트랜잭션을 동시에 적용할 수 있느냐”가 핵심이고
  • GTID는 트랜잭션 경계를 명확히 하므로 “병렬 적용의 단위”를 만들기 쉬워집니다.

GTID 기본 설정(권장)

GTID를 쓰지 않는 환경이라면(또는 혼재라면) 먼저 기반을 정리하는 것이 좋습니다.

# my.cnf (source/replica 공통 권장)
server_id=101
log_bin=mysql-bin
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
log_replica_updates=ON

# crash-safe replication
relay_log_recovery=ON
sync_binlog=1
innodb_flush_log_at_trx_commit=1
  • binlog_format=ROW는 행 기반으로 재현성이 좋고 병렬 적용 시 충돌/재시도 리스크가 낮습니다.
  • enforce_gtid_consistency=ON은 GTID와 충돌하는 비결정적 트랜잭션을 막아줍니다.

> 이미 운영 중인 시스템에서 GTID 전환은 절차가 필요합니다. 여기서는 “지연 튜닝”에 집중하므로 전환 절차 자체는 생략합니다.


MySQL 8 병렬 복제의 핵심: replica_parallel_workers, replica_parallel_type

MySQL의 병렬 복제는 “여러 SQL 스레드(워커)가 동시에 이벤트를 적용”하도록 합니다. 하지만 아무 트랜잭션이나 동시에 적용할 수는 없습니다. 같은 행/같은 테이블/같은 키를 건드리면 충돌하거나, 커밋 순서 때문에 대기해야 할 수 있습니다.

1) 워커 수: replica_parallel_workers

-- 현재 값 확인
SHOW VARIABLES LIKE 'replica_parallel_workers';

-- 동적 변경(가능한 버전에서)
SET GLOBAL replica_parallel_workers = 8;
  • 워커 수를 늘리면 CPU 사용률은 오르고(좋을 수도, 나쁠 수도), 디스크 쓰기도 늘 수 있습니다.
  • “워커가 많으면 무조건 빨라진다”가 아니라, 병렬화 가능한 트랜잭션이 충분히 많을 때만 효과가 납니다.

실무 팁:

  • 레플리카가 8 vCPU라면 4~8부터 시작해 관찰
  • 워커가 늘어도 replication_applier_status_by_worker에서 대부분이 idle이면, 워커 수가 아니라 병렬화 단위/워크로드가 문제

2) 병렬화 방식: replica_parallel_type

MySQL 8에서 중요한 선택지입니다.

SHOW VARIABLES LIKE 'replica_parallel_type';

일반적으로 다음 두 가지를 많이 봅니다.

  • DATABASE: 스키마(데이터베이스) 단위로 병렬 적용
  • LOGICAL_CLOCK: “논리적 시계” 기반으로 병렬 적용(같은 그룹 커밋/의존성 고려)

현대적인 단일 서비스 DB(스키마 하나에 테이블이 몰린 구조)에서는 DATABASE가 거의 효과가 없습니다. 반면 LOGICAL_CLOCK은 같은 스키마 안에서도 병렬화 여지가 커서 보통 더 유리합니다.

SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';

커밋 순서가 발목을 잡는다: replica_preserve_commit_order

병렬로 적용해도, 최종 커밋을 “소스와 동일한 순서”로 맞추려면 워커들이 서로 기다리게 됩니다. 이를 제어하는 대표 옵션이 replica_preserve_commit_order입니다.

SHOW VARIABLES LIKE 'replica_preserve_commit_order';
  • ON: 커밋 순서를 보장(일반적으로 데이터 일관성/읽기 의미론이 단순해짐)
  • OFF: 커밋 순서 보장을 완화(지연 감소 가능, 대신 읽기 관점에서 소스와 시점이 더 어긋날 수 있음)

실무에서의 판단 기준:

  • 레플리카를 읽기 트래픽에 적극 사용하고 “소스 커밋 순서와 유사한 시점”이 중요하면 ON 유지
  • 레플리카를 백업/분석/배치 용도로 쓰고 “최대한 빨리 따라잡기”가 목표면 OFF를 검토

단, OFF가 항상 빠른 것은 아닙니다. 충돌/락 경합이 심한 워크로드에서는 커밋 순서보다 행 잠금 경합이 더 큰 병목일 수 있습니다.


병렬 복제가 안 먹는 대표 패턴 5가지(그리고 대응)

1) 단일 테이블/단일 키 핫스팟

예: UPDATE counters SET v=v+1 WHERE id=1; 같은 패턴. 모든 트랜잭션이 같은 행을 건드리면 병렬 적용은 불가능합니다.

대응:

  • 애플리케이션 레벨에서 샤딩 키/버킷(counter bucket) 도입
  • 집계 테이블을 이벤트 로그로 바꾸고 배치로 합산

2) 초대형 트랜잭션(한 번에 수십만 행)

큰 트랜잭션 하나가 워커 하나를 오래 점유하면, 다른 워커가 놀아도 전체 지연은 줄지 않습니다.

대응:

  • 배치 작업을 작은 커밋 단위로 쪼개기
  • 대량 적재는 LOAD DATA/bulk insert라도 커밋 크기 관리

3) DDL이 섞임

DDL은 복제에서 병렬화가 제한될 수 있고, 메타데이터 락(MDL)로 다른 적용까지 막을 수 있습니다.

대응:

  • 온라인 DDL 도구/전략(gh-ost, pt-online-schema-change 등) 검토
  • DDL 윈도우를 분리

4) 레플리카 디스크/IO 병목

병렬 워커를 늘리면 쓰기 동시성이 증가해 IO가 먼저 터질 수 있습니다.

대응:

  • 스토리지 성능(특히 fsync) 점검
  • innodb_flush_log_at_trx_commit/sync_binlog 완화는 내구성 트레이드오프가 크므로 신중

5) 인덱스/쿼리 설계로 인한 과도한 랜덤 IO

소스에서는 캐시/버퍼로 버티는데 레플리카는 캐시 히트율이 낮아 적용이 느릴 수 있습니다.

대응:

  • 레플리카도 innodb_buffer_pool_size 충분히 확보
  • 불필요한 2차 인덱스 정리(쓰기 증폭 감소)

실전 튜닝 절차(측정 → 변경 → 검증)

아래 순서로 접근하면 “무작정 변수 올리기”를 피할 수 있습니다.

1) 지연이 ‘수신’인지 ‘적용’인지 분리

  • Relay_Log_Space가 계속 증가한다 → 적용 병목 가능성 큼
  • receiver 관련 상태에서 reconnect/timeout이 잦다 → 네트워크/소스 부하/권한 문제도 점검

2) 워커가 실제로 병렬 적용 중인지 확인

SELECT
  WORKER_ID,
  SERVICE_STATE,
  LAST_APPLIED_TRANSACTION,
  LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP,
  LAST_ERROR_NUMBER,
  LAST_ERROR_MESSAGE
FROM performance_schema.replication_applier_status_by_worker
ORDER BY WORKER_ID;
  • 여러 워커가 동시에 APPLYING_TRANSACTION 상태인지
  • 특정 워커만 계속 바쁘고 나머지는 idle인지(핫스팟/대형 트랜잭션 의심)

3) LOGICAL_CLOCK로 전환 + 워커 수 점진 증가

권장 시작점 예시:

STOP REPLICA;

SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_preserve_commit_order = ON;

START REPLICA;
  • 먼저 replica_preserve_commit_order=ON으로 안전하게 시작
  • 개선이 제한적이면 OFF를 A/B 테스트(가능하면 읽기 트래픽이 적은 시간대)

4) 병목이 잠금/IO인지 확인

엔진 레벨에서 확인할 것:

-- InnoDB 상태에서 lock/IO 힌트 확인
SHOW ENGINE INNODB STATUS\G

-- 대기 이벤트/락을 더 체계적으로 보려면 sys 스키마도 활용 가능
SELECT * FROM sys.innodb_lock_waits\G
  • row lock wait가 많으면 병렬 워커를 늘려도 더 느려질 수 있습니다(경합 증가).
  • IO wait가 많으면 워커 수를 늘릴수록 fsync 경쟁이 커질 수 있습니다.

GTID 기반 운영에서 자주 만나는 복제 지연 함정

1) Seconds_Behind_Source만 믿고 튜닝

Seconds_Behind_Source는 편리하지만, 특정 상황에서 0인데도 실제로는 뒤처질 수 있고(예: 이벤트가 없어서), 반대로 크게 튈 수도 있습니다.

대안으로 “GTID 적용 격차”를 보는 습관이 좋습니다.

-- 소스에서
SHOW MASTER STATUS\G

-- 레플리카에서
SHOW REPLICA STATUS\G
-- Retrieved_Gtid_Set, Executed_Gtid_Set 비교

2) 병렬 워커를 늘렸는데 지연이 더 커짐

대개 아래 중 하나입니다.

  • 디스크가 못 버팀(IO wait 증가)
  • 락 경합 증가(특히 핫스팟)
  • 커밋 순서 대기(워커가 서로 기다림)

이때는 “더 올리기”가 아니라, 워크로드를 병렬화 친화적으로 바꾸거나(핫스팟 제거/트랜잭션 쪼개기) 스토리지를 개선하는 쪽이 정답인 경우가 많습니다.


예시: 지연 대응을 위한 최소 설정 템플릿

아래는 MySQL 8 레플리카에서 병렬 복제를 활성화할 때 자주 쓰는 출발점입니다(환경에 맞게 조정).

# my.cnf (replica)
[mysqld]
server_id=201
log_bin=mysql-bin
relay_log=relay-bin
read_only=ON
super_read_only=ON

gtid_mode=ON
enforce_gtid_consistency=ON
log_replica_updates=ON
relay_log_recovery=ON

binlog_format=ROW

# parallel replication
replica_parallel_type=LOGICAL_CLOCK
replica_parallel_workers=8
replica_preserve_commit_order=ON

# InnoDB(예시)
innodb_buffer_pool_size=8G
innodb_flush_method=O_DIRECT

운영 팁:

  • 워커 수는 “CPU 코어 수”가 아니라 “병렬화 가능한 트랜잭션 수와 IO 여력”에 의해 결정됩니다.
  • 레플리카를 읽기에도 쓰면, 튜닝 후 애플리케이션의 읽기 일관성 요구사항(커밋 순서/지연 허용치)을 다시 확인하세요.

마무리

MySQL 8 복제 지연은 단순히 replica_parallel_workers를 올려서 해결되는 문제가 아니라, GTID로 트랜잭션 경계를 명확히 하고, LOGICAL_CLOCK 기반 병렬 적용을 활성화하며, 커밋 순서/락 경합/IO 병목을 함께 다뤄야 안정적으로 줄일 수 있습니다.

정리하면 다음 순서가 가장 재현성이 좋습니다.

  1. performance_schema로 “적용 병목”인지 확인
  2. replica_parallel_type=LOGICAL_CLOCK + 워커 수 점진 증가
  3. replica_preserve_commit_order는 요구사항에 따라 ON/OFF 실험
  4. 핫스팟/대형 트랜잭션/DDL/IO 병목 같은 구조적 원인을 제거

복제 지연을 줄이는 과정은 결국 “쓰기 패턴을 어떻게 만들었는가”에 대한 피드백 루프입니다. 애플리케이션 쿼리/트랜잭션을 함께 손보면 병렬 복제의 효과가 훨씬 크게 나타납니다.