콘텐츠로 이동

(KO) CUBRID compactdb — 오프라인 데이터베이스 압축과 페이지 조각 모음 유틸리티

목차

CUBRID에는 가비지 컬렉터가 있다. 온라인 vacuum 서브시스템 (cubrid-vacuum.md) 은 WAL을 앞으로 따라 걸으며 죽은 MVCC 버전을 그 자리에서 치운다. vacuum은 페이지 안의 slot 은 회수하지만 heap에서 페이지 자체 를 회수하지 않고, 옛 클래스 representation을 떨어뜨리지도 않으며, slotted-page의 자유 공간을 조각 모음하지도 않고, 사라진 객체를 가리키게 된 OID 컬럼을 NULL로 만들지도 않는다. 이 네 가지 문제를 처리하는 것이 오프라인 compactor의 일이다.

compactdb가 다루는 누적된 쓰레기는 다음과 같다.

  1. 끊어진 OID 참조. DB_TYPE_OID 타입의 컬럼이나 set 원소가 이미 삭제되어 vacuum까지 끝난 OID를 가리킬 수 있다. 누군가 referrer 행을 다시 쓰지 않는 한 그 참조는 그대로 남아 있다.
  2. 비어 버린 heap 페이지. 모든 slot이 해제된 heap 페이지도 파일 안에서는 여전히 한 페이지다. 디스크 매니저가 회수하지 않고, 버퍼 풀이 여전히 추적하며, 순차 스캔이 여전히 그 페이지 를 건드린다.
  3. slotted-page 내부 단편화. 비어 있지 않은 페이지조차 slot 배열 사이에 자유 공간이 흩어져 있어, 연속된 바이트 블록을 요구하는 insert를 막을 수 있다.
  4. 옛 클래스 representation. 컬럼을 변경하는 모든 ALTER TABLE 은 새 클래스 representation 을 만들어 낸다. heap의 어떤 행이라도 여전히 참조하고 있는 한 카탈로그는 모든 representation을 보존한다.

교과서적인 틀은 물리적 재구성 (physical reorganization) 이다. 행 단위 delete를 지원하는 모든 엔진은 어떤 형태로든 이를 출하 한다. 모든 구현은 두 개의 설계 결정에 따라 모양이 정해진다.

  1. 온라인이냐 오프라인이냐. 온라인은 살아 있는 DB를 세밀한 락을 잡으며 동작하지만 객체를 더 낮은 OID로 옮길 수 없다. 오프라인은 주소를 다시 쓰고 인덱스를 재구축할 수 있지만 워크로드를 막아 세운다.
  2. OID는 어떻게 처리하는가. 행을 옮기면 그 행을 가리키는 모든 인덱스와 OID를 들고 있는 모든 컬럼이 무효화된다. PostgreSQL VACUUM FULL 은 새 TID로 heap을 다시 쓰고 모든 인덱스를 재구축한다. Oracle ALTER TABLE MOVE 는 ROWID를 다시 쓰고 인덱스를 UNUSABLE로 표시한다. InnoDB OPTIMIZE TABLE 은 클러스터드 인덱스를 재구축한다. CUBRID은 세 번째 길을 택한다. 사용자 행을 재배치하지 않고, 비어 버린 페이지 를 쓸어내고 참조만 다시 쓴다. 모든 기존 OID는 그대로 유효하며, 대신 물리적 클러스터링은 포기한다.

모든 관계형 엔진은 어떤 형태로든 오프라인 (혹은 거의 오프라인) compactor를 출하한다. 그 모양이 한 줌의 레시피로 수렴한다.

Postgres VACUUM FULL / CLUSTER 는 heap 전체를 새 파일로 다시 쓰고 모든 인덱스를 처음부터 재구축한다. 새 heap, dead tuple 없음, 단편화 없음. 비용은 작업 동안의 AccessExclusiveLock 과 두 복사본을 위한 디스크 공간이다. CLUSTER 는 인덱스 순서 정렬을 추가한다. 둘 다 살아 있는 모든 튜플에 새 TID를 부여한다.

MySQL InnoDB OPTIMIZE TABLE 은 클러스터드 인덱스 (이게 곧 테이블이다) 를, 따라서 모든 보조 인덱스를, 새 테이블스페이스 안에 재구축한다. 온라인 DDL이 row log를 replay 해서 작업 대부분 의 시간 동안 reader와 동시 writer를 살려 둔다. 마지막 swap은 잠깐의 메타데이터 락이 필요하다.

Oracle ALTER TABLE … MOVE 는 세그먼트를 새 (혹은 같은) 테이블스페이스로 옮긴다. 새 ROWID가 모든 인덱스를 무효화하므로 ALTER INDEX … REBUILD 로 재구축해야 한다. Oracle 12c의 온라인 DBMS_REDEFINITION 은 테이블 가용성을 유지하지만, 오프라인 형태 는 여전히 정비 시간대에 흔히 쓰인다.

