[MySQL] InnoDB 스토리지 엔진 아키텍처

kwang-sub·2024년 4월 24일

MySQL 8.0

목록 보기
5/14

InnoDB

MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공하여 높은 동시성 처리가 가능하며 안정적이고 성능이 뛰어나다.

특징

프라이머리 키에 의한 클러스터링
다른 스토리지 엔진과 다르게 프라이머리 키 값의 순서대로 디스크에 저장되어(클러스터링) 프라이머리 키를 이용한 레인지 스캔이 상당히 빨리 처리할 수 있다. 다른 보조 인덱스 키들은 프라이머리 키의 주소 값을 가지고 있고 해당 인덱스로 조회시 프라이머리 키를 이용한 조회를 지원한다. 따라서 쿼리의 실행 계획에서 프라이머리 키는 다른 보조 인덱스에 비해 우선순위가 높게 설정되어 있다.

MVCC(Multi Version Concurrency Control)
레코드 레벨의 트랜잭션을 지원하는 DBMS가 지원하는 기능으로 잠금을 사용하지 않는 일관된 읽기 기능을 말한다.

InnoDB는 언두 로그를 이용해 해당 기능을 제공하는데 그 방법은 다음과 같다.

  1. InnoDB 버퍼 풀에 A라는 특정 레코드가 존재할 때 해당 레코드의 값을 변경하는 경우 변경되기 이전 값을 언두 로그라는 공간에 임시로 저장을 해둔다.
  2. 변경되는 값으로 InnoDB 버퍼 풀에 레코드를 수정한다.
  3. 트랜잭션이 롤백 또는 커밋이 되면 언두로그에 데이터를 정리한다.

만약 특정 레코드에 값이 변경되었지만 커밋이 안된 경우 다른 클라이언트가 해당 레코드를 조회하면 시스템 변수(transaction_isolation)에 설정된 격리 수준에 따라 변경 이전인 언두 로그에 데이터를 보여주거나 변경된 버퍼 풀의 데이터를 보여주도록 처리된다.

이러한 기능을 통해 레코드에 변경 여부에 상관 없이 잠금 없는 일관된 읽기(Non-Locking Consistent Read)를 할 수 있다.

자동 데드락 감지
MySQL은 스레드 기반으로 작업이 진행되는데 교착 상태에 빠지지 않도록 데드락 감지 스레드가 별도로 존재한다.
다만 동시 처리 스레드가 많거나 트랜잭션이 가진 잠금의 개수가 많아지면 데드락 감지 스레드에 성능이 저하된다.

따라서 innodb_deadlock_detect 시스템 변수를 통해 해당 기능을 꺼두고 innodb_lock_wait_timeout 시스템 변수를 기본 값인 50초 이하로 설정하여 데드락을 방지할 수 있다.

InnoDB 버퍼 풀

데이터베이스에서는 읽기, 쓰기 작업이 일어난다. 이러한 작업은 실제 저장된 공간(디스크)에 접근해야하기 때문에 많은 비용(IO비용)이 발생한다. 그래서 작업을 한번에 모아두고 실행하는 방식을 사용하는데 이 역할을 InnoDB에서는 InnoDB 버퍼 풀이 한다.

InnoDB 버퍼 풀은 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간이다. 또한 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼역할도 같이 한다.

버퍼 풀은 여러 스레드가 접근할 경우 내부 잠금 경합으로 유발하기 때문에 여러 개의 인스턴스로 나누어져 관리되는데 이를 버퍼 풀 인스턴스라고 하며 기본 8개로 관리된다.

버퍼 풀에서 데이터 관리를 위해서 대표적으로 LRU 리스트, 플러시 리스트, 프리 리스트구조를 사용하고 있다.
프리 리스트
사용자가 쿼리가 버퍼 풀에 없고 디스크에 있는 새로운 데이터를 사용할 때 사용된다.
LRU리스트
실제로 LRU(Old) + MRU(New) 구조이다. 데이터를 읽어오면 나이(Age)가 측정되는데 오래될수록 LRU쪽으로 밀려나며 최종적으로 버퍼 풀에서 제거된다.
플러시 리스트
버퍼 풀 내에 있는 데이터 변경되면 이를 디스크에 반영해야하는데 이를 위해 변경된 시점의 페이지 목록을 관리하는 역할을 한다.

