콘텐츠로 이동

(KO) CUBRID 백업과 복원 — 온라인 볼륨 백업, LSA 마커, 그리고 시점 복구

운영 중인 데이터베이스의 물리 백업은 본질적으로 race가 깔린 작업이다. 백업 도구가 데이터 파일을 읽는 동안에도 페이지는 계속 수정되기 때문이다. 그래서 단순히 데이터 파일을 그대로 복사하면 pre-image와 post-image 바이트가 임의로 섞여 들어간 “퍼지(fuzzy) 스냅샷” 이 잡힌다. WAL 기반 엔진은 이 사실을 받아들이고, redo 재생을 거친 뒤에야 정합성에 도달한다는 모델을 택한다. 백업 안의 데이터 파일은 그 복사 구간을 감싸는 로그 레코드와 짝을 이루었을 때만 비로소 유효해진다는 뜻이다.

감싸기 는 두 개의 LSA(log sequence number) 로 표현된다. 시작 LSA (체크포인트 LSA) 는 효과를 redo로 다시 적용해야 하는 가장 오래된 로그 레코드를 가리킨다. 그 LSA 보다 오래된 모든 페이지 변경은 이미 스냅샷 안에 내구성 있게 보존되어 있다. 끝 LSA (백업 종료 시점의 append LSA) 는 복사된 페이지에 효과가 등장 했을 가능성이 있는 가장 최근의 로그 레코드를 가리킨다. 페이지 스냅샷 + 시작 LSA + [start_lsa, end_lsa] 사이의 모든 로그 레코드를 함께 담은 백업은 그 자체로 self-contained 다. 복원 시 페이지 이미지를 마운트하고 시작 LSA부터 로그를 재생해 두 LSA 사이의 어떤 commit 경계로도 수렴할 수 있다는 뜻이며, 그것이 바로 시점 복구 (point-in-time recovery, PITR) 다.

이 구조에서 세 가지 후속 결정이 자연스럽게 따라 나온다. 첫째, 엔진은 백업 도중 [start_lsa, end_lsa] 의 모든 레코드를 찾을 수 있을 만큼 충분한 양의 로그 아카이브를 남겨두어야 한다. 진행 중인 백업과 아카이브 삭제가 경합한다면 그 백업은 복구 불가가 된다. 둘째, 페이지 복사 단계 자체는 어떤 락도 잡을 필요가 없다. 복사와 경합한 어떤 변경이든 redo 재생 단계에서 다시 등장하기 때문이라는 점이다. 셋째, 시작 LSA는 임의로 고를 수 없다. 그 LSA보다 오래된 모든 dirty 페이지가 적어도 한 번은 flush 되었음이 보장되는 fuzzy 체크포인트 의 LSA여야 한다. CUBRID은 가장 최근의 글로벌 체크포인트 LSA를 사용하고, 복사가 진행되는 동안 다음 체크포인트가 진행되지 못하도록 막는 방식을 택한다.

백업 레벨 은 같은 모델을 더 거친 단위로 확장한 것이다. 레벨 0 (전체) 백업은 할당된 모든 페이지를 복사한다. 레벨 1 백업은 자기 prv.lsa (페이지의 마지막 수정 LSA) 가 레벨 0 백업의 시작 LSA 보다 엄격하게 더 큰 페이지만 복사한다. 레벨 2는 같은 방식으로 레벨 1의 시작 LSA를 기준으로 삼는다. 즉 mtime 기반 차등 이 아니라 LSA 기반 차등 이며, 체인 L0 → L1 → L2 의 복원은 시간을 역순 으로 적용한다. 가장 최신 이미지가 우선 자리잡고, 더 오래된 백업은 이미 채워진 페이지를 건드리지 않는다.

PITR 목표 는 사용자가 직접 만지는 손잡이다. 사용자는 wall-clock 타임스탬프를 제공하고, 복원 코드는 그 타임스탬프를 stop LSA로 번역한다. 로그 레코드를 스캔하다가 LOG_COMMIT 또는 LOG_ABORT 의 시각이 목표를 넘어서는 순간 멈추고, 그 직전 지점에서 redo를 중단한다는 뜻이다. 시간 단조성(time monotonicity) 은 여기서 핵심 부품이 된다. 서로 다른 두 이벤트가 같은 time(NULL) 초를 공유 하면 복원이 둘을 구별할 수 없기 때문이다. CUBRID은 fileio_finish_backup 안에서 1초 강제 sleep을 넣어 백업 종료 타임스탬프가 백업 이후의 모든 commit 타임스탬프보다 엄격하게 작도록 펜스를 친다. 코드 코멘트는 이 잔여 갭을 직접 명시한다.

PostgreSQL 은 pg_basebackup 을 제공한다. 이 도구는 데이터 디렉터리를 스트리밍 복사하고, 내부 base-backup 로그 레코드가 시작 LSA를 마킹한다. WAL은 archive_command 또는 streaming replication 으로 별도 아카이빙된다. 복원 시 base backup의 시작 LSA부터 WAL을 재생해 recovery_target_time / LSN / named point까지 진행하며, 기대한 WAL을 모두 보지 않은 상태에서는 primary로 올라오기를 거부한다. WAL 스트림과 base backup이 분리되어 있어서 제3자 도구가 어느 한쪽만 구현하는 것이 가능하다는 특성이 있다.

MySQL 은 역사적으로 세계를 두 갈래로 나누어 왔다. mysqldump (논리적) 와 다양한 물리 복사 방식. Percona XtraBackup 은 InnoDB 데이터 파일을 복사하면서 redo를 사이드카 파일에 동시에 캡처한다. prepare 단계에서 캡처된 redo로 InnoDB recovery를 실행해 정합성을 맞춘다. PITR은 binary log를 따로 보관해야 가능 하다. XtraBackup의 redo 캡처는 push 기반 이다. 서버가 윈도우를 정해 주는 것이 아니라 도구가 redo 스트림을 구독하는 식이다. 그래서 copy가 아니라 prepare가 정합성을 결정짓는다.

Oracle RMAN 이 가장 통합도가 높다. RMAN은 데이터베이스 안에서 실행되며 블록 단위 incremental (부모 백업의 SCN을 넘는 블록만 — Oracle의 SCN은 CUBRID의 LSA에 해당), archived redo log 백업, 즉시 검증, SCN/시간/restore-point 기반 PITR을 모두 제공한다. RMAN 이 다른 점은 catalog다. 백업 메타데이터가 백업 파일 옆에 평문 으로 놓이지 않고, 데이터베이스 자체 안에 (또는 별도 recovery catalog DB 안에) 산다.

