본문으로 건너뛰기

COMMIT은 Redo Log의 어디까지 기다리는가

기본 내구성 설정에서 COMMIT이 기다리는 대상은 Data Page가 아니라 필요한 Redo 범위입니다.

그 범위는 어디까지일까요? 이 질문에 답하려면 Redo의 위치를 나타내는 LSN과 write, flush의 차이를 먼저 나눠 봐야 합니다.

LSN은 Redo 스트림의 논리적 위치다

LSN은 Log Sequence Number의 약자입니다. InnoDB가 Redo를 추가할 때 계속 증가하는 논리적 위치입니다.

MySQL 용어집은 LSN을 Redo Log 안의 위치로 정의합니다. LSN은 트랜잭션 번호가 아니고, 순환해서 사용하는 Redo File 하나의 고정된 물리 offset도 아닙니다. 여러 User Thread와 내부 작업이 하나의 전역 Redo 스트림을 함께 사용합니다.

text
LSN 100 120 140 160 180
|-----------|----------|----------|----------|
Group A Group B Group C Group D

Mini-Transaction이 만든 Redo Group은 [start_lsn, end_lsn)과 같은 범위를 차지합니다. 한 SQL Transaction이 여러 mtr를 사용한다면 여러 Redo Group이 전역 스트림에 섞여 들어갈 수 있습니다. 특정 트랜잭션의 Redo가 하나의 큰 덩어리로만 저장된다고 볼 수는 없습니다.

Redo가 만들어진 것과 파일에 쓰인 것은 다르다

전용 Log Writer와 Log Flusher를 사용하는 일반 경로를 단순화하면 다음과 같습니다.

text
User Thread가 Log Buffer에 Redo Group을 준비

Log Writer가 연속된 구간을 Redo File로 write

Log Flusher가 fsync 등에 해당하는 flush 수행

저장 계층이 요청을 정상적으로 처리하면 영속화

여러 Thread가 LSN 범위를 예약하고 기록하므로 뒤쪽 범위의 작성이 먼저 끝날 수도 있습니다. Log Writer는 중간에 준비되지 않은 구간을 건너뛰어 파일로 보낼 수 없습니다. 빈틈없이 이어진 범위까지만 진행합니다.

개념적으로는 다음 순서가 유지됩니다.

text
flushed_to_disk_lsn
≤ write_lsn
≤ Log Buffer에서 연속해서 준비된 LSN
≤ current_lsn

각 위치가 항상 같지는 않습니다. Redo 생성이 빠르고 저장 장치가 느리면 차이가 커질 수 있습니다.

write와 durable flush를 구분한다

기본적인 fsync 경로에서 write는 Log Buffer의 내용을 운영체제 I/O 경로로 넘깁니다. write가 완료돼도 내용이 OS Cache나 저장 장치의 휘발성 Cache에 남아 있을 수 있습니다.

text
mysqld의 Log Buffer
↓ write()/pwrite()
OS I/O 경로와 Redo File
↓ fsync()/fdatasync() 등에 해당하는 flush
비휘발성 저장 영역

write가 끝났다고 해서 전원 장애 뒤에도 데이터가 남는 것은 아닙니다. 실제 경로는 innodb_flush_method, 운영체제, File System과 저장 장치에 따라 달라집니다. 여기서는 저장 장치가 Flush 완료를 올바르게 보고한다고 가정합니다.

일반적인 UPDATE COMMIT의 목표 LSN

id=1BOOKED에서 CANCELLED로 바꾼 트랜잭션을 다시 보겠습니다.

일반 테이블을 UPDATE한 트랜잭션은 COMMIT 과정에서 Undo 상태를 바꾸고, Update Undo를 Rollback Segment의 History List에 연결합니다. 이 변경을 묶는 serialization mtr도 Redo로 보호됩니다.

text
UPDATE 과정

mtr A: Undo Page 변경
mtr B: Clustered Index Page 변경

COMMIT 과정

serialization mtr:
Undo Log Segment 상태 전환
Update Undo를 RSEG History List에 연결

이 mtr의 end LSN 획득

Binary Log와 Server 2PC를 제외한 InnoDB 내부 경로에서 innodb_flush_log_at_trx_commit=1이면, 이 end LSN이 Redo Flush를 기다릴 목표가 됩니다. 전역 Redo 스트림이 그 위치까지 이어져 있으므로 목표 LSN까지 영속화되면 앞서 만들어진 Page 변경 Redo도 포함됩니다.

