Published on

MySQL Replication Lag 폭증 - GTID·병렬복제로 0초

Authors

서버가 멀쩡한데도 Seconds_Behind_Master가 갑자기 수백~수천 초로 치솟는 순간이 있습니다. 대개는 "슬레이브가 느리다"가 아니라, 복제 적용(Apply) 파이프라인이 특정 병목에 걸렸거나 병렬화가 깨진 상태입니다. 이 글에서는 복제 지연을 빠르게 분류·진단하고, GTID 기반 운영병렬 복제 설정으로 지연을 0초에 가깝게 만드는 방법을 다룹니다.

아래 내용은 MySQL 5.7/8.0 기준이며, 특히 8.0에서 replica_parallel_type=LOGICAL_CLOCK의 효과가 큽니다.

Replication Lag이 폭증하는 대표 패턴

복제 지연은 크게 세 구간 중 어디가 막히는지부터 나눠야 합니다.

  1. IO Thread 병목: 소스(마스터)에서 바이너리 로그를 못 받아옴
  2. SQL/Applier Thread 병목: 릴레이 로그는 쌓이는데 적용을 못함
  3. Commit/Flush 병목: 적용은 하는데 커밋 단계에서 디스크·락에 막힘

현장에서 가장 흔한 건 2)입니다. 이유는 간단합니다.

  • 기본 설정에서는 적용이 단일 스레드에 가깝고
  • 트랜잭션 간 의존성이 크면 병렬 워커를 늘려도 병렬화가 안 되며
  • 특정 테이블에 쓰기가 몰리면 락 경합으로 워커가 줄줄이 대기합니다

5분 안에 상태 분류하기: 필수 점검 쿼리

먼저 현재 복제 상태를 확인합니다. MySQL 8.0에서는 SHOW REPLICA STATUS를 권장하지만, 호환을 위해 둘 다 예시를 둡니다.

-- 8.0 권장
SHOW REPLICA STATUS\G

-- 5.7 호환
SHOW SLAVE STATUS\G

여기서 핵심 필드만 빠르게 봅니다.

  • Replica_IO_Running 또는 Slave_IO_Running
  • Replica_SQL_Running 또는 Slave_SQL_Running
  • Seconds_Behind_Source 또는 Seconds_Behind_Master
  • Relay_Log_Space (릴레이 로그가 계속 증가하면 Apply 병목)
  • Last_SQL_Error (에러로 멈춘 경우는 지연이 아니라 중단)

추가로 8.0에서는 Performance Schema로 더 정확히 봅니다.

SELECT *
FROM performance_schema.replication_applier_status\G;

SELECT *
FROM performance_schema.replication_applier_status_by_worker
ORDER BY LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP\G;

...by_worker에서 특정 워커가 오래 멈춰 있거나, 워커 대부분이 Waiting for dependent transaction to commit 류로 대기 중이면 병렬화가 깨진 것입니다.

"0초"의 의미를 현실적으로 정의하기

엄밀히는 네트워크·디스크·커밋 지연이 존재하므로 항상 0을 보장할 수는 없습니다. 하지만 운영에서 말하는 0초는 보통 다음을 의미합니다.

  • 평시 Seconds_Behind_*가 0 또는 1을 오가고
  • 스파이크가 와도 수 초 내에 따라잡으며
  • 백로그(릴레이 로그)가 장시간 누적되지 않는 상태

이를 만들려면 GTID로 운영을 단순화하고, 병렬 복제의 병렬화 단위가 깨지지 않도록 스키마/트랜잭션을 정리해야 합니다.

GTID로 바꾸면 뭐가 좋아지나

GTID(Global Transaction ID)는 "이 트랜잭션은 어디서 실행된 몇 번째 트랜잭션"을 식별하는 ID입니다. GTID를 쓰면 다음이 크게 좋아집니다.

  • 장애 조치 시 "어디까지 적용했지"를 포지션이 아니라 트랜잭션 단위로 추적
  • 재복제/재구성 시 좌표 꼬임 감소
  • 멀티 소스, 체인 복제에서 운영 난이도 감소
  • 특히 병렬 복제 튜닝/관측이 쉬워짐

다만 GTID 자체가 병렬 복제를 자동으로 빠르게 만드는 것은 아닙니다. 병렬 복제의 핵심은 동시에 적용 가능한 트랜잭션을 워커가 나눠 처리할 수 있느냐입니다.

GTID 전환(5.7/8.0) 실전 절차

