4.2 InnoDB 스토리지 엔진 아키텍처
InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공하며, 그 때문에 높은 동시성 처리가 가능하고 안정적이며 성능이 뛰어나다.
4.2.1 프라이머리 키에 의한 클러스터링
InnoDB의 모든 테이블은 프라이머리 키를 기준으로 클러스터링되어 저장된다.
따라서 프라이머리 키 값의 순서대로 디스크에 저장되며, 세컨더리 인덱스는 레코드의 주소 대신 프라이머리 키의 값을 논리적인 주소로 사용한다.
이 때문에 프라이머리 키는 다른 인덱스에 비해 실행 계획에서 비중이 높게 설정된다.
4.2.2 외래 키 지원
외래 키에 대한 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능으로 MyISAM 이나 MEMORY 테이블에서는 사용할 수 없다.
외래 키는 개발 환경에서 좋은 가이드 역할을 할 수 있지만, 쿼리 실행에서 잠금이 여러 테이블로 전파되기 때문에 데드락이 발생할 때가 많으므로 주의하는 것이 좋다.
외래 키 관계 체크로 인해 수동으로 데이터를 적재하거나 스키마 변경시 작업이 실패할 수 있다.
이는 foreign_key_checks 옵션으로 일시적으로 비활성화할 수 있지만 데이터를 적재한 후 일관성을 맞춰준 후 다시 활성화해주어야 한다.
foreign_key_checks가 비활성화되면 외래키 관계의 부모 테이블에 대한 작업(ON DELETE CASCADE와 ON UPDATE CASCADE)도 무시하게 된다.
4.2.3 MVCC(Multi Version Concurrency Control
레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능으로 버전 관리를 통해 잠금을 사용하지 않는 일관된 읽기가 가능하다. InnoDB는 이를 Undo log를 이용하여 구현하고 있다.
하나의 레코드에 대해 2개의 버전이 유지되고, 필요에 따라 어느 데이터가 보여지는지 여러 가지 상황에 따라 달라지는 구조이다.
→ 책 보면서 예제를 정확히 이해해보자.
4.2.4 잠금 없는 일관된 읽기(Non-Locking Consistent Read)
InnoDB 스토리지 엔진은 MVCC 기술을 이용하여 잠금을 걸지 않고 읽기 작업을 수행한다. (격리 수준 SERIALIZABLE 제외). 레코드에 대해 변경 작업이 이루어지더라도 다른 트랜잭션의 SELECT 작업을 방해하지 않는다. 이를 잠금 없는 일관된 읽기라고 한다.
격리 수준이 READ_UNCOMMITED라면 InnoDB의 버퍼 풀 데이터를 읽고 READ_COMMITED, REPEATABLE_READ 라면 언두 로그 데이터를 읽게 된다.
오랜 시간 동안 활성 상태인 트랜잭션으로 인해 MySQL 서버가 느려지거나 문제가 발생할 때가 가끔 있는데, 바로 이러한 일관된 읽기를 위해 언두 로그를 삭제하지 못하고 계속 유지해야 하기 때문에 발생하는 문제이다.
따라서 트랜잭션이 시작됐다면 가능한 한 빨리 롤백이나 커밋을 통해 트랜잭션을 완료하는 것이 좋다.
4.2.5 자동 데드락 감지
InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 목록을 그래프(Wait-for List) 형태로 관리한다.
데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사하여 교착 상태에 빠진 트랜잭션을 강제 종료한다.
트랜잭션 중 언두 로그의 양이 가장 작은 트랜잭션이 롤백을 실행해도 서버의 부하를 덜 일으키기 때문에 언두 로그 양으로 종료 대상을 선정한다.
InnoDB 스토리지 엔진은 상위 레이어인 MySQL 엔진의 테이블 잠금을 볼 수 없어 데드락 감지가 불확실할 수도 있다.
innodb_tagle_locks 시스템 변수를 활성화하면, InnoDB 스토리지 엔진 내부의 레코드 잠금뿐만 아니라 테이블 레벨의 잠금까지 감지할 수 있다.
동시 처리 스레드가 많은 경우 데드락 감지 스레드가 잠금 목록에 잠금을 걸면서 많은 CPU 자원을 소모할 수 있다.
이를 innodb_deadlock_detect으로 데드락 감지를 비활성화한 후 innodb_lock_wait_timeout으로 잠금 타임아웃 시간을 작게 설정하는 방법으로 해결할 수 있다.
높은 동시성 처리를 요구하는 서비스라면 이 방법을 고려해보자.
4.2.6 자동화된 장애 복구
MySQL이 장애로 인해 예기치 않게 종료되었을 경우, innodb_force_recovery 시스템 변수를 1~6으로 설정하여 MySQL 서버를 다시 시작할 수 있다.
MySQL 서버를 재시작한 후에는 mysqldump 프로그램이나 SELECT INTO OUTFILE… 명령어를 이용하여 덤프 후 데이터베이스를 다시 구축하는 것이 좋다.
innodb_force_recovery 시스템 변수
4.2.7 InnoDB 버퍼 풀
InnoDB 스토리지 엔진에서 가장 핵심적인 부분으로, 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간이다. 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 한다.
일반적인 애플리케이션에서는 INSERT, UPDATE, DELETE 처럼 데이터를 변경하는 쿼리는 데이터 파일의 이곳저곳에 위치한 레코드를 변경하기 때문에 랜덤한 디스크 작업을 발생시킨다. but, 버퍼 풀이 변경된 데이터를 모아서 처리하면 랜덤한 디스크 작업의 횟수를 줄일 수 있다.
버퍼 풀의 크기 설정
MySQL 서버 내에서 메모리를 필요로 하는 부분은 크게 없지만 아주 독특한 경우 레코드 버퍼가 상당한 메모리를 사용하기도 한다. 커넥션과 사용하는 테이블이 많다면 레코드 버퍼 용도의 메모리 공간이 많이 필요해질 수 있다.
•
레코드 버퍼 : 각 클라이언트 세션에서 테이블의 레코드를 읽고 쓸 때 버퍼로 사용하는 공간.
InnoDB 버퍼 풀은 innodb_buffer_pool_size 시스템 변수로 설정할 수 있으며, 동적으로 크기를 변경할 수 있다.
운영체제의 전체 메모리 공간의 50%정도만 버퍼풀로 설정하고 조금씩 올려가면서 최적점을 찾는 것이 좋다.
innodb_buffer_pool_instances 변수를 통해 버퍼 풀의 개수를 설정할 수 있으며 디폴트 값은 8이다.
버퍼 풀 인스턴스당 5GB 메모리를 사용할 수 있도록 설정하는 것이 좋다.
버퍼 풀의 구조
버퍼 풀이라는 거대한 메모리 공간을 페이지 크기(innodb_page_size 시스템 변수에 설정된)의 조각을 쪼개어 스토리지 엔진이 데이터를 필요로 할 때 해당 데이터 페이지를 읽어서 각 조각에 저장
버퍼 풀의 페이지 크기 조각을 관리하기 위해 LRU(Least Recently Used), Flush, Free 리스트 자료구조를 사용한다.
InnoDB 스토리지 엔진에서 데이터를 찾는 과정
•
어댑티브 해시 인덱스와 B-Tree 인덱스를 이용하여 데이터 페이지가 버퍼 풀에 있는지 검사
◦
없다면 디스크에서 필요한 데이터 페이지를 버퍼 풀에 적재, 해당 페이지에 대한 포인터를 LRU 헤더에 추가. 데이터 페이지가 실제로 읽혔을 경우 MRU 방향으로 승급
◦
있다면 해당 페이지 포인터를 MRU 방향으로 승급
•
필요한 데이터가 자주 접근되었다면 어댑티브 해시 인덱스에 추가
LRU 리스트
디스크로부터 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼풀에 메모리에 유지하여 디스크 읽기를 최소화하기 위해 사용.
거의 사용되지 않는 데이터 페이지는 LRU의 끝으로 밀려나 버퍼 풀에서 제거된다.
Flush 리스트
디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지) 목록 관리
Free 리스트
InnoDB 버퍼 풀에서 실제 사용자 데이터로 채워지지 않은 비어 있는 페이지들의 목록
버퍼 풀과 리두 로그
버퍼 풀은 데이터베이스 서버의 성능 향상을 위해 데이터 캐시와 쓰기 버퍼링의 기능을 제공하는데, 버퍼 풀의 메모리 공간을 늘리는 것은 이 데이터 캐시 기능을 향상시키는 것을 의미한다.
쓰기 버퍼링의 성능 향상을 위해서 버퍼 풀의 메모리 공간과 함께 리두 파일의 크기도 고려해야 한다.
버퍼 풀은 디스크에서 읽은 상태에서 전혀 변경되지 않은 클린 페이지(Clean Page)와, INSERT 등의 명령으로 변경된 더티 페이지(Dirty Page)를 가지고 있다.
더티 페이지가 버퍼 풀에 무한정 머무를 수 없기 때문에 InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발생시켜 변경된 데이터를 디스크로 동기화 시킨다.
더티 페이지는 특정 리두 로그 엔트리와 관계를 가지고 순환 고리처럼 사용한다.
전체 리두 로그 파일에서 아직 디스크로 동기화되지 않은 공간은 재사용이 불가능하며, 이를 활성 리두 로그 공간이라고 한다.
InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발생시켜 리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화하는데, 가장 최근 체크포인트의 LSN(Long Sequence Number)이 활성 리두 로그 공간의 시작점이 된다.
가장 최근 체크 포인트의 LSN과 마지막 리두 로그 엔트리의 LSN 차이를 Checkpoint Age라고 하며, 활성 리두 로그 공간의 크기를 일컫는다
버퍼 풀 플러시(Buffer Pool Flush)
버퍼 풀에서 아직 디스크로 기록되지 않은 더티 페이지들을 성능상의 악영향 없이 디스크에 동기화하기 위해 다음과 같이 2개의 플러시 기능을 백그라운드로 실행한다.
•
플러시 리스트 플러시
•
LRU 리스트 플러시
플러시 리스트 플러시
리두 로그 공간의 재활용을 위해 오래된 리두 로그 엔트리의 사용 공간을 비우는 작업.
이 때 오래된 리두 로그 공간이 지워지려면 반드시 InnoDB 버퍼 풀의 더티 페이지가 먼저 디스크로 동기화되어야 한다. 이를 위해 InnoDB는 주기적으로 플러시 리스트(Flush_list) 플러시 함수를 호출해서 플러시 리스트에서 오래전에 변경된 데이터 페이지 순서대로 디스크에 동기화하는 작업을 수행한다. 이를 수행하는 스레드를 클리너 스레드(Cleaner Thread)라고 한다.
더티 페이지가 많다면 디스크 쓰기 폭발 현상이 발생할 수 있는데, 이를 위해 InnoDB 엔진은 어댑티브 플러시(Adaptive flush)라는 기능을 제공한다.
이는 리두 로그의 증가 속도를 분석하여 적절한 수준의 더티 페이지가 버퍼 풀에 유지될 수 있도록 디스크 쓰기를 실행해 준다.
LRU 리스트 플러시
LRU 리스트에서 사용 빈도가 낮은 데이터 페이지들을 제거하는 작업. 이를 위해 LRU 리스트 플러시 함수가 사용된다.
InnoDB 스토리지 엔진은 InnoDB 버퍼 풀 인스턴스별로 최대 innodb_lru_scan_depth 개수만큼 스캔하기 때문에, 실질적으로 LRU 리스트의 스캔은
(innodb_buffer_pool_instances * innodb_lru_scan_depth) 수만큼 수행한다.
버퍼 풀 상태 백업 및 복구
디스크의 데이터가 버퍼 풀에 적재되어 있는 상태를 워밍업이라고 표현하는데, 이는 몇십 배의 쿼리 처리 속도 차이를 보인다.
이를 위해 innodb_buffer_pool_dump_now를 사용하여 버퍼 풀의 상태를 백업하고 복구할 수 있다.
버퍼 풀의 적재 내용 확인
information_schema의 innodb_cached_indexes 테이블을 통해 인덱스별로 데이터 페이지가 얼마나 InnoDB 버퍼 풀에 적재되어 있는지 확인할 수 있다.
4.2.8 Double Write Buffer
리두 로그는 공간 낭비를 막기 위해 페이지의 변경된 내용만 기록한다.
이로 인해 플러시에 문제가 발생하여 일부만 기록되면 해당 페이지의 내용은 복구할 수 없을 수 있다.
이를 해결하기 위한 방법으로 Double-Write 기법을 이용한다.
더티페이지를 데이터 파일에 기록하기 전에 더티페이지를 묶어 시스템 테이블스페이스의 DoubleWrite 버퍼에 기록한다.
해당 기능은 innodb_doublewrite 시스템 변수로 사용 여부를 결정할 수 있다.
4.2.9 언두 로그
InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML로 변경되기 이전 버전의 데이터를 별도로 백업한다.
이렇게 백업된 데이터를 언두 로그(Undo Log)라고 한다.
언두 로그의 기능
•
트랜잭션 보장
◦
트랜잭션의 롤백시, 데이터 복구 과정에서 언두 로그에 백업한 데이터를 이용한다.
언두 로그 레코드 모니터링
대용량의 데이터를 처리하거나 오랜 시간 실행되는 트랜잭션으로 인해 언두 로그의 양이 증가할 수 있다.