MySQL은 빠른 성능을 위해 메모리 영역의 Buffer Pool을 사용합니다. 해당 영역의 구조 및 동작 방식에 대해 알아보도록 하겠습니다.
Buffer Pool이란
Innodb 엔진에서 테이블이나 인덱스 데이터를 캐시 하는 메모리 영역을 말합니다. 데이터를 메모리에서 직접 액세스 하여 속도가 I/O 작업 시 속도가 빠릅니다.
LRU 알고리즘
MySQL 캐시 전략은 변형된 LRU(Least Recently Used Alogorithm)를 사용하며 가장 오랫동안 참조되지 않은 페이지를 제거합니다.
단순한 LRU 알고리즘의 경우 Head 에는 가장 최근 데이터 Tail 에는 가장 오래된 데이터가 존재하며 이후 버퍼가 가득 찼을 경우 Tail에 있는 데이터를 버퍼 풀에서 제거합니다. 다만 MySQL의 구조는 조금 다릅니다. 버퍼 풀 리스트는 New, Old 서브 리스트로 나눠지며 각각 Head, Tail이 존재합니다. 그리고 New, Old 서브 리스트는 각각 5/8, 3/8 비율로 구성됩니다. 이때 New의 Tail과 Old의 Head가 만나는 지점을 MidPoint라 하며 이후 버퍼 풀에 새로운 페이지가 들어올 경우 중간 지점인 Old의 Head 부분에 저장됩니다.
다만 MySQL의 구조는 조금 다릅니다. 버퍼 풀 리스트는 New, Old 서브 리스트로 나눠지며 각각 Head, Tail이 존재합니다. 그리고 New, Old 서브 리스트는 각각 5/8, 3/8 비율로 구성됩니다. 이때 New의 Tail과 Old의 Head가 만나는 지점을 MidPoint라 하며 이후 버퍼 풀에 새로운 페이지가 들어올 경우 중간 지점인 Old의 Head 부분에 저장됩니다.
왜 새로운 데이터가 New의 Head가 아닌 Old의 Head에 들어가는 걸까?
단순한 LRU 구조의 경우 사용자에 의해 Dump, Where 절 없는 Select 등 일회성의 데이터를 가져올 경우 자주 사용되던 페이지가 제거될 여지가 있습니다. 이러한 문제를 해결하기 위해 MySQL은 중간점 삽입 전략을 사용합니다. 새로운 페이지가 버퍼 풀에 들어왔을 경우 우선 Old의 Head로 이동하게 되며 사용되지 않을 경우 빠르게 버퍼 풀에서 제거됩니다. 만약 해당 페이지가 다시 한번 참조될 경우 New의 Head로 이동시켜 보관하게 됩니다. 이러한 구조로 자주 액세스 하는 페이지가 캐시의 New에 장기간 남아있을 수 있어 엔진은 성능을 유지할 수 있습니다.
Innodb 버퍼 풀 모니터링
MidPoint
MySQL 버퍼풀의 MidPoint는 아래의 매개변수로 확인할 수 있습니다.
기본값은 37이며 이는 Old 서브 리스트의 3/8인 37%를 의미합니다.
SHOW variables LIKE '%innodb%blocks%'
버퍼 풀 메트릭
Innodb의 Buffer Pool 관련 메트릭은 SHOW engine innodb status 명령어로 확인이 가능합니다.
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 810802
Buffer pool size 96978
Free buffers 2048
Database pages 94930
Old database pages 35002
Modified db pages 3
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 57509, not young 14272649
0.00 youngs/s, 0.00 non-youngs/s
Pages read 918448, created 675510, written 14215
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 94930, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
Name
설명
Total memory allocated
버퍼 풀에 할당된 총 메모리(바이트)입니다.
Dictionary memory allocated
InnoDB데이터 사전에 할당된 총 메모리 (바이트)입니다.
Buffer pool size
버퍼 풀에 할당된 총 페이지 크기입니다.
Free buffers
버퍼 풀 사용 가능 목록의 총 크기(페이지)입니다.
Database pages
버퍼 풀 LRU 목록의 총 크기(페이지)입니다.
Old database pages
버퍼 풀 이전 LRU 하위 목록의 총 크기(페이지)입니다.
Modified db pages
버퍼 풀에서 수정된 현재 페이지 수입니다.
Pending reads
버퍼 풀로 읽기를 기다리는 버퍼 풀 페이지 수입니다.
Pending writes LRU
LRU 목록의 맨 아래에서 쓸 버퍼 풀 내의 오래된 더티 페이지 수입니다.
Pending writes flush list
검사점 동안 플러시할 버퍼 풀 페이지 수입니다.
Pending writes single page
버퍼 풀 내에서 보류 중인 독립 페이지 쓰기 수입니다.
Pages made young
버퍼 풀 LRU 목록에서 새로 만든 총 페이지 수( New 서브 리스트 Head로 이동된 페이지).
Pages made not young
버퍼 풀 LRU 목록에서 젊게 만들지 않은 총 페이지 수( Old 서브 리스트에 남아있는 페이지 ).
youngs/s
New로 이전된 Old 페이지에 대한 액세스의 초당 평균입니다.
non-youngs/s
버퍼 풀 LRU 리스트에서 New로 변경되지 못한 Old 페이지에 대한 액세스의 초당 평균입니다.
Pages read
버퍼 풀에서 읽은 총 페이지 수입니다.
Pages created
버퍼 풀 내에서 작성된 총 페이지 수입니다.
Pages written
버퍼 풀에서 작성된 총 페이지 수입니다.
reads/s
초당 평균 버퍼 풀 페이지 읽기 수입니다.
creates/s
초당 작성된 평균 버퍼 풀 페이지 수입니다.
writes/s
초당 평균 버퍼 풀 페이지 쓰기 수입니다.
Buffer pool hit rate
버퍼 풀과 디스크 스토리지에서 읽은 페이지의 버퍼 풀 페이지 적중률입니다.
young-making rate
페이지 액세스로 인해 페이지가 New로 전환된 평균 적중률입니다.
not (young-making rate)
페이지 액세스하였지만 New로 전환되지 않은 페이지에 대한 평균 적중률입니다.
Pages read ahead
미리 읽기 작업의 초당 평균입니다.
Pages evicted without access
버퍼 풀에서 액세스하지 않고 제거된 페이지의 초당 평균입니다.
Random read ahead
무작위 미리 읽기 작업의 초당 평균입니다.
LRU len
버퍼 풀 LRU 목록의 총 크기(페이지)입니다.
unzip_LRU len
버퍼 풀 unzip_LRU 목록의 길이(페이지)입니다.
I/O sum
액세스된 버퍼 풀 LRU 목록 페이지의 총 수입니다.
I/O cur
현재 간격에서 액세스한 버퍼 풀 LRU 목록 페이지의 총 수입니다.
I/O unzip sum
압축 해제된 버퍼 풀 unzip_LRU 목록 페이지의 총 수입니다.
I/O unzip cur
현재 간격으로 압축 해제된 버퍼 풀 unzip_LRU 목록 페이지의 총 수입니다.
결론
경우에 따라 버퍼풀의 MidPoint을 조정하여 자주 사용되는 페이지를 버퍼 풀에 유지할 수 있도록 튜닝이 가능해 보입니다. 다만 서비스에 정확히 어떤 쿼리들이 수행되는지 알지 못한 채 수정한다면 오히려 자주 사용하는 페이지가 유지되지 못하거나 재사용될 수 있는 페이지가 계속 디스크 액세스 될 경우 또한 존재하니 주의해야 합니다.