한 줄 정의
InnoDB는 MySQL 8.0의 사실상 유일한 스토리지 엔진으로, PK 클러스터링, MVCC, 버퍼 풀, 언두/리두 로그를 통해 높은 동시성과 데이터 안정성을 동시에 제공합니다.
쉽게 말하면
InnoDB는 데이터를 안전하고 빠르게 저장하기 위한 종합 시스템 입니다.
- 읽기 성능을 위해 → 버퍼 풀 (디스크 데이터를 메모리에 캐시)
- 동시성을 위해 → MVCC (잠금 없이 일관된 읽기)
- 장애 복구를 위해 → 리두 로그 (변경 내용을 먼저 로그에 기록)
- 롤백을 위해 → 언두 로그 (변경 전 데이터를 보관)
이 네 가지가 서로 맞물려 돌아가는 것이 InnoDB의 핵심입니다.
왜 중요한가?
MySQL 8.0에서 시스템 테이블, 전문 검색, 공간 인덱스까지 모두 InnoDB로 전환 됐습니다. MyISAM/MEMORY를 쓸 이유가 사라진 만큼, InnoDB의 동작 원리를 이해하는 것이 곧 MySQL을 이해하는 것입니다.
실무에서 마주치는 대부분의 성능 문제는 InnoDB의 구조와 직결됩니다:
- 버퍼 풀 크기 설정 → 캐시 히트율 → 읽기 성능
- 리두 로그 크기 설정 → 쓰기 버퍼링 효과 → 쓰기 성능
- 긴 트랜잭션 → 언두 로그 팽창 → 전체 성능 저하
핵심 내용
PK 클러스터링
InnoDB는 프라이머리 키(PK) 순서대로 데이터를 물리적으로 정렬 하여 저장합니다. 이를 클러스터링 인덱스라고 합니다.
- PK를 이용한 레인지 스캔이 매우 빠릅니다 (물리적으로 연속된 데이터를 읽으므로)
- 세컨더리 인덱스는 PK 값을 논리적 주소로 가지고 있습니다 (MyISAM처럼 물리적 ROWID가 아님)
- 따라서 세컨더리 인덱스로 검색 시 PK를 거쳐 다시 데이터에 접근하는 과정이 필요합니다
외래 키 지원
InnoDB는 외래 키를 스토리지 엔진 레벨에서 지원합니다. 외래 키는 부모/자식 테이블 모두에 인덱스가 필요하며, 변경 시 잠금이 전파되므로 데드락이 발생하기 쉽습니다.
개발 중 외래 키 체크를 일시적으로 비활성화할 수 있습니다:
SET foreign_key_checks = OFF;
-- 작업 수행
SET foreign_key_checks = ON;이 설정은 CASCADE 동작까지 무시하므로 주의가 필요합니다.
MVCC (Multi Version Concurrency Control)
MVCC는 잠금 없이 일관된 읽기 를 가능하게 하는 핵심 메커니즘입니다.
sequenceDiagram participant TX_A as 트랜잭션 A (UPDATE) participant BP as InnoDB 버퍼 풀 participant Undo as 언두 로그 participant TX_B as 트랜잭션 B (SELECT) TX_A->>BP: UPDATE member SET area='경기'<br/>WHERE id=12 BP->>Undo: 변경 전 데이터 백업<br/>(area='서울') Note over BP: 버퍼 풀: area='경기' Note over Undo: 언두 로그: area='서울' TX_B->>BP: SELECT * FROM member WHERE id=12 Note over TX_B: 격리 수준에 따라<br/>버퍼 풀 or 언두 로그 참조 alt READ_COMMITTED TX_B-->>Undo: 커밋 전이면 언두 로그 읽기 else REPEATABLE_READ TX_B-->>Undo: 자신의 트랜잭션 시작 시점<br/>이후 변경은 언두 로그 읽기 end
동작 원리:
- UPDATE가 실행되면, 변경 전 데이터를 언두 로그 에 백업합니다
- 버퍼 풀의 데이터 페이지를 새 값으로 변경합니다
- 다른 트랜잭션이 SELECT하면, 격리 수준에 따라 버퍼 풀의 최신 데이터 또는 언두 로그의 이전 데이터를 반환합니다
이로써 읽기 작업에 잠금이 필요 없어지고, 읽기-쓰기 간 경합이 최소화 됩니다.
격리 수준별 읽기 대상
| 격리 수준 | 읽기 대상 |
|---|---|
| READ_UNCOMMITTED | 버퍼 풀 (커밋 전 데이터도 읽음 = Dirty Read) |
| READ_COMMITTED | 커밋된 데이터만 (언두 로그에서 가장 최근 커밋 버전) |
| REPEATABLE_READ | 트랜잭션 시작 시점의 스냅샷 (언두 로그 체인 추적) |
| SERIALIZABLE | 읽기에도 공유 잠금 부여 (MVCC 활용 안 함) |
잠금 없는 일관된 읽기 (Non-Locking Consistent Read)
SERIALIZABLE이 아닌 격리 수준에서, InnoDB는 읽기 작업에 잠금을 걸지 않습니다. 이것이 가능한 이유가 바로 MVCC와 언두 로그입니다.
단, 오래 열려 있는 트랜잭션은 문제 를 일으킵니다. MVCC를 위해 언두 로그를 유지해야 하므로, 트랜잭션이 길어지면 언두 로그가 삭제되지 못하고 쌓여서 성능이 저하됩니다.
자동 데드락 감지
InnoDB는 잠금 대기 목록을 그래프(Wait-for List) 로 관리하며, 데드락 감지 스레드가 주기적으로 이 그래프에서 교착 상태를 찾습니다.
- 데드락이 발견되면, 언두 로그가 가장 적은 트랜잭션 을 롤백합니다 (롤백 비용이 적으므로)
innodb_deadlock_detect를 OFF로 설정하면 데드락 감지를 비활성화할 수 있습니다 (대신innodb_lock_wait_timeout에 의존)- 동시 처리 스레드가 매우 많은 경우, 데드락 감지 자체가 부하를 일으킬 수 있어서 비활성화를 고려할 수 있습니다
자동화된 장애 복구
InnoDB에는 손실이나 장애로부터 데이터를 보호하는 자동 복구 기능이 내장되어 있습니다. MySQL 서버가 시작될 때 불완전한 트랜잭션이나 디스크에 기록되지 못한 데이터를 자동으로 복구합니다.
자동 복구가 실패하면 innodb_force_recovery 설정값을 1~6까지 단계적으로 올려가며 시도합니다:
| 값 | 모드 | 동작 |
|---|---|---|
| 1 | SRV_FORCE_IGNORE_CORRUPT | 손상된 페이지 무시 |
| 2 | SRV_FORCE_NO_BACKGROUND | 메인 스레드 미시작 (언두 퍼지 건너뜀) |
| 3 | SRV_FORCE_NO_TRX_UNDO | 미커밋 트랜잭션 롤백 안 함 |
| 4 | SRV_FORCE_NO_IBUF_MERGE | 인서트 버퍼 병합 안 함 |
| 5 | SRV_FORCE_NO_UNDO_LOG_SCAN | 언두 로그 무시 |
| 6 | SRV_FORCE_NO_LOG_REDO | 리두 로그 무시 |
값이 커질수록 데이터 손실 가능성이 높아집니다. 복구 모드에서는 SELECT만 가능 하며, 서버가 뜨면 즉시 mysqldump로 백업 후 DB를 재구축하는 것이 권장됩니다.
InnoDB 버퍼 풀
InnoDB에서 가장 핵심적인 구성 요소 입니다. 두 가지 역할을 합니다:
- 데이터 캐시: 디스크의 데이터/인덱스를 메모리에 캐시하여 읽기 성능 향상
- 쓰기 버퍼링: 변경된 데이터를 모아서 일괄 기록하여 랜덤 디스크 I/O 감소
버퍼 풀 크기 설정
| 전체 메모리 | 권장 버퍼 풀 크기 |
|---|---|
| 8GB 미만 | 전체의 50% |
| 8GB ~ 50GB | 50%에서 시작해 점진적 증가 |
| 50GB 이상 | 전체 - 15~30GB (OS/기타 프로그램용) |
innodb_buffer_pool_size로 설정하며, MySQL 5.7부터 동적 조절이 가능합니다- 128MB 청크 단위 로 관리되므로 이 단위로 조절해야 합니다
- 버퍼 풀을 늘리는 것은 안전하지만, 줄이는 것은 서비스 영향이 크므로 가급적 피해야 합니다
innodb_buffer_pool_instances로 여러 인스턴스로 분할하여 잠금 경합을 줄일 수 있습니다 (기본값 8, 40GB 이하면 그대로 유지)
버퍼 풀의 내부 구조
버퍼 풀은 3개의 리스트로 관리됩니다:
flowchart TB subgraph BufferPool["InnoDB 버퍼 풀"] Free["프리 리스트<br/>(비어있는 페이지)"] LRU["LRU 리스트"] Flush["플러시 리스트<br/>(더티 페이지 목록)"] subgraph LRU_Detail["LRU 리스트 상세"] MRU["New 서브리스트 (MRU)<br/>자주 접근되는 페이지"] Mid["← 새 페이지 삽입 (5/8 지점)"] OLD["Old 서브리스트 (LRU)<br/>오래된 페이지"] end end Disk[(디스크)] -->|새 페이지 읽기| Free Free -->|사용 시작| Mid OLD -->|Aging → Eviction| Disk MRU <-->|접근 빈도에 따라 이동| OLD
- 프리 리스트: 아직 데이터가 채워지지 않은 빈 페이지
- LRU 리스트: 실제로 LRU + MRU가 결합된 형태
- 새로 읽힌 페이지는 5/8 지점(Old 서브리스트의 헤드) 에 삽입됩니다
- 실제로 읽히면 MRU(New 서브리스트) 방향으로 승급합니다
- Read Ahead로 미리 읽혔지만 실제 사용되지 않는 페이지는 MRU로 승급되지 않고 밀려납니다
- 플러시 리스트: 디스크와 동기화되지 않은 더티 페이지 의 목록 (변경 시점 기준 정렬)
버퍼 풀과 리두 로그의 관계
버퍼 풀의 성능을 제대로 활용하려면 리두 로그와의 균형 이 중요합니다.
| 설정 | 문제 |
|---|---|
| 버퍼 풀 100GB + 리두 로그 100MB | 쓰기 버퍼링 효과 거의 없음. 더티 페이지를 거의 유지 못함 |
| 버퍼 풀 100MB + 리두 로그 100GB | 급작스러운 디스크 쓰기 폭증(flush storm) 발생 가능 |
- 리두 로그는 순환 고리 처럼 사용됩니다. 활성 리두 로그(Active Redo Log)의 크기가 체크포인트 에이지(Checkpoint Age)입니다
- 체크포인트가 발생하면, 해당 LSN 이전의 더티 페이지가 모두 디스크에 동기화됩니다
- 권장: 버퍼 풀 100GB 이하일 때 리두 로그 전체 크기를 5~10GB 로 시작하고 점진적으로 조절
버퍼 풀 플러시
더티 페이지를 디스크에 기록하는 두 가지 메커니즘:
- 플러시 리스트 플러시: 오래된 리두 로그 공간 확보를 위해, 오래전에 변경된 더티 페이지 순서대로 기록
- LRU 리스트 플러시: 새 페이지를 위한 공간 확보를 위해, LRU 끝(Old)의 사용되지 않는 더티 페이지 기록
관련 주요 설정:
innodb_page_cleaners: 클리너 스레드 수 (버퍼 풀 인스턴스 수와 맞추는 것이 권장)innodb_max_dirty_pages_pct(기본 90): 더티 페이지 비율 상한innodb_io_capacity/innodb_io_capacity_max: 디스크 I/O 능력 설정 (SSD면 높게)innodb_adaptive_flushing: 어댑티브 플러시 활성화 (리두 로그 증가 속도에 따라 플러시 빈도 자동 조절)
버퍼 풀 워밍업
MySQL 5.6부터 버퍼 풀의 상태를 디스크에 저장하고 복원할 수 있습니다:
innodb_buffer_pool_dump_now: 현재 버퍼 풀 상태 저장innodb_buffer_pool_load_now: 저장된 상태 복원innodb_buffer_pool_dump_at_shutdown/innodb_buffer_pool_load_at_startup: 종료/시작 시 자동 처리
서버 재시작 후 캐시가 비어있어 느린 문제(Cold Start)를 해결합니다.
Double Write Buffer
InnoDB의 리두 로그는 변경된 부분만 기록합니다. 그런데 더티 페이지를 디스크에 쓰는 도중 비정상 종료가 발생하면 페이지의 일부만 기록된 파셜 페이지(Partial Page) 문제가 생깁니다. 이 경우 리두 로그로도 복구가 불가능합니다.
이를 방지하기 위해 InnoDB는 더티 페이지를 실제 데이터 파일에 쓰기 전에 Double Write 버퍼 에 먼저 기록합니다. 장애 발생 시 이 버퍼의 내용으로 데이터 파일을 복구한 후 리두 로그를 적용합니다.
언두 로그
트랜잭션과 격리 수준을 보장하기 위해 변경 전 데이터를 보관하는 공간입니다.
두 가지 용도:
- 트랜잭션 롤백: 변경 전 데이터로 되돌리기
- MVCC: 다른 트랜잭션에게 변경 전 데이터 제공
언두 로그 관리의 핵심
- 트랜잭션이 커밋되어도 즉시 삭제되지 않습니다. 해당 시점의 데이터를 참조하는 다른 트랜잭션이 있을 수 있기 때문입니다
- 오래 열려 있는 트랜잭션은 언두 로그 팽창의 주범 입니다
- MySQL 8.0부터 언두 테이블스페이스를
CREATE UNDO TABLESPACE로 동적 추가/삭제할 수 있습니다 innodb_undo_log_truncate를 ON으로 설정하면 불필요한 언두 로그 공간을 자동 반납합니다
체인지 버퍼 (Change Buffer)
인덱스 페이지를 즉시 업데이트하지 않고, 변경 사항을 버퍼에 모아뒀다가 나중에 병합하는 메커니즘입니다.
- 유니크 인덱스에는 사용 불가 (중복 체크를 위해 즉시 읽어야 하므로)
- 일반 세컨더리 인덱스에 대해 INSERT, DELETE, UPDATE 시 발생하는 인덱스 변경을 버퍼링합니다
innodb_change_buffering으로 버퍼링 범위를 설정할 수 있습니다
리두 로그
WAL(Write Ahead Logging) 방식으로 데이터 변경 내용을 먼저 로그에 기록하여 ACID의 D(Durability) 를 보장합니다.
| 시점 | 리두 로그 | 데이터 파일 |
|---|---|---|
| 변경 직후 | 기록됨 | 아직 반영 안 됨 (더티 페이지) |
| 체크포인트 후 | 재사용 가능 | 반영 완료 (클린 페이지) |
| 장애 복구 시 | 이 로그로 데이터 파일 복구 | 복구 대상 |
- 커밋 시점에 리두 로그만 디스크에 동기화 하면 되므로, 데이터 파일의 랜덤 I/O를 줄이면서도 영속성을 보장합니다
innodb_flush_log_at_trx_commit으로 커밋 시 동기화 방식을 설정합니다:1(기본): 커밋마다 디스크 동기화 → 가장 안전0: 1초마다 동기화 → 최대 1초 분량 유실 가능2: OS 버퍼까지만 기록, 동기화는 OS에 위임 → 서버 크래시 시 유실 가능
어댑티브 해시 인덱스
InnoDB가 자주 접근되는 데이터에 대해 자동으로 생성 하는 해시 인덱스입니다.
- B-Tree 인덱스를 타고 내려가는 비용을 줄이기 위한 목적입니다
- 관리자가 직접 생성/삭제할 수 없으며, InnoDB가 자동으로 판단합니다
innodb_adaptive_hash_index로 활성화/비활성화할 수 있습니다
비활성화를 고려해야 하는 경우
- 디스크 읽기가 많은 경우 (캐시 미스가 많으면 해시 인덱스 효과 없음)
- 특정 패턴의 쿼리(LIKE, 범위 검색)가 많은 경우
- 매우 큰 데이터에 대해 등가 검색이 분산되는 경우 (해시 충돌)
- 테이블 조인이 많아 해시 인덱스가 빈번하게 무효화되는 경우
SHOW ENGINE INNODB STATUS의 INSERT BUFFER AND ADAPTIVE HASH INDEX 섹션에서 해시 검색 히트율을 확인할 수 있습니다.
정리
버퍼 풀 크기 vs 리두 로그 크기
| 설정 | 캐시 효과 | 쓰기 버퍼링 | 복구 시간 |
|---|---|---|---|
| 버퍼 풀 크게 + 리두 작게 | 좋음 | 나쁨 (더티 페이지 거의 유지 못함) | 짧음 |
| 버퍼 풀 작게 + 리두 크게 | 나쁨 | 이론상 좋지만 flush storm 위험 | 길어짐 |
| 둘 다 적절히 | 균형 잡힘 | 균형 잡힘 | 적절 |
innodb_flush_log_at_trx_commit 트레이드오프
| 값 | 안정성 | 성능 | 적합한 상황 |
|---|---|---|---|
| 1 | 최고 (데이터 유실 없음) | 가장 느림 | 금융, 결제 등 |
| 2 | OS 크래시 시 유실 가능 | 중간 | 일반 서비스 |
| 0 | 최대 1초 유실 가능 | 가장 빠름 | 로그성 데이터 |
내 생각
-
버퍼 풀 크기만 키우고 리두 로그는 기본값으로 두는 경우가 많은데, 이러면 쓰기 성능은 전혀 개선되지 않습니다. 버퍼 풀과 리두 로그는 반드시 함께 튜닝해야 합니다.
-
긴 트랜잭션(배치 작업 등)이 언두 로그를 팽창시키는 문제는 실무에서 자주 마주칩니다.
information_schema.INNODB_TRX테이블로 오래된 트랜잭션을 주기적으로 모니터링하는 것이 좋습니다. -
어댑티브 해시 인덱스는 “자동으로 생기니까 좋겠지”라고 생각하기 쉽지만, CPU 사용률이 높은데 히트율이 낮다면 오히려 비활성화하는 것이 나을 수 있습니다.
performance_schema로 메모리 사용량을 확인해야 합니다. -
서버 재시작 시
innodb_buffer_pool_dump_at_shutdown/innodb_buffer_pool_load_at_startup을 반드시 ON으로 설정해두어야 합니다. 버퍼 풀이 비어있는 상태(Cold Start)로 서비스를 받으면 갑자기 디스크 I/O가 폭증합니다.
관련 개념
출처
- Real MySQL 8.0 (1권), 4.2 InnoDB 스토리지 엔진 아키텍처