CUBRID이 자리 잡은 곳. compactdb는 살아남은 객체의 OID를 명시적으로 보존 한다. 행을 새 파일로 재배치하지 않고, heap 행을 따라 걸으며 끊어진 OID 참조를 NULL로 만들고 (Pass 1), 살아 남은 행을 옮기지 않고 비어 버린 heap 페이지를 회수하고 (Pass 2), slotted-page 자유 공간을 조각 모음한다 (Pass 3). OID가 안정적 으로 유지되기 때문에 모든 B+Tree 인덱스와 모든 외래 OID 컬럼이 손대지 않은 채로 압축을 견뎌 낸다. Postgres / Oracle / InnoDB 패턴과 뚜렷하게 갈리는 대목이다. 그 대가로 compactdb는 관련 행들을 물리적으로 한곳에 모을 수 없다. 그것은 진짜 테이블 재 작성을 요구하는 일이고, CUBRID은 그 일을 사용자 주도의 CREATE TABLE AS SELECT + rename 에 맡긴다. 유틸리티는 작업 중인 클래스 의 root에 IX_LOCK 을, 각 클래스 자체에 X_LOCK 을 잡고, 반복 경계마다 클래스별 락을 풀어 준다. 거의 오프라인 이다. 다른 클래스에 대한 다른 연결은 살아 있을 수 있지만, 압축 중인 클래스 는 배타적으로 잡혀 있다.

compactor는 클라이언트 측 드라이버 (src/executables/ 아래의 compactdb_cl.c, compactdb.c, compactdb_common.c) 와 서버 측 worker (src/storage/ 아래의 compactdb_sr.c) 로 갈라져 있다. 클라이언트는 DBA 사용자로 평범한 DB 세션을 열고, 선택된 클래스를 번호가 매겨진 세 패스를 돌리고, 세션을 닫는다. 서버 측 boot_compact_* 함수가 단일 인스턴스 가드를 강제해서 두 compactdb 실행이 끼어들지 못하게 한다.

flowchart TD
  A[compactdb CLI<br/>인자 파싱] --> B[db_login DBA<br/>db_restart database]
  B --> C[compactdb_start<br/>클래스 목록 결정]
  C --> D[compact_db_start<br/>CSECT_COMPACTDB_ONE_INSTANCE 서버 가드]
  D --> E[Pass 1<br/>boot_compact_classes 루프]
  E -->|반복마다| E1[server: boot_compact_db<br/>클래스 heap 순회]
  E1 -->|process_object<br/>인스턴스마다 X_LOCK| E2[끊어진 OID 참조 NULL화<br/>locator_attribute_info_force]
  E1 -->|delete_old_repr| E3[catalog_drop_old_representations]
  E -->|다음 클래스| E
  E --> F[Pass 2<br/>do_reclaim_addresses]
  F --> F1[클래스별: SCH-M lock<br/>다른 클래스가 가리키지 않는지 확인]
  F1 --> F2[heap_reclaim_addresses HFID<br/>비어 버린 페이지 해제]
  F --> G[Pass 3<br/>boot_heap_compact 루프]
  G --> G1[heap_compact_pages HFID<br/>slotted page 조각 모음]
  G --> H[catalog_reclaim_space<br/>file_tracker_reclaim_marked_deleted<br/>standalone 분기에서만]
  H --> I[compact_db_stop<br/>db_shutdown]

패스 번호는 소스 자체에서 따왔다. 각 단계에서 출력되는 메시지가 COMPACTDB_MSG_PASS1, _PASS2, _PASS3 다.

Pass 1 — 끊어진 OID 참조 고치기와 옛 representation 떨어뜨리기

섹션 제목: “Pass 1 — 끊어진 OID 참조 고치기와 옛 representation 떨어뜨리기”

compactdb_cl.c::compactdb_start 의 드라이버는 boot_compact_classes 를 루프로 호출하고, 각 호출이 max_processed_space 바이트 (pages * DB_PAGESIZE, pages ∈ [1, 20]) 까지 처리한다. 예산이 소진되면 호출이 반환 되고, 드라이버가 commit 한 뒤 다시 호출한다. 이 bounded-work 루프는 온라인 vacuum 패턴과 같다는 점이다. 트랜잭션을 짧게 유지해서 서버가 반복 경계마다 락을 풀 수 있도록 한다.

(last_processed_class_oid, last_processed_oid) 짝이 재개 가능한 cursor 다. 두 OID가 다음 호출이 시작해야 할 정확한 클래스와 인스턴스를 가리킨다. commit 후 재진입하면 cursor가 전진한다. abort (예: 락 매니저로부터의 ER_LK_UNILATERALLY_ABORTED) 면 같은 윈도우를 재시도한다.

서버 측에서 boot_compact_dbOID_EQ (class_oids + start_index, last_processed_class_oid) 로 시작 클래스를 찾고 다음과 같이 순회한다.

