MySQL InnoDB의 동작 - 실제 장애 상황에서 InnoDB는 어떻게 동작할까? Durability
이 한 문장으로 전체 핵심을 말할 수 있을 듯 하다.
Transaction commit은, 변경사항이 즉시 데이터 파일까지 flush 되었다는 의미가 아니다.
Durability
Durability = Commit된 트랜잭션은 장애가 발생해도 반드시 살아남아야 한다는 것이다.
즉 Commit 성공은, 절대 Disk 등 저장소에 모두 반영되었다는 의미가 아니다. 문장 의미가 혼동 될 수 있다. 데이터 반영 = 살아남는 것이라고 생각할 수 있지만, 실제 MySQL에서는 Log 기반 recovery 매커니즘으로 D를 보장한다.
Recovery 매커니즘을 이해하기 전에, 이러한 Log 역할을 하는 Redo Log 및 Checkpoint에 관해 이해해야 한다.
Redo Log
데이터 변경 내용을 기록한 로그이며, MySQL 서버가 비정상적으로 종료(크래시 등) 되었을때 데이터 파일에 기록되지 못한 변경사항을 보관하는 로그이다.
Redo Log의 동작을 설정하는 innodb_flush_log_at_trx_commit 변수의 값에 따라 아래와 같은 경우로 동작이 나뉜다.
-
innodb_flush_log_at_trx_commit = 1(default) : 매번 트랜잭션 커밋마다 Redo Log Buffer의 내용을 OS buffer cache에 write 및 disk에 flush한다.
-
innodb_flush_log_at_trx_commit = 0 : 1초에 한번씩 Redo Log Buffer의 내용이 OS buffer cache에 write 및 disk에 flush된다. 따라서 최대 1초 동안의 트랜잭션 내용이 소실될 수 있다.
-
innodb_flush_log_at_trx_commit = 2 : 트랜잭션 커밋마다 Redo Log Buffer에 기록 및 OS buffer cache에 내용을 write한다. OS buffer cache에서는 1초에 한번마다 Redo Log 디스크 영역에 flush된다. 따라서 최대 1초 동안의 트랜잭션 내용이 소실될 수 있다.
Redo Log Buffer / OS buffer cache / Disk area를 잘 구분하도록 하자.
MySQL이 죽어도, OS가 살아있으면 데이터가 남아있을 수 있다(OS buffer cache에 기록되어있다는 가정). 그러나 전원이 나가면 RAM에 있던 데이터는 증발하므로 OS buffer cache에 적힌 데이터 또한 증발한다.
Checkpoint
DB 비정상 종료 후 복구 과정에서, Log에 기록된 변경 사항을 적용할 수 있는 시작점을 의미한다.
Log에 기록된 모든 변경사항을 매 복구과정마다 처음부터 끝까지 적용할 수는 없다. 따라서 Checkpoint를 복구의 시작점으로 사용한다. Checkpoint 이전 변경은 DB에 이미 disk에 반영되었다고 보기 때문이다.
Checkpoint는 특정 파일 형태로 존재하지는 않고, Redo Log 쪽에 복구의 시작점으로 기록된다.
이제 Redo Log와 Checkpoint에 대해 이해하였다. 본격적으로 MySQL의 recovery 매커니즘을 분석해보자.
구체적으로는 아래와 같은 과정으로 이루어진다:
- MySQL의 crash recovery 과정 : Tablespace Discovery -> Redo Log Application -> Rollback of Incomplete Transaction
1. Tablespace Discovery
Redo log 안의 tablespace ID가 가리키는 실제 파일 경로를 찾아 연결하는 과정이다. 그냥 Redo Log를 열어서 보면, 어떤 테이블이 실제 어느 물리적인 위치에 있는지를 모른다. 따라서 이러한 과정이 필요하다.
MySQL이 여는 파일의 종류는 아래와 같다.
1) Redo Log
Redo Log의 기본 저장 경로는 ${datadir}/#innodb_redo/이며, 따로 innodb_log_group_home_dir 변수를 설정하면 저장 경로는 ${innodb_log_group_home_dir}/#innodb_redo/로 바뀐다.
기본 ${datadir}는 var/lib/mysql이다. 어디서 많이 보지 않았나? yaml 파일 작성할때 volumes로 많이 쓰는 디렉토리이다.
경로 이름을 일일이 다 외울 필요는 없고, ‘MySQL은 Redo Log가 저장된 디렉토리를 알고 그것을 바꿀수도 있다’ 정도만 알면 될 듯 하다.
어쨌든 MySQL은 이렇게 Redo Log를 열어볼 수 있다.
2) Undo Tablespace
Undo Log를 저장하는 공간이다. Redo Log 만으로는 데이터 복구가 완전하지 않고, 이후 3단계(Undo phase)에서 Undo Log를 사용해야 하기에 해당 Log 파일도 필요하다.
마찬가지로, 기본 저장 경로는 ${datadir}/ 내부에 undo_001과 같이 생성되며, innodb_undo_directory 변수를 지정한다면 ${innodb_undo_directory}/ 내부에 undo_002와 같은 방식으로 생성된다.
마찬가지로 경로 이름을 일일이 다 외울 필요는 없고, 위의 Redo Log와 같은 방식으로 이해하면 될 듯 하다.
이제 Redo Log 및 Undo Log를 전부 확보했다. 그러나 아직 복구 대상 테이블들의 실제 물리적인 위치를 모른다. MySQL은 이를 start up 단계에서 미리 전부 discover 하며, 단 하나라도 참조 대상 tablespace file이 미리 발견되지 않으면 crash recovery는 종료된다.
3) Tablespace
이제 본격적으로 Redo가 어떤 tablespace에 적용될지 찾아야한다.
탐색할 디렉터리 목록은 innodb_directories 변수에 정의되며, 초기에는 NULL이지만 recovery 과정이 시작되면 앞선 datadir / innodb_undo_directory(Undo Log 기본 경로) 등이 탐색을 위해 자동으로 포함된다.
이렇게 탐색을 시작하여, Redo Log가 참조하는 모든 tablespace file이 탐색될 때까지 진행한다.
이제 모든 준비가 다 끝났다. Redo / Undo Log 및 접근 대상 tablespace file의 존재도 모두 확인했다. 이제 남은 것은 Log를 읽고 복구하는 것이다.
2. Redo Log Application(Redo Phase)
Redo Log를 이용하여, DB를 크래시 직전 상태로 복구하는 것이다.
이때 복구 시작점은 아까 말했던 Checkpoint의 기록부터 진행하며, Checkpoint가 가리키는 LSN(Log Sequence Number - Redo Log 상의 증가하는 위치 번호) 이후부터 Redo Log를 읽으며 Redo를 수행한다.
이 과정을 수행하고 나면, Checkpoint와 크래시 지점 사이에 이미 종료된 트랜잭션과 크래시 도중 실행되던 트랜잭션의 변경사항이 반영된 상황이다. 이제 우리는 크래시 도중 실행되던 트랜잭션의 변경 사항을 롤백해야한다.
3. Rollback of Incomplete Transactions(Undo Phase)
크래시 시점에 미완료된 트랜잭션을 롤백하는 작업이다. 이는 Undo Log를 이용해 수행되며, 크래시 도중 실행되던 트랜잭션들이 만든 변경사항에 대해서만 Undo가 이루어진다.
결과적으로, 미완료된 트랜잭션을 제외하고, 크래시 직전까지 완전히 종료된 트랜잭션들이 수행한 변경사항만 반영하는데 성공하였다.
앞선 논의들로, 논리적인 복구 로직에 대해 정리해보았다.
그러나, Dirty Page를 디스크로 flush할 때 전부 반영되지 못하고, 일부만 반영되고 장애가 발생한다면 : 즉, 복구 로직을 수행한 후 디스크로 flush하던 도중 변경사항의 일부만 반영되고 나머지는 반영되지 못한채 종료된다면(하드웨어 오작동 등으로 발생 가능) 데이터 정합성에 오류가 발생할 수 있다.
그래서, 논리적인 복구 로직을 실제로 지원하기 위한 물리적 동작이 존재하며, MySQL에서는 이를 Double Write Buffer를 통해 지원한다.
Double Write Buffer
변경 사항이 반영된 더티 페이지들을 실제 Disk에 쓰기 전에 미리 기록해두는 별도의 공간이다.
우선 더티 페이지들을 Double Write Buffer에 기록하고, 그 이후에 각 더티 페이지들을 반영하기 시작한다.
Double Write Buffer가 존재하면, flush 도중 비정상 종료가 발생하여도, 불완전한 페이지(Partial-Page 또는 Torn-page)를 감지한 후 Double Write Buffer의 내용(정상 내용)을 통해 복구할 수 있다.
즉, Double Write Buffer는 Durability를 보장할 수 있도록 해주는 물리적 안전장치인 것이다.
(추가로, 꼭 복구 로직에만 사용되는 것이 아니라 평소에 Dirty Page를 flush 할때마다 동작하는 안전장치이다)
이 또한 시스템 변수(innodb_doublewrite)로 제어할 수 있다.
이렇게 MySQL에서 Durability를 보장하는 논리적 로직 / 물리적 로직을 정리해보았다. 단순히 알고만 있었던 redo / undo log가 어떻게 durability 보장에 반영되는지에 대해 알게 되었으며, “바로 저장”이 아니라 “장애 후에도 복구 가능”에 초점을 맞추었다는 점이 인상적이었다.