[Database] InnoDB 의 특징

땡글이·2023년 5월 1일
1

목차

  • InnoDB 스토리지 엔진의 특징
    • 프라이머리 키(PK)에 의한 클러스터링
    • MVCC (Multi Version Concurrency Control)
    • 잠금없는 일관된 읽기
    • 자동으로 데드락 감지
    • 자동화된 장애 복구

InnoDB 스토리지 엔진의 특징

InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공하고, 그 때문에 동시성 처리가 가능하며 안정적이고 성능이 뛰어납니다. InnoDB의 구조는 다음과 같습니다. 우선 InnoDB의 구조를 기반으로 InnoDB의 특징부터 살펴보도록 하겠습니다.

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

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

PK가 클러스터링 인덱스이기 때문에, PK를 이용한 레인지 스캔은 상당히 빠르고, 쿼리 실행계획에서도 PK는 다른 보조 인덱스에 비해 비중이 높게 설정되어 있습니다. 왜? 디스크에 순서대로 저장할 때 사용된 클러스터링 인덱스를 사용하는 것이 세컨더리 인덱스에 비해 훨씬 효율적이기 때문입니다.

  • MyISAM 스토리지 엔진은 클러스터링 키를 지원하지 않습니다.

MVCC (Multi Version Concurrency Control)

일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며, MVCC는 여러 버전의 데이터를 동시에 유지하면서 다른 트랜잭션의 작업이 해당 데이터에 영향을 미치지 않도록 제어하는 방식을 의미합니다.
또한, MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 것입니다.

잠금을 사용하지 않는 일관된 읽기?

일반적인 잠금 방식의 경우, 트랜잭션이 데이터를 읽거나 쓸 때 해당 데이터에 대한 잠금을 걸어 다른 트랜잭션이 해당 데이터를 수정하는 것을 막습니다. 그러나 이 방식은 다른 트랜잭션의 작업에 대해 잠금을 요청하기 때문에 일부 트랜잭션은 대기할 수 있으며, 이로 인해 성능 저하가 발생할 수 있습니다.
반면 MVCC 방식은 일반적으로 새로운 버전의 데이터를 생성하여 기존 데이터와 함께 유지합니다. 이렇게 생성된 새로운 버전의 데이터는 다른 트랜잭션이 기존 버전의 데이터를 읽거나 쓸 수 있도록 제공하면서도, 동시에 현재 작업 중인 트랜잭션이 보는 데이터는 일관성 있게 보여집니다.
MVCC에서는 각 트랜잭션의 격리 레벨과 시작 시간, 커밋 시간 등을 비교하여 해당 트랜잭션이 보는 데이터 버전을 결정합니다.

InnoDB는 언두 로그(Undo log)를 활용해 이 기능을 구현합니다. 멀티버전이라 함은, 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미입니다. 아래 그림을 통해 이해해보겠습니다.

왼쪽 그림은 INSERT 문을 통해 홍길동 member에 대한 데이터가 삽입된 상태를 의미합니다. 그리고 오른쪽 그림은 UPDATE 문을 통해, 홍길동 유저의 데이터를 수정한 상태입니다.

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

그런데 UPDATE 문장이 실행되면 커밋 실행 여부와 상관없이 InnoDB의 버퍼 풀은 새로운 값인 '경기'로 업데이트됩니다.

아직 COMMIT 이나 ROLLBACK이 되지 않은 상태에서 다른 사용자가 레코드를 조회하면 어떻게 될까요? 답은, 격리 수준(Isolation level)에 따라 다릅니다.

  • READ_UNCOMMITED : InnoDB 버퍼 풀이 현재 가지고 있는 변경된 데이터를 읽어서 반환
  • READ_COMMITED : 변경되기 이전의 내용을 보관하고 있는 언두 영역의 데이터를 반환
  • REPEATABLE_READ : (상동)
  • SERIALIZABLE : (상동)

즉, 하나의 레코드에 대해 2개의 버전이 유지되고, 필요에 따라 어느 데이터가 보여지는지 여러 가지 상황에 따라 달라지는 구조입니다.

지금까지 쿼리가 실행되고, COMMIT이나 ROLLBACK이 실행되기 이전의 상황을 가정해보았습니다. 만약 COMMIT 명령이 실행된다면 InnoDB는 버퍼 풀의 상태를 영구적인 데이터로 만들어버립니다. 단, ROLLBACK을 실행하면 언두 영역에 있는 백업된 데이터를 InnoDB 버퍼풀로 다시 복구하고, 언두 영역의 내용을 삭제합니다.


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

