카테고리 : MySQL/기술노트

InnoDB I/O 모델: sync, async I/O, read/write thread, fsync 비용

InnoDB의 동기·비동기 I/O 경로, read/write thread, fsync 비용을 운영 관점에서 정리한다.

저자: MySQL 기술 노트 작성: 2026.06.15 약 13분 7,316자
다운로드

1. 왜 InnoDB I/O 모델을 운영자가 이해해야 하는가

MySQL 성능 장애를 조사할 때 CPU 사용률이 낮고 쿼리 튜닝도 어느 정도 끝났는데 응답 시간이 길어지는 경우가 있다. 이때 병목은 종종 InnoDB의 I/O 경로에 있다. 버퍼 풀에서 읽지 못한 페이지를 디스크에서 가져오는 과정, dirty page를 백그라운드에서 내리는 과정, 트랜잭션 커밋 시 redo log를 안정 저장소에 강제로 밀어 넣는 과정이 서로 다른 지점에서 대기 시간을 만든다.

운영자가 단순히 “디스크가 느리다”라고만 해석하면 원인과 조치가 흐려진다. 같은 I/O 병목이라도 다음 질문에 따라 대응이 달라진다.

  • 데이터 페이지 read가 느린가, redo log flush가 느린가?
  • foreground thread가 직접 동기 I/O를 기다리는가, background thread가 따라가지 못하는가?
  • 커밋 지연이 fsync() 비용 때문인가, checkpoint 압력 때문인가?
  • 스토리지가 로컬 SSD인가, 네트워크 블록 스토리지인가, Aurora처럼 스토리지 계층이 분리된 구조인가?

이 글은 InnoDB I/O 모델을 sync I/O, async I/O, read/write thread, fsync 비용이라는 축으로 정리한다. 목표는 변수를 암기하는 것이 아니라, 장애 상황에서 “어느 I/O 경로가 막혔는지”를 구분할 수 있는 기준을 갖추는 것이다.

2. InnoDB I/O 경로의 큰 그림

InnoDB는 모든 요청을 즉시 디스크에 읽고 쓰는 엔진이 아니다. 데이터 페이지는 버퍼 풀에 캐시되고, 변경 내용은 먼저 메모리의 dirty page와 redo log buffer에 기록된다. 이후 특정 시점에 데이터 파일과 로그 파일로 내려간다. 이 구조 때문에 InnoDB I/O는 크게 세 부류로 나눌 수 있다.

  1. 페이지 읽기 I/O: 버퍼 풀에 없는 페이지를 데이터 파일에서 읽는다.
  2. 페이지 쓰기 I/O: dirty page를 데이터 파일로 flush한다.
  3. 로그 flush I/O: redo log buffer의 내용을 redo log 파일에 쓰고, 필요하면 fsync()로 저장 안정성을 보장한다.

다음 흐름은 일반적인 InnoDB I/O 경로를 단순화한 것이다.

flowchart TD
  Q[SQL 실행 스레드] --> BP{Buffer Pool hit?}
  BP -- hit --> EX[메모리 페이지 접근]
  BP -- miss --> RI[페이지 read I/O 요청]
  RI --> DT[(Tablespace/Data File)]
  EX --> CH[페이지 변경]
  CH --> RB[Redo Log Buffer]
  CH --> DP[Dirty Page]
  RB --> LF[Log writer / flush]
  LF --> RL[(Redo Log File)]
  DP --> PW[Page cleaner / write thread]
  PW --> DBW[Doublewrite Buffer]
  DBW --> DT
  RL --> FS[fsync / durable flush]
  DT --> FS2[스토리지 flush]

여기서 중요한 점은 데이터 페이지 쓰기와 redo log flush의 의미가 다르다는 것이다. 커밋 내구성은 주로 redo log flush 정책에 의해 결정된다. 데이터 페이지는 커밋 순간마다 즉시 파일에 반영될 필요가 없다. InnoDB는 write-ahead logging 원칙에 따라, 장애 복구 시 redo log를 재적용하여 데이터 페이지를 일관된 상태로 복구한다.

따라서 “디스크 쓰기”라는 표현만으로는 충분하지 않다. 운영 분석에서는 redo log의 커밋 경로, dirty page flush 경로, checkpoint 경로를 분리해서 봐야 한다.

