콘텐츠로 이동

(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은 그 레코드들을 따라 걸으며 무엇을 치울지 알아 낸다.

이 모델 위에서 모든 실제 엔진은 두 가지 구현 결정을 내려야 한다. 두 결정이 본 문서 골격을 만든다.

  1. 회수를 무엇이 구동하는가 — 데이터 측 스캔인가, 로그 측 replay인가. PostgreSQL의 autovacuum은 heap 파일을 스캔 한다. InnoDB의 purge 스레드는 undo 로그를 따라 걷는다. CUBRID은 두 번째 진영이다 — vacuum이 WAL 그 자체 를 따라 걸으며 필요한 페이지를 그때그때 fix한다. 절충은 분명하다 — 데이터 측 스캔은 매 패스마다 모든 튜플을 한 번씩 본다. 로그 측 replay는 변경 횟수 만큼만 본다. 거의 변경되지 않는 튜플이 많은 워크로드에서는 로그 측이 유리하고, 변경 없이 대량 스캔만 일어나는 워크로드에서는 데이터 측이 유리하다.
  2. 작업의 단위. 튜플 단위, 페이지 sweep 단위, 또는 로그 블록 단위. CUBRID은 블록 단위를 고른다. 로그가 고정 크기의 vacuum 블록 으로 잘려 있고 (기본값 31 로그 페이지), 한 블록이 한 worker에게 할당되는 단위 작업이다.

이 두 답이 보이면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나 를 구현하거나 그 답을 더 빠르게 만든다는 점이 분명해진다.

모든 MVCC 엔진은 어떤 형태로든 가비지 컬렉터를 출하한다. 그 모양 이 한 줌의 패턴으로 수렴한다.

가장 오래된 가시 MVCCID 워터마크

섹션 제목: “가장 오래된 가시 MVCCID 워터마크”

회수는 살아 있는 스냅샷 중 가장 작은 MVCCID 너머로 진행할 수 없다. 모든 엔진이 이 워터마크를 유지한다. 트랜잭션이 시작 / 종료 할 때마다 다시 계산된다. PostgreSQL은 이를 OldestXminvacuum_defer_cleanup_age 라 부르고, InnoDB은 purge_view, CUBRID은 로그 헤더 위의 oldest_visible_mvccid 와 각 TDES의 mvccinfo 에 그 값을 둔다.

단일 master가 작업을 고르고, 여러 worker가 그 작업을 한다. master는 작업 후보 영역을 끊임없이 살펴야 하므로 가벼워야 하고, worker는 페이지 fix와 로그 읽기와 undo-data 버퍼가 필요하므로 자기 상태를 따로 가져야 한다. PostgreSQL의 autovacuum launcher

  • workers, InnoDB의 coordinator + workers, CUBRID의 master + workers — 같은 아키텍처다.

테이블이나 인덱스가 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).

단일 대상 페이지 위에서 vacuum 연산은 LSN 순서로 순차여야 페이지 상태가 정합적으로 유지된다. 페이지를 가로지르면 자명하게 병렬 이다. buffer manager의 페이지 fix가 자연스러운 동기화 도구다 — 한 페이지를 두 worker가 동시에 만질 수 없으니, 같은 페이지를 대상으로 하는 작업은 buffer manager에서 직렬화된다.

이론적 개념CUBRID 명칭
가장 오래된 가시 MVCCID 워터마크log_Gl.hdr.oldest_visible_mvccid (log_storage.hpp); TDES별 mvccinfo
Vacuum mastervacuum_master_task : public cubthread::entry_task (vacuum.c:813)
Vacuum workerVACUUM_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
작업 cursorvacuum_job_cursor 클래스 — blockid 재배치를 추적
Heap 측 대상 리스트worker별 VACUUM_HEAP_OBJECT { vfid, oid } 배열
Dropped-file 표vacuum_dropped_files_page + vacuum_dropped_file 레코드 (vacuum.c:580)
WAL 위 블록별 로그 linkLOG_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[] 재사용

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 대상 로그 블록 하나에 해당한다.

// VACUUM_DATA_ENTRY — src/query/vacuum.c
struct 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.c
struct 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_unvacuumedindex_free 가 cursor 역할을 한다. 그 덕에 페이지 머리쪽의 vacuum 끝난 엔트리들은 살아 있는 엔트리를 다시 배치하지 않고도 비울 수 있다. 새 엔트리는 tail에 붙는다. index_unvacuumedindex_free 를 따라잡으면 페이지가 비고, 연결 리스트에서 떨어진다.

