---
title: "Buffer Pool dirty page와 flush 정책: checkpoint와 write pressure"
description: "InnoDB Buffer Pool의 dirty page, checkpoint, flush 정책, write pressure를 운영 관점에서 정리한다."
tags: [ MySQL, InnoDB, 성능최적화, 운영 ]
image: "mysql-report-bg.png"
published: "2026-06-01"
updated: "2026-06-01"
author: "MySQL 기술 노트"
source_url: ""
---

## 1. 왜 dirty page와 flush 정책을 알아야 하는가

InnoDB는 변경된 데이터를 즉시 데이터 파일에 모두 기록하지 않는다. 트랜잭션이 `COMMIT`될 때 redo log를 먼저 안전하게 기록하고, 실제 데이터 페이지는 Buffer Pool 안에서 변경된 상태로 머무르다가 나중에 디스크로 flush된다. 이때 Buffer Pool 안에서 메모리 내용은 최신이지만 데이터 파일에는 아직 반영되지 않은 페이지를 **dirty page**라고 한다.

이 구조는 쓰기 성능을 높인다. 같은 페이지가 짧은 시간에 여러 번 변경되더라도 매번 데이터 파일에 쓰지 않고, 메모리에서 변경을 합친 뒤 한 번에 기록할 수 있기 때문이다. 그러나 dirty page가 과도하게 누적되면 다른 문제가 생긴다.

- checkpoint가 뒤처져 crash recovery 시간이 길어진다.
- redo log 여유 공간이 줄어들어 foreground transaction이 flush를 기다릴 수 있다.
- Buffer Pool에 free page가 부족해져 읽기 작업까지 page cleaner 작업에 간접적으로 영향을 받는다.
- 스토리지 I/O가 평소에는 낮다가 특정 시점에 급격히 몰리는 write spike가 발생한다.

따라서 dirty page 관리는 단순한 내부 구현 세부사항이 아니라, 쓰기 지연, 복구 시간, 스토리지 IOPS, Aurora MySQL 운영 방식까지 연결되는 핵심 운영 주제다.

## 2. 핵심 개념: clean page, dirty page, redo log, checkpoint

InnoDB Buffer Pool의 page는 크게 clean page와 dirty page로 나눌 수 있다.

- **clean page**: 메모리의 페이지 내용과 디스크 데이터 파일의 페이지 내용이 같은 상태다. 필요하면 별도 기록 없이 Buffer Pool에서 제거할 수 있다.
- **dirty page**: 메모리에서 변경되었지만 아직 데이터 파일에 기록되지 않은 상태다. 제거되기 전에 반드시 flush되어야 한다.

트랜잭션 변경은 먼저 Buffer Pool의 page에 적용되고, redo log에도 변경 이력이 기록된다. redo log는 crash recovery 시 데이터 파일에 반영되지 않은 변경을 다시 적용하기 위한 순차 로그다. InnoDB는 redo log의 특정 위치까지 데이터 파일이 충분히 따라왔다고 판단할 수 있는 지점을 checkpoint로 관리한다.

```mermaid
flowchart LR
    A[사용자 UPDATE/INSERT] --> B[Buffer Pool page 변경]
    B --> C[Dirty page 생성]
    B --> D[Redo log 기록]
    D --> E[COMMIT 내구성 확보]
    C --> F[Page cleaner flush]
    F --> G[Tablespace data file]
    G --> H[Checkpoint 진전]
```

중요한 점은 `COMMIT`과 dirty page flush가 같은 순간에 반드시 일어나지 않는다는 것이다. 일반적으로 `COMMIT`은 redo log 내구성을 기준으로 완료되고, dirty page는 page cleaner thread가 배경에서 점진적으로 데이터 파일에 기록한다. 이 분리가 InnoDB 쓰기 성능의 기반이다.

## 3. checkpoint가 의미하는 운영상의 경계

checkpoint는 “여기까지의 변경은 데이터 파일에 충분히 반영되었다”는 경계에 가깝다. checkpoint 이후의 redo log 구간은 장애 복구 시 다시 적용될 수 있어야 한다. 따라서 checkpoint가 너무 오래 진전되지 않으면 다음 문제가 생긴다.

1. redo log 파일 또는 redo log capacity 안에서 재사용 가능한 공간이 줄어든다.
2. dirty page flush를 더 강하게 밀어붙여야 한다.
3. 장애 복구 시 스캔하고 적용해야 할 redo 범위가 길어진다.
4. 쓰기 부하가 높은 상황에서는 사용자 트랜잭션이 flush 압력의 영향을 받을 수 있다.