3. sync I/O와 async I/O의 차이

3.1 동기 I/O가 만드는 직접 대기

동기 I/O는 요청한 스레드가 I/O 완료를 기다리는 방식이다. 예를 들어 쿼리 실행 중 필요한 인덱스 페이지가 버퍼 풀에 없고, 해당 페이지를 읽어야만 다음 단계로 진행할 수 있다면 foreground thread는 읽기 완료를 기다린다. 이 대기는 쿼리 응답 시간에 직접 반영된다.

동기 I/O가 자주 드러나는 상황은 다음과 같다.

  • working set이 버퍼 풀보다 커서 random read miss가 많다.
  • 잘못된 실행 계획으로 큰 범위의 secondary index 또는 table page를 읽는다.
  • restart 직후 buffer pool warm-up이 충분하지 않다.
  • 스토리지 지연 시간이 순간적으로 증가하여 page read latency가 커진다.
  • checkpoint 압력 때문에 foreground thread가 flush를 도와야 하는 상황이 발생한다.

동기 I/O의 문제는 “스레드가 대기한다”는 점이다. 서버 전체 IOPS 여유가 있어 보여도 특정 쿼리가 필요한 페이지 하나를 기다리면 해당 쿼리 응답은 지연된다. 그래서 평균 IOPS보다 tail latency가 더 중요할 때가 많다.

3.2 비동기 I/O와 백그라운드 처리

비동기 I/O는 요청을 제출한 뒤 완료를 나중에 수집하는 방식이다. InnoDB는 가능한 경우 운영체제의 native asynchronous I/O를 사용하여 여러 페이지 read/write를 병렬로 처리한다. Linux에서는 일반적으로 innodb_use_native_aio가 관여한다.

비동기 I/O가 효과적인 영역은 다음과 같다.

  • read-ahead로 여러 페이지를 미리 읽는다.
  • page cleaner가 dirty page를 배치로 flush한다.
  • insert buffer/change buffer merge, purge, checkpoint 등 백그라운드 작업이 데이터 파일 I/O를 수행한다.
  • 높은 I/O capacity를 가진 SSD 또는 네트워크 스토리지에 여러 요청을 동시에 발행한다.

하지만 비동기 I/O가 있다고 해서 모든 지연이 사라지는 것은 아니다. 최종적으로 필요한 페이지가 도착해야 쿼리는 진행된다. 또한 너무 많은 비동기 요청을 밀어 넣으면 스토리지 큐가 길어져 latency가 악화될 수 있다. innodb_io_capacityinnodb_io_capacity_max는 InnoDB가 백그라운드 flush 강도를 조절하는 대표적인 힌트다.

4. read thread와 write thread의 역할

InnoDB의 I/O thread 수는 전통적으로 innodb_read_io_threads, innodb_write_io_threads로 관찰하고 설정한다. 이 값들은 InnoDB가 페이지 read/write 작업을 처리하는 내부 스레드 풀 성격을 갖는다. 다만 최신 MySQL에서는 page cleaner, log writer, flush 관련 스레드의 역할도 함께 봐야 하므로, 이 두 변수만으로 전체 I/O 병렬성을 설명해서는 안 된다.

운영 관점에서 read/write thread를 해석할 때는 다음 원칙이 유용하다.

  • read thread 증가는 random read miss가 많고 스토리지가 병렬 read를 잘 처리할 때 도움이 될 수 있다.
  • write thread 증가는 dirty page flush 또는 doublewrite 경로가 병렬 처리의 제약을 받을 때 도움이 될 수 있다.
  • thread 수를 늘려도 단일 fsync() latency, 스토리지 queue saturation, redo log flush 정책으로 인한 커밋 대기는 해결되지 않는다.
  • thread 수가 너무 많으면 스토리지 큐와 CPU scheduling 비용이 증가하여 tail latency가 나빠질 수 있다.

즉, 이 변수들은 “무조건 크게”가 아니라 “스토리지와 workload에 맞게” 조정해야 한다. 특히 클라우드 블록 스토리지에서는 볼륨 타입, provisioned IOPS, throughput limit, burst credit, 네트워크 경로가 함께 병목이 될 수 있다.

5. fsync 비용: 커밋 내구성과 응답 시간 사이의 핵심 지점

