본문 바로가기

CS/데이터베이스

[DB] Real MySQL 8.0 - 4.2 InnoDB 스토리지 엔진 아키텍처

이 글은 Real MySQL 8.0 책을 읽고 정리한 것입니다.

 

Real MySQL 8.0 1권

《Real MySQL》을 정제해서 꼭 필요한 내용으로 압축하고, MySQL 8.0의 GTID와 InnoDB 클러스터 기능들과 소프트웨어 업계 트렌드를 반영한 GIS 및 전문 검색 등의 확장 기능들을 추가로 수록했다.

www.aladin.co.kr

 

 

 


 

 

 

 위 이미지는 InnoDB의 구조를 나타낸 그림이다. MySQL 8.0 기준으로, InnoDB가 가장 성능이 뛰어난 스토리지 엔진이다. MySQL의 스토리지 엔진 중 거의 유일하게 레코드 락을 지원하며, 이러한 장점 덕분에 높은 동시성 처리가 가능하다. (MyISAM은 테이블 락을 지원한다.)

 

MySQL :: MySQL 8.0 Reference Manual :: 8.11.1 Internal Locking Methods

8.11.1 Internal Locking Methods This section discusses internal locking; that is, locking performed within the MySQL server itself to manage contention for table contents by multiple sessions. This type of locking is internal because it is performed entir

dev.mysql.com

 

 

 

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

 InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 저장된다. 즉, 기본 키 값의 순서대로 디스크에 묶음의 형태로 저장된다는 의미이다. 이때 클러스터링되있는 데이터는 기본키를 기준으로 정렬된 상태를 계속 유지한다.

 InnoDB의 기본 키는 클러스터링 인덱스이며, InnoDB에 존재하는 테이블은 클러스터링 테이블이라고 말한다. 클러스터링 인덱스는 아래와 같은 형태로 디스크에 저장된다. 정렬된 상태를 유지하기 때문에, 레인지 스캔을 빨리 처리한다는 장점을 갖는다.

 MyISAM의 경우 클러스터링 키를 지원하지 않는다.

 

 

4.2.2 외래 키 지원

 외래 키는 InnoDB에서만 지원하며, MyISAM에서는 지원하지 않는다. 유의할 점은, 서비스용 데이터베이스에서는 서버 운영의 불편함 때문에 외래키를 생성하지 않는 경우도 자주 있다는 것이다. 왜냐하면, 외래키로 인해 데드락이 발생할 수 있기 때문이다.

 외래키와 관련하여, 부모 테이블 또는 자식 테이블에서 레코드에 변경이 일어날 경우, 참조 무결성을 유지하고 있는지 확인하는 과정이 필요하다. 이 과정에서 락이 여러 테이블로 전파되고, 이로 인해 데드락이 발생할 수 있는 것이다. 예시는 아래와 같다.

 

외래키 데드락 예시

 2개의 세션이 연결되있다고 가정한다. A라는 세션과 B라는 세션이 존재한다. 데드락 예제는 autocommit이 활성화되있을때는 동작하지 않기 때문에, autocommit을 FALSE로 변경해야 한다.

먼저 테이블을 생성한다.

(SETUP)

mysql> create table Parent_Table (id int, b int, primary key(id));
Query OK, 0 rows affected (0.04 sec)

mysql> create table Child_Table (id int, p_id int, foreign key(p_id) references Parent_Table(id));
Query OK, 0 rows affected (0.05 sec)

mysql> insert into Parent_Table values(10,10);
Query OK, 1 row affected (0.01 sec)

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

 Parent_Table은 id를 기본키로 갖고, Child_Table은 p_id를 외래키로 갖는다.

(DEAD LOCK)

1. (A 세션) mysql> insert into Child_Table values (1, 10);
Query OK, 1 row affected (0.00 sec)

mysql> select * from Child_Table;
+------+------+
| id   | p_id |
+------+------+
|    1 |   10 |
+------+------+

2. (B 세션) mysql> insert into Child_Table values(2, 10);
Query OK, 1 row affected (0.05 sec)

3. (A 세션) mysql> update Parent_Table set b=11 where id=10;
-- Locked