text
flushed_to_disk_lsn ≥ commit target LSN

commit target은 실행 중인 Thread가 기다릴 위치일 뿐, 복구 때 읽는 별도의 영속 COMMIT 표식은 아닙니다. 서버가 비정상 종료되면 메모리의 target 값은 사라지고, Recovery는 영속화된 Redo와 Undo Segment 상태, Rollback Segment 메타데이터로 트랜잭션 상태를 다시 구성합니다.

메모리의 COMMIT과 성공 응답은 같은 시점이 아니다

InnoDB 내부에서는 트랜잭션 상태를 메모리에서 완료 상태로 바꾸고 Lock을 정리하는 작업과 Redo의 영속화를 기다리는 작업이 구분됩니다. 아래는 Binary Log와 Server 2PC를 제외한 InnoDB 내부의 Redo 대기 흐름입니다.

text
COMMIT용 Redo Group 완료

메모리의 Transaction 상태 전환과 Lock 정리

설정에 따른 target LSN write·flush 대기

InnoDB COMMIT 처리 완료

MySQL Server가 클라이언트에 성공 응답

내부 상태가 COMMITTED_IN_MEMORY로 바뀐 것과 클라이언트가 COMMIT 성공을 받은 것은 같은 시점이 아닙니다. 설정값이 1이면 필요한 Redo가 영속화되는 조건까지 만족한 뒤 COMMIT 처리가 서버로 돌아갑니다.

반대로 클라이언트가 성공 응답을 받은 뒤에도 Data Page는 Dirty 상태로 남아 있을 수 있습니다. COMMIT이 확인한 것은 Page Flush가 아니라 Redo의 내구성입니다.

한 번의 flush가 여러 COMMIT을 끝낼 수 있다

각 트랜잭션마다 별도의 fsync를 한 번씩 수행한다면 동시 요청이 많을수록 저장 장치 대기가 반복됩니다. InnoDB는 Redo의 진행 위치를 공유해 여러 대기 조건을 한 번에 만족시킬 수 있습니다.

한 번의 Redo flush가 서로 다른 목표 LSN을 기다리는 세 트랜잭션을 깨우는 과정

세 트랜잭션의 목표가 다음과 같다고 해보겠습니다.

text
T1 target LSN = 150
T2 target LSN = 190
T3 target LSN = 230

Log Flusher가 LSN 230까지 한 번에 영속화하면 세 조건이 모두 충족됩니다.

text
flushed_to_disk_lsn = 230

230 ≥ 150 → T1 대기 완료
230 ≥ 190 → T2 대기 완료
230 ≥ 230 → T3 대기 완료

각 Thread는 통지를 받은 뒤 자기 조건을 다시 확인하고, 목표가 충족됐다면 남은 COMMIT 처리를 이어갑니다. 이미 flushed_to_disk_lsn이 target보다 앞서 있다면 새 I/O 없이 조건을 통과할 수도 있습니다.

이것이 InnoDB Redo 경로의 Group Commit입니다. Binary Log Group Commit과 MySQL Server의 2PC는 별도의 계층입니다.

innodb_flush_log_at_trx_commit이 대기 지점을 바꾼다

설정COMMIT 시 Redo writeCOMMIT 시 durable flush장애 시 영향
1기다림기다림정상적인 저장 계층을 전제로 COMMIT된 Redo의 내구성을 보장합니다.
2기다림기다리지 않음Redo가 durable flush되기 전에 장애가 나면 최근 COMMIT을 잃을 수 있습니다.
0기다리지 않음기다리지 않음Redo가 durable flush되기 전에 장애가 나면 최근 COMMIT을 잃을 수 있습니다.

1이 기본값입니다. 2는 COMMIT마다 write를 기다리지만 0은 COMMIT에서 write도 기다리지 않습니다. 두 설정의 durable flush 주기는 innodb_flush_log_at_timeout이 정하며 기본값은 1초입니다.

이 주기는 정확히 지켜진다는 보장이 없습니다. 스케줄링 때문에 늦어질 수 있고, DDL이나 일부 InnoDB 내부 작업이 별도로 Flush하면 더 빨라질 수도 있습니다. 따라서 장애 때 잃는 범위를 정확히 1초라고 단정할 수 없습니다.