MySQL 8.0에서는 `innodb_redo_log_capacity`가 redo log 전체 용량을 이해하는 중심 변수다. 과거 버전의 `innodb_log_file_size`, `innodb_log_files_in_group` 조합과 운영 감각이 다르므로, 신규 운영 기준은 MySQL 8.0 이상 변수 체계를 기준으로 보는 편이 좋다.

## 4. InnoDB flush 정책의 큰 흐름

InnoDB의 dirty page flush는 하나의 단순한 규칙으로만 움직이지 않는다. 여러 압력이 동시에 작동한다.

### 4.1 LRU flush

Buffer Pool에 새로운 page를 읽어올 공간이 필요할 때, LRU list의 끝 쪽에서 희생 후보 페이지를 찾는다. 후보가 clean page이면 바로 제거할 수 있지만, dirty page이면 먼저 데이터 파일에 기록해야 한다. 읽기 부하 중 갑자기 지연이 커지는 경우에는 “읽기 자체가 느려졌다”기보다, 읽기 전에 dirty page를 밀어내야 하는 상황일 수 있다.

### 4.2 Flush list flush

dirty page는 변경된 순서와 redo log 위치 관점에서 flush list에 연결된다. checkpoint를 진전시키려면 오래된 redo log 위치를 참조하는 dirty page를 먼저 기록해야 한다. redo log 여유 공간이 줄어들수록 flush list 기반 flush 압력은 강해진다.

### 4.3 Adaptive flushing

`innodb_adaptive_flushing`은 dirty page 비율, redo log 발생 속도, checkpoint age 등을 보고 flush 속도를 조정하는 기능이다. 쓰기 부하가 커질 때 미리 flush를 늘려 나중에 급격한 동기 flush가 발생하지 않도록 완충하는 역할을 한다. 대부분의 운영 환경에서는 이 기능을 켜 둔 상태가 기본 선택이다.

### 4.4 Neighbor flushing과 SSD 환경

과거 회전식 디스크에서는 인접 페이지를 함께 flush하는 것이 유리할 수 있었다. SSD, NVMe, 클라우드 블록 스토리지에서는 무작정 인접 page를 함께 쓰는 것이 항상 이득이 아니다. `innodb_flush_neighbors`는 최신 SSD 중심 환경에서 보통 낮게 유지하거나 기본값을 확인해 운영한다. 단, 구체적인 값은 MySQL 버전, 스토리지 특성, 워크로드에 따라 판단해야 한다.

## 5. write pressure가 발생하는 전형적인 경로

write pressure는 dirty page를 디스크로 밀어내야 하는 압력이 foreground 작업에 체감되는 상태라고 볼 수 있다. 대표적인 경로는 다음과 같다.

```mermaid
sequenceDiagram
    participant App as 애플리케이션
    participant BP as InnoDB Buffer Pool
    participant Redo as Redo Log
    participant Cleaner as Page Cleaner
    participant Disk as Data File/Storage

    App->>BP: 대량 UPDATE/INSERT
    BP->>Redo: redo log 생성 증가
    BP->>BP: dirty page 비율 증가
    Cleaner->>Disk: background flush 수행
    Redo->>Cleaner: checkpoint age 증가로 flush 압력 상승
    App->>BP: 새 page 필요 또는 추가 변경
    BP-->>App: free page 부족/flush 대기 시 지연 증가
```

운영자가 체감하는 증상은 다음과 같이 나타날 수 있다.

- 평소보다 `COMMIT` 또는 DML latency가 길어진다.
- `Innodb_buffer_pool_pages_dirty`가 높은 상태로 오래 유지된다.
- `Innodb_buffer_pool_wait_free`가 증가한다.
- 스토리지 write IOPS 또는 write latency가 일정 시간 급격히 상승한다.
- checkpoint 관련 metric이 계속 밀리고 recovery 예상 시간이 커진다.

중요한 점은 dirty page 비율이 높다는 사실만으로 항상 장애는 아니라는 것이다. 충분한 redo log capacity, 안정적인 page cleaner 처리량, 낮은 스토리지 latency가 함께 보장되면 높은 dirty page 비율도 정상 운영 범위일 수 있다. 반대로 dirty page 비율이 절대적으로 높지 않아도 스토리지가 느리거나 redo log 여유가 작으면 write pressure가 빨리 foreground로 번질 수 있다.

## 6. 운영 진단 SQL: 현재 설정과 상태 확인

다음 SQL은 MySQL 8.0 기준으로 Buffer Pool dirty page와 flush 관련 설정, 상태 변수, InnoDB metric 존재 여부를 확인하는 기본 진단 예제다. 실제 운영에서는 한 번의 스냅샷보다 일정 간격으로 여러 번 수집해 변화율을 보는 것이 중요하다.

