InnoDB란?
: MySQL의 기본 스토리지 엔진 중 하나로, 안정성, 동시성, 트랙잭션 지원 등에서 매우 중요한 역할을 함.
즉 MySQL이 데이터를 어떻게 저장하고 관리할지를 결정하는 방식 중 하나라고 할 수 있음.
InnoDB의 중요한 구성요소로 동시성 제어(concurrency control), 장애 복구(Crash recovery)에 많은 영향을 줌.
InnoDB에서의 Undo log 구현은 로그이면서 데이터의 성격을 가짐.
다음 글은 MySQL 8.0을 기반으로 작성함.
Undo log는 각 변경 전에 기록된 이전 값을 저장함. 이는 Redo Log와 함께 장애 복구에 사용됨.
데이터베이스 설계 시 언제든지 시스템이 갑자기 중단(crash)될 수 있음.
커밋되지 않은 트랜잭션이라도 일부 데이터가 디스크에 기록되었을 수 있는데, 이 경우 트랜잭션의 원자성 보장이 깨질 수 있음. 데이터베이스는 하나의 트랜잭션 변경이 전부 반영되거나 전혀 반영되지 않아야 함.
이 문제를 해결하는 직관적인 방식은
'트랜잭션이 커밋될 때 까지 어떤 변경도 디스크에 반영하지 않는' 것.
이를 No-steal 전략이라 함.
그러나 이 전략은 한편으로는 메모리 공간 부족, 커밋 시점의 무작위 I/O로 인한 성능저하를 야기할 수 있음.
이를 보완하여 다음 방식을 채택함:
Undo log는 이러한 crash recovery 뿐만 아니라, 데드락 처리나 사용자가 직접 요청하는 트랜잭션 롤백 등 정상적인 운영 중의 롤백에도 활용됨.
거의 모든 주요 DBMS는 MVCC 방식을 사용하여 읽기 전용 트랜잭션과 쓰기 트랜잭션 간 충돌을 방지하고 쓰기 작업이 읽기 작업을 기다리지 않도록 처리함.
DB는 각 레코드의 여러 버전(이전 값들)을 저장하고,
읽기 트랜잭션은 필요한 시점의 버전을 참조하며,
쓰기 작업은 단순히 새로운 버전을 추가하면 되므로 충돌이 최소화됨.
InnoDB는 Undo log에 저장된 과거 버전의 데이터를 재사용하여 MVCC를 구현함.
페이지 기반 Redo Log는 동시적인 Redo 적용(concurrent Redo application)을 가능하게 하여 데이터베이스의 장애 복구 시간을 줄이는 데 도움을 줌.
InnoDB는 MVCC를 구현하기 위해 Undo log를 사용하며 데이터베이스가 운영 중일 때도 버전 히스토리 데이터를 유지할 수 있게 함.
그렇기 때문에 장애 복구 중 발생하는 Undo log 기반의 트랜잭션 롤백은 일반적인 트랜잭션처럼 백그라운드에서 비동기적으로 처리할 수 있음.
즉, 데이터베이스는 롤백이 끝나기 전에 먼저 서비스를 재개할 수 있음.
Undo log - Redo log 는 설계 원리가 다름.
Undo log는 다음과 같은 점을 중시함:
따라서 InnoDB의 Undo log는 트랜잭션 기반의 논리 로그 방식을 채택하고 있음.
또한 InnoDB는 Undo log를 일종의 Data로 관리하며 다른 일반적인 데이터와 마찬가지로 Undo log 역시 Redo log를 통해 그 변경사항이 기록되고 원자성이 보장됨.
InnoDB에서 레코드가 수정될 필요가 있을 때마다, 그 이전 상태(히스토리 버전)가 Undo log에 기록됨. 이때 생성되는 Undo 레코드는 update 타입.
새로운 레코드가 삽입될 경우에는 과거 버전이 존재하지 않지만, Insert 타입의 Undo 레코드가 기록됨. 이는 트랜잭션 롤백 시 삽입된 레코드를 삭제할 수 있게 하기 위함임.
이 undo 레코드는 코드 상에서 TRX_UNDO_INSERT_REC 타입(InnoDB에서 트랜잭션 도중 새로운 레코드가 삽입되었을 때 기록되는 Undo 레코드 타입)에 해당함.
Update 타입과 달리 insert 타입의 레코드는 이전 데이터가 아닌 새로운 데이터 이므로 MVCC 기능과는 무관함.
오직 트랜잭션이 롤백될 경우를 대비하여 준비되는 것.
따라서 이 경우에는 롤백 시 어떤 레코드를 찾아 삭제할지 알아내기 위해 해당 레코드의 key만 기록하면 충분함.
또한 각 Undo 레코드의 시작과 끝에는 2바이트 공간이 추가되어 있음.
이는 앞선 Undo 레코드와 다음 Undo 레코드의 위치를 기록하기 위한 공간임.
즉, Undo 레코드들은 서로 연결된 구조(연결 리스트 형태)를 가짐.
MVCC는 레코드의 여러 과거 버전(히스토리 버전)을 보존해야 하므로 이 과거 버전이 아직 사용 중일 경우에는 삭제할 수 없음. 이런 경우에는 실제 삭제하지 않고, 단순히 해당 레코드의 Delete Mark만 수정함.
그리고 만약 이 시점에 동일한 레코드가 다시 삽입된다면 사실상 이는 새로운 레코드를 추가하는 것이 아니라 Delete Mark만 다시 해제하는 것.
즉, 삭제와 삽입을 Update로 변환하여 처리하게 됨.
이처럼 일반적인 레코드 수정 작업과 더불어, InnoDB에서는 Update 타입 Undo 레코드를 다음 3가지 유형으로 구분함:
📍Undo Log Header 구조
📍Rollptr에 의한 다중 버전 구성
앞서 논리적 구조를 설명했지만 실제로는 모든 Undo Log가 디스크에 기록되어야 함. 하지만 트랜잭션마다 생성되는 Undo Log의 크기는 제각각임. 반면 InnoDB는 디스크에 16KB 고정 크기 블록 단위로 기록함.
📍InnoDB의 설계 아이디어
📍Undo Segment란?
📍Undo Page 구조
짧은 Undo Log들은 하나의 페이지에 여러 개 저장
긴 Undo Log는 다수의 페이지에 나눠 저장됨
단, 페이지 재사용은 첫 번째 페이지에서만 가능
📍Rollback Segment 구조
📍Rollback Segment 수의 의미
지금까지는 디스크 상의 Undo 구조를 설명함.
실제로는 Undo 로그를 효율적으로 관리하기 위해 메모리 내에도 해당 구조체가 유지됨.
디스크의 각 Undo Tablespace에 대응하여 메모리에도 하나의 Undo::Tablespace 구조체가 존재함.
그 핵심은 여러 개의 trx_rseg_t임. 앞서 설명한 Rollback Segment Header에 해당함.
📍trx_rseg_t 내부 구조
각 Undo Segment는 trx_undo_t 구조체로 관리되며 위 네 리스트 중 하나에 포함됨.
트랜잭션이 시작되면, trx_assing_rseg_durable()을 호출하여 Rollback Segment를 할당함. 이때 트랜잭션 구조체 trx_t는 해당 Rollback Segment를 가리키는 trx_rseg_t 구조체를 연결함.
할당 전략으로는 현재 활성 상태인 Rollback Segment를 순차적으로 시도함.
첫 수정 작업 시 trx_undo_assing_undo()가 호출되어 Undo Segment를 요청함. 먼저 trx_rseg_t의 Cached List에 있는 미사용 Undo Segment를 재사용 시도함. 없다면 trx_undo_create()를 호출하여 새로 생성
현재 Rollback Segment 내 slot을 순회하며 사용 가능한 슬롯(FIL_NUL 값)을 선택. 새로운 Undo Page를 할당하고 Undo Page Header, Undo Segment Header를 초기화함.
메모리 구조체 trx_undo_t를 생성하여 trx_rseg_t의 리스트에 추가
Undo 레코드 기록으로는,
트랜잭션은 할당받은 Undo Segment에 자신만의 Undo Log Header를 초기화하고, trx_undo_report_row_operation() 시퀀스를 통해 Undo 레코드를 기록함.
Undo Page가 가득차면 trx_undo_add_page()를 호출하여 새 페이지 추가
⚠️ 하나의 Undo 레코드는 페이지를 넘지 않음. 현재 페이지에 다 들어가지 않으면 다음 페이지에 전체 Undo 레코드를 기록
트랜잭션 종료 후 처리로는,
💡 롤백이 발생하는 경우
💡 롤백 처리 절차
1. row_undo() 함수가 진입점
2. trx_roll_pop_rec_of_trx() 호출 ➡️ 가장 마지막 Undo 레코드 pop
3. 레코드가 여러 페이지에 걸쳐 있을 경우:
💡 레코드 유형별 롤백 처리
✔ TRX_UNDO_INSERT_REC
함수: row_undo_ins()
처리: 기본 키를 이용해 인덱스에서 위치 탐색
✔ Update 계열 (TRX_UNDO_UPD_EXIST_REC, TRX_UNDO_DEL_MARK_REC, TRX_UNDO_UPD_DEL_REC)
함수: row_undo_mod()
처리:
💡 롤백 후 공간 회수
MVCC의 목적은 쓰기 트랜잭션과 읽기 트랜잭션이 서로 기다리지 않도록 하는 것.
각 읽기 트랜잭션은 레코드에 lock을 걸지 않고도 자신이 읽어야 하는 적절한 과거 버전(히스토리 버전)을 찾아야 함.
이를 위해 InnoDB는 읽기 전용 트랜잭션이 시작될 때 전체 DB의 스냅샷을 본 것처럼 작동시켜야 하지만, 실제로 모든 트랜잭션의 스냅샷을 저장하는 것은 시간과 공간 비용이 너무 큼.
💡 InnoDB의 접근법: ReadView
📍 읽기 일관성 규칙(read commited 기준)
💡 Undo 로그를 통한 버전 조회
💡 Undo 로그에서 과거 버전 복원
장애 복구에에서는 커밋되지 않은 트랜잭션의 모든 변경 사항을 되돌려야 데이터베이스의 원자성을 보장할 수 있음.
Undo는 InnoDB에서 일반 데이터처럼 처리되며, Redo 로그를 통해 내구성(Durability)이 보장됨.
💡 Undo 관련 Redo 타입
💡 장애 복구 절차 (ARIES st.)
1. Redo 로그 재생
: 앞서 언급한 Redo 타입들을 통해 Undo 구조 전체 복원
2. trx_sys_init_at_db_start 함수에서:
💡 비동기 롤백 처리
▶️ 비동기 처리는 어떤 작업을 요청한 후 그 작업이 끝날 때까지 기다리지 않고 다른 작업을 계속 수행할 수 있도록 하는 처리 방식.
Undo는 MVCC를 위해 여러 버전 정보를 유지하지만 더 이상 어떤 트랜잭션에서도 읽히지 않을 과거 버전은 제거해야 함.
💡 불필요한 Undo 판단 기준
💡Purge 작업 흐름
트랜잭션 종료 시, Purge가 필요한 update 타입 Undo 로그는 해당 Rollback Segment Header의 History List에 커밋 순서(trx_no)대로 추가됨.
Undo 로그의 정리(purge) 기본 원칙은 작은 trx_no 부터 큰 순서로 Undo 로그 전체를 순차적으로 순회하며 정리하는 것.
❔Rollback Segment가 여러 개일 경우?
💡 purge queue
purge_queue는 힙 자료구조로서 모든 history list로부터 가장 작은 trx_no를 가진 트랜잭션을 우선적으로 처리할 수 있게 해줌.
▶️ Purge 절차
1. purge_queue에 모든 Rollback Segment의 최상위 Undo Log 추가
2. trx_purge_choose_next_log(): purge_queue에서 trx_no가 가장 작은 Undo log를 꺼냄
3. trx_purge_get_next_rec(): 해당 Undo Log의 Undo 레코드를 위에서 아래 방향으로 순회하며 처리. (일반적인 롤백은 아래-위 방향)
4. 처리 후에는 trx_purge_rseg_get_next_history_log() 호출. 현재 Rollback Segment의 다음 Undo Log를 Purge_queue에 추가하고 이후 반복함.
💡 Undo Log가 여러 페이지에 걸쳐 있는 경우
ex) 하나의 Undo Log가 2개의 Undo Page에 걸쳐 존재하는 경우
Undo Log Header의 Log start offset 값을 이용해 첫 Undo 레코드의 위치 탐색
Next Record offset을 따라 다음 레코드로 이동
page가 끝나면 page list node를 통해 다음 페이지로 이동
모든 레코드가 처리될 때까지 이 과정을 반복함.
💡 인덱스 정리와 연동되는 처리
어떤 Undo 레코드는 인덱스 데이터에 영향을 주므로, purge 전에 특별한 처리가 필요함
ex) trx_undo_del_mark_rec 타입: 이는 단순히 레코드를 삭제하는 것이 아닌 delete mark를 세운 상태로 유지한 것
정리 시 해야 할 일로는,
1. Undo purge: trx_undo_del_mark_rec undo 레코드에 대응하는 실제 인덱스 레코드 삭제
2. Undo Truncate: Undo 레코드 자체를 오래된 순서부터 새 레코드까지 삭제(데이터 자체가 아닌 Undo 로그 공간을 제거하는 의미)
💡 Undo Tablespace Truncate
InnoDB에서 독립 Undo Tablespace 수가 2개 이상으로 설정된 경우, Undo Tablespace가 설정된 크기를 초과하면 자동으로 축소(Rebuild) 가능
이 공간 정리 작업을 Undo Tablespace Truncate라고 함.
이 단계는 주로 trx_undo_del_mark_rec 타입의 Undo 레코드를 대상으로 함. 해당 레코드는 인덱스에서 Delete mark가 지정된 레코드를 실제로 삭제하는 데 사용됨.
💡 처리 절차
1. row_purge_parse_undo_rec 함수를 사용해 Undo 레코드에서 Undo type/table_id/Rollptr/기본 키 정보/update vector(변경 정보)를 추출함.
2. Undo 타입이 trx_undo_del_mark_rec인 경우
Undo purge 후에는 사용되지 않는 Undo 로그 자체를 정리하는 작업이 수행됨.
코디네이터 스레드는 워커 스레드들이 Undo purge를 완료할 때까지 기다리고, 이후 더 이상 필요하지 않은 Undo 로그를 제거하려 시도함.
💡 처리 함수 및 조건
1. trx_purge_truncate() 함수 호출 - 모든 Rollback Segment Undo Segment 순회
2. 각 Undo Segment 상태 확인. 상태가 trx_undo_to_purge이면 trx_purge_free_segment() 호출 - 디스크 공간 해제, 히스토리 리스트에서 제거
상태가 trx_undo_cached 등 다른 상태라면, 단순히 trx_purge_remove_log_hd()를 통해 리스트에서만 제거
⚠️ Undo Truncate는 매번 실행되지 않음. 설정값 innodb_rseg_truncate_frequency에 따라 N번째 Purge 배치마다 한 번 수행
즉, 각 purge 배치는 innodb_purge_batch_size 개의 Undo 레코드를 처리하고 그 결과 show engine innodb status에서 보이는 Undo History List 길이는 간헐적으로 감소(hopping)함
동작 조건으로는 설정값 innodb_trx_purge_truncate가 활성화되어 있어야 함. 이 경우 trx_purge_truncate() 함수는 Undo tablespace 축소도 시도함
💡 처리 흐름
1. Undo Truncate 이후, trx_purge_mark_undo_for_truncate() 함수에서 모든 Undo tablespace의 파일 크기 확인
2. 파일 크기가 innodb_max_undo_log_size를 초과하는 tablespace는 비활성화로 표시됨. 한 번에 최대 하나의 Undo tablespace만 비활성 상태로 유지. 비활성 tablespace의 Rollback Segment는 더 이상 새로운 할당에 참여하지 않음.
3. 해당 tablespace에 있는 모든 트랜잭션이 종료되고, 해당 Undo 로그가 Purge 완료되면 trx_purge_initiate_truncate() 함수 호출. Undo table space의 파일 및 메모리 구조를 재구성(rebuild). 다시 활성화 상태로 전환되어 이후 새로운 트랜잭션에서 재사용 가능.
Reference
https://www.alibabacloud.com/blog/an-in-depth-analysis-of-undo-logs-in-innodb_598966