과정을 즐기자

InnoDB 스토리지 엔진 아키텍처 본문

Database

InnoDB 스토리지 엔진 아키텍처

320Hwany 2023. 7. 14. 20:45

저번 글인 MySQL 엔진 아키텍처에 이어서 이번에는 MySQL의 스토리지 엔진 가운데 가장 많이 사용되는

InnoDB 스토리지 엔진에 대해 알아보겠습니다.

 

MySQL 전체 구조

 

InnoDB 스토리지 엔진 아키텍처

InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공하여

높은 동시성 처리가 가능하고 안정적이며 성능이 뛰어납니다. 

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

InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 저장됩니다.

프라이머리 키가 클러스터링 인덱스이기 때문에 프라이머리 키를 이용한 레인지 스캔은 상당히 빠르게 처리될 수 있습니다.

쿼리의 실행 계획에서 프라이머리 키는 기본적으로 다른 보조 인덱스에 비해 비중이 높게 설정됩니다.

외래키 지원

외래 키에 대한 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능입니다.

InnoDB에서 외래 키는 부모 테이블과 자식 테이블 모두 해당 컬럼에 인덱스 생성이 필요하고 변경 시에는 반드시   

부모 테이블이나 자식 테이블에 데이터가 있는지 체크하는 작업이 필요하므로 잠금이 여러 테이블로 전파되고 

그로 인해 데드락이 발생할 때가 많으므로 개발할 때도 외래 키의 존재에 주의하는 것 좋습니다.

MVCC (Multi Version Concurrency Control)

일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며 MVCC의 가장 큰 목적은  

잠금을 사용하지 않는 일관된 읽기를 제공하는 데 있습니다.

멀티 버전은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미입니다.

즉 하나의 레코드에 대해 여러 버전이 유지되고 격리 수준에 따라 어느 데이터가 보여지는지 달라집니다.

InnoDB에서는 언두 로그를 이용해 이 기능을 구현합니다.

 

잠금 없는 일관된 읽기 (Non-Locking Consistent Read)

InnoDB 스토리지 엔진은 MVCC 기술을 이용해 잠금을 걸지 않고 읽기 작업을 수행합니다.

격리 수준이 SERIALIZABLE이 아닌 이상 INSERT와 연결되지 않은 순수한 읽기(SELECT) 작업은   

다른 트랜잭션의 변경 작업과 관계없이 항상 잠금을 대기하지 않고 바로 실행됩니다.

자동 데드락 감지

InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을  

그래프 형태로 관리합니다. InnoDB 스토리지 엔진은 데드락 감지 스레드를 가지고 있어서 이 스레드가 주기적으로

잠금 대기 그래프를 검사해 교착 상태에 빠진 트랜잭션들을 찾아서 그중 하나를 강제 종료합니다.

보통은 MySQL 엔진에서 관리되는 테이블 잠금은 볼 수 없지만 innodb_table_locks 시스템 변수를 활성화하면

InnoDB 스토리지 엔진 내부의 레코드 잠금뿐만 아니라 테이블 레벨의 잠금까지 감지할 수 있습니다.

하지만 매우 많은 트랜잭션을 동시에 실행할 경우 데드락 감지 스레드가 상당히 성능을 저하시킬 수 있으니 주의해야 합니다.

자동화된 장애 복구

InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 여러 가지 매커니즘이 탑재되어 있습니다.

MySQL 서버가 시작될 때 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 데이터 페이지 등에 대한 

일련의 복구 작업이 자동으로 진행됩니다.

 

InnoDB 버퍼풀

InnoDB 스토리지 엔진에서 가장 핵심적인 부분으로 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간입니다.

쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 같이합니다.

버퍼 풀의 크기 설정

InnoDB 버퍼 풀은 innodb_buffer_pool_size 시스템 변수로 크기를 설정할 수 있으며 동적으로 버퍼 풀의 크기를 

확장할 수 있습니다. 하지만 버퍼 풀의 크기 설정은 크리티컬한 변경이므로 가능하면 MySQL 서버가 한가한 시점을 

골라서 진행해야 합니다. InnoDB 버퍼 풀은 내부적으로 128MB 청크 단위로 쪼개어 관리됩니다.

버퍼 풀 전체를 관리하는 잠금(세마포어)로 인해 내부 잠금 경합을 많이 유발해왔는데 이러한 경합을 줄이기 위해