fsync()는 운영체제 page cache나 장치 write cache에 머물 수 있는 데이터를 안정 저장소로 밀어 넣도록 요청하는 시스템 호출이다. 데이터베이스에서 fsync()는 단순한 파일 쓰기보다 훨씬 비싼 동작일 수 있다. 스토리지가 실제로 데이터를 영속화해야 하고, 장치·커널·가상화 계층·네트워크 스토리지 계층을 통과할 수 있기 때문이다.

InnoDB 커밋 경로에서 가장 자주 보는 설정은 innodb_flush_log_at_trx_commit이다.

일반적 의미 운영 해석
1 매 커밋마다 redo log를 write하고 flush한다 가장 강한 커밋 내구성, fsync() latency가 응답 시간에 직접 반영될 수 있음
2 매 커밋마다 write하지만 flush는 주기적으로 수행한다 OS crash 시 최근 로그 손실 가능성, 커밋 latency 감소 가능
0 write와 flush를 주기적으로 수행한다 성능은 유리할 수 있으나 장애 시 손실 범위가 커질 수 있음

바이너리 로그를 사용하는 환경에서는 sync_binlog도 함께 봐야 한다. sync_binlog=1은 각 트랜잭션 또는 그룹 커밋 단위에서 binary log의 내구성을 강화하지만, redo log flush와 함께 커밋 경로에 추가적인 flush 비용을 만들 수 있다. 복제와 point-in-time recovery를 중요하게 보는 운영 환경에서는 redo log와 binary log의 내구성 정책을 함께 설계해야 한다.

MySQL은 group commit을 통해 여러 트랜잭션의 flush 비용을 묶어 완화한다. 하지만 group commit이 모든 비용을 제거하지는 않는다. 초당 커밋 수가 높고, 각 커밋이 작은 OLTP workload에서는 fsync()의 평균·상위 백분위 latency가 TPS와 응답 시간의 상한을 결정할 수 있다.

6. 현재 서버에서 I/O 관련 변수 확인하기

다음 쿼리는 MySQL 8.0 테스트 인스턴스에서도 실행 가능한 기본 확인 예제다. 운영 환경에서는 이 결과를 스토리지 유형, workload, 장애 증상과 함께 해석해야 한다.

SELECT VERSION() AS mysql_version;

SHOW VARIABLES WHERE Variable_name IN (
  'innodb_use_native_aio',
  'innodb_read_io_threads',
  'innodb_write_io_threads',
  'innodb_io_capacity',
  'innodb_io_capacity_max',
  'innodb_flush_method',
  'innodb_flush_log_at_trx_commit',
  'sync_binlog'
);

실행 결과(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 (
    ->   'innodb_use_native_aio',
    ->   'innodb_read_io_threads',
    ->   'innodb_write_io_threads',
    ->   'innodb_io_capacity',
    ->   'innodb_io_capacity_max',
    ->   'innodb_flush_method',
    ->   'innodb_flush_log_at_trx_commit',
    ->   'sync_binlog'
    -> );

+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
| innodb_flush_method            | fsync |
| innodb_io_capacity             | 200   |
| innodb_io_capacity_max         | 2000  |
| innodb_read_io_threads         | 4     |
| innodb_use_native_aio          | ON    |
| innodb_write_io_threads        | 4     |
| sync_binlog                    | 1     |
+--------------------------------+-------+
8 rows in set (0.01 sec)

위 결과에서 특히 다음 항목을 본다.

  • innodb_use_native_aio: Linux native AIO 사용 여부다. 환경에 따라 비활성화되어 있으면 병렬 I/O 처리 특성이 달라질 수 있다.
  • innodb_read_io_threads, innodb_write_io_threads: InnoDB read/write I/O worker thread 수다.
  • innodb_io_capacity, innodb_io_capacity_max: 백그라운드 flush 강도를 조절하는 기준값이다.
  • innodb_flush_method: 파일 I/O에서 OS cache를 어떻게 사용할지에 영향을 준다. Linux에서는 O_DIRECT 계열 설정이 자주 검토된다.
  • innodb_flush_log_at_trx_commit, sync_binlog: 커밋 내구성과 flush 비용의 핵심 변수다.

7. I/O 활동을 관찰하는 기본 쿼리

InnoDB의 누적 I/O 상태 변수는 장애 순간의 원인을 단정하기보다는 “어떤 종류의 I/O가 많이 발생했는지”를 보는 출발점으로 사용한다. 다음 예제는 MySQL 8.0에서 사용할 수 있는 전역 상태 변수 일부를 조회한다.

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Innodb_data_reads',
  'Innodb_data_writes',
  'Innodb_data_fsyncs',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_pages_dirty',
  'Innodb_buffer_pool_pages_flushed'
);