일반적인 잠금 방식의 경우, 트랜잭션이 데이터를 읽거나 쓸 때 해당 데이터에 대한 잠금을 걸어 다른 트랜잭션이 해당 데이터를 수정하는 것을 막습니다. 그러나 이 방식은 다른 트랜잭션의 작업에 대해 잠금을 요청하기 때문에 일부 트랜잭션은 대기할 수 있으며, 이로 인해 성능 저하가 발생할 수 있습니다.

반면 InnoDB의 MVCC 방식은 일반적으로 새로운 버전의 데이터를 생성하여 기존 데이터와 함께 유지합니다. 이렇게 생성된 새로운 버전의 데이터는 다른 트랜잭션이 기존 버전의 데이터를 읽거나 쓸 수 있도록 제공하면서도, 동시에 현재 작업 중인 트랜잭션이 보는 데이터는 일관성 있게 보여집니다.

MVCC에서는 각 트랜잭션의 격리수준과 시작 시간, 커밋 시간 등을 비교하여 해당 트랜잭션이 보는 데이터 버전을 결정합니다. 격리수준에 따라서 어떻게 MVCC가 동작하는지 이해해보겠습니다.

만약 격리수준이 SERIALIZABLE이 아닌 READ_UNCOMMITED, READ_COMMITED, REPEATABLE_READ 이라면 INSERT와 연결되지 않은 순수한 읽기 작업은 다른 트랜잭션의 변경 작업과 상관없이 항상 잠금을 대기하지 않고 실행됩니다.

즉, 특정 트랜잭션이 레코드를 변경하고 아직 커밋을 수행하지 않았더라도, 이 변경 트랜잭션이 다른 사용자의 읽기 작업을 방해하지 않습니다. 이를 잠금 없는 일관된 읽기 라고 합니다.

가끔 오랜 시간 활성 상태인 트랜잭션으로 인해 MySQL 서버가 느려지거나 문제가 발생할 때가 있는데, 바로 이러한 일관된 읽기를 위해 언두 로그를 삭제하지 못하고 계속 유지해야 하기 때문에 발생하는 문제입니다.

따라서 트랜잭션이 시작됐다면 가능한한 빨리 COMMIT 이나 ROLLBACK 을통해 트랜잭션을 완료하는 것이 데이터베이스 서버의 성능에 좋습니다.


자동으로 데드락 감지

InnoDB는 내부적으로 잠금이 교착상태에 빠지지 않았는지 체크하기 위해, 잠금 대기 목록을 그래프(Wait-for-List) 형태로 관리합니다.

또한, 데드락 감지 스레드가 있어서, 데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사해 교착상태에 빠진 트랜잭션들을 찾아서 그 중 하나를 강제종료시킵니다.

  • 강제종료할 트랜잭션을 고르는 기준은 "언두로그의 양이 작은 트랜잭션"입니다.
  • 언두로그의 양이 많다면, 롤백 시 많은 데이터를 언두 처리해야하지만, 적다면 롤백 시 적은 데이터만 언두 처리해주면 되기 때문입니다.

구글에서는 PK 기반의 조회 및 변경이 아주 높은 빈도로 실행되는 서비스가 많았는데, 이런 서비스는 매우 많은 트랜잭션을 동시에 실행하기 때문에 데드락 감지 스레드가 상당히 성능을 저하시킨다는 것을 알아냈습니다.
그래서 MySQL에서는 데드락 감지 스레드 활성화 여부를 innodb_deadlock_detect 옵션으로 정할 수 있게 수정되었습니다.


자동화된 장애 복구

InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 몇 가지 메커니즘이 탑재되어 있습니다. 그러한 메커니즘을 이용해 MySQL 서버가 시작될 때, 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 (Partial Write) 데이터 페이지 등에 대한 일련의 복구 작업이 자동으로 진행됩니다. InnoDB 스토리지 엔진은 매우 견고해서 데이터 파일이 손상돼 문제가 생기는 경우는 거의 없습니다.

InnoDB 데이터파일은 기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 수행합니다.

