콘텐츠로 이동

(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)이 명시했다. 논문은 단일 축에 두 체크포인트 패밀리를 갈라 둔다 — 체크포인트가 도는 동안 엔진이 얼마나 멈춰야 하는가.

  1. Sharp / consistent / quiescent 체크포인트. in-flight 트랜잭션을 모두 비우고, dirty 페이지를 모두 플러시한 뒤, 체크포인트 레코드 하나를 쓴다. 잡힌 상태는 일관되어 있다. 비용은 모든 체크포인트가 버퍼 풀 플러시 시간 동안 사용자 트랜잭션을 동결한다는 점이다. Bernstein/Hadzilacos/Goodman(6 장 §Checkpointing)이 교과서 변종이라고 묘사하지만, 처리량 타격이 너무 커서 운영 엔진은 거의 쓰지 않는다.
  2. Fuzzy / non-blocking 체크포인트. 액티브 트랜잭션 집합과 dirty 페이지 집합의 스냅샷을 동결 없이 잡고, 두 개의 brackets 레코드(ARIES 용어로 begin-CHKPTend-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이 이를 어떻게 구현하는지 천천히 줌인하는 작업이다.

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 페이지 힌트
PostgreSQLpg_control.checkPointcheckpoint_timeout + max_wal_sizeredoLSN 스칼라; runningXacts 사이드 레코드
InnoDB로그 파일 헤더log-bytes-since-last + dirty-pages-pct페이지별 oldest_modification 추적
Oraclecontrolfile SCNredo 로그 크기 + 수동 체크포인트Mean Time To Recover 타겟
SQL Serverbootpage dbi_checkptLSNrecovery-interval 타겟dirty-page LSN 워터마크
CUBRIDlog_Gl.hdr.chkpt_lsalog_checkpoint_interval(타이머)LOG_REC_CHKPT.redo_lsa 스칼라

모두 fuzzy다. CUBRID은 주류 안에 있고, 식별점은 명시적인 두 bracket 레코드(ARIES 논문에 충실)와 타이머 전용 트리거다.

이론적 개념CUBRID 이름
Fuzzy 체크포인트 데몬log_Checkpoint_daemon(log_checkpoint_daemon_init이 생성)
데몬 period(타이머)log_get_checkpoint_intervalPRM_ID_LOG_CHECKPOINT_INTERVAL_SECS을 읽음
데몬 본체log_checkpoint_executelogpb_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 LSAlog_Gl.hdr.chkpt_lsa
마지막 redo-LSA의 인메모리 사본log_Gl.chkpt_redo_lsa
아카이브 제거 워터마크log_Gl.hdr.smallest_lsa_at_last_chkpt
chkpt_lsachkpt_redo_lsa을 보호하는 mutexlog_Gl.chkpt_lsa_lock
액티브 트랜잭션 walkfor i in trantable.num_total_indices: logpb_checkpoint_trans(...)
액티브 sysop walklogpb_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
모든 볼륨 파일시스템 fsyncfileio_synchronize_all(DWB이 안에서 협력)
체크포인트를 소비하는 재시작 진입점log_recovery → log_recovery_analysis (start_lsa = log_Gl.hdr.chkpt_lsa)
chkpt 레코드의 레코드별 analysis armlog_rv_analysis_start_checkpoint, log_rv_analysis_end_checkpoint
LOG_ISCHECKPOINT_TIME 매크로페이지-카운트 기반 술어(legacy 경로) — log_manager.c

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_daemonlog_manager.c의 파일 스코프에 선언된 cubthread::daemon이다.

// log_Checkpoint_daemon — src/transaction/log_manager.c
static 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.c
REGISTER_DAEMON (log_checkpoint);
void
log_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.c
void
log_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.c
static void
log_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 방어로 남아 있다.

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까지).

  1. LOG_CS_ENTER. 로그 구조 위에 배타적(prior list mutex가 그 안쪽에 있다).
  2. 복구가 끝나지 않았으면 거절. if (BO_IS_SERVER_RESTARTED () && log_Gl.run_nxchkpt_atpageid == NULL_PAGEID) return;NULL_PAGEID sentinel은 한 번에 한 체크포인트만의 가드 역할 도 한다.
  3. 이전 chkpt_lsa을 chkpt_lsa_lock 아래 스냅샷. log_get_db_start_parameters 류 reader가 로그 CS을 잡지 않게 해 준다.
  4. In-flight WAL drain. logpb_flush_pages_direct로 begin-CHKPT 이전 모든 레코드가 durable해야 한다.
  5. LOG_START_CHKPT 발행. 빈 레코드. 그 LSA가 newchkpt_lsa 으로 잡혀 다음 재시작의 analysis 시작점이 된다.
  6. logtb_reflect_global_unique_stats_to_btree. CUBRID의 btree 는 캐시된 unique 카운터를 쥐고 있고, redo-LSA가 그것을 지나 전진하기 전에 카탈로그 btree에 플러시되어야 한다.
  7. pgbuf_flush_checkpoint(newchkpt_lsa, …). oldest_unflush_lsa <= newchkpt_lsa인 모든 BCB을 골라 DWB로 플러시한다. 남은 가장 작은 LSA가 tmp_chkpt.redo_lsa로 돌아온다 — redo-LSA 힌트.
  8. fileio_synchronize_all. 모든 데이터 볼륨에 fsync(2).
  9. trantable을 TR_TABLE_CS_ENTER(read) 아래에서 walk. 0..trantable.num_total_indices을 iterate. system 트랜잭션은 skip(체크포인트가 그 위에서 돈다). 살아 있는 LOG_TDES마다 한 줄의 logpb_checkpoint_trans 행이 된다.
  10. TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE 안의 액티브 sysop을 위해 다시 walklogpb_checkpoint_topops 경유.
  11. LOG_END_CHKPT 발행. prior_lsa_alloc_and_copy_dataLOG_REC_CHKPTdata_header로, 두 트레일링 배열을 페이로드로 잡는다.
  12. end 레코드 강제 — 또 한 번의 logpb_flush_pages_direct.
  13. 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(아카이브 제거 워터마크).
  14. logpb_flush_header. active 로그 헤더 페이지를 쓴다 — 이 fsync 뒤에 경계가 durable해진다.
  15. 모든 볼륨의 디스크 헤더에 스탬프 — 미디어 복구를 위해(백업 에서 재시작 시 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.c
void
logpb_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.hpp
typedef 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 emission
node = 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.c
static int
log_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_ACTIVETRAN_UNACTIVE_ABORTEDTRAN_UNACTIVE_UNILATERALLY_ABORTED로 강제 변환된다 — 복구는 여전히 active인 모든 트랜잭션을 loser로 다룬다. 2PC-prepared 상태는 그대로 유지되어 in-doubt 경로가 찾을 수 있게 한다. 셋째, start_redo_lsachkpt.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_lsachkpt_lsa보다 앞일 수 있다. 그 경우 redo는 analysis 시작에서 backward로 걸어가며, 대상 페이지의 디스크 LSA가 레코드 LSA보다 작을 때만 각 레코드를 적용한다.

pgbuf_flush_checkpoint이 7단계가 호출하는 페이지 버퍼 진입점 이다. 핵심 본체.

// pgbuf_flush_checkpoint — src/storage/page_buffer.c (sketch)
int
pgbuf_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_lsaNULL_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_CHKPTLOG_END_CHKPT arm.
  • 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.hppLOG_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_checkpointprev_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_log trace 활성화.
심볼파일
log_Checkpoint_daemonlog_manager.c359
log_get_checkpoint_intervallog_manager.c10075
log_wakeup_checkpoint_daemonlog_manager.c10113
log_checkpoint_executelog_manager.c10167
log_checkpoint_daemon_initlog_manager.c10407
LOG_ISCHECKPOINT_TIME 매크로log_manager.c122
log_dump_checkpoint_topopslog_manager.c6769
log_dump_record_checkpointlog_manager.c6792
logpb_checkpoint_translog_page_buffer.c6783
logpb_checkpoint_topopslog_page_buffer.c6833
logpb_checkpointlog_page_buffer.c6877
logpb_dump_checkpoint_translog_page_buffer.c7395
log_rv_find_checkpointlog_recovery.c579
log_rv_analysis_start_checkpointlog_recovery.c1797
log_rv_analysis_end_checkpointlog_recovery.c1830
log_rv_analysis_record (LOG_*_CHKPT arms)log_recovery.c2436
log_recovery (chkpt_lsa anchor)log_recovery.c780
LOG_REC_CHKPT structlog_record.hpp345
LOG_INFO_CHKPT_TRANS structlog_record.hpp354
LOG_INFO_CHKPT_SYSOP structlog_record.hpp372
LOG_START_CHKPT enumlog_record.hpp96
LOG_END_CHKPT enumlog_record.hpp97
LOG_HEADER::chkpt_lsalog_storage.hpp141
LOG_HEADER::smallest_lsa_at_last_chkptlog_storage.hpp163
log_global::chkpt_lsa_locklog_impl.h681
log_global::chkpt_redo_lsalog_impl.h683
log_global::chkpt_every_npageslog_impl.h684
log_global::run_nxchkpt_atpageidlog_impl.h678
pgbuf_flush_checkpointpage_buffer.c3960
PRM_ID_LOG_CHECKPOINT_INTERVAL_SECSsystem_parameter.c1368
PRM_ID_LOG_CHECKPOINT_INTERVALsystem_parameter.c1379
PRM_ID_LOG_CHECKPOINT_NPAGESsystem_parameter.c1346
PRM_ID_LOG_CHECKPOINT_SIZEsystem_parameter.c1357
  • bracket 레코드는 LOG_START_CHKPT / LOG_END_CHKPT. 두 문서가 같다 — ARIES 두 단계에 충실하다.
  • log_Gl.hdr.chkpt_lsaanalysis 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 경로와 같다.

체크포인트 레코드는 다른 모든 appender과 동일한 prior list 규율 위에서 흐른다.

  • end-CHKPT 레코드는 prior_lsa_alloc_and_copy_data의 두 페이로드 오버로드로 트레일링 페이로드를 나른다.
  • 체크포인트는 group-commit waiter을 우회하고 logpb_flush_pages_direct 을 쓴다 — 두 bracket 레코드의 동기 durability가 필요하기 때문이 다.
  • LOG_HEADER.mvcc_op_log_lsachkpt_lsa과 함께 갱신되어 vacuum에 durable 핸들을 준다.
  • LOG_INFO_CHKPT_TRANS은 MVCCID을 나르지 않는다. 복구는 TDES별 MVCCID를 스냅샷이 아니라 analysis 도중 레코드별 mvcc_id 필드 에서 재구성한다. MVCCID 발급이 lazy하므로 옳다.
  • pgbuf_flush_checkpoint은 가장 큰 단일 DWB producer다.
  • fileio_synchronize_alldwb_flush_force을 전이적으로 호출 한다. logpb_checkpoint의 8단계가 모든 DWB-staged 페이지를 end-CHKPT 레코드 발행 이전에 home에 도달하게 한다 — redo-LSA 약속이 건전해진다.
  • DWB과 체크포인트 프로토콜은 진행 중 크래시를 독립적으로 막는다. torn 페이지 복구 vs 이전 체크포인트 fallback. 어느 쪽도 다른 쪽에 의지하지 않는다.
  1. PRM_ID_LOG_CHECKPOINT_INTERVAL_SECSPRM_ID_LOG_CHECKPOINT_INTERVAL이 모두 기본값 360으로 정의 되었는가? 전자는 PRM_DEPRECATED로 표시되었고 후자가 모던 대체다. deprecated 쪽을 여전히 읽는 호출처가 있는가? 조사 경로 는 _SECS 접미사 없는 PRM_ID_LOG_CHECKPOINT_INTERVAL을 grep하고, log_get_checkpoint_interval이 swap해야 하는지 확인.

  2. 페이지 카운트 트리거(LOG_ISCHECKPOINT_TIME)이 실제로 쓰이는 가? 매크로는 정의되어 있지만 데몬 주도 타이머가 지배하는 듯 하다. LOG_ISCHECKPOINT_TIME grep이 어떤 append 경로가 여전히 폴링하는지 보여 줄 것이다. 그렇지 않다면 매크로는 legacy 호환 을 위해 보존된 죽은 코드다.

  3. LOG_REC_CHKPT 레코드 크기의 상한은? 액티브 트랜잭션이 많은 바쁜 엔진은 MB 단위 end-CHKPT 레코드를 만들 수 있다. 클램프가 있는가? 직렬화 크기가 한 로그 페이지를 넘으면 어떻게 되는가? log_rv_analysis_end_checkpoint 안의 LOG_READ_ADVANCE_WHEN_DOESNT_FIT 매크로가 복구 측은 다중-페이지 체크포인트 레코드를 처리한다는 걸 시사한다. 발행 측 할당은 monolithic이다 — 그래도 옳은가?

  4. 볼륨별 헤더 쓰기의 크래시 atomicity. logpb_checkpoint의 15단계가 미디어 복구를 위해 모든 데이터 볼륨의 디스크 헤더를 새 chkpt_lsa로 다시 쓴다. 이는 볼륨 사이에서 atomic하지 않다. 루프 도중 크래시가 나면 일부 볼륨은 새 LSA, 다른 일부 는 옛 LSA를 갖게 된다. 미디어 복구가 이를 견디는가? log_rv_find_checkpoint이 볼륨별 최소 LSA를 취하므로 답은 예이지만, 그 성질이 확인되어야 한다.

  5. standalone(SA_MODE) 경로가 체크포인트를 발행하는가? 데몬 등록은 #if defined(SERVER_MODE)로 게이팅된다. standalone 도구 (csql -S, loaddb)은 주기적이 아니라 종료 시점에만 체크포인트 를 잡을 것이다. 조사 경로는 SA_MODE 아래 logpb_checkpoint 호출자를 trace.

  6. analysis 복구의 tdes->client.set_system_internal_with_user (chkpt_one->user_name) 호출이 특이하다. 스냅샷의 사용자 이름 으로 system 마커를 세팅한다. 복구가 왜 사용자 이름이 필요한가? HA/replication 감사 로그용일 가능성. trace해 볼 만한 가치 있음.

  7. 페이지 서버 복제 경로와의 상호작용. CUBRID에는 페이지 버퍼가 원격 노드에 있는 페이지 서버 복제 모드가 있다. 체크포인트 데몬이 페이지 서버와 조정하는가? log_recovery_redo.hpp이 redo 디스패처가 복구와 페이지 서버 복제 사이에서 공유된다고 언급 한다 — 체크포인트 발행기도 공유되는가?

CUBRID 소스 (/data/hgryoo/references/cubrid/)

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/transaction/log_manager.c — 데몬 등록, looper, legacy LOG_ISCHECKPOINT_TIME 매크로, dump 도우미.
  • src/transaction/log_page_buffer.clogpb_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_CHKPT enum 값.
  • src/transaction/log_storage.hppLOG_HEADER::chkpt_lsa, smallest_lsa_at_last_chkpt.
  • src/transaction/log_impl.hlog_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.clogpb_checkpoint 안에서 호출 되는 dirty 페이지 드라이버 pgbuf_flush_checkpoint.
  • src/base/system_parameter.cPRM_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.mdmvcc_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.mdmay_need_synch_checkpoint_2pc로 재시작을 가로질러 살아 남는, 액티브 트랜잭션 스냅샷이 보존하는 in-doubt 트랜잭션.