MySQL 운영 아키텍처: InnoDB, redo/undo, buffer pool, doublewrite
운영자는 InnoDB를 장애 대응 언어로 읽어야 한다
MySQL을 운영하다 보면 문제는 대개 SQL 문장 하나로만 설명되지 않는다. 같은 쿼리라도 어느 날은 빠르고, 어느 날은 갑자기 디스크를 몰아치며 느려진다. 재시작 직후에는 평소보다 지연이 높고, 배치가 끝난 뒤에는 복제 지연이나 체크포인트 압력이 튄다. 이런 현상은 대부분 InnoDB가 메모리, 로그, 디스크 사이에서 데이터를 옮기는 방식과 연결되어 있다.
이번 장의 목표는 InnoDB 내부 구조를 암기하는 것이 아니다. 운영자가 사고를 분류할 때 필요한 지도를 만드는 것이다.
buffer pool은 데이터와 인덱스 페이지를 메모리에 얼마나 잘 붙잡고 있는가?redo log는 커밋된 변경을 장애 후 재생할 수 있을 만큼 안전하게 남기는가?undo log는 롤백과 consistent read를 위해 오래된 버전을 얼마나 오래 붙잡고 있는가?doublewrite buffer는 torn page 같은 물리적 쓰기 실패에서 페이지를 복구할 수 있게 해 주는가?- checkpoint와 flush는 쓰기 피크를 부드럽게 흡수하는가, 아니면 갑자기 응답시간을 밀어 올리는가?
MySQL 운영 아키텍처를 이렇게 읽으면 튜닝 파라미터가 단순한 숫자가 아니라 장애 시나리오의 일부로 보인다.
한 장으로 보는 InnoDB 데이터 경로
그림에서 중요한 순서는 두 가지다.
- 변경은 먼저 메모리의 페이지를 바꾸고, redo log에 복구 가능한 기록을 남긴 뒤 커밋된다.
- 실제 데이터 파일 쓰기는 나중에 background flush와 checkpoint를 통해 진행된다.
즉, 커밋과 데이터 파일 쓰기는 같은 순간이 아니다. 이 분리가 InnoDB의 성능을 만든다. 동시에 장애 후 복구, redo log 압력, dirty page flush 지연 같은 운영 이슈도 여기서 나온다.
Buffer pool: 성능 문제의 첫 번째 관문
buffer pool은 InnoDB가 테이블과 인덱스 페이지를 캐시하는 주 메모리 영역이다. 공식 문서도 전용 DB 서버에서는 물리 메모리의 상당 부분, 흔히 최대 80%까지 buffer pool에 배정한다고 설명한다. 단, 이 숫자는 정답이 아니라 출발점이다. OS page cache, 연결당 메모리, replication thread, 백업 에이전트, 모니터링 에이전트가 함께 쓰는 메모리를 남겨야 한다.
운영 관점에서 buffer pool은 세 가지 질문으로 본다.
| 질문 | 봐야 할 신호 | 해석 |
|---|---|---|
| working set이 메모리에 들어가는가 | Innodb_buffer_pool_reads 증가, hit rate 하락 | 디스크 읽기가 늘어 지연이 커질 수 있다 |
| dirty page가 과도한가 | dirty pages, checkpoint age, flush activity | 쓰기 피크 뒤에 flush storm이 올 수 있다 |
| 큰 scan이 캐시를 오염시키는가 | 백업, 리포트 쿼리 후 hit rate 하락 | 자주 쓰는 페이지가 밀려났을 수 있다 |
MySQL의 buffer pool LRU는 단순한 LRU가 아니다. 새로 읽은 페이지를 리스트 중간에 넣고, 자주 접근된 페이지를 young sublist로 올리는 방식이다. 이 구조는 큰 table scan이 평소 자주 쓰는 페이지를 한 번에 밀어내는 문제를 줄이기 위한 장치다. 그래도 mysqldump, 대량 리포트, 누락된 WHERE 조건의 scan은 여전히 운영 리스크가 된다.
기본 점검 쿼리는 간단하다.
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
SHOW ENGINE INNODB STATUS\GSHOW ENGINE INNODB STATUS의 BUFFER POOL AND MEMORY 섹션은 free page, database page, modified page, young/non-young page 움직임을 보여준다. 한 번의 스냅샷보다 배포, 배치, 백업 전후의 변화가 더 중요하다.
Redo log: 커밋을 빠르게 만들지만 checkpoint 압력을 만든다
redo log는 crash recovery를 위한 디스크 기반 로그다. 데이터 파일에 모든 변경을 즉시 반영하지 않아도, 커밋된 변경이 redo log에 남아 있으면 장애 후 checkpoint LSN부터 재생할 수 있다. 이것이 write-ahead logging의 핵심이다.
운영자가 redo log를 볼 때 흔히 놓치는 점은 redo log가 크면 무조건 좋은 것도, 작으면 무조건 나쁜 것도 아니라는 것이다.
- redo log capacity가 너무 작으면 dirty page를 빨리 밀어내야 해서 checkpoint와 flush가 공격적으로 동작할 수 있다.
- redo log capacity가 크면 쓰기 피크를 더 넓게 흡수할 수 있지만, 장애 후 복구해야 할 로그 범위와 디스크 사용량을 함께 고려해야 한다.
- MySQL 8.4에서는
innodb_redo_log_capacity로 전체 redo log capacity를 조정하고, 실제 적용된 용량은Innodb_redo_log_capacity_resized로 확인할 수 있다.
예시 점검:
SHOW VARIABLES LIKE 'innodb_redo_log_capacity';
SHOW STATUS LIKE 'Innodb_redo_log_capacity_resized';
SHOW STATUS LIKE 'Innodb_redo_log_resize_status';
SELECT FILE_NAME, START_LSN, END_LSN
FROM performance_schema.innodb_redo_log_files
ORDER BY START_LSN;쓰기 지연이 증가할 때는 redo log 자체만 보지 말고 같이 묶어서 본다.
| 현상 | 가능한 내부 원인 | 확인 방향 |
|---|---|---|
| commit latency 상승 | log flush 대기, fsync 지연 | storage latency, innodb_flush_log_at_trx_commit 정책 |
| 쓰기 피크 후 read도 느려짐 | dirty page flush가 I/O를 점유 | checkpoint age, disk queue, buffer pool dirty pages |
| 재시작 복구가 길어짐 | checkpoint 이후 redo 범위가 큼 | shutdown 방식, redo capacity, crash 시점 부하 |
innodb_flush_log_at_trx_commit=1은 트랜잭션 커밋마다 redo log flush를 강하게 보장하는 쪽에 가깝고, 다른 값은 처리량과 내구성 사이에서 타협한다. 운영 DB에서는 이 값을 단순히 성능 튜닝 레버로만 보면 위험하다. 장애 시 몇 초의 커밋 손실을 감당할 수 있는지, 상위 시스템이 재처리를 보장하는지까지 같이 결정해야 한다.
Undo log: 롤백보다 오래 실행되는 조회가 더 자주 문제를 만든다
undo log는 트랜잭션의 최신 변경을 되돌리기 위한 기록이다. 동시에 consistent read에서 다른 트랜잭션이 변경 전 row version을 볼 수 있게 해 준다. 그래서 undo는 단순히 ROLLBACK을 위한 로그가 아니라 MVCC의 핵심 재료다.
운영 장애에서 undo가 드러나는 대표 장면은 긴 트랜잭션이다.
- 오래 열린 트랜잭션이나 커서가 과거 스냅샷을 붙잡는다.
- 그 사이 많은
UPDATE와DELETE가 발생한다. - purge가 오래된 row version을 지우지 못한다.
- undo history가 길어지고, 테이블과 인덱스의 죽은 버전 정리가 늦어진다.
- 쿼리 지연, 디스크 증가, 복제 지연까지 이어질 수 있다.
확인 쿼리:
SELECT
trx_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec,
trx_state,
trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
SHOW ENGINE INNODB STATUS\GSHOW ENGINE INNODB STATUS에서 history list length가 계속 증가한다면, 단순히 디스크가 부족하다는 문제가 아니다. purge가 따라가지 못하는 이유를 찾아야 한다. 장시간 트랜잭션, 큰 batch transaction, 느린 replica, purge thread 압력, secondary index 과다 등이 원인이 될 수 있다.
Doublewrite buffer: 느리게 만드는 기능이 아니라 망가진 페이지를 막는 안전장치
doublewrite buffer는 buffer pool에서 flush되는 page를 최종 데이터 파일 위치에 쓰기 전에 별도 영역에 먼저 기록한다. OS, storage subsystem, mysqld 프로세스가 페이지 쓰기 중간에 실패하면 데이터 파일에는 반쯤 쓰인 페이지가 남을 수 있다. 이때 InnoDB는 doublewrite 영역의 온전한 사본을 사용해 복구할 수 있다.
공식 문서는 doublewrite가 데이터를 두 번 쓴다고 해서 I/O가 정확히 두 배가 되는 것은 아니라고 설명한다. doublewrite 영역에는 큰 순차 chunk로 기록되고, 일반적으로 한 번의 fsync()로 처리되기 때문이다. 그래도 쓰기 경로에 들어가는 안전장치인 것은 맞다.
운영 판단은 보수적으로 해야 한다.
- 일반 운영 DB에서는
innodb_doublewrite를 끄지 않는 쪽이 기본값이다. - benchmark나 일회성 적재처럼 데이터 무결성보다 속도가 중요한 상황에서만 예외를 검토한다.
- 스토리지가 atomic write를 보장한다고 주장해도, 모든 데이터 파일 경로와 장애 모델이 같은 보장을 받는지 확인해야 한다.
- MySQL 8.4의
DETECT_ONLY는 incomplete write 탐지만 하고 page content 복구에는 쓰지 않는 모드이므로,ON과 같은 보호 수준으로 보면 안 된다.
간단한 확인:
SHOW VARIABLES LIKE 'innodb_doublewrite';
SHOW VARIABLES LIKE 'innodb_doublewrite%';Change buffer와 secondary index의 운영 비용
change buffer는 buffer pool에 없는 secondary index page 변경을 임시로 모아 두는 구조다. 변경 대상 페이지를 즉시 읽어 오지 않고 나중에 merge하기 때문에, I/O-bound 쓰기 워크로드에서는 도움이 될 수 있다. 반대로 buffer pool 일부를 차지하고, merge가 밀리면 나중에 큰 I/O로 돌아온다.
MySQL 8.4 문서 기준으로 innodb_change_buffering의 기본값은 none이다. 과거 MySQL 운영 경험에서 change buffering을 당연히 켜져 있는 기능으로 기억하고 있다면 버전 차이를 확인해야 한다.
운영 판단 기준은 다음과 같다.
| 상황 | 판단 |
|---|---|
| secondary index가 많고 random insert/update가 많다 | change buffering의 이득을 테스트할 가치가 있다 |
| working set이 거의 buffer pool에 들어간다 | change buffer 이득이 작을 수 있다 |
| merge I/O 때문에 피크 이후 지연이 생긴다 | change buffer 크기와 활성화 범위를 줄이는 실험이 필요하다 |
| desc index 관련 제약이 있다 | change buffering 적용 여부를 문서 기준으로 확인한다 |
SHOW VARIABLES LIKE 'innodb_change_buffering';
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
SHOW ENGINE INNODB STATUS\G장애 상황별로 내부 구조를 연결하기
InnoDB 구성 요소는 따로 움직이지 않는다. 그래서 알림이 울렸을 때는 한 지표만 보지 말고 내부 경로를 따라가야 한다.
1. 쓰기 지연이 갑자기 커졌다
먼저 commit path와 flush path를 나눈다.
- commit 자체가 느린가: redo log fsync, storage latency, group commit 상태를 본다.
- commit은 괜찮은데 전체 쿼리가 느린가: dirty page flush, buffer pool eviction, checkpoint 압력을 본다.
- 특정 테이블만 느린가: secondary index 수, hot row, lock wait, change buffer merge 가능성을 본다.
2. 재시작 직후 읽기가 느리다
buffer pool이 차갑기 때문일 수 있다. MySQL은 buffer pool state 저장과 복원을 지원한다. 운영에서는 재시작 계획에 warmup 시간을 포함하고, 평소의 hot table과 index를 알고 있어야 한다.
SHOW VARIABLES LIKE 'innodb_buffer_pool_dump%';
SHOW VARIABLES LIKE 'innodb_buffer_pool_load%';3. 디스크 사용량이 계속 늘어난다
데이터 증가만 보지 말고 undo와 purge를 확인한다. 오래 열린 트랜잭션이 있으면 purge가 오래된 row version을 지우지 못한다. 또한 대량 DDL, bulk load, 임시 테이블, binlog 보존 정책도 함께 본다.
4. 백업이나 리포트 후 서비스 지연이 생긴다
큰 scan이 buffer pool을 밀어냈을 수 있다. 백업 도구, scan-resistant 설정, 리포트 replica 분리, read-ahead 동작, buffer pool hit rate 변화를 함께 본다.
운영 체크리스트
| 영역 | 확인할 것 | 실무 기준 |
|---|---|---|
| Buffer pool | 크기, hit rate, dirty page, scan 영향 | 배포·백업·배치 전후 trend로 본다 |
| Redo log | capacity, resize status, checkpoint 압력 | 쓰기 피크 흡수와 복구 시간의 균형을 잡는다 |
| Undo log | long transaction, history list, purge 지연 | 오래 열린 트랜잭션을 먼저 찾는다 |
| Doublewrite | enabled mode, storage 특성 | 운영 DB에서는 무결성 우선으로 판단한다 |
| Change buffer | 활성화 범위, max size, merge I/O | secondary index가 많은 쓰기 workload에서만 실험적으로 본다 |
| Flush/checkpoint | disk latency, I/O capacity, dirty page 비율 | commit path와 background flush path를 분리해 진단한다 |
마지막으로, InnoDB 파라미터는 한 번에 많이 바꾸지 않는다. innodb_buffer_pool_size, innodb_redo_log_capacity, innodb_io_capacity, innodb_flush_log_at_trx_commit, innodb_doublewrite, innodb_change_buffering은 서로 영향을 준다. 변경 전 기준선을 저장하고, 한 번에 하나의 가설만 검증해야 한다.
마무리
MySQL 운영 아키텍처의 핵심은 단순하다. 읽기는 buffer pool을 최대한 활용해야 하고, 쓰기는 redo log로 빠르게 안전성을 확보한 뒤 background flush로 데이터 파일에 반영한다. undo log는 MVCC와 롤백을 위해 과거 버전을 붙잡고, doublewrite buffer는 물리적 페이지 쓰기 실패에서 마지막 안전망이 된다.
운영자가 해야 할 일은 이 구조를 장애 대응 질문으로 바꾸는 것이다. "CPU가 높다"가 아니라 "buffer pool miss 때문에 디스크 읽기가 늘었는가", "커밋이 느리다"가 아니라 "redo fsync가 병목인가, checkpoint flush가 병목인가", "디스크가 늘었다"가 아니라 "undo purge가 막혔는가"를 묻는 식이다. 이 질문이 가능해지면 다음 장의 replication lag, online DDL, PITR 같은 주제도 훨씬 덜 추상적으로 보인다.
References
- MySQL 8.4 Reference Manual,
17.4 InnoDB Architecture: https://dev.mysql.com/doc/refman/8.4/en/innodb-architecture.html - MySQL 8.4 Reference Manual,
17.5.1 Buffer Pool: https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html - MySQL 8.4 Reference Manual,
17.5.2 Change Buffer: https://dev.mysql.com/doc/refman/8.4/en/innodb-change-buffer.html - MySQL 8.4 Reference Manual,
17.6.4 Doublewrite Buffer: https://dev.mysql.com/doc/refman/8.4/en/innodb-doublewrite-buffer.html - MySQL 8.4 Reference Manual,
17.6.5 Redo Log: https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html - MySQL 8.4 Reference Manual,
17.6.6 Undo Logs: https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html - Percona Blog,
Understanding the Differences Between InnoDB Undo Log and Redo Log: https://www.percona.com/blog/understanding-the-differences-between-innodb-undo-log-and-redo-log/