CUBRID 은 Postgres와 RMAN 사이에 자리한다. 백업 유틸리티가 서버 안에서 (또는 SA 모드에서 in-process로) 실행되어 체크포인트 상태, LSN cursor, 아카이브 디렉터리에 직접 접근한다. 데이터 페이지 와 아카이빙된 로그 레코드, 그리고 메타데이터를 한 덩어리에 묶은 self-contained 백업 볼륨 하나를 쓴다는 점에서 Postgres의 분리형 보다는 XtraBackup의 통합 출력에 더 가깝다. 세 단계의 incremental 이 제공된다. PITR 인자는 wall-clock 시간 (-d backuptime 또는 -d YYYY-MM-DD...) 으로 받지만 내부적으로는 redo 도중 LSA 기반 정지 지점으로 변환된다. 디스크 포맷은 file_io.c 안에서 FILEIO_BACKUP_HEADER 와 pageid sentinel (FILEIO_BACKUP_START_PAGE_ID, FILEIO_BACKUP_END_PAGE_ID, FILEIO_BACKUP_FILE_START_PAGE_ID, FILEIO_BACKUP_FILE_END_PAGE_ID) 로 문서화된다. 외부 catalog가 없고, 제3자가 읽을 수 있는 reader도 없다.

end-to-end 파이프라인은 세 모듈을 가로지른다. src/storage/file_io.c 가 디스크 백업 포맷과 볼륨별 복사를 소유하고, src/transaction/log_page_buffer.c 가 전체 오케스트레이션과 log-archive 감싸기를 소유하며, src/transaction/log_recovery.c 가 복원 시점의 redo 재생을 소유한다. 사용자 진입점은 src/executables/util_cs.c (backupdb) 와 src/executables/util_sa.c (restoredb) 두 곳이며, 둘 다 결과적으로는 src/transaction/boot_sr.c 를 거쳐 서버 측 코드 경로로 들어간다.

백업 볼륨은 헤더로 시작하는 고정 크기 페이지의 평면 시퀀스다. 페이지 식별자는 in-band marker로 사용되는 음수 sentinel이다.

file_io.c
#define FILEIO_BACKUP_START_PAGE_ID (-2) // header page
#define FILEIO_BACKUP_END_PAGE_ID (-3) // last page in this backup volume
#define FILEIO_BACKUP_FILE_START_PAGE_ID (-4) // start of one DB volume's payload
#define FILEIO_BACKUP_FILE_END_PAGE_ID (-5) // end of one DB volume's payload
#define FILEIO_BACKUP_VOL_CONT_PAGE_ID (-6) // continuation in next backup volume

FILEIO_BACKUP_PAGE 는 모든 payload page의 wire format이다. 머리에 실제 iopageid 를 담고, runtime에 계산된 꼬리 offset에 iopageid_dup 를 한 번 더 적는다. 이 dual pageid가 end-to-end 정합성을 검사하는 유일한 장치다 (페이로드 안의 데이터베이스 페이지 자체는 건드리지 않는다). FILEIO_BACKUP_HEADERFILEIO_BACKUP_START_PAGE_ID 자리에 놓이며, 복원이 볼륨을 인증하고 incremental 체인을 잇기 위해 필요한 모든 것을 담는다.

// FILEIO_BACKUP_HEADER — file_io.h (key fields)
struct fileio_backup_header {
PAGEID iopageid; // = FILEIO_BACKUP_START_PAGE_ID
char magic[]; // "CUBRID DATABASE BACKUP"
INT64 db_creation; // binds backup to its source DB
INT64 start_time, end_time; // bounds for PITR comparator
char db_fullname[PATH_MAX];
PGLENGTH db_iopagesize;
FILEIO_BACKUP_LEVEL level; // 0=full, 1=big incr, 2=small incr
LOG_LSA start_lsa; // skip predicate: copy if prv.lsa > start_lsa
LOG_LSA chkpt_lsa; // restore's redo-start cursor
int unit_num, bkup_iosize, bkpagesize;
FILEIO_BACKUP_RECORD_INFO previnfo[FILEIO_BACKUP_UNDEFINED_LEVEL]; // parent-level chain
char db_prec_bkvolname[PATH_MAX]; // multi-volume back-chain
FILEIO_ZIP_METHOD zip_method; // NONE or LZ4
FILEIO_ZIP_LEVEL zip_level;
};

전체 백업의 경우 CUBRID은 FILEIO_FULL_LEVEL_EXP = 32 개의 DB 페이지를 하나의 백업 페이지로 묶는다 (bkpagesize = db_iopagesize × 32). 이 묶음 단위가 페이지당 오버헤드를 분산시키고, LZ4가 선호하는 입력 윈도우와도 정렬된다. 반면 incremental은 접근 패턴이 sparse하기 때문에 백업 페이지 하나당 DB 페이지 하나로 유지한다.

FILEIO_BACKUP_FILE_HEADER (iopageid = FILEIO_BACKUP_FILE_START_PAGE_ID, volid, nbytes, vlabel) 는 모든 데이터베이스 볼륨 payload 앞에 선행한다. FILEIO_BACKUP_SESSION (io_backup_session) 은 런타임 상태 — 한 개의 열린 백업 볼륨을 메모리에 비춘 모습 (bkup), 진행 중인 DB 볼륨용 버퍼 영역 (dbfile), 읽기 스레드 풀 (read_thread_info), verbose 진행 스트림, 그리고 1MB 단위 throttle (sleep_msecs) 까지를 모두 담는다.

백업 오케스트레이션: logpb_backup

섹션 제목: “백업 오케스트레이션: logpb_backup”

logpb_backup (src/transaction/log_page_buffer.c) 가 전체 오케스트레이터다. 이 함수가 게이팅 관심사들을 순서 있게 엮는다. 체크포인트와의 직렬화, log-archive 감싸기, level-chain 검증, TDE key-file 분리. 골격은 다음과 같다.