// boot_compact_db — src/storage/compactdb_sr.c (condensed)
for (i = start_index; i < n_classes; i++)
{
lock_ret =
lock_object_wait_msecs (thread_p, class_oids + i, oid_Root_class_oid, IX_LOCK, LK_UNCOND_LOCK,
class_lock_timeout);
if (lock_ret != LK_GRANTED)
{
total_objects[i] = COMPACTDB_LOCKED_CLASS;
OID_SET_NULL (last_processed_oid);
continue;
}
if (OID_ISNULL (last_processed_oid))
initial_last_repr_id[i] = heap_get_class_repr_id (thread_p, class_oids + i);
if (process_class (thread_p, class_oids + i, &hfid, max_space_to_process, &instance_lock_timeout,
&space_to_process, last_processed_oid, total_objects + i, failed_objects + i,
modified_objects + i, big_objects + i) != NO_ERROR) { /* rollback */ }
if (delete_old_repr && OID_ISNULL (last_processed_oid) && failed_objects[i] == 0
&& heap_get_class_repr_id (thread_p, class_oids + i) == initial_last_repr_id[i])
{
/* upgrade IX_LOCK -> X_LOCK; catalog_drop_old_representations; mark COMPACTDB_REPR_DELETED */
}
if (space_to_process == 0) break;
}

클래스 단위 루프를 지배하는 세 가지 invariant가 있다.

  • 초기 representation 스냅샷. 클래스 i 를 처리하기 전에 서버는 initial_last_repr_id[i] 를 기록한다. 처리가 끝난 뒤 옛 repr을 떨어뜨리기 전에 repr ID를 다시 읽는다. 동시 ALTER TABLE 이 IX_LOCK 경계 너머에서 끼어들어 ID를 바꿨다면 drop은 건너뛴다. 옛 repr이 지금은 새 repr 아래 쓰이는 행을 encoding 하고 있을 수 있기 때문이다.
  • repr drop은 X_LOCK이 필요하다. catalog_drop_old_representations 를 호출하기 전에 클래스 root에 대한 IX_LOCK이 X_LOCK으로 승격 된다. 실패하면 그 클래스는 손대지 않은 채 남는다.
  • 락 획득 실패는 치명적이지 않다. class_lock_timeout 안에 IX-락을 잡지 못한 클래스는 COMPACTDB_LOCKED_CLASS 로 표시되고 건너뛴다. 다음 반복에서 재시도된다.

process_class 는 그 다음 xlocator_lock_and_fetch_all 로 인스턴스를 가져오고 행마다 process_object 를 호출한다.

// process_object — src/storage/compactdb_sr.c (condensed)
scan_code = locator_lock_and_get_object (thread_p, oid, &upd_scancache->node.class_oid, &copy_recdes, upd_scancache,
X_LOCK, COPY, NULL_CHN, LOG_WARNING_IF_DELETED);
for (i = 0, value = attr_info->values; i < attr_info->num_values; i++, value++)
{
error_code = process_value (thread_p, &value->dbvalue);
if (error_code > 0)
{
value->state = HEAP_WRITTEN_ATTRVALUE;
atts_id[updated_n_attrs_id++] = value->attrid;
}
}
if (updated_n_attrs_id > 0 || /* representation drift */)
locator_attribute_info_force (thread_p, &upd_scancache->node.hfid, oid, attr_info, atts_id, updated_n_attrs_id,
LC_FLUSH_UPDATE, SINGLE_ROW_UPDATE, upd_scancache, &force_count, false,
REPL_INFO_TYPE_RBR_NORMAL, DB_NOT_PARTITIONED_CLASS, NULL, NULL, NULL,
UPDATE_INPLACE_NONE, &copy_recdes, false);

process_value 가 Pass 1의 심장이다.

// process_value — src/storage/compactdb_sr.c (condensed)
case DB_TYPE_OID:
{
OID *ref_oid = db_get_oid (value);
if (OID_ISNULL (ref_oid)) break;
heap_scancache_quick_start (&scan_cache);
scan_cache.mvcc_snapshot = logtb_get_mvcc_snapshot (thread_p);
scan_code = heap_get_visible_version (thread_p, ref_oid, &ref_class_oid, NULL, &scan_cache, PEEK, NULL_CHN);
heap_scancache_end (thread_p, &scan_cache);
if (scan_code != S_SUCCESS)
{
OID_SET_NULL (ref_oid);
return_value = 1; /* mark "this attribute changed" */
}
}
case DB_TYPE_SET: case DB_TYPE_MULTISET: case DB_TYPE_SEQUENCE:
return_value = process_set (thread_p, db_get_set (value));

가시성 검사는 갓 잡힌 MVCC 스냅샷을 heap_get_visible_version 으로 한다. 대상에 가시 버전이 없으면 attribute가 NULL로 바뀌고 written으로 표시된다. set 타입에는 서버가 process_set 으로 재귀한다. non-zero 반환은 모두 locator_attribute_info_force 를 트리거하고, 이 함수가 B+Tree 정비와 복제 hook을 함께 거치며 행을 다시 쓴다.

