콘텐츠로 이동

(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 식별자 공간을 정의하는 이유다.

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

  1. 한 사이트가 coordinator와 participant 모두일 수 있는가. 하나의 CUBRID 서버가 두 역할을 동시에 할 수 있다. 이 서버 를 갱신하고 동시에 연결된 다른 서버를 갱신하는 query는 분산 트랜잭션이며, 이 서버가 coordinator고 다른 서버가 participant 다. CUBRID의 LOG_2PC_EXECUTE enum이 역할별로 디스패치한다.
  2. prepared 트랜잭션을 충돌을 가로질러 어떻게 식별하는가. 프로토콜은 글로벌 ID (gtrid / XID) 가 필요하다. local trid 가 재사용되어도 살아남아야 한다. CUBRID은 log_2pc_start 에서 gtrid를 할당하고 TDES에 저장한다. in-doubt 복구가 prepared 상태 로그 레코드로부터 gtrid → tid map을 다시 만든다.

이 두 답이 보이고 나면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나를 구현하거나 그 프로토콜을 내구하게 만들기 위해 존재한다는 점이 분명해진다.

2PC를 지원하는 모든 엔진이 Gray의 프로토콜 위에 비슷한 패턴을 얹는다.

결정 경계에서의 강제 로그 쓰기

섹션 제목: “결정 경계에서의 강제 로그 쓰기”

prepared 상태와 결정은 네트워크 메시지가 보내지기 전에 안정 저장소에 닿아야 한다. 두 레코드 모두 force-flush 된다 (cubrid-log-manager.md §Force-at-commit). PG, InnoDB, Oracle 모두 이 규율을 공유한다.

local trid는 트랜잭션이 끝나면 재사용된다. gtrid 는 in-doubt 복구가 끝나거나 TM이 heuristic하게 잊을 때까지 살아남는다. 엔진 은 gtrid를 별도 채널 (TDES 필드, 별도 테이블) 에 저장해, local trid가 재활용되어도 prepared 트랜잭션을 잃지 않게 한다.

분산 트랜잭션의 coordinator 측은 participant 리스트와 ack 상태 를 기억해야 한다. 별도 coordinator 테이블이 아니라 TDES에 붙여 두면, 충돌 복구가 그 정보를 TDES와 함께 LOG_2PC_START / LOG_2PC_PREPARE 로그 레코드로부터 같이 복원한다.

PREPARE를 보낸 뒤 결정 전에 coordinator가 충돌하면, in-doubt participant들은 ABORT를 가정해야 한다. commit-decision 레코드를 찾을 수 없기 때문이다. 표준 presumed-abort 최적화는 간단하다. 이미 보낸 abort 결정에 대해서는 로그 레코드를 발행 하지 않는다. coordinator의 침묵을 participant가 abort로 가정 한다.

Java/JTA + X/Open XA 사양이 TM↔RM 계약을 정의한다. CUBRID은 XA 를 tran_2pc_* 클라이언트 API로 노출한다. 여기서 gtrid 가 XA의 XID 가 된다. CUBRID 서버는 RM (participant 로 붙을 때) 이자 내부 coordinator (자기 의존 participant 들을 구동할 때) 의 역할을 함께 한다.

이론적 개념CUBRID 명칭
Coordinator/participant 역할 enumLOG_2PC_EXECUTE { FULL, PREPARE, COMMIT_DECISION, ABORT_DECISION }
Global transaction idLOG_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 commitlog_2pc_commit_first_phase (log_2pc.c:437)
Phase 2 commitlog_2pc_commit_second_phase (log_2pc.c:503)
Phase dispatchlog_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 querylog_2pc_recovery_prepared (log_2pc.c:915)
XA gtrid attachlog_2pc_attach_global_tran (log_2pc.c:1036)
XA prepare for attached gtridlog_2pc_prepare_global_tran (log_2pc.c:1126)

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 — src/transaction/log_2pc.h:45
enum 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_STATE
log_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 역할을 할 때 coord 포인터가 LOG_2PC_COORDINATOR 블록을 가리킨다.

// LOG_2PC_COORDINATOR — src/transaction/log_2pc.h:64
struct 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:399
struct 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 가 트랜잭션 종료 시 풀어 준다.

gtridlog_2pc_start 시점에 발급되는 정수이며, log_tdes::gtrid (log_impl.h:499) 에 저장된다. 짝이 되는 LOG_2PC_GTRINFO 가 XA 스타일 페이로드를 들고 다닌다.

// LOG_2PC_GTRINFO — src/transaction/log_2pc.h:57
struct 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와 충돌하지 않는지 확인).