// logpb_backup — log_page_buffer.c
int
logpb_backup (THREAD_ENTRY *thread_p, int num_perm_vols, const char *allbackup_path,
FILEIO_BACKUP_LEVEL backup_level, ...)
{
// 1. Serialise — only one backup at a time
LOG_CS_ENTER (thread_p);
if (log_Gl.backup_in_progress) { LOG_CS_EXIT(thread_p); return ER_LOG_BKUP_DUPLICATE_REQUESTS; }
log_Gl.backup_in_progress = true;
LOG_CS_EXIT (thread_p);
// 2. Initialise session: allocates header, area, thread pool
fileio_initialize_backup (log_Db_fullname, allbackup_path, &session, backup_level, ...);
// 3. Wait for in-flight checkpoint, then freeze the next one
loop:
LOG_CS_ENTER (thread_p);
if (log_Gl.run_nxchkpt_atpageid == NULL_PAGEID) { // checkpoint in progress
LOG_CS_EXIT (thread_p); thread_sleep (1000); goto loop;
}
saved_run_nxchkpt_atpageid = log_Gl.run_nxchkpt_atpageid;
log_Gl.run_nxchkpt_atpageid = NULL_PAGEID; // freeze
LSA_COPY (&chkpt_lsa, &log_Gl.hdr.chkpt_lsa); // capture start LSA
LOG_CS_EXIT (thread_p);
// 4. Resolve start_lsa from the level chain
switch (backup_level) {
case FILEIO_BACKUP_BIG_INCREMENT_LEVEL: // L1: parent must exist
if (LSA_ISNULL (&log_Gl.hdr.bkup_level0_lsa)) return ER_LOG_BACKUP_LEVEL_NOGAPS;
LSA_COPY (&bkup_start_lsa, &log_Gl.hdr.bkup_level0_lsa); break;
case FILEIO_BACKUP_SMALL_INCREMENT_LEVEL: // L2: requires L1
if (LSA_ISNULL (&log_Gl.hdr.bkup_level1_lsa)) return ER_LOG_BACKUP_LEVEL_NOGAPS;
LSA_COPY (&bkup_start_lsa, &log_Gl.hdr.bkup_level1_lsa); break;
default:
LSA_SET_NULL (&bkup_start_lsa); break; // L0 starts from -1|-1
}
// 5. Stamp + write header, emit FILEIO_BACKUP_START_PAGE_ID
fileio_start_backup (thread_p, log_Db_fullname, &log_Gl.hdr.db_creation,
backup_level, &bkup_start_lsa, &chkpt_lsa, all_bkup_info,
&session, zip_method, zip_level);
// 6. Walk every DB volume in volid order
volid = LOG_DBTDE_KEYS_VOLID;
do {
if (volid >= LOG_DBFIRST_VOLID)
logpb_backup_for_volume (thread_p, volid, &chkpt_lsa, &session, isincremental);
else
fileio_backup_volume (thread_p, &session, from_vlabel, volid, -1, false);
volid = fileio_find_next_perm_volume (thread_p, volid);
} while (volid != NULL_VOLID);
// 7. Archive the active log so the backup is self-contained
LOG_CS_ENTER (thread_p);
first_arv_needed = log_Gl.hdr.last_arv_num_for_syscrashes >= 0
? log_Gl.hdr.last_arv_num_for_syscrashes
: log_Gl.hdr.nxarv_num;
if (first_arv_needed < log_Gl.hdr.nxarv_num)
logpb_backup_needed_archive_logs (thread_p, &session,
first_arv_needed, log_Gl.hdr.nxarv_num - 1);
// 8. Append _lginf, finalise bkvinf, write end-time, 1-second monotonicity sleep
fileio_backup_volume (thread_p, &session, log_Name_info, LOG_DBLOG_INFO_VOLID, -1, false);
logpb_update_backup_volume_info (log_Name_bkupinfo);
fileio_finish_backup (thread_p, &session);
// 9. Release checkpoint freeze
log_Gl.run_nxchkpt_atpageid = saved_run_nxchkpt_atpageid;
log_Gl.backup_in_progress = false;
}

LSA 마커 발행. 시작 LSA는 chkpt_lsa = log_Gl.hdr.chkpt_lsa 이다. LOG_CS 와 체크포인트 mutex를 잡은 상태에서 샘플링된다. 복원이 알아야 할 LSA는 이 하나뿐이다. prv.lsa < chkpt_lsa 인 모든 페이지는 이미 내구성이 있고, prv.lsa >= chkpt_lsa 인 페이지는 아카이빙된 로그로부터 redo로 다시 적용된다는 뜻이다. 헤더의 start_lsa 필드는 다른 의미를 가진다. L0의 경우 -1|-1 (항상 복사), incremental의 경우 부모의 chkpt_lsa 이며 log_Gl.hdr.bkup_level0_lsa / bkup_level1_lsa 에도 같은 값이 저장된다. incremental 동안에는 prv.lsa <= start_lsa 인 페이지는 건너뛴다.

체크포인트 freeze. log_Gl.run_nxchkpt_atpageid = NULL_PAGEID 는 체크포인트 daemon이 자기 진행 중 표시로 쓰는 sentinel과 같은 값이다. 백업이 그 상태 머신을 빌려 쓰는 셈이다. freeze가 없다면 새 체크포인트가 복사 도중에 chkpt_lsa 를 앞으로 끌어 올려, ” chkpt_lsa 시점에 dirty였던 모든 페이지가 스냅샷이나 redo 로그 중 한쪽에는 반드시 등장해야 한다” 는 불변식을 깨버릴 수 있다.

Archive-log self-containment. logpb_backup_needed_archive_logsfirst_arv_needed 부터 nxarv_num - 1 까지의 모든 archive를 백업 볼륨 안으로 복사한다. first_arv_neededlast_arv_num_for_syscrashes (충돌 복구에 여전히 필요한 가장 오래된 archive) 또는 nxarv_num 이며, vacuum의 vacuum_min_log_pageid_to_keep 가 더 오래된 레코드를 요구하면 그쪽으로 끌려 내려간다.

Level chain. all_bkup_info[] 가 모든 이전 레벨의 timestamp 와 LSA를 들고 다닌다. 헤더는 이 체인 전체를 기록한다. 복원 시점에 부모 timestamp를 검증해서 서로 다른 run에서 만들어진 L0/L1/L2가 섞이지 않도록 거부한다.

End-time 단조성 우회. fileio_finish_backuptime(NULL)end_time 에 적은 뒤, time(NULL) 이 그 값보다 엄격하게 커질 때까지 sleep한다. 이 펜스는 PITR 비교자 (commit_timestamp > end_time) 를 보호한다. 초 단위 타임스탬프는 펜스 없이는 단조 적이지 않기 때문이다. 코드 코멘트는 이를 millisecond 해상도 LOG_REC_DONETIME 으로 가는 잠정 우회로 명시한다.

fileio_backup_volume 이 한 개의 데이터베이스 볼륨을 백업 페이지 시퀀스로 변환하는 안쪽 루프다.

// fileio_backup_volume — file_io.c
int
fileio_backup_volume (THREAD_ENTRY *thread_p, FILEIO_BACKUP_SESSION *session_p,
const char *from_vol_label_p, VOLID from_vol_id,
PAGEID last_page, bool is_only_updated_pages)
{
// 1. Mount source volume read-only (or reuse fd for log_active)
session_p->dbfile.vlabel = from_vol_label_p;
session_p->dbfile.volid = from_vol_id;
session_p->dbfile.vdes = (from_vol_id == LOG_DBLOG_ACTIVE_VOLID)
? fileio_get_volume_descriptor (LOG_DBLOG_ACTIVE_VOLID)
: fileio_open (session_p->dbfile.vlabel, O_RDONLY, 0);
fstat (session_p->dbfile.vdes, &from_stbuf);
session_p->dbfile.nbytes = from_stbuf.st_size;
from_npages = CEIL_PTVDIV (session_p->dbfile.nbytes, backup_header_p->bkpagesize);
// 2. Emit FILEIO_BACKUP_FILE_START_PAGE_ID with FILEIO_BACKUP_FILE_HEADER inside
session_p->dbfile.area->iopageid = FILEIO_BACKUP_FILE_START_PAGE_ID;
file_header_p = (FILEIO_BACKUP_FILE_HEADER *) (&session_p->dbfile.area->iopage);
file_header_p->volid = session_p->dbfile.volid;
file_header_p->nbytes = session_p->dbfile.nbytes;
strncpy (file_header_p->vlabel, session_p->dbfile.vlabel, PATH_MAX);
fileio_write_backup (thread_p, session_p, FILEIO_BACKUP_FILE_HEADER_PAGE_SIZE);
// 3. Read pages (parallel via fileio_start_backup_thread, or single-threaded here)
for (page_id = 0; page_id < from_npages; page_id++) {
node_p = fileio_allocate_node (queue_p, backup_header_p);
node_p->pageid = page_id;
node_p->nread = fileio_read_backup (thread_p, session_p, node_p->pageid);
// Incremental skip predicate: only copy if prv.lsa > parent start_lsa
if (!is_only_updated_pages
|| LSA_ISNULL (&session_p->dbfile.lsa)
|| LSA_LT (&session_p->dbfile.lsa, &node_p->area->iopage.prv.lsa)) {
node_p->nread += FILEIO_BACKUP_PAGE_OVERHEAD;
FILEIO_SET_BACKUP_PAGE_ID_COPY (node_p->area, node_p->pageid, backup_header_p->bkpagesize);
if (backup_header_p->zip_method != FILEIO_ZIP_NONE_METHOD)
fileio_compress_backup_node (node_p, backup_header_p);
fileio_write_backup_node (thread_p, session_p, node_p, backup_header_p);
}
fileio_free_node (queue_p, node_p);
}
// 4. Emit FILEIO_BACKUP_FILE_END_PAGE_ID
node_p = fileio_allocate_node (queue_p, backup_header_p);
FILEIO_SET_BACKUP_PAGE_ID (node_p->area, FILEIO_BACKUP_FILE_END_PAGE_ID, backup_header_p->bkpagesize);
fileio_write_backup_node (thread_p, session_p, node_p, backup_header_p);
}

