MySQL

InnoDB 스토리지 엔진

iksadnorth 2023. 8. 1. 20:38

해당 게시물은 'Real MySQL 8.0'라는 책을 참고해서 작성했습니다.

👣 아키텍처

InnoDB는 MySQL 스토리지 엔진 가운데 유일하게 레코드 기반 잠금을 제공한다.
때문에 높은 동시성 처리가 가능하고 안정적이고 성능이 좋다.

👣 프라이머리 키에 의한 클러스터링

InnoDB의 모든 테이블은 기본적으로 Primary Key를 기준으로 클러스터링 된다.
그리고 모든 세컨더리 인덱스는 레코드 주소값이 아닌 Primary Key값을 주소로 사용한다.
때문에 실행 계획에서 다른 보조 인덱스 값보다 Primary 인덱스 값이 더 높은 점수를 가져간다.
즉, Primary Key가 선택되어 실행될 확률이 높다.

 

인덱스

👣 개요 인덱스는 보통 B-트리로 구성되어 특정 값에 대한 조회가 O(log n) 복잡도로 수행된다. 만약 인덱스가 존재하지 않는 칼럼을 이용해서 조회를 하면 O(n) 복잡도로 , 즉 선형 탐색으로 검색

ikadnorth.tistory.com

👣 외래 키 지원

외래 키는 외래 키를 설정하지 않았을 때와 달리 잠금이 여러 테이블로 전파된다.
이를 통해 부모 테이블, 자식 테이블 모두 해당 칼럼에 인덱스를 생성하고 변경 시, 일관성을 체크한다.

하지만 이런 점 때문에 레코드 잠금이 빈번하고 이는 데드락의 원인이 될 수도 있다.

👣 MVCC - Multi Version Concurrency Control

InnoDB는 레코드가 Commit되지 않고 Rollback되어 기존 데이터로 돌아오게 하기 위해
각 데이터의 버전을 여러 개 생성할 수 있다.
여러 버전의 데이터를 만들어놓아 특정 시점의 데이터를 사용할 수 있게 도와주는 역할을 한다.

이런 구조를 사용하지 않고도 동시성을 제어할 수 있다. 알고 있는 방식대로 해당 레코드에 Lock을 걸면 간단하게 동시성을 제어할 수 있다. 하지만 위 방법과 같은 MVCC를 이용해서 락을 사용하지 않고도 동시성을 제어할 수 있으므로 이런 방식을 차용하는 것이다. 이런 방식의 읽기를 잠금 없는 일관된 읽기[Non-Locking Consistent Read]라고 한다.

실제로 InnoDB에서는 Undo Log 영역을 이용해서 이런 방식을 구현했다.
Insert 문을 실행하면 Buffer Pool[Memory]과 데이터 파일[Disk]에 내용이 올라가게 된다.

차후 Update 문을 실행하면 Commit 여부와 상관없이 Undo Log에 기존의 데이터를 적재한다.
Update에 의해 수정되는 데이터는 Buffer Pool에는 반영이 되지만 Undo Log에는 반영되지 않는다.

만약 다른 트랜잭션이 해당 레코드에 접근을 한다면 격리 수준에 따라 다르겠지만
Select문은 버퍼풀, 데이터 파일이 아닌 Undo Log에 접근해 이전 데이터에 접근할 것이다.

INSERT INTO table (id,name,area) VALUES (1, Mangkyu,서울);
UPDATE table SET area=경기 WHERE id=1;

👣 자동 데드락 감지

InnoDB는 내부적으로 데드락을 감지하기 위해 잠금 대기 목록 그래프[Wait-for List]를 관리한다.
이것을 검사하는 스레드인 데드락 감지 스레드가 주기적으로 검사해 교착 상태에 빠진 트랜잭션을 강제 롤백시킨다.

이 때, 어떤 트랜잭션을 강제 종료할 것인지에 대한 기준은 언두 레코드를 가장 적게 가진 트랜잭션이다.
언두 레코드를 적게 가지고 있다는 것은 롤백할 내용이 적다는 것이므로
최소한의 리스크로 정상 상태로 만들 수 있다는 것이다.

물론 일반적인 서비스에서의 데드락 감지 스레드는 큰 부하를 주지 않지만 
트랜잭션이 너무 많이 몰리게 되면 데드락 감지 스레드가 부하를 줄 수 있으므로
MySQL 서버의 innodb_deadlock_detect 변수를 OFF 시키고
대신 innodb_lock_wait_timeout  짧은 시간으로 줘서 데드락 현상을 막아야 한다.

👣 InnoDB Buffer Pool

버퍼 풀은 직접적으로 디스크 접근을 막기 위해 존재하는 캐싱을 위한 메모리 공간이라고 생각할 수 있다.
해당 공간의 초기 버전에선 메모리의 대부분을 하나의 버퍼풀로 사용하면서
세마포어로 인해 잠금 경합을 많이 유발 시켰다.
때문에 이런 경합을 막기 위해 여러 개의 작은 버퍼 풀로 나누고
각각의 버퍼 풀 인스턴스를 나눠서 관리하게 되어 있다.