log_2pc_commit (..., FULL, &decision) 또는 log_2pc_commit (..., PREPARE, &decision) 으로 호출되는 phase 1 함수가 다음을 한다.

  1. (root에서만) LOG_2PC_START 레코드를 append. participant들을 나열한다.
  2. 각 participant에 PREPARE 송신 (log_2pc_send_prepare, log_2pc.c:190).
  3. 로컬 TDES를 LOG_2PC_PREPARE 를 append (LOG_REC_2PC_PREPCOMMIT 페이로드 포함).
  4. 로그 force-flush.
  5. 상태를 TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES 로 전이.
  6. participant vote를 기다린다.
  7. 모두 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:387
struct 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 1이 결정을 산출한 다음에는 다음을 한다.

  1. 결정 레코드를 append — LOG_2PC_COMMIT_DECISION (log_record.hpp enum 값 30) 또는 LOG_2PC_ABORT_DECISION (31).
  2. force-flush.
  3. TRAN_UNACTIVE_*_INFORMING_PARTICIPANTS 로 전이.
  4. 결정을 각 participant에 송신 (log_2pc_send_commit_decision / _send_abort_decision, log_2pc.c:222 / 261).
  5. LOG_2PC_*_INFORM_PARTICPS 를 append (32 / 33).
  6. participant ack를 기다린다.
  7. ack가 도착할 때마다 LOG_2PC_RECV_ACK (34) 를 append. ack를 보낸 participant의 인덱스를 함께 적는다.
  8. 모든 ack가 수신되면 TRAN_UNACTIVE_COMMITTED / _ABORTED 로 전이하고 lock을 해제한다.

Prepared 상태 내구화 — 로그 레코드들

섹션 제목: “Prepared 상태 내구화 — 로그 레코드들”

여섯 record type이 내구 2PC 흔적을 만든다.

타입 번호이름목적
28LOG_2PC_PREPARElock 카탈로그를 가진 로컬 prepared 상태
29LOG_2PC_STARTparticipant 리스트에 대한 coordinator 기록
30LOG_2PC_COMMIT_DECISIONPhase 2 commit 결정
31LOG_2PC_ABORT_DECISIONPhase 2 abort 결정
32LOG_2PC_COMMIT_INFORM_PARTICPSparticipant들에 commit 송신
33LOG_2PC_ABORT_INFORM_PARTICPSparticipant들에 abort 송신
34LOG_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_PARTICPS
LOG_2PC_RECV_ACK (×N)
LOG_COMMIT

LOG_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 API (xa_prepare, xa_commit, xa_rollback, xa_recover) 가 클라이언트의 tran_2pc_* (transaction_cl.h) 를 거쳐 서버의 xtran_2pc_* 로 흐른다. 핵심 진입점은 다음과 같다.

  • tran_2pc_startlog_2pc_start (log_2pc.c:833) — gtrid 를 발급하고 TDES에 설치.
  • tran_2pc_preparelog_2pc_prepare (log_2pc.c:877) — 로컬 서버가 root이면 LOG_2PC_EXECUTE_FULL 로 phase 1 실행. 중간이면 LOG_2PC_EXECUTE_PREPARE 로.
  • tran_2pc_recovery_preparedlog_2pc_recovery_prepared (log_2pc.c:915) — xa_recover 등가물. TM이 해결해야 할 in-doubt gtrid 리스트를 반환한다.
  • tran_2pc_attach_global_tranlog_2pc_attach_global_tran (log_2pc.c:1036) — xa_start resume. 기존 gtrid에 다시 붙기 (연결 failover나 thread-per-request 서버에서의 thread switch 후에 사용).
  • tran_2pc_prepare_global_tranlog_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_EXECUTE enum (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 페이로드.
  • 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) — 해제.
  • 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 재획득.
  • 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_start resume.
  • 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 enumlog_2pc.h45
