COMMIT과 Page Flush는 같은 일이 아니다
UPDATE 직후 Buffer Pool의 Clustered Index Page에는 CANCELLED가 있지만 Tablespace의 같은 Page에는 아직 BOOKED가 남아 있을 수 있습니다. 그렇다면 COMMIT할 때 이 Page까지 바로 저장해야 할까요?
Redo Log가 활성화된 일반 영구 InnoDB 테이블에서는 COMMIT할 때 변경된 Page 전체가 저장되기를 기다리지 않습니다. COMMIT과 Page Flush는 서로 다른 경로로 진행되며, Page를 저장할 때는 관련 Redo가 먼저 영속화돼야 한다는 WAL 조건을 지켜야 합니다.
두 경로는 서로 다른 일을 한다
InnoDB 내부의 COMMIT 경로는 트랜잭션을 완료 상태로 바꾸고, 설정에 따라 필요한 Redo가 파일에 쓰이거나 저장 장치까지 Flush되기를 기다립니다.
UPDATE를 구성한 mtr 완료
↓
COMMIT에 필요한 내부 상태 변경
↓
기다릴 Redo의 목표 LSN 결정
↓
설정에 따른 Redo write·flush 대기
↓
COMMIT 성공 응답
Page Flush 경로는 Buffer Pool의 Dirty Page를 골라 대상 Tablespace에 기록합니다.
Dirty Page 선택
↓
해당 Page 변경의 Redo가 영속화됐는지 확인
↓
필요하면 Redo를 먼저 flush
↓
Dirty Page를 대상 Tablespace에 기록
Dirty Page는 보통 Page Cleaner가 백그라운드에서 Flush합니다. LRU eviction이나 User Thread가 저장에 참여할 때도 Flush가 일어날 수 있습니다.
COMMIT은 특정 트랜잭션을 완료하는 경로이고, Page Flush는 Buffer Pool의 Page 사본을 Data File에 반영하는 경로입니다. 목적이 다르기 때문에 둘의 전체 순서는 고정되지 않습니다.
WAL은 Page를 바꾸기 전에 fsync하라는 뜻이 아니다
WAL은 Write-Ahead Logging의 약자입니다. 이름만 보면 메모리의 Page를 바꾸기 전에 Redo를 디스크에 저장해야 한다고 생각하기 쉽습니다.
InnoDB는 Buffer Pool의 Page를 먼저 바꾸고 Redo Log Record를 만들 수 있습니다. WAL이 정하는 순서는 Dirty Page를 Data File에 쓰려는 시점에 적용됩니다. 개념적으로는 다음 조건입니다.
log.flushed_to_disk_lsn
≥
bpage.get_newest_lsn()
get_newest_lsn()은 Buffer Pool Page에 반영된 최신 변경의 LSN을 반환합니다. Page를 쓰기 전에 그 지점까지 Redo가 영속화돼 있어야 합니다. MySQL 8.4.10의 Buffer Flush 구현에도 같은 조건이 있습니다.
이 순서를 지키지 않으면 새 값이 들어간 Data Page만 남고, 장애 뒤 그 변경과 Undo 구조를 복원할 Redo는 사라질 수 있습니다.
Buffer Pool Page 변경
↓
Redo Log Records 생성
↓
Page를 Data File에 쓰기 전에 관련 Redo 영속화
↓
Page Flush
Page Flush가 COMMIT보다 먼저 끝날 수 있다
id=1을 CANCELLED로 바꾼 뒤 아직 COMMIT하지 않았다고 해보겠습니다. 이때도 Buffer Pool 관리상 Page를 내보내야 할 수 있습니다.
UPDATE
↓
Buffer Pool Page = CANCELLED
Transaction = ACTIVE
↓
관련 Redo 영속화
↓
Data Page Flush
↓
나중에 COMMIT 또는 ROLLBACK
미완료 트랜잭션의 변경이 포함된 Page도 먼저 Data File에 저장할 수 있는 정책을 복구 이론에서는 STEAL이라고 부릅니다. Buffer Pool 공간을 다른 Page에 사용할 수 있다는 뜻입니다.
Page Flush 뒤 트랜잭션이 Rollback되거나 서버가 비정상 종료되면 Data File에는 CANCELLED가 남아 있을 수 있습니다. 최종 상태는 BOOKED여야 하므로 Undo가 필요합니다.
Data File = CANCELLED
Transaction = ACTIVE
↓ Crash Recovery
Undo로 CANCELLED → BOOKED
이것이 STEAL 구조에서 Undo가 필요한 이유입니다.
COMMIT이 Page Flush보다 먼저 끝날 수도 있다
반대 순서도 정상입니다.
UPDATE
↓
Buffer Pool Page = CANCELLED
Data File Page = BOOKED
↓
COMMIT에 필요한 Redo 영속화
↓
COMMIT 성공
↓
나중에 Data Page Flush
COMMIT할 때 변경된 모든 Page를 강제로 저장하지 않는 정책을 NO-FORCE라고 부릅니다. 여러 Data Page와 Index Page, Undo Page가 바뀌었더라도 COMMIT 경로에서 모두 찾아 쓰기를 기다리지 않습니다.
Page Flush보다 먼저 장애가 발생하면 Data File에는 BOOKED가 남아 있을 수 있습니다. COMMIT에 필요한 Redo가 영속화됐다면 복구 과정에서 Page 변경을 다시 적용해 CANCELLED를 만들 수 있습니다.
Data File = BOOKED
Transaction = COMMITTED
↓ Crash Recovery
Redo로 BOOKED → CANCELLED
이것이 NO-FORCE 구조에서 Redo가 필요한 이유입니다.
Dirty와 트랜잭션 상태는 서로 다른 축이다
아래 표는 COMMIT에 필요한 Redo가 장애 뒤에도 남아 있다는 전제에서 두 경로를 단순화한 것입니다.
| 트랜잭션 | Data File의 값 | 복구 뒤 값 | 필요한 처리 |
|---|---|---|---|
| COMMITTED | BOOKED | CANCELLED | 필요한 Redo를 적용합니다. |
| COMMITTED | CANCELLED | CANCELLED | Page가 이미 최신이면 오래된 Redo를 다시 적용할 필요가 없습니다. |
| ACTIVE | BOOKED | BOOKED | Redo로 미완료 변경이 복원됐다면 Undo합니다. 그렇지 않으면 기존 값이 남습니다. |
| ACTIVE | CANCELLED | BOOKED | 먼저 저장된 미완료 변경을 Undo합니다. |
Dirty는 Buffer Pool의 Page가 대상 Data File보다 최신인지 나타내는 상태입니다. ACTIVE, COMMITTED 같은 트랜잭션 상태와는 다른 정보입니다.
복구할 때도 행별 COMMIT 여부를 먼저 가려 Redo를 적용하지는 않습니다. 영속화된 Redo로 필요한 Page 변경을 복원한 뒤, 끝나지 않은 트랜잭션을 Rollback합니다. InnoDB Recovery도 두 작업을 별도 단계로 나눕니다.
STEAL과 NO-FORCE는 MySQL 설정명이 아니다
STEAL과 NO-FORCE는 my.cnf에 넣는 옵션 이름이 아닙니다. Buffer Manager가 Page를 언제 저장할 수 있는지 설명하는 복구 이론의 분류입니다.