UPDATE_INPLACE_NONE 이 의미가 있다. Pass 1은 정상 MVCC update (새 버전, 옛 버전은 vacuum될 때까지 가시) 를 만들어 내지, in-place 재작성을 하지 않는다. 그래서 Pass 1은 죽은 버전을 남겨 두고 온라인 vacuum이 나중에 치우게 한다. 그리고 패스 도중 충돌이 나면 다른 WAL 로깅 update와 똑같이 복구된다.

반복별 에러 정책은 다음과 같다. NO_ERROR → commit 후 계속. ER_LK_UNILATERALLY_ABORTEDtran_abort_only_client, cursor를 유효한 것으로 보고 계속. ER_FAILED → abort 후 종료. 클래스별 합계 (total_objects, failed_objects, modified_objects, big_objects) 가 누적되어 show_statistics 로 노출된다.

Pass 2 — 비어 버린 heap 페이지 회수

섹션 제목: “Pass 2 — 비어 버린 heap 페이지 회수”

Pass 1이 끊어진 OID를 모두 고친 뒤, 드라이버는 do_reclaim_addresses 를 돌린다. 클래스마다 다음을 한다.

// do_reclaim_class_addresses — src/executables/compactdb_cl.c (condensed)
db_set_isolation (TRAN_READ_COMMITTED);
locator_fetch_class (sm_Root_class_mop, DB_FETCH_QUERY_WRITE); /* IX_LOCK on root */
class_ = locator_fetch_class (class_mop, DB_FETCH_WRITE); /* SCH-M lock */
locator_flush_all_instances (class_mop, DECACHE);
/* reachability analysis */
if (class_->flags & SM_CLASSFLAG_SYSTEM) can_reclaim_addresses = false;
else if (class_->flags & SM_CLASSFLAG_REUSE_OID) can_reclaim_addresses = true;
else
{
lmops = locator_get_all_class_mops (DB_FETCH_CLREAD_INSTREAD, is_not_system_class);
class_instances_can_be_referenced (class_mop, parent_mop, &class_can_be_referenced,
any_class_can_be_referenced, lmops->mops, lmops->num);
can_reclaim_addresses = !class_can_be_referenced && !*any_class_can_be_referenced;
}
if (can_reclaim_addresses)
heap_reclaim_addresses (hfid);

도달 가능성 (reachability) 이 부담을 떠받치는 안전 검사다. class_referenced_by_class 가 다른 모든 클래스를 따라 걷고, class_referenced_by_domain 이 각 attribute의 domain을 살핀다.

// class_referenced_by_domain — src/executables/compactdb_cl.c (condensed)
if (type == DB_TYPE_OBJECT)
{
DB_OBJECT *class_ = db_domain_class (crt_domain);
if (class_ == NULL)
*any_class_can_be_referenced = true; /* "object" wildcard */
else if (referenced_class == class_ || db_is_subclass (referenced_class, class_) > 0)
*class_can_be_referenced = true;
}
else if (pr_is_set_type (type))
class_referenced_by_domain (referenced_class, db_domain_set (crt_domain),
class_can_be_referenced, any_class_can_be_referenced);

두 개의 boolean이 세 가지 상태를 encoding한다. any_class_can_be_referenced어떤 스키마 attribute든 제약 없는 OBJECT domain (any object 와일드카드) 을 가질 때 켜진 다. 한 번 켜지면 이 실행 동안 sticky하게 남아 Pass 2를 글로벌 하게 비활성화한다. class_can_be_referenced 는 현재 클래스 (또는 partition 부모) 를 구체적으로 포함하는 domain을 표시한다. 둘 다 false → 안전 → heap_reclaim_addresses 호출. 두 개의 플래그 기반 단축 경로가 있다. SM_CLASSFLAG_REUSE_OID 클래스 는 항상 회수 가능 (그 클래스에 대한 OID 참조를 누구도 들 수 없게 되어 있다). SM_CLASSFLAG_SYSTEM 클래스는 항상 건너뛴다 (도달 가능성 검사가 시스템 클래스를 포함하지 않는다).

xheap_reclaim_addresses (서버 측, heap_file.c) 는 heap 파일을 따라 걸으며 모든 slot이 비어 있는 페이지를 해제한다. 살아 남은 행을 옮기지 않는다. 그 전제 조건은 누군가 dereference 할 수 있는 모든 OID가 여전히 유효해야 한다는 것이다. Pass 1이 그 조건을 참으로 만들어 준다는 점이다.