1) 사전 조건

  • server_id 유일
  • 바이너리 로그 활성화
  • 복제 포맷은 가급적 ROW
-- 소스(마스터) 권장 설정
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';

ROW는 로그가 커질 수 있지만, 복제 안정성과 병렬화/호환성이 좋습니다.

2) 소스(마스터)에서 GTID 활성화

my.cnf 또는 mysqld 설정에 반영 후 재시작이 일반적입니다.

[mysqld]
server_id=1
log_bin=mysql-bin
binlog_format=ROW

gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON

enforce_gtid_consistency=ON은 GTID와 호환되지 않는 트랜잭션(예: 일부 비결정적 작업)을 제한합니다.

3) 레플리카(슬레이브)에서도 동일하게 설정

[mysqld]
server_id=2
log_bin=mysql-bin
relay_log=mysql-relay-bin
read_only=ON
super_read_only=ON

# GTID

gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON

4) GTID 기반으로 복제 연결

포지션 기반이 아니라 MASTER_AUTO_POSITION=1로 전환합니다. (8.0에서는 SOURCE_AUTO_POSITION 표기도 있지만 문서/버전에 따라 다릅니다.)

STOP SLAVE;

CHANGE MASTER TO
  MASTER_HOST='source-db',
  MASTER_USER='repl',
  MASTER_PASSWORD='***',
  MASTER_AUTO_POSITION=1;

START SLAVE;

전환 후에는 SHOW SLAVE STATUS\G에서 Auto_Position: 1을 확인합니다.

병렬 복제로 Apply 병목 없애기

복제 지연의 대부분은 "릴레이 로그는 쌓이는데 적용이 느린" 상황입니다. 이때 병렬 복제가 가장 직접적인 해결책입니다.

1) MySQL 8.0: LOGICAL_CLOCK 기반 병렬 적용

8.0에서는 커밋 순서를 추적해 병렬 적용을 더 잘합니다.

-- 현재 값 확인
SHOW VARIABLES LIKE 'replica_parallel%';
SHOW VARIABLES LIKE 'slave_parallel%';

-- 핵심 튜닝(8.0)
SET GLOBAL replica_parallel_workers = 16;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;
  • replica_parallel_workers: 워커 수
  • replica_parallel_type=LOGICAL_CLOCK: 트랜잭션 의존성을 더 똑똑하게 분석
  • replica_preserve_commit_order=ON: 커밋 순서를 소스와 맞춰 애플리케이션 일관성을 유지

커밋 순서를 보존하면 일부 상황에서 처리량이 줄 수 있지만, 운영에서는 대개 ON이 안전합니다.

2) MySQL 5.7: DATABASE 기반 병렬화의 한계

5.7의 대표 옵션은 slave_parallel_type=DATABASE입니다.

SET GLOBAL slave_parallel_workers = 16;
SET GLOBAL slave_parallel_type = 'DATABASE';

이 방식은 DB(schema) 단위로만 병렬화되기 때문에, 대부분의 서비스가 단일 schema를 쓰면 효과가 거의 없습니다. 이런 경우 8.0 업그레이드가 체감 개선이 큽니다.

병렬 복제가 안 먹힐 때: 의존성·락·핫스팟을 부숴라

워커 수를 늘렸는데도 lag이 줄지 않으면, 보통 다음 중 하나입니다.

1) 단일 테이블 핫스팟(특히 secondary index 갱신)

  • 특정 테이블로 UPDATE/INSERT가 몰림
  • 동일한 PK 범위 또는 동일한 보조 인덱스 키에 경쟁
  • 결과적으로 InnoDB 락 경합으로 워커가 줄줄이 대기

대응:

  • 쓰기 집중 테이블 샤딩/파티셔닝
  • 인덱스 재설계(불필요한 보조 인덱스 제거)
  • "한 트랜잭션에서 너무 많은 행"을 갱신하지 않기

2) 큰 트랜잭션(대량 배치)

대량 배치는 병렬화보다 "한 방"의 비용이 커서 lag을 폭발시킵니다.

대응:

  • 배치를 작은 청크로 쪼개기
  • 커밋을 자주 하되, 너무 잦은 커밋은 또 다른 병목(redo flush)을 만들 수 있으니 균형 잡기

3) DDL이 섞인 경우

일부 DDL은 복제 적용에서 사실상 직렬화됩니다.

대응:

  • 온라인 DDL 활용
  • 트래픽 낮은 시간에 수행
  • DDL 전후로 lag 모니터링 및 롤백 플랜 준비

