Published on

MySQL Replication Lag 폭증 원인·튜닝 7단계

Authors

서버가 멀쩡해 보이는데도 Seconds_Behind_Master(또는 Seconds_Behind_Source)가 갑자기 수백~수천 초로 치솟는 순간이 있습니다. 이때 가장 흔한 실수는 원인을 특정하기 전에 무작정 파라미터를 만지거나, 반대로 “일시적이겠지” 하고 방치하는 것입니다. 복제 지연은 대개 I/O(바이너리 로그 읽기), SQL 적용(릴레이 로그 실행), 락/트랜잭션 구조, 스토리지/네트워크, 스키마/인덱스, 운영 이벤트(배치/DDL) 중 하나로 수렴합니다.

이 글은 “지연이 폭증했을 때”를 전제로, 원인 분류 → 병목 포인트 식별 → 안전한 튜닝 순서로 진행하는 7단계 플로우를 제공합니다.

> 참고: 쿠버네티스/클라우드 환경에서 네트워크/노드 이슈가 간헐적으로 섞여 들어오는 경우가 많습니다. 노드나 CNI 문제가 의심되면 EKS Node NotReady - CNI ENI 할당 실패 해결 가이드도 함께 확인해 두면 좋습니다.

1단계: “어느 스레드가 막혔는지” 먼저 분리하기

복제 지연은 크게 두 가지로 나뉩니다.

  • IO Thread 지연: 소스(마스터)에서 binlog를 못 가져옴(네트워크/권한/연결/디스크)
  • SQL Thread 지연: 릴레이 로그는 쌓이는데 적용이 느림(쿼리/락/인덱스/병렬성)

MySQL 8.0 기준으로 아래를 먼저 확인합니다.

-- 레플리카 상태 요약(MySQL 8.0)
SHOW REPLICA STATUS\G

-- 성능 스키마로 스레드 상태 확인
SELECT
  THREAD_ID, NAME, PROCESSLIST_STATE, PROCESSLIST_INFO
FROM performance_schema.threads
WHERE NAME LIKE 'thread/sql/%'
   OR NAME LIKE 'thread/io/%';

핵심 포인트:

  • Replica_IO_Running/Replica_SQL_Running 중 무엇이 No인지
  • Seconds_Behind_Source가 증가하는 동안 Relay_Log_Space가 증가하는지(=SQL 적용이 느림)
  • Last_Errno, Last_Error가 있는지(=지연이 아니라 “정지”)

> Seconds_Behind_*는 “현재 실행 중인 이벤트의 타임스탬프 기반”이라 0이어도 지연이 있을 수 있고, 반대로 갑자기 튈 수 있습니다. 반드시 릴레이 로그 적체량과 함께 보세요.

2단계: 지연 폭증 시점의 “단일 대형 트랜잭션”부터 의심하기

레플리카 SQL 스레드가 느린 가장 흔한 이유는 대형 트랜잭션 1개입니다.

  • 수백만 row UPDATE/DELETE
  • 인덱스 없는 조건으로 대량 변경
  • 대형 INSERT…SELECT
  • 큰 DDL(특히 온라인 DDL이 아닌 경우)

바이너리 로그가 row 기반(binlog_format=ROW)이면 이벤트 자체가 커지고, 레플리카에서 적용 비용이 커집니다.

확인 방법(소스에서):

-- 현재 긴 트랜잭션/락 확인
SELECT
  trx_id, trx_started, trx_mysql_thread_id, trx_rows_locked, trx_rows_modified
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- 오래 실행 중인 쿼리
SHOW FULL PROCESSLIST;

레플리카에서 “어떤 이벤트를 실행 중인지”는 SHOW REPLICA STATUS\GReplica_SQL_Running_State(또는 Slave_SQL_Running_State)가 힌트를 줍니다. Waiting for table metadata lock, Waiting for dependent transaction to commit 같은 문구가 보이면 다음 단계로 바로 넘어가세요.

3단계: 락/메타데이터 락(MDL)로 막히는지 확인하기

복제 지연이 폭증하는데 CPU는 놀고 있고, 디스크도 여유가 있다면 락 대기일 확률이 높습니다.

