티스토리 뷰

728x90
반응형

InnoDB 스토리지 엔진 아키텍처


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

프라이머리 키에 의한 클러스트링
  • InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스트링되어 저장됩니다. 즉 프라이머리 키 값의 순서대로 디스크에 저장된다는 의미이며, 모든 세컨더리 인덱스는 레코드의 주소 대신 프라이머리 키의 값을 논리적인 주소로 사용합니다. 프라이머리 키가 클러스터링 인덱스이기 때문에 프라이머리 키를 이용한 범위 스캔은 상당히 빨리 처리될 수 있습니다. 
외래키 지원
  • 외래 키에 대한 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능으로 MyISAM이나 Memory 테이블에서는 사용할 수 없습니다. 
  • InnoDB에서 외래키는 부모 테이블과 자식 테이블 모두 해당 컬럼에 인덱스 생성이 필요하고, 변경시에는 반드시 부모 테이블이나 자식 테이블에 데이터가 있는지 확인하는 작업이 필요하므로 잠금이 여러 테이블로 전파되고, 그로 인해 데드락이 발생할 때가 많으므로 개발할 때도 외래키의 존재에 주의하는것이 좋습니다.
  • 외래키 체크를 일시적으로 해제했다고 해서 부모와 자식 테이블 간의 관계가 깨진 상태 그대로 유지해도 된다는것을 의미하지는 않습니다. 부모 레코드를 삭제했다면 일관성있게 연관된 자식 레코드도 삭제해주어야 합니다.   
MVCC (Multi Version Concurrency Control)
  • 일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며, MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는데 있습니다. InnoDB는 언두로그를 사용하여 이 기능을 구현합니다. 멀티 버전이라고 하는 것은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리 된다는 의미입니다. 
  • 아래 사진처럼 업데이트 쿼리가 실행되면 커밋 실행 여부와 상관없이 InnoDB의 버퍼 풀은 새로운 값인 제주도로 업데이트가 됩니다. 그리고 디스크의 데이터 파일에는 체크 포인트나 InnoDB의 Write 스레드에 의해 새로운 값으로 업데이트가 되어 있을수도 있고 아닐 수 도 있습니다. 그리고 기존 데이터는 언두 로그 영역으로 복사가 되는 과정을 거치게 되며 이 상태에서 커밋을 한다면 InnoDB는 더 이상의 변경 작업 없이 지금의 상태를 영구적인 데이터로 만들어 버립니다. 하지만 롤백을 한다면 언두 로그 영역에 있는 백업된 데이터를 다시 버퍼 풀로 복구하고 언두 로그 영역의 내용을 삭제하게 됩니다. 또한 커밋이 된다고해서 언두 로그 영역의 내용이 바로 삭제되는 것이 아닌 해당 언두 영역을 필요로 하는 트랜잭션이 없는 경우 삭제되게 됩니다.
  • 또한 아래의 상태에서 사용자가 SELECT 쿼리를 보낸다면 UPDATE된 레코드를 반환할까? 아니면 기존의 레코드를 반환할까? 그것은 격리 수준에 따라 달라집니다. 격리 수준이 READ_UNCOMMITTED인 경우에는 변경된 상태의 레코드를 READ_COMMITED는 기존 레코드를 반환하게 됩니다.

잠금 없는 일관된 읽기
  • InnoDB 스토리지 엔진은 MVCC 기술을 이용해 잠금을 걸지 않고 읽기 작업을 수행합니다. 잠금을 걸지 않기 때문에 InnoDB에서 읽기 작업은 다른 트랜잭션이 가지고 있는 잠금을 기다리지 않고, 읽기 작업이 가능합니다.

