12. 복구 시스템
12. 복구 시스템¶
이전: 동시성 제어 | 다음: NoSQL와 NewSQL
학습 목표¶
- 실패 유형을 분류하고 저장 계층 이해
- 로그 기반 복구 기법 마스터: 지연 및 즉시 수정
- 선행 기록 로깅(WAL) 프로토콜과 그것이 필수적인 이유 이해
- 퍼지 체크포인트를 포함한 체크포인트 메커니즘 학습
- ARIES 복구 알고리즘 상세 학습: 분석, 재실행, 취소 단계
- 복구 접근 비교: 그림자 페이징 vs. WAL 기반 복구
- 버퍼 관리 정책 이해: force/no-force, steal/no-steal
- 미디어 복구와 백업 전략 학습
1. 실패 분류¶
데이터베이스 시스템은 다양한 유형의 실패를 우아하게 처리해야 한다. 실패 유형을 이해하는 것은 적절한 복구 메커니즘을 설계하는 데 필수적이다.
1.1 트랜잭션 실패¶
단일 트랜잭션 내의 실패. 시스템의 나머지는 영향을 받지 않는다.
논리적 오류: - 0으로 나누기 - 제약 위반 (예: 고유 키, 외래 키) - 응용 프로그램 수준 단언 실패 - 사용자가 시작한 중단 (ROLLBACK)
트랜잭션에 영향을 미치는 시스템 오류: - 데드락 희생자 선택 (데드락을 깨기 위해 트랜잭션이 중단됨) - 타임아웃 만료 - 트랜잭션 작업 영역의 메모리 부족
트랜잭션 실패 복구:
T₁: read(A) write(A) read(B) ← 제약 위반!
↓
T₁의 A에 대한 쓰기를 취소
T₁의 잠금 해제
T₁이 중단 상태로 들어감
1.2 시스템 실패 (충돌)¶
전체 시스템이 예기치 않게 정지. 휘발성 저장소(주 메모리, 버퍼 풀)가 손실되지만 디스크 내용은 생존.
원인: - 운영 체제 충돌 - 정전 (UPS 없이) - 하드웨어 실패 (CPU, 메모리) - DBMS의 소프트웨어 버그
시스템 실패:
┌──────────────────────────────────┐
│ 주 메모리 (손실됨) │
│ ┌────────────┐ ┌────────────┐ │
│ │ 버퍼 풀 │ │ 잠금 테이블 │ │
│ │ (더티 │ │ (모든 잠금 │ │
│ │ 페이지) │ │ 손실됨) │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────┘
↕ 충돌 ↕
┌──────────────────────────────────┐
│ 디스크 (생존함) │
│ ┌────────────┐ ┌────────────┐ │
│ │ 데이터베이스│ │ 로그 파일 │ │
│ │ 파일 │ │ (WAL) │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────┘
복구 후:
- 커밋된 트랜잭션: 재실행 (효과가 지속되어야 함)
- 커밋되지 않은 트랜잭션: 취소 (효과가 역전되어야 함)
1.3 디스크 실패 (미디어 실패)¶
디스크 저장소의 물리적 파괴. 영향받은 디스크의 데이터가 손실됨.
원인: - 디스크 헤드 충돌 - 컨트롤러 실패 - 화재, 홍수, 또는 다른 물리적 손상
디스크 실패 복구:
주 디스크 파괴됨
↓
백업에서 복원 (테이프, 원격 복제본)
↓
백업 시점부터 실패 시점까지 로그 재생
↓
데이터베이스가 일관된 상태로 복원됨
1.4 실패 요약¶
| 실패 유형 | 손실된 것 | 복구 방법 |
|---|---|---|
| 트랜잭션 | 트랜잭션의 부분 작업 | 로그를 사용한 취소 (롤백) |
| 시스템 (충돌) | 휘발성 저장소 (버퍼 풀) | 커밋됨 재실행 + 커밋 안 됨 취소 |
| 디스크 (미디어) | 디스크 내용 | 백업에서 복원 + 로그 재생 |
2. 저장 유형¶
2.1 저장 계층¶
저장 계층:
┌─────────────────────┐
│ 휘발성 저장소 │ CPU 레지스터, 캐시, 주 메모리
│ (빠름, 정전 시 │ 접근: 나노초
│ 손실됨) │
└─────────┬───────────┘
│
┌─────────┴───────────┐
│ 비휘발성 저장소 │ SSD, HDD, 플래시
│ (정전은 견디지만, │ 접근: 마이크로초 (SSD) ~ 밀리초 (HDD)
│ 디스크 실패는 │
│ 견디지 못함) │
└─────────┬───────────┘
│
┌─────────┴───────────┐
│ 안정 저장소 │ 미러링 디스크, RAID, 원격 복제본, 테이프
│ (이상적으로 모든 │ 접근: 밀리초 ~ 시간
│ 실패를 견딤) │
└─────────────────────┘
2.2 안정 저장소¶
진정한 안정 저장소는 이상화다 -- 실제 저장소는 모든 가능한 실패를 견디지 못한다. 실제로, 다음을 사용하여 안정 저장소를 근사한다:
- RAID (Redundant Array of Independent Disks):
- RAID 1: 미러링 (모든 블록의 두 복사본)
-
RAID 5/6: 패리티 기반 중복성
-
원격 복제: 지리적으로 분리된 위치에 쓰기
-
다중 복사본: 다른 미디어에 복사본 유지 (디스크 + 테이프 + 클라우드)
로그 파일의 경우, 가장 강한 내구성 보장이 필요하므로, 로그는 일반적으로 사용 가능한 가장 신뢰할 수 있는 저장소에 저장된다.
3. 로그 기반 복구¶
3.1 로그¶
로그(Log) (또는 저널(Journal) 또는 선행 기록 로그(Write-Ahead Log))는 모든 데이터베이스 수정의 순차적 기록이다. 이는 복구의 기초다.
로그 레코드 유형:
┌──────────────────────────────────────────────────────────┐
│ <T_i, start> 트랜잭션 T_i가 시작됨 │
│ <T_i, X, V_old, V_new> T_i가 X를 V_old에서 V_new로 변경│
│ <T_i, commit> 트랜잭션 T_i가 커밋됨 │
│ <T_i, abort> 트랜잭션 T_i가 중단됨 │
│ <checkpoint L> 활성 트랜잭션 목록 L과 체크포인트│
└──────────────────────────────────────────────────────────┘
예시 로그:
LSN 로그 레코드
─── ─────────────────────────────
1 <T₁, start>
2 <T₁, A, 100, 50> T₁이 A를 100에서 50으로 변경
3 <T₁, B, 200, 250> T₁이 B를 200에서 250으로 변경
4 <T₂, start>
5 <T₂, C, 300, 400> T₂가 C를 300에서 400으로 변경
6 <T₁, commit>
7 <T₂, D, 500, 600> T₂가 D를 500에서 600으로 변경
← 충돌 (T₂ 커밋 안 됨)
3.2 로그 속성¶
주요 속성: 1. 추가 전용: 새 레코드는 항상 끝에 추가됨 2. 순차 쓰기: 디스크 I/O에 효율적 3. 선행 기록: 로그 레코드가 대응하는 데이터 수정 전에 기록됨 (WAL 규칙) 4. 커밋 시 강제: 트랜잭션의 로그 레코드가 커밋이 완료되기 전에 안정 저장소에 있어야 함
3.3 지연 데이터베이스 수정¶
지연 수정(Deferred Modification) 방식에서, 모든 쓰기는 트랜잭션이 커밋할 때까지 지연된다. 실행 중에, 수정은 로그에만 기록된다.
지연 수정:
T₁ 실행:
로그: <T₁, start>
로그: <T₁, A, _, 50> 쓰기 의도 기록 (오래된 값 불필요)
로그: <T₁, B, _, 250> 쓰기 의도 기록
로그: <T₁, commit>
── 이제 데이터베이스에 쓰기 적용 ──
데이터베이스: A = 50, B = 250
복구는 재실행만 필요 (취소 불필요 왜냐하면 커밋되지 않은
트랜잭션은 절대 데이터베이스에 쓰지 않았기 때문).
충돌 후 복구: - 커밋된 트랜잭션: 모든 쓰기를 재실행 (로그에서) - 커밋되지 않은 트랜잭션: 무시 (데이터베이스를 절대 수정하지 않았음)
장점: - 취소 불필요 (더 간단한 복구) - 데이터베이스에 더티 데이터 없음
단점: - 모든 쓰기가 커밋까지 메모리에 맞아야 함 - 긴 트랜잭션은 큰 버퍼 필요 - 잠금을 조기에 해제할 수 없음 (커밋 + 쓰기까지 모든 잠금 보유해야 함)
3.4 즉시 데이터베이스 수정¶
즉시 수정(Immediate Modification) 방식에서, 쓰기는 발생 시 데이터베이스에 적용된다 (커밋 전에 가능). 이는 모든 주요 데이터베이스 시스템이 사용하는 접근이다.
즉시 수정:
T₁ 실행:
로그: <T₁, start>
로그: <T₁, A, 100, 50> 오래된 값과 새 값 모두 기록
데이터베이스: A = 50 ← 즉시 데이터베이스에 씀
로그: <T₁, B, 200, 250>
데이터베이스: B = 250 ← 즉시 데이터베이스에 씀
로그: <T₁, commit>
복구는 재실행과 취소 모두 필요할 수 있음.
충돌 후 복구: - 커밋된 트랜잭션: 쓰기를 재실행 (변경사항이 지속되도록 보장) - 커밋되지 않은 트랜잭션: 쓰기를 취소 (부분 변경사항을 역전)
예시 로그에서 복구:
LSN 로그 레코드
─── ─────────────────────
1 <T₁, start>
2 <T₁, A, 100, 50>
3 <T₁, B, 200, 250>
4 <T₂, start>
5 <T₂, C, 300, 400>
6 <T₁, commit>
7 <T₂, D, 500, 600>
← 충돌
복구:
T₁ 커밋됨 (<T₁, commit> 발견) → 재실행:
A = 50 (로그 레코드 2에서)
B = 250 (로그 레코드 3에서)
T₂ 커밋 안 됨 (<T₂, commit> 없음) → 취소:
D = 500 (로그 레코드 7 역전: 오래된 값 복원)
C = 300 (로그 레코드 5 역전: 오래된 값 복원)
재실행은 순방향 (커밋된 트랜잭션에 대해 가장 오래된 것부터 최신까지)
취소는 역방향 (커밋되지 않은 트랜잭션에 대해 최신에서 가장 오래된 것으로)
왜 커밋된 트랜잭션에도 재실행이 필요한가: 커밋된 트랜잭션의 쓰기가 충돌이 발생했을 때 여전히 버퍼 풀(휘발성 메모리)에 있고 디스크에 플러시되지 않았을 수 있기 때문.
왜 커밋되지 않은 트랜잭션에 취소가 필요한가: 커밋되지 않은 트랜잭션의 쓰기가 충돌 전에 (버퍼 관리자에 의해) 디스크에 플러시되었을 수 있기 때문.
4. 선행 기록 로깅 (WAL) 프로토콜¶
4.1 WAL 규칙¶
선행 기록 로깅(Write-Ahead Logging, WAL) 프로토콜은 로그 기반 복구가 작동하게 하는 기본 규칙이다:
WAL 규칙: 수정된 데이터 페이지가 버퍼 풀에서 디스크로 기록되기 전에, 그 페이지에 관련된 모든 로그 레코드가 안정 저장소로 플러시되어야 한다.
WAL 프로토콜:
단계 1: 로그 레코드를 로그 버퍼(메모리)에 쓰기
단계 2: 로그 레코드를 디스크(안정 저장소)로 플러시
단계 3: 그 다음(그 다음에만) 수정된 데이터를 디스크에 쓰기
올바른 순서:
로그 버퍼: <T₁, A, 100, 50> → 디스크의 로그: <T₁, A, 100, 50> → 디스크의 데이터: A=50
↑
데이터 쓰기 전에 일어나야 함
왜? 로그 레코드가 기록되기 전에 데이터가 먼저 기록되고 시스템이
충돌하면, 변경사항을 취소할 수 없음 (오래된 값을 모름).
4.2 커밋 규칙 (Force-at-Commit)¶
트랜잭션이 커밋할 때, 모든 로그 레코드가 커밋이 사용자에게 확인되기 전에 안정 저장소로 플러시되어야 한다:
커밋 프로토콜:
T₁: ... 연산 ...
로그: <T₁, commit>
단계 1: T₁의 모든 로그 레코드를 디스크로 플러시 (<T₁, commit> 포함)
단계 2: 사용자에게 커밋 확인 ("트랜잭션 커밋됨")
단계 3: 수정된 데이터 페이지는 나중에 플러시될 수 있음 (지연)
단계 1 후이지만 단계 3 전에 충돌이 발생하면:
로그가 디스크에 <T₁, commit> 있음 → 복구 중 T₁의 변경사항 재실행 ✓
단계 1 전에 충돌이 발생하면:
디스크에 <T₁, commit> 없음 → 복구 중 T₁의 변경사항 취소 ✓
4.3 그룹 커밋¶
모든 단일 커밋에 대해 로그를 디스크로 플러시하는 것은 비싸다. 그룹 커밋(Group Commit)은 여러 커밋을 단일 디스크 쓰기로 일괄 처리한다:
그룹 커밋 없이:
T₁ 커밋 → 플러시 → 확인
T₂ 커밋 → 플러시 → 확인
T₃ 커밋 → 플러시 → 확인
= 3번의 디스크 플러시 (각각 HDD의 경우 ~5-10ms)
그룹 커밋과 함께:
T₁ 커밋 → 버퍼
T₂ 커밋 → 버퍼 → 단일 플러시 → T₁, T₂, T₃ 확인
T₃ 커밋 → 버퍼
= 1번의 디스크 플러시
트레이드오프: 개별 커밋에 대해 약간 더 높은 지연시간,
하지만 훨씬 더 높은 처리량 (초당 더 많은 커밋).
PostgreSQL은 기본적으로 그룹 커밋을 사용한다 (commit_delay와 commit_siblings 설정).
4.4 PostgreSQL의 WAL¶
PostgreSQL WAL 아키텍처:
┌──────────────────────────────────────────┐
│ 공유 버퍼 (버퍼 풀) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Page1│ │Page2│ │Page3│ │Page4│ ... │
│ │더티 │ │깨끗 │ │더티 │ │깨끗 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└────────────────┬─────────────────────────┘
│
│ 더티 페이지가 기록되기 전에
│ WAL이 플러시되어야 함
↓
┌──────────────────────────────────────────┐
│ WAL 버퍼 → WAL 세그먼트 파일 │
│ ┌─────────────┐ ┌────────────────────┐│
│ │ WAL 버퍼 │ → │ 000000010000000042 ││
│ │ (메모리에) │ │ 000000010000000043 ││
│ └─────────────┘ │ ... (각 16MB) ││
│ └────────────────────┘│
└──────────────────────────────────────────┘
-- PostgreSQL WAL 구성
SHOW wal_level; -- minimal, replica, logical
SHOW max_wal_size; -- 체크포인트 전 최대 WAL 크기 (기본: 1GB)
SHOW wal_buffers; -- WAL 버퍼 크기 (기본: ~16MB)
SHOW synchronous_commit; -- on (기본), off (비동기), remote_write, 등
-- WAL 통계 보기
SELECT * FROM pg_stat_wal;
5. 체크포인트¶
5.1 체크포인트가 필요한 이유?¶
체크포인트 없이, 복구는 전체 로그를 처음부터 스캔하여 어떤 트랜잭션을 재실행하고 취소할지 결정해야 한다. 몇 달간 실행된 데이터베이스의 경우, 이는 시간이 걸릴 수 있다.
체크포인트(Checkpoint)는 알려진 좋은 상태를 설정하여, 복구가 얼마나 뒤로 스캔해야 하는지를 제한한다.
5.2 단순 (정지) 체크포인트¶
가장 간단한 체크포인트 프로토콜:
SIMPLE-CHECKPOINT():
1. 새 트랜잭션 수락을 일시적으로 중단
2. 모든 활성 트랜잭션이 완료(커밋 또는 중단)될 때까지 대기
3. 버퍼 풀의 모든 더티 페이지를 디스크로 플러시
4. 로그에 <checkpoint> 레코드 쓰기
5. 로그를 디스크로 플러시
6. 트랜잭션 수락 재개
단순 체크포인트가 있는 로그:
... <T₁,start> <T₁,A,100,50> <T₁,commit> <checkpoint> <T₂,start> ...
↑
모든 이전 트랜잭션이
디스크에 완전히 반영됨.
복구는 여기서 시작.
문제: 모든 트랜잭션을 중단해야 함 -- 프로덕션 시스템에 허용 불가.
5.3 비-정지 (활성) 체크포인트¶
모든 트랜잭션을 중단할 필요가 없는 더 실용적인 체크포인트:
ACTIVE-CHECKPOINT():
1. 로그에 <checkpoint L> 쓰기, 여기서 L = 활성 트랜잭션 목록
2. 버퍼 풀의 모든 더티 페이지를 디스크로 플러시
(이 플러시 중에 새 트랜잭션과 쓰기가 계속될 수 있음)
3. 플러시가 완료되면 로그에 <end_checkpoint> 쓰기
활성 체크포인트가 있는 로그:
<T₁,start> <T₁,A,100,50> <checkpoint {T₁,T₂}> <T₂,B,200,300>
↑
T₁과 T₂가 체크포인트 시작 시
활성이었음
복구:
L에서 트랜잭션의 가장 이른 시작까지 스캔해야 함
(즉, <T₁,start> T₁이 활성 목록에 있기 때문)
5.4 퍼지 체크포인트¶
퍼지 체크포인트(Fuzzy Checkpoint)는 ARIES와 대부분의 현대 시스템이 사용하는 가장 실용적인 접근이다:
FUZZY-CHECKPOINT():
1. 로그에 <begin_checkpoint> 쓰기
2. 기록:
- 활성 트랜잭션 테이블 (ATT): 모든 활성 트랜잭션과 그 상태
- 더티 페이지 테이블 (DPT): 버퍼 풀의 모든 더티 페이지
3. 로그에 <end_checkpoint ATT, DPT> 쓰기
4. 로그를 안정 저장소로 플러시
5. 마스터 레코드(디스크에)를 <begin_checkpoint>를 가리키도록 업데이트
참고: 더티 페이지는 체크포인트 중에 플러시되지 않음!
백그라운드 쓰기자에 의해 비동기적으로 플러시됨.
퍼지 체크포인트 내용:
<begin_checkpoint>
<end_checkpoint>
활성 트랜잭션 테이블:
┌─────────┬──────────┬────────────────┐
│ TxnID │ Status │ LastLSN │
├─────────┼──────────┼────────────────┤
│ T₁ │ Active │ LSN 150 │
│ T₂ │ Active │ LSN 180 │
└─────────┴──────────┴────────────────┘
더티 페이지 테이블:
┌─────────┬────────────────┐
│ PageID │ RecoveryLSN │
├─────────┼────────────────┤
│ Page 5 │ LSN 120 │
│ Page 12 │ LSN 145 │
│ Page 8 │ LSN 170 │
└─────────┴────────────────┘
퍼지 체크포인트의 장점: - 트랜잭션 중단 없음 - 체크포인트 중에 페이지 플러시 강제 없음 - 매우 빠른 체크포인트 연산 - 복구는 체크포인트 테이블을 사용하여 시작 지점 결정
6. ARIES 복구 알고리즘¶
6.1 개요¶
ARIES (Algorithm for Recovery and Isolation Exploiting Semantics)는 가장 널리 사용되는 복구 알고리즘이다. 1990년대 초 IBM Research의 C. Mohan 등이 개발했으며, DB2, SQL Server, PostgreSQL (적응됨) 및 많은 다른 시스템에서 복구의 기반이다.
핵심 원칙: 1. 선행 기록 로깅 (WAL): 데이터 전에 로그 2. 재실행 중 역사 반복: (커밋되지 않은 트랜잭션의 것을 포함한) 모든 작업을 재실행하여 충돌 전 정확한 상태 복원 3. 취소 중 변경사항 로깅: 보상 로그 레코드(CLR)를 취소 중에 기록하여 취소 연산이 멱등성을 보장
6.2 주요 데이터 구조¶
로그 시퀀스 번호(Log Sequence Number, LSN): 모든 로그 레코드는 고유하고 단조 증가하는 LSN을 가진다. 각 데이터 페이지는 그것을 수정한 가장 최근 로그 레코드의 LSN을 저장한다 (pageLSN).
로그 레코드:
┌───────┬──────┬────────┬────────────┬──────────┐
│ LSN │ TxnID│ Type │ PageID │ PrevLSN │
│ │ │(update,│ │(동일 │
│ │ │ commit,│ │ 트랜잭션의│
│ │ │ CLR,...│ │ 이전 │
│ │ │) │ │ 로그 레코드│
├───────┴──────┴────────┴────────────┴──────────┤
│ Before Image (오래된 값) │
│ After Image (새 값) │
│ UndoNextLSN (CLR만 해당) │
└────────────────────────────────────────────────┘
트랜잭션 테이블(Transaction Table, ATT - Active Transaction Table): 메모리에 유지. 각 활성 트랜잭션에 대해: - TransID - Status (활성, 커밋 중, 중단 중) - LastLSN: 이 트랜잭션의 가장 최근 로그 레코드의 LSN
더티 페이지 테이블(Dirty Page Table, DPT): 메모리에 유지. 버퍼 풀의 각 더티 페이지에 대해: - PageID - RecoveryLSN (recLSN): 마지막 플러시 이후 이 페이지를 더럽힌 첫 번째 로그 레코드의 LSN
트랜잭션 테이블: 더티 페이지 테이블:
┌─────────┬────────┬───────┐ ┌────────┬──────────┐
│ TransID │ Status │LastLSN│ │ PageID │ recLSN │
├─────────┼────────┼───────┤ ├────────┼──────────┤
│ T₁ │ active │ 45 │ │ P5 │ 20 │
│ T₂ │ active │ 60 │ │ P3 │ 35 │
│ T₃ │commit │ 55 │ │ P8 │ 42 │
└─────────┴────────┴───────┘ └────────┴──────────┘
6.3 보상 로그 레코드 (CLR)¶
CLR은 취소 단계 중에 기록되는 특별한 로그 레코드다. 이전 업데이트의 취소를 기록하고 다음 취소할 로그 레코드에 대한 포인터(UndoNextLSN)를 포함한다.
CLR 구조:
┌──────────────────────────────────────────────┐
│ LSN: 100 │
│ TransID: T₁ │
│ Type: CLR │
│ UndoNextLSN: 30 (← 다음 취소할 레코드) │
│ Redo info: "A를 오래된 값 50으로 복원" │
│ PrevLSN: 80 │
└──────────────────────────────────────────────┘
목적:
- 시스템이 복구 중(취소 중)에 충돌하면,
CLR은 동일한 연산을 두 번 취소하지 않도록 보장.
- CLR은 재실행 전용 (절대 그 자체로 취소되지 않음).
6.4 세 단계¶
ARIES 복구는 세 단계로 진행된다:
로그 타임라인:
─────────────────────────────────────────────────────────→
│ │ │ │
│ │ │ │
│ 체크포인트 시작 │ 충돌 │
│ │ │ │
│ │ 체크포인트 끝 │
│ │ │ │
│ ┌───────────┴──────────────┴────────────────────┤
│ │ │
│ │ ← 단계 1: 분석 → │
│ │ (체크포인트부터 순방향 스캔) │
│ │ │
│ │ ← 단계 2: 재실행 ──────────────────→ │
│ │ (DPT의 최소 recLSN부터, 순방향) │
│ │ │
│ │ ← 단계 3: 취소 ──────────────────→ │
│ │ (로그 끝부터, 역방향) │
│ │ │
6.5 단계 1: 분석¶
목표: 충돌 시점에 어떤 트랜잭션이 활성이었고 어떤 페이지가 더러웠는지 정확히 결정.
ANALYSIS-PHASE():
// 가장 최근 체크포인트부터 시작
Read <end_checkpoint ATT, DPT>
Initialize ATT and DPT from checkpoint data
// 체크포인트부터 로그 끝까지 순방향 스캔
for each log record in forward order:
if record is <T_i, start>:
Add T_i to ATT (status = active)
if record is an UPDATE or CLR:
if T_i not in ATT:
Add T_i to ATT
ATT[T_i].LastLSN = record.LSN
if record.PageID not in DPT:
Add page to DPT with recLSN = record.LSN
if record is <T_i, commit>:
ATT[T_i].status = committed
if record is <T_i, abort>:
ATT[T_i].status = aborting
if record is <T_i, end>:
Remove T_i from ATT
// 분석 후:
// ATT는 충돌 시점에 활성이었던 모든 트랜잭션을 포함
// DPT는 충돌 시점에 더러웠을 수 있는 모든 페이지를 포함
6.6 단계 2: 재실행 (역사 반복)¶
목표: 데이터베이스를 충돌 순간의 정확한 상태로 복원. 이는 커밋되지 않은 트랜잭션의 것을 포함한 모든 수정을 재실행하는 것을 포함한다.
REDO-PHASE():
// DPT의 가장 작은 recLSN부터 시작
start_LSN = min(recLSN for all pages in DPT)
// start_LSN부터 로그 끝까지 순방향 스캔
for each UPDATE or CLR log record with LSN ≥ start_LSN:
// 재실행을 건너뛸 세 가지 조건:
if record.PageID not in DPT:
skip (페이지가 깨끗함)
if DPT[record.PageID].recLSN > record.LSN:
skip (이 업데이트 후 페이지가 이미 플러시됨)
// 디스크에서 페이지 읽기
page = read_page(record.PageID)
if page.pageLSN ≥ record.LSN:
skip (이 업데이트가 페이지에 이미 반영됨)
// 재실행 적용
Apply the REDO information from the log record to the page
page.pageLSN = record.LSN
// 재실행 후:
// 데이터베이스가 정확한 충돌 전 상태에 있음
// (커밋되지 않은 트랜잭션의 효과 포함)
왜 커밋되지 않은 트랜잭션을 재실행하는가? ARIES는 취소 단계가 올바르게 작동하도록 "역사를 반복"한다. 취소 단계는 데이터베이스가 모든 트랜잭션의 모든 효과를 포함한 정확한 충돌 전 상태에 있다고 가정한다(커밋됨과 커밋 안 됨).
6.7 단계 3: 취소¶
목표: 충돌 시점에 활성이었던 (커밋되지 않은) 모든 트랜잭션을 롤백.
UNDO-PHASE():
// 취소 목록 구축: ATT의 모든 커밋되지 않은 트랜잭션
undo_list = {T_i in ATT where status != committed}
// 취소할 각 트랜잭션의 LastLSN 수집
// 로그 끝에서 역방향으로 처리
ToUndo = {ATT[T_i].LastLSN for T_i in undo_list}
while ToUndo is not empty:
// 가장 큰 LSN 선택 (가장 최근 연산)
lsn = max(ToUndo)
record = log_record at lsn
T_i = record.TransID
if record is a CLR:
// CLR은 취소되지 않음; UndoNextLSN을 따름
if record.UndoNextLSN != null:
replace lsn in ToUndo with record.UndoNextLSN
else:
// T_i의 모든 업데이트가 취소됨
Write <T_i, end> to log
Remove T_i's entries from ToUndo
else if record is an UPDATE:
// 이 업데이트를 취소
// 단계 1: CLR 쓰기
CLR = create_CLR(
TransID = T_i,
UndoNextLSN = record.PrevLSN,
Redo_info = "before-image로 복원"
)
Write CLR to log
// 단계 2: 취소 적용 (before-image 복원)
Restore the page to its before-image value
// 단계 3: 체인 따르기
if record.PrevLSN != null:
replace lsn in ToUndo with record.PrevLSN
else:
Write <T_i, end> to log
Remove T_i's entries from ToUndo
6.8 완전한 ARIES 예¶
충돌 시점의 로그:
LSN Record PrevLSN UndoNextLSN
─── ──────────────────────────────── ─────── ───────────
10 <T₁, start> -
20 <T₁, P5, A: 10→20> 10
30 <T₂, start> -
40 <T₂, P3, B: 30→40> 30
50 <begin_checkpoint>
55 <end_checkpoint ATT={T₁,T₂}, DPT={P5:20, P3:40}>
60 <T₂, P3, B: 40→50> 40
70 <T₃, start> -
80 <T₁, P5, C: 60→70> 20
90 <T₃, P8, D: 80→90> 70
100 <T₁, commit>
110 <T₃, P8, E: 15→25> 90
← 충돌
단계 1: 분석 (LSN 55부터 110까지 스캔)
LSN 60: T₂ update → ATT: T₂.LastLSN=60
LSN 70: T₃ start → ATT adds T₃
LSN 80: T₁ update → ATT: T₁.LastLSN=80, DPT: P5.recLSN stays 20
LSN 90: T₃ update → ATT: T₃.LastLSN=90, DPT adds P8.recLSN=90
LSN 100: T₁ commit → ATT: T₁.status=committed
LSN 110: T₃ update → ATT: T₃.LastLSN=110
결과:
ATT = {T₁(committed, LSN=80), T₂(active, LSN=60), T₃(active, LSN=110)}
DPT = {P5(recLSN=20), P3(recLSN=40), P8(recLSN=90)}
단계 2: 재실행 (최소 recLSN = 20부터)
LSN 20: P5 in DPT, recLSN=20 ≤ 20, pageLSN 확인 → 필요시 재실행
LSN 40: P3 in DPT, recLSN=40 ≤ 40, pageLSN 확인 → 필요시 재실행
LSN 60: P3 in DPT, recLSN=40 ≤ 60, pageLSN 확인 → 필요시 재실행
LSN 80: P5 in DPT, recLSN=20 ≤ 80, pageLSN 확인 → 필요시 재실행
LSN 90: P8 in DPT, recLSN=90 ≤ 90, pageLSN 확인 → 필요시 재실행
LSN 110: P8 in DPT, recLSN=90 ≤ 110, pageLSN 확인 → 필요시 재실행
단계 3: 취소 (커밋 안 됨: T₂, T₃)
ToUndo = {60 (T₂), 110 (T₃)}
단계 1: LSN 110 취소 (T₃, P8, E: 15→25)
CLR 쓰기: <LSN=120, T₃, CLR, UndoNext=90>
E를 15로 복원
ToUndo = {60, 90}
단계 2: LSN 90 취소 (T₃, P8, D: 80→90)
CLR 쓰기: <LSN=130, T₃, CLR, UndoNext=70>
D를 80으로 복원
ToUndo = {60, 70}
단계 3: LSN 70 취소 → <T₃, start>임
실제로 LSN 70은 start 레코드이므로, T₃가 70에서 PrevLSN 없음.
LSN 90의 PrevLSN을 통해 추적 → 70은 start.
<T₃, end> 쓰기
ToUndo = {60}
단계 4: LSN 60 취소 (T₂, P3, B: 40→50)
CLR 쓰기: <LSN=140, T₂, CLR, UndoNext=40>
B를 40으로 복원
ToUndo = {40}
단계 5: LSN 40 취소 (T₂, P3, B: 30→40)
CLR 쓰기: <LSN=150, T₂, CLR, UndoNext=30>
B를 30으로 복원
ToUndo = {30}
단계 6: LSN 30은 <T₂, start>
<T₂, end> 쓰기
ToUndo = {} → 완료
최종 상태:
T₁의 변경사항 지속 (커밋됨)
T₂의 변경사항 취소됨 (B가 30으로 복원)
T₃의 변경사항 취소됨 (D가 80으로, E가 15로 복원)
6.9 복구 중 충돌¶
ARIES의 주요 강점 중 하나는 복구 중 충돌을 처리하는 것이다:
시나리오: 취소 단계 중에 시스템 충돌
원래 충돌 복구가 단계 3 (취소)에 있었음:
LSN 110 취소 → CLR 썼음 (LSN 120)
LSN 90 취소 → CLR 썼음 (LSN 130)
← 다시 충돌
두 번째 복구:
단계 1 (분석): T₂, T₃가 여전히 활성임을 발견
단계 2 (재실행): LSN 120, 130의 CLR을 포함한 모든 로그 레코드 재실행
(이것이 취소 연산을 재적용)
단계 3 (취소): T₃에 대해, LSN 130의 CLR을 따름 → UndoNextLSN = 70
T₃의 LSN 110과 90은 다시 취소되지 않음 (CLR이 건너뜀)
CLR 체인이 취소 연산이 멱등임을 보장.
복구가 몇 번 충돌하든 작업이 반복되지 않음.
7. 그림자 페이징¶
7.1 개념¶
그림자 페이징(Shadow Paging)은 WAL 기반 복구의 대안이다. 변경사항을 로깅하는 대신, 두 페이지 테이블을 유지한다:
그림자 페이징:
현재 페이지 테이블: 그림자 페이지 테이블 (디스크에):
┌───┬──────┐ ┌───┬──────┐
│ 1 │ → P1'│ (수정됨) │ 1 │ → P1 │ (원본)
│ 2 │ → P2 │ (변경 없음) │ 2 │ → P2 │
│ 3 │ → P3'│ (수정됨) │ 3 │ → P3 │ (원본)
│ 4 │ → P4 │ (변경 없음) │ 4 │ → P4 │
└───┴──────┘ └───┴──────┘
커밋 시: 그림자 페이지 테이블을 현재 페이지 테이블로 교체
중단 시: 현재 페이지 테이블을 버리고, 그림자로 되돌림
7.2 그림자 페이징 vs. WAL¶
| 측면 | 그림자 페이징 | WAL (ARIES) |
|---|---|---|
| 복구 속도 | 빠름 (그냥 그림자 사용) | 느림 (세 단계) |
| 정상 작동 | 느림 (copy-on-write) | 빠름 (제자리 업데이트) |
| 동시 트랜잭션 | 지원 어려움 | 자연스럽게 지원 |
| 단편화 | 높음 (흩어진 페이지) | 낮음 |
| 커밋 오버헤드 | 원자적 페이지 테이블 교환 | 로그 플러시 |
| 사용처 | SQLite (저널 모드), CouchDB | PostgreSQL, MySQL, Oracle, SQL Server |
그림자 페이징은 동시 트랜잭션에 대한 나쁜 지원과 페이지 단편화 때문에 현대 다중 사용자 데이터베이스 시스템에서 거의 사용되지 않는다.
8. 버퍼 관리 정책¶
8.1 네 가지 조합¶
버퍼 관리 정책은 더티 페이지가 디스크에 기록될 때를 결정한다:
Steal 정책: 버퍼 관리자가 커밋되지 않은 트랜잭션에 속한 더티 페이지를 플러시할 수 있는가? - Steal: 예, 더티 페이지가 언제든지 플러시될 수 있음 (취소 능력 필요) - No-Steal: 아니오, 커밋되지 않은 트랜잭션의 더티 페이지는 버퍼에 유지됨
Force 정책: 트랜잭션의 모든 더티 페이지가 커밋 시점에 디스크로 플러시되어야 하는가? - Force: 예, 커밋 시 모든 더티 페이지 플러시 (충돌 후 재실행 불필요) - No-Force: 아니오, 더티 페이지가 나중에 플러시될 수 있음 (재실행 능력 필요)
│ No-Steal │ Steal
───────────┼───────────────────┼─────────────────────
Force │ 취소 없음, 재실행│ 취소, 재실행 없음
│ 없음 (가장 단순한│ (실제로 드묾)
│ 복구이지만 │
│ 최악의 성능) │
───────────┼───────────────────┼─────────────────────
No-Force │ 취소 없음, 재실행│ 취소와 재실행
│ (지연 수정) │ (ARIES, 대부분 시스템)
│ │ ← 최고 성능
8.2 Steal/No-Force가 최고인 이유¶
Steal 장점:
Steal 없이: 커밋되지 않은 트랜잭션의 더티 페이지가 버퍼에 유지되어야 함
→ 큰 트랜잭션이 버퍼 풀을 고갈시킬 수 있음
→ 동시 트랜잭션 수 제한
Steal과 함께: 버퍼 관리자가 필요에 따라 어떤 페이지든 축출 가능
→ 더 나은 버퍼 활용
→ 더 큰 트랜잭션 지원
No-Force 장점:
No-force 없이: 커밋 시 모든 더티 페이지 플러시
→ 매우 느린 커밋 (특히 많은 페이지가 수정된 경우)
→ 랜덤 I/O 패턴 (흩어진 더티 페이지)
No-force와 함께: 백그라운드 쓰기자가 지연하여 페이지 플러시
→ 빠른 커밋 (로그만 플러시)
→ 로그에 대한 순차 I/O
→ 페이지가 배치로 플러시됨 (더 효율적)
8.3 복구와의 상호작용¶
| 정책 | 취소 필요? | 재실행 필요? | 설명 |
|---|---|---|---|
| Steal | 예 | - | 축출된 페이지가 디스크에 커밋되지 않은 데이터를 가질 수 있음 |
| No-Steal | 아니오 | - | 커밋되지 않은 데이터가 절대 디스크에 도달하지 않음 |
| Force | - | 아니오 | 모든 커밋된 데이터가 커밋 시 디스크에 있음 |
| No-Force | - | 예 | 커밋된 데이터가 여전히 버퍼에만 있을 수 있음 |
ARIES는 Steal + No-Force를 사용하며, 이는 취소와 재실행 능력 모두를 필요로 하지만 -- 최고의 런타임 성능을 제공한다.
9. 미디어 복구와 백업 전략¶
9.1 백업의 필요성¶
WAL 기반 복구는 트랜잭션 실패와 시스템 충돌을 처리하지만, 미디어 실패(디스크 파괴)는 처리하지 않는다. 미디어 복구를 위해 백업이 필요하다.
미디어 복구:
시간: ─────────────────────────────────────────────→
│ │ │ │
전체 백업 │ 증분 디스크 실패
B₁ │ 백업 B₂ ↓
│ │ B₁ 복원
로그 계속 │ B₂ 적용
│ │ B₂부터 실패까지 로그 재생
│ │ ↓
│ │ 데이터베이스 복구됨!
9.2 백업 유형¶
전체 백업 (기본 백업): - 전체 데이터베이스의 완전한 복사본 - 자체 포함: 이것만으로 복원 가능 - 큰 크기, 시간 소모 - 일반적으로 주간 또는 월간 수행
증분 백업: - 마지막 백업 이후 변경된 페이지/블록만 - 전체 백업보다 작고 빠름 - 복원하려면 기본 백업 + 모든 증분 필요 - 일반적으로 매일 수행
연속 아카이빙 (WAL 아카이빙): - WAL 세그먼트를 완료되면 아카이브 - 기본 백업과 결합하여 시점 복구(Point-in-Time Recovery, PITR) 허용
PostgreSQL 백업 전략:
┌─────────────────────────────────────────────────────┐
│ 일요일: 전체 기본 백업 (pg_basebackup) │
│ 월-토: 연속 WAL 아카이빙 │
│ │
│ 수요일 오후 3:00로 복원하려면: │
│ 1. 일요일 기본 백업 복원 │
│ 2. 일요일부터 수요일 오후 3:00까지 WAL 재생 │
│ 3. 데이터베이스가 수요일 오후 3:00의 정확한 상태에 │
└─────────────────────────────────────────────────────┘
9.3 PostgreSQL 백업 명령어¶
# 전체 기본 백업
pg_basebackup -D /backup/base -Ft -z -P
# WAL 아카이빙 구성 (postgresql.conf에서)
# archive_mode = on
# archive_command = 'cp %p /backup/wal/%f'
# 시점 복구
# 1. PostgreSQL 중지
# 2. 기본 백업을 데이터 디렉토리로 복원
# 3. recovery.signal 파일 생성
# 4. postgresql.conf에서 restore_command와 recovery_target_time 설정
# restore_command = 'cp /backup/wal/%f %p'
# recovery_target_time = '2025-03-15 15:00:00'
# 5. PostgreSQL 시작 → 대상 시간으로 자동 복구
9.4 논리적 vs. 물리적 백업¶
물리적 백업 (pg_basebackup):
┌────────────────────────────────────────────┐
│ 원시 데이터 파일 복사 (바이너리) │
│ + 빠른 백업 및 복원 │
│ + WAL 아카이빙과 함께 PITR 지원 │
│ - 동일한 PostgreSQL 주 버전 필요 │
│ - 동일한 아키텍처 (OS, 엔디언) │
└────────────────────────────────────────────┘
논리적 백업 (pg_dump):
┌────────────────────────────────────────────┐
│ SQL 문이나 사용자 지정 형식 내보내기 │
│ + 버전 독립적 │
│ + 다른 아키텍처로 복원 가능 │
│ + 선택적으로 테이블 복원 가능 │
│ - 느린 백업 및 복원 │
│ - PITR 없음 (시점 스냅샷만) │
└────────────────────────────────────────────┘
9.5 백업 모범 사례¶
3-2-1 규칙:
3 데이터 복사본 (원본 + 2 백업)
2 다른 저장 미디어 (디스크 + 테이프/클라우드)
1 오프사이트 복사본 (다른 물리적 위치)
추가 지침:
- 정기적으로 복원 테스트 (테스트되지 않은 백업 = 백업 없음)
- WAL 아카이빙 모니터링 (갭은 데이터 손실 위험을 의미)
- 보존 정책: 규제 기간 동안 백업 보관
- 백업 암호화 (특히 오프사이트)
- 복구 절차 문서화
10. 현대 시스템의 복구¶
10.1 복구와 복제¶
현대 시스템은 종종 복구와 복제(Replication)를 결합하여 내구성과 고가용성 모두를 위해:
동기 복제:
주 ──WAL──→ 대기
│ │
│ 대기가 WAL 수신 확인 후에만
│ 커밋
│ │
▼ ▼
데이터 파일 데이터 파일
주가 실패하면:
대기가 이미 모든 커밋된 WAL을 가짐
대기를 주로 승격 (초 단위)
데이터 손실 없음 (RPO = 0)
10.2 성능 고려사항¶
복구 시간 구성요소:
┌────────────────────────────────────────┐
│ 분석 단계: 빠름 (로그를 한 번 스캔) │
│ 시간: 마지막 체크포인트 이후 │
│ 로그에 비례 │
├────────────────────────────────────────┤
│ 재실행 단계: 느릴 수 있음 │
│ 시간: DPT의 min(recLSN) 이후 │
│ 로그 레코드 수에 비례 │
│ 최적화: 페이지별 병렬 재실행 │
├────────────────────────────────────────┤
│ 취소 단계: 보통 빠름 │
│ 시간: 커밋되지 않은 트랜잭션의 │
│ 업데이트 수에 비례 │
│ 참고: 지연 가능 (지연 취소) │
└────────────────────────────────────────┘
복구 시간을 최소화하려면:
- 빈번한 체크포인트 (재실행할 것이 적음)
- 짧은 트랜잭션 (취소할 것이 적음)
- 충분한 WAL 버퍼 (강제 플러시 감소)
10.3 병렬 복구¶
현대 시스템은 병렬로 재실행을 수행한다:
병렬 재실행:
로그 레코드: [P1, P3, P1, P2, P3, P1, P2, ...]
스레드 1 (페이지 P1): [LSN1, LSN3, LSN6, ...] 재실행
스레드 2 (페이지 P2): [LSN4, LSN7, ...] 재실행
스레드 3 (페이지 P3): [LSN2, LSN5, ...] 재실행
동일한 페이지의 레코드는 순서대로 적용되어야 함.
다른 페이지의 레코드는 병렬로 적용 가능.
11. 연습문제¶
개념적 질문¶
연습문제 1: 다음 각 실패를 분류하고 적절한 복구 조치를 설명하라: (a) 트랜잭션이 0으로 나눔 (b) 정전 발생 (c) 디스크 헤드 충돌, 데이터 디스크 파괴 (d) 데드락 감지됨 (e) 운영 체제 커널 패닉
연습문제 2: 휘발성, 비휘발성, 안정 저장소의 차이를 설명하라. 왜 "진정한" 안정 저장소가 실제로 달성 불가능한가? 실제 시스템은 어떻게 근사하는가?
연습문제 3: WAL (선행 기록 로깅) 프로토콜이 대응하는 데이터 페이지 전에 로그 레코드를 안정 저장소로 플러시하도록 요구하는 이유를 설명하라. 데이터 페이지가 먼저 플러시되면 무엇이 잘못될 수 있는가?
로그 기반 복구¶
연습문제 4: 다음 로그가 주어졌다 (체크포인트 없음):
LSN Record
1 <T₁, start>
2 <T₁, A, 10, 20>
3 <T₂, start>
4 <T₂, B, 30, 40>
5 <T₁, C, 50, 60>
6 <T₁, commit>
7 <T₂, D, 70, 80>
8 <T₃, start>
9 <T₃, A, 20, 30>
10 <T₃, commit>
← 충돌
(a) 어떤 트랜잭션을 재실행해야 하는가? 어떤 것을 취소해야 하는가? (b) 복구 후 A, B, C, D의 최종 값은? (c) 완전한 복구 과정을 단계별로 보여라.
연습문제 5: 연습문제 4를 반복하되, LSN 6과 LSN 7 사이에 체크포인트 <checkpoint {T₂}>가 삽입된 경우. 체크포인트가 복구 과정을 어떻게 변경하는가?
ARIES 알고리즘¶
연습문제 6: 퍼지 체크포인트가 있는 다음 로그가 주어졌다:
LSN Record PrevLSN
10 <T₁, start> -
20 <T₁, P1, X: 5→10> 10
30 <T₂, start> -
40 <T₂, P2, Y: 15→25> 30
50 <begin_checkpoint>
55 <end_checkpoint ATT={T₁(20),T₂(40)}, DPT={P1:20, P2:40}>
60 <T₁, P3, Z: 35→45> 20
70 <T₂, commit>
80 <T₃, start> -
90 <T₃, P1, X: 10→20> 80
100 <T₁, P2, W: 50→60> 60
← 충돌
(a) 분석 단계를 수행하라. 최종 ATT와 DPT를 보여라. (b) 재실행 단계의 시작 LSN을 결정하라. (c) 재실행 단계를 수행하라. 재실행된 로그 레코드를 나열하라 (모든 페이지가 재실행 필요하다고 가정). (d) 취소 단계를 수행하라. 각 단계에서 기록된 모든 CLR과 최종 ToUndo 집합을 보여라. (e) 복구 후 X, Y, Z, W의 최종 값은?
연습문제 7: ARIES가 재실행 단계 중 "역사를 반복"하는 이유를 설명하라 (즉, 커밋되지 않은 트랜잭션의 업데이트도 재실행). 왜 커밋된 것만 재실행하고 커밋되지 않은 트랜잭션의 업데이트는 건너뛰지 않는가?
연습문제 8: 시스템이 ARIES 복구의 취소 단계 중에 충돌한다. 두 번째 충돌 시점에, 일부 (전부는 아닌) 취소 연산에 대한 CLR이 기록되었다. 두 번째 복구 시도 중 단계별로 무슨 일이 일어나는지 설명하라. 왜 작업이 반복되지 않는가?
버퍼 관리¶
연습문제 9: steal/no-steal과 force/no-force 정책의 각 조합에 대해 설명하라: (a) 취소 능력이 필요한지와 그 이유 (b) 재실행 능력이 필요한지와 그 이유 (c) 런타임 성능에 미치는 영향 (커밋 지연시간, 버퍼 활용) (d) 이 조합이 적절한 시스템이나 컨텍스트
연습문제 10: 트랜잭션 T가 1000개 페이지를 수정한다. force 정책 하에서, 모든 1000개 페이지가 커밋 시점에 디스크로 플러시되어야 한다. no-force 정책 하에서, 로그 레코드만 (~10 KB) 플러시하면 된다. 각 페이지 플러시가 5ms (랜덤 I/O) 걸리고 로그 플러시가 2ms (순차 I/O) 걸린다면:
(a) force vs. no-force 정책 하의 커밋 지연시간 계산 (b) 시스템이 초당 100개의 그런 트랜잭션을 처리하면, 각 정책 하의 I/O 대역폭 요구사항은? (c) 왜 거의 모든 현대 데이터베이스 시스템이 no-force를 사용하는가?
백업과 미디어 복구¶
연습문제 11: PostgreSQL 데이터베이스가 다음 백업 일정을 가진다: - 매주 일요일 오전 2:00에 전체 기본 백업 - 연속 WAL 아카이빙
수요일 오후 4:00에, 주니어 DBA가 실수로 DROP TABLE orders;를 실행하고 커밋한다. 오류가 오후 5:00에 발견된다. 수요일 오후 3:59 (DROP TABLE 1분 전)로 데이터베이스를 복원하는 단계별 시점 복구 과정을 설명하라.
연습문제 12: 다음을 가진 시스템에서 세 실패 시나리오에 대한 복구 시간 목표를 비교하라: - 5분마다 체크포인트 - 매주 기본 백업 - WAL 연속 아카이브 - 동기 대기 복제본
(a) 트랜잭션 실패 (단일 트랜잭션 중단): 예상 복구 시간? (b) 시스템 실패 (서버 충돌): 예상 복구 시간? (c) 디스크 실패 (주 데이터 디스크 파괴됨): 예상 복구 시간? 대기를 승격하는 것과 백업에서 복원하는 것의 차이는?
이전: 동시성 제어 | 다음: NoSQL와 NewSQL