4. (B 세션) mysql> update Parent_Table set b=12 where id=10;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  1. A 세션이 자식 테이블에 INSERT 를 수행할 때, 외래키에 의해 참조되었던 부모 테이블의 레코드에 대한 shared-read-only lock(S-LOCK)을 잡는다.
  2. B 세션이 자식 테이블에 INSERT를 수행할 때, 1번과 동일하게, 동일 레코드에 대한 S-LOCK을 잡는다. (S-LOCK 은 공유가능하다.)
  3. A 세션이 부모테이블에서 id = 10에 해당하는 레코드에 UPDATE를 수행하려고 시도한다. UPDATE 쿼리에는 X-LOCK을 거는 것을 시도한다. 그러나, (B 세션이 잡고있는)S-LOCK과 X-LOCK은 동시에 걸릴 수 없으므로, A 세션은 B 세션이 S-LOCK을 놓을 때까지 기다린다.
  4. B 세션에서도 3번과 동일하게, UPDATE를 시도한다. 마찬가지로 X-LOCK을 거는것을 시도하지만, (A 세션이 잡고있는)S-LOCK과 X-LOCK은 공존할 수 없기 때문에 ,B 세션은 A 세션이 S-LOCK을 놓을 때까지 기다린다. 이렇게 데드락이 발생한다.

어떤 락을 잡으려다 데드락이 걸렸는지에 대한 구조는 아래와 같다.

 

MySQL :: MySQL 8.0 Reference Manual :: 13.1.20.5 FOREIGN KEY Constraints

13.1.20.5 FOREIGN KEY Constraints MySQL supports foreign keys, which permit cross-referencing related data across tables, and foreign key constraints, which help keep the related data consistent. A foreign key relationship involves a parent table that hol

dev.mysql.com

 

 

 

4.2.3 MVCC(Multi Version Concurrency Control)

 MVCC란, 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미이다. MVCC의 목적은 락을 걸지 않고 일관된 읽기를 제공하기 위함이다. InnoDB에서는 언두 로드(Undo log)를 이용해 이 기능을 구현한다.

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

 

 위 쿼리문을 실행하면 InnoDB의 버퍼 풀과 데이터 파일의 상태는 아래와 같아진다.

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

 

 이후 위의 UPDATE 구문을 실행시키면, 변경되지 전의 데이터에 대한 정보가 언두 로그에 담겨진다. 이때 데이터 파일에 변경된 사항을 적용하는 것은 백그라운드 스레드가 수행하기 때문에, 정확히 언제 반영되는지는 알 수 없다. 다만, InnoDB는 ACID를 보장하기 때문에, 데이터 파일과 버퍼 풀의 상태가 동일하다고 가정해도 된다.

 그렇다면 위 상황처럼 COMMIT하기 전에, 레코드를 조회하면 어떤 영역에서 데이터를 가져올까? 격리수준에 따라 달라진다.

  • READ_UNCOMMITTED : InnoDB 버퍼 풀의 데이터
  • READ_COMMITED or 이상의 격리수준 : 언두 로그의 데이터

 

 

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

 InnoDB 스토리지 엔진은 MVCC 기술을 이용해서 잠금을 걸지 않고 읽기 작업을 수행한다. 즉, SELECT 구문을 사용해서 읽기를 수행할 때, 락을 잡지 않는다는 것이다. 다만, 격리수준이 SERIALIZABLE 일 경우에는 S-LOCK을 잡는다.

 

 

4.2.5 자동 데드락 감지

mysql> select * from data_lock_waits\\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 140361292872920:30:4:2:140361209006376
REQUESTING_ENGINE_TRANSACTION_ID: 4226
            REQUESTING_THREAD_ID: 191
             REQUESTING_EVENT_ID: 49
REQUESTING_OBJECT_INSTANCE_BEGIN: 140361209006376
         BLOCKING_ENGINE_LOCK_ID: 140361292874536:30:4:2:140361209018192
  BLOCKING_ENGINE_TRANSACTION_ID: 4227
              BLOCKING_THREAD_ID: 193
               BLOCKING_EVENT_ID: 25
  BLOCKING_OBJECT_INSTANCE_BEGIN: 140361209018192
