(KO) CUBRID 트랜잭션 — TDES, 격리 수준, 그리고 savepoint
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 데이터베이스에서 트랜잭션은 엔진이 클라이언트에 파는 원자성 (atomicity) 과 격리성 (isolation) 의 단위다. Database Internals (Petrov) 5장 §Transactions 는 ACID 의 네 글자를 책임 지는 주체가 서로 다르다는 점을 강조한다. 원자성 (ACID-A) 은 복구 매니저가 WAL 과 CLR 로 책임지고, 내구성 (ACID-D) 은 commit 시점의 로그 force-flush 가 책임진다. 일관성 (ACID-C) 은 데이터 측 제약이 책임지고, 격리성 (ACID-I) 은 lock 매니저와 MVCC 가시성 기계가 함께 책임진다. 트랜잭션 모듈은 이 네 가닥이 한 지점에서 만나는 허브에 위치하면서 트랜잭션별 상태 를 소유한다. lock 매니저, 로그 매니저, MVCC 테이블, 복구 매니저 가 모두 그 상태를 읽고 쓴다.
이 상태의 단위가 트랜잭션 descriptor 다. CUBRID 에서는 TDES, PostgreSQL 에서는 PROC, InnoDB 에서는 trx_t 로 부른다. descriptor 가 지는 의무가 길다.
- 연결이 끊기거나 다시 붙어도 변하지 않는 안정 식별자 (
trid). 로그 레코드 모두에 이 trid가 박혀 들어간다. - 라이프사이클 상태 (active, committed, aborted, in-doubt 등). 복구의 analysis 패스가 재시작 시 이 상태를 다시 만들어 낸다.
- 격리 수준. 이 값이 access 경로에서 가시성과 lock 획득의 모양 을 결정한다.
- 일련의 LSA (head, tail, undo-next, postpone-next, savepoint, top-op). 이 LSA들이 복구와 rollback이 트랜잭션을 거꾸로 걸어가는 데 필요한 닻이다.
- 그 외 부수 상태 — modified-classes 레지스트리, 복제 레코드, unique-index 통계, lob locator. commit / abort 시점에 정리 되어야 하는 자투리들이다.
이 모델을 실제로 구현할 때 모든 엔진이 마주치는 두 갈림길이 있고, 그 답이 본 문서의 골격을 만든다.
- TDES 가 어디에 살고 어떻게 명명되는가. 교과서적인 답은
트랜잭션 인덱스로 색인되는 고정 크기 트랜잭션 테이블이다.
변형은 테이블이 정적인지 동적인지, 인덱스를 어떻게 재사용
하는지, descriptor 안에서 hot 필드와 cold 필드를 분리하는지
정도다. CUBRID 은 고정 크기 테이블 측이며, 연속된 영역에서
슬롯을 할당하고
hint_free_index로 빈 슬롯을 빠르게 찾는 방식을 쓴다. - 격리를 어디서 강제하는가 — access 경계인가, statement 경계인가, 아니면 둘 다인가. 순수 SI 엔진은 가시성으로 읽기 격리를 강제한다. 순수 2PL 엔진은 lock으로 강제한다. 하이브리드 엔진 — CUBRID이 그렇다 — 은 SERIALIZABLE / REPEATABLE READ에서 스냅샷을 잡으면서 동시에 key-range 락도 잡는다. TDES 위의 isolation 필드가 이 디스패치의 키다.
이 두 질문의 답이 분명해지면, TDES 위 다른 모든 상태는 그 답을 구현하기 위해 존재하는 부속이라는 점이 보인다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”nested rollback, 격리 수준 토글, 복구를 지원하는 모든 관계형 엔진은 트랜잭션 descriptor 주변에서 비슷한 패턴을 채택한다.
트랜잭션별 descriptor 테이블
섹션 제목: “트랜잭션별 descriptor 테이블”작은 정수 인덱스 (transaction index) 로 색인되는 고정 크기
descriptor 배열이다. 절충은 분명하다 — 고정 크기는 클라이언트
수의 상한이 테이블 크기에 묶이지만, 인덱스 색인이 O(1)이고
메모리 레이아웃이 캐시 친화적이다. PostgreSQL은
MaxBackends 크기의 PROC 배열을 쓰고, InnoDB은
trx_sys->trx_list 를, SQL Server는 tid 해시를 쓴다. CUBRID은
고정 배열 진영이다.
TDES enum으로서의 격리 수준
섹션 제목: “TDES enum으로서의 격리 수준”격리는 TDES별 enum이며 보통 3-4 값을 가진다 (READ COMMITTED, REPEATABLE READ, SERIALIZABLE; 일부 엔진은 READ UNCOMMITTED를 추가). 이 값을 세 곳에서 읽는다. 첫째, 스냅샷 획득 시 (어떤 모양의 MVCC 스냅샷을 만들 것인가). 둘째, lock 획득 시 (key-range 락을 잡을 것인가 말 것인가). 셋째, statement 경계 시 (cursor stability 격리에서 짧은 락을 일찍 풀 것인가). 모든 엔진이 같은 3-way 스위치를 쓴다는 뜻이다.
Nested top-operation / 시스템 op
섹션 제목: “Nested top-operation / 시스템 op”여러 페이지를 atomic하게 만지는 연산 — B+Tree split, heap overflow 할당, 스키마 변경 — 은 서브트랜잭션 단위의 복구가 필요 하다. CUBRID 에서 system op, PostgreSQL 에서 subxact, InnoDB 에서 mtr 이라 부른다. TDES 는 진행 중인 system op 를 스택으로 관리한다. commit이 일어나면 top frame이 pop되고, 그 frame의 로그 범위가 부모로 합쳐진다. abort가 일어나면 frame이 pop되면서 그 범위가 rollback된다. 즉 트리 구조가 아니라 재귀-스택 구조다.
명명된 LSA로서의 savepoint
섹션 제목: “명명된 LSA로서의 savepoint”savepoint는 “savepoint 생성 시점의 가장 최근 로그 레코드 LSA에
이름을 붙인 것” 이다. rollback-to-savepoint는 “현재 tail LSA에서
savepoint LSA까지 모든 로그 레코드를 undo한다” 라는 뜻이다.
구현은 TDES 위의 사슬이다 — savept_lsa 와 각 savepoint 로그
레코드 안의 prv_savept 가 그 사슬을 만든다. 복구 시점에 사슬
을 다시 세우는 일도 같은 정의를 따른다.
Flag 모음이 아닌 상태 머신으로서의 lifecycle
섹션 제목: “Flag 모음이 아닌 상태 머신으로서의 lifecycle”순진한 엔진은 bool committed 와 bool aborted 두 개를 둔다.
실제 엔진은 10 개 이상의 값을 가진 enum 을 쓴다. 2PC 와
postpone 동작이 두 boolean 으로는 표현할 수 없는 전이를 만들어
내기 때문이다. committed-with-postpone, committed-informing-
participants, 2PC-prepared, unilaterally-aborted 같은 상태들이
그 예다. CUBRID 의 TRAN_STATE 는 15 개 값을 가진다.
PostgreSQL 은 2PC 가 별도 서브시스템이라 더 적게 쓰고, InnoDB
은 서버 측 2PC 가 없어 더 적다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| Transaction identifier | LOG_TDES::trid (TRANID) |
| Transaction descriptor | LOG_TDES (log_impl.h) |
| Transaction table | TRANTABLE log_Gl.trantable (log_impl.h) |
| Transaction state enum | TRAN_STATE — 15개 상태 (log_comm.h) |
| Isolation level enum | DB_TRAN_ISOLATION (compat/dbtran_def.h), TRAN_ISOLATION 으로 별칭 |
| Active / committed / aborted predicates | LOG_ISTRAN_ACTIVE, LOG_ISTRAN_COMMITTED, LOG_ISTRAN_ABORTED 매크로 (log_impl.h) |
| MVCC info (가시성, 스냅샷) | LOG_TDES::mvccinfo (MVCC_INFO) |
| Nested top-op 스택 | LOG_TDES::topops (LOG_TOPOPS_STACK, log_impl.h) |
| Top-op 로그 범위 | LOG_TOPOPS_ADDRESSES { lastparent_lsa; posp_lsa } per stack frame |
| Savepoint | LOG_TDES::savept_lsa + LOG_REC_SAVEPT 사슬 |
| Postpone (commit 시 deferred 동작) | LOG_TDES::posp_nxlsa + log_postpone_cache m_log_postpone_cache |
| Modified-class 레지스트리 | LOG_TDES::m_modified_classes (tx_transient_class_registry) |
| Per-tran B+Tree unique 통계 | LOG_TDES::m_multiupd_stats (multi_index_unique_stats) |
| 2PC coordinator info | LOG_TDES::coord (LOG_2PC_COORDINATOR *, cubrid-2pc.md 에서 다룸) |
| 2PC global tran info | LOG_TDES::gtrinfo (LOG_2PC_GTRINFO) |
| 복구 시점 TDES 부가 정보 | LOG_TDES::rcv (LOG_RCV_TDES) |
| 서버 측 commit 진입점 | xtran_server_commit (transaction_sr.c) |
| 서버 측 abort 진입점 | xtran_server_abort (transaction_sr.c) |
| 클라이언트 측 commit | tran_commit (transaction_cl.c) |
| 인덱스 재사용 할당 | logtb_assign_tran_index (log_tran_table.c) |
| System op (서브트랜잭션) 스택 push | log_sysop_start (log_manager.c) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”트랜잭션 모듈에는 네 개의 이동 부품이 있다. 모든 살아 있는 TDES 를 보관하는 trantable, TDES 그 자체, trantable 의 엔트리 들이 따라 움직이는 라이프사이클 상태 머신, 그리고 TDES 가 서브 트랜잭션 rollback 을 위해 소유하는 system-op 스택이다. 이 순서 로 본다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph CL["클라이언트 측 (transaction_cl.c)"]
TCL["tm_Tran_index\ntm_Tran_isolation\ntm_Tran_ID"]
API["tran_commit\ntran_abort\ntran_savepoint_internal"]
TCL --> API
end
subgraph SR["서버 측 (transaction_sr.c)"]
XSC["xtran_server_commit"]
XSA["xtran_server_abort"]
XSV["xtran_server_savepoint"]
end
subgraph TT["Trantable (log_Gl.trantable)"]
HDR["TRANTABLE { num_total_indices, hint_free_index, all_tdes[] }"]
T1["LOG_TDES idx=1\ntrid=42"]
T2["LOG_TDES idx=2\ntrid=43"]
Tn["..."]
HDR --> T1
HDR --> T2
HDR --> Tn
end
subgraph LM["log_manager"]
LC["log_commit"]
LA["log_abort"]
LS["log_sysop_start / commit / abort"]
end
API -->|RPC| XSC
API -->|RPC| XSA
API -->|RPC| XSV
XSC --> LC
XSA --> LA
LC --> T1
LA --> T1
LS --> T1
이 그림이 보여주는 것은 트랜잭션 모듈이 걸쳐 있는 세 개의
경계다. (a) 클라이언트와 서버 사이. 클라이언트 측 TDES 는 얇은
그림자다 (tm_Tran_* 글로벌 변수들). 무거운 상태는 서버 측에
산다. (b) TDES 와 trantable 사이. trantable 이 슬롯을 소유하고,
인덱스로의 lookup 이 O(1) 이다. (c) TDES 와 로그 사이. commit,
abort, savepoint, system-op 호출이 일어날 때 TDES 와 로그가
함께 갱신된다. 두 상태가 lockstep 으로 움직이는
것이 이 모듈의 정합성 약속이다.
TDES — descriptor 본체
섹션 제목: “TDES — descriptor 본체”log_impl.h 의 LOG_TDES 가 트랜잭션 모듈의 중심 데이터
구조다. 큰 struct이며, 아래는 주석을 곁들여 핵심만 추린 슬라이스
다.
// LOG_TDES — src/transaction/log_impl.hstruct log_tdes{ /* === MVCC and identity === */ MVCC_INFO mvccinfo; /* MVCC info — snapshot, MVCCID, sub-IDs */
int tran_index; /* Index into trantable */ TRANID trid; /* Stable transaction identifier */
/* === lifecycle === */ bool isloose_end; TRAN_STATE state; /* 15-value enum */ TRAN_ISOLATION isolation; /* READ_COMMITTED | REPEATABLE_READ | SERIALIZABLE */ int wait_msecs; /* Lock wait timeout */
/* === LSA chain — these are the recovery anchors === */ LOG_LSA head_lsa; /* First record of this transaction */ LOG_LSA tail_lsa; /* Last record */ LOG_LSA undo_nxlsa; /* Next record to undo (compensate-aware) */ LOG_LSA posp_nxlsa; /* First / next postpone record */ LOG_LSA savept_lsa; /* Last user/system savepoint */ LOG_LSA topop_lsa; /* Last system op */ LOG_LSA tail_topresult_lsa; /* Last partial abort/commit */ LOG_LSA commit_abort_lsa; /* Commit/abort record (used by checkpoint) */
/* === client identity, locking, and 2PC === */ int client_id; int gtrid; /* Global tran ID for 2PC */ CLIENTIDS client; SYNC_RMUTEX rmutex_topop; /* Reentrant mutex serialising sysop begin/end */ LOG_TOPOPS_STACK topops; /* Active sub-transactional ops */ LOG_2PC_GTRINFO gtrinfo; LOG_2PC_COORDINATOR *coord; /* NULL unless this site is the 2PC coordinator */
/* === per-transaction caches and stats === */ int num_unique_btrees; multi_index_unique_stats m_multiupd_stats; volatile sig_atomic_t interrupt; tx_transient_class_registry m_modified_classes; int num_transient_classnames; int num_repl_records; struct log_repl *repl_records; LOG_LSA repl_insert_lsa; LOG_LSA repl_update_lsa; void *first_save_entry; int suppress_replication; struct lob_rb_root lob_locator_root; INT64 query_timeout; INT64 query_start_time; INT64 tran_start_time; XASL_ID xasl_id; LK_RES *waiting_for_res; /* The lock-resource I'm blocked on, if any */ int disable_modifications; TRAN_ABORT_REASON tran_abort_reason; int num_exec_queries; DB_VALUE_ARRAY bind_history[MAX_NUM_EXEC_QUERY_HISTORY]; int num_log_records_written; LOG_TRAN_UPDATE_STATS log_upd_stats; bool has_deadlock_priority; bool block_global_oldest_active_until_commit; bool is_user_active; LOG_RCV_TDES rcv; /* Recovery-time annotations only */ log_postpone_cache m_log_postpone_cache; bool has_supplemental_log; char *ddl_sql_user_text; // ... member functions for sysop locking and oldest-mvccid pinning ...};밀도가 높지만 층은 깔끔하게 나뉜다. 첫 블록 (mvcc info, identity)
은 모든 페이지 access가 읽는 구간이다. 두 번째 블록 (state,
isolation, wait_msecs) 은 lock 획득과 가시성 결정이 읽는 구간이
다. LSA 사슬은 복구 측이 의지하는 약속이다 — 모든 TDES는 analysis
가 자기를 식별할 수 있게 head_lsa/tail_lsa 를 두고, rollback
이 가능하도록 undo_nxlsa 를 두고, postpone replay를 위한
posp_nxlsa, 부분 rollback 의미론을 위한 savept_lsa /
topop_lsa / tail_topresult_lsa 를 둔다. 나머지 필드들은
부기에 가깝다 — 이 트랜잭션이 기다리고 있는 lock 자원, commit
시 정리해야 하는 lob 들, 다운스트림으로 보낼 복제 레코드, commit
시 무효화해야 하는 modified-class 들.
Trantable — TDES들의 테이블
섹션 제목: “Trantable — TDES들의 테이블”log_impl.h 의 trantable은 작은 헤더와 그 뒤를 잇는 TDES 연속
영역으로 구성된다.
// TRANTABLE — src/transaction/log_impl.hstruct trantable{ int num_total_indices; /* Capacity (configured at boot) */ int num_assigned_indices; /* Currently in use */ int num_coord_loose_end_indices; int num_prepared_loose_end_indices; int hint_free_index; /* Speeds up next assignment */ volatile sig_atomic_t num_interrupts; LOG_ADDR_TDESAREA *area; /* Linked list of TDES storage areas */ LOG_TDES **all_tdes; /* Indexed pointer table */};이 구조에서 짚을 점이 두 가지다. (a) lookup 비용. all_tdes
는 트랜잭션 인덱스로 색인되는 포인터 테이블이라
LOG_FIND_TDES(idx) 가 한 번의 load 로 끝난다. (b) 영역의 사슬.
area 는 단일 블록이 아니라 연속 할당 영역들의 사슬이다. 그래서
테이블이 (logtb_grow_* 경로) 커지더라도 기존 포인터를 무효화
하지 않고 자랄 수 있다.
logtb_assign_tran_index (log_tran_table.c:796) 가 슬롯
할당자다. 이 함수는 hint_free_index 를 이용해 빈 슬롯을 빨리
찾고, 필요시 새 영역을 할당하고, 새 TDES를 초기화한다 — 첫
head_lsa 와 함께 logtb_set_loose_end_chkpt_lsa 가 같이
호출된다. 짝이 되는 logtb_release_tran_index (1139) 가 TDES를
비우고, 트랜잭션이 들고 있던 lock을 해제하고, hint_free_index
를 갱신한다.
trantable 은 TR_TABLE_CS critical section
(csect_enter (CSECT_TRAN_TABLE)) 뒤에서 산다. writer (assign /
release) 는 write 모드로 진입하고, reader (대부분의 TDES lookup)
는 read 모드로 진입하거나 인덱스를 통한 직접 접근이라 진입을
건너뛴다.
Lifecycle — TRAN_STATE 상태 머신
섹션 제목: “Lifecycle — TRAN_STATE 상태 머신”TRAN_STATE (log_comm.h) 가 라이프사이클 enum 이다. 15 개
값을 가진다. 이 숫자가 의미 있는 이유는 분명하다. 트랜잭션 한
건의 진행을 alive / dead / in-progress 셋만으로는 표현할 수
없기 때문이다. postpone, 2PC, unilateral abort 가 각자 자기
중간 상태를 만들어
낸다. analysis 패스가 이 중간 상태들을 구분할 수 있어야 복구가
정확해진다.
// TRAN_STATE — src/transaction/log_comm.henum{ TRAN_RECOVERY, /* system tran for recovery */ TRAN_ACTIVE, /* normal in-flight */
TRAN_UNACTIVE_COMMITTED, /* commit complete */ TRAN_UNACTIVE_WILL_COMMIT, /* commit log written, force pending */ TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE, /* committed, postpones running */ TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE, /* sysop-postpone variant */
TRAN_UNACTIVE_ABORTED, /* user-initiated abort */ TRAN_UNACTIVE_UNILATERALLY_ABORTED, /* system aborted (crash) */
TRAN_UNACTIVE_2PC_PREPARE, /* prepared, waiting decision */ TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES, /* coordinator phase 1 */ TRAN_UNACTIVE_2PC_ABORT_DECISION, /* coordinator phase 2 — abort */ TRAN_UNACTIVE_2PC_COMMIT_DECISION, /* coordinator phase 2 — commit */ TRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS, /* informing after commit */ TRAN_UNACTIVE_ABORTED_INFORMING_PARTICIPANTS, /* informing after abort */
TRAN_UNACTIVE_UNKNOWN} TRAN_STATE;log_impl.h:143-183 의 LOG_ISTRAN_* 매크로들이 이 enum 을
나머지 엔진이 던지는 질문 단위로 압축한다. LOG_ISTRAN_ACTIVE
는 재시작된 서버 위에서 정상적으로 in-flight 인지를 묻고,
LOG_ISTRAN_COMMITTED 는 다섯 commit 측 상태를 한 묶음으로
본다. LOG_ISTRAN_ABORTED 는 네 abort 측 상태를 묶고,
LOG_ISTRAN_2PC_IN_SECOND_PHASE 는 두 번째 phase에 있는 2PC
상태들을 묶는다. 복구 매니저가 “redo가 필요한가, undo가 필요한
가, 2PC를 마저 끝내야 하는가” 를 결정할 때 의지하는 시점이 바로
이 매크로의 압축된 시점이다.
Isolation — 세 개의 수준, access 시점에서 디스패치
섹션 제목: “Isolation — 세 개의 수준, access 시점에서 디스패치”DB_TRAN_ISOLATION (compat/dbtran_def.h) 은 3비트 필드다.
// DB_TRAN_ISOLATION — src/compat/dbtran_def.htypedef enum{ TRAN_UNKNOWN_ISOLATION = 0x00,
TRAN_READ_COMMITTED = 0x04, /* alias TRAN_REP_CLASS_COMMIT_INSTANCE, TRAN_CURSOR_STABILITY */ TRAN_REPEATABLE_READ = 0x05, /* alias TRAN_REP_READ */ TRAN_SERIALIZABLE = 0x06, /* alias TRAN_NO_PHANTOM_READ */
TRAN_DEFAULT_ISOLATION = TRAN_READ_COMMITTED,} DB_TRAN_ISOLATION;값은 TDES별로 (log_tdes::isolation) 보관되며, 클라이언트 측에는
tm_Tran_isolation (transaction_cl.h) 그림자가 따로 있다. 이
값을 읽는 자리가 세 곳이다.
- 스냅샷 획득 (
mvcc_satisfies_snapshot,cubrid-mvcc.md). READ COMMITTED 는 statement 단위로 다시 잡고, REPEATABLE READ 는 트랜잭션 동안 유지하며, SERIALIZABLE 은 스냅샷 측면에서는 REPEATABLE READ 와 같지만 추가로 key-range 락을 잡는다. - Lock 획득 (lock manager). SERIALIZABLE 은 스캔 경계에서 key-range 락을 잡고, REPEATABLE READ 는 읽기 안정성을 MVCC 에 맡기고 쓰기 대상에만 lock 을 걸며, READ COMMITTED 는 최소 lock 만 걸고 statement 끝에서 lock 을 푼다.
- Statement 경계 (
xtran_*_query_end_*). cursor stability 의미론을 위해 READ COMMITTED 는 자기 스냅샷을 풀고, 나머지는 유지한다.
별칭이 의도적으로 살아 있는 점도 짚어 둘 만하다 —
TRAN_CURSOR_STABILITY 는 TRAN_READ_COMMITTED 와 같은 값이다.
이는 호환성 seam이다. 옛 API는 lock-engine 어휘
(cursor-stability, repeatable-class) 로 격리 수준을 명명했고,
새 코드는 SQL-표준 이름을 쓴다. 둘 다 같은 디스패치 코드로
컴파일된다.
Commit과 abort — 정상 경로
섹션 제목: “Commit과 abort — 정상 경로”서버 측 commit은 xtran_server_commit (transaction_sr.c:71)
이 처리한다. abort는 xtran_server_abort (128) 다. 둘 다 RPC
래퍼이며, 실제 동작은 log_manager.c 의 log_commit /
log_abort 안에서 일어난다.
// xtran_server_commit — src/transaction/transaction_sr.c (condensed)TRAN_STATExtran_server_commit (THREAD_ENTRY *thread_p, bool retain_lock){ TRAN_STATE state; int tran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);
/* Guard rails: no in-flight queries, no held mutex stack. */ // ... condensed ...
state = log_commit (thread_p, tran_index, retain_lock);
/* Fire post-commit triggers (replication, CDC supplemental flush). */ // ... condensed ... return state;}log_commit 안의 시퀀스는 cubrid-log-manager.md 에 자세히
나오지만 여기서는 — postpone 레코드가 버퍼에 있다면
LOG_COMMIT_WITH_POSTPONE 을 append하고 그 postpone들을
실행한다. 그 다음 LOG_COMMIT 을 append하고 force-flush하며,
TDES 상태를 TRAN_UNACTIVE_COMMITTED 로 전이한다. lock을 해제
하거나 (retain_lock 이면 유지) 한 뒤,
logtb_release_tran_index 로 TDES를 비운다. log_abort 는 이
거울이다 — LOG_ABORT 를 append하고, undo를 구동하고, lock을
해제하고, TDES를 비운다.
System op — 서브트랜잭션 단위의 복구
섹션 제목: “System op — 서브트랜잭션 단위의 복구”여러 페이지를 함께 만지지만 한 그룹으로만 atomic 이어야 하는
연산들 (B+Tree split, heap overflow 할당, 스키마 변경) 은
시스템 op 를 사용한다. 시스템 op 는 log_sysop_start
(log_manager.c:3599) 로 열리고, TDES의 topops 스택에 nest
된다. 그 다음 자기 서브 동작을 실행한 뒤 log_sysop_commit
(3916) 으로 commit하거나 log_sysop_abort 로 abort한다.
// LOG_TOPOPS_STACK / LOG_TOPOPS_ADDRESSES — src/transaction/log_impl.hstruct log_topops_addresses{ LOG_LSA lastparent_lsa; /* Where the parent's log range was when this op began */ LOG_LSA posp_lsa; /* First postpone of this op */};
struct log_topops_stack{ int max; int last; /* -1 ⇒ no system op in progress */ LOG_TOPOPS_ADDRESSES *stack;};log_sysop_start 는 새 LOG_TOPOPS_ADDRESSES 를 push한다. 그
때의 lastparent_lsa 는 부모의 현재 tail_lsa 다. 시스템 op이
활성화되어 있는 동안 그 op의 로그 레코드들은 로그 위에서 연속된
범위를 이룬다. log_sysop_commit 은 그 범위가 끝났음을
LOG_SYSOP_END_COMMIT 레코드로 표시한다 (lastparent_lsa 와
prv_topresult_lsa 가 사슬을 이룬다). log_sysop_abort 는
LOG_SYSOP_END_ABORT 를 쓰면서 그 범위를 거꾸로 걷어 undo를
적용한다.
log_sysop_end_* 변종들은 LOG_REC_SYSOP_END 의 union arm에
대응한다 (cubrid-log-manager.md 의 §Log record 참조).
log_sysop_end_logical_undo— 시스템 op이 자기 logical undo 이미지를 함께 들고 다닌다. 인덱스 split처럼 물리 undo가 어려운 경우에 쓰인다.log_sysop_end_logical_compensate— 되돌려진 시스템 op이 CLR 포인터를 남긴다.log_sysop_end_logical_run_postpone— 시스템 op이 postpone 실행을 구동한다.
복구 측은 이 변종들을 인지한다. analysis 패스가 sysop 범위를
LOG_SYSOP_END_TYPE 으로 분류하고, redo / undo 패스가 변종별로
다른 경로를 호출한다는 뜻이다 (자세한 내용은
cubrid-recovery-manager.md).
Savepoint — 사슬에 매달린 명명된 LSA
섹션 제목: “Savepoint — 사슬에 매달린 명명된 LSA”savepoint 생성은 log_append_savepoint (log_manager.c,
log_manager.h:132 에 선언) 가 처리한다. 이 함수는 savepoint
이름을 담은 LOG_SAVEPOINT 레코드를 발행한다. TDES의
savept_lsa 가 그 새 레코드의 LSA로 갱신되고, 레코드 안의
prv_savept 가 직전 savepoint를 가리키는 포인터 역할을 한다.
사슬이 만들어지는 셈이다. rollback-to-savepoint가 그 사슬을
거슬러 갈 수 있는 것은 이 구조 덕분이다.
// LOG_REC_SAVEPT — src/transaction/log_record.hppstruct log_rec_savept{ LOG_LSA prv_savept; /* Previous savepoint record */ int length; /* Savepoint name length follows */};savepoint로의 rollback은 다음처럼 진행된다.
log_abort_partial(savepoint_name, savept_lsa) → savept_lsa 사슬을 따라 그 이름의 savepoint를 찾는다 → tail_lsa부터 그 savepoint까지 undo → 단계마다 CLR을 발행 → tail_lsa를 그 savepoint 레코드의 LSA로 되돌린다savepoint에는 두 종류가 있다 (SAVEPOINT_TYPE,
transaction_cl.h:49). USER_SAVEPOINT 는 SQL SAVEPOINT foo
로 명시적으로 만들어진 것이고, SYSTEM_SAVEPOINT 는 엔진이
내부적으로 만든 것이다 (예 — statement 한 단위만 자동으로 abort
하기 위해 statement 경계를 감싸는 용도).
Lifecycle, 끝에서 끝까지
섹션 제목: “Lifecycle, 끝에서 끝까지”stateDiagram-v2 [*] --> ACTIVE: logtb_assign_tran_index ACTIVE --> WILL_COMMIT: log_commit\n(append LOG_COMMIT) WILL_COMMIT --> COMMITTED_W_POSTPONE: postpones queued COMMITTED_W_POSTPONE --> COMMITTED: log_do_postpone done WILL_COMMIT --> COMMITTED: no postpones ACTIVE --> ABORTED: log_abort\n(append LOG_ABORT, drive undo) ACTIVE --> UNILATERALLY_ABORTED: crash detected\nin recovery analysis ACTIVE --> PREPARED_2PC: log_2pc_prepare PREPARED_2PC --> COMMIT_DECISION: coordinator says commit PREPARED_2PC --> ABORT_DECISION: coordinator says abort COMMIT_DECISION --> INFORMING_PARTICIPANTS_C: phase 2 send ABORT_DECISION --> INFORMING_PARTICIPANTS_A: phase 2 send INFORMING_PARTICIPANTS_C --> COMMITTED: all acks received INFORMING_PARTICIPANTS_A --> ABORTED: all acks received COMMITTED --> [*]: logtb_release_tran_index ABORTED --> [*] UNILATERALLY_ABORTED --> [*]
각 전이는 자기 로그 레코드 발행으로 이름이 붙는다. log_commit
이 LOG_COMMIT_WITH_POSTPONE 또는 LOG_COMMIT 을 발행하고,
log_abort 는 LOG_ABORT 를 발행하며, 2PC 경로 (cubrid-2pc.md
에서 다룸) 는 LOG_2PC_* 타입을 발행한다. 복구의 analysis 패스
는 이 로그를 앞으로 걸으며 TDES별 그림을 만들어 가고,
LOG_ISTRAN_* 매크로로 각 TDES의 운명을 결정한다.
클라이언트 측 그림자와 API 표면
섹션 제목: “클라이언트 측 그림자와 API 표면”트랜잭션 모듈의 클라이언트 측은 작다. tm_Tran_index,
tm_Tran_isolation, tm_Tran_ID, tm_Tran_async_ws,
tm_Tran_wait_msecs 같은 글로벌 변수들과 transaction_cl.h 의
tran_* API가 전부다.
// Client-visible API — src/transaction/transaction_cl.h (excerpt)extern int tran_commit (bool retain_lock);extern int tran_abort (void);extern int tran_unilaterally_abort (void);extern int tran_reset_isolation (TRAN_ISOLATION isolation, bool async_ws);extern int tran_reset_wait_times (int wait_in_msecs);extern int tran_savepoint_internal (const char *name, SAVEPOINT_TYPE type);extern int tran_abort_upto_user_savepoint (const char *name);extern int tran_abort_upto_system_savepoint (const char *name);extern int tran_2pc_start (void);extern int tran_2pc_prepare (void);extern int tran_set_global_tran_info (int gtrid, void *info, int size);extern bool tran_has_updated (void);각 진입점이 하는 일은 작은 로컬 부기 (예 — broker를 위한
tran_end_libcas_function 호출) 와 그 다음 서버 RPC다. 서버
측의 일은 위에서 본 그대로다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”앵커는 심볼명에 둔다. 라인 번호는 개정마다 어긋나므로 위치 힌트 표 안에서만 다룬다.
헤더와 타입
섹션 제목: “헤더와 타입”log_tdes(log_impl.h) — descriptor.trantable(log_impl.h) — descriptor 테이블.log_topops_stack,log_topops_addresses(log_impl.h) — system op 스택.log_rcv_tdes(log_impl.h) — 복구 시점에만 채워지는 부가 정보.TRAN_STATE(log_comm.h) — 15개 라이프사이클 enum.DB_TRAN_ISOLATION(compat/dbtran_def.h) — 3-수준 격리 enum.LOG_TOPOP_RANGE(log_manager.h) —(start_lsa, end_lsa)쌍. nested-top postpone replay에 쓰인다.tx_transient_class_registry(transaction_transient.hpp) — invalidation이 필요한 modified-class 리스트.
Trantable 관리
섹션 제목: “Trantable 관리”logtb_assign_tran_index(log_tran_table.c) — 새 트랜잭션 슬롯 할당.logtb_release_tran_index(log_tran_table.c) — 슬롯 반환.logtb_set_current_tran_index(log_tran_table.c) — 스레드 의 현재 인덱스 설정.logtb_complete_mvcc(log_tran_table.c) — commit/abort 시 점에 MVCC 정보 마무리.logtb_grow_*(log_tran_table.c) — 테이블 확장.
서버 진입점
섹션 제목: “서버 진입점”xtran_server_commit(transaction_sr.c) — 서버 commit.xtran_server_abort(transaction_sr.c) — 서버 abort.xtran_server_savepoint(transaction_sr.c) — savepoint 생성.xtran_server_unilaterally_abort_tran(transaction_sr.c) — 에러 복구 중 강제 abort.
System op 표면
섹션 제목: “System op 표면”log_sysop_start(log_manager.c) — frame push.log_sysop_commit(log_manager.c) — pop with commit,LOG_SYSOP_END_COMMIT발행.log_sysop_abort(log_manager.c) — pop with abort,LOG_SYSOP_END_ABORT발행, 거꾸로 undo.log_sysop_start_atomic(log_manager.c) — 복구에 민감한 연산 (파일 할당/해제) 을 위한 atomic 변종.log_sysop_end_logical_undo(log_manager.c) — 자기 logical undo를 가진 시스템 op.log_sysop_end_logical_compensate/log_sysop_end_logical_run_postpone(log_manager.c) — 보상과 postpone replay 변종.log_sysop_attach_to_outer(log_manager.c) — end record를 쓰지 않고 시스템 op의 로그 범위를 부모에 붙이기 (시스템 op이 실질적으로 marker 일 때).
클라이언트 측 API
섹션 제목: “클라이언트 측 API”tran_commit(transaction_cl.c).tran_abort(transaction_cl.c).tran_savepoint_internal(transaction_cl.c) — USER와 SYSTEM savepoint 모두 여기로 수렴.tran_abort_upto_user_savepoint/tran_abort_upto_system_savepoint(transaction_cl.c).tran_reset_isolation(transaction_cl.c) —tm_Tran_isolation을 뒤집고 서버로 forward.
이 개정 시점의 위치 힌트 (2026-05-01)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-05-01)”| 심볼 | 파일 | 라인 |
|---|---|---|
log_tdes (struct) | log_impl.h | 475 |
log_topops_stack | log_impl.h | 362 |
log_topops_addresses | log_impl.h | 353 |
log_rcv_tdes | log_impl.h | 458 |
trantable | log_impl.h | 602 |
LOG_ISTRAN_ACTIVE (macro) | log_impl.h | 143 |
LOG_ISTRAN_COMMITTED (macro) | log_impl.h | 146 |
LOG_ISTRAN_ABORTED (macro) | log_impl.h | 153 |
LOG_ISTRAN_2PC (macro) | log_impl.h | 173 |
TRAN_STATE enum | log_comm.h | 36 |
DB_TRAN_ISOLATION enum | dbtran_def.h | 36 |
logtb_assign_tran_index | log_tran_table.c | 796 |
logtb_release_tran_index | log_tran_table.c | 1139 |
logtb_complete_mvcc | log_tran_table.c | 4050 |
logtb_set_current_tran_index | log_tran_table.c | 6002 |
xtran_server_commit | transaction_sr.c | 71 |
xtran_server_abort | transaction_sr.c | 128 |
xtran_server_savepoint | transaction_sr.c | 348 |
log_sysop_start | log_manager.c | 3599 |
log_sysop_start_atomic | log_manager.c | 3665 |
log_sysop_commit_internal | log_manager.c | 3825 |
log_sysop_commit | log_manager.c | 3916 |
log_commit | log_manager.c | 5352 |
log_abort | log_manager.c | 5461 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”검증된 사실
섹션 제목: “검증된 사실”-
LOG_TDES는 약 50개 필드를 가진 단일 struct다 — hot/cold 분리가 없다.log_impl.h:475에서 검증. PostgreSQL은PROC와PGXACT를 분리해 가시성 스캔이 hot 필드만 읽도록 하지만, CUBRID은mvccinfo(가시성과 직결) 를bind_history나query_timeout같은 부기 필드 옆에 함께 둔다. 함의 — TDES 테이블을 가시성 스캔할 때 strictly necessary한 캐시 라인보다 더 많은 라인을 읽게 된다. -
TRAN_STATE의 15개 값 중 7개는 2PC 상태 머신에 속한다.log_comm.h:36-67에서 검증.log_impl.h:173-176의LOG_ISTRAN_2PC매크로가 그 중 6개를 “2PC에 있다” 로 묶는다. 15개 enum이TRAN_RECOVERY를 별도 2PC 변종으로 포함하지는 않는다 — 이는 복구 worker의 의사-트랜잭션을 위한 가짜 상태다. -
기본 격리 수준은
TRAN_READ_COMMITTED(0x04) 다.dbtran_def.h:53(TRAN_DEFAULT_ISOLATION = TRAN_READ_COMMITTED) 와dbtran_def.h:54(MVCC_TRAN_DEFAULT_ISOLATION = TRAN_READ_COMMITTED) 에서 검증. 두 default가 같은 이유는 CUBRID이 전체적으로 MVCC다 — default가 갈릴 만한 비-MVCC 모드가 따로 없다. -
격리 수준 enum 값은 의도적으로 별칭이 걸려 있다.
TRAN_READ_COMMITTED == TRAN_REP_CLASS_COMMIT_INSTANCE == TRAN_CURSOR_STABILITY == 0x04다.dbtran_def.h:40-42에서 확인된다. 별칭은 옛 lock-engine 어휘 이름과의 API 호환을 위한 것이며 같은 디스패치 경로로 컴파일된다. -
trantable 크기는 boot 시점에 정해지며 트랜잭션마다 동적이지 않다.
logtb_assign_tran_index(log_tran_table.c:796) 본문 으로 확인된다. 슬롯은LOG_ADDR_TDESAREA연결 리스트가 관리 하는 연속 영역에서 할당된다. 부족하면 영역이 자라지만 줄어들 지는 않는다. 상한은max_clients서버 파라미터가 정한다. -
System op 은 별도 테이블이 아니라 TDES 위 스택으로 nest 된다.
log_impl.h:361-367의LOG_TOPOPS_STACK에서 확인된다. 스택의last필드가-1이면 진행 중인 시스템 op 이 없다는 뜻이고, 정수 인덱스이면 그 깊이라는 뜻이다. 글로벌 시스템-op 테이블이 따로 존재하지 않으며, 모든 TDES 가 자기 스택을 소유 한다. -
Lock 획득 wait timeout 은 TDES 별이다.
log_impl.h:486(wait_msecs필드) 와 클라이언트 측 글로벌tm_Tran_wait_msecs(transaction_cl.h:58) 에서 확인된다. 매크로TRAN_LOCK_INFINITE_WAIT = -1(log_comm.h:29) 가 영원히 기다린다는 sentinel 이다. -
block_global_oldest_active_until_commit는 자기 vacuum 을 직접 해야 하는 장기 연산을 위해 존재한다.log_impl.h:555(필드 정의) 와log_impl.h:585의lock_global_oldest_visible_mvccid멤버 함수에서 확인된다. 큰 데이터를 스캔하는 reorganize-partition / upgrade-domain 같은 코드 경로가 동시 트랜잭션에 의해 자기 MVCC 임계가 앞으로 밀리는 것을 막기 위해 이 플래그를 켠다. -
LOG_2PC_GTRINFO와LOG_2PC_COORDINATOR *는 TDES 의 inline 필드라 2PC 가 아닌 트랜잭션에도 자리가 있다.log_impl.h:505-508에서 확인된다. coordinator 가 아닌 사이트에서는coord가NULL이다. 비용은 TDES 당 포인터 하나이고, 이득은 로컬 트랜 잭션을 2PC 역할로 승격할 때 재할당이 필요 없다는 점이다. -
LOG_RCV_TDES는 복구 시점에만 비어 있지 않다.log_impl.h:458(struct 정의) 와 558 (log_tdes::rcv인라인 필드) 에서 확인된다. 그 안의 필드들 (sysop_start_postpone_lsa,tran_start_postpone_lsa,atomic_sysop_start_lsa,analysis_last_aborted_sysop_*) 은 analysis 패스가 채우고 redo/undo 패스가 소비한다.
미해결 질문
섹션 제목: “미해결 질문”-
TDES 의 hot / cold 분리.
mvccinfo가bind_history옆에 놓인 캐시 미스 비용을 측정해 본 사람이 있는가? 다른 엔진 들이 분리하는 데는 이유가 있을 것이다. 추적 경로는 동시성 높은 read 워크로드에서perf stat -e cache-misses를 돌리고 가상의 분리 TDES 와 비교하는 것이다. -
trantable 확장. 헤더 필드
LOG_ADDR_TDESAREA *area는 런타임 확장이 지원된다는 인상을 준다. 그러나 트리거와 동기화는 검증되지 않았다. 추적 경로는log_tran_table.c에서area에 쓰기를 grep 으로 찾고, 확장이 요청 경로에서 일어나는지 quiescent 시점에만 일어나는지 확인하는 것이다. -
경합 상황에서의
hint_free_index정합성. 여러 thread 가 동시에logtb_assign_tran_index를 부를 수 있다. hint 는 단일 값이라, 무엇이 그 값을 보호하는가? 추적 경로는logtb_assign_tran_index본문을 읽고 compare-and-swap 이나 mutex 사용을 확인하는 것이다. -
System op
rmutex_topop의 동작. TDES 별 reentrant mutex 는 시스템 op 이 같은 thread 위에서 재귀적으로 시작될 수 있음을 시사한다. 그러나 깊이의 상한은 검증되지 않았다. 추적 경로는log_sysop_start안의lock_topop()호출들을 살피고 reentrance 카운트를 추적하는 것이다. -
Postpone 캐시 통합.
m_log_postpone_cache는 TDES 에 inline 된 C++ 클래스 (log_postpone_cache) 다. 필드 코멘트에 따르면 그 목적은log_do_postpone에서 replay 할 수 있는 postpone 레코드를 기억해 두는 것이다. 정확한 lifetime (commit 시 비워지는가, abort 시에도 비워지는가, sysop 경계를 넘어 살아 남는가) 은 검증되지 않았다. 추적 경로는log_postpone_cache.cpp와log_manager.c의log_do_postpone을 함께 읽는 것이다. -
클라이언트 측 TDES 그림자와 서버 실제 사이의 정합성.
tm_Tran_*는 클라이언트 측 글로벌이다. 연결 failover 시 서버가 다른wait_msecs를 갖고 있다면 어떻게 되는가? 추적 경로는tran_cache_tran_settings의 소비자를 따라가고, CAS broker 가 reconnect 시 다시 동기화하는지 확인하는 것이다.
CUBRID 너머 — 비교 설계와 연구 동향
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향”본격적인 비교 분석이 아니라, 후속 문서로 이어질 단서들의 모음이다.
-
PostgreSQL 의
PROC/PGXACT분리. PG 는 descriptor 를 hot 절반 (PGXACT: xid, xmin, vacuumFlags) 과 cold 절반 (PROC: locktag arrays, myProcLocks) 으로 가른다. 가시성 스캔은 hot 만 읽는다. CUBRID 의 monolithic TDES 와 나란히 두면 캐시 비용이 측정될 만한 비교 자리다. -
InnoDB 의
trx_t+lock_sys예약. InnoDB 은 lock 예약을trx_t::lock안에 박아 두고 글로벌lock_sys_tmutex 를 쓴다. CUBRID 은 이를 분리한다. TDES 위의LK_RES *waiting_for_res와 lock manager 의 자원별 hash 가 나뉘어 있다. 두 설계를 비교하면 lock 획득 critical path 가 분명해진다. -
Hekaton 의 in-memory transaction map (Larson 외, VLDB 2011). Hekaton 은 TDES 를 트랜잭션-id 로 키된 lock-free hash 에 저장하며, 버전 데이터는 레코드 안에 inline 저장된다. CUBRID 의 고정 배열 trantable 은 그 반대 방향 설계점이다.
-
PostgreSQL subtransaction 의 partial rollback 사슬. PG 는
SubTransactionId와 백엔드별 스택을 쓰는데, CUBRID 의 topops 스택과 거의 같은 구조다. PG 의 두 버전 subxact-id 매핑 (subxact 와 parent xid) 이 CUBRID 의LOG_TOPOPS_ADDRESSES보다 조금 더 정교하지만, lifecycle 은 구조적으로 동일하다. -
RDMA 위의 optimistic concurrency control (FaRM, NSDI 2014). FaRM 은 TDES 테이블을 없애고 트랜잭션 상태를 레코드 버전에 직접 인코드한다. CUBRID 의 TDES 가 살아남는 이유는 격리 모드 들이 lock 획득 시 descriptor 를 필요로 하기 때문이라는 점이 이 비교로 분명해진다.
-
JTA
XAResource의미론 (JSR 907). CUBRID 2PCTRAN_STATE분기는 JTA 의 prepared / commit / rollback 의미론에 부합한다.cubrid-2pc.md가 이 부합점을 열거하는 자연스러운 후속 문서다. -
CockroachDB serializable + parallel commits (Taft 외, SIGMOD 2020). Cockroach 는 descriptor 를 분산 KV 위로 밀어 넣고, 단일 intent 레코드를 쓰는 것으로 commit 을 표시한 뒤 그 상태를 나중에 lazy 하게 해소한다. 그 transaction record 가 CUBRID 의 TDES 역할을 하지만 고정 크기 테이블에 묶이지 않는다는 점이 다르다. 두 설계를 비교하면 공유 메모리 엔진이 trantable 상한으로 지불하는 비용과 shared-nothing 엔진이 intent 해소 트래픽으로 지불하는 비용이 함께 보일 것이다.
원본 분석 (raw/code-analysis/cubrid/storage/transaction/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/transaction/)”Transaction Internals.pdfTransaction Internals.pptx
교재 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”- Database Internals (Petrov), 5장 Transactions and Recovery, §ACID, §Isolation levels.
- Concurrency Control and Recovery in Database Systems (Bernstein, Hadzilacos, Goodman), 1-4장.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/transaction/log_impl.h— TDES, trantable, sysop 스택.src/transaction/log_tran_table.c— trantable 관리.src/transaction/transaction_cl.{h,c}— 클라이언트 API.src/transaction/transaction_sr.{h,c}— 서버 진입점.src/transaction/transaction_global.hpp— 시스템 tran 상수.src/transaction/transaction_transient.hpp— modified-class 레지스트리, lob locator 사슬.src/transaction/log_comm.h—TRAN_STATEenum.src/transaction/log_manager.c— sysop, commit, abort.src/compat/dbtran_def.h—DB_TRAN_ISOLATIONenum.
이 지식 베이스의 형제 문서
섹션 제목: “이 지식 베이스의 형제 문서”knowledge/code-analysis/cubrid/cubrid-log-manager.md— TDES가 발행하는 로그 레코드.knowledge/code-analysis/cubrid/cubrid-mvcc.md—log_tdes::mvccinfo의 소비자.knowledge/code-analysis/cubrid/cubrid-lock-manager.md—log_tdes::wait_msecs의 소비자이자log_tdes::waiting_for_res의 producer.knowledge/code-analysis/cubrid/cubrid-recovery-manager.md— analysis 시점에 TDES를 소비. 같은 배치에서 진행 중.knowledge/code-analysis/cubrid/cubrid-2pc.md— 2PC 상태 머신 가지와coord/gtrinfo의 소유자. 같은 배치에서 진행 중.