InnoDB 버퍼 풀
디스크의 파일이나 인덱스 정보를 메모리에 캐시해두는 공간이다. 쓰기 지연을 위한 버퍼로도 사용된다. DML을 통한 데이터 변경은 디스크의 여러 곳에 저장된 레코드를 변경한다. 이는 디스크의 랜덤 I/O를 발생시킨다. 따라서 쓰기 지연을 통해 랜덤 I/O를 줄여 성능을 향샹시킬 수 있다.
데이터 페이지
InnoDB가 디스크와 데이터를 주고 받는 최소 단위를 데이터 페이지라 한다. 데이터 페이지에는 최소 하나의 행이 포함될 수 있다. 하나의 행이 너무 크다면 다음 페이지를 포인터로 쪼개서 데이터 페이지는 전송한다.
구조
InnoDB 버퍼 풀은 메모리 공간을 페이지 단위로 쪼개서 관리한다. 쪼개진 조각을 관리하기 위해 LRU 리스트, Flush 리스트, Free 리스트라는 3개의 자료구조를 사용한다.
Free 리스트
비어 있는 페이지들의 목록이다. 사용자의 쿼리가 새롭게 디스크의 데이터 페이지를 읽어와야 하는 경우 사용된다. 즉, 버퍼 풀에 캐시되어 있지 않은
LRU 리스트
LRU 리스트와 MRU 리스트가 결합된 형태이다. 이러한 자료구조를 사용하는 목적은 디스크로부터 한 번 읽어온 페이지를 최대한 오랫동안 버퍼 풀에 유지하기 위함이다.
최초 디스크로부터 읽힌 데이터 페이지는 자주 사용될 수록 New 서브리스트의 Head로 이동한다. 반대로 사용되지 않는 페이지는 Old 서브리스트의 Tail로 점점 밀려나 결국 버퍼 풀에서 제거된다. 밀려나는 것을 Aging이라 한다.
이해를 돕기위해 InnoDB 스토리지 엔진이 데이터를 읽어오는 과정을 살펴보자.
- 필요한 레코드가 저장된 데이터 페이지가 버퍼풀에 있는지 검사
- 어뎁티브 해시 인덱스를 사용해 페이지 검색
- 해당 테이블의 인덱스를 이용해 버퍼 풀에서 페이지 검색
- 버퍼 풀에 페이지가 있다면 페이지의 포인터를 MRU 방향으로 승급
- 디스크에서 필요한 데이터 페이지를 버퍼풀에 적재하고 LRU 리스트에 데이터 페이지의 포인터 삽입
- 필요한 데이터가 자주 접근됐다면 해당 페이지의 인덱스 키를 어뎁티브 해시 인덱스에 추가
Flush 리스트
디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지)의 변경 시점 기준의 페이지 목록을 관리한다. (변경이 없는 페이지는 클린 페이지라 한다)
버퍼 풀과 리두 로그
버퍼 풀과 리두 로그는 밀접한 관계를 맺고 있다. InnoDB 버퍼 풀은 성능 향상을 위해 데이터 캐시와 쓰기 버퍼링을 지원한다. 버퍼 풀의 공간을 늘리는 것은 데이터 캐시 공간을 늘리는 것이다. 쓰기 버퍼링 기능까지 향상 시키려면 위 둘의 관계를 알아야 한다.
InnoDB의 버퍼 풀에는 클린 페이지와 더티 페이지가 저장되어 있다. 더티 페이지의 변경 전 데이터는 리두 로그에 저장된다. 리두 로그의 공간은 재사용된다. 그래서 재사용 가능한 공간과 불가능한 공간을 구분해서 관리한다. 재사용 불가능한 공간을 활성 리두 로그라 한다.
리두 로그 파일의 공간은 재사용되지만 로그 포지션은 계속 증가한다. 이를 LSN(Log Sequence Number)라 한다. InnoDB 스토리지 엔진은 주기적으로 체크포인트를 발생시켜 리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화 한다. 이렇게 발생한 체크 포인트 중 가장 최근 체크포인트 지점의 LSN이 활성 리두 로그 공간의 시작점이 된다. 체크 포인트의 LSN보다 작은 LSN을 가진 리두 로그 엔트리는 연관된 더티 페이지와 함께 디스크로 동기화 된다.
가장 최근 체크 포인트의 LSN과 마지막 리두 로그 엔트리의 LSN의 차이를 체크포인트 에이지라 하고 이는 활성 로그 공간의 크기가 된다.
쓰기 지연은 버퍼 풀의 더티 페이지와 관련 있고 더티 페이지는 리두 로그의 특정 엔트리와 관계를 맺는다. 즉, 리두 로그의 크기가 쓰기 버퍼링과 관계를 가진다. 리두 로그의 크기가 작으면 쓰기 버퍼링 효과를 거의 볼 수 없다.
버퍼 풀 플러시
MySQL 5.6 버전까지는 InnoDB 스토리지 더티 페이지 플러시 기능에서 디스크 기록 폭증하는 경우가 있었다. 하지만 버전이 업그레이드 되면서 대부분 해결되었다.
InnoDB 스토리지 엔진은 더티 페이지 플러시가 성능상의 악영향을 주지 않게 하기위해 2개의 플러시 기능을 백그라운드로 실행한다.
- Flush 리스트 플러시
- LRU 리스트 플러시
Flush 리스트 플러시
리두 로그 공간을 재활용하려면 오래된 리두 로그 엔트리가 사용하는 공간을 비워야 한다. 리두 로그 공간이 지워지려면 InnoDB 버퍼 풀의 더티 페이지가 먼저 디스크로 동기화돼야 한다. 이를 위해 Flush 리스트 플러시 함수를 통해 오래 전에 변경된 데이터 페이지 순서대로 디스크에 동기화한다. 이때 얼마나 많은 더티 페이지를 한 번에 플러시하는지에 따라 사용자 쿼리가 악영향을 받지 않고 부드럽게 처리된다.
더티 페이지를 디스크로 동기화하는 스레드를 클리너 스레드라 한다. innodb_page_cleaners로 설정 가능하다. InnoDB는 여러 개의 버퍼 풀 인스턴스를 동시에 사용할 수 있다. innodb_buffer_pool_instances의 설정값이 innodb_page_cleaners보다 클 경우 innodb_page_cleaners의 값이 자동으로 innodb_buffer_pool_instances 값과 같은 값으로 설정된다. 즉, 하나의 클리너 스레드가 하나의 버퍼 풀 인스턴스를 처리한다.
기본적으로 전체 버퍼 풀의 90%까지 더티 페이지를 가질 수 있다. innodb_max_dirty_pages_pct로 비율을 조정할 수 있다. 더티 페이지를 많이 가질 수록 쓰기 버퍼링 효과를 극대화할 수 있다.
더티 페이지가 많은 수록 디스크 폭발 현상이 발생할 가능성이 높아진다. 더티 페이지가 계속 증가하다 설정한 비율을 넘어가면 InnoDB 스토리지 엔진은 급작스럽게 더티 페이지를 디스크에 기록해야 한다고 판단한다. 때문에 디스크 쓰기 폭증 현상이 발생할 수 있다. 이런 문제를 완화하기 위해 innodb_max_dirty_pages_pct_lwm 시스템 변수로 일정 수준 이상의 더티 페이지가 발생하면 조금씩 더티 페이지를 디스크로 기록하게 하고 있다. 디스크 쓰기는 많이 발생하지만 더티 페이지 비율이 낮은 상태를 유지한다면 innodb_max_dirty_pages_pct_lwm을 좀 더 높은 값으로 설정해야 한다. 높은 값으로 설정해야 쓰기 버퍼링 효과를 얻을 수 있다.
LRU 리스트 플러시
LRU 리스트는 사용 빈도가 낮은 데이터 페이지를 제거해 새로운 페이지를 읽어올 공간을 만들어야 한다. 이를 위해 LRU 리스트 플러시 함수를 사용한다. LRU 리스트의 끝부분부터 innodb_lru_scan_depth까지 페이지들을 스캔한다. 더티 페이지는 디스크에 동기화되며, 클린 페이지는 즉시 Free 리스트로 페이지를 옮긴다.
Double Write Buffer
리두 로그는 공간 낭비를 막기 위해 페이지의 변경된 내용만 저장한다. 만약 더티 페이지 플러시 중 일부만 기록된다면 페이지의 내용을 복구 할 수 없다. 이를 파셜 페이지 혹은 톤 페이지라 한다. 하드웨어 오작동이나 시스템의 비정상 종료 등으로 발생할 수 있다.
해당 문제를 막기 위해 Double-Write 기법을 이용한다. 동작 과정을 살펴보면
- 실제 디스크의 데이터 파일을 변경하기 전에 더티 페이지를 묶어 디스크 쓰기로 시스템 테이블스페이스의 DoubleWrite 버퍼에 기록한다.
- 각 더티 페이지를 파일의 적당한 위치에 하나씩 랜덤으로 쓰기를 실행한다.
- 시스템이 비정상적으로 종료됐다면 DoubleWrite 버퍼의 내용을 사용하여 복구한다.
DoubleWrite 버퍼는 데이터의 안정성을 위해 사용된다. 하지만 SSD처럼 랜덤 IO나 순차 IO의 비용이 비슷한 경우 상당히 부담스럽다.
'Database' 카테고리의 다른 글
[MySQL] InnoDB 스토리지 엔진 아키텍처 - 4 (0) | 2023.03.01 |
---|---|
[MySQL] InnoDB 스토리지 엔진 아키텍처 - 3 (0) | 2023.02.08 |
[MySQL] InnoDB 스토리지 엔진 아키텍처 - 1 (0) | 2023.01.11 |
[MySQL] MySQL 쿼리 실행 구조 (0) | 2023.01.10 |
[MySQL] MySQL 아키텍처 (0) | 2023.01.06 |