skip 술어 LSA_LT (&session_p->dbfile.lsa, &node_p->area->iopage.prv.lsa) 는 “이 페이지의 마지막 수정 LSA가 부모의 시작 LSA보다 엄격하게 새로우면 복사한다” 는 의미다. session_p->dbfile.lsafileio_start_backupbkup_start_lsa 로부터 채워 둔다. L1의 경우 L0 체크포인트 LSA, L2의 경우 L1 체크포인트 LSA, L0의 경우 LSA_NULL 이다 (이때 술어는 항상 복사 로 fold 된다). 전체 백업은 FILEIO_FULL_LEVEL_EXP = 32 개의 DB 페이지를 한 백업 chunk에 묶고, incremental은 비교가 DB 페이지 단위가 되도록 한 chunk에 한 DB 페이지만 담는다.

dual pageid 인코딩 (FILEIO_SET_BACKUP_PAGE_ID_COPY 가 머리에 iopageid 를, 런타임에 계산된 꼬리 offset에 iopageid_dup 를 적 는다) 이 end-to-end 정합성 검사의 전부다. FILEIO_CHECK_RESTORE_PAGE_ID 가 압축 해제 단계에서 둘을 비교한다.

모든 볼륨 복사가 끝나면 fileio_finish_backupFILEIO_BACKUP_END_PAGE_ID 마커를 적고, 디바이스의 I/O quantum 까지 버퍼를 패딩한 뒤 (record-aligned write를 요구하는 tape 디바이스가 trailer를 받아들일 수 있게 하기 위해서다), 디바이스를 fsync 하고, offsetof(FILEIO_BACKUP_HEADER, end_time) 으로 다시 seek해서 헤더에 종료 timestamp를 stamp한다. 앞서 말한 1초 단조성 sleep은 이 함수의 가장 끝, 데이터가 내구성 있게 쓰인 뒤 그러나 제어가 logpb_backup 으로 돌아가기 직전에 자리한다.

flowchart TD
    A[backupdb CLI] --> B[boot_backup → xboot_backup]
    B --> C{logpb_backup}
    C --> D[LOG_CS 획득, backup_in_progress 게이팅]
    D --> E[체크포인트 완료 대기 후<br/>run_nxchkpt_atpageid freeze]
    E --> F[시작 LSA 캡처<br/>chkpt_lsa = log_Gl.hdr.chkpt_lsa]
    F --> G[fileio_initialize_backup<br/>session + bkuphdr 할당]
    G --> H[fileio_start_backup<br/>FILEIO_BACKUP_START_PAGE_ID + 헤더 기록]
    H --> I[DB 볼륨마다:<br/>fileio_backup_volume]
    I --> J[페이지 청크별:<br/>read → LSA skip 술어 → optional LZ4 →<br/>dual pageid 가진 FILEIO_BACKUP_PAGE]
    J --> I
    I --> K[데이터 볼륨 이후:<br/>logpb_backup_needed_archive_logs<br/>redo 필요 archive 복사]
    K --> L[_lginfo + bkvinf append]
    L --> M[fileio_finish_backup<br/>FILEIO_BACKUP_END_PAGE_ID + end_time<br/>· 1초 단조성 sleep]
    M --> N[체크포인트 freeze 해제<br/>backup_in_progress = false]

복원 측은 FILEIO_BACKUP_SESSIONtype = FILEIO_BACKUP_READ 로 다시 사용하고, incremental replay 추적을 위해 두 개의 구조를 추가한다.

FILEIO_RESTORE_PAGE_BITMAP (struct page_bitmap, 필드는 vol_id, size, bitmap[], next) 가 한 개의 대상 볼륨에서 어떤 물리 페이지가 이미 복원되었는지를 기록한다. incremental은 시간을 역순 으로 적용하기 (가장 최신부터) 때문에, L2가 채운 페이지를 나중에 L1이 덮어 써서는 안 된다. fileio_page_bitmap_set 이 쓰기 시 비트를 켜고, fileio_page_bitmap_is_set 이 매 쓰기 직전에 조회되어 더 오래된 백업이 더 새 백업이 이미 채운 페이지를 건너뛰 도록 만든다. 비트맵의 리스트 (vol_id 당 하나) 는 logpb_restore 의 스택 위에 산다.

BO_RESTART_ARG 는 사용자 측 요청 봉투다. restoredb 부터 boot_restart_from_backup 을 거쳐 logpb_restore 까지 흘러 들어 간다.

// bo_restart_arg — boot_sr.h
struct bo_restart_arg {
bool printtoc; // -t: dump backup TOC and exit
time_t stopat; // -d "YYYY-MM-DD..." or current time
const char *backuppath; // -B: explicit backup file path
int level; // -l: highest level to apply
const char *verbose_file;
bool newvolpath; // -u: relocate volumes per databases.txt
bool restore_upto_bktime; // -d backuptime: stop at end_time
bool restore_slave;
bool is_restore_from_backup;
INT64 db_creation;
LOG_LSA restart_repl_lsa, restart_committed_lsa;
char keys_file_path[PATH_MAX]; // TDE master-key path override
};

복원 오케스트레이션: logpb_restore

섹션 제목: “복원 오케스트레이션: logpb_restore”

logpb_restore 의 모양은 level-walk다. 사용자가 요청한 레벨에서 시작해 L0까지 내려오면서 각 레벨을 차례로 적용한다.

