- Published on
MySQL 8.0 쓰기폭증 - REDO 로그·버퍼풀 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 한가해 보이는데 디스크 쓰기만 폭증하고, fsync 대기 때문에 TPS가 출렁이는 현상은 MySQL 8.0에서 꽤 전형적인 장애 패턴입니다. 특히 InnoDB는 모든 변경을 곧바로 데이터 파일에 쓰지 않고, 먼저 REDO 로그에 기록한 뒤(내구성), 버퍼풀에 더티 페이지로 쌓아두었다가(캐시), 백그라운드에서 플러시(반영)합니다.
즉 “쓰기폭증”은 단순히 쿼리가 많아서가 아니라, REDO 로그 기록/플러시와 버퍼풀 더티 페이지 플러시의 균형이 깨질 때 발생합니다. 이 글에서는 MySQL 8.0 기준으로 REDO 로그·버퍼풀을 중심으로 원인 진단부터 튜닝까지, 운영에서 안전하게 적용하는 순서로 정리합니다.
관련해서 잠깐 곁가지로, 쓰기 지연이 데드락 빈도/양상을 바꾸기도 합니다. 데드락 원인 추적이 필요하면 MySQL 8.0 InnoDB 데드락 원인추적·해결 실전도 함께 참고하면 좋습니다.
1) 쓰기폭증을 “두 종류”로 분리해서 보기
쓰기폭증이라고 뭉뚱그리면 해결이 어렵습니다. 운영에서 체감되는 증상은 보통 아래 둘 중 하나(또는 혼합)입니다.
A. REDO 로그 플러시 병목
- 커밋이 느려짐
fsync또는 로그 파일 쓰기 대기 증가- TPS가 짧은 주기로 요동침(스파이크)
핵심은 “커밋 시점에 로그를 디스크에 얼마나 자주 강제 반영하느냐”입니다.
B. 버퍼풀 플러시 병목(더티 페이지 폭증)
- 특정 구간에서 디스크 쓰기가 길게 치솟음
- 버퍼풀에 더티 페이지가 과도하게 쌓였다가 한꺼번에 밀어내며 IO 폭주
- 읽기까지 느려짐(버퍼풀 미스 증가)
핵심은 “변경된 페이지를 얼마나 부드럽게(steady) 데이터 파일로 반영하느냐”입니다.
2) 먼저 확인할 관측 지표(필수)
튜닝은 관측 없이 하면 거의 확률게임이 됩니다. 아래는 최소 세트입니다.
InnoDB 상태/메트릭
SHOW ENGINE INNODB STATUS의LOG섹션SHOW GLOBAL STATUS:Innodb_os_log_fsyncsInnodb_os_log_writtenInnodb_log_waitsInnodb_buffer_pool_pages_dirtyInnodb_buffer_pool_reads
Performance Schema(가능하면)
MySQL 8.0에서는 Performance Schema가 진단에 매우 유용합니다.
-- 파일 IO 대기 상위(로그 파일/데이터 파일 구분)
SELECT
event_name,
count_star,
ROUND(sum_timer_wait/1e12, 2) AS sum_sec,
ROUND(avg_timer_wait/1e9, 2) AS avg_ms
FROM performance_schema.file_summary_by_event_name
WHERE event_name LIKE 'wait/io/file/%'
ORDER BY sum_timer_wait DESC
LIMIT 20;
-- InnoDB 로그 관련 상태를 한 번에
SHOW GLOBAL STATUS LIKE 'Innodb_os_log%';
SHOW GLOBAL STATUS LIKE 'Innodb_log%';
OS 레벨(간단히)
iostat -x 1에서await,svctm,util이 튀는지- NVMe라도
util이 100%에 붙거나await가 튀면 병목이 맞습니다.
3) REDO 로그 튜닝: “커밋 fsync”의 비용을 다루기
3-1. innodb_flush_log_at_trx_commit 이해
1(기본): 매 커밋마다 로그 버퍼를 로그 파일에 쓰고fsync까지 수행. 가장 안전(강한 내구성)하지만 쓰기압이 높으면 스파이크가 생기기 쉽습니다.2: 매 커밋마다 로그 파일에 쓰지만fsync는 1초에 한 번. 장애 시 최근 1초 내 트랜잭션 유실 가능.0: 로그 버퍼를 1초에 한 번 로그 파일로 flush. 유실 가능성이 더 커짐.
운영에서 “절대 유실 불가”가 아니라면 2는 체감 성능과 안정성 사이의 타협점이 되는 경우가 많습니다. 다만 이는 비즈니스 RPO 요구사항과 장애 시나리오(전원 장애 포함)를 먼저 합의해야 합니다.
-- 동적 변경 가능(즉시 반영)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
3-2. sync_binlog과의 조합(복제/포인트인타임 복구)
바이너리 로그도 내구성에 관여합니다.
sync_binlog=1: 매 트랜잭션마다 binlogfsyncsync_binlog=0또는N: OS flush에 의존하거나 N개마다fsync
InnoDB 로그만 느슨하게 하고 binlog를 강하게 잡으면 여전히 fsync 병목이 남을 수 있습니다. 반대로 둘 다 느슨하게 하면 성능은 좋아지지만 유실 윈도우가 커집니다.
SHOW VARIABLES LIKE 'sync_binlog';
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
3-3. REDO 로그 파일 크기: “너무 작으면 폭주, 너무 크면 복구가 느림”
MySQL 8.0에서는 REDO 로그 용량이 innodb_redo_log_capacity로 관리됩니다(환경에 따라 기존 innodb_log_file_size를 보는 경우도 있어 버전/설정 확인 필요).
REDO 로그가 너무 작으면 체크포인트가 자주 발생하고, 더티 페이지 플러시 압력이 커져 디스크 쓰기가 출렁일 수 있습니다. 반대로 너무 크게 잡으면 크래시 리커버리 시간이 늘어납니다.
권장 접근:
- “쓰기 워크로드에서 체크포인트/플러시가 잦아 보인다”면 REDO 용량을 늘려 변동성을 줄입니다.
- 단, 복구 시간 목표(RTO)를 고려합니다.
SHOW VARIABLES LIKE 'innodb_redo_log_capacity';
-- 구버전/호환 설정 확인용
SHOW VARIABLES LIKE 'innodb_log_file_size';
3-4. innodb_log_buffer_size: 큰 트랜잭션/대량 적재에 특히 중요
로그 버퍼가 작으면 큰 트랜잭션에서 중간 flush가 자주 발생해 지연이 튈 수 있습니다.
- OLTP(짧은 트랜잭션)만 있으면 기본값도 충분한 경우가 많습니다.
- 대량
INSERT ... SELECT, 대규모 배치 업데이트, 인덱스 재구성 등 “한 트랜잭션이 무거운” 작업이 있으면 키워야 합니다.
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
4) 버퍼풀 튜닝: 더티 페이지를 “부드럽게” 흘려보내기
쓰기폭증의 또 다른 축은 더티 페이지 플러시입니다. 핵심은 더티 비율이 임계치에 닿기 전에 꾸준히 플러시해서 IO를 평탄화하는 것입니다.
4-1. innodb_buffer_pool_size는 기본 중의 기본
버퍼풀이 작으면:
- 더티 페이지를 오래 못 들고 있음
- 자주 밀어내며 쓰기 증가
- 읽기 캐시 효율도 떨어져 전체 성능 악화
일반적인 전용 DB 서버라면 메모리의 60~75% 수준을 버퍼풀로 잡는 구성이 많습니다(공유 환경/컨테이너는 별도 계산 필요).
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
Innodb_buffer_pool_reads가 빠르게 증가한다면 디스크 읽기까지 늘고 있을 가능성이 높습니다.
4-2. 더티 페이지 임계치: innodb_max_dirty_pages_pct와 innodb_max_dirty_pages_pct_lwm
innodb_max_dirty_pages_pct: 더티 페이지 최대 목표치innodb_max_dirty_pages_pct_lwm: low water mark. 이 값부터 백그라운드 플러시를 더 적극적으로 시작
운영에서 흔한 패턴은 LWM이 너무 높거나(혹은 기본 동작이 워크로드에 안 맞아) 더티가 한참 쌓이다가 임계치 근처에서 급격히 플러시가 발생하는 것입니다.
접근 방법:
- 디스크가 감당 가능한 선에서 LWM을 낮춰 “미리미리” 플러시 유도
- 단, 너무 공격적으로 낮추면 불필요한 쓰기가 늘 수 있어 관측하면서 조정
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct_lwm';
4-3. innodb_io_capacity / innodb_io_capacity_max: 플러시 속도 상한선
InnoDB는 “디스크가 초당 어느 정도의 IO를 감당할 수 있는지”를 바탕으로 플러시 속도를 조절합니다.
- 값이 너무 낮으면: 더티가 쌓이고, 결국 한 번에 몰아서 쓰며 스파이크
- 값이 너무 높으면: 평소에도 과도하게 플러시하여 쓰기량 증가
NVMe 환경에서 기본값이 지나치게 보수적인 경우가 많습니다. iostat와 InnoDB dirty 추이를 보며 올려서 스파이크를 줄이는 쪽이 효과적일 때가 많습니다.
SHOW VARIABLES LIKE 'innodb_io_capacity';
SHOW VARIABLES LIKE 'innodb_io_capacity_max';
4-4. 플러시 방식: innodb_flush_method
리눅스에서 흔한 선택지는 O_DIRECT 계열입니다. OS 페이지 캐시와 InnoDB 버퍼풀이 이중 캐시가 되면 메모리 낭비/예측 불가한 flush가 생길 수 있습니다.
SHOW VARIABLES LIKE 'innodb_flush_method';
환경에 따라 최적값은 다르므로, 변경 시에는 반드시 재시작/롤백 계획과 함께 벤치마크를 권합니다.
5) “쓰기폭증”을 만드는 대표 시나리오와 처방
시나리오 1: 커밋이 갑자기 느려지고 fsync 대기 증가
- 원인 후보:
innodb_flush_log_at_trx_commit=1+ 높은 TPS, 또는sync_binlog=1까지 겹침 - 처방:
- RPO가 허용되면
innodb_flush_log_at_trx_commit=2검토 - binlog 내구성도 함께 설계(
sync_binlog) - 스토리지 latency가 튀는지 OS에서 확인
- RPO가 허용되면
시나리오 2: 일정 시간은 괜찮다가 주기적으로 디스크 쓰기 폭주
- 원인 후보: 더티 페이지가 쌓였다가 임계치 근처에서 급격히 플러시
- 처방:
innodb_io_capacity상향으로 평탄화innodb_max_dirty_pages_pct_lwm조정으로 조기 플러시- 버퍼풀 증설로 더티를 더 오래/안정적으로 흡수
시나리오 3: 배치 작업 시간에만 폭증
- 원인 후보: 대량 트랜잭션으로 redo/log buffer 압박, secondary index 갱신 폭증
- 처방:
- 배치 트랜잭션을 더 작은 단위로 커밋(애플리케이션 변경)
innodb_log_buffer_size상향 검토- 배치 시간대에만 리소스/파라미터를 다르게 가져가는 운영 전략도 고려
애플리케이션 계층의 병목이 DB 쓰기 패턴을 악화시키는 경우도 많습니다. 예를 들어 불필요한 다건 업데이트나 N+1로 쿼리 수가 폭증하면, 결국 redo와 더티 페이지가 늘어납니다. 이 관점은 Spring Boot JPA N+1 폭탄 - 배치·페치조인 튜닝도 함께 보면 연결이 됩니다.
6) 실전 튜닝 순서(안전한 변경 플로우)
아래 순서는 “원인 분리”와 “리스크 최소화”를 목표로 합니다.
- 관측부터 고정
- 1분 단위로
Innodb_os_log_fsyncs,Innodb_log_waits, dirty pages, TPS, p95/p99 latency를 함께 수집
- 1분 단위로
- REDO 플러시 병목인지 먼저 판정
Innodb_log_waits가 증가하고 커밋 지연이 동반되면 로그 플러시 쪽 가능성이 큼
- 버퍼풀/플러시 병목 판정
- 더티 페이지 비율이 치솟고,
iostat에서 datafile 쓰기 대기가 튀면 플러시 쪽
- 더티 페이지 비율이 치솟고,
- 동적 파라미터부터 점진 조정
- 예:
innodb_io_capacity,innodb_flush_log_at_trx_commit등
- 예:
- 재시작 필요한 변경은 마지막에
- 예: 버퍼풀 증설(대개 재시작), flush method 변경 등은 점검창에
7) 최소 재현 테스트(벤치마크) 예시
운영에 바로 적용하기 전에, 동일 스토리지/동일 설정에 가까운 스테이징에서 간단히 재현하는 것이 좋습니다.
sysbench로 쓰기 부하 만들기
아래는 대표적인 OLTP 쓰기 테스트입니다.
# 준비
sysbench /usr/share/sysbench/oltp_write_only.lua \
--mysql-host=127.0.0.1 --mysql-user=root --mysql-password='pass' \
--mysql-db=sbtest --tables=8 --table-size=100000 \
prepare
# 실행(쓰기 위주)
sysbench /usr/share/sysbench/oltp_write_only.lua \
--mysql-host=127.0.0.1 --mysql-user=root --mysql-password='pass' \
--mysql-db=sbtest --tables=8 --table-size=100000 \
--threads=64 --time=120 --report-interval=5 \
run
테스트 중에 다음을 같이 확인합니다.
- MySQL:
Innodb_os_log_fsyncs,Innodb_log_waits, dirty pages 추이 - OS:
iostat -x 1의await,util
파라미터를 바꿨을 때 “TPS만” 보지 말고, p95/p99 latency와 IO 스파이크가 줄었는지를 함께 봐야 합니다.
8) 자주 하는 실수 5가지
- 버퍼풀만 키우면 해결될 거라 믿기
- 로그
fsync병목이면 버퍼풀을 키워도 커밋 지연은 그대로입니다.
- 로그
innodb_io_capacity를 과감히 올리고 관측을 안 함- 플러시가 과도해져 오히려 쓰기량이 늘 수 있습니다.
- 내구성 요구사항 합의 없이
innodb_flush_log_at_trx_commit를 낮춤- 장애 시 데이터 유실 가능성을 명확히 문서화해야 합니다.
- binlog 내구성을 따로 보지 않음
sync_binlog가 병목을 만들거나, 반대로 복구 전략을 약화시킬 수 있습니다.
- 스파이크가 DB만의 문제라고 단정
- 스토리지 레이어(네트워크 블록 스토리지), 커널 flush, 컨테이너 IO 제한 등 외부 요인도 흔합니다.
9) 결론: “평탄화”가 목표다
MySQL 8.0의 쓰기폭증은 대개 REDO 로그 플러시와 버퍼풀 플러시의 균형이 무너질 때 나타납니다. 해결의 핵심은
- 커밋 내구성 정책(
innodb_flush_log_at_trx_commit,sync_binlog)을 요구사항에 맞게 정하고, - REDO 용량/로그 버퍼로 변동성을 완충하며,
innodb_io_capacity와 더티 페이지 임계치로 플러시를 평탄화하고,- 충분한 버퍼풀로 읽기/쓰기를 안정화하는 것 입니다.
무엇보다 “한 번에 크게 바꾸기”보다, 지표를 고정해두고 한 단계씩 바꾸면서 스파이크가 줄어드는지 확인하는 접근이 운영에서 가장 안전합니다.