버퍼 풀 크기 설정

버퍼 풀의 크기는 innodb_buffer_pool_size 시스템 변수로 설정 가능하며 변경시 많은 부하를 걸기 때문에 서버 사용량이 적은 시간대에 변경을 권장한다 또한 사이즈를 줄이는 경우 영향도가 있을 수 있기에 가급적 줄이는 작업은 하지 말자.
버퍼 풀은 MySQL가 설치된 물리 서버의 메모리를 사용하기 때문에 초기 설정 시에는 물리 서버 메모리가 8GB 미만이라면 50%만 설정하고 그 이상이라면 15~30GB를 남겨놓는 선까지 조금씩 올려가며 설정하며 최적 값을 찾아보자

리두 로그

리두 로그는 InnoDB 버퍼 풀의 기능 중 쓰기 버퍼링 기능에 관련있다. 버퍼 풀에 데이터를 읽어오면 클린 페이지(변경x)와 더티 페이지(변경o)를 가지고 있는데 더티 페이지는 리두 로그에 기록되고 LSN(Log Sequence Number)를 매겨 관리한다.
InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발생시켜 이전 체크 포인트에 마지막 반영한 LSN을 읽어 해당 부분을 시작점으로 활성 리두 로그의 마지막 LSN을 읽어 그 사이의 내용들을 디스크에 반영한다.

리두 로그를 이용해서 더티 페이지를 디스크에 적용하고 나면 클린 페이지가 된다. 따라서 더티 페이지를 가르키고 있던 리두 로그는 지워지게 되고 해당 공간은 비어진 비활성화된 리두 로그가 된다.

여기서 추가로 알아야 할 점은 리두 로그를 이용해 디스크로 플러시할 때 하드웨어 문제가 강제 종료가 발생한다면 어떻게 처리해야될까?

먼저 답은 Double Write Buffer 기능을 활성화해두면 된다.
InnoDB는 리두 로그를 통해 변경 사항을 반영할 때 해당 버퍼에 더티 페이지(변경된 페이지)를 적재하고 디스크에 반영을 한다. 이때 만약 MySQL서버가 오류를 발생해 종료된다면 InnoDB 스토리지 엔진이 재시작될 때 실제 디스크와 Doble Write Buffer에 값을 비교해서 다시 반영을 해준다.

언두 로그

언두 로그는 DML로 인해 변경된 데이터들에 대해서 트랜잭션과 격리 수준을 보장하기 위해 이전 버전의 데이터를 백업한 기록이다. 과거에 언두 로그는 변경된 내용을 전부 저장하고 있기에 변경 내용과 같은 크기에 공간을 차지했었다. 하지만 5.7부터는 언두 로그에 크기를 제한하고 순차적으로 사용하게 함으로써 무한정 사용하는 것을 막아주었다.

언두 로그를 이용한 트랜잭션 보장은 다음과 같이 동작한다.
1. 데이터 변경 쿼리 실행
2. 변경전 데이터 언두로그에 백업
3. 데이터 변경
4. 커밋할 경우 현재 상태 유지 롤백일 경우 언두 로그에 백업 데이터를 다시 적용

버퍼 풀 상태 백업 및 복구

MySQL 서버를 재시작하면 버퍼 풀이 초기화되기 때문에 다시 버퍼 풀에 페이지를 가져오기까지 재시작 전과 성능 차이를 보인다.

따라서 재시작전 버퍼 풀을 백업 해두었다가 재시작한 후 로드할 수 있는데 수동, 자동 두 가지 방식을 지원한다.

아래는 수동으로 하는 방식이다.

--  재시작 전 버퍼 풀 백업
SET GLOVAL innodb_buffer_pool_dump_now=ON;

-- 재시작 후 버퍼 풀 상태 복구
SET GLOBAL innodb_buffer_pool_load_now=ON;

자동으로 하는 방식은 MySQL 설정 파일의 innodb_buffer_pool_dump_at_shutdown, innodb_buffer_pool_load_at_startup 설정을 넣어두면 된다.

profile
백엔드 개발일지

0개의 댓글