```sql
SELECT VERSION() AS mysql_version;

SHOW VARIABLES
WHERE Variable_name IN (
  'innodb_buffer_pool_size',
  'innodb_page_size',
  'innodb_max_dirty_pages_pct',
  'innodb_max_dirty_pages_pct_lwm',
  'innodb_io_capacity',
  'innodb_io_capacity_max',
  'innodb_adaptive_flushing',
  'innodb_flush_neighbors',
  'innodb_redo_log_capacity'
);

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Innodb_buffer_pool_pages_total',
  'Innodb_buffer_pool_pages_dirty',
  'Innodb_buffer_pool_pages_free',
  'Innodb_buffer_pool_wait_free',
  'Innodb_data_fsyncs',
  'Innodb_data_pending_fsyncs',
  'Innodb_data_pending_writes',
  'Innodb_pages_written'
);

SELECT NAME, SUBSYSTEM, COUNT, STATUS
FROM information_schema.INNODB_METRICS
WHERE NAME IN (
  'buffer_flush_adaptive_total_pages',
  'buffer_flush_background_total_pages',
  'buffer_flush_lru_total_pages',
  'buffer_flush_sync_total_pages'
)
ORDER BY NAME;
```

실행 결과(MySQL 8.0.46):

```text
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_adaptive_flushing       | ON        |
| innodb_buffer_pool_size        | 67108864  |
| innodb_flush_neighbors         | 0         |
| 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_size               | 16384     |
| innodb_redo_log_capacity       | 104857600 |
+--------------------------------+-----------+
9 rows in set (0.01 sec)

mysql> SHOW GLOBAL STATUS WHERE Variable_name IN (...);

+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| Innodb_buffer_pool_pages_dirty | 0     |
| Innodb_buffer_pool_pages_free  | 2930  |
| Innodb_buffer_pool_pages_total | 4096  |
| Innodb_buffer_pool_wait_free   | 0     |
| Innodb_data_fsyncs             | 99    |
| Innodb_data_pending_fsyncs     | 0     |
| Innodb_data_pending_writes     | 0     |
| Innodb_pages_written           | 191   |
+--------------------------------+-------+
8 rows in set (0.00 sec)

mysql> SELECT NAME, SUBSYSTEM, COUNT, STATUS
    -> FROM information_schema.INNODB_METRICS
    -> WHERE NAME IN (...)
    -> ORDER BY NAME;

+-------------------------------------+-----------+-------+----------+
| NAME                                | SUBSYSTEM | COUNT | STATUS   |
+-------------------------------------+-----------+-------+----------+
| buffer_flush_adaptive_total_pages   | buffer    |     0 | disabled |
| buffer_flush_background_total_pages | buffer    |     0 | disabled |
| buffer_flush_sync_total_pages       | buffer    |     0 | disabled |
+-------------------------------------+-----------+-------+----------+
3 rows in set (0.00 sec)
```

결과를 해석할 때는 다음 관점을 함께 본다.

- `Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total` 비율이 장시간 높은가?
- `Innodb_buffer_pool_pages_free`가 낮은 상태에서 `Innodb_buffer_pool_wait_free`가 증가하는가?
- `Innodb_pages_written` 증가율이 평소보다 급격히 높거나 낮은가?
- pending write/fsync가 쌓이는 시간이 있는가?
- adaptive, background, LRU, sync flush 중 어떤 계열의 metric이 증가하는가?

단일 값보다 변화율이 중요하므로 운영 모니터링에서는 1분 또는 5분 단위 rate로 보는 것이 좋다.

## 7. dirty page 비율 계산 예제

다음 예제는 현재 상태 변수에서 dirty page 비율을 계산한다. 테스트 컨테이너처럼 부하가 없는 환경에서는 dirty page 수가 0에 가깝게 나오는 것이 정상이다.

```sql
SELECT ROUND(dirty.Variable_value / total.Variable_value * 100, 2) AS dirty_page_pct,
       dirty.Variable_value AS dirty_pages,
       total.Variable_value AS total_pages
FROM performance_schema.global_status AS dirty
JOIN performance_schema.global_status AS total
WHERE dirty.Variable_name = 'Innodb_buffer_pool_pages_dirty'
  AND total.Variable_name = 'Innodb_buffer_pool_pages_total';
```

실행 결과(MySQL 8.0.46):

