본문 바로가기

CS/Database

[Real MySQL] 4.아키텍처 - InnoDB 스토리지 엔진 아키텍처

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

PK에 의한 클러스터링

InnoDB의 모든 테이블은 기본적으로 PK를 기준으로 클러스터링되어 저장된다. 즉, PK의 순서대로 디스크에 저장된다는 뜻이며, 모든 세컨더리 인덱스는 레코드의 주소 대신 PK를 논리적인 주소로 사용한다. 

 

PK가 클러스터링 인덱스이기 때문에 PK를 이용한 레인지 스캔은 상당히 빠르게 처리될 수 있다. 따라서 쿼리 실행 계획에서 PK는 기본적으로 다른 보조 인덱스에 비해 비중 높게 설정된다. (쿼리 실행 계획에서 다른 보조 인덱스보다 PK가 선택될 확률이 높다.) 

 

InnoDB 스토리지 엔진과 달리 MyISAM 스토리지 엔진은 다음 특징을 갖는다. 

  • MyISAM 스토리지 엔진은 클러스터링 키를 지원하지 않는다. 그래서 MyISAM 테이블에서는 PK와 세컨더리 인덱스는 구조적으로 아무런 차이가 없다. PK는 유니크 제약 조건을 가진 세컨더리 인덱스일 뿐이다.  
  • MyISAM 테이블의 PK를 포함한 모든 인덱스는 물리적인 레코드 주소 값을 갖는다. 

외래 키 지원

외래 키에 대한 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능으로 MyISAM이나 MEMORY 테이블에서는 사용할 수 없다. InnoDB에서 외래 키는 부모 테이블과 자식 테이블 모두 해당 칼럼에 인덱스 생성이 필요하고, 변경 시에는 반드시 부모 테이블이나 자식 테이블에 데이터가 있는지 체크하는 작업이 필요하다. 따라서 잠금이 여러 테이블로 전파되고 그로 인해 데드락이 발생하는 경우가 많으므로, 외래 키 사용을 주의해야 한다.

MVCC(Multi Version Concurrency Control)와 잠금 없는 일관된 읽기

일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이다. MVCC의 가장 큰 목적은 잠금(락)을 사용하지 않는 일관된 읽기를 제공하는 데 있다. 그리고 InnoDB는 언두 로그(Undo log)를 통해 이 기능을 구현한다. 여기서 Multi Version이라 하면, 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미이다. 

 

예제를 통해 MVCC가 어떻게 동작하는지 알아보자. 만약 트랜잭션 1이 데이터를 조회해와서 값을 "경기"에서 "서울"로 변경했으며 아직 커밋하지 않은 상태라고 가정하자. 

데이터를 변경하면, 커밋 여부와 상관없이 InnoDB 버퍼 풀에 변경된 데이터가 반영된다. 그리고 변경 전의 데이터는 언두 영역에 보관된다.

이때 트랜잭션 2가 해당 데이터를 조회하면 어떤 데이터가 조회될까? 이는 설정한 트랜잭션 격리 수준에 따라 다르다.

  • READ_UNCOMMITTED: InnoDB 버퍼 풀이 현재 가지고 있는 변경된 데이터("서울")를 반환한다.
  • READ_COMMITED나 그 이상의 격리 수준: 아직 커밋되지 않았기 때문에 InnoDB 버퍼 풀이나 데이터 파일에 있는 내용 대신 변경 전의 데이터를 보관하고 있는 언두 영역의 데이터를 반환한다. 

만약 이 상태에서 트랜잭션 1이 커밋이나 롤백을 하면 어떻게 될까?

  • 커밋: InnoDB 버퍼 풀은 더 이상의 변경 작업 없이 지금의 상태를 영구적인 데이터로 만들어 버린다. 그리고 이때 커밋되었다고 해서 언두 영역의 백업 데이터가 바로 삭제되지는 않는다. 이 언두 영역을 필요로 하는 트랜잭션이 더는 없을 때가 되어서야 삭제된다.
  • 롤백: InnoDB의 언두 영역에 백업된 데이터를 InnoDB 버퍼 풀로 다시 복구하고 언두 영역의 내용을 삭제한다.

이러한 과정을 MVCC라고 한다. 즉, 하나의 레코드에 대해 2개의 버전이 관리되고 필요에 따라 어느 데이터가 보여지는지 여러 상황에 따라 달라진다. InnoDB 스토리지 엔진은 MVCC 기술을 통해 잠금을 걸지 않고 일관된 읽기를 수행할 수 있다. 