버퍼 풀을 여러 개로 쪼개어 관리할 수 있게 개선되었습니다. 각 버퍼 풀을 버퍼 풀 인스턴스라고 합니다.

버퍼 풀의 구조

InnoDB 스토리지 엔진은 버퍼 풀이라는 거대한 메모리 공간을 페이지 크기의 조각으로 쪼개어 InnoDB 스토리지 엔진이

데이터를 필요로 할 때 해당 데이터 페이지를 읽어서 각 조각에 저장합니다.

버퍼 풀의 페이지 크기 조각을 관리하기 위해 InnoDB 스토리지 엔진은 크게 프리리스트, LRU 리스트, 플러시 리스라는

3개의 자료 구조를 관리합니다.

 

프리리스트는 InnoDB 버퍼 풀에서 실제 사용자 데이터로 채워지지 않은 비어 있는 페이지들의 목록이며

사용자의 쿼리가 새롭게 디스크의 데이터 페이지를 읽어와야 하는 경우에 사용됩니다.

 

LRU 리스트를 관리하는 목적은 디스크로부터 한 번 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼 풀의 메모리에 

유지해서 디스크 읽기를 최소화하는 것입니다.

 

플러시 리스트는 디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지)의 변경 시점 기준의 페이지 목록을

관리합니다. 데이터가 변경되면 InnoDB는 변경 내용을 리두 로그에 기록하고 버퍼 풀의 데이터 페이지에도 변경 내용을 

반영합니다.

버퍼 풀과 리두로그

InnoDB의 버퍼 풀과 리두 로그는 매우 밀접한 관계가 있습니다. InnoDB 버퍼 풀은 데이터베이스 서버의 성능 향상을 위해

데이터 캐시쓰기 버퍼링이라는 2가지 용도가 있는데 버퍼 풀의 메모리 공간만 단순히 늘리는 것은 데이터 캐시 기능만

향상시키는 것입니다.

 

InnoDB 스토리지 엔진에서 리두 로그는 1개 이상의 고정 크기 파일을 연결해서 순환 고리처럼 사용합니다.

리두 로그 파일의 공간은 계속 순환되어 재사용되지만 매번 기록될 때마다 로그 포지션은 계속 증가된 값을 갖게 되는데

이를 LSN이라고 합니다.

InnoDB 버퍼 풀의 더티 페이지는 특정 리두 로그 엔트리와 관계를 가지고 체크포인트가 발생하면 체크포인트 LSN 보다

작은 리두 로그 엔트리와 관련된 더티 페이지는 모두 디스크로 동기화되어야 합니다. 물론 당연히 체크포인트 LSN 보다

작은 LSN 값을 가진 리두 로그 엔트리도 디스코로 동기화되어야 합니다.

버퍼 풀 플러시

MySQL 8.0 버전으로 업그레이드 되면서 대부분 서비스에서는 더티 페이지를 디스크에 동기화하는 부분에서 예전과 같은

디스크 쓰기 폭증 현상은 발생하지 않게되었습니다.

InnoDB 스토리지 엔진은 버퍼 풀에서 아직 디스크로 기록되지 않은 더티 페이지들을 성능상의 악영향 없이 

디스크에 동기화하기 위해 플러시 리스트 플러시, LRU 리스트 플러시 2개의 플러시 기능을 백그라운드로 실행합니다.

 

InnoDB 스토리지 엔진은 리두 로그 공간의 재활용을 위해 주기적으로 오래된 리두 로그 엔트리가 사용하는 공간을 비워야합니다.

이때 오래된 리두 로그 공간이 지워지려면 반드시 InnoDB 버퍼 풀의 더티 페이지가 먼저 디스크로 동기화되어야 합니다.

InnoDB 스토리지 엔진에서 더티 페이지를 디스크로 동기화하는 스레드를 클리너 스레드라고 합니다.

하나의 클리너 스레드는 하나의 버퍼 풀 인스턴스를 처리하도록 맞추는 것이 좋습니다.

Double Write Buffer

InnoDB 스토리지 엔진의 리두 로그는 리두 로그 공간의 낭비를 막기 위해 페이지의 변경된 내용만 기록합니다.

이로 인해 InnoDB의 스토리지 엔진에서 더티 페이지를 디스크 파일로 플러시할 때 일부만 기록되는 문제가 발생하면