// logpb_restore — log_page_buffer.c
int
logpb_restore (THREAD_ENTRY *thread_p, const char *db_fullname, const char *logpath,
const char *prefix_logname, bo_restart_arg *r_args)
{
try_level = (FILEIO_BACKUP_LEVEL) r_args->level;
start_level = try_level;
fileio_page_bitmap_list_init (&page_bitmap_list);
LOG_CS_ENTER (thread_p);
while (try_level >= FILEIO_BACKUP_FULL_LEVEL && try_level < FILEIO_BACKUP_UNDEFINED_LEVEL)
{
if (!first_time) {
bkup_match_time = session->bkup.bkuphdr->previnfo[try_level].at_time; // chain check
fileio_finish_restore (thread_p, session);
}
// 1. Locate + authenticate the backup volume for this level
fileio_get_backup_volume (thread_p, db_fullname, logpath,
r_args->backuppath, try_level, from_volbackup);
fileio_start_restore (thread_p, db_fullname, from_volbackup, db_creation,
&bkdb_iopagesize, &bkdb_compatibility, &session_storage,
try_level, printtoc, bkup_match_time,
r_args->verbose_file, r_args->newvolpath);
session = &session_storage;
if (first_time) {
// Resolve PITR target
if (r_args->restore_upto_bktime)
r_args->stopat = (time_t) session->bkup.bkuphdr->end_time;
else if (r_args->stopat > 0)
logpb_check_stop_at_time (session, r_args->stopat,
(time_t) (session->bkup.bkuphdr->end_time > 0
? session->bkup.bkuphdr->end_time
: session->bkup.bkuphdr->start_time));
LSA_COPY (&session->bkup.last_chkpt_lsa, &session->bkup.bkuphdr->chkpt_lsa);
}
// 2. Walk the volumes inside this backup file
while (true) {
another_vol = fileio_get_next_restore_file (thread_p, session, to_volname, &to_volid);
if (another_vol == 0) break; // FILEIO_BACKUP_END_PAGE_ID seen
// Log volumes get staged to *_tmp first
if (to_volid == LOG_DBLOG_ACTIVE_VOLID || to_volid == LOG_DBLOG_INFO_VOLID
|| to_volid == LOG_DBLOG_ARCHIVE_VOLID) {
fileio_make_temp_log_files_from_backup (tmp_logfiles_from_backup, ...);
volume_name_p = tmp_logfiles_from_backup;
} else volume_name_p = to_volname;
// Skip log/info volumes on lower-level passes (highest level wins)
if (!first_time && (to_volid == LOG_DBLOG_BKUPINFO_VOLID
|| to_volid == LOG_DBLOG_ACTIVE_VOLID
|| to_volid == LOG_DBLOG_INFO_VOLID
|| to_volid == LOG_DBVOLINFO_VOLID
|| to_volid == LOG_DBLOG_ARCHIVE_VOLID
|| to_volid == LOG_DBTDE_KEYS_VOLID)) {
fileio_skip_restore_volume (thread_p, session); continue;
}
// 3. Per-volid bitmap (created on first sight, reused across levels)
if (to_volid >= LOG_DBFIRST_VOLID) {
page_bitmap = fileio_page_bitmap_list_find (&page_bitmap_list, to_volid);
if (page_bitmap == NULL) {
page_bitmap = fileio_page_bitmap_create (to_volid, total_pages);
fileio_page_bitmap_list_add (&page_bitmap_list, page_bitmap);
}
}
// 4. Restore pages; bitmap suppresses overwrites of newer-level pages
fileio_restore_volume (thread_p, session, volume_name_p, verbose_to_volname,
prev_volname, page_bitmap,
/*remember_pages=*/ start_level > FILEIO_BACKUP_FULL_LEVEL,
is_prev_volheader_restored, unlinked_volinfo);
// 5. Promote staged log iff backup's copy is fresher than on-disk
if (volume_name_p == tmp_logfiles_from_backup) {
if (logpb_is_log_active_from_backup_useful (...))
os_rename_file (tmp_logfiles_from_backup, to_volname);
else
unlink (tmp_logfiles_from_backup);
}
}
try_level = (FILEIO_BACKUP_LEVEL) (try_level - 1);
}
// 6. Late-fix linkage between volumes whose headers were absent in incrementals
for (const auto &[volid, volnames] : unlinked_volinfo)
disk_set_link (...);
fileio_finish_restore (thread_p, session);
LOG_CS_EXIT (thread_p);
fileio_page_bitmap_list_destroy (&page_bitmap_list);
}

Reverse-time 재생. try_level 이 감소한다. 각 패스는 (volid, pageid) 비트가 아직 켜지지 않은 페이지만 쓴다. 그래서 가장 신선한 사본이 자리잡는다. 대안 (L0 적용 후 L1, L2로 패치) 은 공유 페이지를 이중으로 읽고 incremental의 부분적 볼륨 헤더와 충돌을 일으킨다.

로그 staging. Active/archive/info 로그는 먼저 *_tmp 파일로 추출된다. 그 다음 logpb_is_log_active_from_backup_useful 가 백업의 로그가 디스크에 이미 있는 것보다 더 새로운지를 판정한다. 디스크의 active log가 백업의 다음 archive보다 더 새롭다면 staged 사본은 폐기되고, 그렇지 않으면 in-tree 파일을 대체한다. 같은 호스트에서 충돌 후 그 자리에서 복원하는 시나리오에서 디스크의 로그가 백업의 로그보다 더 새로울 때 의미가 있다.

헤더 페이지 관용성. Incremental은 페이지 0 (디스크 헤더) 을 종종 빼먹는다. 그 페이지의 prv.lsa 가 부모의 시작 LSA를 넘지 않았기 때문이다. fileio_restore_volume 은 디스크 헤더가 있었는지 여부 (incremental_includes_volume_header) 를 기록한다. 없었다면 disk_set_link 호출은 unlinked_volinfo map으로 미루 어졌다가 모든 레벨이 처리된 뒤에 적용된다. L0의 디스크 헤더가 정본으로 살아남고, 이후 패스들은 볼륨의 나머지를 계속 쓴다.

TDE 키 처리. _keys master-key 파일은 체인에서 가장 높은 레벨 (stopat에 가장 가까운 백업) 의 것을 복원한다. 더 낮은 레벨의 패스는 이를 건너뛴다. --keys-file-path 가 부모 이후에 회전된 키를 위해 이 동작을 override 한다.

fileio_restore_volumefileio_backup_volume 과 거울 관계이며 두 가지 관심사를 추가로 짊어진다. 구멍 메우기 (전체 백업의 경우 연속하는 iopageid 사이의 갭은 unallocated 페이지를 의미하므로 0으로 채워야 한다) 그리고 더 새 레벨이 이미 채운 페이지 건너뛰기.