실행 결과(MySQL 8.0.46):

mysql> SHOW GLOBAL STATUS WHERE Variable_name IN (
    ->   'Innodb_data_reads',
    ->   'Innodb_data_writes',
    ->   'Innodb_data_fsyncs',
    ->   'Innodb_buffer_pool_reads',
    ->   'Innodb_buffer_pool_read_requests',
    ->   'Innodb_buffer_pool_pages_dirty',
    ->   'Innodb_buffer_pool_pages_flushed'
    -> );

+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| Innodb_buffer_pool_pages_dirty   | 0     |
| Innodb_buffer_pool_pages_flushed | 194   |
| Innodb_buffer_pool_read_requests | 15396 |
| Innodb_buffer_pool_reads         | 1085  |
| Innodb_data_fsyncs               | 100   |
| Innodb_data_reads                | 1109  |
| Innodb_data_writes               | 382   |
+----------------------------------+-------+
7 rows in set (0.00 sec)

Innodb_buffer_pool_reads가 빠르게 증가하면 버퍼 풀 miss에 의해 실제 data page read가 발생하고 있음을 의심할 수 있다. Innodb_data_fsyncs가 빠르게 증가하고 커밋 latency가 함께 증가한다면 redo log 또는 data file flush 경로의 fsync() 비용을 더 자세히 봐야 한다. 단, 누적 카운터는 서버 기동 이후 값이므로 짧은 간격으로 두 번 샘플링하여 delta를 계산하는 방식이 더 실용적이다.

Performance Schema의 file I/O summary는 어느 파일 이벤트 계층에서 시간이 쓰였는지 보는 데 도움이 된다. 다음 쿼리는 InnoDB file I/O 이벤트 중 대기 시간이 큰 항목을 보여준다.

SELECT EVENT_NAME,
       COUNT_READ,
       COUNT_WRITE,
       ROUND(SUM_TIMER_WAIT / 1000000000000, 6) AS total_wait_seconds
FROM performance_schema.file_summary_by_event_name
WHERE EVENT_NAME LIKE 'wait/io/file/innodb/%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

실행 결과(MySQL 8.0.46):

mysql> SELECT EVENT_NAME,
    ->        COUNT_READ,
    ->        COUNT_WRITE,
    ->        ROUND(SUM_TIMER_WAIT / 1000000000000, 6) AS total_wait_seconds
    -> FROM performance_schema.file_summary_by_event_name
    -> WHERE EVENT_NAME LIKE 'wait/io/file/innodb/%'
    -> ORDER BY SUM_TIMER_WAIT DESC
    -> LIMIT 10;

+-------------------------------------------------+------------+-------------+--------------------+
| EVENT_NAME                                      | COUNT_READ | COUNT_WRITE | total_wait_seconds |
+-------------------------------------------------+------------+-------------+--------------------+
| wait/io/file/innodb/innodb_log_file             |          6 |         151 |             0.0320 |
| wait/io/file/innodb/innodb_data_file            |       1098 |         196 |             0.0250 |
| wait/io/file/innodb/innodb_dblwr_file           |          2 |          15 |             0.0038 |
| wait/io/file/innodb/innodb_temp_file            |          0 |          20 |             0.0004 |
| wait/io/file/innodb/innodb_tablespace_open_file |          0 |           0 |             0.0000 |
| wait/io/file/innodb/innodb_arch_file            |          0 |           0 |             0.0000 |
| wait/io/file/innodb/innodb_clone_file           |          0 |           0 |             0.0000 |
| wait/io/file/innodb/meb::redo_log_archive_file  |          0 |           0 |             0.0000 |
+-------------------------------------------------+------------+-------------+--------------------+
8 rows in set (0.00 sec)

이 쿼리는 환경과 workload에 따라 COUNT_READ, COUNT_WRITE, 대기 시간이 작거나 0에 가까울 수 있다. 중요한 것은 “어느 SQL 한 번으로 모든 답을 얻는다”가 아니라, page read, page write, log flush, file wait를 분리해서 관찰하는 습관이다.

