(KO) CUBRID 2PC — 2단계 commit과 in-doubt 복구
목차
학술적 배경
섹션 제목: “학술적 배경”2단계 commit (2PC) 프로토콜이 답하는 문제는 분명하다. N개의 독립된 사이트가 한 분산 트랜잭션을 commit 또는 abort에 어떻게 합의할 것인가, 중앙의 진실 보유자 없이. Jim Gray가 Notes on Database Operating Systems (1978) 에서 이 프로토콜을 명명 했다. 그 다음 JTA/XA 사양 (X/Open, 그리고 Java JSR 907) 이 이 프로토콜을 transaction manager와 resource manager 사이의 상호 운용성 표준으로 만들었다. Database Internals (Petrov) 13장 Distributed Transactions 가 그 교과서적 설명이다.
이 프로토콜에는 두 역할과 두 단계가 있다.
- Coordinator 가 트랜잭션을 구동한다. Participant 들은 상태의 일부를 들고 있는 resource manager 들이다.
- Phase 1 (prepare). coordinator가 각 participant에게 commit 준비됐는가? 를 묻는다. 각 participant는 YES (그러면 unilateral abort 하지 않겠다는 약속과 함께 투표) 또는 NO를 돌려 준다. 투표는 답이 돌아가기 전에 내구화된다.
- Phase 2 (decision). 모두 YES 였으면 coordinator가 COMMIT 을 결정해 각 participant에 알린다. 아니면 ABORT. 결정도 보내 지기 전에 내구화된다. participant들이 ack를 보내고, coordinator 가 gtrid를 잊는다.
2PC의 까다로운 상태가 in-doubt 다. participant가 YES를 투표했지만 coordinator의 결정을 듣지 못한 상태에서 충돌이 일어난 경우다. 재시작 시 그 in-doubt 트랜잭션은 잠겨 있고 (participant 가 결정이 돌아올 때까지 모든 lock을 들고 있다), coordinator에게 질의가 간다. coordinator도 사라진 상태라면 operator의 개입이 필요하다. 이 자리가 XA가 heuristic 결정과 별도의 XID 식별자 공간을 정의하는 이유다.
이 모델 위에서 모든 실제 엔진은 두 가지 결정을 내려야 한다. 두 결정이 본 문서 골격을 만든다.
- 한 사이트가 coordinator와 participant 모두일 수 있는가.
하나의 CUBRID 서버가 두 역할을 동시에 할 수 있다. 이 서버
를 갱신하고 동시에 연결된 다른 서버를 갱신하는 query는 분산
트랜잭션이며, 이 서버가 coordinator고 다른 서버가 participant
다. CUBRID의
LOG_2PC_EXECUTEenum이 역할별로 디스패치한다. - prepared 트랜잭션을 충돌을 가로질러 어떻게 식별하는가.
프로토콜은 글로벌 ID (
gtrid/XID) 가 필요하다. local trid 가 재사용되어도 살아남아야 한다. CUBRID은log_2pc_start에서 gtrid를 할당하고 TDES에 저장한다. in-doubt 복구가 prepared 상태 로그 레코드로부터 gtrid → tid map을 다시 만든다.
이 두 답이 보이고 나면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나를 구현하거나 그 프로토콜을 내구하게 만들기 위해 존재한다는 점이 분명해진다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”2PC를 지원하는 모든 엔진이 Gray의 프로토콜 위에 비슷한 패턴을 얹는다.
결정 경계에서의 강제 로그 쓰기
섹션 제목: “결정 경계에서의 강제 로그 쓰기”prepared 상태와 결정은 네트워크 메시지가 보내지기 전에 안정 저장소에 닿아야 한다. 두 레코드 모두 force-flush 된다 (cubrid-log-manager.md §Force-at-commit). PG, InnoDB, Oracle 모두 이 규율을 공유한다.
local trid와 분리된 gtrid
섹션 제목: “local trid와 분리된 gtrid”local trid는 트랜잭션이 끝나면 재사용된다. gtrid 는 in-doubt
복구가 끝나거나 TM이 heuristic하게 잊을 때까지 살아남는다. 엔진
은 gtrid를 별도 채널 (TDES 필드, 별도 테이블) 에 저장해, local
trid가 재활용되어도 prepared 트랜잭션을 잃지 않게 한다.
TDES 위에 붙는 coordinator 정보
섹션 제목: “TDES 위에 붙는 coordinator 정보”분산 트랜잭션의 coordinator 측은 participant 리스트와 ack 상태
를 기억해야 한다. 별도 coordinator 테이블이 아니라 TDES에 붙여
두면, 충돌 복구가 그 정보를 TDES와 함께 LOG_2PC_START /
LOG_2PC_PREPARE 로그 레코드로부터 같이 복원한다.
Presumed-abort 최적화
섹션 제목: “Presumed-abort 최적화”PREPARE를 보낸 뒤 결정 전에 coordinator가 충돌하면, in-doubt participant들은 ABORT를 가정해야 한다. commit-decision 레코드를 찾을 수 없기 때문이다. 표준 presumed-abort 최적화는 간단하다. 이미 보낸 abort 결정에 대해서는 로그 레코드를 발행 하지 않는다. coordinator의 침묵을 participant가 abort로 가정 한다.
XA 다리
섹션 제목: “XA 다리”Java/JTA + X/Open XA 사양이 TM↔RM 계약을 정의한다. CUBRID은 XA
를 tran_2pc_* 클라이언트 API로 노출한다. 여기서 gtrid
가 XA의 XID 가 된다. CUBRID 서버는 RM (participant 로 붙을 때)
이자 내부 coordinator (자기 의존 participant 들을 구동할 때) 의
역할을 함께 한다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| Coordinator/participant 역할 enum | LOG_2PC_EXECUTE { FULL, PREPARE, COMMIT_DECISION, ABORT_DECISION } |
| Global transaction id | LOG_TDES::gtrid; LOG_2PC_NULL_GTRID = -1 |
| TDES 위 coordinator 상태 | LOG_2PC_COORDINATOR { num_particps, particp_id_length, block_particps_ids } |
| Global tran user info (XID payload) | LOG_2PC_GTRINFO { info_length, info_data } |
| Read-prepare 시 lock 획득 플래그 | LOG_2PC_OBTAIN_LOCKS = true / LOG_2PC_DONT_OBTAIN_LOCKS = false |
| Phase 1 commit | log_2pc_commit_first_phase (log_2pc.c:437) |
| Phase 2 commit | log_2pc_commit_second_phase (log_2pc.c:503) |
| Phase dispatch | log_2pc_commit (log_2pc.c:632) |
| Prepared 로그 레코드 | LOG_2PC_PREPARE + LOG_REC_2PC_PREPCOMMIT (log_record.hpp:387) |
| Start 레코드 | LOG_2PC_START + LOG_REC_2PC_START (log_record.hpp:399) |
| Decision 레코드 | LOG_2PC_COMMIT_DECISION, LOG_2PC_ABORT_DECISION |
| Inform-participants 레코드 | LOG_2PC_COMMIT_INFORM_PARTICPS, LOG_2PC_ABORT_INFORM_PARTICPS |
| Ack 레코드 | LOG_2PC_RECV_ACK + LOG_REC_2PC_PARTICP_ACK (log_record.hpp:412) |
| Prepared TDES 상태 | TRAN_UNACTIVE_2PC_PREPARE |
| In-doubt-collecting 상태 | TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES |
| Phase-2 decision 상태 | TRAN_UNACTIVE_2PC_COMMIT_DECISION / _ABORT_DECISION |
| 결정 후 informing 상태 | TRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS / _ABORTED_INFORMING_* |
| In-doubt 복구 | log_2pc_recovery (log_2pc.h:96) |
| In-doubt analysis-pass 부가 정보 | log_2pc_recovery_analysis_info (log_2pc.h:95) |
| XA prepared-list query | log_2pc_recovery_prepared (log_2pc.c:915) |
| XA gtrid attach | log_2pc_attach_global_tran (log_2pc.c:1036) |
| XA prepare for attached gtrid | log_2pc_prepare_global_tran (log_2pc.c:1126) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”2PC 모듈에는 네 개의 이동 부품이 있다. 한 TDES를 coordinator 역할 / participant 역할에 따라 다르게 routing 하는 디스패치 기계, 프로토콜을 내구하게 만드는 prepared 상태 로그 레코드, 재시작 시 prepared 트랜잭션을 살려 내는 in-doubt 복구, 그리 고 외부 transaction manager가 프로토콜을 구동할 수 있게 해 주는 XA 다리. 이 순서로 본다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph CL["클라이언트 / TM (XA)"]
XA["xa_prepare\nxa_commit\nxa_rollback"]
TC["tran_2pc_∗\n(transaction_cl.c)"]
XA --> TC
end
subgraph SR["서버 (transaction_sr + log_2pc)"]
XSC["xtran_2pc_∗"]
L2C["log_2pc_∗"]
XSC --> L2C
end
subgraph TDES["log_tdes (트랜잭션별 상태)"]
GT["gtrid"]
GI["gtrinfo (XID payload)"]
CO["coord (coordinator가 아니면 NULL)"]
ST["state (TRAN_UNACTIVE_2PC_∗)"]
end
subgraph LOG["WAL 레코드"]
R0["LOG_2PC_PREPARE"]
R1["LOG_2PC_START"]
R2["LOG_2PC_COMMIT_DECISION"]
R3["LOG_2PC_ABORT_DECISION"]
R4["LOG_2PC_∗_INFORM_PARTICPS"]
R5["LOG_2PC_RECV_ACK"]
end
subgraph PART["Participant들"]
P1["사이트 B"]
P2["사이트 C"]
end
TC -->|RPC| XSC
L2C --> TDES
L2C --> LOG
L2C -->|prepare| PART
PART -->|vote| L2C
L2C -->|commit / abort| PART
PART -->|ack| L2C
이 그림이 보여 주는 세 경계가 있다. 첫째, 클라이언트 / 서버.
XA / tran_2pc_* API가 클라이언트 표면이다. 서버 측 log_2pc_*
가 구현이다. 둘째, TDES / 로그. TDES가 살아 있는 상태 (gtrid,
coord, 격리 관련 필드) 를 들고 있다. 로그가 복구가 다시 세우는
내구 흔적을 보관한다. 셋째, coordinator / participant. 같은
TDES가 어느 쪽이든 될 수 있다. LOG_2PC_EXECUTE enum이 그
디스패치를 한다.
역할 디스패치 — LOG_2PC_EXECUTE
섹션 제목: “역할 디스패치 — LOG_2PC_EXECUTE”// LOG_2PC_EXECUTE — src/transaction/log_2pc.h:45enum log_2pc_execute{ LOG_2PC_EXECUTE_FULL, /* The root coordinator */ LOG_2PC_EXECUTE_PREPARE, /* Participant that is also a non-root coordinator running phase 1 */ LOG_2PC_EXECUTE_COMMIT_DECISION, /* Participant + non-root coordinator running phase 2 (commit) */ LOG_2PC_EXECUTE_ABORT_DECISION /* Same but abort, possibly without phase 1 */};typedef enum log_2pc_execute LOG_2PC_EXECUTE;네 값이 한 CUBRID 서버가 분산 트랜잭션에서 맡을 수 있는 네 역할 에 대응한다.
- FULL — 이 서버가 root coordinator다. prepare를 구동하고, vote를 모으고, 결정을 내리고, 모든 participant에게 알린다.
- PREPARE — 이 서버가 트리 중간 어딘가에 있다. 위쪽 coordinator의 관점에서는 participant 이고, 그 동시에 아래 participant 들의 coordinator다. 위에서 phase 1이 떨어지면 아래로도 phase 1 을 트리거한다.
- COMMIT_DECISION / ABORT_DECISION — 같은 중간 위치이지만 phase 2를 실행하는 시점이다.
디스패치는 log_2pc_commit (log_2pc.c:632) 에서 일어난다.
// log_2pc_commit — src/transaction/log_2pc.c (signature)TRAN_STATElog_2pc_commit (THREAD_ENTRY *thread_p, log_tdes *tdes, LOG_2PC_EXECUTE execute_2pc_type, bool *decision);execute_2pc_type 인자가 경로를 선택한다. *decision 에 로컬
결과 (true=commit, false=abort) 가 채워져, parent coordinator가
있다면 위로 전파된다.
TDES 위의 coordinator 상태
섹션 제목: “TDES 위의 coordinator 상태”TDES가 coordinator 역할을 할 때 coord 포인터가
LOG_2PC_COORDINATOR 블록을 가리킨다.
// LOG_2PC_COORDINATOR — src/transaction/log_2pc.h:64struct log_2pc_coordinator{ int num_particps; /* Number of participating sites */ int particp_id_length; /* Length of one participant identifier */ void *block_particps_ids; /* Block of N × particp_id_length bytes */#ifdef LOG_2PC_ACK_RECV_REQUIRED bool *ack_received; /* Per-participant ack vector */#endif};block_particps_ids 는 N개의 participant ID가 길이
particp_id_length 만큼씩 나란히 들어 있는 평탄한 byte 블록이
다. 네트워크 주소, 이름, 또는 호출 코드가 log_2pc_alloc_coord_info
에 넘기는 무엇이든 들어 갈 수 있다. 평탄한 블록으로 저장하면 그
대로 LOG_2PC_START 레코드에 직렬화된다는 점이 이 형식의
이유다.
// LOG_REC_2PC_START — src/transaction/log_record.hpp:399struct log_rec_2pc_start{ char user_name[DB_MAX_USER_LENGTH + 1]; int gtrid; int num_particps; int particp_id_length; /* immediately followed by num_particps × particp_id_length bytes */};LOG_2PC_ACK_RECV_REQUIRED #ifdef 가 coordinator가
participant별 ack를 추적할지 여부를 정한다. 정의되면 ack 벡터
가 phase 2 동안 LOG_2PC_RECV_ACK 레코드로 채워진다.
log_2pc_alloc_coord_info (log_2pc.h:93 에 선언) 가 이 struct
를 TDES에 붙이고, log_2pc_free_coord_info 가 트랜잭션 종료 시
풀어 준다.
Global 트랜잭션 식별
섹션 제목: “Global 트랜잭션 식별”gtrid 는 log_2pc_start 시점에 발급되는 정수이며,
log_tdes::gtrid (log_impl.h:499) 에 저장된다. 짝이 되는
LOG_2PC_GTRINFO 가 XA 스타일 페이로드를 들고 다닌다.
// LOG_2PC_GTRINFO — src/transaction/log_2pc.h:57struct log_2pc_gtrinfo{ int info_length; void *info_data; /* opaque to the engine — XID payload */};log_2pc_set_global_tran_info (log_2pc.c:705) 가 페이로드를
TDES에 쓴다. log_2pc_get_global_tran_info (log_2pc.c:772) 가
다시 읽어 온다.
log_2pc_make_global_tran_id (log_2pc.c:323) 가 새 gtrid를
생성한다. log_2pc_check_duplicate_global_tran_id
(log_2pc.c:407) 가 gtrid 충돌을 막아 준다 (in-doubt 복구 시
복원된 gtrid가 새로 발급되는 gtrid와 충돌하지 않는지 확인).
Phase 1 — log_2pc_commit_first_phase
섹션 제목: “Phase 1 — log_2pc_commit_first_phase”log_2pc_commit (..., FULL, &decision) 또는
log_2pc_commit (..., PREPARE, &decision) 으로 호출되는 phase 1
함수가 다음을 한다.
- (root에서만)
LOG_2PC_START레코드를 append. participant들을 나열한다. - 각 participant에 PREPARE 송신 (
log_2pc_send_prepare,log_2pc.c:190). - 로컬 TDES를
LOG_2PC_PREPARE를 append (LOG_REC_2PC_PREPCOMMIT페이로드 포함). - 로그 force-flush.
- 상태를
TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES로 전이. - participant vote를 기다린다.
- 모두 YES면
*decision = true;TRAN_UNACTIVE_2PC_COMMIT_DECISION으로 전이. 하나라도 NO면*decision = false;TRAN_UNACTIVE_2PC_ABORT_DECISION으로 전이.
로컬 prepared 레코드는 lock 카탈로그를 함께 들고 다닌다.
// LOG_REC_2PC_PREPCOMMIT — src/transaction/log_record.hpp:387struct log_rec_2pc_prepcommit{ char user_name[DB_MAX_USER_LENGTH + 1]; int gtrid; int gtrinfo_length; /* length of XID payload that follows */ unsigned int num_object_locks; unsigned int num_page_locks; /* followed by gtrinfo bytes, object-lock list, page-lock list */};lock 카탈로그가 들어 있는 이유가 분명하다. in-doubt 복구가 prepared 트랜잭션을 다시 노출하기 전에 lock을 다시 획득해야 하기 때문이다. 그렇지 않으면 막 재시작한 서버에서 다른 동시 트랜잭션이 prepared 트랜잭션이 들고 있던 객체를 읽거나 쓸 수 있다.
Phase 2 — log_2pc_commit_second_phase
섹션 제목: “Phase 2 — log_2pc_commit_second_phase”phase 1이 결정을 산출한 다음에는 다음을 한다.
- 결정 레코드를 append —
LOG_2PC_COMMIT_DECISION(log_record.hppenum 값 30) 또는LOG_2PC_ABORT_DECISION(31). - force-flush.
TRAN_UNACTIVE_*_INFORMING_PARTICIPANTS로 전이.- 결정을 각 participant에 송신 (
log_2pc_send_commit_decision/_send_abort_decision,log_2pc.c:222 / 261). LOG_2PC_*_INFORM_PARTICPS를 append (32 / 33).- participant ack를 기다린다.
- ack가 도착할 때마다
LOG_2PC_RECV_ACK(34) 를 append. ack를 보낸 participant의 인덱스를 함께 적는다. - 모든 ack가 수신되면
TRAN_UNACTIVE_COMMITTED/_ABORTED로 전이하고 lock을 해제한다.
Prepared 상태 내구화 — 로그 레코드들
섹션 제목: “Prepared 상태 내구화 — 로그 레코드들”여섯 record type이 내구 2PC 흔적을 만든다.
| 타입 번호 | 이름 | 목적 |
|---|---|---|
| 28 | LOG_2PC_PREPARE | lock 카탈로그를 가진 로컬 prepared 상태 |
| 29 | LOG_2PC_START | participant 리스트에 대한 coordinator 기록 |
| 30 | LOG_2PC_COMMIT_DECISION | Phase 2 commit 결정 |
| 31 | LOG_2PC_ABORT_DECISION | Phase 2 abort 결정 |
| 32 | LOG_2PC_COMMIT_INFORM_PARTICPS | participant들에 commit 송신 |
| 33 | LOG_2PC_ABORT_INFORM_PARTICPS | participant들에 abort 송신 |
| 34 | LOG_2PC_RECV_ACK | 한 participant로부터 ack 수신 |
성공적인 분산 commit의 로그 위 순서는 다음과 같다.
LOG_2PC_START ...participant work records...LOG_2PC_PREPARE (force flush, send prepare) (collect votes)LOG_2PC_COMMIT_DECISION (force flush, send decision)LOG_2PC_COMMIT_INFORM_PARTICPSLOG_2PC_RECV_ACK (×N)LOG_COMMITLOG_2PC_RECV_ACK 의 페이로드는 LOG_REC_2PC_PARTICP_ACK { particp_index } (log_record.hpp:412) 다. start 레코드의
participant 블록에 대한 인덱스 하나가 전부다.
In-doubt 복구 — analysis 패스의 춤
섹션 제목: “In-doubt 복구 — analysis 패스의 춤”복구 analysis 패스 (cubrid-recovery-manager.md §Analysis 패스) 가 모든 TRANID를 분류한다. 2PC의 경우 분류는 어떤 레코드가 보이 는지에 달려 있다.
LOG_2PC_PREPARE가 있고 결정 레코드가 없음 → 상태TRAN_UNACTIVE_2PC_PREPARE. In-doubt. 복구가 lock을 들고 있으면서 coordinator의 결정을 기다려야 한다는 뜻이다.LOG_2PC_COMMIT_DECISION이 있지만 일부 participant를LOG_2PC_*_INFORM_PARTICPS가 보이지 않음 → 상태TRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS. 결정은 내구 화되어 있고, 누락된 participant 에게 다시 보내고 ack를 모아야 한다는 뜻이다.- 모든 participant를
LOG_2PC_RECV_ACK가 있음 → 끝남.TRAN_UNACTIVE_COMMITTED로 전이.
log_2pc_recovery_analysis_info (log_2pc.h:95) 가 analysis
패스에서 2PC를 들고 있는 각 TDES를 호출된다. analysis
패스가 끝나면 log_2pc_recovery (log_2pc.h:96) 가 in-doubt
집합을 walk한다.
- 각
TRAN_UNACTIVE_2PC_PREPARE를 — lock을 다시 획득 (log_2pc_read_prepare가 prepared 레코드의 lock 카탈로그를 읽고,LOG_2PC_OBTAIN_LOCKS = true가 그 획득을 강제한다). 트랜잭션은 coordinator (또는 operator) 가 결정할 때까지 in-doubt로 머문다. - 각
TRAN_UNACTIVE_*_INFORMING_PARTICIPANTS를 — inform-and-ack를 재개. 누락된 ack를 가진 participant에게 결정 을 다시 보낸다.
다섯 번째 복구 phase LOG_RECOVERY_FINISH_2PC_PHASE
(log_impl.h:631 에 선언) 가 이 작업을 위한 명명된 자리다.
cubrid-recovery-manager.md 의 현재 log_recovery driver가 이
phase를 직접 호출하지 않지만, log_2pc_recovery 가 이 자리를
차지한다. 본 문서의 미해결 질문 §4.
XA 다리 — 외부 transaction manager
섹션 제목: “XA 다리 — 외부 transaction manager”XA API (xa_prepare, xa_commit, xa_rollback, xa_recover)
가 클라이언트의 tran_2pc_* (transaction_cl.h) 를 거쳐 서버의
xtran_2pc_* 로 흐른다. 핵심 진입점은 다음과 같다.
tran_2pc_start→log_2pc_start(log_2pc.c:833) — gtrid 를 발급하고 TDES에 설치.tran_2pc_prepare→log_2pc_prepare(log_2pc.c:877) — 로컬 서버가 root이면LOG_2PC_EXECUTE_FULL로 phase 1 실행. 중간이면LOG_2PC_EXECUTE_PREPARE로.tran_2pc_recovery_prepared→log_2pc_recovery_prepared(log_2pc.c:915) —xa_recover등가물. TM이 해결해야 할 in-doubt gtrid 리스트를 반환한다.tran_2pc_attach_global_tran→log_2pc_attach_global_tran(log_2pc.c:1036) —xa_startresume. 기존 gtrid에 다시 붙기 (연결 failover나 thread-per-request 서버에서의 thread switch 후에 사용).tran_2pc_prepare_global_tran→log_2pc_prepare_global_tran(log_2pc.c:1126) — 이미 attach된 gtrid를 prepare 구동.
log_2pc_find_tran_descriptor (log_2pc.c:952) 가 gtrid →
TDES lookup이며, 모든 attach 스타일 호출이 사용한다.
분산 commit 한 번, 처음부터 끝까지
섹션 제목: “분산 commit 한 번, 처음부터 끝까지”sequenceDiagram participant TM as Transaction Manager (XA) participant CO as Coordinator (CUBRID 서버) participant LM as log_manager participant P1 as Participant 1 participant P2 as Participant 2 TM->>CO: xa_start (gtrid) CO->>CO: log_2pc_start: assign gtrid, install on TDES Note over CO: ...트랜잭션 작업 진행... TM->>CO: xa_prepare CO->>LM: append LOG_2PC_START (participant 블록) CO->>P1: send PREPARE CO->>P2: send PREPARE CO->>LM: append LOG_2PC_PREPARE (로컬 lock 카탈로그) CO->>LM: force flush CO->>CO: state = TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES P1-->>CO: vote YES P2-->>CO: vote YES CO->>CO: state = TRAN_UNACTIVE_2PC_COMMIT_DECISION TM->>CO: xa_commit CO->>LM: append LOG_2PC_COMMIT_DECISION CO->>LM: force flush CO->>P1: send COMMIT CO->>P2: send COMMIT CO->>LM: append LOG_2PC_COMMIT_INFORM_PARTICPS P1-->>CO: ack CO->>LM: append LOG_2PC_RECV_ACK (idx=1) P2-->>CO: ack CO->>LM: append LOG_2PC_RECV_ACK (idx=2) CO->>LM: append LOG_COMMIT CO->>CO: state = TRAN_UNACTIVE_COMMITTED CO->>CO: lock 해제
소스 코드 가이드
섹션 제목: “소스 코드 가이드”anchor는 심볼명 이다. 라인은 흘러간다.
헤더 타입과 상수
섹션 제목: “헤더 타입과 상수”LOG_2PC_NULL_GTRID(log_2pc.h) — no gtrid sentinel.LOG_2PC_OBTAIN_LOCKS/LOG_2PC_DONT_OBTAIN_LOCKS(log_2pc.h) —log_2pc_read_prepare의 플래그.LOG_2PC_EXECUTEenum (log_2pc.h) — 역할 디스패치.LOG_2PC_GTRINFO(log_2pc.h) — XA 페이로드 wrapper.LOG_2PC_COORDINATOR(log_2pc.h) — TDES 위 coordinator 상태.LOG_REC_2PC_PREPCOMMIT(log_record.hpp) — prepared 레코드 페이로드.LOG_REC_2PC_START(log_record.hpp) — start 레코드 페이로드.LOG_REC_2PC_PARTICP_ACK(log_record.hpp) — ack 페이로드.
Coordinator 경로
섹션 제목: “Coordinator 경로”log_2pc_start(log_2pc.c) — gtrid 할당.log_2pc_make_global_tran_id(log_2pc.c) — gtrid 생성기.log_2pc_check_duplicate_global_tran_id(log_2pc.c) — 복구 시점 가드.log_2pc_send_prepare(log_2pc.c) — phase-1 송신.log_2pc_send_commit_decision/log_2pc_send_abort_decision(log_2pc.c) — phase-2 송신.log_2pc_alloc_coord_info(log_2pc.h) —LOG_2PC_COORDINATOR를 TDES에 붙이기.log_2pc_free_coord_info(log_2pc.h) — 해제.
Phase orchestration
섹션 제목: “Phase orchestration”log_2pc_commit_first_phase(log_2pc.c).log_2pc_commit_second_phase(log_2pc.c).log_2pc_commit(log_2pc.c) — top-level 디스패처.log_2pc_prepare(log_2pc.c) — XA prepare 진입점.log_2pc_append_start(log_2pc.c).log_2pc_append_decision(log_2pc.c).
log_2pc_recovery_analysis_info(log_2pc.h) — analysis 패스 동안 TDES별 분류.log_2pc_recovery(log_2pc.h) — analysis 후 in-doubt와 informing-participants TDES를 위한 driver.log_2pc_read_prepare(log_2pc.h) — prepared 레코드 읽기. 필요시 lock 재획득.
XA / global-tran helper
섹션 제목: “XA / global-tran helper”log_2pc_set_global_tran_info/log_2pc_get_global_tran_info(log_2pc.c).log_2pc_recovery_prepared(log_2pc.c) —xa_recover등가물.log_2pc_find_tran_descriptor(log_2pc.c).log_2pc_attach_client(log_2pc.c) — 클라이언트를 TDES 에 바인딩.log_2pc_attach_global_tran(log_2pc.c) —xa_startresume.log_2pc_prepare_global_tran(log_2pc.c).
log_2pc_get_num_participants(log_2pc.c).log_2pc_dump_participants/log_2pc_dump_gtrinfo/log_2pc_dump_acqobj_locks(log_2pc.c) — 디버그 dump.log_2pc_is_tran_distributed(log_2pc.h) — bool 질의.log_2pc_clear_and_is_tran_distributed(log_2pc.h).
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”| 심볼 | 파일 | 라인 |
|---|---|---|
LOG_2PC_EXECUTE enum | log_2pc.h | 45 |
LOG_2PC_GTRINFO (struct) | log_2pc.h | 58 |
LOG_2PC_COORDINATOR (struct) | log_2pc.h | 65 |
log_2pc_get_num_participants | log_2pc.c | 132 |
log_2pc_dump_participants | log_2pc.c | 162 |
log_2pc_send_prepare | log_2pc.c | 190 |
log_2pc_send_commit_decision | log_2pc.c | 222 |
log_2pc_send_abort_decision | log_2pc.c | 261 |
log_2pc_make_global_tran_id | log_2pc.c | 323 |
log_2pc_check_duplicate_global_tran_id | log_2pc.c | 407 |
log_2pc_commit_first_phase | log_2pc.c | 437 |
log_2pc_commit_second_phase | log_2pc.c | 503 |
log_2pc_commit | log_2pc.c | 632 |
log_2pc_set_global_tran_info | log_2pc.c | 705 |
log_2pc_get_global_tran_info | log_2pc.c | 772 |
log_2pc_start | log_2pc.c | 833 |
log_2pc_prepare | log_2pc.c | 877 |
log_2pc_recovery_prepared | log_2pc.c | 915 |
log_2pc_find_tran_descriptor | log_2pc.c | 952 |
log_2pc_attach_client | log_2pc.c | 984 |
log_2pc_attach_global_tran | log_2pc.c | 1036 |
log_2pc_prepare_global_tran | log_2pc.c | 1126 |
log_2pc_read_prepare (LSA variant) | log_2pc.c | 1313 |
log_2pc_read_prepare (reader variant) | log_2pc.c | 1389 |
log_2pc_dump_gtrinfo | log_2pc.c | 1476 |
log_2pc_dump_acqobj_locks | log_2pc.c | 1491 |
log_2pc_append_start | log_2pc.c | 1513 |
log_2pc_append_decision | log_2pc.c | 1570 |
소스 검증 (2026-04-30 기준)
섹션 제목: “소스 검증 (2026-04-30 기준)”검증된 사실
섹션 제목: “검증된 사실”-
LOG_2PC_EXECUTEenum은 네 값을 가지며, 그 중 셋이 비-root coordinator를 위한 것이다.log_2pc.h:45에서 검증.FULL이 root 경로다. 나머지 셋이 위로부터의 participant 이자 아래의 coordinator인 트리 중간 노드에 대응한다. -
Coordinator 정보는 별도 구조가 아니라 TDES에 붙는다.
log_impl.h:506에서 검증 (LOG_TDES::coord가LOG_2PC_COORDINATOR *) 와log_2pc.h:65. coordinator가 아닌 사이트에서 포인터는NULL이고, coordinator일 때 그것이 participant 블록의 소유자다. -
Participant별 ack 추적은
#ifdef LOG_2PC_ACK_RECV_REQUIRED이다.log_2pc.h:70에서 검증. 매크로는 보수적인 ack 추적 이 필요한 빌드에서 정의되는 것으로 보인다. 그 대안은 per-participant ack를 건너뛰고LOG_2PC_*_INFORM_PARTICPS레코드의 시퀀싱에 의지하는 것이다. -
여섯 종류의 로그 record type이 2PC 내구 흔적을 만든다 (28-34).
log_record.hpp:99-107에서 검증 —LOG_2PC_PREPARE(28),LOG_2PC_START(29),LOG_2PC_COMMIT_DECISION(30),LOG_2PC_ABORT_DECISION(31),LOG_2PC_COMMIT_INFORM_PARTICPS(32),LOG_2PC_ABORT_INFORM_PARTICPS(33),LOG_2PC_RECV_ACK(34). 값은 안정적이다. 옛 archive 로그도 현재와 동일한 번호로 등장한다. -
Prepared 레코드는 lock 카탈로그 전체를 들고 다닌다.
log_record.hpp:387-396에서 검증 (LOG_REC_2PC_PREPCOMMIT::num_object_locks,num_page_locks). 고정 헤더 다음에 gtrinfo 바이트, 그 다음에 lock 리스트가 따라 붙는다. 복구 시점에log_2pc_read_prepare가 lock을 다시 획득할 때 읽는 자리가 바로 이곳이다. -
log_2pc_read_prepare에는 두 overload가 있다.log_2pc.h:88-90에서 검증 — 하나는LOG_LSA *+LOG_PAGE *를 받고, 다른 하나는log_reader &를 받는다. 두 overload가 있는 이유는 호환성이다. 옛 코드 경로는 명시적 LSA를 쓰고, 새 코드 경로는log_reader클래스 (cubrid-recovery-manager.md) 를 쓴다. -
In-doubt 복구는
LOG_RECOVERY_FINISH_2PC_PHASE라는 별도 phase로 이름이 붙어 있다.log_impl.h:631에서 검증. 이 phase가 enum에 명명되어 있으나, cubrid-recovery-manager.md 의log_recovery본문에서 직접 호출되지 않는다. 대신log_2pc_recovery로 analysis / undo 패스에서 호출된다 (미해결 질문 §4). -
gtrid는 int이다. opaque XID 가 아니다.log_impl.h:499(LOG_TDES::gtrid가 int) 와log_2pc.h:41(LOG_2PC_NULL_GTRID = -1) 에서 검증. XA 스타일 XID 페이로드 는LOG_2PC_GTRINFO::info_data로 별도로 흐른다. -
log_2pc_recovery_prepared가xa_recover등가물이다. 시그니처 (int gtrids[],int size) 와 이름으로 검증. 현재 in-doubt인 gtrid 리스트를 반환한다. 외부 TM이 이를 받아 해결 한다. -
log_2pc_attach_global_tran이 gtrid로 트랜잭션을 resume 한다.log_2pc.c:1036에서 검증. XAxa_startresume 경로가 사용한다. 이전에 suspend된 트랜잭션이 다른 thread에서 다시 attach되는 시나리오에 쓰인다. -
prepare 도중의 lock 획득은 플래그로 제어된다.
log_2pc.h:42-43에서 검증 —LOG_2PC_OBTAIN_LOCKS = true/LOG_2PC_DONT_OBTAIN_LOCKS = false. 플래그가log_2pc_read_prepare에 전달된다. false 는 prepared 레코드 의 진단 dump에 쓰이고, true 는 실제 복구 사용에 쓰인다.
미해결 질문
섹션 제목: “미해결 질문”-
Heuristic abort / heuristic commit 처리. XA가 해결된 heuristic 결정을 위해
xa_forget을 정의한다. CUBRID의 API 표면 (tran_2pc_*) 에 heuristic 결정 record type이 명시적 으로 노출되지는 않는다. 추적 경로 —tran_2pc_*와xtran_2pc_*에서 forget 호출을 검색. -
Presumed-abort 최적화. 표준 “coordinator timeout 시 abort 로그 레코드 안 씀” 패턴이 CUBRID에서 구현되어 있는가?
log_2pc_send_abort_decision이 송신 전에 레코드를 append 한다. 이 동작이 force-flush 되는지, 또는 coordinator timeout 시 건너뛰어 지는지가 추적되지 않았다. 추적 경로 —log_2pc_send_abort_decision본문 읽기. -
다중 레벨 coordination 트리.
LOG_2PC_EXECUTE_PREPARE가 내가 participant이자 아래의 coordinator 를 처리한다. 3+ 레벨은 어떻게 처리되는가? vote가 직렬로 위로 전파되는가? 추적 경로 —log_2pc_commit_first_phase의LOG_2PC_EXECUTE_PREPAREarm 읽기. -
LOG_RECOVERY_FINISH_2PC_PHASE호출 자리. phase가log_impl.h:631에 명명되어 있으나, cubrid-recovery-manager.md 의log_recoverydriver는 이를 명시적으로 부르지 않는다.log_2pc_recovery가 어디에서 호출되는가? 추적 경로 —log_2pc_recovery의 호출자를 grep. -
Coordinator-down-during-decision 복구. root coordinator 가
LOG_2PC_COMMIT_DECISION이후,LOG_2PC_COMMIT_INFORM_PARTICPS이전에 충돌한다면, participant들은 in-doubt 이며 coordinator 의 재시작이 다시 송신해야 한다. 상태TRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS가 이를 잡지만, 재송신 타이밍 (얼마나 자주, 얼마나 오래) 은 추적되지 않았다. 추적 경로 —log_2pc_recovery본문 읽기. -
gtrid 공간 소진.
gtrid가 int (~20억) 다. 재활용 vs. 소진 동작이 추적되지 않았다. 추적 경로 —log_2pc_make_global_tran_id와log_2pc_check_duplicate_global_tran_id본문 읽기.
CUBRID 너머 — 비교 설계와 연구 동향
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향”분석이 아닌 포인터(pointers).
-
Paxos commit (Gray & Lamport, 2006) — blocking 2PC를 Paxos 합의로 대체한 non-blocking 프로토콜이다. coordinator 여러 명이 동작한다. CUBRID의
LOG_2PC_*는 고전 2PC 다. Paxos-commit follow-up 문서가 CUBRID이 다중 coordinator를 운영하지 않음으로써 무엇을 포기했는지를 문서화할 것이다. -
Spanner의 Paxos 그룹 위 2PC (Corbett 외, OSDI 2012) — 전역으로 분산된 2PC. 각 participant가 자기 안에서 Paxos 그룹 이다. 프로토콜은 같지만 participant 측이 복제된다. CUBRID 범위 바깥이지만, failure 모델 대비로 유용하다.
-
Presumed-abort 와 presumed-commit 최적화 — ARIES/PA, ARIES/PC. 흔한 케이스에서 로그 양을 줄인다. CUBRID의 규율은 모두 logging 으로 보인다. 그 최적화가 적용 가능한지에 대한 audit 이 좋은 follow-up이다.
-
JTA/XA (X/Open CAE Spec C193, 1991) — 표준 RM-TM 계약. CUBRID이 C XA 라이브러리와 JDBC 드라이버의 XADataSource 로 지원한다.
-
Spanner의 TrueTime + commit wait — 한정된 시계 불확실성 을 사용해 외부적으로 직렬화 가능성을 보장한다. CUBRID의 2PC 는 시계 기반 순서가 없다. 서버 간 read가 일관되지 않은 시간을 볼 수 있다.
-
eXtended Architecture for distributed transactions (D-XA, P-XA) — 병렬 / 파이프라인 2PC를 위한 확장. 현대 CUBRID이 participant 간의 phase 1 을 더 적극적으로 파이프라인할 수 있다.
원본 분석 (raw/code-analysis/cubrid/storage/transaction/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/transaction/)”Transaction Internals.pdfTransaction Internals.pptx— 2PC 챕터들. 문서가 cubrid-transaction.md 와 같은 파일이며, scope 결정은.meta/cubrid-2pc.yaml에 분리 근거가 적혀 있다.
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-transaction.md— 부모: TDES, 격리, savepoint. 라이프사이클 상태TRAN_UNACTIVE_2PC_*가 그 doc에 모두 나열되어 있다.knowledge/code-analysis/cubrid/cubrid-log-manager.md— 여섯 2PC 로그 record type의 디스크 형식.knowledge/code-analysis/cubrid/cubrid-recovery-manager.md— in-doubt 와 informing TDES를 분류하는 analysis 패스.knowledge/code-analysis/cubrid/cubrid-lock-manager.md— prepared 레코드가 직렬화하는 lock 카탈로그를 가진 lock 매니저.
교재 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”- Database Internals (Petrov), 13장 Distributed Transactions 2PC, Paxos commit, presumed abort/commit.
- Gray, Notes on Database Operating Systems, 1978 — 원본 2PC 프로토콜 기술.
- Concurrency Control and Recovery in Database Systems (Bernstein 외), 7장 Distributed Recovery.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/transaction/log_2pc.{c,h}src/transaction/log_record.hpp—LOG_REC_2PC_*페이로드 struct.src/transaction/log_recovery.c— 2PC 레코드의 analysis-pass 분류.src/transaction/log_tran_table.c— TDES 할당 (gtrid가 TDES 위에 산다).src/transaction/transaction_{cl,sr}.{h,c}— 공개tran_2pc_*/xtran_2pc_*API.