InnoDB: 버퍼풀
- MySQL 구조
- InnoDB: 버퍼풀
InnoDB 구조
아래의 사진은 InnoDB의 전체 구조이다.
InnoDB 스토리지 엔진은 크게 메모리 영역, 백그라운드 스레드, 디스크 영역 세 계층으로 나뉜다.
버퍼풀이란?
버퍼 풀(Buffer Pool)은 InnoDB가 디스크에서 읽어온 데이터 페이지를 메모리에 캐싱하는 공간이다.
매번 쿼리가 실행될 때마다 디스크에서 데이터를 읽어오면 I/O 비용이 매우 크기 때문에, 자주 접근하는 데이터를 메모리에 올려두어 디스크 I/O를 최소화하고 성능을 높인다.
버퍼풀 내부 구성
버퍼 풀은 단일 공간이 아니라 여러 목적으로 나뉜 영역을 포함한다.
1. 데이터 페이지 버퍼
데이터 페이지 버퍼는 실제 테이블의 레코드(Record)들이 저장된 ‘페이지(Page)’들이 머무는 공간이다.
InnoDB는 데이터를 한 건씩 읽지 않고, 효율성을 위해 16KB 크기의 덩어리(페이지) 단위로 메모리에 올린다.
왜 데이터 페이지(16KB) 단위로 관리할까?
우리가 책에서 단어 하나를 찾을 때, 그 단어만 오려오는 것이 아닌 해당 단어가 포함된 페이지 전체를 읽는 것과 같다.
- I/O 효율성: 디스크는 아주 작은 데이터를 여러번 읽는 것보다, 큰 덩어리를 한번에 읽는게 훨씬 빠름
- 공간 지역성: 특정 데이터를 읽었다면, 그 주변 데이터도 곧 읽힐 가능성이 크다. 페이지 단위로 미리 읽어두면 다음 조회 성능이 향상됨
디스크에서 읽어온 후 한번도 수정하지 않은 상태를 클린 페이지라고 부르며, 사용자가 데이터를 수정하여 메모리상의 값은 바뀌었지만, 아직 디스크 파일에 반영되지 않은 상태를 더티 페이지라고 한다.
흐름
- 읽기: 사용자가 특정 데이터를 찾으면, 그 데이터가 포함된 16KB 페이지 전체를 디스크에서 읽어와 페이지 버퍼에 올림
- 수정: 데이터를 업데이트 하면, 디스크 파일에 바로 기록하지 않음. 버퍼풀에 있는 해당 페이지의 값만 쓱 고치고 더티 페이지라고 표시해둠
- 쓰기: 백그라운드 스레드가 시스템이 한가할때 또는 체크포인 시점에 이 더티 페이지들을 모아서 한꺼번에 기록함
데이터를 업데이트 시, 버퍼풀(RAM)에 기록 해놓으면 서버 비정상적 종료 시 다 날라가는거 아닌가..?
버퍼 풀은 메모리(RAM)에 존재하기 때문에, 서버가 비정상적으로 종료되면 더티 페이지는 모두 사라지게 된다.
그래서 InnoDB는 데이터 변경 시 변경 내용을 먼저 Redo Log에 기록한다.
정확히는 변경 사항을 Log Buffer(메모리)에 기록한 뒤, 트랜잭션 커밋 시점에 이를 디스크의 Redo Log 파일에 반영한다.
따라서 서버가 비정상적으로 종료되더라도, 재시작 시 Redo Log를 기반으로 변경 사항을 다시 적용(redo)하여 데이터의 일관성을 유지할 수 있다.
결국 Redo Log도 디스크에 적는거라면, 굳이 왜 Redo Log를 사용할까? 데이터 변경사항을 바로 적용하는거와 같지 않나?
데이터를 실제 저장 위치에 기록하는 방식은 랜덤 I/O(여기 저기 지정된 위치 찾기)가 발생 매우 느리지만, Redo Log는 파일 끝에 순서대로 이어 적는 가벼운 순차 I/O 방식이라 훨씬 빠르다.
페이지 교체 정책
버퍼 풀은 메모리 공간이 한정되어 있기 때문에, 새로운 데이터를 적재하려면 기존 데이터를 제거해야 한다.
이때 어떤 페이지를 제거할지 결정하는 기준이 바로 LRU(Least Recently Used)이다.
LRU는 “가장 오랫동안 사용되지 않은 페이지를 제거”하는 방식이다.
다만 InnoDB는 단순한 LRU가 아니라, Midpoint Insertion 전략을 사용한다.
- 새로 읽은 페이지 → 리스트의 중간에 삽입
- 자주 사용되는 페이지 → 점점 앞쪽으로 이동
이러한 방식으로 한 번 읽고 버려지는 데이터를 뒤쪽에 유지하여, 자주 사용하는 데이터가 캐시에서 밀려나는 것을 방지한다.
2. 언두 페이지
언두 페이지는 데이터를 수정하거나 삭제하기 전, 수정 전의 예전 데이터를 보관하는 공간이다.
언두 페이지는 두 가지 목적으로 사용된다.
1) 트랜잭션 롤백
UPDATE나 DELETE를 실행하면 변경 전 데이터를 언두 페이지에 먼저 기록해둔다.
트랜잭션이 ROLLBACK되면 언두 페이지에 저장된 이전 값으로 되돌린다.
2) MVCC(Multi-Version Concurrency Control)
MVCC는 여러 버전의 데이터를 유지해서, 읽기와 쓰기가 서로 막지 않도록 하는 동시성 제어 방식이다.
트랜잭션 A가 데이터를 수정하는 도중, 트랜잭션 B가 같은 데이터를 조회하면 어떤 값을 보여줘야 할까?
InnoDB는 트랜잭션 B에게 현재 수정 중인 값이 아니라, 언두 로그(Undo Log)에 보관된 이전 버전의 데이터를 조회하게 한다.
즉, 각 트랜잭션은 자신이 시작된 시점을 기준으로 일관된 데이터를 읽게 된다.
덕분에 읽기 작업이 쓰기 잠금을 기다리지 않아도 되므로 동시성 성능이 크게 향상된다.
격리 수준과 언두 페이지
트랜잭션 격리 수준에 따라 언두 페이지에서 어느 버전의 데이터를 읽을지가 결정된다.
| 격리 수준 | 언두 페이지 활용 | 읽는 버전 |
|---|---|---|
| READ UNCOMMITTED | 사용 안 함 | 버퍼 풀의 최신값 그대로 (커밋 여부 무관) |
| READ COMMITTED | 사용 | 쿼리 실행 시점 기준, 커밋된 가장 최신 버전 |
| REPEATABLE READ | 사용 | 트랜잭션 시작 시점 기준 스냅샷 |
| SERIALIZABLE | 사용 | REPEATABLE READ와 동일하나 읽기에도 잠금 |
READ UNCOMMITTED
언두 페이지를 전혀 보지 않고 버퍼 풀에 있는 값을 그대로 읽는다.
아직 커밋되지 않은 데이터도 보일 수 있어 Dirty Read가 발생한다.
READ COMMITTED
쿼리를 실행할 때마다 “지금 이 순간 커밋된 가장 최신 버전”을 언두 페이지에서 찾아 읽는다.
같은 트랜잭션 안에서도 조회 시점마다 결과가 달라질 수 있어 Non-Repeatable Read가 발생한다.
REPEATABLE READ (InnoDB 기본값)
트랜잭션이 시작된 시점의 스냅샷을 기억해두고, 이후 조회는 항상 그 시점 기준의 버전을 언두 페이지에서 읽는다.
같은 트랜잭션 안에서는 몇 번을 조회해도 결과가 동일하다.
언두 페이지는 언제 삭제될까?
해당 데이터를 수정한 트랜잭션보다 이전에 시작된 트랜잭션이 모두 종료되면 InnoDB가 자동으로 정리(Purge)한다.
데이터가 여러 번 수정되면 언두 페이지는 체인 형태로 연결되는데, 트랜잭션을 오래 열어두면 이 체인이 길어져 조회 성능이 저하되고 디스크 용량을 차지하게 된다.
언두 로그 VS 리두 로그
두 로그 모두 데이터 안정성을 위해 존재하지만 목적이 다르다.
| 구분 | 언두 로그 (Undo Log) | 리두 로그 (Redo Log) |
|---|---|---|
| 목적 | 트랜잭션 롤백 / MVCC | 장애 발생 시 복구 |
| 저장 내용 | 변경 전 데이터 | 변경 후 데이터 |
| 사용 시점 | ROLLBACK, 다른 트랜잭션의 이전 버전 조회 | 서버 재시작 시 커밋된 내용 재적용 |
| 위치 | 언두 테이블스페이스 (디스크) / 언두 페이지 (메모리) | 트랜잭션 로그 파일 (디스크) / 로그 버퍼 (메모리) |
| 정리 시점 | 해당 버전을 참조하는 트랜잭션이 모두 종료되면 Purge | 체크포인트 이후 불필요한 로그 순환 덮어씀 |
한 줄 요약
리두 로그는 “커밋한 건 반드시 살린다”, 언두 로그는 “커밋 안 한 건 반드시 되돌린다”