모종닷컴

MySQL InnoDB 스토리지 엔진 (1) 본문

Programming/데이터베이스

MySQL InnoDB 스토리지 엔진 (1)

모종 2022. 7. 30. 14:11
반응형

InnoDB 스토리지 엔진 아키텍처

InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공해서 높은 동시성 처리가 가능한 스토리지 엔진입니다.

InnoDB의 특징들을 하나씩 알아보도록 하겠습니다.

Primary Key 클러스터링

InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 디스크에 저장됩니다. 프라이머리 키 외에 세컨더리 인덱스가 존재하는데 이곳에는 레코드의 주소(디스크의 데이터 위치)가 아닌 프라이머리 키의 값을 논리적인 주소로 사용합니다. 따라서 디스크는 프라이머리 키 기준으로 저장되어 있어 프라이머리 키를 이용한 레인지 스캔은 상당히 빨리 처리될 수 있습니다.

Foreign Key 지원

외래 키에 대한 지원은 InnoDB 스토리지 엔진 특징 중 하나입니다. MyISAM이나 MEMORY 테이블에서는 사용이 불가합니다. 외래 키는 부모 테이블과 자식 테이블 모두 해당 칼럼에 인덱스 생성이 필요하고, 변경 시에는 반드시 부모 테이블이나 자식 테이블에 데이터가 있는 체크하는 작업이 필요하므로 잠금이 여러 테이블로 전파됩니다. 이로 인해 데드락이 발생할 때가 많아 개발 때는 잘 설정하지 않는 키입니다.

외래 키를 사용 중인데 복잡하게 얽혀 있어 처리하기 힘들 때가 있는데 이 때는 foreign_key_checks 시스템을 변수를 OFF로 설정하면 일시적으로 외래 키 관계에 대한 체크 작업을 멈출 수 있습니다. 체크 작업이 멈추면 레코드 삽입 및 삭제에 대해 부가적인 체크가 필요 없기 때문에 빠르게 처리 가능합니다.

하지만 이 체크 작업을 일시적으로 해제했다고 해서 부모와 자식 테이블 간의 관계가 깨진 상태로 유지하면 안되고, 처리가 필요하다면 일관성을 맞춘 후 다시 체크 기능을 활성화해야 합니다.

MVCC (Multi Version Concurrency Control)

레코드 레벨의 트랜잭션을 지원하는 DMBS가 제공하는 기능입니다. MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 것입니다. InnoDB에서는 언두 로그(Undo Log)를 이용해 이 기능을 구현합니다. 여기서 멀티 버전이라 함은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미입니다. 

글을 길게 쓰고 싶지 않아서 그냥 넘어가려고 했지만 이해를 하는데에 있어 너무 중요할 것 같아 예시를 같이 확인해보겠습니다.

예시 

CREATE TABLE member (
  m_id INT NOT NULL,
  m_name VARCHAR(20) NOT NULL,
  m_area VARCHAR(100) NOT NULL,
  PRIMARY KEY (m_id),
  INDEX ix_area(m_area)
);

INSERT INTO member(m_id, m_name, m_area) VALUE (12, '홍길동', '서울');

COMMIT;

위의 SQL을 수행하면 데이터베이스는 아래와 같은 상태가 됩니다.

위 상태에서 아래의 UPDATE 문장을 실행하겠습니다.

UPDATE member SET m_area = '경기' WHERE m_id = 12;

위 업데이트문을 실행하면 메모리는 아래와 같은 형태가 됩니다.

UPDATE 문장이 실행되면 커밋 실행 여부와 관계없이 InnoDB의 버퍼 풀은 새로운 값인 '경기'로 업데이트됩니다. 아직 커밋이나 롤백이 되지 않은 상태에서 다른 사용자가 해당 레코드에 대해 조회를 하게 되면 트랜잭션 격리 레벨에 따라 다르게 동작합니다.

READ_UNCOMMITED 인 경우 InnoDB 버퍼 풀이나 데이터 파일로부터 변경되지 않는 데이터를 읽어서 반환합니다. READ_COMMITED나 그 이상의 격리 수준인 경우 언두 로그 영역의 데이터를 반환합니다. 