8. 작은 재현 예제로 보는 버퍼 풀 miss와 데이터 파일 I/O

다음 예제는 작은 테이블을 만들고 데이터를 읽은 뒤 InnoDB 상태 변수를 확인한다. 임시 테스트 컨테이너에서는 데이터가 작고 버퍼 풀에 쉽게 올라오므로 큰 I/O를 만들지는 않는다. 운영 환경에서는 같은 패턴을 더 큰 테이블과 실제 workload 지표에 적용한다.

DROP TABLE IF EXISTS io_model_demo;
CREATE TABLE io_model_demo (
  id BIGINT NOT NULL PRIMARY KEY,
  payload VARCHAR(200) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_created_at (created_at)
) ENGINE=InnoDB;

INSERT INTO io_model_demo (id, payload)
VALUES (1, REPEAT('a', 100)),
       (2, REPEAT('b', 100)),
       (3, REPEAT('c', 100)),
       (4, REPEAT('d', 100));

SELECT id, LEFT(payload, 8) AS payload_prefix
FROM io_model_demo
WHERE id BETWEEN 1 AND 4
ORDER BY id;

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Innodb_data_reads',
  'Innodb_data_writes',
  'Innodb_data_fsyncs',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_read_requests'
);

DROP TABLE io_model_demo;

실행 결과(MySQL 8.0.46):