"0초"에 가까워지게 만드는 운영 튜닝 체크리스트

1) 레플리카 디스크·IO 튜닝

Apply가 빨라도 디스크가 못 받쳐주면 lag이 생깁니다.

  • innodb_flush_log_at_trx_commit (레플리카에서 2로 완화하는 선택지가 있으나 내구성 트레이드오프)
  • sync_binlog (레플리카에서 완화 가능)

주의: 이 값들은 장애 시 데이터 유실 가능성과 직결됩니다. 레플리카가 "조회용"인지, "장애조치 후보"인지에 따라 정책을 나눠야 합니다.

2) 복제 필터링은 신중하게

replicate_do_db 같은 필터는 운영 중 예기치 않은 불일치 원인이 되기 쉽습니다. 가능하면 애플리케이션 레벨에서 쓰기 분리를 권장합니다.

3) 지연 스파이크에 대한 타임아웃·재시도 설계

복제 지연이 커지면 읽기 분산에서 오래된 데이터를 읽거나, 타임아웃이 연쇄적으로 터질 수 있습니다. 이때는 DB 튜닝만큼이나 클라이언트의 타임아웃·재시도 정책이 중요합니다. gRPC나 HTTP 클라이언트에서 무지성 재시도는 오히려 DB 부하를 키워 lag을 악화시킵니다.

실제 적용 예시: lag 폭증 상황을 "따라잡기"로 안정화

아래는 "릴레이 로그가 쌓이고 워커가 놀고 있는" 전형적 상황에서의 접근입니다.

1) 병목 확인

SHOW SLAVE STATUS\G
  • Relay_Log_Space가 계속 증가
  • Slave_SQL_Running: Yes인데 Seconds_Behind_Master가 증가

그 다음 워커 상태:

SELECT
  WORKER_ID,
  SERVICE_STATE,
  LAST_ERROR_NUMBER,
  LAST_ERROR_MESSAGE,
  LAST_APPLIED_TRANSACTION
FROM performance_schema.replication_applier_status_by_worker
ORDER BY WORKER_ID;

워커 대부분이 Waiting for dependent transaction 류로 대기하면 병렬화 단위가 부족합니다.

2) 8.0 기준 병렬 복제 활성화

STOP REPLICA;

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

START REPLICA;

이후 Seconds_Behind_*가 빠르게 감소하는지, Relay_Log_Space가 줄어드는지 확인합니다.

3) 그래도 안 줄면: 핫스팟/큰 트랜잭션 추적

소스에서 대량 트랜잭션이 있는지 확인합니다.

SHOW PROCESSLIST;

-- InnoDB 트랜잭션 관측
SELECT *
FROM information_schema.innodb_trx\G;

대량 배치가 원인이면, 애플리케이션/배치 로직을 "작게 자주"로 바꾸는 것이 병렬 워커를 늘리는 것보다 효과적입니다.

장애조치(페일오버) 관점에서 GTID가 주는 결정적 이점

lag을 0초로 만드는 이유는 단지 "지표가 예뻐서"가 아니라, 다음을 가능하게 하기 위함입니다.

  • 레플리카를 읽기 전용이 아니라 즉시 승격 가능한 후보로 유지
  • 장애 시점에 "마지막으로 적용된 트랜잭션"을 기준으로 승격 판단
  • 운영자가 포지션 계산/비교에 시간을 쓰지 않음

GTID는 복제 운영을 단순화하고, 병렬 복제는 평시/피크의 적용 처리량을 올려 lag을 줄입니다. 둘을 함께 가져가야 "늘 0초에 가까운" 상태가 됩니다.

마무리: lag을 없애는 순서

정리하면, MySQL Replication Lag 폭증을 다룰 때는 다음 순서가 가장 빠릅니다.

  1. IO 병목인지 Apply 병목인지 먼저 분류
  2. GTID로 전환해 운영·관측·재구성을 단순화
  3. 8.0에서는 LOGICAL_CLOCK 병렬 복제 + 워커 수 튜닝
  4. 병렬화가 안 되는 근본 원인(핫스팟, 큰 트랜잭션, DDL)을 제거
  5. 클라이언트 재시도/타임아웃을 보수적으로 설계해 2차 폭주를 막기

이 과정을 거치면 Seconds_Behind_*가 장시간 누적되는 구조적 문제를 대부분 제거할 수 있고, 피크에도 수 초 내로 따라잡는 "사실상 0초" 복제 상태에 가까워집니다.