자동 데드락 감지

InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프 형태로 관리한다. InnoDB 스토리지 엔진은 데드락 감지 스레드를 가지고 있어서 데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사해 교착 상태에 빠진 트랜잭션들을 찾아서 그중 하나를 강제 종료한다. 이때 어느 트랜잭션을 먼저 강제 종료할 것인지는 트랜잭션의 언두 로그 양이 기준이다. 언두 로그 레코드를 더 작게 가진 트랜잭션이 롤백된다. 그래야 언두 처리를 할 내용이 상대적으로 적으며 트랜잭션 강제 롤백으로 인한 MySQL 서버의 부하도 덜 유발하기 때문이다. 

  • 데드락 감지 스레드의 문제: 일반적인 서비스에서는 데드락 감지 스레드가 트랜잭션의 잠금 목록을 검사해서 데드락을 찾아내는 작업이 크게 부담되지 않는다. 하지만 동시 처리 스레드가 매우 많아지거나 각 트랜잭션이 가진 잠금 개수가 많아지면 데드락 감지 스레드가 느려진다. 동시 처리 스레드 상황에서, 데드락 감지 스레드의 속도가 느려지면 서비스 쿼리를 처리 중인 스레드는 더는 작업을 진행하지 못하고 대기하면서 서비스에 악영향을 미치게 된다. 이렇게 동시 처리 스레드가 매우 많은 경우 데드락 감지 스레드는 더 많은 CPU 자원을 소모할 수도 있다.
  • 데드락 감지 스레드 off: 이런 문제점을 해결하기 위해 MySQL 서버는 innodb_deadlock_detect 시스템 변수를 제공한다. innodb_deadlock_detect를 off로 설정하면 데드락 감지 스레드는 더는 작동하지 않는다. 그러면 InnoDB 스토리지 엔진 내부에서 2개 이상의 트랜잭션이 상대방이 가진 잠금을 요구하는 데드락 상황이 발생해도 누군가 중재하지 않기 때문에 무한정 락을 대기하게 될 것이다.
  • 데드락 감지 스레드를 off하는 대신 timeout 시간 설정innodb_lock_wait_timeout 시스템 변수를 활설화하면 데드락 상황에서 일정 시간이 지나면 자동으로 요청이 실패하고 에러 메시지를 반환하게 된다. 만약 데드락 감지 스레드가 부담되어 innodb_deadlock_detect를 off로 설정하는 경우라면, innodb_lock_wait_timeout을 기본 값인 50초보다 훨씬 낮은 시간으로 변경해서 사용할 것이 권장된다. 

InnoDB 버퍼 풀

InnoDB 스토리지 엔진에서 가장 핵심적인 부분으로, 각종 데이터를 메모리에 캐시하는 공간이다. 그리고 쓰기 작업을 지연시켜 일괄적으로 처리할 수 있는 버퍼의 역할도 같이 한다. 

 

INSERT, UPDATE, DELETE처럼 데이터를 변경하는 쿼리는 데이터 파일의 이곳저곳에 위치한 레코드를 변경하기 때문에 랜덤한 디스트 작업을 발생시킨다. 하지만 버퍼 풀이 이러한 변경된 데이터를 모아서 처리하면 랜덤한 디스크 작업의 횟수를 줄일 수 있다.

언두 로그

InnoDB 스토리지 엔진은 트랜잭과 격리 수준을 보장하기 위해 DML(INSERT, UPDATE, DELETE)로 변경되기 이전 버전의 데이터를 별도로 백업한다. 이렇게 백업된 데이터를 언두 로그(Undo Log)라고 한다. 언두 로그의 데이터는 크게 다음 두 가지 용도로 사용된다.

  • 트랜잭션 롤백에 대비: 트랜잭션이 롤백되면 트랜잭션 도중 변경된 데이터를 변경 전 데이터로 복구해야 하는데, 이때 언두 로그에 백업해준 이전 버전의 데이터를 이용해 복구한다.
  • 트랜잭션의 격리 수준을 보장하며 높은 동시성 제공: 특정 커넥션에서 데이터를 변경하는 도중에 다른 커넥션에서 데이터를 조회하면 트랜잭션 격리 수준에 맞게 변경중인 레코드가 아닌 언두 로그에 백업해둔 데이터를 읽어서 반환하기도 한다.

Reference

  • 위키북스, Real MySQL 8.0