InnoDB background threads: page cleaner, purge, IO thread의 역할
InnoDB page cleaner, purge, IO thread가 dirty page 정리, undo 정리, 비동기 I/O를 어떻게 분담하는지 운영 관점에서 정리한다.
1. 왜 InnoDB background thread를 이해해야 하는가
InnoDB는 foreground query thread만으로 동작하지 않는다. 사용자가 INSERT, UPDATE, DELETE, SELECT를 실행하는 동안 뒤에서는 page cleaner, purge, read/write IO thread, log writer, insert buffer merge, checkpoint 관련 작업이 계속 움직인다. 이 백그라운드 작업은 평소에는 눈에 잘 띄지 않지만, 쓰기 지연, history list 증가, checkpoint 압박, read latency 급증, shutdown 지연, crash recovery 시간 증가 같은 운영 이슈가 발생하면 원인을 구분하는 핵심 단서가 된다.
특히 다음과 같은 상황에서는 background thread의 역할을 모르면 잘못된 처방을 내리기 쉽다.
- dirty page가 많이 쌓여 쓰기 burst가 발생하는데 단순히
innodb_buffer_pool_size만 조정하는 경우 - purge가 밀려 undo history가 길어졌는데 오래 열린 트랜잭션을 보지 않고 I/O 성능만 의심하는 경우
- storage latency가 높은데
innodb_write_io_threads숫자만 늘리면 해결될 것으로 기대하는 경우 - Aurora MySQL에서 일반 MySQL의 file I/O 병목 모델을 그대로 적용하는 경우
InnoDB background thread는 “성능을 빠르게 만드는 부가 기능”이 아니라, MVCC, buffer pool, redo log, checkpoint, undo, 스토리지 I/O를 안정적으로 연결하는 운영 제어면이다. 이 글에서는 page cleaner, purge, IO thread를 중심으로 내부 동작과 진단 기준을 정리한다.
2. InnoDB 내부 작업의 큰 흐름
InnoDB는 transaction processing과 page management를 분리한다. foreground thread는 논리적 변경을 수행하고, InnoDB는 변경된 page를 buffer pool에 dirty page로 남긴 뒤 redo log로 내구성을 확보한다. 실제 data file에 page를 언제, 얼마나, 어떤 순서로 쓸지는 checkpoint와 page cleaner가 조정한다. 삭제되었거나 갱신되어 더 이상 필요 없는 old version은 purge thread가 undo log를 따라 정리한다. 물리적인 read/write 요청은 InnoDB I/O subsystem과 OS, storage가 함께 처리한다.
flowchart LR
Q[Foreground query thread] --> BP[Buffer pool page 변경]
Q --> REDO[Redo log 기록]
BP --> DIRTY[Dirty page 누적]
DIRTY --> PC[Page cleaner thread]
PC --> CKPT[Checkpoint 진전]
PC --> DATA[Data file write]
Q --> UNDO[Undo log / old row version]
UNDO --> PURGE[Purge thread]
PURGE --> IDX[불필요한 row version 및 secondary index entry 정리]
PC --> IO[InnoDB read/write IO threads]
PURGE --> IO
IO --> OS[OS / Storage]
이 구조에서 중요한 점은 각 thread가 독립적으로 보이지만 실제로는 서로의 지연을 증폭시킬 수 있다는 것이다. 예를 들어 page cleaner가 dirty page를 충분히 flush하지 못하면 checkpoint age가 커지고, 결국 foreground write가 flush를 기다릴 수 있다. purge가 오래 밀리면 undo tablespace와 history list가 커지고, secondary index 정리와 buffer pool 사용에도 영향을 준다. I/O thread나 storage 계층이 포화되면 page cleaner와 purge가 모두 느려진다.
3. Page cleaner: dirty page를 조절하고 checkpoint를 진전시키는 작업자
Page cleaner의 주요 임무는 buffer pool 안의 dirty page를 적절한 속도로 data file에 flush하는 것이다. InnoDB는 row 변경 시 매번 data page를 즉시 디스크에 쓰지 않는다. 대신 변경된 page를 buffer pool에 dirty 상태로 두고 redo log를 먼저 기록한다. 이 방식은 쓰기 경로를 효율화하지만, dirty page가 과도하게 쌓이면 checkpoint를 진전시키기 위해 급격한 flush가 필요해진다.
Page cleaner는 다음 기준을 함께 보며 flush를 수행한다.
- buffer pool의 dirty page 비율
- redo log checkpoint age와 log capacity
- 최근 write workload와 adaptive flushing 판단
- LRU list에서 free page 확보가 필요한 정도
innodb_io_capacity,innodb_io_capacity_max등으로 표현되는 예상 I/O 처리량
MySQL 5.6 이후 InnoDB에는 page cleaner thread가 분리되어 flush 작업을 더 명시적으로 담당한다. MySQL 8.0에서는 innodb_page_cleaners 설정이 남아 있지만, 일반적으로 buffer pool instance 수와 내부 정책에 따라 실제 효과가 제한될 수 있다. 운영자는 thread 숫자를 무작정 늘리기보다 dirty page, checkpoint, storage latency, redo log pressure를 함께 보아야 한다.
Page cleaner가 충분히 따라가지 못할 때 흔한 증상은 다음과 같다.
Innodb_buffer_pool_pages_dirty가 장시간 높게 유지된다.- write latency가 갑자기 튀는 구간이 생긴다.
- checkpoint age가 커져 foreground 작업이 flush 압박을 받는다.
- shutdown 또는 crash recovery가 길어진다.
- I/O 사용률은 높은데 query 자체의 논리적 변경량보다 data file write가 burst 형태로 나타난다.
Aurora MySQL에서는 storage 계층이 분산 로그 구조를 사용하므로 일반 MySQL의 local data file flush 모델과 완전히 같지 않다. 그러나 buffer pool dirty page 관리, checkpoint 압박, foreground가 storage 응답을 기다리는 현상은 여전히 운영상 중요하다. Aurora에서는 CloudWatch 지표, Performance Insights, SHOW ENGINE INNODB STATUS의 checkpoint 관련 정보, wait event를 함께 해석해야 한다.
4. Purge thread: MVCC가 남긴 과거 버전을 정리하는 작업자
InnoDB의 MVCC는 consistent read를 위해 과거 row version을 보존한다. UPDATE나 DELETE가 실행되면 기존 row의 이전 상태를 undo log로 추적할 수 있어야 하며, 아직 그 과거 버전을 볼 수 있는 트랜잭션이 남아 있으면 즉시 지울 수 없다. Purge thread는 더 이상 어떤 read view에서도 필요하지 않은 undo record와 관련 index entry를 정리한다.
Purge가 하는 일은 단순한 파일 삭제가 아니다. 대략 다음 흐름을 따른다.
- 오래된 read view가 없는지 확인한다.
- purge 가능한 undo record를 찾는다.
- delete-marked record를 실제로 제거한다.
- secondary index에서 더 이상 필요 없는 entry를 정리한다.
- undo tablespace truncate와 재사용 조건에 영향을 준다.
Purge가 밀리는 대표적인 원인은 오래 열린 트랜잭션이다. 예를 들어 어떤 세션이 START TRANSACTION 후 오랫동안 commit하지 않고 consistent read를 유지하면, 그 이후 변경된 row version 상당수가 purge 대상이 되지 못한다. 이때 쓰기 트래픽이 계속 들어오면 history list가 증가하고 undo 공간이 커지며, purge가 나중에 한꺼번에 많은 일을 처리해야 한다.
운영에서 purge 지연은 다음 문제로 이어질 수 있다.
- undo tablespace 사용량 증가
- buffer pool에 오래된 version과 관련 page가 남아 cache 효율 저하
- secondary index 정리 지연
- long-running transaction 종료 후 purge burst 발생
- replication replica 또는 batch job에서 read view가 오래 유지될 때 primary의 undo 부담 증가
MySQL 8.0에서는 innodb_purge_threads로 purge thread 수를 조정할 수 있다. 하지만 purge thread를 늘리는 것이 항상 해결책은 아니다. purge 대상이 없도록 막고 있는 오래 열린 transaction, I/O 병목, CPU 포화, secondary index가 많은 schema 구조, 대량 삭제 방식이 더 근본 원인일 수 있다.
5. InnoDB IO thread: 비동기 I/O 요청을 처리하는 계층
InnoDB I/O thread는 read와 write 요청을 처리하는 내부 작업자다. MySQL 설정에는 innodb_read_io_threads, innodb_write_io_threads가 있으며, 기본값은 일반적인 범용 환경을 대상으로 한다. 이 thread들은 buffer pool page read, data page write, read-ahead, flush 같은 물리 I/O 경로와 관련된다.
그러나 IO thread를 “스토리지 병목을 해결하는 간단한 병렬도 knob”으로 해석하면 위험하다. I/O 성능은 다음 요소의 조합으로 결정된다.
- 실제 storage latency와 IOPS/throughput 한계
- Linux native AIO 또는 io_uring 사용 여부와 MySQL 빌드/버전
- doublewrite, redo flush, fsync 정책
- buffer pool hit ratio와 read pattern
- page cleaner가 요청하는 flush 양
- checkpoint pressure
- OS scheduler, filesystem, cloud block storage 특성
IO thread 수가 너무 적으면 고성능 storage를 충분히 활용하지 못할 수 있다. 반대로 storage가 이미 포화되어 있거나 latency가 높은 상황에서 thread 수만 늘리면 대기열이 길어지고 tail latency가 악화될 수 있다. 운영에서는 “thread 수를 늘렸다”보다 “대기 시간이 줄었는가, dirty page와 checkpoint가 안정화되었는가, foreground wait가 줄었는가”를 확인해야 한다.
6. MySQL 8.0에서 확인할 기본 설정과 상태
다음 쿼리는 MySQL 8.0 이상에서 InnoDB background thread와 관련된 주요 설정값을 확인하는 예제다. 이 값들은 원인 분석의 출발점이지, 단독으로 좋고 나쁨을 판정하는 기준은 아니다.
SELECT VERSION() AS mysql_version;
SHOW VARIABLES
WHERE Variable_name IN (
'innodb_page_cleaners',
'innodb_purge_threads',
'innodb_read_io_threads',
'innodb_write_io_threads',
'innodb_io_capacity',
'innodb_io_capacity_max',
'innodb_max_dirty_pages_pct',
'innodb_max_dirty_pages_pct_lwm'
);
SHOW GLOBAL STATUS
WHERE Variable_name IN (
'Innodb_buffer_pool_pages_dirty',
'Innodb_buffer_pool_pages_total',
'Innodb_data_pending_reads',
'Innodb_data_pending_writes',
'Innodb_os_log_pending_fsyncs',
'Innodb_os_log_pending_writes',
'Innodb_history_list_length'
);
실행 결과(MySQL 8.0.46):
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
1 row in set (0.00 sec)
mysql> SHOW VARIABLES WHERE Variable_name IN (...);
+--------------------------------+-----------+
| Variable_name | Value |
+--------------------------------+-----------+
| innodb_io_capacity | 200 |
| innodb_io_capacity_max | 2000 |
| innodb_max_dirty_pages_pct | 90.000000 |
| innodb_max_dirty_pages_pct_lwm | 10.000000 |
| innodb_page_cleaners | 1 |
| innodb_purge_threads | 4 |
| innodb_read_io_threads | 4 |
| innodb_write_io_threads | 4 |
+--------------------------------+-----------+
8 rows in set (0.00 sec)
mysql> SHOW GLOBAL STATUS WHERE Variable_name IN (...);
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| Innodb_buffer_pool_pages_dirty | 0 |
| Innodb_buffer_pool_pages_total | 4096 |
| Innodb_data_pending_reads | 0 |
| Innodb_data_pending_writes | 0 |
| Innodb_os_log_pending_fsyncs | 0 |
| Innodb_os_log_pending_writes | 0 |
+--------------------------------+-------+
6 rows in set (0.00 sec)
결과를 해석할 때는 다음 원칙을 적용한다.
- dirty page 수는 전체 buffer pool page 수와 함께 비율로 본다.
- pending read/write가 순간적으로 0이 아니라고 해서 곧바로 장애는 아니다. 지속성과 latency를 함께 본다.
Innodb_history_list_length는 workload와 long transaction 여부를 함께 보아야 한다.innodb_io_capacity는 실제 storage 처리량을 반영하도록 조정해야 하며, 너무 높게 잡으면 flush가 공격적으로 변할 수 있다.
Background thread 자체는 Performance Schema thread 계측에서도 일부 확인할 수 있다. 이름은 버전과 빌드에 따라 조금 달라질 수 있으므로 LIKE 'thread/innodb/%' 패턴으로 먼저 관찰하는 편이 안전하다.
SELECT NAME, TYPE, PROCESSLIST_ID
FROM performance_schema.threads
WHERE NAME LIKE 'thread/innodb/%'
ORDER BY NAME
LIMIT 20;
실행 결과(MySQL 8.0.46):
mysql> SELECT NAME, TYPE, PROCESSLIST_ID
-> FROM performance_schema.threads
-> WHERE NAME LIKE 'thread/innodb/%'
-> ORDER BY NAME
-> LIMIT 20;
+-----------------------------------------+------------+----------------+
| NAME | TYPE | PROCESSLIST_ID |
+-----------------------------------------+------------+----------------+
| thread/innodb/buf_dump_thread | BACKGROUND | NULL |
| thread/innodb/buf_resize_thread | BACKGROUND | NULL |
| thread/innodb/clone_gtid_thread | BACKGROUND | NULL |
| thread/innodb/dict_stats_thread | BACKGROUND | NULL |
| thread/innodb/fts_optimize_thread | BACKGROUND | NULL |
| thread/innodb/io_ibuf_thread | BACKGROUND | NULL |
| thread/innodb/io_read_thread | BACKGROUND | NULL |
| thread/innodb/io_write_thread | BACKGROUND | NULL |
| thread/innodb/log_checkpointer_thread | BACKGROUND | NULL |
| thread/innodb/log_flusher_thread | BACKGROUND | NULL |
| thread/innodb/log_writer_thread | BACKGROUND | NULL |
+-----------------------------------------+------------+----------------+
20 rows in set (0.00 sec)
운영 서버에서는 위 결과에 thread 이름이 보이지 않거나 일부만 보일 수 있다. Performance Schema 설정, instrument 활성화 상태, MySQL minor version에 따라 관찰 범위가 달라지기 때문이다. 따라서 이 쿼리는 “어떤 내부 thread가 계측되는지 확인하는 진단 입구”로 사용하고, 최종 판단은 status 변수, InnoDB status, OS/storage 지표와 함께 내려야 한다.
7. page cleaner 지연을 의심할 때의 판단 순서
Page cleaner 문제는 보통 “쓰기 성능이 느리다”라는 증상으로 들어온다. 이때 바로 innodb_page_cleaners를 늘리는 방식은 좋은 순서가 아니다. 다음 순서로 좁혀 가는 것이 안전하다.
- dirty page 비율이 장시간 높은지 확인한다.
- redo log checkpoint pressure가 있는지 확인한다.
innodb_io_capacity와 실제 storage 처리량이 크게 어긋나는지 본다.- write latency가 storage 계층에서 증가했는지 본다.
- 대량 write batch, bulk load, secondary index rebuild, purge burst 같은 workload 이벤트를 확인한다.
- foreground query가
buf_flush,io/file/innodb, log flush 관련 wait를 보이는지 확인한다.
예를 들어 dirty page 비율이 높고 storage write latency도 높은데 innodb_io_capacity를 크게 올리면 InnoDB가 더 많은 flush를 요청하면서 전체 latency를 악화시킬 수 있다. 반대로 storage 여유가 충분한데 innodb_io_capacity가 지나치게 낮으면 dirty page가 불필요하게 쌓일 수 있다. 이 값은 “디스크가 이 정도는 안정적으로 처리할 수 있다”는 운영 가정에 가깝다.
Aurora MySQL에서는 EBS volume처럼 단일 local block device를 직접 튜닝하는 모델이 아니므로, innodb_io_capacity 해석도 조심해야 한다. Aurora storage와 compute 사이의 지연, redo 기반 분산 저장, cluster volume의 특성이 관여한다. 따라서 Aurora에서는 DB 파라미터뿐 아니라 VolumeWriteIOPs, commit latency 관련 wait, Performance Insights의 wait class를 함께 보는 것이 중요하다.
8. purge 지연을 의심할 때의 판단 순서
Purge 지연은 “디스크가 찼다”, “SELECT가 느려졌다”, “삭제했는데 공간이 줄지 않는다”, “replica lag가 늘었다” 같은 다른 증상으로 나타날 수 있다. 원인은 purge thread 자체보다 오래 열린 transaction인 경우가 많다.
점검 순서는 다음과 같다.
- history list length가 지속적으로 증가하는지 확인한다.
- 오래 열린 transaction이 있는지
information_schema.INNODB_TRX에서 확인한다. - batch job, backup, logical dump, long reporting query가 consistent read를 오래 유지하는지 확인한다.
- 대량
DELETE또는UPDATE가 purge 부하를 만든 시점을 확인한다. - secondary index가 많아 purge할 index entry가 과도하게 많은지 schema를 검토한다.
- purge thread 수를 늘리기 전에 I/O와 CPU 여유가 있는지 확인한다.
다음 쿼리는 현재 열린 InnoDB transaction을 오래된 순서로 확인하는 기본 예제다. 테스트 컨테이너에서는 결과가 비어 있을 수 있지만, 운영 서버에서는 오래 유지되는 transaction을 찾는 데 유용하다.
SELECT trx_id,
trx_started,
trx_state,
trx_rows_locked,
trx_rows_modified,
trx_mysql_thread_id
FROM information_schema.INNODB_TRX
ORDER BY trx_started
LIMIT 10;
실행 결과(MySQL 8.0.46):
mysql> SELECT trx_id,
-> trx_started,
-> trx_state,
-> trx_rows_locked,
-> trx_rows_modified,
-> trx_mysql_thread_id
-> FROM information_schema.INNODB_TRX
-> ORDER BY trx_started
-> LIMIT 10;
Empty set (0.00 sec)
이 결과에서 오래된 transaction이 보인다고 해서 즉시 kill해야 한다는 뜻은 아니다. 먼저 애플리케이션 요청, 배치, 백업, 복제 지연, 온라인 DDL과의 관계를 확인해야 한다. 그러나 purge가 명백히 막혀 있고 undo 사용량이 계속 증가한다면, 해당 transaction의 종료 또는 업무 로직 수정이 가장 직접적인 해결책일 수 있다.
9. IO thread 병목을 해석할 때의 주의점
IO thread 관련 문제는 OS와 storage 계층의 관측을 함께 보아야 한다. MySQL 안에서 pending write가 보인다고 해서 반드시 InnoDB thread 수가 부족한 것은 아니다. storage가 이미 포화되어 완료 응답이 늦는 경우에도 pending이 쌓인다.
운영자가 자주 오해하는 지점은 다음과 같다.
innodb_write_io_threads를 늘리면 모든 write latency가 줄어든다고 기대한다.- read latency가 높은데 buffer pool miss 원인과 query access pattern을 보지 않는다.
- cloud storage의 burst credit, network storage latency, noisy neighbor 가능성을 무시한다.
- MySQL status 변수의 순간값만 보고 장기 추세를 보지 않는다.
- Aurora에서 local file I/O 중심의 runbook을 그대로 적용한다.
일반 MySQL에서는 iostat, pidstat, filesystem latency, MySQL wait event를 함께 보는 것이 좋다. Aurora에서는 OS-level block device 지표를 직접 볼 수 없으므로 CloudWatch, Performance Insights, Enhanced Monitoring에서 DB load와 wait event를 중심으로 해석해야 한다.
10. 운영 체크리스트
Background thread 관련 장애나 성능 저하를 조사할 때는 다음 체크리스트를 사용한다.
-
innodb_io_capacity,innodb_io_capacity_max -
Innodb_history_list_length
11. 설정 변경의 기준
Background thread 관련 설정은 운영 환경에 따라 효과가 다르다. 다음 기준을 적용하면 불필요한 변경을 줄일 수 있다.
| 설정 | 변경을 검토할 상황 | 주의점 |
|---|---|---|
innodb_io_capacity |
storage 여유가 있는데 flush가 지나치게 보수적일 때 | 너무 높으면 flush가 공격적이 되어 latency를 키울 수 있다 |
innodb_io_capacity_max |
burst flush가 필요한 workload에서 상한 조정이 필요할 때 | checkpoint 압박 완화와 foreground latency를 함께 비교해야 한다 |
innodb_purge_threads |
purge 대상이 많고 CPU/I/O 여유가 있으며 purge가 지속적으로 밀릴 때 | 오래 열린 transaction이 원인이면 thread 증가만으로 해결되지 않는다 |
innodb_read_io_threads |
고성능 storage에서 concurrent read I/O 처리 여지가 있을 때 | buffer pool miss와 query access pattern이 먼저다 |
innodb_write_io_threads |
write I/O 병렬 처리 여지가 명확할 때 | storage 포화 상태에서는 대기열만 늘 수 있다 |
innodb_page_cleaners |
특수한 flush 병목 분석 후 제한적으로 검토 | MySQL 8.0에서는 실제 효과와 허용 범위를 버전별로 확인해야 한다 |
설정 변경은 단일 수치 튜닝이 아니라 가설 검증이어야 한다. 변경 전후에 같은 지표를 같은 시간대 또는 같은 workload에서 비교하고, rollback 기준을 정해 두는 것이 좋다.
12. 결론
InnoDB background thread는 보이지 않는 곳에서 MySQL의 일관성과 성능을 유지한다. Page cleaner는 dirty page와 checkpoint를 조절하고, purge thread는 MVCC가 남긴 과거 버전을 정리하며, IO thread는 물리 I/O 요청을 처리한다. 이 셋은 분리된 기능처럼 보이지만 실제 운영에서는 redo log, undo, buffer pool, secondary index, storage latency와 함께 움직인다.
따라서 background thread 문제를 다룰 때는 “thread 수를 늘릴 것인가”보다 “어떤 내부 부채가 어디에서 쌓이고 있는가”를 먼저 보아야 한다. Dirty page 부채인지, undo history 부채인지, storage 대기열인지, long transaction인지 구분하면 설정 변경의 방향도 명확해진다. 다음 글들에서는 이러한 내부 작업이 checkpoint, crash recovery, undo tablespace, locking 진단과 어떻게 연결되는지 더 세밀하게 이어서 다룰 수 있다.