```text
mysql> SELECT ROUND(dirty.Variable_value / total.Variable_value * 100, 2) AS dirty_page_pct,
    ->        dirty.Variable_value AS dirty_pages,
    ->        total.Variable_value AS total_pages
    -> FROM performance_schema.global_status AS dirty
    -> JOIN performance_schema.global_status AS total
    -> WHERE dirty.Variable_name = 'Innodb_buffer_pool_pages_dirty'
    ->   AND total.Variable_name = 'Innodb_buffer_pool_pages_total';

+----------------+-------------+-------------+
| dirty_page_pct | dirty_pages | total_pages |
+----------------+-------------+-------------+
|              0 | 0           | 4096        |
+----------------+-------------+-------------+
1 row in set (0.00 sec)
```

운영 환경에서는 이 값을 절대 기준 하나로만 판단하지 않는다. 예를 들어 `innodb_max_dirty_pages_pct`가 90이라고 해서 89%까지 항상 안전하다는 뜻은 아니다. 스토리지 write latency가 높거나 redo log capacity가 작으면 훨씬 낮은 비율에서도 문제가 발생할 수 있다. 반대로 충분히 큰 Buffer Pool과 빠른 스토리지를 가진 배치 시스템은 일시적으로 높은 dirty page 비율을 허용해도 전체 처리량이 더 좋을 수 있다.

## 8. 설정 변수의 의미와 조정 방향

### 8.1 innodb_io_capacity와 innodb_io_capacity_max

`innodb_io_capacity`는 InnoDB가 백그라운드 작업에서 기대하는 일반적인 I/O 처리 능력의 힌트다. `innodb_io_capacity_max`는 flush 압력이 커졌을 때 사용할 수 있는 상한에 가깝다. 값이 너무 낮으면 dirty page가 늦게 빠지고 checkpoint가 밀릴 수 있다. 값이 너무 높으면 백그라운드 flush가 평상시 애플리케이션 I/O와 경쟁해 read/write latency를 흔들 수 있다.

클라우드 환경에서는 스토리지의 advertised IOPS만 보고 값을 정하면 부족하다. 실제 볼륨 크기, burst credit, latency, multi-tenant 영향, 파일 시스템, flush/fsync 특성을 함께 봐야 한다.

### 8.2 innodb_max_dirty_pages_pct와 lwm

`innodb_max_dirty_pages_pct`는 dirty page 비율에 대한 목표 상한으로 이해할 수 있다. `innodb_max_dirty_pages_pct_lwm`은 더 낮은 비율에서 미리 flush를 시작하도록 하는 low water mark다. write spike를 줄이려면 너무 늦게 flush를 시작하는 것보다 낮은 지점부터 완만하게 flush하는 편이 유리할 수 있다.

다만 dirty page 비율을 무조건 낮게 유지하는 것이 정답은 아니다. 너무 낮게 잡으면 InnoDB가 변경 병합의 이점을 충분히 활용하지 못하고, 백그라운드 write가 과도하게 늘 수 있다.

### 8.3 innodb_redo_log_capacity

redo log capacity가 충분하면 checkpoint가 조금 늦어져도 InnoDB가 더 여유 있게 flush를 조정할 수 있다. 대량 적재, 배치 UPDATE, 피크 시간 쓰기 집중이 있는 시스템에서는 redo log capacity가 작을 때 write pressure가 더 빨리 발생한다.

그러나 redo log capacity를 키우면 crash recovery에서 처리해야 할 잠재적 redo 범위도 커질 수 있다. 운영 목표가 “짧은 복구 시간”인지 “피크 쓰기 처리량”인지에 따라 균형이 필요하다.

## 9. Aurora MySQL에서의 해석 차이

Aurora MySQL은 스토리지 계층이 일반 MySQL과 다르다. 데이터는 분산 스토리지에 기록되고, redo 성격의 로그 레코드 전송과 스토리지 계층 복제가 핵심 설계다. 따라서 로컬 디스크에 데이터 파일을 직접 flush하는 전통적인 MySQL의 감각을 그대로 적용하면 안 된다.

그렇다고 dirty page와 checkpoint 개념이 운영상 사라지는 것은 아니다. Aurora에서도 Buffer Pool에는 변경된 page가 존재하고, writer instance의 메모리 압력, checkpoint/flush 관련 내부 작업, 커밋 경로, replica lag, failover 후 cache warm-up은 여전히 중요하다. 다만 해석은 다음처럼 조정해야 한다.

- OS 블록 디바이스 write latency보다 Aurora 스토리지 계층 지표와 CloudWatch 지표를 함께 본다.
- Performance Insights에서 commit latency, wait event, storage 관련 대기 이벤트를 함께 확인한다.
- writer failover 후 새 writer의 Buffer Pool warm-up 상태와 쓰기 지연 변화를 구분한다.
- 파라미터 그룹에서 조정 가능한 InnoDB 변수와 Aurora가 내부적으로 관리하는 영역을 구분한다.