또한 위의 상황에서 업데이트가 실행되고 커밋을 하면 더 이상의 변경 작업 없이 지금의 상태를 데이터로 저장하고, 롤백을 하면 언두 영역에 있는 데이터를 버퍼 풀로 다시 복구하고, 언두 영역의 내용을 삭제해버립니다.

추가로 예시에서는 레코드 하나를 가지고 설명하였는데 이러한 이전 버전의 데이터는 무한히 많아질 수 있는데(ex 트랜잭션이 길어지는 경우) 이 경우 언두에서 관리하는 데이터가 삭제되지 못하고 오랫동안 관리돼야 하며, 언두 영역이 저장되는 시스템 테이블스페이스의 공간이 많이 늘어나는 상황이 발생할 수 있습니다.

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

InnoDB 스토리지 엔진은 위의 MVCC기술을 이용해 잠금을 걸지 않고 읽기 작업을 수행합니다. 격리 레벨이 READ_UNCOMMITED, READ_COMMITTED, REPEATABLE_READ인 경우 읽기 작업에서 다른 트랜잭션의 변경 작업과 관계없이 항상 잠금을 대기하지 않고 바로 실행됩니다.

오랜 시간 동안 활성 상태인 트랜잭션으로 인해 MySQL 서버가 느려지거나 문제가 발생할 때가 가끔 있는데, 바로 이러한 일관된 읽기를 위해 언두 로그를 삭제하지 못하고 계속 유지해야 하기 때문에 발생하는 문제입니다. 따라서 트랜잭션이 시작됐다면 가능한 한 빨리 롤백이나 커밋을 통해 트랜잭션을 완료하는 것이 좋습니다.

자동 데드락 감지

InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프 형태로 관리합니다. 이 작업에는 데드락 감지 스레드가 사용되는데 이 스레드가 주기적으로 잠금 대기 그래프를 검사해 교착 상태에 빠진 트랜잭션들을 찾아서 그중 하나를 강제 종료합니다. 강제 종료의 판단 기준은 트랜잭션의 언두 로그 양이며, 언두 로그 레코드를 더 적게 가진 트랜잭션이 일반적으로 롤백의 대상이 됩니다. 

InnoDB 스토리지 엔진은 상위 레이어인 MySQL 엔진에서 관리되는 테이블 잠금은 볼 수가 없어서 데드락 감지가 불확실할 수도 있습니다. 이 때는 innodb_table_locks 변수를 활성화하면 스토리지 엔진 내부의 레코드 잠금뿐만 아니라 테이블 레벨의 잠금까지 감지할 수 있게 됩니다. 책에서는 특별한 이유가 없다면 innodb_table_locks 시스템 변수를 활성화하는 걸 추천하고 있습니다.

데드락 감지 스레드는 잠금 목록을 검사할 때 잠금 상태가 변경되지 않도록 잠금 목록이 저장된 리스트에 새로운 잠금을 걸고 데드락 스레드를 찾게 되는데, 이 데드락 감지 스레드가 느려지면 서비스 쿼리를 처리 중인 스레드는 더는 작업을 진행하지 못하고 대기하면서 서비스에 악영향을 미치게 됩니다. 따라서 동시 처리 스레드가 매우 많은 경우 데드락 감지 스레드는 많은 CPU 자원을 소모할 수도 있습니다.

이런 문제는 innodb_deadlock_detect 변수를 OFF로 설정하면 데드락 감지 스레드는 더는 작동하지 않게 됩니다. 하지만 이걸 비활성화 시키게 되면 데드락이 발생해도 종료되지 않고 무한정 대기하게 되는데, innodb_lcok_wait_timeout 변수를 활용하면 데드락이 발생했을 때 일정 시간이 지나면 자동으로 요청이 실패하고 에러 메시지를 반환하게 할 수 있습니다. 

참고

Real MySQL 8.0 1편의 "InnoDB 스토리지 엔진 아키텍처"을 간략하게 정리한 내용입니다. 자세한 내용이 필요하다면 책을 구매해서 보시는 걸 추천드립니다. 

반응형