(KO) CUBRID Checkpoint — 퍼지 ARIES 체크포인트 프로토콜, 액티브 트랜잭션 스냅샷, 그리고 복구 anchor LSA
목차
학술적 배경
섹션 제목: “학술적 배경”체크포인트는 데이터베이스 엔진이 복구 매니저와 맺는 휴전이다. 주기적으로 로그에 박히는 레코드 한 줄이며, 그 위치가 다음 재시작 이 해야 할 작업량을 묶어 둔다. 체크포인트가 없으면 재시작의 analysis 패스는 로그 처음부터 — 지금까지 쓴 모든 레코드 — 를 걸어야 한다. 엔진이 죽었을 때 어떤 트랜잭션이 active였고 어떤 dirty 페이지가 home에 도달하지 못했는지를 다른 방식으로는 알 수 없기 때문이다. 체크포인트가 있으면 엔진은 자기 자신에 대한 지식의 한 조각을 주기적으로 로그에 commit한다. “이 LSA 시점에 in-flight 트랜잭션 집합은 이러하고, 메모리에 dirty인 페이지의 가장 작은 LSA는 이러하다. 복구는 그 LSA에서 안전하게 analysis 패스를 시작 할 수 있다. Database Internals(Petrov, 5장 §Recovery”)이 이를 재시작 시간을 엔진 가동 시간이 아니라 묶인 값으로 만드는 핵심 도구라고 자리매김한다.
체크포인트 프로토콜이 만족해야 할 계약은 ARIES 논문(Mohan 외, TODS 17.1, 1992)이 명시했다. 논문은 단일 축에 두 체크포인트 패밀리를 갈라 둔다 — 체크포인트가 도는 동안 엔진이 얼마나 멈춰야 하는가.
- Sharp / consistent / quiescent 체크포인트. in-flight 트랜잭션을 모두 비우고, dirty 페이지를 모두 플러시한 뒤, 체크포인트 레코드 하나를 쓴다. 잡힌 상태는 일관되어 있다. 비용은 모든 체크포인트가 버퍼 풀 플러시 시간 동안 사용자 트랜잭션을 동결한다는 점이다. Bernstein/Hadzilacos/Goodman(6 장 §Checkpointing)이 교과서 변종이라고 묘사하지만, 처리량 타격이 너무 커서 운영 엔진은 거의 쓰지 않는다.
- Fuzzy / non-blocking 체크포인트. 액티브 트랜잭션 집합과
dirty 페이지 집합의 스냅샷을 동결 없이 잡고, 두 개의 brackets
레코드(ARIES 용어로
begin-CHKPT과end-CHKPT) 사이에 로그에 쓰고, 트래픽을 계속 받는다. 잡힌 상태는 내부적으로 일관되지 않다 — bracket 사이에 트랜잭션이 commit·abort하고 새 페이지가 dirty가 된다. 그러나 ARIES은 analysis 패스가 그 윈도우 안의 레코드를 end-CHKPT 이후의 레코드와 똑같이 다루면 이 비일관성을 견딘다는 사실을 증명한다.
CUBRID은 fuzzy 변종을 고른다. ARIES은 정확성을 세 가지 성질로 프레이밍한다.
- Coverage. end-CHKPT 시점에 active인 모든 트랜잭션은 스냅샷에 나타나거나, end-CHKPT 이후의 레코드에서 활동이 보여야 한다. ARIES은 trantable을 read 모드 lock 아래에서 걸어 이를 보장한다.
- Anchoring. redo-LSA 힌트는 어떤 dirty 페이지의 LSA보다도 작거나 같아야 한다. 그 아래에 있는 것은 디스크에 있다고 증명 되며, 그 위는 있을 수도 없을 수도 있다.
- Restartability. 체크포인트 도중의 크래시가 복구 경계를 망치
지 않아야 한다. ARIES은 체크포인트를 완전히 로그 상주로 만들어
이를 달성한다 — 인메모리 포인터(
chkpt_lsa)는 두 bracket 레코드 가 모두 durable해진 뒤에야 전진한다.
두 단계(begin-CHKPT / end-CHKPT) 패턴이 이 성질들의 ARIES적 표현 이다. 이 문서의 나머지는 CUBRID이 이를 어떻게 구현하는지 천천히 줌인하는 작업이다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”WAL 기반 엔진은 거의 다 fuzzy 체크포인트 프로토콜을 구현한다.
이름은 다르지만 메커니즘은 놀랍도록 안정적이다. 이 절은 공유
어휘를 명명해서 ## CUBRID의 구현의 CUBRID 특화 심볼이 익숙한
모양에 맞물리도록 한다.
주기적 타이머 또는 임계 트리거
섹션 제목: “주기적 타이머 또는 임계 트리거”체크포인트는 wall-clock 타이머 또는 이전 체크포인트 이후 로그가
지정 용량을 소비했을 때 발사된다. 두 정책은 보통 결합한다. 시간
기반 트리거는 조용한 시간이 길어진 뒤의 복구 시간을 묶어 두고,
공간 기반 트리거는 바쁜 시간 뒤의 복구 시간을 묶어 둔다.
PostgreSQL은 checkpoint_timeout + max_wal_size을 노출하고,
CUBRID은 log_checkpoint_interval(시간)과 log_checkpoint_size
(로그 페이지 카운트)을 노출한다.
액티브 트랜잭션 스냅샷
섹션 제목: “액티브 트랜잭션 스냅샷”체크포인트는 in-flight 트랜잭션마다 한 항목을 발행한다. 복구가
체크포인트 레코드 하나만으로 트랜잭션 테이블을 다시 부트스트랩
하기에 충분한 상태를 담는다. 최소는 (trid, state, head_lsa, tail_lsa)이고, state가 loser(active, undo 대상)와 in-doubt(2PC
prepared, 살려 둠)와 committed-with-postpone(postpone 재생 필요)
을 가른다. 대부분의 엔진은 undo_nxlsa과 savepoint LSA도 잡는다.
Dirty 페이지 스냅샷 — 또는 그것을 흡수하는 redo-LSA 힌트
섹션 제목: “Dirty 페이지 스냅샷 — 또는 그것을 흡수하는 redo-LSA 힌트”두 갈래 설계점. 첫째, 일부 엔진은 dirty 페이지 테이블 전체 —
(volid, pageid, recovery_lsa) 트리플 — 을 발행해 복구가 DPT를
직접 재구성하게 한다. ARIES 논문 방식. 단점은 바쁜 엔진이 MB 단위
의 레코드를 발행한다는 점. 둘째, 대부분의 모던 엔진은 DPT를 단일
redo-LSA 스칼라(가장 작은 recovery_lsa)로 압축한다. 복구는
그 LSA에서 forward로 걸어 DPT를 인라인으로 재구성한다. CUBRID은
압축된 접근을 따른다 — LOG_REC_CHKPT.redo_lsa은 단일 LSA다.
Bracket 레코드 — begin-CHKPT과 end-CHKPT
섹션 제목: “Bracket 레코드 — begin-CHKPT과 end-CHKPT”체크포인트는 begin-CHKPT(다음 analysis가 시작할 LSA를 표시)과
end-CHKPT(스냅샷을 실어 나름)을 발행한다. 그 사이에 평범한
트랜잭션이 로그 레코드를 만들어 내고, ARIES은 bracket 윈도우 안의
레코드가 마치 end-CHKPT 이후인 듯 analysis가 올바로 처리한다는
것을 증명한다.
페이지 버퍼 플러시 조정과 헤더 durability
섹션 제목: “페이지 버퍼 플러시 조정과 헤더 durability”체크포인트는 다음 redo-LSA가 전진할 수 있을 만큼 dirty 페이지를
디스크로 몰아 낸다 — 그렇지 않으면 redo-LSA가 핀에 박혀 복구
작업량이 계속 자란다. PostgreSQL의 CheckPointBuffers, InnoDB의
flush-list 워커, CUBRID의 pgbuf_flush_checkpoint 모두 같은 역할
을 한다. 복구는 active 로그 헤더를 읽어 체크포인트 LSA를 찾는다.
헤더에는 그래서 체크포인트 레코드가 durable해진 뒤에야 전진하는
chkpt_lsa 필드가 있다.
비교 풍경
섹션 제목: “비교 풍경”| 엔진 | 헤더 / 포인터 | 트리거 | Dirty 페이지 힌트 |
|---|---|---|---|
| PostgreSQL | pg_control.checkPoint | checkpoint_timeout + max_wal_size | redoLSN 스칼라; runningXacts 사이드 레코드 |
| InnoDB | 로그 파일 헤더 | log-bytes-since-last + dirty-pages-pct | 페이지별 oldest_modification 추적 |
| Oracle | controlfile SCN | redo 로그 크기 + 수동 체크포인트 | Mean Time To Recover 타겟 |
| SQL Server | bootpage dbi_checkptLSN | recovery-interval 타겟 | dirty-page LSN 워터마크 |
| CUBRID | log_Gl.hdr.chkpt_lsa | log_checkpoint_interval(타이머) | LOG_REC_CHKPT.redo_lsa 스칼라 |
모두 fuzzy다. CUBRID은 주류 안에 있고, 식별점은 명시적인 두 bracket 레코드(ARIES 논문에 충실)와 타이머 전용 트리거다.
이론 ↔ CUBRID 매핑
섹션 제목: “이론 ↔ CUBRID 매핑”| 이론적 개념 | CUBRID 이름 |
|---|---|
| Fuzzy 체크포인트 데몬 | log_Checkpoint_daemon(log_checkpoint_daemon_init이 생성) |
| 데몬 period(타이머) | log_get_checkpoint_interval이 PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS을 읽음 |
| 데몬 본체 | log_checkpoint_execute → logpb_checkpoint |
| Begin-CHKPT 로그 레코드 | LOG_START_CHKPT(= 25) |
| End-CHKPT 로그 레코드 | LOG_END_CHKPT(= 26) |
| End-CHKPT 페이로드 | LOG_REC_CHKPT { redo_lsa, ntrans, ntops } |
| 트랜잭션별 스냅샷 행 | LOG_INFO_CHKPT_TRANS { trid, state, head_lsa, tail_lsa, undo_nxlsa, posp_nxlsa, savept_lsa, ... } |
| 액티브 sysop별 스냅샷 행 | LOG_INFO_CHKPT_SYSOP { trid, sysop_start_postpone_lsa, atomic_sysop_start_lsa } |
| 로그 헤더의 복구 anchor LSA | log_Gl.hdr.chkpt_lsa |
| 마지막 redo-LSA의 인메모리 사본 | log_Gl.chkpt_redo_lsa |
| 아카이브 제거 워터마크 | log_Gl.hdr.smallest_lsa_at_last_chkpt |
chkpt_lsa과 chkpt_redo_lsa을 보호하는 mutex | log_Gl.chkpt_lsa_lock |
| 액티브 트랜잭션 walk | for i in trantable.num_total_indices: logpb_checkpoint_trans(...) |
| 액티브 sysop walk | logpb_checkpoint_topops(두 번 호출 — user trans과 system trans) |
| 페이지 버퍼 플러시 도우미 | pgbuf_flush_checkpoint (flush_upto, prev_redo, *out_redo, *out_npages) |
| 로그 헤더 강제 디스크 쓰기 | logpb_flush_header |
| WAL 페이지 강제 디스크 쓰기 | logpb_flush_pages_direct |
| 모든 볼륨 파일시스템 fsync | fileio_synchronize_all(DWB이 안에서 협력) |
| 체크포인트를 소비하는 재시작 진입점 | log_recovery → log_recovery_analysis (start_lsa = log_Gl.hdr.chkpt_lsa) |
| chkpt 레코드의 레코드별 analysis arm | log_rv_analysis_start_checkpoint, log_rv_analysis_end_checkpoint |
LOG_ISCHECKPOINT_TIME 매크로 | 페이지-카운트 기반 술어(legacy 경로) — log_manager.c |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID 체크포인트 서브시스템에는 여섯 개의 움직이는 부품이 있다.
데몬 등록이 체크포인트 스레드를 타이머에 깨운다. preflight 단계
가 기존 dirty 페이지를 플러시하고 LOG_START_CHKPT을 발행한다.
액티브 트랜잭션 스냅샷이 read 모드 lock 아래에서 trantable을
걸어 잡힌다. 액티브 sysop 스냅샷이 commit-postpone 안의 nested
top-operation에 한해 잡힌다. end-CHKPT 발행이 스냅샷을
LOG_REC_CHKPT과 트레일링 배열에 패킹한다. 마지막으로
헤더 갱신 + fsync이 새 chkpt_lsa을 publish해 다음 재시작이
거기 anchor를 박게 한다. 위 순서로 걸어 본 뒤, 복구 측 시각
(log_recovery_analysis이 무엇을 소비하는가)과 더블 라이트 버퍼
와의 협력으로 마무리한다.
데몬 등록
섹션 제목: “데몬 등록”log_Checkpoint_daemon은 log_manager.c의 파일 스코프에 선언된
cubthread::daemon이다.
// log_Checkpoint_daemon — src/transaction/log_manager.cstatic cubthread::daemon *log_Checkpoint_daemon = NULL;서버 시작 시점에 글로벌 로그 데몬 부트스트랩(log_daemons_init)이
호출하는 log_checkpoint_daemon_init이 데몬을 만든다. 데몬의 tick
period는 PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS 시스템 파라미터에서
온다. looper은 매 tick마다 그 파라미터를 다시 읽는 콜백을 묶어
두므로 db_change_active_log_arg 류 호출로 period가 런타임에 다시
튜닝될 수 있다.
// log_checkpoint_daemon_init — src/transaction/log_manager.cREGISTER_DAEMON (log_checkpoint);
voidlog_checkpoint_daemon_init (){ assert (log_Checkpoint_daemon == NULL);
cubthread::looper looper = cubthread::looper (log_get_checkpoint_interval); cubthread::entry_callable_task *daemon_task = new cubthread::entry_callable_task (log_checkpoint_execute);
log_Checkpoint_daemon = cubthread::get_manager ()->create_daemon (looper, daemon_task, "log-checkpoint");}
// log_get_checkpoint_interval — src/transaction/log_manager.cvoidlog_get_checkpoint_interval (bool & is_timed_wait, cubthread::delta_time & period){ int log_checkpoint_interval_sec = prm_get_integer_value (PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS); assert (log_checkpoint_interval_sec >= 0);
if (log_checkpoint_interval_sec > 0) { is_timed_wait = true; period = std::chrono::seconds (log_checkpoint_interval_sec); } else { // infinite wait — checkpoint disabled until someone wakes it explicitly is_timed_wait = false; }}기본은 360초(6분)이며, system_parameter.c에서
PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS이 기본값 360로 선언된 것
으로 확인된다. 파라미터를 0으로 두면 타이머가 비활성화된다 —
데몬은 그 뒤로 누군가 log_wakeup_checkpoint_daemon을 부를 때까지
무한 대기한다. 사용자 측 wakeup 경로는 ad-hoc cubrid_check 요청
과, 재시작이 끝난 뒤 log_recovery 안의
(void) logpb_checkpoint (thread_p) 호출 — 엔진은 재시작의 마지막
행위로 새 체크포인트를 잡아 다음 크래시가 깨끗한 경계에서 시작
하게 한다 — 가 쓴다.
데몬 본체는 얇은 래퍼다.
// log_checkpoint_execute — src/transaction/log_manager.cstatic voidlog_checkpoint_execute (cubthread::entry & thread_ref){ if (!BO_IS_SERVER_RESTARTED ()) { // wait for boot to finish — if we ran during analysis the trantable // would not be populated yet return; }
logpb_checkpoint (&thread_ref);}BO_IS_SERVER_RESTARTED은 부트가 완전히 끝났을 때(세 패스 재시작
복구 — analysis → redo → undo — 와 모든 postpone 재생까지 포함)
flip되는 boolean이다. 그 플래그가 true가 되기 전까지 데몬은 도는
일을 거부한다. 재시작 도중 체크포인트를 잡으면 복구가 트랜잭션
테이블을 변경하는 와중의 트랜잭션 테이블을 기록하게 된다.
legacy 페이지-카운트 트리거가 매크로로 보존되어 있다.
// LOG_ISCHECKPOINT_TIME — src/transaction/log_manager.c#define LOG_ISCHECKPOINT_TIME() \ (log_Gl.rcv_phase == LOG_RESTARTED \ && log_Gl.run_nxchkpt_atpageid != NULL_PAGEID \ && log_Gl.hdr.append_lsa.pageid >= log_Gl.run_nxchkpt_atpageid)run_nxchkpt_atpageid은 프로세스별 카운터이며, logpb_checkpoint
이 끝날 때마다 chkpt_every_npages(기본 100000 로그 페이지,
PRM_ID_LOG_NBUFFERS 이상으로 클램프)만큼 전진시킨다. 의도는 로그
appender이 append마다 폴해서 인라인 체크포인트를 트리거하는 것
이었으나, 모던 코드 경로에서는 데몬 주도 타이머가 지배하고 매크로
는 belt-and-braces 방어로 남아 있다.
최상위 흐름 — logpb_checkpoint
섹션 제목: “최상위 흐름 — logpb_checkpoint”sequenceDiagram participant T as log-checkpoint daemon participant CHK as logpb_checkpoint participant LOG as log append participant TT as trantable participant PB as page buffer participant DWB as DWB participant FS as filesystem participant HDR as log header page Note over T: timer tick (PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS) T->>CHK: log_checkpoint_execute CHK->>LOG: logpb_flush_pages_direct (drain in-flight WAL) CHK->>LOG: prior_lsa_alloc_and_copy_data(LOG_START_CHKPT) Note right of CHK: newchkpt_lsa = LSA of begin record CHK->>PB: pgbuf_flush_checkpoint(newchkpt_lsa, prev_redo, &out_redo, &nflushed) PB->>DWB: dwb_flush_force (page-by-page) DWB->>FS: fsync DWB volume DWB->>FS: write home pages PB-->>CHK: tmp_chkpt.redo_lsa = smallest dirty LSA remaining CHK->>FS: fileio_synchronize_all (force all data volumes) CHK->>TT: TR_TABLE_CS_ENTER (read mode) CHK->>TT: walk trantable, build LOG_INFO_CHKPT_TRANS[] CHK->>TT: walk trantable, build LOG_INFO_CHKPT_SYSOP[] CHK->>LOG: prior_lsa_alloc_and_copy_data(LOG_END_CHKPT, packed_arrays) CHK->>TT: TR_TABLE_CS_EXIT CHK->>LOG: logpb_flush_pages_direct (force end record) CHK->>HDR: log_Gl.hdr.chkpt_lsa = newchkpt_lsa CHK->>HDR: log_Gl.hdr.smallest_lsa_at_last_chkpt = smallest_head_lsa CHK->>LOG: logpb_flush_header CHK->>FS: write checkpoint LSA into every volume's disk header
logpb_checkpoint의 번호화된 구조(대략 log_page_buffer.c:6877
부터 :7300까지).
LOG_CS_ENTER. 로그 구조 위에 배타적(prior list mutex가 그 안쪽에 있다).- 복구가 끝나지 않았으면 거절.
if (BO_IS_SERVER_RESTARTED () && log_Gl.run_nxchkpt_atpageid == NULL_PAGEID) return;—NULL_PAGEIDsentinel은 한 번에 한 체크포인트만의 가드 역할 도 한다. - 이전 chkpt_lsa을
chkpt_lsa_lock아래 스냅샷.log_get_db_start_parameters류 reader가 로그 CS을 잡지 않게 해 준다. - In-flight WAL drain.
logpb_flush_pages_direct로 begin-CHKPT 이전 모든 레코드가 durable해야 한다. LOG_START_CHKPT발행. 빈 레코드. 그 LSA가newchkpt_lsa으로 잡혀 다음 재시작의 analysis 시작점이 된다.logtb_reflect_global_unique_stats_to_btree. CUBRID의 btree 는 캐시된 unique 카운터를 쥐고 있고, redo-LSA가 그것을 지나 전진하기 전에 카탈로그 btree에 플러시되어야 한다.pgbuf_flush_checkpoint(newchkpt_lsa, …).oldest_unflush_lsa <= newchkpt_lsa인 모든 BCB을 골라 DWB로 플러시한다. 남은 가장 작은 LSA가tmp_chkpt.redo_lsa로 돌아온다 — redo-LSA 힌트.fileio_synchronize_all. 모든 데이터 볼륨에fsync(2).- trantable을
TR_TABLE_CS_ENTER(read) 아래에서 walk.0..trantable.num_total_indices을 iterate. system 트랜잭션은 skip(체크포인트가 그 위에서 돈다). 살아 있는LOG_TDES마다 한 줄의logpb_checkpoint_trans행이 된다. TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE안의 액티브 sysop을 위해 다시 walk —logpb_checkpoint_topops경유.LOG_END_CHKPT발행.prior_lsa_alloc_and_copy_data이LOG_REC_CHKPT을data_header로, 두 트레일링 배열을 페이로드로 잡는다.- end 레코드 강제 — 또 한 번의
logpb_flush_pages_direct. chkpt_lsa_lock아래에서 인메모리 포인터 갱신:log_Gl.hdr.chkpt_lsa = newchkpt_lsa,log_Gl.chkpt_redo_lsa = tmp_chkpt.redo_lsa,log_Gl.hdr.smallest_lsa_at_last_chkpt← 가장 작은 트랜잭션head_lsa(아카이브 제거 워터마크).logpb_flush_header. active 로그 헤더 페이지를 쓴다 — 이 fsync 뒤에 경계가 durable해진다.- 모든 볼륨의 디스크 헤더에 스탬프 — 미디어 복구를 위해(백업
에서 재시작 시
log_rv_find_checkpoint이 읽음).
체크포인트 자체의 두 단계 commit 패턴은 명명할 가치가 있다.
체크포인트는 end-CHKPT 레코드가 durable해질 때가 아니라 로그
헤더가 durable해질 때 효력을 갖는다. 12단계와 14단계 사이에
크래시가 나면 두 체크포인트 레코드가 모두 로그에 있지만 인메모리
헤더 포인터는 아직 디스크에 없는 상태가 된다. 다음 재시작이
디스크 헤더를 읽고 이전 chkpt_lsa을 찾아 거기서 analysis를
돌린다. 새 bracket 레코드는 그 analysis 도중에 등장해 평범한
레코드처럼 처리된다. 그 레코드들이 analysis가 필요로 하는 정보를
나르지 않으므로 (이전 체크포인트가 이미 그 시점이 말할 모든 것을
덮었고 그 이상이다) 사실상 무시된다 — log_rv_analysis_end_checkpoint
arm은 *may_use_checkpoint == false일 때 short-circuit하는데, start
레코드의 LSA가 analysis 시작 LSA과 일치하지 않을 때가 그 경우다.
정확성은 한 체크포인트 윈도우만큼의 재처리 비용으로 보존된다.
액티브 트랜잭션 스냅샷 — logpb_checkpoint_trans
섹션 제목: “액티브 트랜잭션 스냅샷 — logpb_checkpoint_trans”TDES별 추출기는 그 자리에서 통째로 읽을 만큼 짧다.
// logpb_checkpoint_trans — src/transaction/log_page_buffer.cvoidlogpb_checkpoint_trans (LOG_INFO_CHKPT_TRANS * chkpt_entries, log_tdes * tdes, int &ntrans, int &ntops, LOG_LSA & smallest_lsa){ LOG_INFO_CHKPT_TRANS *chkpt_entry = &chkpt_entries[ntrans];
if (tdes != NULL && tdes->trid != NULL_TRANID && !tdes->tail_lsa.is_null () && tdes->commit_abort_lsa.is_null ()) { chkpt_entry->isloose_end = tdes->isloose_end; chkpt_entry->trid = tdes->trid; chkpt_entry->state = tdes->state; LSA_COPY (&chkpt_entry->head_lsa, &tdes->head_lsa); LSA_COPY (&chkpt_entry->tail_lsa, &tdes->tail_lsa);
if (chkpt_entry->state == TRAN_UNACTIVE_ABORTED) { /* Transaction is in the middle of an abort, since rollback is * not run in a critical section. Set the undo point to be the * same as its tail. The recovery process will read the last * record which is likely a compensating one, and find where to * continue a rollback operation. */ LSA_COPY (&chkpt_entry->undo_nxlsa, &tdes->tail_lsa); } else { LSA_COPY (&chkpt_entry->undo_nxlsa, &tdes->undo_nxlsa); }
LSA_COPY (&chkpt_entry->posp_nxlsa, &tdes->posp_nxlsa); LSA_COPY (&chkpt_entry->savept_lsa, &tdes->savept_lsa); LSA_COPY (&chkpt_entry->tail_topresult_lsa, &tdes->tail_topresult_lsa); LSA_COPY (&chkpt_entry->start_postpone_lsa, &tdes->rcv.tran_start_postpone_lsa); strncpy (chkpt_entry->user_name, tdes->client.get_db_user (), LOG_USERNAME_MAX); ntrans++;
if (tdes->topops.last >= 0 && (tdes->state == TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE)) { ntops += tdes->topops.last + 1; }
if (LSA_ISNULL (&smallest_lsa) || LSA_GT (&smallest_lsa, &tdes->head_lsa)) { LSA_COPY (&smallest_lsa, &tdes->head_lsa); } }}세 가지를 표시하고 싶다. 첫째, 적격성 검사가 세 부류를 거른다.
null trantable 슬롯, 아직 아무것도 로깅하지 않은 트랜잭션
(tail_lsa.is_null ()), 그리고 commit/abort 레코드를 이미
append해 둔 트랜잭션(commit_abort_lsa.is_null ()이 false).
세 번째 검사가 눈여겨볼 부분이다. commit 레코드가 prior list에는
있지만 아직 drain되지 않은 트랜잭션은 trantable 관점에서 여전히
in-flight로 카운트된다. 그러나 commit 레코드가 이미 append되어
있다면 복구는 그것을 보고 스냅샷 항목 없이 해결할 수 있다. 소스
주석이 명시적이다 — “commit 또는 abort 로그 레코드가 append된
한, 실제 트랜잭션 상태는 체크포인트 메커니즘에 의해 무시된다.”
둘째, TRAN_UNACTIVE_ABORTED 경로는 undo_nxlsa = tail_lsa을
강제한다. CUBRID의 rollback은 critical section을 들고 있지 않
으므로, 체크포인트가 rollback 도중에 떨어질 수 있다. 스냅샷은
복구가 재개할 위치여야 하고, 체인의 꼬리(가장 최근 완료된 undo
단계의 CLR일 가능성이 높다)가 안전한 랑데부다. 셋째,
smallest_lsa 누산기는 log_Gl.hdr.smallest_lsa_at_last_chkpt을
갱신하기 위해 루프 바깥에서 쓰이는 워터마크다. 이는 analysis
패스가 쓰는 redo-LSA가 아니다 — 아카이브 보존 워터마크다. 이
LSA 아래 페이지만으로 구성된 로그 아카이브는 크래시 복구에 필요
하지 않으므로, 아카이브 제거가 이 워터마크에 게이트된다.
액티브 sysop 스냅샷 — logpb_checkpoint_topops
섹션 제목: “액티브 sysop 스냅샷 — logpb_checkpoint_topops”CUBRID의 sysop(ARIES의 미니 트랜잭션과 동격인 nested top-operation)은 복구의 postpone 패스가 재생할 부수효과가 있을 때만 스냅샷되어야 한다. 추출기의 적격성 검사는 이렇다.
// logpb_checkpoint_topops — src/transaction/log_page_buffer.c (excerpt)if (tdes != NULL && tdes->trid != NULL_TRANID && (!LSA_ISNULL (&tdes->rcv.sysop_start_postpone_lsa) || !LSA_ISNULL (&tdes->rcv.atomic_sysop_start_lsa))) { /* this transaction is running system operation postpone or an * atomic system operation * note: we cannot compare tdes->state with * TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE. we are * not synchronizing setting transaction state. * however, setting tdes->rcv.sysop_start_postpone_lsa is * protected by log_Gl.prior_info.prior_lsa_mutex. so we * check this instead of state. */ ... LOG_INFO_CHKPT_SYSOP *chkpt_topop = &chkpt_topops[ntops]; chkpt_topop->trid = tdes->trid; chkpt_topop->sysop_start_postpone_lsa = tdes->rcv.sysop_start_postpone_lsa; chkpt_topop->atomic_sysop_start_lsa = tdes->rcv.atomic_sysop_start_lsa; ntops++; }주석이 짐을 받친다. 자연스러운 적격성 술어는
tdes->state == TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE이지
만, tdes->state은 mutex 없이 변경된다(writer가 낙관적으로 갱신
하고 정확성은 레코드별 로그 항목에 의지한다). 정작 mutex로
보호되는 것은 이 트랜잭션이 sysop-postpone에 들어갔다를 기록
하는 LSA다. sysop_start_postpone_lsa 세팅이 prior_lsa_mutex을
요구한다. 체크포인트는 walk 시점에 이미 prior_lsa_mutex을 잡고
있으므로(walk 도중 새 prior list 노드가 생기지 않게 하려고 mutex
를 잡았다) LSA 검사가 안전한 대체재다.
잡힌 LOG_INFO_CHKPT_SYSOP은 단 세 필드만 있다 — trid,
sysop_start_postpone_lsa, atomic_sysop_start_lsa. 복구의
analysis 패스는 이 셋을 써서 재구성된 TDES의
tdes->rcv.sysop_start_postpone_lsa을 seed하고,
log_recovery_finish_all_postpone 도중의 postpone 재생을 구동한다.
ntops 카운터에는 미묘한 상호작용이 있다. 첫 trantable walk이
TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE 트랜잭션을 세서
ntops을 계산한다. 두 번째 walk은 위 적격성 검사로 실제 ntops를
재유도한다. 두 카운트가 갈릴 수 있다 — 두 walk 사이에 트랜잭션이
상태를 바꿀 수 있다 — 그리고 두 번째 walk이 이긴다.
length_all_tops 버퍼는 두 번째 walk의 진행 카운트가 첫 walk의
추정을 넘으면 logpb_checkpoint_topops 안에서 재할당된다.
end 레코드 — LOG_REC_CHKPT과 트레일링 배열
섹션 제목: “end 레코드 — LOG_REC_CHKPT과 트레일링 배열”on-log 모양.
// LOG_REC_CHKPT — src/transaction/log_record.hpptypedef struct log_rec_chkpt LOG_REC_CHKPT;struct log_rec_chkpt{ LOG_LSA redo_lsa; /* Oldest LSA of dirty data page in page buffers */ int ntrans; /* Number of active transactions */ int ntops; /* Total number of system operations */};
/* Transaction descriptor */typedef struct log_info_chkpt_trans LOG_INFO_CHKPT_TRANS;struct log_info_chkpt_trans{ int isloose_end; TRANID trid; /* Transaction identifier */ TRAN_STATE state; /* Transaction state (e.g., Active, aborted) */ LOG_LSA head_lsa; /* First log address of transaction */ LOG_LSA tail_lsa; /* Last log record address of transaction */ LOG_LSA undo_nxlsa; /* Next log record address for UNDO purposes */ LOG_LSA posp_nxlsa; /* First address of a postpone record */ LOG_LSA savept_lsa; /* Address of last savepoint */ LOG_LSA tail_topresult_lsa; /* Address of last partial abort/commit */ LOG_LSA start_postpone_lsa; /* Address of start postpone */ char user_name[LOG_USERNAME_MAX];};
typedef struct log_info_chkpt_sysop LOG_INFO_CHKPT_SYSOP;struct log_info_chkpt_sysop{ TRANID trid; LOG_LSA sysop_start_postpone_lsa; LOG_LSA atomic_sysop_start_lsa;};end-CHKPT 레코드의 디스크 위 레이아웃은 따라서 다음과 같다.
| LOG_RECORD_HEADER (back/forw/trid/type=LOG_END_CHKPT) || LOG_REC_CHKPT { redo_lsa, ntrans, ntops } || LOG_INFO_CHKPT_TRANS [ntrans] || LOG_INFO_CHKPT_SYSOP [ntops] |표시할 만한 두 구현 디테일. 첫째, 두 배열은 별도로 힙 할당되고
(malloc(ntrans * sizeof(...))과 topops도 같은 식), prior-list
할당기에 그것들을 append된 레코드로 복사하라고 요청한다.
// from logpb_checkpoint, end-CHKPT emissionnode = prior_lsa_alloc_and_copy_data ( thread_p, LOG_END_CHKPT, RV_NOT_DEFINED, NULL, length_all_chkpt_trans, (char *) chkpt_trans, (int) length_all_tops, (char *) chkpt_topops);
chkpt = (LOG_REC_CHKPT *) node->data_header;*chkpt = tmp_chkpt;
prior_lsa_next_record_with_lock (thread_p, node, tdes);prior_lsa_alloc_and_copy_data의 두 페이로드 오버로드가 두 배열을
받아 data_header 뒤에 이어 붙인다. 헤더 자체는 할당 이후
node->data_header을 가로질러 쓰는 식으로 채워진다. 둘째, 레코드
는 RV_NOT_DEFINED을 복구 인덱스로 쓴다. 체크포인트 레코드는 redo
함수도 undo 함수도 없다 — 순전히 정보성이며 analysis 패스가
소비한다. RV_fun[RV_NOT_DEFINED] 슬롯은 디버그 전용 dump 항목
이고, 체크포인트 레코드를 redo / undo하려고 시도하면 assert이
실패한다.
복구 상호작용 — analysis가 chkpt_lsa에 anchor를 박는다
섹션 제목: “복구 상호작용 — analysis가 chkpt_lsa에 anchor를 박는다”log_recovery_analysis의 시작 LSA는 log_recovery 안에서 가장
처음 행위로 세팅된다.
// log_recovery — src/transaction/log_recovery.c (excerpt)LSA_COPY (&rcv_lsa, &log_Gl.hdr.chkpt_lsa);
if (ismedia_crash != false) { /* Media crash, we may have to start from an older checkpoint... */ (void) fileio_map_mounted ( thread_p, (bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint, &rcv_lsa); }크래시 vs 미디어의 갈래는 의미가 있다. 크래시 복구의 경우
chkpt_lsa이 정확히 analysis가 시작해야 할 LSA다 — 그 이전의
모든 레코드는 이전 체크포인트가 이미 소비했고 더 이상 필요하지
않다. 미디어 복구의 경우(백업에서 재시작) 볼륨별 디스크 헤더에
글로벌 chkpt_lsa보다 앞선 rcv-LSA가 있을 수 있다. 백업이 다른
시점에 잡혔기 때문이다. 루프는 마운트된 모든 볼륨을 walk하며
가장 작은 rcv-LSA를 취한다.
두 체크포인트 레코드를 위한 analysis 패스의 arm.
// log_rv_analysis_start_checkpoint — src/transaction/log_recovery.cstatic intlog_rv_analysis_start_checkpoint (LOG_LSA * log_lsa, LOG_LSA * start_lsa, bool * may_use_checkpoint){ /* Use the checkpoint record only if it is the first record in the * analysis. */ if (LSA_EQ (log_lsa, start_lsa)) { *may_use_checkpoint = true; } return NO_ERROR;}
// log_rv_analysis_end_checkpoint — src/transaction/log_recovery.c (sketch)if (*may_use_checkpoint == false) return NO_ERROR;*may_use_checkpoint = false;
LSA_COPY (check_point, log_lsa);/* read LOG_REC_CHKPT header, then the trans + topops trailing arrays */...for (i = 0; i < chkpt.ntrans; i++) { tdes = logtb_rv_find_allocate_tran_index (thread_p, chkpt_trans[i].trid, log_lsa); logtb_clear_tdes (thread_p, tdes);
if (chkpt_one->state == TRAN_ACTIVE || chkpt_one->state == TRAN_UNACTIVE_ABORTED) tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; else tdes->state = chkpt_one->state;
LSA_COPY (&tdes->head_lsa, &chkpt_one->head_lsa); LSA_COPY (&tdes->tail_lsa, &chkpt_one->tail_lsa); LSA_COPY (&tdes->undo_nxlsa, &chkpt_one->undo_nxlsa); LSA_COPY (&tdes->posp_nxlsa, &chkpt_one->posp_nxlsa); /* ...savept_lsa, tail_topresult_lsa, tran_start_postpone_lsa... */
if (LOG_ISTRAN_2PC (tdes)) *may_need_synch_checkpoint_2pc = true; }세 가지 관찰. 첫째, 적격성 게이트 if (LSA_EQ (log_lsa, start_lsa))
이 첫 체크포인트만 소비되도록 강제한다. analysis 윈도우 안의
이후 체크포인트 레코드는 skip된다 — 평범한 로그 트래픽으로 존재
하지만 스냅샷이 stale하다. 둘째, 스냅샷의 TRAN_ACTIVE과
TRAN_UNACTIVE_ABORTED은 TRAN_UNACTIVE_UNILATERALLY_ABORTED로
강제 변환된다 — 복구는 여전히 active인 모든 트랜잭션을 loser로
다룬다. 2PC-prepared 상태는 그대로 유지되어 in-doubt 경로가 찾을
수 있게 한다. 셋째, start_redo_lsa이 chkpt.redo_lsa에서 세팅
된다. 이것이 log_recovery_redo이 walk하기 시작하는 하한이 된다.
복구 경계 다이어그램
섹션 제목: “복구 경계 다이어그램”flowchart LR
subgraph LOG["WAL on disk"]
direction LR
OLD["...older records..."] --> SC["LOG_START_CHKPT @ chkpt_lsa"]
SC --> M1["LOG_UNDOREDO_DATA"]
M1 --> M2["LOG_COMMIT (T17)"]
M2 --> EC["LOG_END_CHKPT (snapshot, redo_lsa=R)"]
EC --> P1["LOG_UNDOREDO_DATA"]
P1 --> P2["LOG_MVCC_REDO_DATA"]
P2 --> EOF["LOG_END_OF_LOG (crash here)"]
end
subgraph HDR["log header (pageid -9)"]
HC["chkpt_lsa = SC.lsa"]
HS["smallest_lsa_at_last_chkpt"]
end
subgraph PASS["analysis pass"]
A1["start_lsa = chkpt_lsa"]
A2["walk forward"]
A3["seed TT/DPT from end-CHKPT"]
A4["redo_lsa = chkpt.redo_lsa"]
end
HDR -.->|read at restart| A1
A1 --> SC
SC -.->|"start: may_use_chkpt=true"| A2
EC -.->|"consume snapshot"| A3
A3 --> A4
A4 -.->|"redo from R (may be < SC.lsa)"| LOG
다이어그램이 redo와 analysis의 갈래를 가시화한다. analysis는
chkpt_lsa에서 시작한다(start-CHKPT 레코드) 하지만 redo는
chkpt.redo_lsa에서 시작한다(end-CHKPT 레코드의 가장 작은-dirty
LSA 힌트). 둘은 보통 같은 LSA지만 항상 그렇지는 않다 — 페이지
버퍼 항목에 체크포인트보다 앞선 oldest_unflush_lsa(오래된 dirty
페이지)이 있다면 redo_lsa이 chkpt_lsa보다 앞일 수 있다. 그
경우 redo는 analysis 시작에서 backward로 걸어가며, 대상 페이지의
디스크 LSA가 레코드 LSA보다 작을 때만 각 레코드를 적용한다.
페이지 버퍼와 DWB와의 협력
섹션 제목: “페이지 버퍼와 DWB와의 협력”pgbuf_flush_checkpoint이 7단계가 호출하는 페이지 버퍼 진입점
이다. 핵심 본체.
// pgbuf_flush_checkpoint — src/storage/page_buffer.c (sketch)intpgbuf_flush_checkpoint (THREAD_ENTRY *thread_p, const LOG_LSA *flush_upto_lsa, const LOG_LSA *prev_chkpt_redo_lsa, LOG_LSA *smallest_lsa, int *flushed_page_cnt){ /* Things must be truly flushed up to this lsa */ logpb_flush_log_for_wal (thread_p, flush_upto_lsa); LSA_SET_NULL (smallest_lsa);
for (bufid = 0; bufid < pgbuf_Pool.num_buffers; bufid++) { bufptr = PGBUF_FIND_BCB_PTR (bufid); PGBUF_BCB_LOCK (bufptr);
/* skip non-dirty, post-window-dirty, temp-volume BCBs */ if (!pgbuf_bcb_is_dirty (bufptr) || LSA_GT (&bufptr->oldest_unflush_lsa, flush_upto_lsa) || pgbuf_is_temporary_volume (bufptr->vpid.volid)) { PGBUF_BCB_UNLOCK (bufptr); continue; }
/* defensive invariant: oldest_unflush_lsa must not predate * the previous checkpoint's redo-LSA */ if (LSA_LT (&bufptr->oldest_unflush_lsa, prev_chkpt_redo_lsa)) { er_set (...ER_LOG_CHECKPOINT_SKIP_INVALID_PAGE...); assert (false); }
/* enqueue for flush, sorted by VPID */ f_list[collected_bcbs++].bufptr = bufptr; ... } /* drain via pgbuf_flush_chkpt_seq_list → DWB → home volume */}세 가지가 중요하다. 첫째, 첫 호출 logpb_flush_log_for_wal이
WAL 순서를 강제한다 — begin-CHKPT까지의 모든 레코드
(flush_upto_lsa = newchkpt_lsa)가 어떤 데이터 페이지 쓰기보다
먼저 force된다. 둘째, begin-CHKPT 이후에 dirty가 된 페이지는
의도적으로 skip된다 — 플러시해도 다음 redo-LSA를 유용하게 전진
시키지 못하고, in-flight 트랜잭션과 간섭만 한다. 셋째,
prev_chkpt_redo_lsa 불변식이 이전 체크포인트의 잘못된 redo-LSA
를 단언으로 잡는다.
실제 쓰기 경로는 DWB을 거친다. BCB은 VPID로 정렬되어
pgbuf_flush_chkpt_seq_list로 넘어가고, 함수는 각 페이지를
dwb_add_page로 구동해서 DWB 플러시 데몬이 home 페이지보다
먼저 staging 슬롯을 쓰게 한다. 엔진이 체크포인트 도중 죽더라도
home 쓰기 도중의 모든 페이지는 디스크에 완전히 있거나(클린)
재시작의 pre-redo DWB scan 동안 자기 DWB 슬롯에서 복구 가능
하다. cubrid-double-write-buffer.md 참조.
flowchart LR
subgraph CHK["logpb_checkpoint"]
direction TB
SC["1) emit LOG_START_CHKPT"]
PF["2) pgbuf_flush_checkpoint(newchkpt_lsa)"]
FA["3) fileio_synchronize_all"]
EC["4) emit LOG_END_CHKPT"]
HDR["5) flush log header"]
end
subgraph PB["page buffer"]
BCBS["BCBs with oldest_unflush_lsa <= newchkpt_lsa"]
end
subgraph DWBP["DWB"]
SLOT["staged page in DWB slot"]
HOME["home volume page"]
end
subgraph FS["filesystem"]
DV["data volumes"]
LV["log file"]
HV["log header page"]
end
SC --> LV
PF --> BCBS
BCBS -->|dwb_add_page| SLOT
SLOT -->|fsync DWB volume| DV
SLOT -->|then write home| HOME
HOME --> DV
PF --> FA
FA -->|fsync each volume| DV
EC --> LV
HDR --> HV
HV -.->|"chkpt_lsa = newchkpt_lsa"| LV
실패 사례
섹션 제목: “실패 사례”프로토콜은 어떤 시점에 크래시가 나도 안전하게 설계되어 있다.
- begin-CHKPT 이전. 영향 없음. 이전 체크포인트가 그대로 경계.
- begin-CHKPT과 end-CHKPT 사이, 또는 end-CHKPT과 헤더 플러시
사이. 디스크 위의
log_Gl.hdr.chkpt_lsa은 여전히 이전 체크포인트 를 가리킨다(헤더가 가장 마지막에 쓰인다). analysis는 이전 chkpt_lsa에서 돌면서, 부분 새 bracket 레코드를 평범한 로그 트래픽 으로 만난다. begin-CHKPT arm의 적격성 게이트 (LSA_EQ(log_lsa, start_lsa))이 실패한다 — analysis가 새 begin 레코드에서 시작하지 않았으므로 — 그래서 arm이 no-op을 하고 부분 bracket이 무해해진다. 비용은 한 체크포인트 윈도우만큼의 재처리. - 헤더 플러시 이후. 새
chkpt_lsa이 durable해졌다. analysis가 새 bracket에서 시작하고 게이트가 올바로 발화한다. - 체크포인트 누락(새로 설치).
chkpt_lsa이NULL_LSA. analysis 가 로그 처음부터 walk한다. 느리지만 정확. - 체크포인트 LSA가 durable end-of-log을 넘어감. 헤더 손상 —
치명적(
logpb_fatal_error), 백업에서 복원. - dirty 페이지 플러시 도중(7단계). DWB이 torn 페이지를 막는다 —
home 쓰기 도중인 페이지는 redo가 돌기 전
dwb_load_and_recover_pages에서 자기 DWB 슬롯으로 복원된다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”데몬 등록과 타이밍
섹션 제목: “데몬 등록과 타이밍”log_Checkpoint_daemon(log_manager.c) — 파일 스코프cubthread::daemon포인터.log_checkpoint_daemon_init(log_manager.c) — 서버 시작 시 데몬을 만든다.log_get_checkpoint_interval(log_manager.c) — looper period 를 위해PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS을 읽는다.log_checkpoint_execute(log_manager.c) — 데몬 본체.logpb_checkpoint로 위임.log_wakeup_checkpoint_daemon(log_manager.c) — out-of-band wakeup 훅.log_daemons_init/log_daemons_destroy(log_manager.c) — 부트스트랩과 teardown.LOG_ISCHECKPOINT_TIME매크로(log_manager.c) — 페이지 카운트 기반 legacy 트리거.
체크포인트 발행
섹션 제목: “체크포인트 발행”logpb_checkpoint(log_page_buffer.c) — orchestrator.logpb_checkpoint_trans(log_page_buffer.c) — TDES별 추출기.logpb_checkpoint_topops(log_page_buffer.c) — 액티브 sysop별 추출기.logpb_dump_checkpoint_trans(log_page_buffer.c) —cubrid logdump용 디버그 dumper.log_dump_record_checkpoint(log_manager.c) — 체크포인트 레코드 dumping의 최상위 디스패처.log_dump_checkpoint_topops(log_manager.c) — 액티브 sysop 배열 디버그 dumper.prior_lsa_alloc_and_copy_data(log_append.cpp) — 모든 로그 appender이 공유. 두 bracket 레코드 모두 사용.prior_lsa_next_record_with_lock(log_append.cpp) — LSA 부여 와 prior list 노드 link.
페이지 버퍼 협력
섹션 제목: “페이지 버퍼 협력”pgbuf_flush_checkpoint(page_buffer.c) — dirty BCB 선택, VPID 정렬, DWB 구동.pgbuf_flush_chkpt_seq_list(page_buffer.c) — 한 배치의 실제 플러시.pgbuf_Pool.is_checkpoint(page_buffer.c) — 페이지 버퍼 flusher가 동시 victim과 조정하기 위해 읽는 atomic 플래그.logpb_flush_log_for_wal(log_page_buffer.c) —pgbuf_flush_checkpoint이 호출하는 WAL 순서 강제.fileio_synchronize_all(file_io.c) — dirty 페이지 플러시 후 모든 볼륨 fsync.dwb_flush_force(double_write_buffer.cpp) — pending DWB 블록 강제.fileio_synchronize_all이 전이적으로 호출.
복구 측 소비
섹션 제목: “복구 측 소비”log_recovery(log_recovery.c) —rcv_lsa = log_Gl.hdr.chkpt_lsa을 analysis 시작점으로 세팅.log_rv_find_checkpoint(log_recovery.c) — 미디어 복구 도중 쓰는 볼륨별 rcv-LSA scan.log_recovery_analysis(log_recovery.c) —chkpt_lsa에서 forward walk.log_rv_analysis_record(log_recovery.c) —LOG_RECTYPE위 switch.LOG_START_CHKPT과LOG_END_CHKPTarm.log_rv_analysis_start_checkpoint(log_recovery.c) — start 레코드의 LSA가 analysis 시작과 일치하면may_use_checkpoint세팅.log_rv_analysis_end_checkpoint(log_recovery.c) —LOG_REC_CHKPT, trans 배열, topops 배열을 읽고 trantable을 seed하고start_redo_lsa을 세팅.logtb_rv_find_allocate_tran_index(log_tran_table.c) — trans 배열의 행마다 trid 키로 TDES 슬롯을 할당.logtb_clear_tdes(log_tran_table.c) — 스냅샷에서 다시 채우기 전에 TDES을 zero-out.
헤더와 인메모리 포인터
섹션 제목: “헤더와 인메모리 포인터”log_Gl.hdr.chkpt_lsa(log_storage.hpp의LOG_HEADER::chkpt_lsa필드) — 디스크 위 복구 anchor.log_Gl.hdr.smallest_lsa_at_last_chkpt(log_storage.hpp) — 아카이브 제거 워터마크.log_Gl.chkpt_redo_lsa(log_impl.h::log_global) — 마지막 end-CHKPT의 redo_lsa의 인메모리 사본.pgbuf_flush_checkpoint이prev_chkpt_redo_lsa로 사용.log_Gl.chkpt_lsa_lock(log_impl.h::log_global) — 위 두 LSA 를 보호하는 pthread mutex.log_Gl.run_nxchkpt_atpageid/log_Gl.chkpt_every_npages(log_impl.h::log_global) — legacy 페이지-카운트 트리거 상태.logpb_flush_header(log_page_buffer.c) — active 로그 헤더 페이지를 디스크에 쓴다.
로그 레코드 타입과 페이로드
섹션 제목: “로그 레코드 타입과 페이로드”LOG_START_CHKPT(log_record.hpp, 값 25) — begin 마커.LOG_END_CHKPT(log_record.hpp, 값 26) — 스냅샷을 나르는 end 레코드.LOG_REC_CHKPT(log_record.hpp) —{ redo_lsa, ntrans, ntops }헤더.LOG_INFO_CHKPT_TRANS(log_record.hpp) — 트랜잭션별 행.LOG_INFO_CHKPT_SYSOP(log_record.hpp) — 액티브 sysop별 행.
시스템 파라미터
섹션 제목: “시스템 파라미터”PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS(system_parameter.c, 기본 360 s, deprecated) — 타이머 period.PRM_ID_LOG_CHECKPOINT_INTERVAL(system_parameter.c, 기본 360 s, 대체) — 같은 역할, 다른 단위 처리.PRM_ID_LOG_CHECKPOINT_NPAGES(system_parameter.c, 기본 100000, deprecated) — 페이지 카운트 트리거.PRM_ID_LOG_CHECKPOINT_SIZE(system_parameter.c, 기본 100000, 대체) — 크기 기반 등가물.PRM_ID_LOG_CHECKPOINT_SLEEP_MSECS(system_parameter.c, 기본 1 ms, hidden) — 페이지 간 플러시 throttle.PRM_ID_LOG_CHKPT_DETAILED(system_parameter.c) —logpb_checkpoint안의detailed_er_logtrace 활성화.
위치 힌트 (2026-05-01 기준)
섹션 제목: “위치 힌트 (2026-05-01 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
log_Checkpoint_daemon | log_manager.c | 359 |
log_get_checkpoint_interval | log_manager.c | 10075 |
log_wakeup_checkpoint_daemon | log_manager.c | 10113 |
log_checkpoint_execute | log_manager.c | 10167 |
log_checkpoint_daemon_init | log_manager.c | 10407 |
LOG_ISCHECKPOINT_TIME 매크로 | log_manager.c | 122 |
log_dump_checkpoint_topops | log_manager.c | 6769 |
log_dump_record_checkpoint | log_manager.c | 6792 |
logpb_checkpoint_trans | log_page_buffer.c | 6783 |
logpb_checkpoint_topops | log_page_buffer.c | 6833 |
logpb_checkpoint | log_page_buffer.c | 6877 |
logpb_dump_checkpoint_trans | log_page_buffer.c | 7395 |
log_rv_find_checkpoint | log_recovery.c | 579 |
log_rv_analysis_start_checkpoint | log_recovery.c | 1797 |
log_rv_analysis_end_checkpoint | log_recovery.c | 1830 |
log_rv_analysis_record (LOG_*_CHKPT arms) | log_recovery.c | 2436 |
log_recovery (chkpt_lsa anchor) | log_recovery.c | 780 |
LOG_REC_CHKPT struct | log_record.hpp | 345 |
LOG_INFO_CHKPT_TRANS struct | log_record.hpp | 354 |
LOG_INFO_CHKPT_SYSOP struct | log_record.hpp | 372 |
LOG_START_CHKPT enum | log_record.hpp | 96 |
LOG_END_CHKPT enum | log_record.hpp | 97 |
LOG_HEADER::chkpt_lsa | log_storage.hpp | 141 |
LOG_HEADER::smallest_lsa_at_last_chkpt | log_storage.hpp | 163 |
log_global::chkpt_lsa_lock | log_impl.h | 681 |
log_global::chkpt_redo_lsa | log_impl.h | 683 |
log_global::chkpt_every_npages | log_impl.h | 684 |
log_global::run_nxchkpt_atpageid | log_impl.h | 678 |
pgbuf_flush_checkpoint | page_buffer.c | 3960 |
PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS | system_parameter.c | 1368 |
PRM_ID_LOG_CHECKPOINT_INTERVAL | system_parameter.c | 1379 |
PRM_ID_LOG_CHECKPOINT_NPAGES | system_parameter.c | 1346 |
PRM_ID_LOG_CHECKPOINT_SIZE | system_parameter.c | 1357 |
Cross-check 노트
섹션 제목: “Cross-check 노트”vs cubrid-recovery-manager.md
섹션 제목: “vs cubrid-recovery-manager.md”- bracket 레코드는
LOG_START_CHKPT/LOG_END_CHKPT. 두 문서가 같다 — ARIES 두 단계에 충실하다. log_Gl.hdr.chkpt_lsa은 analysis anchor지 redo anchor가 아니 다. redo anchor(start_redo_lsa)는LOG_REC_CHKPT.redo_lsa에서 유도된다. 둘은 보통 일치하지만 오래된 dirty 페이지가 있으면 갈라질 수 있다.- 2PC: 복구 매니저 문서가
LOG_RECOVERY_FINISH_2PC_PHASE을 조건부 단계로 나열한다. 트리거는 스냅샷이TRAN_2PC_PREPARED행을 담을 때log_rv_analysis_end_checkpoint안에서*may_need_synch_checkpoint_2pc = true로 세팅된다. log_recovery끝의 재시작 후 마지막 체크포인트 호출은 클린 shutdown 경로와 같다.
vs cubrid-log-manager.md
섹션 제목: “vs cubrid-log-manager.md”체크포인트 레코드는 다른 모든 appender과 동일한 prior list 규율 위에서 흐른다.
- end-CHKPT 레코드는
prior_lsa_alloc_and_copy_data의 두 페이로드 오버로드로 트레일링 페이로드를 나른다. - 체크포인트는 group-commit waiter을 우회하고
logpb_flush_pages_direct을 쓴다 — 두 bracket 레코드의 동기 durability가 필요하기 때문이 다.
vs cubrid-mvcc.md
섹션 제목: “vs cubrid-mvcc.md”LOG_HEADER.mvcc_op_log_lsa은chkpt_lsa과 함께 갱신되어 vacuum에 durable 핸들을 준다.LOG_INFO_CHKPT_TRANS은 MVCCID을 나르지 않는다. 복구는 TDES별 MVCCID를 스냅샷이 아니라 analysis 도중 레코드별mvcc_id필드 에서 재구성한다. MVCCID 발급이 lazy하므로 옳다.
vs cubrid-double-write-buffer.md
섹션 제목: “vs cubrid-double-write-buffer.md”pgbuf_flush_checkpoint은 가장 큰 단일 DWB producer다.fileio_synchronize_all이dwb_flush_force을 전이적으로 호출 한다.logpb_checkpoint의 8단계가 모든 DWB-staged 페이지를 end-CHKPT 레코드 발행 이전에 home에 도달하게 한다 — redo-LSA 약속이 건전해진다.- DWB과 체크포인트 프로토콜은 진행 중 크래시를 독립적으로 막는다. torn 페이지 복구 vs 이전 체크포인트 fallback. 어느 쪽도 다른 쪽에 의지하지 않는다.
풀리지 않은 질문
섹션 제목: “풀리지 않은 질문”-
왜
PRM_ID_LOG_CHECKPOINT_INTERVAL_SECS과PRM_ID_LOG_CHECKPOINT_INTERVAL이 모두 기본값 360으로 정의 되었는가? 전자는PRM_DEPRECATED로 표시되었고 후자가 모던 대체다. deprecated 쪽을 여전히 읽는 호출처가 있는가? 조사 경로 는_SECS접미사 없는PRM_ID_LOG_CHECKPOINT_INTERVAL을 grep하고,log_get_checkpoint_interval이 swap해야 하는지 확인. -
페이지 카운트 트리거(
LOG_ISCHECKPOINT_TIME)이 실제로 쓰이는 가? 매크로는 정의되어 있지만 데몬 주도 타이머가 지배하는 듯 하다.LOG_ISCHECKPOINT_TIMEgrep이 어떤 append 경로가 여전히 폴링하는지 보여 줄 것이다. 그렇지 않다면 매크로는 legacy 호환 을 위해 보존된 죽은 코드다. -
LOG_REC_CHKPT레코드 크기의 상한은? 액티브 트랜잭션이 많은 바쁜 엔진은 MB 단위 end-CHKPT 레코드를 만들 수 있다. 클램프가 있는가? 직렬화 크기가 한 로그 페이지를 넘으면 어떻게 되는가?log_rv_analysis_end_checkpoint안의LOG_READ_ADVANCE_WHEN_DOESNT_FIT매크로가 복구 측은 다중-페이지 체크포인트 레코드를 처리한다는 걸 시사한다. 발행 측 할당은 monolithic이다 — 그래도 옳은가? -
볼륨별 헤더 쓰기의 크래시 atomicity.
logpb_checkpoint의 15단계가 미디어 복구를 위해 모든 데이터 볼륨의 디스크 헤더를 새 chkpt_lsa로 다시 쓴다. 이는 볼륨 사이에서 atomic하지 않다. 루프 도중 크래시가 나면 일부 볼륨은 새 LSA, 다른 일부 는 옛 LSA를 갖게 된다. 미디어 복구가 이를 견디는가?log_rv_find_checkpoint이 볼륨별 최소 LSA를 취하므로 답은 예이지만, 그 성질이 확인되어야 한다. -
standalone(
SA_MODE) 경로가 체크포인트를 발행하는가? 데몬 등록은#if defined(SERVER_MODE)로 게이팅된다. standalone 도구 (csql -S, loaddb)은 주기적이 아니라 종료 시점에만 체크포인트 를 잡을 것이다. 조사 경로는 SA_MODE 아래logpb_checkpoint호출자를 trace. -
analysis 복구의
tdes->client.set_system_internal_with_user (chkpt_one->user_name)호출이 특이하다. 스냅샷의 사용자 이름 으로 system 마커를 세팅한다. 복구가 왜 사용자 이름이 필요한가? HA/replication 감사 로그용일 가능성. trace해 볼 만한 가치 있음. -
페이지 서버 복제 경로와의 상호작용. CUBRID에는 페이지 버퍼가 원격 노드에 있는 페이지 서버 복제 모드가 있다. 체크포인트 데몬이 페이지 서버와 조정하는가?
log_recovery_redo.hpp이 redo 디스패처가 복구와 페이지 서버 복제 사이에서 공유된다고 언급 한다 — 체크포인트 발행기도 공유되는가?
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/transaction/log_manager.c— 데몬 등록, looper, legacyLOG_ISCHECKPOINT_TIME매크로, dump 도우미.src/transaction/log_page_buffer.c—logpb_checkpoint본체 와 도우미들logpb_checkpoint_trans,logpb_checkpoint_topops,logpb_dump_checkpoint_trans,logpb_flush_header.src/transaction/log_record.hpp— on-log 모양:LOG_REC_CHKPT,LOG_INFO_CHKPT_TRANS,LOG_INFO_CHKPT_SYSOP,LOG_START_CHKPT/LOG_END_CHKPTenum 값.src/transaction/log_storage.hpp—LOG_HEADER::chkpt_lsa,smallest_lsa_at_last_chkpt.src/transaction/log_impl.h—log_global의 체크포인트 관련 필드들(chkpt_lsa_lock,chkpt_redo_lsa,chkpt_every_npages,run_nxchkpt_atpageid).src/transaction/log_recovery.c— 소비자:log_rv_find_checkpoint,log_rv_analysis_start_checkpoint,log_rv_analysis_end_checkpoint,log_rv_analysis_record의 analysis 디스패치 arm,log_recovery안의chkpt_lsa = rcv_lsa대입.src/transaction/log_tran_table.c— 체크포인트가 walk하는TR_TABLE_CS_ENTER/EXIT 프리미티브, 그리고 복구가 쓰는logtb_clear_tdes,logtb_rv_find_allocate_tran_index.src/storage/page_buffer.c—logpb_checkpoint안에서 호출 되는 dirty 페이지 드라이버pgbuf_flush_checkpoint.src/base/system_parameter.c—PRM_ID_LOG_CHECKPOINT_*항목 과 기본값.
학술적 참조
섹션 제목: “학술적 참조”- Mohan, Haderle, Lindsay, Pirahesh, Schwarz, ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging, ACM TODS 17.1, 1992 — 명시적 begin/end 레코드를 쓰는 fuzzy 체크포인트 프로토콜이 ARIES §6.
- Bernstein, Hadzilacos, Goodman, Concurrency Control and Recovery in Database Systems, 1987 — 6장 (Recovery)의 체크포인트 교과 서적 처리. consistent vs fuzzy 변종 구분.
- Petrov, Database Internals, 2019, 5장 §Recovery와 §ARIES — 모던 교과서적 프레이밍. redo-LSA 힌트와 체크포인트 주기와 복구 시간의 관계 도입.
- Silberschatz, Korth, Sudarshan, Database System Concepts, 7판 19장 (Recovery System) — 표준 학부 발표. 체크포인트는 redo 패스를 묶기 위한 도구로 프레이밍.
본 저장소의 자매 문서
섹션 제목: “본 저장소의 자매 문서”knowledge/code-analysis/cubrid/cubrid-recovery-manager.md— 이 체크포인트가 발행하는 것을 소비하는 세 패스 재시작 프로토콜.knowledge/code-analysis/cubrid/cubrid-log-manager.md— 두 bracket 레코드 두 곳 모두에서 체크포인트가 쓰는 prior-list와 append 규율을 가진 WAL 프레임워크.knowledge/code-analysis/cubrid/cubrid-mvcc.md—mvcc_op_log_lsa을 통한 MVCC 상호작용과 lazy MVCCID 발급 모델.knowledge/code-analysis/cubrid/cubrid-double-write-buffer.md— 체크포인트 7단계(pgbuf_flush_checkpoint)와 8단계 (fileio_synchronize_all)에서 협력하는 torn-페이지 가드.knowledge/code-analysis/cubrid/cubrid-page-buffer-manager.md— redo-LSA 힌트를 구동하는 dirty 페이지 추적.knowledge/code-analysis/cubrid/cubrid-2pc.md—may_need_synch_checkpoint_2pc로 재시작을 가로질러 살아 남는, 액티브 트랜잭션 스냅샷이 보존하는 in-doubt 트랜잭션.