각 버퍼 풀은 또다시 페이지라는 메모리 조각으로 나뉘어서 관리된다.
각 페이지 조각을 관리하기 위해
LRU 리스트, Flush 리스트, Free 리스트라는 자료구조를 운용한다.
위 리스트들을 관리하는 이유는 디스크로 한번 들어온 페이지 중
자주 사용하는 페이지를 최대한 오랫동안 메모리 위에 유지하고 싶어서다.

LRU 리스트


위 자료구조의 Old Sublist는 LRU 자료구조에 해당하고
New Sublist는 MRU 자료구조에 해당한다.
새롭게 자료가 들어오면 Midpoint로 삽입된다.
LRU 헤더 부분에 적재된 데이터 페이지를 실제로 다시 읽으면 MRU 헤더 부분으로 이동한다.
필요한 데이터가 자주 접근됐다면 어댑티브 해시 인덱스에 추가한다.

Flush 리스트 Disk와 동기화되지 않은 데이터를 가진 데이터 페이지[= Dirty Page]를 관리한다.
1번 이상 데이터 변형이 가해진 데이터 페이지는 Flush 리스트에서 관리된고 특정 시점에서 Disk로 기록된다.

👣 Redo Log

Redo Log란 Disk로 변경된 데이터를 쓰기 전에 임시로 보관하는 데이터다.
Disk I/O 작업은 Cost를 많이 잡아먹기 때문에 자주 일어나면 안 된다.
때문에 읽기 캐시를 위해 Buffer Pool을 사용했던 것처럼 쓰기 캐시를 위해 Redo Log를 사용한다고 보면 된다.

Redo Log는 장애 대응을 위한 복구 대책으로도 사용된다.
Redo Log는 항상 Disk 위에도 저장이된다.
얼핏들으면 데이터 파일에도 저장할 거면서 왜 Redo Log에도 저장하는 거지라고 생각할 수 있지만
데이터 파일은 검색 기능을 위해 B+Tree 구조로 저장된다. 즉 랜덤한 형태로 저장된다.
때문에 특히나 Disk I/O에 큰 Cost가 지불되고 시간이 오래걸린다.
이를 방지할 겸 이중으로 데이터를 저장해 신뢰성을 높일 겸
Redo Log Linear하게 Disk 위에 저장한다.

당연하지만 Redo Log는 변경 작업이 디스크에 반영되면 더 이상 필요하지 않으므로 주기적으로 청소한다.

👣 CheckPoint

주기적으로 Redo Log와 Buffer Pool의 Dirty Page를 Disk와 동기화시키는 행위를 일컫는다.
리두 로그는 주기적으로 디스크에 쓰여질 수 있지만, 변경된 데이터들이 메모리의 버퍼 풀에만 존재하고
디스크에는 적용되지 않은 경우가 발생할 수 있다.
이러한 경우 데이터의 내구성에 문제가 발생할 수 있으므로 주기적으로
체크포인트를 수행하여 Redo Log와 Buffer Pool의 데이터를 디스크에 반영한다.

👣 LSN - Log Sequence Number

Redo Log File 공간은 계속 순환되면서 재사용하지만 매번 Log를 기록할 때마다
Log Position이 계속 증가되는 값을 기록한다. 이 값을 LSN이라고 한다.
이것은 차후 CheckPoint가 얼마나 오래되었는지를 측정하는 지표가 된다.

👣 Buffer Pool Flush

InnoDB 스토리지 엔진은 버퍼 풀에서 아직 디스크로 기록되지 않은 더티 페이지들을
성능상의 악영향 없이 디스크에 동기화하기 위해 아래와 같이 2 개의 Flush 기능을 백그라운드로 실행한다.

Flush List Flush
오래된 Redo Log 공간이 지워지려면 반드시 InnoDB 버퍼 풀의 더티 페이지가 먼저 디스크로 동기화돼야 한다.
이를 위해 InnoDB 스토리지 엔진은 주기적으로 플러시 리스트에서
오래전에 변경된 데이터 페이지 순서대로 동기화하는 작업을 수행한다.
InnoDB 스토리지 엔진은 리두 로그 공간의 재활용을 위해 주기적으로
오래된 Redo Log가 존재하는 공간을 비워야 한다.

LRU List Flush
LRU 리스트에서 사용 빈도가 낮은 페이지들을 제거해서 새로운 페이지들을 읽어올 공간을 만든다.

👣 Buffer Pool Backup

Buffer Pool은 쿼리 성능에 매우 지대한 영향을 준다.
서버를 셧다운 후 재시작한 경우[즉, Buffer Pool의 내용이 완전히 비워졌을때],
평상시의 1/10의 성능도 안 되는 경우가 발생한다.
때문에 의도적으로 Warming Up 과정을 통해 버퍼 풀 내용을 채워두고 서비스를 재개하는 경우가 있다.