// fileio_restore_volume — file_io.c (key parts)
while (true) {
fileio_decompress_restore_volume (thread_p, session_p, nbytes);
if (FILEIO_GET_BACKUP_PAGE_ID (session_p->dbfile.area) == FILEIO_BACKUP_FILE_END_PAGE_ID) {
if (session_p->dbfile.level == FILEIO_BACKUP_FULL_LEVEL && next_page_id < npages)
fileio_fill_hole_during_restore (thread_p, &next_page_id, npages, session_p, bitmap);
break;
}
// Sanity check: pageid in range, dual pageid match
if (FILEIO_GET_BACKUP_PAGE_ID (session_p->dbfile.area) > from_npages
|| !FILEIO_CHECK_RESTORE_PAGE_ID (session_p->dbfile.area, bkpagesize))
return ER_IO_RESTORE_READ_ERROR;
// Hole-fill on full backup
if (session_p->dbfile.level == FILEIO_BACKUP_FULL_LEVEL
&& next_page_id < FILEIO_GET_BACKUP_PAGE_ID (session_p->dbfile.area))
fileio_fill_hole_during_restore (thread_p, &next_page_id,
session_p->dbfile.area->iopageid, session_p, bitmap);
// Write each DB page in this chunk; bitmap suppresses overwrites
buffer_p = (char *) &session_p->dbfile.area->iopage;
for (i = 0; i < unit && next_page_id < npages; i++) {
fileio_write_restore (thread_p, bitmap, session_p->dbfile.vdes,
buffer_p + i * IO_PAGESIZE, session_p->dbfile.volid,
next_page_id++, session_p->dbfile.level);
}
}
// After streaming: stamp checkpoint LSA into volume header (only if disk header was present)
if (session_p->dbfile.volid >= LOG_DBFIRST_VOLID
&& (session_p->dbfile.level == FILEIO_BACKUP_FULL_LEVEL
|| incremental_includes_volume_header)) {
disk_set_creation (thread_p, volid, to_vol_label_p, &backup_header_p->db_creation,
&session_p->bkup.last_chkpt_lsa, false, DISK_FLUSH_AND_INVALIDATE);
if (volid != LOG_DBFIRST_VOLID && is_prev_vol_header_restored)
disk_set_link (thread_p, prev_volid, volid, to_vol_label_p, false,
DISK_FLUSH_AND_INVALIDATE);
}

fileio_write_restore 가 비트맵 조회 자리다. bitmap != NULL && fileio_page_bitmap_is_set (bitmap, page_id) 라면 쓰기는 건너뛰어 지고, 그렇지 않으면 쓰기가 진행되며 fileio_page_bitmap_set 이 비트를 켠다. 순수 L0 복원은 bitmap = NULL 을 넘기므로 모든 페이지 가 무조건 쓰여진다.

disk_set_creation 이 백업 헤더의 chkpt_lsa (session_p->bkup.last_chkpt_lsa) 를 볼륨의 헤더 페이지에 적는다. 이후 redo 단계가 모든 볼륨의 헤더 LSA를 읽어 그 중 최소를 redo 시작 cursor로 고르게 된다.

flowchart TD
    A[restoredb CLI] --> B[boot_restart_from_backup]
    B --> C[boot_restart_server with from_backup=true]
    C --> D[logpb_restore]
    D --> E{first_time?}
    E -->|yes| F[fileio_get_backup_volume<br/>백업 파일 경로 해석]
    E -->|no| G[bkup_match_time = previnfo at_time<br/>이전 레벨 종료]
    G --> F
    F --> H[fileio_start_restore<br/>FILEIO_BACKUP_HEADER 읽기 + 인증]
    H --> I[stopat 해석<br/>backup_time + r_args->stopat]
    I --> J[fileio_get_next_restore_file 루프<br/>DB 볼륨 한 개씩]
    J --> K{volid가 log?}
    K -->|yes| L[*_tmp 파일명으로 staging]
    K -->|no| M[직접 복원]
    L --> N[fileio_restore_volume]
    M --> N
    N --> O[페이지별: bitmap-skip → write → set bit<br/>full level은 hole 채움]
    O --> J
    J --> P{try_level > 0?}
    P -->|yes| Q[try_level--<br/>previnfo at_time 매칭]
    Q --> F
    P -->|no| R[unlinked 볼륨 헤더 후처리<br/>disk_set_link]
    R --> S[fileio_finish_restore<br/>fsync + close]
    S --> T[boot_restart_server 진행<br/>마운트 + log_recovery]
    T --> U[log_recovery analysis →<br/>chkpt_lsa부터 stopat까지<br/>log_recovery_redo]
    U --> V[log_recovery_resetlog<br/>stopat 이후 로그 절단]

Recovery 재생: log_recoverylog_recovery_resetlog

섹션 제목: “Recovery 재생: log_recovery 와 log_recovery_resetlog”

logpb_restore 가 반환되면 boot_restart_server 가 복원된 볼륨 들을 mount하고, boot_get_db_parm 을 호출하고, boot_find_rest_volumes 로 나머지 영구 볼륨을 walk한 뒤 마지막에 log_recovery (thread_p, ismedia_crash=1, &stopat) 으로 들어간다. ismedia_crash=1 이라는 플래그가 이번 재시작이 백업으로부터의 재시작이라는 신호다. analysis가 redo 시작 LSA를 고를 때 log_Gl.hdr.chkpt_lsa 만 보지 않고 모든 볼륨의 디스크 헤더를 함께 보아야 한다는 뜻이다.