자동 데드락 감지
  • InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 확인하기 위해 잠금 대기 목록을 그래프 형태로 관리합니다. InnoDB 스토리지 엔진은 데드락 감지 스레드를 가지고 있어서 데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사하여 교착 상태에 빠진 트랜잭션을 찾아서 강제 종료를 하게 됩니다. 이때 교착 상태에 빠진 트랜잭션이 여러개인 경우 어느 트랜잭션을 먼저 강제 종료 할지는 트랜잭션의 언두 로그의 양으로 판단하게 되며, 언두 로그 레코드를 더 적게 가진 트랜잭션이 일반적으로 롤백의 대상이 됩니다. 트랜잭션이 언두 레코드를 적게 가졌다는 말은 롤백을 해도 언두 처리를 해야할 양이 작다는 것을 의미하며, 트랜잭션을 강제 롤백해도 MySQL 서버의 부하도 적다는 말입니다.
InnoDB 버퍼 풀
  • 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간입니다. 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼의 역할도 같이 수행합니다. 일반적인 애플리케이션에서는 INSERT, UPDATE, DELETE 처럼 데이터를 변경하는 쿼리는 데이터 파일의 이곳저곳에 위치한 레코드를 변경하기 때문에 랜덤한 디스크 작업을 발생시키는데 버퍼 풀은 이러한 변경된 데이터를 모아서 한번에 처리하면 랜덤한 디스크 작업의 횟수를 줄일 수 있습니다. 
버퍼 풀의 구조
  • InnoDB 스토리지 엔진은 버퍼 풀이라는 거대한 메모리 공간을 페이지 크기의 조각으로 쪼개어 InnoDB 스토리지 엔진이 데이터를 필요로 할때 해당 데이터 페이지를 읽어서 각 조각에 저장합니다.
  • 버퍼 풀의 페이지 크기 조각을 관리하기 위해 InnoDB 스토리지 엔진은 크게 LRU 리스트와 플러시 리스트, 프리 리스트라는 3개의 자료 구조를 관리합니다.
    • 프리 리스트 : InnoDB 버퍼 풀에서 실제 사용자가 데이터로 채워지지 않은 비어 있는 페이지들의 목록이며, 사용자의 쿼리가 새롭게 디스크의 데이터 페이지를 읽어와야 하는 경우 사용
    • 플러시 리스트 : 디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지)의 변경 시점 기준의 페이지 목록을 관리합니다. 디스크에서 읽은 상태 그대로 전혀 변경이 없다면 플러시 리스트에서 관리하지 않지만 일단 한번 데이터 변경이 가해진 데이터 페이지는 플러시 리스트에 의해 관리가 되고 특정 시점이 되면 디스크로 기록되어야 합니다.
    • LRU 리스트 : 아래와 같은 구조를 가지고 있으며, LRU와 MRU가 결합된 형태라고 보면 됩니다. LRU 리스트를 관리하는 목적은 디스크로부터 한번 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼 풀의 메모리에 유지해서 디스크 읽기 작업을 최소화하는 것입니다.

버퍼 풀과 리두 로그
  • InnoDB의 버퍼 풀은 서버의 메모리가 허용하는 만큼 크게 설정하면 할수록 쿼리의 성능이 빨라집니다. InnoDB 버퍼 풀은 데이터 베이스 서버의 성능을 향상 시키기 위래 데이터 캐시와 쓰기 버퍼링이라는 두가지 용도가 있는데, 버퍼 풀의 메모리 공간만 단순히 늘리는 것은 데이터 캐시 기능만 향상시키는 것입니다.
  • InnoDB의 버퍼 풀은 디스크에서 읽은 상태로 전혀 변경되지 않은 클린 페이지와 INSERT, UPDATE, DELETE명령을 통해 변경된 데이터를 가지는 더티 페이지도 가지고 있습니다. 더티 페이지는 디스크와 버퍼 풀의 데이터 상태가 다르기 때문에 언젠가는 디스크로 기록해야 합니다. 또한 더티 페이지는 버퍼 풀에 무한정 머무를 수 없고 데이터 변경이 일어나면 리두 로그 파일에 기록했던 로그 엔트리는 어느 순간 다시 새로운 로그 엔트리로 덮어 쓰입니다. 그래서 InnoDB 스토리지 엔진은 전체 리두 로그 파일에서 재사용 가능한 공간과 당장 재사용 불가능한 공간을 구분해서 관리해야 하는데 재사용 불가능한 공간을 활성 리두 로그라고 합니다.
  • 리두 로그 파일의 공간은 계속 순환되어 재사용되지만 매번 기록할때마다 로그 포지션은 증가된 값을 가지게 되는데 이를 LSN이라고 하며 InnoDB 스토리지 엔진은 주기적으로 체크 포인트를 발생시켜 리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화하는데 이렇게 발생한 체크 포인트 중 가장 최근 체크 포인트 지점의 LSN이 활성 리두 로그 공간의 시작점이 됩니다. 또한 활성 리두 로그 공간의 마지막은 계속 증가하기 때문에 체크포인트와 무관하며 가장 최근 체크 포인트의 LSN과 마지막의 리두 로그 엔트리의 LSN의 차이를 체크 포인트 에이지라고 하며 이 에이지는 활성 리두 공간의 크기를 말합니다.