mysql> DROP TABLE IF EXISTS io_model_demo;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE io_model_demo (
    ->   id BIGINT NOT NULL PRIMARY KEY,
    ->   payload VARCHAR(200) NOT NULL,
    ->   created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    ->   KEY idx_created_at (created_at)
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO io_model_demo (id, payload)
    -> VALUES (1, REPEAT('a', 100)),
    ->        (2, REPEAT('b', 100)),
    ->        (3, REPEAT('c', 100)),
    ->        (4, REPEAT('d', 100));

Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT id, LEFT(payload, 8) AS payload_prefix
    -> FROM io_model_demo
    -> WHERE id BETWEEN 1 AND 4
    -> ORDER BY id;

+----+----------------+
| id | payload_prefix |
+----+----------------+
|  1 | aaaaaaaa       |
|  2 | bbbbbbbb       |
|  3 | cccccccc       |
|  4 | dddddddd       |
+----+----------------+
4 rows in set (0.00 sec)

mysql> SHOW GLOBAL STATUS WHERE Variable_name IN (
    ->   'Innodb_data_reads',
    ->   'Innodb_data_writes',
    ->   'Innodb_data_fsyncs',
    ->   'Innodb_buffer_pool_reads',
    ->   'Innodb_buffer_pool_read_requests'
    -> );

+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| Innodb_buffer_pool_read_requests | 16154 |
| Innodb_buffer_pool_reads         | 1087  |
| Innodb_data_fsyncs               | 114   |
| Innodb_data_reads                | 1111  |
| Innodb_data_writes               | 406   |
+----------------------------------+-------+
5 rows in set (0.00 sec)

mysql> DROP TABLE io_model_demo;

Query OK, 0 rows affected (0.01 sec)

실무에서는 이 예제보다 다음 방식이 더 유용하다.

  1. 장애 또는 부하 구간 직전 상태 값을 수집한다.
  2. 10초 또는 60초 뒤 같은 값을 다시 수집한다.
  3. delta를 TPS, QPS, redo log write, buffer pool hit ratio, OS disk latency와 함께 본다.
  4. 평균보다 p95/p99 latency와 queue depth를 같이 확인한다.

9. Aurora MySQL에서의 차이

Aurora MySQL은 Community MySQL과 SQL 호환성을 제공하지만 스토리지 구조가 다르다. 전통적인 MySQL은 인스턴스 로컬 또는 연결된 블록 스토리지에 데이터 파일과 redo log를 쓰는 구조로 이해할 수 있다. 반면 Aurora는 분산 스토리지 계층이 로그 중심으로 동작하며, 여러 AZ에 걸쳐 복제된 스토리지 노드와 통신한다.

운영 해석에서 차이가 나는 지점은 다음과 같다.

  • 로컬 파일 시스템의 fsync() 비용을 그대로 Aurora의 내구성 비용으로 해석하면 안 된다.
  • Aurora는 스토리지 계층이 분리되어 있어 redo/log 전송, quorum, storage service latency가 커밋 지연에 영향을 줄 수 있다.
  • 일부 InnoDB 파일 I/O 상태 변수는 Community MySQL과 같은 의미로 보이더라도 실제 물리 I/O 경로는 다를 수 있다.
  • Performance Insights, CloudWatch 지표, Aurora wait event를 함께 봐야 한다.
  • reader 인스턴스는 writer와 다른 버퍼 풀 상태를 가지므로 read latency 분석 시 인스턴스별 관찰이 필요하다.

즉, Aurora에서도 innodb_flush_log_at_trx_commit, buffer pool, dirty page, read/write path의 개념은 유효하지만, 물리 스토리지 병목을 해석할 때는 Aurora 전용 지표와 대기 이벤트를 함께 사용해야 한다.

10. 장애 모드와 흔한 오해

10.1 “IOPS가 남으니 I/O 병목이 아니다”라는 오해

IOPS 평균이 한계보다 낮아도 지연 시간이 길 수 있다. 데이터베이스는 작은 random read, 커밋 flush, checkpoint flush처럼 latency에 민감한 요청이 많다. 특히 단일 페이지 read 또는 commit fsync는 평균 처리량보다 순간 latency에 더 민감하다.

10.2 read thread를 늘리면 모든 read 지연이 해결된다는 오해

read thread 수는 병렬 처리에 영향을 주지만, 필요한 페이지가 느린 스토리지에서 늦게 도착하면 foreground query는 여전히 기다린다. 실행 계획이 잘못되어 읽는 페이지 수가 많은 경우에는 thread 수보다 인덱스와 쿼리 구조가 우선이다.

10.3 innodb_flush_log_at_trx_commit=2가 항상 좋은 선택이라는 오해

값을 2로 낮추면 커밋 latency가 개선될 수 있지만, OS crash 또는 host failure에서 최근 트랜잭션 손실 가능성이 생긴다. 복제, 결제, 재고, 감사 로그처럼 손실 허용도가 낮은 시스템에서는 성능만 보고 선택해서는 안 된다.

10.4 dirty page flush를 늦추면 쓰기 성능이 좋아진다는 오해

flush를 늦추면 당장은 write 부담이 줄어든 것처럼 보일 수 있다. 그러나 checkpoint age가 커지고 dirty page가 쌓이면 나중에 더 강한 flush 압력이 생긴다. 이때 foreground thread가 영향을 받거나 shutdown/recovery 시간이 길어질 수 있다.

10.5 fsync()는 데이터 파일에만 관련된다는 오해

운영자가 체감하는 커밋 지연은 redo log flush, binary log sync, group commit, 스토리지 내구성 계층이 함께 만든다. 데이터 파일 flush보다 redo/binary log sync가 더 직접적인 커밋 병목이 되는 경우가 많다.

11. 운영 점검표

InnoDB I/O 병목이 의심될 때는 다음 순서로 점검한다.

  • Innodb_buffer_pool_reads
  • Innodb_data_fsyncs
  • innodb_flush_log_at_trx_commit, sync_binlog
  • innodb_io_capacity, innodb_io_capacity_max

12. 결론

InnoDB I/O 모델은 단순히 “디스크를 읽고 쓴다”는 수준으로 이해하면 운영 판단에 충분하지 않다. 페이지 read는 버퍼 풀 miss와 쿼리 실행 경로에 직접 연결되고, dirty page write는 checkpoint와 백그라운드 flush 정책에 영향을 받으며, redo log와 binary log flush는 커밋 내구성과 응답 시간의 핵심 지점이 된다.

운영자는 sync I/O와 async I/O, read/write thread, fsync() 비용을 분리해서 관찰해야 한다. 그래야 read 지연에는 buffer pool·인덱스·스토리지 latency를, write 지연에는 dirty page·checkpoint·I/O capacity를, commit 지연에는 redo/binary log flush와 group commit을 각각 맞는 도구로 분석할 수 있다.

다음 글에서는 이러한 I/O 경로와 밀접하게 연결되는 checkpoint, flush list, LRU list, 그리고 dirty page 관리가 실제 성능과 복구 시간에 어떤 영향을 주는지 더 구체적으로 다룰 수 있다.