대표 패턴:

  • 레플리카에서 read 트래픽이 많고(리드 스케일), 긴 SELECT가 존재
  • DDL 또는 DML이 테이블 메타데이터 락(MDL)에 걸림
  • FK/트리거/갭락 등으로 잠금 범위가 커짐

MySQL 8.0에서 MDL/락 대기를 빠르게 확인:

-- 어떤 세션이 어떤 락을 기다리는지
SELECT
  r.trx_id waiting_trx_id,
  r.trx_mysql_thread_id waiting_thread,
  b.trx_id blocking_trx_id,
  b.trx_mysql_thread_id blocking_thread,
  TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) waiting_seconds
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id
ORDER BY waiting_seconds DESC;

-- performance_schema로 메타데이터 락 확인
SELECT *
FROM performance_schema.metadata_locks
WHERE LOCK_STATUS = 'PENDING';

해결 방향:

  • 레플리카의 긴 읽기 트랜잭션(특히 REPEATABLE READ)을 줄이기
  • DDL을 트래픽 저점에 수행하거나, 온라인 DDL 전략으로 변경
  • 애플리케이션에서 불필요한 테이블 스캔을 줄이고 인덱스를 보강

4단계: 병렬 복제(Parallel Replication) 설정을 점검하기

SQL 적용이 느린데 “단일 스레드”로 적용 중이면, 병렬화를 통해 지연을 크게 줄일 수 있습니다.

MySQL 8.0에서 핵심 변수:

  • replica_parallel_workers (예: 4~32)
  • replica_parallel_type (LOGICAL_CLOCK 권장)
  • replica_preserve_commit_order (일관성 위해 ON이 일반적)

예시 설정:

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

-- 병렬 워커 활성화(동적 적용 가능)
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;

주의할 점:

  • 병렬 워커를 늘려도 단일 대형 트랜잭션은 병렬화되지 않습니다.
  • 워커 수를 과도하게 올리면 레플리카에서 CPU/IO가 포화되어 오히려 지연이 악화될 수 있습니다.
  • Waiting for dependent transaction to commit가 자주 보이면 커밋 순서 보존/의존성이 병목일 수 있습니다(스키마/트랜잭션 구조 개선 필요).

5단계: 릴레이 로그/바이너리 로그 I/O 병목을 제거하기

IO Thread가 느리거나, SQL Thread가 빠른데도 Relay_Log_Space가 비정상적으로 증가한다면 디스크/로그 I/O를 확인해야 합니다.

체크리스트:

  • 레플리카 디스크가 gp2/gp3, io1/io2 등에서 IOPS 한계에 걸렸는지
  • relay_log가 느린 볼륨에 있지 않은지
  • sync_binlog, innodb_flush_log_at_trx_commit 조합이 과도하게 보수적인지(특히 레플리카)

레플리카는 “데이터 소스”가 아니고 재해복구/읽기 분산 목적이므로, 내구성 정책을 소스보다 완화할 수 있는 경우가 많습니다(조직의 RPO/RTO 정책에 따름).

예시(신중히 적용):

-- 레플리카에서만 내구성 완화(정책 검토 필수)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL sync_binlog = 0;

또한 네트워크가 원인일 수 있습니다. 같은 AZ/리전에 두었는지, NAT/프록시를 타는지, 패킷 드롭이 있는지 확인하세요. 쿠버네티스 환경이라면 노드/네트워크 이상이 애플리케이션 증상으로 먼저 보일 때가 많습니다(예: 이미지 풀 실패/노드 불안정). 비슷한 운영 맥락의 트러블슈팅 글로 EKS에서 kubectl port-forward 끊김·hang 해결도 참고가 됩니다.

6단계: “문제 쿼리/스키마”를 복제 친화적으로 바꾸기

복제 지연은 결국 레플리카가 실행해야 하는 SQL(또는 row 이벤트)의 비용 문제로 귀결됩니다. 다음 패턴은 지연을 구조적으로 키웁니다.

  • 조건 컬럼에 인덱스가 없어 레플리카에서 테이블 풀스캔 발생
  • 업데이트 대상이 넓고(대량), 트랜잭션이 길며, 커밋 빈도가 낮음
  • 핫 테이블에 단일 PK 순차 업데이트로 경합 증가

실전 개선 예시:

대량 UPDATE를 배치로 쪼개기