버퍼 풀 플러시
  • MySQL 5.6 버전까지는 InnoDB 스토리지 더티 페이지 플러시 기능이 부드럽지 않았는데 MySQL 5.7을 거쳐서 MySQL 8.0버전으로 업그레이드되면서 더티 페이지를 디스크에 동기화하는 부분이 부드러워 졌습니다. InnoDB 스토리지 엔진은 버퍼 풀에서 더티 페이지를 동기화하는 방법에는 다음과 같디 2개의 플러시 기능을 백그라운드로 실행합니다.

💡 플러시 리스트 플러시

  • InnoDB 스토리지 엔진은 리두 로그 공간의 재활용을 위해 주기적으로 오래된 리두 로그 엔트리가 사용하는 공간을 비워야합니다. 그런데 이때 오래된 리두 로그 공간이 지워지려면 반드시 InnoDB 버퍼 풀의 더티 페이지가 먼저 디스크로 동기화해야하며 이를 위해 InnoDB 스토리지 엔진은 주기적으로 플러시 리스트 플러시 함수를 호출합니다.

💡 LRU 리스트 플러시

  • InnoDB 스토리지 엔진은 LRU 리스트에서 사용 빈도가 낮은 데이터 페이지들을 제거해서 새로운 페이지들을 읽어올 공간을 만들어야 하는데 이를 위해 LRU 리스트 플러시 함수가 사용됩니다.
Double Write Buffer
  • InnoDB 스토리지 엔진의 리두 로그는 리두 로그의 공간의 낭비를 막기 위해 페이지의 변경된 내용만을 기록합니다. 이로 인해 InnoDB의 스토리지 엔진에서 더티 페이지를 디스크 파일로 플러시할 때 일부만 기록되는 문제가 발생하면 그 페이지의 내용은 복구할 수 없을 수도 있습니다. 이렇게 페이지 일부만 기록되는 현상을 파셜 페이지 또는 톤 페이지라고 하는데 이런 현상은 하드웨어의 오작동이나 시스템의 비정상 종료 등으로 발생할 수 있습니다. InnoDB 스토리지 엔진에서는 이 같은 문제를 해결하기 위해 Double-Write 기법을 이용합니다.
  • InnoDB 스토리지 엔진은 실제 데이터 파일에 변경 내용을 기록하기 전에 A~E까지의 더티 페이지를 우선 묶어서 한 번의 디스크 쓰기로 Double Write 버퍼에 기록합니다. 그리고 InnoDB 스토리지 엔진은 각 더티 페이지를 파일의 적당 위치에 하나씩 랜덤으로 쓰기를 실행합니다.
  • 이렇게 Double Write 버퍼 공간에 기록된 변경 내용은 실제 데이터 파일에 A ~ E 까지의 더티 페이지가 정상적으로 기록되면 더 이상 필요가 없어집니다. Double Write 버퍼의 내용은 실제 데이터 파일의 쓰기가 중간에 실패할 때만 원래의 목적으로 사용됩니다. A와 B 페이지는 정상적으로 데이터 파일에 기록되었지만 C 페이지가 기록되는 도중에 운영체체가 비정상적으로 종료하게 된다면 InnoDB 스토리지 엔진은 재시작 될 때 항상 Double Write 버퍼의 내용과 데이터 파일의 페이지들을 모두 비교하여 다른 내용을 담고 있는 페이지가 있으면 Double Write 버퍼의 내용을 데이터 파일의 페이지로 복사하게 되빈다.

