InnoDB 스토리지 엔진은 ACID 원칙을 지키기 위해 다양한 기능을 제공한다. 여기서는 그 중에서도 언두로그와 리두로그에 대해 알아본다.
InnoDB 스토리지 엔진이란?
MySQL 서버는 사람의 머리 역할을 담당하는 MySQL 엔진과 손발 역할을 담당하는 스토리지 엔진으로 나눌 수 있다. MySQL 엔진과 달리 스토리지 엔진은 핸들러 API만 만족하면 직접 구현하여 사용하는 것도 가능하다. InnoDB는 MySQL 서버에서 기본적으로 제공하는 스토리지 엔진이다.
ACID 원칙이란?
ACID 원칙은 데이터베이스에서 트랜잭션이 안정적으로 동작하도록 보장하기 위한 원칙이다. 각 철자는 다음의 의미를 가진다.
원자성(Atomicity): 트랜잭션의 변경사항이 데이터베이스에 모두 반영되거나 모두 반영되지 않아야 한다.
일관성(Consistency): 트랜잭션이 실행되기 전과 후에 데이터베이스가 일관된 상태를 유지해야 한다.
독립성(Isolation): 동시에 실행되는 트랜잭션이 서로 영향을 주지 않아야 한다.
지속성(Durability): 완료된 트랜잭션의 결과는 영구적으로 반영되어야 한다.
InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML(INSERT
, UPDATE
, DELETE
)로 변경되기 이전의 데이터를 백업해두는데, 이를 언두 로그라 한다.
언두 로그가 무엇인지 어느 정도 감이 오는 것 같다. 이번에는 실제로 어떻게 동작하는지 간단하게 알아보자. 데이터베이스에서 현재 구조 설명에 필요한 부분만 시각화해보면 다음 그림과 같이 표현할 수 있다.
데이터베이스에는 현재 12 / 홍길동 / 서울
데이터가 존재한다. 여기서 아래 쿼리가 실행되면 메모리 공간은 어떻게 될까?
UPDATE member SET m_area='경기' WHERE m_id=12;
트랜잭션의 관점에서 보면 COMIMT이 될 때 비로소 데이터가 영속화되니 버퍼 풀이나 데이터 파일에는 COMMIT 시점에 변경 내용이 반영될 것 같지만 아니다.
실제로는 UPDATE 쿼리가 들어온 순간 버퍼 풀은 바로 변경 내용을 반영하고, 이와 함께 언두 로그에 변경 전 데이터를 임시로 저장한다. 데이터 파일은 어떻게 될까? 우리는 데이터 파일에 변경 전 내용이 들어있는지 변경 후 내용이 들어있는지 정확히 알 수 없다(이건 마치 슈뢰딩거의 데이터베이스...). 버퍼 풀의 변경 내용은 백그라운드 스레드에 의해 데이터 파일에 물리적으로 기록되기 때문에 정확히 언제 데이터 파일에 기록되는지는 모르는 것이다.
의외로 데이터 파일에 변경 내용이 반영되었는지는 여부는 우리에게 중요하지 않다. 이 이유는 언두 로그가 사용되는 과정을 보면 알 수 있다.
언두로그는 트랜잭션 롤백에 대응하기 위해 사용된다. 트랜잭션이 롤백되면 해당 트랜잭션에서 변경된 내용이 복구되어야 하는데, 그 정보가 언두 로그에 기록되어있기 때문에 언두 로그의 데이터를 버퍼 풀과 데이터 파일에 다시 반영하여 롤백을 수행할 수 있다.
언두 로그를 이용하면 격리 수준도 보장할 수 있다. 어떤 트랜잭션에서 데이터를 변경하고 아직 COMMIT하지 않았다고 가정해보자. 그리고 다른 트랜잭션에서 여기에 접근하려고 했을 때 격리 수준이 READ_UNCOMMITTED
라면 버퍼 풀에 있는 COMMIT되기 전의 데이터를 그대로 반환한다. 하지만 READ_COMMITTED
나 그 이상의 격리 수준이라면 어떻게 될까? 이 때는 커밋이 완료된 데이터만을 가져와야 하는데, 버퍼 풀이나 데이터 파일에 있는 내용 대신 이전 데이터인 언두 로그의 데이터를 반환한다. 이렇게 되면 커밋된 데이터만을 반환할 수 있게 되는 것이다.
더 자세히 알아보고 싶다면 MVCC(Multi-Version Concurrency Control)에 대해 학습해보자.
언두 로그는 서버가 비정상적으로 종료(Crash)된 경우에도 사용된다.
위의 두 상황은 모두 데이터를 되돌려야 하는데, 이 때 언두 로그가 사용된다. 이 상황에서 서버가 재시작되면 InnoDB 스토리지 엔진은 변경되기 전의 데이터를 언두 로그에서 가져와 데이터 파일에 복사함으로써 복구를 진행한다.
언두 로그는 언제 소멸할까? 기본적으로는 변경 내용을 만들어낸 트랜잭션이 COMMIT되면 그 데이터를 영속화하며 언두 로그를 제거한다. 하지만 트랜잭션이 COMMIT되더라도 언두 로그가 필요하다면 제거되지 않고 남아있다. COMMIT된 데이터의 언두 로그가 필요한 상황은 언제일까?
다음 그림을 보자.
트랜잭션 A가 가장 먼저 시작하고 이어서 B와 C가 각각 데이터를 수정한 후 COMMIT했다. 만약 격리 수준이 REPEATABLE READ
이상이라면 어떻게 될까? 트랜잭션은 항상 동일한 결과를 반환해야 하기 때문에 처음 SELECT와 B, C의 커밋 후 SELECT 시 동일한 결과가 반환되어야 한다. 다른 트랜잭션의 COMMIT된 변경사항을 무시해야 한다는 뜻이다. 이 때를 위해 InnoDB는 B, C가 COMMIT되더라도 당장 언두 로그를 제거하지 않고 A 트랜잭션이 종료될 때까지 유지한다.
위에서 언두 로그는 MySQL 서버의 비정상 종료 시 복구를 위해 사용되는 중요한 정보라는 것을 알았다. 언두 로그는 메모리 상에 존재할텐데 서버가 갑자기 종료되도 남아있으려면 디스크에 동기화가 필요하지 않을까? 그래서 MySQL 5.6 이전 버전에서는 언두 로그가 항상 시스템 테이블스페이스(ibdata.ibd
)에 기록되었다. 하지만 이는 성능이나 관리 측면에서 좋지 못했다(다른 데이터와 함께 저장되었기에 Race가 일어나 성능이 떨어지고, 고정 크기로 관리되어 용량의 확장/축소가 어려움). 때문에 MySQL 8.0.14 버전 이후부터는 성능 최적화를 위해 언두 테이블스페이스라는 외부(디스크)의 별도 로그 파일에 기록되도록 변경되었다. 이를 통해 시스템 테이블스페이스와 언두 로그가 분리되어 성능 최적화와 관리의 유연함을 제공할 수 있게 되었다.
언두 로그는 ACID 원칙 중 Atomicity(원자성)을 지키기 위해 활용된다. 데이터베이스는 작업 단위(트랜잭션)가 실패하거나 취소될 경우 모두 한 번에 되돌릴 수 있어야 한다. 언두 로그는 트랜잭션이 중간에 실패하거나 취소될 경우, 해당 트랜잭션이 변경한 데이터를 이전 상태로 되돌리는 역할을 한다. 이를 통해 트랜잭션의 원자성을 보장할 수 있다.
레코드가 변경될 때 변경 내용을 버퍼 풀과 실제 데이터 파일에 적용하며 이전 버전을 언두 로그에 기록한다고 했다. 만약 이 과정에서 인덱스의 변경이 필요해지면 어떻게 될까? 실제로 INSERT
나 UPDATE
구문이 실행될 경우에는 해당 테이블 인덱스의 업데이트가 필요해지게 되는데, 이 작업은 랜덤 디스크 읽기가 필요하여 비용이 크다. 이 때 MySQL에서는 인덱스 업데이트를 즉시 수행하지 않고 임시 공간에 기록 후 바로 사용자에게 결과를 반환함으로써 성능을 개선했는데, 이 임시 메모리 공간을 체인지 버퍼(Change Buffer)라고 한다. 그리고 여기에 저장된 인덱스 정보는 이후 백그라운드 스레드에 의해 병합되는데, 이 스레드를 체인지 버퍼 머지 스레드(Merge thread)라고 한다.
버퍼 풀은 거대한 메모리 공간을 수많은 페이지로 쪼개두고 데이터가 필요할 때 해당 데이터 페이지를 읽어와 버퍼 풀 속 페이지에 저장한다. 이 페이지들은 최초에는 클린 페이지이지만 데이터가 변경되면서 더티 페이지가 되기도 한다.
클린 페이지(Clean Page): 디스크에서 버퍼 풀로 가져온 상태로 변경되지 않은 페이지
더티 페이지(Dirty Page): 디스크에서 버퍼 풀로 가져온 후 변경된 페이지
더티 페이지를 버퍼 풀에 계속 남겨두면 낭비되는 메모리가 점점 심해질 것이다. 더티 페이지는 디스크와 상태가 다르기 때문에 언젠가 반드시 디스크로 기록되어야 한다. 하지만 디스크 쓰기를 매번 바로 수행하면 성능에 악영향을 미칠 수 있기 때문에 더티 페이지를 모아두고 백그로운드 스레드에 의해 기록된다. 그리고 더티 페이지를 모아두는 공간을 바로 리두 로그라고 한다.
리두 로그는 변경된 정보(더티 페이지의 변경 내용)의 모음이다. 다음은 버퍼 풀과 리두 로그 사이의 관계를 표현한 그림이다.
리두 로그는 1개 이상의 파일을 연결해서 순환시켜 사용한다. 이 중에서 사용중인(재사용 불가능한) 공간을 활성 리두 로그(Active Redo Log)라고 한다. 리두 로그 파일은 순환되어 재사용되지만 로그 포지션은 계속 증가하는데, 이 값을 LSN(Log Sequence Number)라고 한다. InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발생시키는데, 이 때 리두 로그와 버퍼 풀의 더티 페이지가 디스크로 동기화된다. 그리고 가장 최근에 종료된 체크포인트의 LSN이 활성 리두 로그의 시작점이 된다. 다만 활성 리두 로그의 마지막 부분은 더티 페이지가 쌓이며 계속 이동하기 때문에 체크포인트와 무관하다.
정리하면 LSN 이전의 리두 로그는 비활성 리두 로그이고 이미 디스크와 동기화가 완료되어 있는 상태다. 단, 리두 로그가 순환 구조라는 부분은 감안해야 한다.
더티 페이지를 디스크에 동기화하는 것을 더티 페이지 플러시라고 하는데, 이를 성능 상 악영향을 최소화하며 진행하기 위해 백그라운드에서 플러시 리스트(Flush_list) 플러시 또는 LRU 리스트(LRU_list) 플러시를 수행한다. 여기에 대해 궁금하다면 별도로 찾아보자.
언두 로그는 언두 테이블스페이스에 기록되지만 리두 로그는 별도의 리두 로그 파일에 저장된다. 리두 로그는 빠른 I/O 성능이 중요하기 때문에 별도의 연속적인 파일로 분리하여 리두 로그의 기록과 복구를 빠르게 진행할 수 있도록 설계되었다. 반면 언두 로그는 I/O 성능보다는 데이터 무결성이 더 중요하기 때문에 테이블스페이스로 설계되었다고 한다. 그래도 별도의 테이블 스페이스로 분리해내는 노력은 했잖아 한잔해
리두 로그를 사용하는 주요 목적은 서버가 비정상적으로 종료(Crash)된 경우에도 COMMIT된 데이터를 지키기 위해서이다.
언두 로그는 트랜잭션이 COMMIT되더라도 변경사항이 즉시 디스크에 쓰이지 않고 백그라운드 스레드에 의해 기록된다고 했다. 그럼 트랜잭션이 COMMIT되었으나 아직 디스크에 기록되지 않은 상황에 서버가 비정상적으로 종료되면 어떻게 될까? 서버가 다시 실행되면 버퍼 풀이 초기화되어버려서 더티 페이지가 더이상 남아있지 않다. 하지만 리두 로그에는 COMMIT하기까지 실행한 모든 변경내용이 들어있기 때문에 리두 로그에서 디스크에 반영되지 않은 부분부터 디스크 쓰기를 마저 진행한다.
서버 시작 시 이전 버퍼 풀 데이터를 가져오는 설정을 켜두면 복구 과정이 달라지나요?
MySQL에서
innodb_buffer_pool_restore_at_startup
시스템 변수를 활성화해두면 서버 재시작 시 자동으로 버퍼 풀 상태를 복구해준다. 그럼 여기에 더티 페이지도 있을테니 복구 과정을 조금이라도 최적화할 수 있지 않을까?아쉽게도 이건 불가능하다. 버퍼 풀을 자동으로 저장하고 복구하는 이 설정은 주기적인 디스크 쓰기를 통해 이루어지는데, 이 때 클린 페이지만 기록하기 때문에 더티 페이지는 복구되지 않는다. 이 설정은 그저 서버 재시작 시 버퍼 풀에 데이터를 미리 가져와둠으로써 응답속도를 개선하기 위한 최적화 설정일 뿐이다.
앞에서는 트랜잭션이 COMMIT되었으나 아직 디스크에 기록되지 않은 상황에 서버가 비정상적으로 종료된 경우에 대해 알아보았다. 그럼 만약 디스크에 기록중인 상황에 서버가 비정상적으로 종료되버리면 어떻게 될까?
위 상황에서는 리두 로그에 있는 일부 더티 페이지만 디스크에 기록되고 나머지는 기록되지 않은 상태가 될 것이고 이 데이터는 복구가 불가능할 수 있다. 이렇게 일부 페이지만 기록되는 현상을 파셜 페이지(Partial-page)나 톤 페이지(Torn-page)라고 한다.
이 상황은 리두 로그 선에서 복구가 불가능하기 때문에 InnoDB 스토리지 엔진에서는 Double-Write 기법을 이용한다. 다음 그림은 Double-Write 기법의 작동 방식에 대해 표현한 것이다.
버퍼 풀에 더티 페이지가 생기면 InnoDB 스토리지 엔진은 우선적으로 DoubleWrite 버퍼에 더티 페이지를 기록하고, 이 데이터는 시스템 테이블스페이스에 저장된다. 그 이후에 실제 데이터 파일(디스크)에 더티 페이지를 기록한다. (참고로 DoubleWrite 버퍼는 디스크의 실제 데이터 파일과 달리 순차적으로 기록 및 접근하기 때문에 비용이 상대적으로 작다.)
만약 실제 디스크에 쓰기를 하던 중 서버가 비정상적으로 종료되어 일부 더티 페이지가 기록되지 않았다면 어떻게 될까? InnoDB 스토리지 엔진은 재시작 시마다 DoubleWrite 버퍼와 데이터 파일들을 모두 비교하여 서로 다른 페이지가 있다면 DoubleWrite 버퍼의 내용을 데이터 파일로 복사한다.
디스크에 쓰기중이던 중에 비정상적으로 서버가 종료되더라도 이 기법을 통해 InnoDB 스토리지 엔진은 안정적으로 데이터를 복구할 수 있다.
앞서 언두 로그는 트랜잭션이 ROLLBACK되었지만 디스크 동기화 중에 서버가 종료된 경우
데이터 동기화를 위해 사용된다고 설명했다. 하지만 이 기능은 언두 로그 혼자만으로는 수행이 불가능하고, 리두 로그의 도움이 반드시 필요하다.
사실 리두 로그는 변경된 데이터뿐만 아니라 트랜잭션 진행 상태도 함께 관리한다. 트랜잭션이 커밋되었는지 롤백되었는지 진행중이었는지 상태를 기록한다. 위 상황에서는 서버 재시작 시 트랜잭션이 ROLLBACK되었다
는 것을 리두 로그를 통해 확인한다. 그럼 해당 트랜잭션에 대한 언두 로그의 데이터를 기반으로 디스크의 변경된 데이터를 복구한다.
리두 로그에서 그 변경사항이 커밋되었는지 롤백되었는지 실행중이었는지의 정보를 가지기 때문에 이 정보를 기반으로 복구 여부를 결정하는 것이다.
리두 로그는 ACID 원칙 중 Durability(지속성)을 지키기 위해 활용된다. 데이터베이스는 완료된 트랜잭션이 변경한 내용에 대해 영구적으로 반영되어야 한다. 트랜잭션이 커밋된 이후 데이터파일에 반영되기 전 시스템에 장애가 발생하더라도 리두 로그의 변경 내용을 데이터파일에 적용하여 해당 내용을 복구할 수 있다.
서버가 비정상적으로 종료하면 리두 로그를 통해 데이터 파일을 복구한다고 했다. 이를 위해 리두 로그는 메모리가 아니라 데이터 파일로써 저장되어야 하고, 실제로 시스템 테이블스페이스에 기록된다. 하지만 당연히 디스크 쓰기작업이기 때문에 트랜잭션이 커밋될 때마다 리두 로그를 디스크에 쓰는 것은 성능에 부담이 될 수 있다. 이를 개선하기 위해 InnoDB 스토리지 엔진은 버퍼 풀이나 리두 로그를 버퍼링할 수 있는 로그 버퍼를 가지고 있다. 로그 버퍼에 담긴 내용을 디스크(리두 로그 파일)에 저장하는 기준은 우리가 직접 시스템 변수를 통해 제어할 수 있다.
자세한 내용이 궁금하다면 innodb_flush_log_at_trx_commit
시스템 변수에 대해 알아보자.
언두 로그는 변경되기 이전 데이터를 관리하고, 리두 로그는 변경된 데이터를 관리하여 데이터베이스의 안정성을 보장한다.
언두 로그와 리두 로그는 트랜잭션 롤백이나 서버 비정상 종료 시 복구에 반드시 필요한 민감한 정보이기 때문에 버퍼 풀에 변경 내용이 기록되기 전에 해당 로그에 우선적으로 기록된다. 다만 두 로그는 메모리상에만 존재하고, 각각은 백그라운드 스레드에 의해 디스크(언두 테이블스페이스, 리두 로그 파일)로 영속화된다.
언두 로그와 리두 로그는 MySQL 서버가 비정상적으로 종료된 경우 데이터를 복구하는 데 중요한 역할을 한다. 정리하면 다음과 같다.
MySQL 스터디를 시작하면서 Real MySQL 8.0 책을 읽기 시작하고 2주에 한 번씩 블로깅을 하기로 했다. 처음에는 분명 간단하게 정리하고 넘기려고 했는데 공부하다 보니 점점 궁금한 게 많아져서 이만큼이나 길어졌다.
이건 버퍼가 있는데 저건 왜 버퍼를 안쓰지? 이건 시스템 테이블스페이스를 안쓰고 왜 별도 테이블스페이스로 분리했지? 저건 또 왜 테이블스페이스를 안쓰고 별도 로그 파일로 분리했지?
궁금증이 꼬리에 꼬리를 물고 그 해답을 여기에 최대한 정리하려다 보니 글이 난잡해진 감도 있어서 읽는 사람들이 쉽게 이해할 수 있을지 잘 모르겠다. 체인지 버퍼나 로그 버퍼, MVCC에 대해서도 자세히 다뤄보고 싶었는데 글이 너무 길어질까봐 여기서는 넘기기로 했다.
아무튼 책을 읽고 정리하면서 평소 MySQL을 쓸 때 가려웠던 곳들을 조금씩 긁어주는 것 같아서 좋았다. 다만 읽기만 해서는 책 특성 상 설명하는 개념이 왔다갔다 하다보니 완전히 해소하기 쉽지 않았고, 다시 나만의 글로 작성하면서 온전히 이해할 수 있었다. 역시 완전한 이해를 위해 설명하는 과정은 필수불가결한 것 같다. 앞으로도 열심히 해보자~!
Real MySQL 8.0
트랜잭션 관리와 ACID 원칙의 중요성
InnoDB Reference Manual