세 번째 패스는 클래스별로 boot_heap_compact 를 부른다. 이것이 서버에서 boot_heap_compact_pages 를 부르고, 다시 heap_file.cheap_compact_pages (thread_p, class_oid) 를 부른다. 페이지 당 작업은 표준 slotted-page 압축이다. 살아 있는 레코드를 자유 공간 영역의 시작쪽으로 다시 packing 하고, slot 테이블을 갱신 하고, 페이지별 자유 공간 추적기를 동기화한다. 페이지 사이로 행이 옮겨가지 않는다. 페이지 안의 자유 공간 layout만 바뀐다.

// boot_heap_compact_pages — src/storage/compactdb_sr.c
int
boot_heap_compact_pages (THREAD_ENTRY * thread_p, OID * class_oid)
{
if (boot_can_compact (thread_p) == false)
{
return ER_COMPACTDB_ALREADY_STARTED;
}
return heap_compact_pages (thread_p, class_oid);
}

각 클래스별 호출은 자기 트랜잭션을 commit한다. Pass 1, Pass 2와 마찬가지로, 루프는 tran_abort_only_client 를 부르고 계속 진행 함으로써 ER_LK_UNILATERALLY_ABORTED 를 견딘다.

단일 인스턴스 가드와 재시작 안전성

섹션 제목: “단일 인스턴스 가드와 재시작 안전성”

compactdb_sr.c 안의 boot_compact_startboot_compact_stop 이 critical section CSECT_COMPACTDB_ONE_INSTANCE 아래에서 유틸리티 전체를 보호한다. 상태는 두 개의 file-scope 변수다.

// boot_compact_start / boot_compact_stop — src/storage/compactdb_sr.c
static bool compact_started = false;
static int last_tran_index = -1;
int
boot_compact_start (THREAD_ENTRY * thread_p)
{
if (csect_enter (thread_p, CSECT_COMPACTDB_ONE_INSTANCE, INF_WAIT) != NO_ERROR)
return ER_FAILED;
current_tran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);
if (current_tran_index != last_tran_index && compact_started == true)
{
csect_exit (thread_p, CSECT_COMPACTDB_ONE_INSTANCE);
return ER_COMPACTDB_ALREADY_STARTED;
}
last_tran_index = current_tran_index;
compact_started = true;
csect_exit (thread_p, CSECT_COMPACTDB_ONE_INSTANCE);
return NO_ERROR;
}

의미는 이렇다. 한 번에 하나의 트랜잭션만 활성 compactdb 세션이 될 수 있다. 두 번째 compactdb가 첫 번째가 살아 있는 동안 시작 하려 하면 ER_COMPACTDB_ALREADY_STARTED 를 받는다. 첫 번째 compactdb의 프로세스가 패스 도중 죽었다면, 같은 트랜잭션 인덱스 로부터의 다음 시작은 성공한다 (인덱스는 새 프로세스가 재사용 하지만 compact_started 는 이전 소유자로부터 여전히 true다. 이로 인해 생기는 미묘함은 Cross-check 노트 를 보라).

모든 패스의 모든 반복이 자기 트랜잭션을 commit하므로, 실행 중 충돌은 깔끔한 재시작 지점이다. 복구 후 운영자는 그저 compactdb를 다시 시작하면 된다. Pass 1의 cursor는 클래스 OID 경계에서 다시 시작하고, Pass 2 / Pass 3은 클래스별이라 commit 되지 않은 클래스의 작업만 다시 한다. compactdb 전용 복구 경로는 없으며, 부팅 시점에 엔진이 돌리는 평범한 WAL 복구만 있다는 점이다.

저장소에는 int compactdb (UTIL_FUNCTION_ARG * arg) 를 정의하는 compactdb 변환 단위가 두 개 있다.

  • src/executables/compactdb_cl.c — 위에서 설명한 클라이언트 /서버 형태. cubrid compactdb CLI가 SERVER_MODE 아래에서 호출한다. 살아 있는 서버에 연결하고 클래스 락을 잡으며, boot_compact_classes / boot_heap_compact / do_reclaim_addresses 로 세 패스를 구동한다.
  • src/executables/compactdb.c — standalone 형태 (SA_MODE). 엔진을 in-process로 링크하고 locator_fetch_all 로 직접 heap을 따라 걷는다. PRM_ID_COMPACTDB_PAGE_RECLAIM_ONLY 로 게이팅된다. 그 Phase 3은 추가로 catalog_reclaim_spacefile_tracker_reclaim_marked_deleted 를 호출하며, 이는 다른 프로세스가 DB를 열고 있지 않을 것을 요구한다.

두 바이너리는 compactdb_common.c 의 클래스 목록 결정 코드를 공유하지만, 패스 자체는 약간 다른 invariant로 독립적으로 구현 한다.