// log_recovery — log_recovery.c
void
log_recovery (THREAD_ENTRY *thread_p, int ismedia_crash, time_t *stopat)
{
LSA_COPY (&rcv_lsa, &log_Gl.hdr.chkpt_lsa);
if (ismedia_crash) {
// Lower rcv_lsa to the oldest chkpt_lsa stored in any volume header
// (written there during fileio_restore_volume → disk_set_creation)
fileio_map_mounted (thread_p,
(bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint,
&rcv_lsa);
} else if (stopat) *stopat = -1; // crash recovery never stops early
log_recovery_analysis (thread_p, &rcv_lsa, &start_redolsa, &end_redo_lsa,
ismedia_crash, stopat, &did_incom_recovery, &num_redo_log_records);
log_recovery_redo (thread_p, &start_redolsa, &end_redo_lsa);
log_recovery_undo (thread_p);
if (did_incom_recovery) { // analysis cut log short at *stopat
log_recovery_resetlog (thread_p, &record_header_lsa, prev_lsa);
}
}

PITR 자르기는 redo가 아니라 log_recovery_analysis 안에서 일어 난다. analysis가 rcv_lsa 부터 forward로 걸으면서 매 레코드 헤더 를 파싱한다. LOG_REC_DONETIME.at_time > *stopatLOG_COMMIT 또는 LOG_ABORT 를 만나면 did_incom_recovery = true 로 표시 하고 end_redo_lsa 를 그 직전 레코드의 LSA로 끌어 내린다. 그 다음 redo는 [start_redolsa, end_redo_lsa] 만 재생하므로 늦게 commit한 그 트랜잭션은 포함되지 않는다는 점이다. 이어서 log_recovery_resetlog 가 active log를 end_redo_lsa 이후로 절단 하고, 절단점 이후 레코드를 담은 archive를 무효화한다. 그래서 이후의 충돌 복구가 그것을 다시 적용할 수 없게 된다. PITR을 비가 역적으로 만든다는 뜻이다 (Postgres의 recovery_target 의미와 동일).

복원된 볼륨에 대한 마운트: boot_restart_server

섹션 제목: “복원된 볼륨에 대한 마운트: boot_restart_server”

boot_restart_serverfrom_backup 분기는 위의 logpb_restore 를 실행한 뒤 표준 재시작 시퀀스로 이어진다. boot_mount (LOG_DBFIRST_VOLID), disk_get_boot_hfid, boot_get_db_parm, tde_cipher_initialize (r_args->keys_file_path 를 사용), heap_cache_class_info, boot_find_rest_volumes (_vinf 를 walk하며 first가 아닌 모든 영구 볼륨을 마운트한다. r_args != NULL 일 때 databases.txt 기반 relocation을 위해 newvolpath = true 를 받아들인다), disk_manager_init, 그리고 마지막으로 log_recovery (..., &r_args->stopat). redo + undo가 끝난 뒤 logpb_recreate_volume_info_vinf 를 다시 적어서, 이후의 정상 재시작이 복원이 재구성한 볼륨 집합을 그대로 보게 한다.

목적지가 디렉터리이고 cubrid_backup_volume_max_size_bytes 가 설정되어 있다면, 다음 쓰기가 한도를 넘게 될 때 fileio_flush_backupFILEIO_BACKUP_VOL_CONT_PAGE_ID 마커를 적은 뒤 현재 파일을 닫는다. 그리고 다음 단위 (db.bkLv0v002, v003, …) 를 fileio_get_next_backup_volume 으로 자동 생성하거나 사용자에게 prompt한다. fileio_add_volume_to_backup_info_lginfo/db.bkupinfo 로 flush될 in-memory bkvinf 캐시를 갱신한다. 복원 시점에는 fileio_continue_restoredb_prec_bkvolname 을 따라 체인을 걸으며 누락된 단위를 prompt한다.

backupdb (util_cs.c) 의 플래그는 logpb_backup 파라미터로 mapping된다. -D (목적지, 디렉터리 또는 FIFO), -l (레벨 0/1/2), -r (백업 후 chkpt_lsa 보다 오래된 archive 제거), -o (verbose 출력), --no-check (CHECKDB 사전 패스 생략 — 정합성 검증이 백업 자체보다 더 오래 걸릴 수 있다), --no-compress (LZ4 비활성화), -t (read worker 수, server 모드만), --sleep-msecs (1MB 단위 throttle), --separate-keys (TDE master key sidecar), -S (SA 모드).

restoredb (util_sa.c, 정의상 SA 전용) 의 플래그는 BO_RESTART_ARG 로 mapping된다. -d YYYY-MM-DD... 또는 -d backuptime (stopat / restore_upto_bktime), -l (level, 적용할 가장 높은 레벨), -B (backuppath, 명시적 백업 경로), -o (verbose_file), -u (newvolpath, databases.txt 사용), -t (printtoc, 목록만), -p (부분 archive log 재생), --keys (keys_file_path).

다음 몇 가지는 짚어 둘 가치가 있다.

  • 백업 페이지 CRC가 없다. dual-pageid 트릭이 정합성 검사의 전부다. pageid는 멀쩡하지만 내용은 손상된 페이지는 그대로 통과 한다. CUBRID은 하부 파일시스템과 백업 볼륨 무결성에 대한 운영자 프로세스에 의존한다 (LZ4의 checksum-bearing 프레임으로 압축하는 것이 엔진이 가장 가까이 가는 지점이며, 이는 --no-compress 로 꺼지는 것과 동일한 것이다).
  • incremental verify가 없다. restoredb -t (printtoc) 는 백업 안의 내용을 나열할 뿐, Oracle RMAN의 restore validate 처럼 end-to-end로 검증하지 않는다. 백업은 실제 복원을 시도해 봐야만 검증 된다.
  • streaming WAL이 없다. Postgres와 달리 백업에 함께 묶인 것보다 더 멀리 WAL을 가지고 가서 복원을 이어 나가게 해주는 archive_command 같은 것이 없다. 그 백업의 PITR 윈도우는 [chkpt_lsa, end_lsa] 다. 윈도우를 늘리려면 더 새 백업이나 HA replica replay가 필요하다 (replay는 cubrid_replica_* 모듈에 있는 완전히 별도의 코드 경로다).
  • 부분 볼륨 복원이 없다. 볼륨 단위로 전부 돌아오거나 아무 것도 돌아오지 않는다. per-table 복원은 없다. 논리적 export는 unloaddb / loaddb 가 담당한다.

Sentinel 재사용으로 체크포인트 freeze. log_Gl.run_nxchkpt_atpageid = NULL_PAGEID 를 세팅함으로써 새로운 mutex를 도입하는 대신 체크포인트 daemon의 in-progress sentinel을 빌려 쓴다. 경제적이지만, 백업 정합성을 체크포인트 의미론에 결합시킨다. 동시 체크포인트를 허용하는 미래의 변경이 백업 게이팅 을 조용히 깨버릴 수 있다는 뜻이다.

LSA-내부, timestamp-외부. LSA_LT(parent_chkpt_lsa, page.prv.lsa) 규칙은 LSA 측에서 깔끔하지만, 사용자가 보는 PITR 표면은 wall-clock 시간이다. logpb_check_stop_at_time 은 거의 아무 일도 하지 않는다. 실제 변환은 log_recovery_analysis 가 매 commit/abort 레코드를 파싱하는 그 자리에서 일어난다. --stopat-lsa 를 추가한다면 변경 은 BO_RESTART_ARGlog_recovery_analysis 양쪽에 작은 양으로 충분할 것이다.

End-time 단조성 sleep. fileio_finish_backup 의 1초 강제 sleep은 millisecond 해상도 LOG_REC_DONETIME 까지 가는 잠정 우회 임이 코드 코멘트로 명시되어 있다. 그 전까지는 end_time - 1 까지 복원하는 것이 보수적인 선택이다.

비트맵을 동반한 reverse-time level walk. 볼륨당 DB 페이지당 1비트 — 16KB 페이지의 10TB 데이터베이스에서 ~80MB 정도, 충분히 다룰 수 있다. 비트맵 리스트 (FILEIO_RESTORE_PAGE_BITMAP_LIST) 의 fileio_page_bitmap_list_find 는 선형 탐색이라서 볼륨이 수백 개 단위가 되면 O(volumes²) 가 되지만, 현재로서는 hotspot이 아니 이다.

디스크 헤더 관용성. incremental_includes_volume_header + unlinked_volinfo 는 사후 fixup이다. L1 백업이 디스크 헤더 페이지를 자주 빼먹기 (그 페이지의 prv.lsa 가 부모 LSA를 넘지 않았기) 때문에 등장한 장치 다. 모든 incremental에 헤더를 강제로 포함시키면 복원이 단순해 지지만, incremental이 약간 더 커지는 비용이 있다.

심볼파일라인
FILEIO_BACKUP_LEVEL enumsrc/storage/file_io.h96
FILEIO_BACKUP_VOL_TYPE enumsrc/storage/file_io.h122
FILEIO_BACKUP_TYPE enumsrc/storage/file_io.h146
fileio_backup_page structsrc/storage/file_io.h239
page_bitmap structsrc/storage/file_io.h255
fileio_backup_record_info structsrc/storage/file_io.h271
fileio_backup_header structsrc/storage/file_io.h280
fileio_backup_buffer structsrc/storage/file_io.h317
fileio_backup_db_buffer structsrc/storage/file_io.h341
fileio_backup_file_header structsrc/storage/file_io.h351
io_backup_session structsrc/storage/file_io.h423
FILEIO_BACKUP_*_PAGE_ID 상수src/storage/file_io.c273
FILEIO_FULL_LEVEL_EXPsrc/storage/file_io.c197
FILEIO_BACKUP_HEADER_IO_SIZEsrc/storage/file_io.c205
FILEIO_BACKUP_FILE_HEADER_PAGE_SIZEsrc/storage/file_io.c227
fileio_initialize_backupsrc/storage/file_io.c6732
fileio_initialize_backup_threadsrc/storage/file_io.c6654
fileio_finalize_backup_threadsrc/storage/file_io.c6966
fileio_abort_backupsrc/storage/file_io.c7055
fileio_start_backupsrc/storage/file_io.c7158
fileio_write_backup_end_time_to_headersrc/storage/file_io.c7258
fileio_finish_backupsrc/storage/file_io.c7340
fileio_remove_all_backupsrc/storage/file_io.c7486
fileio_compress_backup_nodesrc/storage/file_io.c7702
fileio_write_backup_nodesrc/storage/file_io.c7788
fileio_read_backup_volumesrc/storage/file_io.c7844
fileio_write_backup_volumesrc/storage/file_io.c8055
fileio_start_backup_threadsrc/storage/file_io.c8190
fileio_backup_volumesrc/storage/file_io.c8258
fileio_flush_backupsrc/storage/file_io.c8620
fileio_write_backup_headersrc/storage/file_io.c8930
fileio_initialize_restoresrc/storage/file_io.c8987
fileio_abort_restoresrc/storage/file_io.c9023
fileio_read_restoresrc/storage/file_io.c9039
fileio_read_restore_headersrc/storage/file_io.c9240
fileio_start_restoresrc/storage/file_io.c9313
fileio_continue_restoresrc/storage/file_io.c9384
fileio_finish_restoresrc/storage/file_io.c9732
fileio_list_restoresrc/storage/file_io.c9752
fileio_get_backup_volumesrc/storage/file_io.c9895
fileio_get_next_restore_filesrc/storage/file_io.c10009
fileio_fill_hole_during_restoresrc/storage/file_io.c10084
fileio_decompress_restore_volumesrc/storage/file_io.c10133
fileio_restore_volumesrc/storage/file_io.c10287
fileio_write_restoresrc/storage/file_io.c10594
fileio_skip_restore_volumesrc/storage/file_io.c10647
fileio_get_next_backup_volumesrc/storage/file_io.c10891
fileio_add_volume_to_backup_infosrc/storage/file_io.c11042
fileio_page_bitmap_list_initsrc/storage/file_io.c11694
fileio_page_bitmap_list_findsrc/storage/file_io.c11746
fileio_page_bitmap_list_addsrc/storage/file_io.c11777
fileio_page_bitmap_setsrc/storage/file_io.c11850
fileio_page_bitmap_is_setsrc/storage/file_io.c11865
logpb_initialize_backup_infosrc/transaction/log_page_buffer.c1277
logpb_backup_for_volumesrc/transaction/log_page_buffer.c7447
logpb_backupsrc/transaction/log_page_buffer.c7593
logpb_check_stop_at_timesrc/transaction/log_page_buffer.c8356
logpb_restoresrc/transaction/log_page_buffer.c8416
logpb_copy_databasesrc/transaction/log_page_buffer.c9404
logpb_backup_needed_archive_logssrc/transaction/log_page_buffer.c10752
logpb_backup_level_info_to_stringsrc/transaction/log_page_buffer.c11253
log_recoverysrc/transaction/log_recovery.c736
log_recovery_redosrc/transaction/log_recovery.c3251
log_recovery_resetlogsrc/transaction/log_recovery.c5221
bo_restart_arg structsrc/transaction/boot_sr.h112
boot_restart_server (from_backup branch)src/transaction/boot_sr.c1969
xboot_restart_from_backupsrc/transaction/boot_sr.c2808
boot_reset_mk_after_restart_from_backupsrc/transaction/boot_sr.c2899
xboot_backupsrc/transaction/boot_sr.c3918
backupdb (CLI entry)src/executables/util_cs.c130
restoredb (CLI entry)src/executables/util_sa.c967

자연스러운 확장들을, 변경 비용이 적은 순서대로 정리한다.

페이지 단위 CRC. FILEIO_BACKUP_PAGE 의 trailer에 4바이트 체크섬을 두면 fileio_write_backupfileio_decompress_restore_volume 사이의 silent corruption을 잡아낼 수 있다. bk_hdr_version 의 하위 호환성을 깨는 bump가 필요하다. 다행히 엔진은 이미 버전 게이팅 읽기를 지원한다 (fileio_continue_restoreCUBRID_MAGIC_DATABASE_BACKUP_OLD 분기).

LSA 기반 stopat. --stopat-lsa 를 노출하면 timestamp 단조성 문제를 우회할 수 있다. 내부 변경은 log_recovery_analysis 안의 비교자를 at_time 에서 LSA로 바꾸는 작은 양이다. UX 과제는 LSA가 사람이 다루기에 친화적이지 않다는 점이다. 완화책으로 cubrid_log 출력이 at_time, LSA 쌍을 함께 인쇄해서 운영자가 목표를 고를 수 있도록 확장한다.

온라인 incremental verify. restoredb --validate 모드가 디스크에 쓰지 않은 채 모든 페이지를 FILEIO_CHECK_RESTORE_PAGE_ID (그리고 옵션으로 CRC) 로 흘려 보내게 한다. 골격은 이미 fileio_list_restore 에 있다 (헤더만). 페이지 단위로 확장하는 일은 직진이다.

백업 목적지로의 streaming archive log. Postgres 식의 연속 WAL 아카이빙을 백업 디렉터리로 하면 PITR 윈도우를 end_time 너머로 확장할 수 있다. 새로운 cub_backup_writer daemon이나 archive flush 경로의 hook이 필요하다. 디스크 포맷 변경은 적다 (bkvinf는 이미 archive 단위 entry가 있다).

block-device direct I/O. fileio_initialize_backup 은 raw device를 감지하지만 항상 buffered I/O를 쓴다. 백업 목적지를 O_DIRECT 를 추가하면 매우 큰 백업에서 이중 버퍼링을 줄일 수 있다. 까다로운 부분은 bkpagesize 를 디바이스의 logical block size에 정렬하는 것이다.

per-table 백업. CUBRID의 디스크 포맷은 테이블 데이터를 heap file, B-tree, overflow file에 흩어 둔다. 논리적 unloaddb 가 이미 이 use case를 커버한다. 물리적 per-table 백업은 file_tracker.c 를 walk하면서 한 클래스에 속한 file ID만 복사하는 형태가 될 텐데, class catalog, B-tree, partitioning 코드를 가로지르는 큰 프로젝트 다.