- Published on
MySQL InnoDB 버퍼풀 튜닝으로 P95 지연 50%↓
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 “평균 응답은 괜찮은데 P95가 튄다”는 증상을 보일 때, 원인은 종종 느린 쿼리 자체가 아니라 디스크 I/O 대기입니다. InnoDB는 데이터/인덱스 페이지를 innodb_buffer_pool에 캐시하는데, 이 캐시가 부족하거나 운영 방식이 맞지 않으면 특정 순간에 랜덤 I/O가 폭증하면서 꼬리 지연이 커집니다.
이 글은 “버퍼풀 튜닝만으로 P95 지연을 50%까지 낮추는” 것을 목표로, 측정 → 가설 → 변경 → 검증 순서로 정리합니다. (환경마다 최적값은 다르므로, 반드시 지표 기반으로 접근하세요.)
왜 버퍼풀이 P95를 흔드는가
InnoDB의 읽기 경로는 단순합니다.
- 필요한 페이지가 버퍼풀에 있으면 메모리에서 즉시 반환
- 없으면 디스크에서 읽어와 버퍼풀에 적재 후 반환
문제는 “없으면 디스크”가 생각보다 비싸다는 점입니다. 특히 다음 상황에서 P95가 급격히 악화됩니다.
- 워킹셋이 버퍼풀보다 큼: 캐시 미스가 상시 발생
- 버퍼풀이 너무 작아
read-ahead나 핫 인덱스가 유지되지 못함 - 더티 페이지 플러시가 몰려서 I/O 큐가 막힘(읽기까지 지연)
- 버퍼풀 인스턴스 경쟁(락/래치)으로 CPU가 놀고 대기
결국 P95는 “일부 요청이 디스크를 밟는 순간”과 “플러시 폭주로 I/O가 잠기는 순간”에 튑니다.
튜닝 전: 반드시 수집할 지표(변경 전 스냅샷)
버퍼풀 튜닝은 체감이 크지만, 잘못 건드리면 오히려 쓰기 지연이 커질 수 있습니다. 아래 지표를 변경 전후로 비교할 수 있게 최소 30분~수시간 단위로 수집하세요.
1) 버퍼풀 히트율과 읽기 I/O
MySQL 8.0 기준으로 먼저 SHOW ENGINE INNODB STATUS를 보거나, 성능 스키마/상태 변수를 확인합니다.
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages%';
Innodb_buffer_pool_reads: 디스크에서 읽은 페이지 수(캐시 미스)Innodb_buffer_pool_read_requests: 논리 읽기 요청 수
대략적인 히트율은 다음처럼 계산합니다.
SELECT
(1 - (bp_reads / NULLIF(bp_read_requests, 0))) * 100 AS buffer_pool_hit_ratio_pct
FROM (
SELECT
CAST((SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') AS DECIMAL(30,0)) AS bp_reads,
CAST((SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') AS DECIMAL(30,0)) AS bp_read_requests
) s;
히트율이 높아도 P95가 튈 수는 있습니다. 중요한 건 캐시 미스가 특정 시간대에 급증하는지, 그리고 그때 OS 레벨 디스크 대기가 같이 뛰는지입니다.
2) 더티 페이지와 플러시 압력
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';
SHOW GLOBAL STATUS LIKE 'Innodb_data_pending%';
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_pending%';
- 더티 페이지가 누적되다가 특정 순간에 플러시가 몰리면, 읽기까지 밀려 P95가 악화됩니다.
3) OS 지표(필수)
iostat -x 1:await,svctm(환경에 따라),util확인vmstat 1:si/so(스왑),wa(I/O wait)- 컨테이너라면 cgroup 메모리 제한과 OOM 여부
버퍼풀을 키우다가 OS 캐시까지 포함해 메모리가 부족해지면 스왑이 발생하고, 이 경우 P95는 “개선”이 아니라 “재앙”이 됩니다.
1단계: innodb_buffer_pool_size를 제대로 잡기
가장 임팩트가 큰 레버입니다. 하지만 무작정 크게 잡는 게 정답은 아닙니다.
권장 출발점
- 전용 DB 서버: 물리 메모리의 약 60%~75%
- 같은 호스트에 앱/에이전트/백업 등이 공존: 40%~60%부터 보수적으로
주의할 점은 “MySQL 프로세스 RSS”만이 메모리 사용량이 아니라는 것입니다.
- InnoDB 버퍼풀
- 바이너리 로그 캐시, 정렬/조인 버퍼(세션별)
- 스레드 스택
- OS 페이지 캐시
- 모니터링/백업 도구
실전 접근: 워킹셋 추정
버퍼풀을 키우는 목적은 “자주 쓰는 데이터/인덱스를 메모리에 고정”하는 것입니다. 워킹셋이 40GB인데 버퍼풀이 8GB라면 P95 개선 여지가 큽니다.
MySQL 8.0에서는 information_schema.innodb_buffer_stats_by_schema 등을 참고할 수 있습니다.
SELECT
object_schema,
SUM(number_of_pages) * @@innodb_page_size / 1024 / 1024 AS mb
FROM information_schema.innodb_buffer_stats_by_schema
GROUP BY object_schema
ORDER BY mb DESC;
특정 스키마/테이블이 버퍼풀을 압도한다면, 그 테이블이 P95의 원인(핫스팟)인지 함께 의심해볼 수 있습니다.
변경 예시
my.cnf:
[mysqld]
innodb_buffer_pool_size=48G
MySQL 8.0은 온라인으로도 변경 가능하지만, 안정적으로는 재시작 계획을 권장합니다.
SET PERSIST innodb_buffer_pool_size = 51539607552;
2단계: innodb_buffer_pool_instances로 경쟁 줄이기
버퍼풀이 커지면 내부 구조(해시/리스트 등)에서 경쟁이 생길 수 있습니다. 이를 줄이기 위해 인스턴스를 나눕니다.
- 버퍼풀이 작으면 인스턴스가 많아도 효과가 제한적
- 버퍼풀이 큰데 인스턴스가 1이면 핫한 워크로드에서 래치 경합이 커질 수 있음
일반적인 가이드:
- 버퍼풀 1GB
8GB: 14 - 버퍼풀 16GB 이상: 4~8 (상황에 따라 16)
설정 예시:
[mysqld]
innodb_buffer_pool_size=48G
innodb_buffer_pool_instances=8
주의: 인스턴스 변경은 재시작이 필요합니다.
3단계: 플러시 폭주를 막아 P95 꼬리를 자르기
P95가 튀는 또 다른 큰 이유는 “읽기 요청이 디스크를 밟는 순간”뿐 아니라, “쓰기 플러시가 몰려서 디스크가 막히는 순간”입니다.
핵심 설정: innodb_flush_method
리눅스에서 일반적으로 O_DIRECT를 사용해 이중 캐싱을 줄입니다.
[mysqld]
innodb_flush_method=O_DIRECT
다만 스토리지/커널/파일시스템 조합에 따라 예외가 있어, 변경 후 반드시 P95와 I/O 지표를 검증해야 합니다.
innodb_flush_neighbors(SSD라면 특히)
HDD 시절에는 인접 페이지를 함께 플러시하는 것이 유리했지만, SSD/NVMe에서는 불필요한 쓰기 증폭이 될 수 있습니다.
[mysqld]
innodb_flush_neighbors=0
innodb_io_capacity와 innodb_io_capacity_max
InnoDB가 “이 정도 IOPS는 낼 수 있다”고 가정하고 백그라운드 플러시 강도를 조절합니다. 기본값이 낮으면 더티 페이지가 쌓였다가 한 번에 터질 수 있습니다.
[mysqld]
innodb_io_capacity=2000
innodb_io_capacity_max=4000
- SATA SSD, 네트워크 스토리지, NVMe 등 장치에 따라 적정값이 크게 다릅니다.
- 무작정 올리면 쓰기 I/O가 과도해져 읽기 지연이 커질 수 있으니,
iostat -x로util이 지속적으로 90% 이상 붙는지 확인하세요.
innodb_max_dirty_pages_pct와 innodb_max_dirty_pages_pct_lwm
더티 페이지 비율이 너무 높아지면 급격한 플러시가 발생할 수 있습니다. “미리미리” 플러시를 시작하게 만드는 것이 목적입니다.
[mysqld]
innodb_max_dirty_pages_pct=75
innodb_max_dirty_pages_pct_lwm=10
워크로드에 따라 다르지만, P95 꼬리가 플러시 이벤트와 함께 튄다면 이 축을 점검할 가치가 큽니다.
4단계: 버퍼풀 프리로드로 재시작 후 P95 스파이크 줄이기
재시작 직후 P95가 급격히 나빠지는 경우가 많습니다. 버퍼풀이 차기 전까지 캐시 미스가 폭증하기 때문입니다.
MySQL은 버퍼풀 덤프/로드를 지원합니다.
[mysqld]
innodb_buffer_pool_dump_at_shutdown=ON
innodb_buffer_pool_load_at_startup=ON
innodb_buffer_pool_dump_pct=25
dump_pct는 너무 높이면 셧다운/스타트업 시간이 늘 수 있어 25% 정도로 시작해 조정합니다.
5단계: “버퍼풀만”으로 해결 안 되는 패턴
버퍼풀 튜닝은 강력하지만, 아래 케이스는 병행 처방이 필요합니다.
1) 쿼리/인덱스 문제로 불필요한 페이지를 읽는 경우
- 인덱스 미스, 범위가 큰 스캔, 정렬/그룹핑으로 임시 테이블 유발
- 이 경우 버퍼풀을 키워도 워킹셋이 계속 커지거나, 읽기 패턴이 비효율적이라 P95가 제한적으로만 좋아집니다.
애플리케이션 레벨에서 흔한 원인으로는 N+1 패턴이 있고, DB에는 불필요한 랜덤 읽기를 유발합니다. (DB 튜닝과 함께 앱 쿼리 패턴도 같이 보세요.)
관련해서는 다음 글도 함께 참고할 만합니다.
2) 잠금/데드락으로 “대기”가 P95를 만드는 경우
P95가 튀는 원인이 I/O가 아니라 락 대기일 수도 있습니다. 이때는 버퍼풀을 아무리 키워도 꼬리가 남습니다.
- 트랜잭션이 길다
- 같은 레코드를 갱신하는 핫스팟이 있다
- 데드락 재시도 로직으로 지연이 늘어난다
이 경우 데드락 로그로 원인 쿼리를 추적하는 것이 빠릅니다.
3) 리플리카 지연이 읽기 P95를 만드는 경우
읽기를 리플리카로 보내는 구조에서, 리플리카 지연이 커지면 특정 요청이 재시도/폴백(프라이머리로 우회)하면서 P95가 악화될 수 있습니다. 버퍼풀 튜닝은 도움이 되지만, 복제 병목 자체를 같이 봐야 합니다.
변경 적용 순서(추천)와 롤백 전략
운영에서 안전하게 P95를 줄이려면 “리스크 낮은 것부터” 적용하세요.
- 관측 지표 확립(P95, 디스크 대기, 캐시 미스, 더티 페이지)
innodb_buffer_pool_dump_at_shutdown및load_at_startup적용(리스크 낮음)- 버퍼풀 크기 상향(메모리 여유 확인 필수)
- SSD라면
innodb_flush_neighbors=0 innodb_io_capacity계열은 소폭 상향 후 관측 반복- 버퍼풀 인스턴스 조정(재시작 필요, 효과는 워크로드 의존)
롤백은 다음을 기준으로 빠르게 판단합니다.
- 스왑 발생 또는 OOM: 즉시 버퍼풀 축소
iostat에서util이 장시간 95% 이상: 플러시 관련 설정 재조정- P95는 좋아졌는데 P99가 더 나빠짐: 플러시 폭주 또는 락 경합 가능성 재점검
검증: “P95 50% 감소”를 재현하는 체크리스트
튜닝 후 다음을 함께 만족하면, 개선이 일시적 착시가 아니라 구조적 개선일 가능성이 큽니다.
Innodb_buffer_pool_reads증가율이 유의미하게 감소- 같은 트래픽에서
iostat의await와util이 안정화 - 더티 페이지 비율이 급등락하지 않고 완만하게 유지
- 재시작 직후에도 P95 스파이크가 줄어듦(덤프/로드 효과)
P95는 평균과 달리 “나쁜 순간”에 민감합니다. InnoDB 버퍼풀 튜닝의 본질은 그 나쁜 순간을 만드는 디스크 의존 구간과 플러시 폭주 구간을 줄이는 것입니다.
마무리
InnoDB 버퍼풀은 MySQL 성능 튜닝에서 가장 ROI가 큰 영역 중 하나입니다. 다만 innodb_buffer_pool_size만 키우는 단발성 접근은 위험하고, 플러시 정책과 I/O 용량 가정, 재시작 후 워밍업, 경쟁(인스턴스)까지 함께 맞춰야 P95 꼬리 지연이 안정적으로 내려갑니다.
다음 액션으로는, 현재 서버의 메모리 여유와 워킹셋 대비 버퍼풀 비율을 먼저 계산하고, 변경은 한 번에 하나씩 적용하면서 P95와 디스크 대기 지표를 같이 추적해보세요.