LOG_2PC_GTRINFO (struct)log_2pc.h58
LOG_2PC_COORDINATOR (struct)log_2pc.h65
log_2pc_get_num_participantslog_2pc.c132
log_2pc_dump_participantslog_2pc.c162
log_2pc_send_preparelog_2pc.c190
log_2pc_send_commit_decisionlog_2pc.c222
log_2pc_send_abort_decisionlog_2pc.c261
log_2pc_make_global_tran_idlog_2pc.c323
log_2pc_check_duplicate_global_tran_idlog_2pc.c407
log_2pc_commit_first_phaselog_2pc.c437
log_2pc_commit_second_phaselog_2pc.c503
log_2pc_commitlog_2pc.c632
log_2pc_set_global_tran_infolog_2pc.c705
log_2pc_get_global_tran_infolog_2pc.c772
log_2pc_startlog_2pc.c833
log_2pc_preparelog_2pc.c877
log_2pc_recovery_preparedlog_2pc.c915
log_2pc_find_tran_descriptorlog_2pc.c952
log_2pc_attach_clientlog_2pc.c984
log_2pc_attach_global_tranlog_2pc.c1036
log_2pc_prepare_global_tranlog_2pc.c1126
log_2pc_read_prepare (LSA variant)log_2pc.c1313
log_2pc_read_prepare (reader variant)log_2pc.c1389
log_2pc_dump_gtrinfolog_2pc.c1476
log_2pc_dump_acqobj_lockslog_2pc.c1491
log_2pc_append_startlog_2pc.c1513
log_2pc_append_decisionlog_2pc.c1570
  • LOG_2PC_EXECUTE enum은 네 값을 가지며, 그 중 셋이 비-root coordinator를 위한 것이다. log_2pc.h:45 에서 검증. FULL 이 root 경로다. 나머지 셋이 위로부터의 participant 이자 아래의 coordinator인 트리 중간 노드에 대응한다.

  • Coordinator 정보는 별도 구조가 아니라 TDES에 붙는다. log_impl.h:506 에서 검증 (LOG_TDES::coordLOG_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_preparedxa_recover 등가물이다. 시그니처 (int gtrids[], int size) 와 이름으로 검증. 현재 in-doubt인 gtrid 리스트를 반환한다. 외부 TM이 이를 받아 해결 한다.

  • log_2pc_attach_global_tran 이 gtrid로 트랜잭션을 resume 한다. log_2pc.c:1036 에서 검증. XA xa_start resume 경로가 사용한다. 이전에 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 는 실제 복구 사용에 쓰인다.

  1. Heuristic abort / heuristic commit 처리. XA가 해결된 heuristic 결정을 위해 xa_forget 을 정의한다. CUBRID의 API 표면 (tran_2pc_*) 에 heuristic 결정 record type이 명시적 으로 노출되지는 않는다. 추적 경로 — tran_2pc_*xtran_2pc_* 에서 forget 호출을 검색.

  2. Presumed-abort 최적화. 표준 “coordinator timeout 시 abort 로그 레코드 안 씀” 패턴이 CUBRID에서 구현되어 있는가? log_2pc_send_abort_decision 이 송신 전에 레코드를 append 한다. 이 동작이 force-flush 되는지, 또는 coordinator timeout 시 건너뛰어 지는지가 추적되지 않았다. 추적 경로 — log_2pc_send_abort_decision 본문 읽기.

  3. 다중 레벨 coordination 트리. LOG_2PC_EXECUTE_PREPARE 가 내가 participant이자 아래의 coordinator 를 처리한다. 3+ 레벨은 어떻게 처리되는가? vote가 직렬로 위로 전파되는가? 추적 경로 — log_2pc_commit_first_phaseLOG_2PC_EXECUTE_PREPARE arm 읽기.

  4. LOG_RECOVERY_FINISH_2PC_PHASE 호출 자리. phase가 log_impl.h:631 에 명명되어 있으나, cubrid-recovery-manager.md 의 log_recovery driver는 이를 명시적으로 부르지 않는다. log_2pc_recovery 가 어디에서 호출되는가? 추적 경로 — log_2pc_recovery 의 호출자를 grep.

  5. 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 본문 읽기.

  6. gtrid 공간 소진. gtrid 가 int (~20억) 다. 재활용 vs. 소진 동작이 추적되지 않았다. 추적 경로 — log_2pc_make_global_tran_idlog_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.pdf
  • Transaction 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.hppLOG_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.