CLI 노브는 compactdb_cl.c::compactdb 에서 파싱된다. --pages-commited-once ([1, 20] 으로 클램프, Pass 1 바이트 예산을 위해 DB_PAGESIZE 와 곱함), --instance-lock-timeout--class-lock-timeout (각각 [1, 10] 초로 클램프되고, 1000을 곱해서 lock_object_wait_msecs 로 넘김), --delete-old-repr (Pass 1의 representation drop 활성화), --input-class-file (클래스 이름이 들어 있는 파일. CLI 인자와 상호 배타), --standby-cs-mode (HA standby 노드를 위해 클라이언트 타입을 DB_CLIENT_TYPE_ADMIN_COMPACTDB_WOS 로 전환).

standalone 형태는 추가로 PRM_ID_COMPACTDB_PAGE_RECLAIM_ONLY 를 참조한다. 0 = 세 패스 모두, 1 = Pass 1 건너뛰기 (페이지 회수 만), 2 = Pass 1과 Pass 2 건너뛰기 (카탈로그와 tracker-삭제 파일 회수만).

compactor의 심볼 표면을 책임별로 묶는다. 심볼명을 anchor로 잡고, 다음 절 끝의 위치 표가 각각을 updated: 시점의 (파일, 라인) 에 핀으로 박는다.

CLI 드라이버. compactdbcompactdb.c (standalone) 와 compactdb_cl.c (클라이언트/서버) 양쪽의 진입점이다. 각각이 CLI 인자를 파싱하고, db_login / db_restart 를 부른 뒤 compactdb_start 를 부른다. 클라이언트/서버 쪽 compactdb_startcompact_db_startcompact_db_stop 사이에서 세 패스 루프를 돌린다. standalone 쪽 compactdb_startPRM_ID_COMPACTDB_PAGE_RECLAIM_ONLY 로 게이팅된 phase1 / phase2 / phase3 goto 흐름을 따른다. 보조 함수들 — compactdb_usage / compact_usage (메시지 카탈로그로 help 출력), show_statistics (클래스별 요약), get_name_from_class_oid, find_oid.

클래스 목록 결정. 두 CLI가 compactdb_common.c 에서 공유 한다. get_class_mopslocator_find_class 로 이름을 풀고 (소유자 한정자와 case-folding 처리), get_class_mops_from_file 이 파일에서 줄 단위로 이름을 읽고, get_num_requested_class 가 배열 크기를 잡는다.

Pass 2 — 도달 가능성과 페이지 회수 (클라이언트). compactdb_cl.c 안 — do_reclaim_addresses 는 클래스별 루프다. do_reclaim_class_addressesTRAN_READ_COMMITTED 로 전환 하고 SCH-M 락을 잡고 도달 가능성을 돌린 뒤 heap_reclaim_addresses 를 부른다. 도달 가능성 walk는 class_instances_can_be_referencedclass_referenced_by_classclass_referenced_by_attributesclass_referenced_by_domain 이고, is_not_system_classlocator_get_all_class_mops 를 위한 필터다.

서버 측 worker (compactdb_sr.c). boot_compact_db 가 Pass 1 진입점이다. heap 인스턴스 fetch 루프인 process_class 를 부르고, 그것이 process_object (행을 락 잡고, attribute 순회, 변경 시 force-write) 를 부르고, 그것이 process_value (각 DB_VALUE 검사. DB_TYPE_OID 면 가시성 확인 후 죽었으면 참조 NULL화. set 타입이면 process_set 으로 재귀) 를 부른다. desc_disk_to_attr_infoRECDESHEAP_CACHE_ATTRINFO 로 변환한다. is_class 는 클래스 객체를 재작성에서 제외하는 predicate다. boot_heap_compact_pages 는 Pass 3 서버 진입점 이다. boot_compact_start / boot_compact_stop / boot_can_compactCSECT_COMPACTDB_ONE_INSTANCE 둘레의 단일 인스턴스 가드를 이룬다.

compactor가 부르는 cross-module 진입점들. xlocator_lock_and_fetch_all (인스턴스 단위 락과 함께 bulk fetch), locator_lock_and_get_object (단일 인스턴스 fetch + X-락), locator_attribute_info_force (B+Tree와 복제 hook으로 행을 force-write), heap_get_visible_version (Pass 1이 쓰는 MVCC 가시성 확인), heap_get_class_repr_id (현재 representation ID. 동시 ALTER 감지에 쓰임), catalog_drop_old_representations (옛 repr drop), heap_file.cxheap_reclaim_addressesheap_compact_pages (Pass 2 / Pass 3 작업), catalog_reclaim_spacefile_tracker_reclaim_marked_deleted (standalone Pass 3에서만 호출).