-- 나쁜 예: 한 번에 대량 업데이트
UPDATE orders
SET status='ARCHIVED'
WHERE created_at < NOW() - INTERVAL 180 DAY;

-- 개선: PK 범위/리밋 기반으로 분할(애플리케이션/배치에서 반복)
UPDATE orders
SET status='ARCHIVED'
WHERE id BETWEEN ? AND ?;

필요한 인덱스 추가(단, DDL 자체가 지연을 유발할 수 있음)

-- created_at 조건이 자주 쓰이면 인덱스 고려
ALTER TABLE orders
  ADD INDEX idx_orders_created_at (created_at);

DDL은 레플리카에서 적용될 때도 비용이 큽니다. 가능하면 온라인 DDL 도구(예: gh-ost, pt-online-schema-change)나 MySQL 8.0의 ALGORITHM=INPLACE/INSTANT 가능 여부를 검토하세요.

7단계: “회복(캐치업) 모드”로 안전하게 따라잡기

원인을 제거했는데도 이미 릴레이 로그가 많이 쌓였다면, 레플리카가 따라잡는 시간을 줄이는 운영 모드가 필요합니다.

권장 순서:

  1. 레플리카의 읽기 트래픽을 일시적으로 다른 노드로 우회(가능하면)
  2. 레플리카 자원(CPU/IOPS) 임시 증설
  3. 병렬 워커 조정 및 내구성 옵션(정책 범위 내) 완화
  4. 캐치업 후 원복

읽기 트래픽 우회는 서비스/배포 시스템과 맞물립니다. GitOps로 트래픽 라우팅/리드 엔드포인트를 바꾸는 환경이라면, 동기화/상태 꼬임이 함께 발생할 수 있으니 Argo CD Sync 실패 - OutOfSync·Degraded 해결법처럼 배포 상태까지 함께 점검하는 습관이 도움이 됩니다.

캐치업 중 모니터링할 지표:

  • Seconds_Behind_Source 추세(절대값보다 기울기)
  • Relay_Log_Space 감소 속도
  • 레플리카의 CPU iowait, 디스크 latency
  • performance_schema.replication_applier_status_by_worker(워커별 지연)
-- 워커별 적용 상태(8.0)
SELECT
  WORKER_ID,
  SERVICE_STATE,
  LAST_APPLIED_TRANSACTION,
  LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP,
  APPLYING_TRANSACTION,
  APPLYING_TRANSACTION_START_APPLY_TIMESTAMP
FROM performance_schema.replication_applier_status_by_worker
ORDER BY WORKER_ID;

운영에서 자주 놓치는 “지연 폭증” 트리거 5가지

마지막으로, 원인 분석을 빠르게 하기 위해 현장에서 자주 만나는 트리거를 정리합니다.

  1. 배치 작업이 평소보다 큰 범위로 실행(조건 누락/파티션 누락)
  2. DDL이 피크 타임에 수행되어 MDL 경합 발생
  3. 레플리카에 리드 트래픽 과다로 인해 SQL thread가 자원 경쟁
  4. 스토리지 크레딧/IOPS 한계(버스트 소진)로 릴레이 로그 I/O 급락
  5. 네트워크 순간 장애로 IO thread 재연결/재시도 반복

결론: 튜닝은 “원인 분류 → 병목 제거 → 캐치업” 순서로

Replication Lag 폭증은 대부분 “SQL 적용 병목(락/대형 트랜잭션/스키마)” 또는 “로그 I/O 병목(디스크/네트워크)”로 수렴합니다. 가장 빠른 해결은 파라미터를 무작정 올리는 것이 아니라, 1) IO/SQL 스레드 분리, 2) 대형 트랜잭션/락 확인, 3) 병렬 복제 점검, 4) I/O 병목 제거, 5) 쿼리/스키마 개선, 6) 캐치업 모드 운영의 순서로 접근하는 것입니다.

현 시점에서 환경(MySQL 5.7/8.0, GTID 사용 여부, binlog_format, 레플리카 수, 인스턴스 스펙/스토리지 타입)을 알려주면, 위 7단계를 당신의 조건에 맞춘 “우선순위 체크 SQL 세트”로 더 구체화해 드릴 수 있습니다.