MySQL 5.6 버전부터 Buffer Full Dump 기능이 지원되면서 이런 Warming Up 과정이 불필요하게 되었다.
Buffer Pool의 전체 데이터를 백업하지 않고 메타데이터만 저장하기 때문에 백업 파일의 크기는 크지 않다.
하지만 복구 작업 속도가 오래 걸릴 수 있다는 것을 인지해야 한다.

👣 Double Write Buffer

Checkpoint를 위해 Memory와 Disk를 동기화시키는 과정 중 비정상적으로 서버가 다운될 수도 있다.
이 경우, 다시 복구하기 위해 Double Write Buffer를 이용한다.

순차적으로 Double Write Buffer에 1개씩 변경 내용을 기록하면서 Disk에 해당 내용을 입력하는 것이다.
이렇게 하면 장애로 인해 Buffer Pool 내용을 잃어버려도 추적 가능하다.

하지만, (성능 > 무결성)인 서비스에서는 이 기능을 OFF해 성능을 높일 수 있다.

👣 Undo Log

트랜잭션과 격리 수준을 보장하기 위해 데이터 백업을 수행한다.
해당 백업 데이터는 Undo Log라는 곳에 저장된다.

트랜잭션이 서로 같은 데이터를 가지고 수정하고 삭제하고 해도
커밋 이전의 트랜잭션에선 Undo Log 내부 데이터로 연산하기 때문에 동시성 문제를 해결할 수 있다.

👣 Change Buffer

클러스터형 인덱스의 경우 정렬된 순서로 삽입되는 반면
보조 인덱스의 경우 일반적으로 유니크하지 않고 정렬되지 않은 채 처리가 되기 때문에
최신 상태로 유지하기 위해서는 상당한 I/O 가 발생하게 된다.
이러한 단점을 극복하고자 DML 작업이 발생할 경우 실시간으로 업데이트하지 않고
체인지 버퍼를 통해 변경사항을 캐시한다.

다음은 Change Buffer가 사용되는 과정이다.

  1. 인덱스 쓰기 작업
    • 트랜잭션에서 INSERT, UPDATE 또는 DELETE 작업이 수행될 때, 해당 인덱스에 대한 변경 작업이 발생한다.
  2. 변경 작업 기록
    • 인덱스 변경 작업이 발생하면, InnoDB는 이러한 변경 작업을 Change Buffer에 기록한다.
      Change Buffer는 디스크가 아닌 메모리에 기록되는 메모리 버퍼로서,
      변경 작업을 임시로 저장하는 역할을 한다.
  3. 데이터 페이지 읽기
    • 데이터가 읽혀질 때, InnoDB는 해당 데이터 페이지를 메모리에 로드한다.
      이때, 데이터 페이지에 연결된 인덱스를 찾아야 할 경우,
      인덱스 페이지가 존재하지 않더라도 디스크에 접근하지 않고 Change Buffer를 검사한다.
  4. Change Buffer 적용
    • 데이터 페이지에 연결된 인덱스가 Change Buffer에 존재하는 경우,
      InnoDB는 Change Buffer에 저장된 변경 작업들을
      데이터 페이지에 적용하여 인덱스를 업데이트한다.
      즉, 인덱스 페이지를 디스크에 쓰지 않고도 인덱스를 최신 상태로 유지할 수 있다.
  5. Change Buffer Flush
    • Change Buffer에 쌓인 변경 작업들은 주기적으로 디스크에 기록되어야 한다.
      이때, InnoDB는 변경 작업들을 디스크에 반영하고, 해당 인덱스 페이지들을 디스크에 쓰는 작업을 수행한다.
      이를 Change Buffer Flush라고 한다.

👣 어댑티브 해시 인덱스

해당 저장소는 이름값을 하듯 Hash Table로 이뤄졌다.
이 저장소는 B-Tree로 이뤄진 인덱스 역시 수많은 트랜잭션이 한꺼번에 집중되면
연산 시간이 오래걸리는 것에 대한 대비책으로 만들어진 저장소다.

해시 인덱스는
'B-Tree의 인덱스 고유번호(ID)'과 'B-Tree의 실제 키값'을 조합한 값을 Key값으로
'Buffer Pool 데이터 페이지 주소값' Value값으로 구성한 Hash Table이다.

MySQL 8.0부터는 세마포어 경합을 줄이기 위해 해시 인덱스를 여러 개로 쪼개서 운영한다.

'MySQL' 카테고리의 다른 글

MySQL 로그 파일  (0) 2023.08.01
MyISAM 스토리지 엔진  (0) 2023.08.01
MySQL 아키텍처  (0) 2023.08.01
사용자 계정  (0) 2023.07.31
왜 MySQL을 사용하는가?  (0) 2023.07.31