카운터 sentinel과 CLI 옵션. COMPACTDB_LOCKED_CLASS, COMPACTDB_INVALID_CLASS, COMPACTDB_UNPROCESSED_CLASS, COMPACTDB_REPR_DELETED 가 클래스별 결과를 encoding 하기 위해 total_objects[i] 에 저장되는 sentinel 값이다. COMPACT_MIN_PAGES / COMPACT_MAX_PAGES 가 페이지 예산을 [1, 20] 으로 클램프한다. COMPACT_INSTANCE_MIN/MAX_LOCK_TIMEOUTCOMPACT_CLASS_MIN/MAX_LOCK_TIMEOUT 이 timeout 노브를 [1, 10] 초로 클램프한다. 옵션 문자열은 COMPACT_VERBOSE_S, COMPACT_PAGES_COMMITED_ONCE_S, COMPACT_INSTANCE_LOCK_TIMEOUT_S, COMPACT_CLASS_LOCK_TIMEOUT_S, COMPACT_INPUT_CLASS_FILE_S, COMPACT_DELETE_OLD_REPR_S, COMPACT_STANDBY_CS_MODE_S 다.

심볼파일라인
boot_compact_dbsrc/storage/compactdb_sr.c517
process_class (server)src/storage/compactdb_sr.c333
process_object (server)src/storage/compactdb_sr.c194
process_value (server)src/storage/compactdb_sr.c85
process_set (server)src/storage/compactdb_sr.c157
desc_disk_to_attr_infosrc/storage/compactdb_sr.c297
is_class (server)src/storage/compactdb_sr.c68
boot_heap_compact_pagessrc/storage/compactdb_sr.c680
boot_compact_startsrc/storage/compactdb_sr.c695
boot_compact_stopsrc/storage/compactdb_sr.c725
boot_can_compactsrc/storage/compactdb_sr.c754
compactdb (standalone)src/executables/compactdb.c97
compactdb_start (standalone)src/executables/compactdb.c172
process_class (standalone)src/executables/compactdb.c361
process_object (standalone)src/executables/compactdb.c492
process_value (standalone)src/executables/compactdb.c534
disk_update_instancesrc/executables/compactdb.c652
update_indexessrc/executables/compactdb.c770
compactdb (client/server)src/executables/compactdb_cl.c783
compactdb_start (client/server)src/executables/compactdb_cl.c252
do_reclaim_addressessrc/executables/compactdb_cl.c927
do_reclaim_class_addressessrc/executables/compactdb_cl.c1012
class_instances_can_be_referencedsrc/executables/compactdb_cl.c1286
class_referenced_by_classsrc/executables/compactdb_cl.c1321
class_referenced_by_attributessrc/executables/compactdb_cl.c1406
class_referenced_by_domainsrc/executables/compactdb_cl.c1435
show_statisticssrc/executables/compactdb_cl.c136
get_class_mopssrc/executables/compactdb_common.c92
get_class_mops_from_filesrc/executables/compactdb_common.c186
xheap_reclaim_addressessrc/storage/heap_file.c6227
heap_compact_pagessrc/storage/heap_file.c17562
catalog_reclaim_spacesrc/storage/system_catalog.c2725
file_tracker_reclaim_marked_deletedsrc/storage/file_manager.c10687

단일 인스턴스 가드는 프로세스에 취약하다. compact_started / last_tran_index 짝은 file-scope 서버 상태다. compactdb 클라이언트가 compact_db_stop 을 부르지 않고 죽으면, 다음 시도는 stale last_tran_index 와 함께 compact_started == true 를 보고 ER_COMPACTDB_ALREADY_STARTED 로 거절된다. 서버가 재시작되거나 원래의 트랜잭션 인덱스가 재사용될 때까지 그렇다는 점이다. standalone 형태는 이를 비껴간다. 그 서버는 자기 프로 세스 안에서만 산다.

Pass 1과 vacuum의 중복. process_value지금 이 순간에 잡힌 MVCC 스냅샷을 가시성을 확인한다. 클래스 단위 IX_LOCK 은 이 클래스의 컬럼이 참조하는 다른 클래스의 행에 대한 vacuum 을 배제하지 않는다. vacuum이 동시에 돌고 있으면, 곧 죽을 행이 끊어진 것으로 표시되어 컬럼이 약간 일찍 NULL화될 수 있다. 실제 로는 무해하다. 운명이 결정된 행을 가리키는 soft pointer 자리에 NULL이 들어가는 것이 결과다. 다만 MVCC 상호작용으로 짚어 둘 만하다.

도달 가능성은 보수적이다. 분석은 domain 타입 만 살피지 런타임 값을 보지 않는다. 스키마 어디든 단 하나의 OBJECT domain 컬럼이 있으면 any_class_can_be_referenced 가 켜지고 Pass 2가 글로벌하게 비활성화된다. OBJECT 를 폭넓게 쓰는 스키마에서는 Pass 2가 거의 아무 일도 하지 못할 것이다.

Standalone과 C/S Pass 1이 다르다. standalone 형태 (compactdb.c) 는 도달 가능성을 완전히 건너뛴다. 항상 Pass 1 다음에 Pass 2를 무조건 돌린다. C/S 형태 (compactdb_cl.c::do_reclaim_class_addresses) 는 클래스마다 분석을 돌린다. Standalone은 DBA 전용 정비 시간대용이고, C/S는 공유 서버 환경용이다.

