MySQL InnoDB 버퍼풀 — 캐시 히트율 70%에서 시작한 튜닝
개요#
운영 중인 서비스의 DB 메트릭을 Prometheus로 보고 있었는데, 버퍼풀 캐시 히트율이 70% 부근에서 계속 맴돌고 있었습니다. 일반적으로 99% 이상이 권장되는 수치라서, 10개 중 3개 쿼리가 디스크를 직접 읽고 있다는 뜻이었습니다.
처음엔 쿼리나 인덱스 문제를 의심했는데, 뜯어보니 더 밑단이 문제였습니다. 인스턴스 스펙이 낮아서 전체 메모리 대비 버퍼풀에 30% 정도만 할당된 상태였고, 그게 히트율 저하로 바로 드러나고 있었습니다. 이 일을 겪고 나서 버퍼풀 구조와 튜닝 포인트를 다시 정리하게 됐습니다.
버퍼풀이란#
InnoDB 스토리지 엔진이 테이블과 인덱스 데이터를 캐시하는 메모리 영역입니다. 디스크에서 읽어온 데이터 페이지를 메모리에 올려놓고, 이후 같은 데이터를 요청하면 디스크를 거치지 않고 메모리에서 바로 응답합니다.
DB 성능의 핵심은 결국 디스크 I/O를 얼마나 줄이느냐입니다. 버퍼풀이 그 역할을 합니다.
히트와 미스#
- 쿼리가 데이터 페이지를 요청하면 먼저 버퍼풀을 확인합니다
- 버퍼풀에 있으면 히트 — 디스크 접근 없이 바로 반환합니다
- 버퍼풀에 없으면 미스 — 디스크에서 읽어와서 버퍼풀에 올린 후 반환합니다
히트율이 99%면 100번 요청 중 1번만 디스크를 읽는 것이고, 70%면 30번을 디스크에서 읽는 것입니다. 디스크 읽기는 메모리 읽기 대비 수십~수백 배 느리기 때문에 이 차이가 쿼리 응답 시간에 직접적으로 영향을 줍니다.
버퍼풀 크기 산정#
innodb_buffer_pool_size가 핵심 파라미터입니다.
일반적인 권장값#
전용 DB 서버라면 **전체 물리 메모리의 7080%**를 버퍼풀에 할당하는 것이 일반적입니다. 나머지 2030%는 OS, 커넥션, 임시 테이블 등에 사용됩니다.
| 인스턴스 메모리 | 권장 버퍼풀 | 비율 |
|---|---|---|
| 8GB | 5.5~6.5GB | 70~80% |
| 4GB | 2.8~3.2GB | 70~80% |
| 1GB | 500~600MB | 50~60% |
메모리가 작은 인스턴스에서는 70%까지 잡으면 OS가 부족해질 수 있어서 50~60% 정도로 낮추는 것이 안전합니다.
30%가 문제였던 이유#
운영 인스턴스의 스펙이 낮았고, DB 외에 애플리케이션도 같은 서버에서 돌리고 있어서 버퍼풀에 메모리의 30% 정도만 할당한 상태였습니다. 데이터 양이 적을 때는 30%로도 대부분의 데이터가 버퍼풀에 올라왔지만, 데이터가 늘어나면서 버퍼풀 용량을 초과했고 히트율이 떨어지기 시작했습니다.
캐시 교체 알고리즘#
버퍼풀 용량이 부족하면 기존 페이지를 밀어내고 새 페이지를 올려야 합니다. InnoDB는 LRU(Least Recently Used) 변형 알고리즘을 씁니다.
InnoDB의 서브리스트 구조#
일반적인 LRU는 최근에 접근한 데이터를 앞에 두고, 오래된 데이터를 뒤에서 밀어내는 방식입니다. 하지만 InnoDB는 여기에 변형을 줬습니다.
버퍼풀을 두 개의 서브리스트로 나눠서 관리합니다:
- New 서브리스트 (5/8): 최근에 접근된, 자주 사용되는 페이지
- Old 서브리스트 (3/8): 덜 최근에 접근된 페이지
새로운 페이지가 처음 읽혀서 버퍼풀에 올라오면 Old 서브리스트의 헤드에 배치됩니다. 이후 다시 접근되면 New 서브리스트로 승격됩니다.
이렇게 하는 이유는 풀 스캔 같은 일회성 대량 조회가 자주 사용되는 데이터를 밀어내는 것을 방지하기 위해서입니다. 서브리스트 구조에 대한 상세한 설명은 MySQL 공식 문서 - Buffer Pool에서 확인할 수 있습니다.
LRU vs LFU#
| 알고리즘 | 기준 | 특징 |
|---|---|---|
| LRU | 최근 접근 시점 | 오래 안 쓰인 데이터부터 제거 |
| LFU | 접근 빈도 | 적게 쓰인 데이터부터 제거 |
InnoDB는 기본적으로 LRU를 쓰지만, 서브리스트 구조로 LFU의 장점도 일부 가져갑니다.
Read-Ahead (프리페칭)#
버퍼풀 히트율을 높이는 또 다른 방법이 read-ahead입니다. 곧 필요할 것으로 예상되는 페이지를 미리 버퍼풀에 올려놓는 기법입니다.
선형 Read-Ahead#
순차적으로 접근되는 페이지 패턴을 감지하면, 다음 extent의 페이지를 미리 가져옵니다. innodb_read_ahead_threshold 파라미터로 민감도를 조절할 수 있습니다.
무작위 Read-Ahead#
동일 extent 내에서 13개 이상의 연속 페이지가 버퍼풀에 있으면, 나머지 페이지도 미리 가져옵니다. innodb_random_read_ahead를 ON으로 설정해서 활성화합니다.
모니터링#
버퍼풀 상태는 Prometheus로 수집해서 Grafana에서 확인하고 있었습니다. MySQL Exporter를 쓰면 InnoDB 관련 메트릭이 자동으로 노출됩니다.
핵심 메트릭#
캐시 히트율
히트율 = Innodb_buffer_pool_read_requests /
(Innodb_buffer_pool_read_requests + Innodb_buffer_pool_reads) * 100
Innodb_buffer_pool_read_requests: 버퍼풀에서 읽은 횟수 (히트)Innodb_buffer_pool_reads: 디스크에서 읽은 횟수 (미스)
99% 이상이 정상입니다. 95% 아래로 떨어지면 버퍼풀 크기를 늘리거나 쿼리 패턴을 점검해야 합니다.
버퍼풀 사용률
사용률 = Innodb_buffer_pool_pages_data / Innodb_buffer_pool_pages_total * 100
이 값이 계속 100%에 가까우면 버퍼풀이 부족하다는 신호입니다. 페이지 교체가 빈번하게 일어나면서 히트율이 떨어집니다.
튜닝 결과#
버퍼풀 할당 비율을 30%에서 올린 이후 히트율이 개선됐습니다. 동시에 슬로우 쿼리 빈도도 줄어들었습니다.
튜닝 시 주의할 점:
- 버퍼풀을 너무 크게 잡으면 OS나 다른 프로세스가 메모리 부족으로 스왑을 타게 됩니다. 스왑이 발생하면 오히려 전체 성능이 떨어집니다
- RDS를 쓴다면 파라미터 그룹에서
innodb_buffer_pool_size를 조정할 수 있습니다. 변경 후 재시작이 필요한 경우가 있으니 유지보수 윈도우에 적용하는 것이 안전합니다 - 변경 전후로 히트율, 슬로우 쿼리 빈도, 응답 시간을 비교해서 효과를 확인해야 합니다
참고 자료#
- MySQL 8.0 Reference Manual - InnoDB Buffer Pool
- InnoDB Buffer Pool LRU Implementation - Shubham Raizada
- An In-Depth Analysis of Buffer Pool in InnoDB - Alibaba Cloud
버퍼풀은 생각보다 먼저 봐야 하는 지점이었습니다. 쿼리 최적화나 인덱스 설계도 중요하지만, 버퍼풀이 부족하면 결국 디스크 I/O에서 막히기 때문에 그 위에서 하는 튜닝이 자꾸 힘을 못 받습니다.
그래서 이 글 이후로는 히트율을 그냥 참고 수치로 보지 않게 됐습니다. 95% 아래로 떨어지면 쿼리만 볼 게 아니라 버퍼풀 크기와 데이터 증가 추세를 같이 보는 쪽으로 습관이 바뀌었습니다.