Published on

MySQL InnoDB 버퍼풀 튜닝으로 P95 지연 50%↓

Authors

서버가 “평균 응답은 괜찮은데 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이면 핫한 워크로드에서 래치 경합이 커질 수 있음

일반적인 가이드:

  • 버퍼풀 1GB8GB: 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_capacityinnodb_io_capacity_max

InnoDB가 “이 정도 IOPS는 낼 수 있다”고 가정하고 백그라운드 플러시 강도를 조절합니다. 기본값이 낮으면 더티 페이지가 쌓였다가 한 번에 터질 수 있습니다.

[mysqld]
innodb_io_capacity=2000
innodb_io_capacity_max=4000
  • SATA SSD, 네트워크 스토리지, NVMe 등 장치에 따라 적정값이 크게 다릅니다.
  • 무작정 올리면 쓰기 I/O가 과도해져 읽기 지연이 커질 수 있으니, iostat -xutil이 지속적으로 90% 이상 붙는지 확인하세요.

innodb_max_dirty_pages_pctinnodb_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를 줄이려면 “리스크 낮은 것부터” 적용하세요.

  1. 관측 지표 확립(P95, 디스크 대기, 캐시 미스, 더티 페이지)
  2. innodb_buffer_pool_dump_at_shutdownload_at_startup 적용(리스크 낮음)
  3. 버퍼풀 크기 상향(메모리 여유 확인 필수)
  4. SSD라면 innodb_flush_neighbors=0
  5. innodb_io_capacity 계열은 소폭 상향 후 관측 반복
  6. 버퍼풀 인스턴스 조정(재시작 필요, 효과는 워크로드 의존)

롤백은 다음을 기준으로 빠르게 판단합니다.

  • 스왑 발생 또는 OOM: 즉시 버퍼풀 축소
  • iostat에서 util이 장시간 95% 이상: 플러시 관련 설정 재조정
  • P95는 좋아졌는데 P99가 더 나빠짐: 플러시 폭주 또는 락 경합 가능성 재점검

검증: “P95 50% 감소”를 재현하는 체크리스트

튜닝 후 다음을 함께 만족하면, 개선이 일시적 착시가 아니라 구조적 개선일 가능성이 큽니다.

  • Innodb_buffer_pool_reads 증가율이 유의미하게 감소
  • 같은 트래픽에서 iostatawaitutil이 안정화
  • 더티 페이지 비율이 급등락하지 않고 완만하게 유지
  • 재시작 직후에도 P95 스파이크가 줄어듦(덤프/로드 효과)

P95는 평균과 달리 “나쁜 순간”에 민감합니다. InnoDB 버퍼풀 튜닝의 본질은 그 나쁜 순간을 만드는 디스크 의존 구간플러시 폭주 구간을 줄이는 것입니다.

마무리

InnoDB 버퍼풀은 MySQL 성능 튜닝에서 가장 ROI가 큰 영역 중 하나입니다. 다만 innodb_buffer_pool_size만 키우는 단발성 접근은 위험하고, 플러시 정책과 I/O 용량 가정, 재시작 후 워밍업, 경쟁(인스턴스)까지 함께 맞춰야 P95 꼬리 지연이 안정적으로 내려갑니다.

다음 액션으로는, 현재 서버의 메모리 여유와 워킹셋 대비 버퍼풀 비율을 먼저 계산하고, 변경은 한 번에 하나씩 적용하면서 P95와 디스크 대기 지표를 같이 추적해보세요.