disk_update_instance 의 재시도 경로. compactdb.c 의 코드는 desc_obj_to_disk 를 돌리고, 크기 오버플로 시 더 큰 record 버퍼를 (DB_PAGESIZE 단위로 라운딩해서) 다시 할당한 뒤 한 번 더 시도한다. 두 번째 오버플로면 0을 반환한다. 세 번째 시도는 없다.

update_indexes 는 마지막 on-disk 버전을 읽는다. standalone 은 활성 MVCC 스코프 없이 돌고 인덱스 키 delta 계산을 위해 물리 적으로 가장 최신 행이 필요하기 때문에, visible-version이 아니라 heap_get_last_version 을 쓴다.

compactdb_sr.cis_class. 이 정적 보조 함수는 클래스 MOP 참조를 실수로 재작성하는 일을 막는다 (OID_EQ (class_oid, oid_Root_class_oid)). 같은 게이트가 카탈로그가 클래스 객체를 별도로 처리하는 코드에 암시적으로 들어 있어서, 실제 효과는 작다.

  • Pass 1은 locator_lock_and_get_object 로 인스턴스마다 X_LOCK 을 잡고, 그 위에 locator_attribute_info_force 가 다시 락을 건다. process_object 헤더는 “oid는 locator_lock_and_get_object 에서 이미 잡혀 있음” 이라고 적혀 있는데도 force 경로가 락을 한 번 더 acquire 한다. 짐작컨대 re-entrant short-circuit이지만 그 근거가 명시되어 있지 않다는 점이다.
  • SCH-M 클래스 락 없이 Pass 2를 할 수 있는가? 현재 경로는 heap_reclaim_addresses 동안 그 클래스에 대한 모든 접근을 잠재운다. 페이지마다 latch 프로토콜과 이 페이지는 회수 중 비트를 두면 HA 환경에서 더 좋겠지만, 동시 insert와의 안전성 이 자명하지 않다.
  • standalone Pass 1의 TODO는 Pass 1이 실패한 항목이 있을 때 Pass 2의 전제 조건 (끊어진 참조 없음) 이 실제로는 보장되지 않는다는 점을 인정하고 있다. 실제 워크플로는 운영자가 failed_objects 를 직접 확인하는 데 의존한다.

CUBRID 너머 — 비교 설계와 연구 동향

섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향”

CUBRID의 재배치 없는 compactor는 흔치 않다. 주류 패턴은 압축 이 row-id 재작성을, 따라서 인덱스 재구축을 함의한다고 가정한다. 안정 OID는 인덱스와 외래 OID 컬럼이 압축을 손대지 않은 채 견뎌 내게 해 주지만, CUBRID이 CLUSTER 나 InnoDB의 클러스터드 인덱스 재구축이 가져다주는 물리적 클러스터링 이득은 결코 회복 하지 못한다는 뜻이기도 하다. OID 그래프 스키마 (CUBRID의 OO 유산) 에는 안정 OID가 기능이고, 스타 스키마 OLAP 워크로드에는 그렇지 않다.

현대 연구는 버퍼된 redirection을 동반하는 in-place 재구성 으로 움직여 왔다. SAP HANA의 delta-merge, Hyper / Umbra, SingleStore 모두가 transient forwarding layer로 살아 있는 참조를 유효하게 유지하면서 백그라운드에서 행을 재배치한다. CUBRID의 disk-resident, 객체 유산 모델에 곧장 매핑되는 것은 없지만, forwarding layer 아이디어는 재배치를 원하는 어떤 오프라인 compactor에도 맞는 모양이다.

CUBRID 모델 안에서의 더 단순한 두 가지 확장이 있다. (a) Pass 2의 도달 가능성 절벽을 런타임 attribute 값 샘플링으로 누그러 뜨리기 (오늘은 단 하나의 OBJECT 컬럼이 Pass 2를 글로벌하게 끈다), (b) 버퍼 매니저에서 페이지별 liveness를 추적하는 온라인 Pass 2 / Pass 3. (b) 의 어려운 부분은 도달 가능성이다. OBJECT 컬럼이 있는 환경의 온라인은 잠재적으로 참조할 수 있는 모든 클래스에 대한 비관적 락이거나, 외래 OID 쓰기마다 낙관적 충돌 검사를 요구한다. 어느 쪽도 채택되지 않았다. 오프라인 compactor가 운영자의 도구로 남아 있는 이유다.

코드 경로는 frontmatter references: 에 나열되어 있고 위 위치 표에 라인 번호로 핀이 박혀 있다. 형제 문서들 — cubrid-vacuum.md (compactdb가 보완하는 온라인 MVCC 회수), cubrid-heap-manager.md (compactdb가 회수하고 조각 모음하는 heap-file 계층), cubrid-disk-manager.md (그 밑의 볼륨 / 섹터 계층). 교과서 틀 — Silberschatz, Korth, Sudarshan, Database System Concepts, 저장과 파일 구조 장.