(KO) CUBRID Vacuum — 로그 재생을 통한 죽은 MVCC 버전 회수
목차
학술적 배경
섹션 제목: “학술적 배경”MVCC 시스템은 죽은 버전을 끊임없이 만들어 낸다. UPDATE는 새 버전을 쓰고 옛 버전을 그 자리에 남긴다. DELETE는 행을 지웠다고 표시 하지만 물리적으로 회수하지는 않는다. abort된 INSERT는 tombstone을 남긴다. 이를 그대로 두면 heap과 인덱스가 tombstone과 옛 버전으로 가득 차고, 결국 저장 공간과 스캔 비용이 garbage 자체에 잡아 먹힌다. vacuum 서브시스템 이 그 garbage를 치우는 MVCC의 가비지 컬렉터다.
Database Internals (Petrov) 5장 §MVCC 가 회수 문제를 한 줄로 요약한다 — 어떤 버전이 회수 가능하려면, 어떤 in-flight 트랜잭션 이나 미래의 트랜잭션 스냅샷도 그 버전을 볼 수 없어야 한다. 이를 구현하는 기계가 무엇인지는 엔진의 결정에 달려 있다.
- 엔진은 가장 오래된 가시 MVCCID 를 알고 있다. 모든 트랜잭션 의 스냅샷은 자기 가시성 경계를 가진다. 살아 있는 모든 트랜잭션 의 가시성 경계 중 가장 작은 값이 그 임계다.
xmax < oldest_visible_mvccid인 버전, 또는 abort된 insertion 의 tombstone에서xmin < oldest_visible_mvccid인 것은 죽었 으며 회수 대상이다.- 회수 작업 자체는 데이터 페이지 위에서 일어난다 (heap 행,
B+Tree 리프 엔트리, OID 리스트 압축). 그러나 그 작업을 구동
하는 것은 로그다. 모든 MVCC 연산이
LOG_MVCC_*레코드를 발행 했고, vacuum은 그 레코드들을 따라 걸으며 무엇을 치울지 알아 낸다.
이 모델 위에서 모든 실제 엔진은 두 가지 구현 결정을 내려야 한다. 두 결정이 본 문서 골격을 만든다.
- 회수를 무엇이 구동하는가 — 데이터 측 스캔인가, 로그 측 replay인가. PostgreSQL의 autovacuum은 heap 파일을 스캔 한다. InnoDB의 purge 스레드는 undo 로그를 따라 걷는다. CUBRID은 두 번째 진영이다 — vacuum이 WAL 그 자체 를 따라 걸으며 필요한 페이지를 그때그때 fix한다. 절충은 분명하다 — 데이터 측 스캔은 매 패스마다 모든 튜플을 한 번씩 본다. 로그 측 replay는 변경 횟수 만큼만 본다. 거의 변경되지 않는 튜플이 많은 워크로드에서는 로그 측이 유리하고, 변경 없이 대량 스캔만 일어나는 워크로드에서는 데이터 측이 유리하다.
- 작업의 단위. 튜플 단위, 페이지 sweep 단위, 또는 로그 블록 단위. CUBRID은 블록 단위를 고른다. 로그가 고정 크기의 vacuum 블록 으로 잘려 있고 (기본값 31 로그 페이지), 한 블록이 한 worker에게 할당되는 단위 작업이다.
이 두 답이 보이면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나 를 구현하거나 그 답을 더 빠르게 만든다는 점이 분명해진다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”모든 MVCC 엔진은 어떤 형태로든 가비지 컬렉터를 출하한다. 그 모양 이 한 줌의 패턴으로 수렴한다.
가장 오래된 가시 MVCCID 워터마크
섹션 제목: “가장 오래된 가시 MVCCID 워터마크”회수는 살아 있는 스냅샷 중 가장 작은 MVCCID 너머로 진행할 수
없다. 모든 엔진이 이 워터마크를 유지한다. 트랜잭션이 시작 / 종료
할 때마다 다시 계산된다. PostgreSQL은 이를 OldestXmin 과
vacuum_defer_cleanup_age 라 부르고, InnoDB은 purge_view,
CUBRID은 로그 헤더 위의 oldest_visible_mvccid 와 각 TDES의
mvccinfo 에 그 값을 둔다.
Master / worker 풀
섹션 제목: “Master / worker 풀”단일 master가 작업을 고르고, 여러 worker가 그 작업을 한다. master는 작업 후보 영역을 끊임없이 살펴야 하므로 가벼워야 하고, worker는 페이지 fix와 로그 읽기와 undo-data 버퍼가 필요하므로 자기 상태를 따로 가져야 한다. PostgreSQL의 autovacuum launcher
- workers, InnoDB의 coordinator + workers, CUBRID의 master + workers — 같은 아키텍처다.
Dropped-file 표
섹션 제목: “Dropped-file 표”테이블이나 인덱스가 drop되었는데도 옛 MVCC 버전이 그 파일을 여전
히 참조하는 경우, vacuum은 그 파일 id를 따라 가지 말아야 한다.
이미 해제된 extent를 따라 가는 일을 막아야 한다는 뜻이다. 모든
엔진이 별도의 map을 두고, drop된 파일 id를 그 시점의 MVCCID와
함께 기록한다. vacuum 작업은 어떤 record를 따라 가기 전에 이
map을 먼저 본다. CUBRID에서 이 구조가
vacuum_dropped_files_page 다.
블록 / 배치 작업 단위
섹션 제목: “블록 / 배치 작업 단위”worker가 한 번에 소비하는 단위가 블록 / 배치 / 페이지 다.
크기는 튜닝 knob이다 — 큰 블록은 per-job overhead를 흡수하고,
작은 블록은 더 잘 병렬화된다. PostgreSQL은 heap 페이지를,
InnoDB은 undo-log 배치를, CUBRID은 31 로그 페이지를 기본값으로
쓴다 (VACUUM_LOG_BLOCK_PAGES_DEFAULT).
Per-page 순차, 페이지 간 병렬
섹션 제목: “Per-page 순차, 페이지 간 병렬”단일 대상 페이지 위에서 vacuum 연산은 LSN 순서로 순차여야 페이지 상태가 정합적으로 유지된다. 페이지를 가로지르면 자명하게 병렬 이다. buffer manager의 페이지 fix가 자연스러운 동기화 도구다 — 한 페이지를 두 worker가 동시에 만질 수 없으니, 같은 페이지를 대상으로 하는 작업은 buffer manager에서 직렬화된다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 가장 오래된 가시 MVCCID 워터마크 | log_Gl.hdr.oldest_visible_mvccid (log_storage.hpp); TDES별 mvccinfo |
| Vacuum master | vacuum_master_task : public cubthread::entry_task (vacuum.c:813) |
| Vacuum worker | VACUUM_WORKER struct, 최대 VACUUM_MAX_WORKER_COUNT = 50 |
| Worker 상태 | VACUUM_WORKER_STATE { INACTIVE, PROCESS_LOG, EXECUTE } (vacuum.h) |
| 작업 블록 | VACUUM_DATA_ENTRY { blockid, start_lsa, oldest_visible_mvccid, newest_mvccid } |
| 블록 크기 (로그 페이지 단위) | VACUUM_LOG_BLOCK_PAGES_DEFAULT = 31 |
| Vacuum 데이터 파일 | vacuum_Data 글로벌. first_page / last_page 캐시. |
| 페이지별 블록 리스트 | VACUUM_DATA_PAGE { next_page, index_unvacuumed, index_free, data[] } |
| 블록 상태 비트 패킹 | blockid 의 상위 3비트: STATUS_VACUUMED, IN_PROGRESS, AVAILABLE; +INTERRUPTED |
| 작업 cursor | vacuum_job_cursor 클래스 — blockid 재배치를 추적 |
| Heap 측 대상 리스트 | worker별 VACUUM_HEAP_OBJECT { vfid, oid } 배열 |
| Dropped-file 표 | vacuum_dropped_files_page + vacuum_dropped_file 레코드 (vacuum.c:580) |
| WAL 위 블록별 로그 link | LOG_VACUUM_INFO::prev_mvcc_op_log_lsa (log_record.hpp) |
| 로그 헤더의 블록 경계 추적 | log_header::vacuum_last_blockid, does_block_need_vacuum |
| Per-record 디스패치 | vacuum 측 undo / mvcc-undo 경로로 RV_fun[] 재사용 |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”vacuum 서브시스템에는 네 개의 이동 부품이 있다. 할 일 목록인 vacuum data, 다음 블록을 골라 보내는 master 작업, 블록을 병렬로 소비하는 worker 풀, 그리고 건너뛸 파일을 기록한 dropped-file 표. 이 순서로 본다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph LOG["WAL 로그 (cubrid-log-manager.md)"]
LR1["MVCC op record\n블록 B-1"]
LR2["MVCC op record\n블록 B"]
LR3["MVCC op record\n블록 B+1"]
end
subgraph VD["vacuum_Data (vacuum 데이터 파일)"]
VP1["VACUUM_DATA_PAGE 1\n엔트리들..."]
VP2["VACUUM_DATA_PAGE 2\n엔트리들..."]
VPn["..."]
end
subgraph M["Master (vacuum_master_task)"]
CUR["vacuum_job_cursor"]
SEL["워터마크 아래\n다음 블록 선택"]
end
subgraph W["Worker 풀 (≤ 50 VACUUM_WORKER)"]
W1["worker 1\nstate=PROCESS_LOG"]
W2["worker 2\nstate=EXECUTE"]
Wn["..."]
end
subgraph DF["Dropped files (vacuum_dropped_files_page)"]
DFP["vfid → mvccid map"]
end
subgraph TGT["Heap / B+Tree 대상 페이지"]
HP["heap page"]
BT["btree leaf"]
end
LOG -->|consume_buffer_log_blocks| VD
VD -->|cursor visit| M
M -->|블록 dispatch| W
W -->|블록 안의 MVCC op 읽기| LOG
W -->|chase 전에 검사| DF
W -->|fix + clean| TGT
이 그림이 보여 주는 세 개의 루프가 있다. 첫째, producer
루프. WAL이 MVCC 연산 레코드를 발행한다.
vacuum_consume_buffer_log_blocks 가 그 레코드들을 vacuum-data
엔트리로 변환한다. 둘째, master 루프. master가 vacuum_Data
를 따라 걸으며 워터마크 아래에 들어간 블록을 찾아 worker에게
보낸다. 셋째, worker 루프. worker가 블록을 받아 그 안의 로그
레코드를 따라 걸으며 대상 페이지를 정리한다.
Vacuum data — 할 일들의 목록
섹션 제목: “Vacuum data — 할 일들의 목록”각 엔트리가 vacuum 대상 로그 블록 하나에 해당한다.
// VACUUM_DATA_ENTRY — src/query/vacuum.cstruct vacuum_data_entry{ VACUUM_LOG_BLOCKID blockid; /* blockid + flags packed in top bits */ LOG_LSA start_lsa; /* LSA of last MVCC op log record in block */ MVCCID oldest_visible_mvccid; /* threshold at the time the block was logged */ MVCCID newest_mvccid; /* newest MVCCID in this block */
vacuum_data_entry () = default; vacuum_data_entry (const log_lsa &lsa, MVCCID oldest, MVCCID newest); vacuum_data_entry (const log_header &hdr);
VACUUM_LOG_BLOCKID get_blockid () const; bool is_available () const; bool is_vacuumed () const; bool is_job_in_progress () const; bool was_interrupted () const; void set_vacuumed (); void set_job_in_progress (); void set_interrupted ();};여기서 짚을 점은 비트 패킹이다. blockid 는 64비트지만, 상위
3비트 가 4-state 상태 (AVAILABLE, IN_PROGRESS, VACUUMED — 한
조합은 비어 있음) 를 들고 있고, 위에서 4번째 비트 가
INTERRUPTED 플래그를 들고 있다. 나머지 60비트가 실제 블록 id다.
매크로가 이 패킹을 풀어 준다.
// 블록 상태 매크로 — src/query/vacuum.c#define VACUUM_DATA_ENTRY_FLAG_MASK 0xE000000000000000#define VACUUM_DATA_ENTRY_BLOCKID_MASK 0x1FFFFFFFFFFFFFFF
#define VACUUM_BLOCK_STATUS_VACUUMED 0x8000000000000000#define VACUUM_BLOCK_STATUS_IN_PROGRESS_VACUUM 0x4000000000000000#define VACUUM_BLOCK_STATUS_AVAILABLE 0x0000000000000000
#define VACUUM_BLOCK_FLAG_INTERRUPTED 0x2000000000000000이 비트 패킹이 절약하는 것은 엔트리당 int status; bool interrupted; 정도의 패딩이다. 수백만 엔트리에 걸치면 절약 효과
가 실질적이다.
블록들은 vacuum 데이터 파일의 페이지 안에 산다.
// VACUUM_DATA_PAGE — src/query/vacuum.cstruct vacuum_data_page{ VPID next_page; /* 페이지들의 연결 리스트 */ INT16 index_unvacuumed; /* data[] 안에서 아직 vacuum되지 않은 첫 인덱스 */ INT16 index_free; /* data[] 안에서 첫 free 인덱스 */ VACUUM_DATA_ENTRY data[1]; /* variable-size array */
bool is_empty () const; bool is_index_valid (INT16 index) const; INT16 get_index_of_blockid (VACUUM_LOG_BLOCKID blockid) const; VACUUM_LOG_BLOCKID get_first_blockid () const;};index_unvacuumed 와 index_free 가 cursor 역할을 한다. 그 덕에
페이지 머리쪽의 vacuum 끝난 엔트리들은 살아 있는 엔트리를 다시
배치하지 않고도 비울 수 있다. 새 엔트리는 tail에 붙는다.
index_unvacuumed 가 index_free 를 따라잡으면 페이지가 비고,
연결 리스트에서 떨어진다.
vacuum_Data 글로벌은 이 리스트의 첫 페이지와 마지막
페이지를 buffer pool에 영구 fix해 둔다. vacuum이 매 사이클마다
이 두 페이지를 읽으니 매번 fix overhead를 내는 것은 부담이라는
점이다. vacuum_fix_data_page 매크로는 요청된 VPID가 캐시 페이지
와 일치하면 그 캐시로 short-circuit한다.
Vacuum data 구축 — 로그 → 블록
섹션 제목: “Vacuum data 구축 — 로그 → 블록”vacuum_consume_buffer_log_blocks (vacuum.c:5096) 가 로그에서
vacuum data로 가는 다리다. 로그에 MVCC 연산이 쌓이면 주기적으로
호출된다.
- 마지막으로 소비한 LSA부터 로그를 앞으로 읽는다. (방향
대비를 짚어 둔다 — 이 구축 단계는 WAL을 앞으로 스캔
하지만, §Worker 의 per-block 처리는 블록 안 MVCC 사슬
을
prev_mvcc_op_log_lsa를 따라 거꾸로 걷는다.) LOG_MVCC_*레코드를 만날 때마다, 이 레코드가 들어가야 할 블록을 찾거나 만든다 (블록 id =pageid / vacuum_Data.log_block_npages—VACUUM_LOG_BLOCK_PAGES_DEFAULT로 초기화되지만PRM_ID_VACUUM_LOG_BLOCK_PAGES로 override 가능한 런타임 필드라서, 매크로가 아니라 살아 있는 값으로 나눈다).- 블록의
start_lsa(마지막으로 본 MVCC op),newest_mvccid, 그리고oldest_visible_mvccid(그 레코드가 기록될 당시 잡혀 있던 워터마크. 지금 의 워터마크가 아니다) 를 갱신 한다. - 블록이 다 차면 (로그가 그 블록을 지나갔으니 더 이상 MVCC op 이 그 블록에 들어올 수 없게 되면) 그 엔트리를 AVAILABLE 상태로 vacuum data에 쓴다.
여기서 미묘한 부분이 있다 — 잡혀 있는 oldest_visible_mvccid
는 로깅 당시 의 워터마크이지 소비 시점의 워터마크가 아니라
는 점이다. 그래서 블록이 dispatch 가능해지는 조건은 현재 워터
마크가 그 블록의 newest_mvccid 보다 큰지 여부다. 워터마크가
로깅 이후 어떻게 움직였는지는 상관 없다.
Master 작업 — 블록 선택과 작업 dispatch
섹션 제목: “Master 작업 — 블록 선택과 작업 dispatch”master는 cubthread::entry_task 의 서브클래스다.
// vacuum_master_task — src/query/vacuum.c:813class vacuum_master_task : public cubthread::entry_task{public: void execute (cubthread::entry &thread_ref) override;
private: bool check_shutdown () const; bool is_task_queue_full () const; bool should_interrupt_iteration () const; bool is_cursor_entry_ready_to_vacuum () const; bool is_cursor_entry_available () const; void start_job_on_cursor_entry (); bool should_force_data_update () const; void increase_outstanding_job (); void decrease_outstanding_job (int count);
vacuum_job_cursor m_cursor; /* vacuum data 안 어디까지 왔는지 */ // ... condensed ...};vacuum_master_task::execute 가 master 루프다. 매 tick마다 다음
을 한다.
- shutdown / 큐 가득 / interrupt 조건 확인.
- cursor를 다음 AVAILABLE 엔트리로 전진. 그 엔트리의
newest_mvccid가 현재 가장 오래된 가시 워터마크보다 작아야 한다. - 엔트리 상태를 atomic하게
IN_PROGRESS로 전이. - outstanding-job 카운터를 증가.
- thread 풀로 엔트리를 worker에 dispatch.
cursor가 별도 클래스 (vacuum_job_cursor, vacuum.c:277) 인
이유는 vacuum 데이터 페이지가 매 tick 사이에 추가되거나 제거될
수 있기 때문이다. 완전히 vacuum된 페이지는 free되고,
새 블록은 항상 마지막 페이지에 들어간다. cursor의
readjust_to_vacuum_data_changes 가 이런 변화 후에 cursor의
blockid → 페이지 매핑을 다시 맞춘다.
Worker — 블록별 로그 replay와 대상 정리
섹션 제목: “Worker — 블록별 로그 replay와 대상 정리”vacuum_process_log_block (vacuum.c:3251) 가 worker 진입점이
다. 블록을 받으면 다음을 한다.
- worker 상태를
PROCESS_LOG로 설정. - 블록 안의 로그 레코드를
LOG_VACUUM_INFO::prev_mvcc_op_log_lsa사슬을 따라 거꾸로 걷는다. 이 사슬이 존재하는 이유는 분명하다 — 로그 매니저가 vacuum 서브시스템에 친절을 베풀기 때문이다. 모든 MVCC 레코드는 이전 MVCC 레코드를 가리키는 back-pointer를 들고 다닌다 (cubrid-log-manager.md §MVCC 계열 레코드). - 각 레코드의 undo 이미지를 풀어낸다 (worker별
log_zip_p사용). - 정리 후보마다
VACUUM_HEAP_OBJECT(vfid + oid) 를 만든다. - 상태를
EXECUTE로 전이. 대상 페이지를 fix하고, 죽은 버전을 제거하고, B+Tree OID 리스트를 압축한다. - 성공이면 블록 상태를
VACUUMED로 전이. 실패 (interrupt, 페이지 latch 경합, 에러) 면INTERRUPTED를 표시해서 master 가 다시 dispatch할 수 있게 한다.
worker는 작업 사이에 재사용되는 버퍼들을 들고 다닌다. 매번 할당 하지 않기 위함이다.
// VACUUM_WORKER — src/query/vacuum.hstruct vacuum_worker{ VACUUM_WORKER_STATE state; /* INACTIVE / PROCESS_LOG / EXECUTE */ INT32 drop_files_version; /* Last seen dropped-files version */
struct log_zip *log_zip_p; /* Decompression context */
VACUUM_HEAP_OBJECT *heap_objects; /* Targets to clean this job */ int heap_objects_capacity; int n_heap_objects;
char *undo_data_buffer; int undo_data_buffer_capacity;
int private_lru_index; /* Per-worker LRU list in page buffer */
char *prefetch_log_buffer; /* Prefetched log pages */ LOG_PAGEID prefetch_first_pageid; LOG_PAGEID prefetch_last_pageid;
bool allocated_resources; int idx; /* -1 for master; sequence for workers */};private LRU 인덱스 는 짚어 둘 만하다. CUBRID buffer manager 는 thread별 LRU 리스트를 지원한다 (cubrid-page-buffer-manager.md §Quota and private lists). vacuum worker마다 자기 LRU 리스트 를 가진다. 그래서 vacuum 스캔이 글로벌 hot 리스트를 오염시키지 않는다는 뜻이다. prefetch 버퍼는 곧 읽을 로그 페이지를 미리 가져 둔다. worker가 MVCC 레코드 사슬을 따라가면서 페이지마다 fault 지연을 내지 않게 해 준다.
Dropped files — 건너뛸 파일 리스트
섹션 제목: “Dropped files — 건너뛸 파일 리스트”class가 drop되었는데도 옛 MVCC 버전이 그 파일을 가리키는 상황
에서, vacuum worker는 그 파일 id를 따라 가서는 안 된다. 이미
free된 저장 공간을 따라 가게 되기 때문이다.
vacuum_dropped_file 표가 vfid 를 drop된 시점의 MVCCID에
매핑한다.
// vacuum_dropped_file — src/query/vacuum.c:580struct vacuum_dropped_file{ VFID vfid; MVCCID mvccid;};
struct vacuum_dropped_files_page{ VPID next_page; INT16 n_dropped_files; vacuum_dropped_file dropped_files[1]; /* variable-size */};worker는 heap으로 vfid를 따라가기 전에 vacuum_is_file_dropped
(vacuum.c:6587) 를 부른다. 답이 yes 이고, 버전의 MVCCID가
drop 시점보다 앞이면, 그 버전은 자동으로 죽은 것으로 간주되어
worker가 건너뛴다.
dropped-files 페이지 리스트는 vacuum_log_add_dropped_file
(vacuum.c:6121) 가 갱신한다. VACUUM_LOG_ADD_DROPPED_FILE_POSTPONE
와 VACUUM_LOG_ADD_DROPPED_FILE_UNDO 가 두 시나리오를 구분한다
(이 둘은 OR로 결합되는 플래그 비트가 아니라 pospone_or_undo
인자에 넘기는 단순 bool selector라는 점을 짚어 둔다). POSTPONE은
“이 파일은 commit 시점에 drop되었다 — commit 측 postpone replay
때 vacuum하라 다. UNDO는 이 파일은 만들어졌다가 abort되었다 —
undo replay 때 vacuum하라” 다.
복구 통합
섹션 제목: “복구 통합”vacuum_data_load_and_recover (vacuum.c:4183) 가 재시작 후의
진입점이다. log_recovery 가 ARIES 세 패스를 마친 뒤 이 함수가
다음을 한다.
vacuum_Data를 디스크 페이지에서 다시 로드.- 충돌 시점에
IN_PROGRESS였던 블록들을AVAILABLE로 되돌린 다 (INTERRUPTED플래그 켜고). master가 다시 집어 가도록. vacuum_recover_lost_block_data(vacuum.c:5465) 를 호출 해, WAL 안에는 있었지만 vacuum data에 아직 기록되지 않은 블록을 메운다. 충돌이 MVCC 레코드 발행과 다음vacuum_consume_buffer_log_blockstick 사이에 일어나면 이런 블록이 생긴다.
한 블록의 처음부터 끝까지
섹션 제목: “한 블록의 처음부터 끝까지”sequenceDiagram
participant LM as log_manager
participant CB as vacuum_consume_buffer_log_blocks
participant VD as vacuum_Data 파일
participant M as vacuum_master_task
participant W as vacuum_worker
participant PG as page buffer / heap / btree
LM->>LM: append LOG_MVCC_* 레코드
Note over LM: 모든 레코드의 prev_mvcc_op_log_lsa\n가 이전 MVCC 레코드를 가리킴
CB->>LM: 마지막 소비 LSA 이후를 read
CB->>VD: 다 찬 블록의 AVAILABLE 엔트리 append
loop master tick
M->>VD: 워터마크 아래의 다음 AVAILABLE 엔트리로 cursor 이동
M->>VD: 상태 → IN_PROGRESS (CAS)
M->>W: dispatch (블록)
W->>LM: 블록 안의 MVCC 사슬 따라 걷기
W->>W: VACUUM_HEAP_OBJECT 리스트 만들기
W->>PG: fix + 죽은 버전 제거 / OID 리스트 압축
alt 성공
W->>VD: 상태 → VACUUMED (CAS)
else interrupt / 에러
W->>VD: INTERRUPTED 플래그 + 상태 → AVAILABLE
end
end
소스 코드 가이드
섹션 제목: “소스 코드 가이드”anchor는 심볼명 이다. 라인은 흘러간다.
헤더와 타입
섹션 제목: “헤더와 타입”vacuum_worker(vacuum.h) — worker별 부기.vacuum_worker_stateenum (vacuum.h) — INACTIVE / PROCESS_LOG / EXECUTE.VACUUM_HEAP_OBJECT(vacuum.h) — heap 측 대상.VACUUM_LOG_BLOCK_PAGES_DEFAULT(vacuum.h) — 블록 크기.vacuum_data_entry(vacuum.c) — 한 블록의 작업 엔트리.vacuum_data_page(vacuum.c) — 엔트리들이 모인 페이지.vacuum_data(vacuum.c) — 글로벌 상태.vacuum_dropped_file/vacuum_dropped_files_page(vacuum.c) — skip 리스트.vacuum_master_task(vacuum.c) — master 루프 클래스.vacuum_job_cursor(vacuum.c) — 재배치를 견디는 cursor.
Lifecycle
섹션 제목: “Lifecycle”vacuum_initialize(vacuum.c) — boot 시점 init.vacuum_finalize(vacuum.c) — shutdown.vacuum_data_load_and_recover(vacuum.c) — 복구 후 reload.vacuum_recover_lost_block_data(vacuum.c) — 충돌 전 소비 되지 않았던 in-flight 블록 메우기.
블록 lifecycle
섹션 제목: “블록 lifecycle”vacuum_consume_buffer_log_blocks(vacuum.c) — 로그 → vacuum data.vacuum_master_task::execute(vacuum.c) — master tick.vacuum_master_task::start_job_on_cursor_entry(vacuum.c) — IN_PROGRESS 로 CAS + dispatch.vacuum_process_log_block(vacuum.c) — worker 진입점.vacuum_worker_allocate_resources(vacuum.c) — 첫 손길 시log_zip_p, 버퍼 할당.
Dropped files
섹션 제목: “Dropped files”vacuum_log_add_dropped_file(vacuum.c) — 등록.vacuum_is_file_dropped(vacuum.c) — 조회.
Worker 상태 inline accessor (in vacuum.h)
섹션 제목: “Worker 상태 inline accessor (in vacuum.h)”vacuum_get_vacuum_worker,vacuum_is_thread_vacuum,vacuum_is_thread_vacuum_worker,vacuum_is_thread_vacuum_master,vacuum_get_worker_state,vacuum_set_worker_state,vacuum_worker_state_is_*. 모두__attribute__ ((ALWAYS_INLINE)). worker hot path 위에 앉기 때문이다.
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”| 심볼 | 파일 | 라인 |
|---|---|---|
VACUUM_WORKER (struct) | vacuum.h | 106 |
VACUUM_WORKER_STATE enum | vacuum.h | 85 |
VACUUM_LOG_BLOCK_PAGES_DEFAULT | vacuum.h | 82 |
VACUUM_MAX_WORKER_COUNT | vacuum.h | 132 |
vacuum_data_entry (struct) | vacuum.c | 104 |
vacuum_data_page (struct) | vacuum.c | 194 |
vacuum_data (struct) | vacuum.c | 350 |
vacuum_dropped_file (struct) | vacuum.c | 580 |
vacuum_dropped_files_page (struct) | vacuum.c | 588 |
vacuum_job_cursor (class) | vacuum.c | 277 |
vacuum_master_task (class) | vacuum.c | 813 |
vacuum_master_task::execute | vacuum.c | 3002 |
vacuum_initialize | vacuum.c | 1180 |
vacuum_finalize | vacuum.c | 1416 |
vacuum_process_log_block | vacuum.c | 3251 |
vacuum_worker_allocate_resources | vacuum.c | 3620 |
vacuum_finalize_worker | vacuum.c | 3689 |
vacuum_data_load_and_recover | vacuum.c | 4183 |
vacuum_consume_buffer_log_blocks | vacuum.c | 5096 |
vacuum_recover_lost_block_data | vacuum.c | 5465 |
vacuum_log_add_dropped_file | vacuum.c | 6121 |
vacuum_is_file_dropped | vacuum.c | 6587 |
소스 검증 (2026-04-30 기준)
섹션 제목: “소스 검증 (2026-04-30 기준)”검증된 사실
섹션 제목: “검증된 사실”-
vacuum 소스는
src/query/아래에 있다 —src/transaction/이 아니다.find로 검증 —src/query/vacuum.{c,h}가 존재 하고src/transaction/vacuum.*는 없다. 함의 — 이 doc의 meta 와 frontmatterreferences:는 초안 시점에 그에 맞게 수정 되었다. 원본 skeleton은 heap이나 lock 형제와 비슷하게src/transaction/에 있다고 가정했었다. -
블록 크기 기본값은 31 로그 페이지이며,
VACUUM_LOG_BLOCK_PAGES_DEFAULT로 표시된다.vacuum.h:82에서 검증. 대응하는 런타임 파라 미터는prm_get_integer_value (PRM_ID_VACUUM_LOG_BLOCK_PAGES)다 (vacuum_initialize본문에 있다). 31은 기본값이며 boot 시점에 override 가능하다. -
worker 풀 상한은 50이다.
VACUUM_MAX_WORKER_COUNT = 50(vacuum.h:132) 에서 검증. 실제 카운트는 서버 파라미터로 설정 가능하지만, 매크로가 hard upper bound다. -
블록 상태는 64비트 blockid 의 상위 3비트에 패킹되며,
INTERRUPTED플래그가 4번째 비트를 쓴다.vacuum.c:135-186에서 검증. AVAILABLE은0x0000000000000000(상위 비트 모두 0), VACUUMED는0x8000000000000000, IN_PROGRESS는0x4000000000000000. BLOCKID_MASK0x1FFFFFFFFFFFFFFF가 실제 id를 추출한다 — 60비트, 약 1.15 × 10^18 블록까지가 소진 되기 전 한도다. -
vacuum_Data.first_page와vacuum_Data.last_page는 영구 fix 상태로 buffer pool에 머문다.vacuum.c:223(vacuum_fix_data_page매크로가 캐시 페이지로 short-circuit 한다) 에서 검증. 함의 — 페이지 buffer 교체가 이 두 페이지를 건드리지 않는다. master tick이 fix overhead를 내지 않는다는 뜻이다. -
worker는 page buffer 안에 자기만의 LRU 리스트를 유지한다.
vacuum.h:122(VACUUM_WORKER::private_lru_index) 에서 검증. vacuum 스캔이 글로벌 hot 리스트를 오염시키는 것을 막아 준다는 점이다. (cross-doc — cubrid-page-buffer-manager.md 가 thread 별 LRU 메커니즘을 다룬다.) -
worker는 곧 읽을 로그 페이지를 prefetch한다.
vacuum.h:124-126(prefetch_log_buffer,prefetch_first_pageid,prefetch_last_pageid) 에서 검증. 버퍼는 worker별이며,vacuum_worker_allocate_resources의 첫 할당 시점에 크기가 잡힌다. -
블록 안에서 거꾸로 걷는 동작은 MVCC 로그 사슬이 구동한다.
vacuum_process_log_block본문과LOG_VACUUM_INFO::prev_mvcc_op_log_lsa필드 (cubrid-log-manager.md §MVCC 계열 레코드) 를 함께 읽으 며 검증. 이 사슬 덕분에 vacuum이 전체 forward log walk보다 빠를 수 있다. -
dropped-files 표는 vacuum data 안에 inline이 아니라 별도 페이지에 저장된다.
vacuum.c:588(vacuum_dropped_files_page) 에서 검증. 함의 — dropped files 는 vacuum data 페이지 churn을 견디고, vacuum 작업이 vacuum data가 압축된 뒤에도 계속 동작할 수 있다. -
충돌 시 worker 복구 — IN_PROGRESS 블록은 AVAILABLE 로 되돌리되 INTERRUPTED 플래그가 붙는다.
vacuum_data_load_and_recover본문과vacuum_data_entry의was_interrupted/set_interruptedaccessor를 읽으며 검증. 플래그가 master에게 이 블록은 이전에 부분적으로 작업되었음 을 알려 준다. 그래서 worker는 이전 시도가 이미 정리한 레코드 를 건너뛸 수 있다 (대상 페이지 LSA가 이미 전진했기 때문이라 는 점이다).
미해결 질문
섹션 제목: “미해결 질문”-
master tick 간격과 적응적 throttling. master 루프의 wake 간격, 그리고 시스템이 바쁠 때 속도를 낮추는 backpressure 메커니즘은 위치를 잡지 못했다. 추적 경로 —
vacuum_master_task의 cubthread daemon 등록을 읽고,should_interrupt_iteration/is_task_queue_full메서드를 살피기. -
워터마크 전진 트리거.
log_Gl.hdr.oldest_visible_mvccid가 언제 다시 계산되는가? 트랜잭션 commit / abort마다? 주기적 으로? 둘 다? 추적 경로 —oldest_visible_mvccid에 쓰기를 하는 자리를 grep으로 찾고,logtb_complete_mvcc(cubrid-transaction.md) 와 cross-reference. -
heap vs btree 디스패치.
VACUUM_HEAP_OBJECT는 단순히(vfid, oid)다. worker가 vfid가 heap을 가리키는지 B+Tree 를 가리키는지 어떻게 결정하고, 정확한 서브시스템 정리 함수를 호출하는가? 추적 경로 — vacuum-execute 경로 본문을 읽기 (worker 상태가EXECUTE로 전이되는 자리 근처). -
온라인 스키마 변경과의 상호작용. vacuum이 한 블록을 작업 하는 도중 그 블록이 만지는 B+Tree가 drop된다면, dropped-files 표가 그 파일을 따라가는 것은 막아 준다. 그런데 in-flight
VACUUM_HEAP_OBJECT리스트는 어떻게 되는가? 엔트리가 dropped-files를 필터링되는가, 아니면 worker가 페이지 fix 계층에서 ER_FILE_DROPPED 를 다루는가? 추적 경로 — execute 경로 안의vacuum_is_file_dropped호출자를 찾기. -
경합 상황에서의 page-buffer private LRU 의미론. worker의 private LRU가 가득 찼고 다른 worker가 같은 페이지를 필요로 한다면 어떻게 되는가? 핸드오프인가, 글로벌 LRU를 통한 공유 인가? 추적 경로 — cubrid-page-buffer-manager.md 와
pgbuf_*_private_lru_*경로를 살피기. -
vacuum_recover_lost_block_data의 복구 의미. “lost blocks” 는 정확히 무엇인가 — 충돌 시점에 vacuum 소비자가 실행 중이었던 블록인가, 아니면 로그가 MVCC 레코드를 발행 했지만 소비자가 한 번도 돌지 못한 블록인가? 함수 이름은 둘 다를 시사한다. 추적 경로 — 함수 본문을 읽고 어떤 범위를 메우는지 확인.
CUBRID 너머 — 비교 설계와 연구 동향
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향”분석이 아닌 포인터(pointers).
-
PostgreSQL VACUUM — heap 패스와 인덱스 패스가 모두 데이터 페이지 를 스캔한다. WAL을 따라가지 않는다. dead-tuple 비트 맵이 relation별로 계산되고 정리에 쓰인다. 비용은 relation 전체 스캔이고, 이득은 로그 측 의존 없음이다. CUBRID의 로그 구동 설계는 InnoDB와 더 가깝다.
-
InnoDB purge 스레드 — undo 로그 (rollback segment) 를 거꾸로 걸으며 MVCCID 워터마크가 허용할 때 죽은 버전을 제거 한다. CUBRID의 로그 구동 walk와 구조적으로 유사하다. 차이는 사용하는 로그가 redo 로그다 (CUBRID이 같은
LOG_MVCC_*레코드 안에 MVCC undo를 logging하기 때문). -
Hekaton 가비지 컬렉션 (Larson 외, VLDB 2011) — epoch 기반, lock-free, 가장 오래된 active 트랜잭션의 epoch이 retire된 뒤 thread별로 동작. CUBRID의 master/worker 모델은 같은 아이디어 의 disk-resident 형태라 볼 수 있다.
-
Aurora의 스토리지 계층 MVCC — 버전이 컴퓨트 노드가 아니라 스토리지 엔진에서 회수되어 컴퓨트 위 vacuum이 사라진다. CUBRID은 프로세스 로컬이라 이는 기능 격차라기보다 구조적 대비다.
-
자기 튜닝 autovacuum (PostgreSQL 16+) — 더티 페이지율에 맞춰 블록 크기와 worker 수가 동적으로 조정된다. CUBRID의
VACUUM_LOG_BLOCK_PAGES_DEFAULT는 정적이며, 적응적 변종이 CBRD 후속 티켓의 좋은 출발점이 될 것이다. -
VACUUM-as-replication-source (Debezium 스타일) — vacuum이 이 행이 사라졌다 라는 로그 스트림을 발행한다. CUBRID의 supplemental 로그 레코드 (cubrid-log-manager.md §보조 레코드) 를 같은 용도로 재활용할 수 있다 —
cubrid-cdc.md가 자연 스러운 후속 문서다.
원본 분석 (raw/code-analysis/cubrid/storage/vacuum/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/vacuum/)”vacuum.pdfvacuum.pptx
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-mvcc.md— 가시성 모델과oldest_visible_mvccid워터마크.knowledge/code-analysis/cubrid/cubrid-log-manager.md—LOG_MVCC_*레코드와LOG_VACUUM_INFO::prev_mvcc_op_log_lsa사슬.knowledge/code-analysis/cubrid/cubrid-heap-manager.md— heap 측 record-level vacuum.knowledge/code-analysis/cubrid/cubrid-page-buffer-manager.md— vacuum worker가 쓰는 thread별 LRU 리스트.knowledge/code-analysis/cubrid/cubrid-recovery-manager.md— 세 패스 재시작 후vacuum_data_load_and_recover가 동작.
교재 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”- Database Internals (Petrov), 5장 §MVCC, §“Garbage collection in MVCC”.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/query/vacuum.{c,h}src/transaction/mvcc.{c,h}