vacuum_Data 글로벌은 이 리스트의 첫 페이지와 마지막 페이지를 buffer pool에 영구 fix해 둔다. vacuum이 매 사이클마다 이 두 페이지를 읽으니 매번 fix overhead를 내는 것은 부담이라는 점이다. vacuum_fix_data_page 매크로는 요청된 VPID가 캐시 페이지 와 일치하면 그 캐시로 short-circuit한다.

vacuum_consume_buffer_log_blocks (vacuum.c:5096) 가 로그에서 vacuum data로 가는 다리다. 로그에 MVCC 연산이 쌓이면 주기적으로 호출된다.

  1. 마지막으로 소비한 LSA부터 로그를 앞으로 읽는다. (방향 대비를 짚어 둔다 — 이 구축 단계는 WAL을 앞으로 스캔 하지만, §Worker 의 per-block 처리는 블록 안 MVCC 사슬 을 prev_mvcc_op_log_lsa 를 따라 거꾸로 걷는다.)
  2. LOG_MVCC_* 레코드를 만날 때마다, 이 레코드가 들어가야 할 블록을 찾거나 만든다 (블록 id = pageid / vacuum_Data.log_block_npagesVACUUM_LOG_BLOCK_PAGES_DEFAULT 로 초기화되지만 PRM_ID_VACUUM_LOG_BLOCK_PAGES 로 override 가능한 런타임 필드라서, 매크로가 아니라 살아 있는 값으로 나눈다).
  3. 블록의 start_lsa (마지막으로 본 MVCC op), newest_mvccid, 그리고 oldest_visible_mvccid (그 레코드가 기록될 당시 잡혀 있던 워터마크. 지금 의 워터마크가 아니다) 를 갱신 한다.
  4. 블록이 다 차면 (로그가 그 블록을 지나갔으니 더 이상 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:813
class 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마다 다음 을 한다.

  1. shutdown / 큐 가득 / interrupt 조건 확인.
  2. cursor를 다음 AVAILABLE 엔트리로 전진. 그 엔트리의 newest_mvccid현재 가장 오래된 가시 워터마크보다 작아야 한다.
  3. 엔트리 상태를 atomic하게 IN_PROGRESS 로 전이.
  4. outstanding-job 카운터를 증가.
  5. 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 진입점이 다. 블록을 받으면 다음을 한다.

  1. worker 상태를 PROCESS_LOG 로 설정.
  2. 블록 안의 로그 레코드를 LOG_VACUUM_INFO::prev_mvcc_op_log_lsa 사슬을 따라 거꾸로 걷는다. 이 사슬이 존재하는 이유는 분명하다 — 로그 매니저가 vacuum 서브시스템에 친절을 베풀기 때문이다. 모든 MVCC 레코드는 이전 MVCC 레코드를 가리키는 back-pointer를 들고 다닌다 (cubrid-log-manager.md §MVCC 계열 레코드).
  3. 각 레코드의 undo 이미지를 풀어낸다 (worker별 log_zip_p 사용).
  4. 정리 후보마다 VACUUM_HEAP_OBJECT (vfid + oid) 를 만든다.
  5. 상태를 EXECUTE 로 전이. 대상 페이지를 fix하고, 죽은 버전을 제거하고, B+Tree OID 리스트를 압축한다.
  6. 성공이면 블록 상태를 VACUUMED 로 전이. 실패 (interrupt, 페이지 latch 경합, 에러) 면 INTERRUPTED 를 표시해서 master 가 다시 dispatch할 수 있게 한다.

worker는 작업 사이에 재사용되는 버퍼들을 들고 다닌다. 매번 할당 하지 않기 위함이다.

// VACUUM_WORKER — src/query/vacuum.h
struct 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:580
struct 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_POSTPONEVACUUM_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 세 패스를 마친 뒤 이 함수가 다음을 한다.

  1. vacuum_Data 를 디스크 페이지에서 다시 로드.
  2. 충돌 시점에 IN_PROGRESS 였던 블록들을 AVAILABLE 로 되돌린 다 (INTERRUPTED 플래그 켜고). master가 다시 집어 가도록.
  3. vacuum_recover_lost_block_data (vacuum.c:5465) 를 호출 해, WAL 안에는 있었지만 vacuum data에 아직 기록되지 않은 블록을 메운다. 충돌이 MVCC 레코드 발행과 다음 vacuum_consume_buffer_log_blocks tick 사이에 일어나면 이런 블록이 생긴다.
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_state enum (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.
  • 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 블록 메우기.
  • 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, 버퍼 할당.
  • 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.h106
VACUUM_WORKER_STATE enumvacuum.h85
VACUUM_LOG_BLOCK_PAGES_DEFAULTvacuum.h82
VACUUM_MAX_WORKER_COUNTvacuum.h132
vacuum_data_entry (struct)vacuum.c104
vacuum_data_page (struct)vacuum.c194
vacuum_data (struct)vacuum.c350
vacuum_dropped_file (struct)vacuum.c580
vacuum_dropped_files_page (struct)vacuum.c588
vacuum_job_cursor (class)vacuum.c277
vacuum_master_task (class)vacuum.c813
vacuum_master_task::executevacuum.c3002
vacuum_initializevacuum.c1180
vacuum_finalizevacuum.c1416
vacuum_process_log_blockvacuum.c3251
vacuum_worker_allocate_resourcesvacuum.c3620
vacuum_finalize_workervacuum.c3689
vacuum_data_load_and_recovervacuum.c4183
vacuum_consume_buffer_log_blocksvacuum.c5096
vacuum_recover_lost_block_datavacuum.c5465
vacuum_log_add_dropped_filevacuum.c6121
vacuum_is_file_droppedvacuum.c6587
  • vacuum 소스는 src/query/ 아래에 있다 — src/transaction/ 이 아니다. find 로 검증 — src/query/vacuum.{c,h} 가 존재 하고 src/transaction/vacuum.* 는 없다. 함의 — 이 doc의 meta 와 frontmatter references: 는 초안 시점에 그에 맞게 수정 되었다. 원본 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_MASK 0x1FFFFFFFFFFFFFFF 가 실제 id를 추출한다 — 60비트, 약 1.15 × 10^18 블록까지가 소진 되기 전 한도다.

  • vacuum_Data.first_pagevacuum_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_entrywas_interrupted / set_interrupted accessor를 읽으며 검증. 플래그가 master에게 이 블록은 이전에 부분적으로 작업되었음 을 알려 준다. 그래서 worker는 이전 시도가 이미 정리한 레코드 를 건너뛸 수 있다 (대상 페이지 LSA가 이미 전진했기 때문이라 는 점이다).

  1. master tick 간격과 적응적 throttling. master 루프의 wake 간격, 그리고 시스템이 바쁠 때 속도를 낮추는 backpressure 메커니즘은 위치를 잡지 못했다. 추적 경로 — vacuum_master_task 의 cubthread daemon 등록을 읽고, should_interrupt_iteration / is_task_queue_full 메서드를 살피기.

  2. 워터마크 전진 트리거. log_Gl.hdr.oldest_visible_mvccid 가 언제 다시 계산되는가? 트랜잭션 commit / abort마다? 주기적 으로? 둘 다? 추적 경로 — oldest_visible_mvccid 에 쓰기를 하는 자리를 grep으로 찾고, logtb_complete_mvcc (cubrid-transaction.md) 와 cross-reference.

  3. heap vs btree 디스패치. VACUUM_HEAP_OBJECT 는 단순히 (vfid, oid) 다. worker가 vfid가 heap을 가리키는지 B+Tree 를 가리키는지 어떻게 결정하고, 정확한 서브시스템 정리 함수를 호출하는가? 추적 경로 — vacuum-execute 경로 본문을 읽기 (worker 상태가 EXECUTE 로 전이되는 자리 근처).

  4. 온라인 스키마 변경과의 상호작용. vacuum이 한 블록을 작업 하는 도중 그 블록이 만지는 B+Tree가 drop된다면, dropped-files 표가 그 파일을 따라가는 것은 막아 준다. 그런데 in-flight VACUUM_HEAP_OBJECT 리스트는 어떻게 되는가? 엔트리가 dropped-files를 필터링되는가, 아니면 worker가 페이지 fix 계층에서 ER_FILE_DROPPED 를 다루는가? 추적 경로 — execute 경로 안의 vacuum_is_file_dropped 호출자를 찾기.

  5. 경합 상황에서의 page-buffer private LRU 의미론. worker의 private LRU가 가득 찼고 다른 worker가 같은 페이지를 필요로 한다면 어떻게 되는가? 핸드오프인가, 글로벌 LRU를 통한 공유 인가? 추적 경로 — cubrid-page-buffer-manager.md 와 pgbuf_*_private_lru_* 경로를 살피기.

  6. 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.pdf
  • vacuum.pptx
  • knowledge/code-analysis/cubrid/cubrid-mvcc.md — 가시성 모델과 oldest_visible_mvccid 워터마크.
  • knowledge/code-analysis/cubrid/cubrid-log-manager.mdLOG_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}