1 row in set (0.02 sec)

 

 InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프(Wait-for List) 형태로 관리한다. 데드락에 걸린 트랜잭션을 발견하면, 일반적으로 트랜잭션들 중에서 언두 로그가 가장 적은 트랜잭션을 롤백한다. 왜냐하면 상대적으로 롤백하는데 비용이 적게 들어가기 때문이다.

 

 다만, InnoDB는 기본적으로 상위 레이어에서의 잠금은 감지하지 못한다. 하지만, innodb_table_locks 시스템 변수를 활성화하면, 테이블락도 감지할 수 있다.

 

 동시 처리 스레드가 매우 많아지거나, 각 트랜잭션이 가진 잠금의 개수가 많아지면 데드락 감지 스레드가 느려진다. 데드락 감지 스레드는 잠금 목록을 검사하기 위해, 잠금 테이블에 락을 걸고 검사한다. 데드락 감지 스레드가 느려지면, 이에 따라 서비스 쿼리를 처리 중인 스레드는 대기해야되기 때문에, 서비스에 악영향을 끼칠 수 있다.

 

 만약 자동 데드락 감지 기능을 비활성화 하고 싶다면, innodb_deadlock_detect를 OFF로 설정하면 가능하다. 다만, 무한정 데드락에 빠지는 경우가 생길 수 있기 때문에, 락을 기다리는 시간에 제한을 걸어야 한다. innodb_lock_wait_timeout 시스템 변수를 활성화하여 시간제한을 걸 수 있다. 기본설정은 50초로 되어있다.

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.00 sec)

 

4.2.6 자동화된 장애 복구

 

 MySQL 서버가 시작될 때 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 데이터 페이지 등에 대한 일련의 복구 작업이 자동으로 진행된다. 일반적으로 InnoDB에 의한 데이터 파일 손상은 거의 없으며, 하드웨어 이슈로 인해 InnoDB 스토리지 엔진이 복구하지 못하는 경우가 있을 수 있다. InnoDB 데이터 파일은 기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 수행한다. 이 단계에서 자동으로 복구될 수 없는 손상이 있다면 자동 복구를 멈추고 MySQL 서버는 종료된다.

 

4.2.7 InnoDB 버퍼 풀

 ‘버퍼 풀’은 InnoDB의 가장 핵심적인 부분으로, 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간이다. 뿐만 아니라, 쓰기 작업을 지연시켜 일괄 작업을 처리하는 버퍼 역할도 수행한다. INSERT, UPDATE, DELETE는 디스크에 랜덤한 영역에 대해 동작하는데, 이러한 작업을 모아서 수행함으로써, 랜덤한 디스크 작업의 횟수를 줄일 수 있다.

 

4.2.7.1 버퍼 풀의 크기 설정

 InnoDB 버퍼 풀의 크기는 동적으로 조절할 수 있다. 가능하면 버퍼 풀의 크기를 적절히 작은 값으로 설정해서 조금씩 상황을 봐 가면서 증가시키는 방법이 적절하다. 다만, 버퍼 풀의 크기 변경은 크리티컬한 작업이기 때문에, 유의해야한다.

 

 InnoDB 버퍼 풀은 전체 버퍼 풀을 여러 개의 작은 버퍼 풀로 쪼개서 관리한다. 이를 통해, 버퍼 풀 관리를 위한 잠금(세마포어)경합을 줄일 수 있었다. 각 버퍼 풀은 ‘버퍼 풀 인스턴스’라고 표현한다.

 

4.2.7.2 버퍼 풀의 구조

 InnoDB 스토리지 엔진은 버퍼 풀이라는 거대한 메모리 공간을 페이지 크기의 조각으로 쪼개어 InnoDB 엔진이 데이터를 필요로 할 때 해당 데이터 페이지를 읽어서 각 조각에 저장한다. 버퍼 풀 페이지들을 관리하기 위해 아래의 자료구조가 사용된다.

 

LRU 리스트(위 이미지)

한 번 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼 풀에 유지하여, 디스크 읽기를 최소화 하기 위한 리스트. 엄밀히 말하면 LRU와 MRU(Most Recently Used)의 조합.

 

플러시(Flush) 리스트

디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지)의 변경 시점 기준의 페이지 목록을 관리. 한번이라도 변경이 가해진 데이터 페이지는 플러시 리스트에 의해 관리된다. 리두 로그(수행된 쿼리를 로깅)의 각 엔트리는 플러시 리스트의 특정 페이지와 연결된다. 리두 로그는 데이터를 복구할 때 사용한다. 어떤 리두 로그의 엔트리부터 복구해야되는지 알기 위해 리두 로그와 데이터 페이지를 ‘체크포인트’를 통해 동기화한다.

 

프리(Free) 리스트

실제 사용자 데이터로 채워지기 전인, 비어 있는 페이지들

 

'CS > 데이터베이스' 카테고리의 다른 글

[DB] Real MySQL 8.0 - 8. 인덱스 & B-tree  (1) 2024.01.23
[DB] SQL - JOIN  (1) 2024.01.23
[DB] SQL 질문 정리  (0) 2024.01.18
[DB] SELECT 문장 처리 순서  (2) 2024.01.18
[DB] SQL -DDL  (0) 2024.01.17