언두 로그
  • InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML로 변경되기 이전 버전의 데이터를 별도로 백업합니다. 이렇게 백업된 데이터를 언두 로그라고 합니다. 아래는 언두 로그의 용도입니다.

💡 트랜잭션 보장

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

💡 격리 수준 보장

  • 특정 커넥션에서 데이터를 변경하는 도중 다른 커넥션에서 데이터를 조회하면 트랜잭션의 격리 수준에 맞게 변경중인 레코드를 읽지 않고 언두 로그에 백업해둔 테이더를 읽어서 반환하기도 합니다.
언두 테이블 스페이스 관리
  • 언두 로그가 저장되는 공간을 언두 테이블스페이스라고 합니다. MySQL 5.6 이전 버전에서는 언두 로그가 모두 시스템 테이블 스페이스에 저장되었지만 MySQL 8.0버전부터는 항상 시스템 테이블스페이스 외부의 별도 로그 파일에 기록되도록 개선되었습니다.

💡 언두 테이블스페이스의 불필요한 공간을 잘라내는 2가지 방법

 

✔️ 자동 모드

  • 트랜잭션이 데이터를 변경하면 이전 버전의 데이터를 언두 로그로 기록하는데, 트랜잭션이 커밋되면 더 이상 언두 로그에 복사된 값은 불필요합니다. InnoDB 스토리지 엔진의 퍼지 스레드는 주기적으로 깨어나서 언두 로그 공간에서 불필요해진 언두 로그를 삭제하는 작업을 실행하는데 이 작업을 언두 퍼지라고 합니다.

✔️ 수동 모드

  • 언두 테이블스페이스를 비활성화해서 언두 테이블스페이스가 더이상 사용되지 않도록 설정하면 퍼지 스레드는 비활성 상태의 언두 테이블스페이스를 찾아서 불필요한 공간을 잘라내고 운영체제로 해당 공간을 반납하게 됩니다. 반납이 완료되면 언두 테이블스페이스를 다시 활성화합니다. 그리고 수동 모드는 언두 테이블스페이스가 최소 3개 이상은 돼야 작동합니다.
체인지 버퍼
  • RDBMS에서 레코드가 INSERT되거나 UPDATE될 때는 데이터 파일을 변경하는 작업뿐만 아니라 해당 테이블에 포함된 인덱스를 업데이트하는 작업도 필요합니다. 그런데 인덱스를 업데이트하는 작업은 랜덤하게 디스크를 읽는 작업이 필요하므로 테이블에 인덱스가 많다면 이 작업은 상당히 많은 자원을 소모하게 됩니다. 그래서 InnoDB는 변경해야할 인덱스 페이지가 버퍼 풀에 있으면 바로 업데이트를 수행하지만 그렇지 않고 디스크로부터 읽어와서 업데이트해야하는 경우면 이를 즉시 실행하지 않고 임시 공간에 저장해두고 바로 사용자에게 결과를 반환하는 형태로 성능을 향상시키는데 이때 사용하는 임시 메모리 공간을 체인지 버퍼라고 합니다.
  • 사용자에게 결과를 전달하기 전에 반드시 중복 여부를 체크해야 하는 유니크 인덱스는 체인지 버퍼를 사용할 수 없습니다. 체인지 버퍼에 임시로 저장된 인덱스 레코드 조각은 이후 백그라운드 스레드에 의해 병합되는데, 이 스레드를 체인지 버퍼 머지 스레드라고 합니다.