Aurora에서는 “디스크가 느리니 dirty page flush가 밀린다”라는 단순 설명보다, 스토리지 서비스와 인스턴스 메모리/로그 경로를 함께 보는 관점이 필요하다.

## 10. 장애 모드와 흔한 오해

### 10.1 dirty page가 높으면 무조건 나쁜가

아니다. dirty page는 InnoDB가 쓰기를 효율화하기 위해 의도적으로 허용하는 상태다. 문제는 dirty page가 “얼마나 높은가”보다 “얼마나 오래 높은가”, “checkpoint가 얼마나 밀리는가”, “foreground 지연으로 전파되는가”다.

### 10.2 스토리지 IOPS를 올리면 항상 해결되는가

부분적으로만 맞다. 스토리지 처리량이 병목이면 도움이 되지만, `innodb_io_capacity`가 낮거나 redo log capacity가 작거나 대량 DML 패턴이 한 테이블/인덱스에 집중되어 있으면 효과가 제한될 수 있다. 반대로 설정을 과하게 높이면 background flush가 애플리케이션 I/O를 압박할 수 있다.

### 10.3 checkpoint age를 줄이면 항상 좋은가

짧은 recovery time 관점에서는 유리하지만, 너무 공격적인 flush는 쓰기 병합 효과를 줄이고 I/O를 평탄화하기보다 상시 압력으로 만들 수 있다. RTO, 스토리지 비용, 피크 처리량 사이의 균형을 잡아야 한다.

### 10.4 테스트 환경에서 문제가 없으면 운영도 안전한가

아니다. dirty page와 flush 문제는 부하 패턴, Buffer Pool 크기, redo log capacity, 스토리지 latency, 배치 작업 시간대에 강하게 의존한다. 작은 테스트 컨테이너에서 SQL이 실행되는 것은 진단 쿼리의 문법과 객체 존재 확인에 가깝고, 실제 write pressure 재현은 운영과 유사한 데이터 크기와 I/O 한계가 있어야 의미가 있다.

## 11. 운영 체크리스트

- [ ] `Innodb_buffer_pool_pages_dirty` 비율을 총 page 대비 rate와 함께 모니터링한다.
- [ ] `Innodb_buffer_pool_wait_free`가 증가하는지 확인한다. 증가한다면 free page 부족이 foreground 지연으로 번지는 신호일 수 있다.
- [ ] `Innodb_pages_written`, pending writes/fsyncs를 스토리지 write latency와 함께 본다.
- [ ] `innodb_io_capacity`, `innodb_io_capacity_max`가 실제 스토리지 처리 능력과 지나치게 동떨어져 있지 않은지 검토한다.
- [ ] `innodb_redo_log_capacity`가 피크 쓰기량과 복구 시간 목표 사이에서 적절한지 평가한다.
- [ ] 대량 배치 작업은 피크 시간과 분리하거나 chunk 단위로 나누어 checkpoint와 flush 압력을 완화한다.
- [ ] Aurora MySQL에서는 CloudWatch, Performance Insights, wait event, 파라미터 그룹 제약을 함께 확인한다.
- [ ] 설정 변경은 한 번에 여러 개를 바꾸지 말고, 변경 전후의 dirty page 비율, write latency, checkpoint 관련 지표를 비교한다.
- [ ] 장애 대응 중에는 dirty page를 즉시 0에 가깝게 만들려고 하기보다 foreground latency와 redo/checkpoint 압력을 낮추는 방향으로 판단한다.

## 12. 결론

Buffer Pool dirty page는 InnoDB가 쓰기를 빠르게 처리하기 위해 사용하는 정상적인 메커니즘이다. 그러나 dirty page가 checkpoint, redo log capacity, page cleaner 처리량, 스토리지 latency와 맞물리면 write pressure가 되고, 이 압력은 DML 지연과 복구 시간 증가로 나타날 수 있다.

운영자는 dirty page 비율 하나만 보지 말고, free page 부족, pending write, page flush 계열 metric, redo log capacity, 스토리지 지표를 함께 해석해야 한다. 특히 Aurora MySQL에서는 전통적인 로컬 디스크 flush 관점과 분산 스토리지 기반 커밋/복제 관점을 함께 가져야 한다.

다음 단계의 InnoDB 학습에서는 Buffer Pool 안의 page 교체 정책, redo log, checkpoint, doublewrite buffer를 서로 분리된 기능이 아니라 하나의 쓰기 경로로 연결해 보는 것이 중요하다.