그 페이지의 내용은 복구할 수 없을 수도 있습니다. 이렇게 페이지가 일부만 기록되는 현상을 파셜 페이지 또는 톤페이지라고 합니다.

 

InnoDB 스토리지 엔진은 이와 같은 문제를 막기 위해 Double-Write 기법을 이용합니다.

실제 데이터 파일에 변경내용을 기록하기 전에 더티 페이지를 우선 묶어서 한 번의 디스크 쓰기로 시스템 테이블 스페이스의 

DoubleWrite 버퍼에 기록합니다. DoubleWirte 버퍼의 내용은 실제 데이터 파일의 쓰기가 중간에 실패할 때만 원래의 목적으로 

사용됩니다. 데이터의 무결성이 매우 중요한 서비스에서는 DoubleWrite의 활성화를 고려해봐야 합니다.

언두 로그

InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML이 변경되기 이전 버전의 데이터를 별도로 백업합니다.

이렇게 백업된 데이터를 언두 로그라고 합니다.

트랜잭션이 롤백되면 트랜잭션 도중 변경된 데이터를 변경 전 데이터로 복구해야 하는데 이때 언두 로그에 백업해둔

이전 버전의 데이터를 이용해 복구합니다.

특정 커넥션에서 데이터를 변경하는 도중에 다른 커넥션에서 데이터를 조회하면 트랜잭션 격리 수준에 맞게 변경 중인

레코드를 읽지 않고 언두 로그에 백업해둔 데이터를 읽어서 반환하기도 합니다.

체인지 버퍼

RDBMS에서 레코드가 INSERT 되거나 UPDATE 될 때는 데이터 파일을 변경하는 작업뿐 아니라 해당 테이블에 포함된

인덱스를 업데이트하는 작업도 필요합니다.

디스크로부터 읽어와서 업데이트해야 한다면 이를 즉시 실행하지 않고 임시 공간에 저장해두고 바로 사용자에게

반환하는 형태로 성능을 향상시키게 되는데 이때 사용하는 임시 메모리 공간체인지 버퍼라고 합니다.

하지만 중복 여부를 체크해야 하는 유니크 인덱스는 체인지 버퍼를 사용할 수 없습니다.

리두 버퍼

리두 로그는 하드웨어나 소프트웨어 등 여러가지 문제점으로 인해 MySQL 서버가 비정상적으로 종료되었을 때

데이터 파일에 기록되지 못한 데이터를 잃지 않게 해주는 안전 장치입니다. 

성능 저하를 막기 위해 데이터 베이스 서버는 쓰기 비용이 낮은 자료 구조를 가진 리두 로그를 가지고 있습니다.

비정상 종료가 발생하면 리두 로그의 내용을 이용해 데이터 파일을 다시 서버가 종료되기 직전의 상태로 복구합니다.

데이터 베이스 서버는 ACID도 중요하지만 성능도 중요하기 때문에 데이터 파일뿐만 아니라 리두 로그를 버퍼링할 수 있는 

InnoDB 버퍼 풀이나 리두 로그를 버퍼링할 수 있는 로그 버퍼와 같은 자료 구조도 가지고 있습니다.

어댑티브 해시 인덱스

어댑티브 해시 인덱스는 사용자가 수동으로 생성하는 인덱스가 아니라 InnoDB 스토리지 엔진에서 사용자가 자주 

요청하는 데이터에 대해 자동으로 생성하는 인덱스입니다.

어댑티브 해시 인덱스는 B-Tree 검색 시간을 줄여주기 위해 도입된 기능으로 InnoDB 스토리지 엔진에서 

자주 읽히는 데이터 페이지의 키 값을 이용해 해시 인덱스를 만들고 필요할 때마다 어댑티브 해시 인덱스를 검색해서

레코드가 저장된 데이터 페이지를 즉시 찾아갈 수 있습니다.

어댑티브 해시 인덱스는 데이터 페이지를 버퍼 풀 내에서 접근하는 것을 더 빠르게 만드는 기능이기 때문에 디스크에서 읽어와야

하는 경우에는 아무런 도움이 되지 않습니다.

항상 어댑티브 해시 인덱스의 효율이 좋은 것은 아니기 때문에 메모리 공간, CPU 사용량을 종합해서 도입할지 판단해야 합니다.

 

출처 : Real MySQL 8.0 1권