다만, MySQL 서버와 무관하게 디스크나 서버 하드웨어의 이슈로 InnoDB 엔진이 자동으로 복구를 못하는 경우도 있는데 이는 복구하기가 쉽지 않습니다. 그래서 innodb_force_recovery 시스템 변수를 이용해서 여러 단계를 적용해봄으로써 데이터베이스 서버를 복구해야합니다.

  • innodb_force_recovery 값을 키울수록 심각한 상황임을 의미하고 데이터 손실 가능성이 커지고 복구 가능성이 줄어듭니다.

참고로, innodb_force_recovery 가 0이 아닌 복구모드에선 select 이외의 insert, delete, update 쿼리는 사용불가능합니다.

innodb_force_recovery 값에 따른 단계별 복구절차

  • 1(SRV_FORCE_IGNORE_CORRUPT)

    • InnoDB의 테이블스페이스의 데이터나 인덱스 페이지에서 손상된 부분이 발견돼도 무시하고 MySQL 서버 시작
    • "Database page corruption on disk or a failed" 메시지가 출력될 때 해당 모드로 복구
  • 2(SRV_FORCE_NO_BACKGROUND)

    • InnoDB의 언두 데이터를 삭제하는 과정에서 장애가 발생했다면 해당 모드로 복구
  • 3(SRV_FORCE_NO_TRX_UNDO)

    • 커밋되지 않고 종료된 트랜잭션은 계속 그 상태로 남아있게 MySQL 서버를 시작하는 모드
    • MySQL 서버가 시작되면 mysqldump 를 이용해 데이터를 백업해서 다시 데이터베이스를 구축하는 것이 좋다.
  • 4(SRV_FORCE_NO_IBUF_MERGE)

    • InnoDB는 데이터 변경 작업으로 인한 인덱스 변경 작업을 상황에 따라 즉시 처리할 수도 있고 insert buffer에 저장해두고 나중에 처리할 수도 있다. 언제 insert buffer의 내용이 merge 될지는 알 수 없다.
    • MySQL을 종료해도 merge되지 않을 수 있는데 만약 MySQL 재시작 시에 insert buffer의 손상을 감지하면 에러 발생
    • 해당 모드는 InnoDB 스토리지 엔진이 insert buffer의 내용을 무시하게 강제로 MySQL을 시작하게 하는 모드
    • Insert buffer는 실제 데이터와 관련된 것이 아닌, 인덱스와 관련된 부분으로 테이블을 덤프한 후 다시 데이터베이스를 구축하면 데이터의 손실 없이 복구 가능
  • 5(SRV_FORCE_NO_UNDO_LOG_SCAN)

    • 어떤 이유로 MySQL 서버가 종료될 때, 진행 중인 트랜잭션이 있다면, MySQL은 커넥션을 끊고 별도의 정리작업 없이 종료
    • MySQL이 다시 시작되면, InnoDB는 언두 레코드를 이용해 데이터 페이지를 복구하고 리두 로그를 적용해 종료 시점이나 장애 발생 시점의 상태를 재현
    • 마지막으로 InnoDB는 커밋되지 않은 트랜잭션에서 변경한 작업을 모두 롤백 처리
    • 만약 언두 로그를 사용할 수 없다면 InnoDB 엔진의 에러로 MySQL 서버 시작이 불가능
    • 해당 모드는 InnoDB 엔진이 언두로그를 모두 무시하고 MySQL을 시작하는 모드
    • 단, 이 모드로 복구되면 MySQL 서버가 종료되던 시점에 커밋되지 않았던 작업도 모두 커밋된 것처럼 처리되므로 실제로는 잘못된 데이터가 남게 된다.
    • 이 때에도 mysqldump 를 이용해 데이터를 백업하고 데이터베이스를 새로 구축해야한다.
  • 6(SRV_FORCE_NO_LOG_REDO)

    • InnoDB의 리두 로그가 손상되면 MySQL 서버 재시작 불가능
    • 해당 모드는 리두 로그를 무시한 채 MySQL 서버 재시작하는 모드
      • 그래서 커밋됐다하더라도 리두 로그에만 기록되고 데이터 파일에 기록되지 않은 데이터는 모두 무시
      • 즉, 마지막 체크포인트 시점의 데이터만 남게 된다
    • 이 때에도 mysqldump를 이용해 데이터를 모두 백업하고 서버를 새로 구축하는 것이 좋다.

Reference

Real MySQL 8.0.1

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글