리두 로그 및 로그 버퍼
  • 리두 로그는 트랜잭션의 4가지 요소인 ACID 중에서 D(Durable)에 해당하는 영속성과 가장 밀접하게 연관되어 있습니다. 리두 로그는 하드웨어나 소프트웨어 등 여러 가지 문제점으로 인해 MySQL 서버가 비정상적으로 종료했을때 데이터 파일에 기록되지 못한 데이터를 잃지 않게 해주는 안전장치입니다.
  • 데이터 베이스 서버에서 리두 로그는 트랜잭션이 커밋할때마다 즉시 디스크로 기록되도록 시스템 변수를 설정하는것을 권장하고, 당연히 그렇게 돼야만 서버가 비정상적으로 종료되었을 때 복구를할 수 있습니다. 하지만 트랜잭션이 커밋될때마다 리두 로그를 디스크에 기록하는 작업은 많은 부하를 유발할 수 있습니다. 그래서 InnoDB 스토리지 엔진은 리두 로그를 어느 주기마다 디스크에 동기화할지를 시스템 변수를 사용하여 결정할 수 있습니다.
리두 로그 활성화 및 비활성화
  • InnoDB 스토리지 엔진의 리두 로그는 하드웨어나 소프트웨어 등 여러가지 문제점으로 MySQL 서버가 비정상적으로 종료되었을 때 데이터 파일에 기록하지 못한 트랜잭션을 복구하기 위해 항상 활성화되어 있습니다. MySQL 서버에서 트랜잭션이 커밋되도 데이터 파일은 즉시 디스크로 동기화되지 않는 반면, 리두 로그는 항상 디스크로 기록됩니다.
어댑티브 해시 인덱스
  • 어댑티브 해시 인덱스란 사용자가 수동으로 생성한 인덱스가 아닌 InnoDB 스토리지 엔진에서 사용자가 자주 요청하는 데이터에 대해 자동으로 생성하는 인덱스이며, 시스템 변수를 사용하여 어댑티브 해시 인덱스 기능을 활성화하거나 비활성화할 수 있습니다.
  • InnoDB 스토리지 엔진은 자주 읽히는 데이터 페이지의 키 값을 사용하여 해시 인덱스를 만들고, 필요할 때마다 어댑티브 해시 인덱스를 검색해서 레코드가 저장된 데이터 페이지를 즉시 찾아갈 수 있습니다. 
  • 해시 인덱스는 '인덱스 키 값과' 해당 인덱스 키 값이 저장된 '데이터 페이지 주소'의 쌍으로 관리되는데 인덱스 키 값은 B-Tree 인덱스의 고유 번호(ID)와 B-Tree 인덱스의 실제 키 값의 조합으로 생성됩니다.
  • 어댑티브 해시 인덱스의 키 값에 B-Tree 인덱스의 고유 번호가 저장되는 이유는 InnoDB 스토리지 엔진에서 어댑티브 해시 인덱스는 하나만 존재하기 때문입니다.
  • 어댑티브 해시 인덱스는 버퍼 풀에 올려진 데이터 페이지에 대해서만 관리하고 버퍼 풀에서 해당 데이터 페이지가 없어지면 어댑티브 해시 인덱스에서도 해당 페이지의 정보는 사라집니다.
  • 어댑티브 해시 인덱스를 사용한다고 해서 모든 상황에서 좋은 것은 아닙니다. 어댑티브 해시 인덱스 또한 데이터 페이지의 인덱스 키가 해시 인덱스로 만들어져야 하고 불필요한 경우 제거되어야하며, 어댑티브 해시 인덱스가 활설화되면 InnoDB 스토리지 엔진은 그 키 값이 해시 인덱스에 있든 없든 검색해봐야 합니다. 즉 해시 인덱스의 효율이 없는 경우에도 InnoDB는 계속해서 해시 인덱스를 사용할 것 입니다.

 

 


해당 내용은 Real Mysql 8.0 책을 바탕으로 작성되었습니다.

 



 

 

728x90
반응형

'Mysql' 카테고리의 다른 글

Mysql - 인덱스  (0) 2022.04.27
Mysql - 트랜잭션과 잠금  (0) 2022.04.19
Mysql - 엔진 아키텍처  (0) 2022.04.14
Mysql - 시스템 변수  (0) 2022.04.12
Mysql - 뷰(View)란 무엇일까?  (2) 2022.01.09