콘텐츠로 이동

(KO) CUBRID HA 복제 — copylogdb와 applylogdb로 구현되는 논리 로그 기반 master/slave 복제

목차

관계형 데이터베이스 엔진에서의 복제는, 추상화 수준에서 보면, 두 on-disk 데이터베이스를 어떤 질의 부하 아래에서도 동등하게 유지 하면서, 그 두 노드 위의 writer 들이 statement 별로 서로 협조하지 않게 만드는 일이다. 교과서적인 설정 (Kleppmann Designing Data-Intensive Applications 5장 Replication; Petrov Database Internals 13장 Replication) 은 이 설계 공간을 세 축으로 쪼갠다. CUBRID 을 비롯한 모든 실제 엔진은 이 세 축 위에서 자기 좌표를 골라야 한다.

첫째 축은 무엇을 운반하는가 다. 물리 복제는 엔진 자기 WAL 레코드 — 페이지 단위 redo 레코드를 그 on-disk 형식 그대로 — 운반 한다. slave 는 master 와 byte 단위로 일치한다. 같은 페이지 레이아웃, 같은 heap 의 free space 분포, 같은 LSA 에서의 같은 B-tree 페이지 분할까지 똑같다. PostgreSQL streaming replication, MySQL InnoDB redo log replication, Oracle physical standby 가 이 모델이다. 논리 복제는 statement 또는 행 이벤트 를 운반한다. “INSERT INTO t VALUES (…) 라거나, PK=X 인 행 이 갱신됐다, before=A, after=B” 같은 식이다. slave 는 동등하지 만 byte 단위로 같지는 않다. 물리 레이아웃을 다르게 잡을 수도 있 다는 점이다. MySQL row-based binlog, PostgreSQL logical replication slot, Oracle GoldenGate 가 이 길을 간다.

이 trade-off 는 대칭적이다. 물리 복제는 만들기 싸다 (WAL 은 어차 피 존재한다. 추가 emission 비용이 없다). 적용도 빠르다 (페이지 위에 memcpy). 하지만 slave 가 master 와 byte 호환이어야 한다는 강한 제약이 따른다. 스키마 분기, 혼합 버전 클러스터, 부분 복제 (테이블 일부만 복제) 가 모두 막힌다는 점이다. 논리 복제는 만들 기 비싸다 (엔진이 DML 시점에 행 이미지와 메타데이터를 추출해야 한다). 적용도 느리다 (행 이벤트마다 SQL 실행 경로를 다시 탄다). 대신 slave 가 master 의 물리 레이아웃과 분리되며, 혼합 버전과 테이블 단위 필터링이 가능해지고, wire format 이 합의만 된다면 다른 엔진과도 호환 가능해진다.

둘째 축은 statement 단위인가 행 단위인가 다. 논리 진영 안에 서, 한 이벤트가 SQL 문 자체일 수도 있고 (“UPDATE t SET c = c+1 WHERE x > 10), 행 이미지일 수도 있다 (OID=O 인 행, before=A, after=B”). statement 단위 이벤트는 작지만 비결정적이다. NOW()RAND() 호출이 양쪽 노드에서 같은 결과를 내야 하고, ORDER BY 가 없는 정렬-민감 질의는 양쪽이 행을 다른 순서로 적용해 결과 가 갈릴 수 있다. 행 단위 이벤트는 더 크지만 결정적이다. slave 가 질의 plan 이 아니라 행 이미지를 그대로 적용하기 때문이 다. 현대 엔진 대부분은 행 단위를 기본으로 두거나 (MySQL 5.7 이후, PostgreSQL logical replication) 둘을 함께 노출한다 (CUBRID 은 두 형식 모두 발행한다. 행 이벤트는 LOG_REPLICATION_DATA, DDL 과 trigger-bound 문은 LOG_REPLICATION_STATEMENT).

셋째 축은 동기인가 비동기인가 다. 동기 복제는 master 의 commit 을 slave 가 해당 commit 의 레코드를 ack 할 때까지 잡고 있다. 무손실 failover 를 보장하지만 master commit latency 가 slave round-trip 시간 + apply 시간만큼 늘어난다는 비용이 있다. 비동기 복제는 master 가 즉시 commit 하고, slave 는 자기 페이스 로 따라잡으며, 다음 배치가 출하되기 전 master 가 죽으면 그 in- flight 윈도우가 손실된다. 교과서적 의미의 eventually consistent 다. slave 는 master 의 추가 쓰기가 없고 시간이 충분하면 master 상태로 수렴한다. CUBRID HA 복제는 비동기다. slave 의 apply 가 master 의 commit 으로부터 분리되어 있고, 유일한 계약은 slave 의 apply 순서가 master 의 commit 순서와 일치한다는 점뿐이라는 점이 다.

이 세 축을 답하고 나면, 본 문서에 등장하는 모든 CUBRID 고유 구조는 이 세 축 위에서의 한 좌표 선택을 구현하는 것이거나, 그 선택으로 만들어지는 상태 기계를 내구하게 만들기 위한 것임이 드러난다.

교과서적 추상화의 한 단계 아래에는, primary/standby DBMS 가 논리 복제 경로를 출하하는 한 — MySQL row-based binlog, PostgreSQL logical decoding, Oracle GoldenGate, CUBRID HA — 같은 한 줌의 패턴들이 반복적으로 등장한다. 이 패턴들은 원본 복제 챕터에는 없지만, 모델과 소스 사이의 빈자리를 채우는 엔지니어 링 어휘다.

DML 시점에 발행되는 보조 로그 레코드

섹션 제목: “DML 시점에 발행되는 보조 로그 레코드”

master 의 정상 WAL 은 physiological 이다. LOG_UNDOREDO_DATA 한 레코드가 한 페이지 mutation 의 page id, slot id, before-image, after-image 를 들고 다닌다. 그 레코드는 master 자기 crash recovery 에는 충분하지만, slave 측 applier 는 master 의 그 시점 카탈로 그 상태 — 어떤 class id 가 어떤 테이블 이름에, 어떤 인덱스가 어떤 컬럼에, 어떤 heap 레이아웃이 어떤 slot 에 대응하는지 — 를 함께 알지 못하면 그 레코드를 디코딩할 수 없다. 처방은 보편적이다. DML 시점에 두 번째 레코드를 함께 발행한다. 그 레코드는 카탈로그까지 풀린 논리 view (테이블 이름, primary key 컬럼, primary key 값, 연산 종류) 를 들고 다닌다. MySQL row-based binlog, PostgreSQL pgoutput 플러그인, CUBRID 의 LOG_REPLICATION_DATA 모두가 같은 발상이다. physiological WAL 옆에 함께 앉아 있는 잉여 레코드다. 대역폭을 미리 지불해, slave 가 master 의 카탈로그를 묻지 않고도 적용할 수 있게 한다.

트랜잭션 디스크립터 위의 master 측 staging

섹션 제목: “트랜잭션 디스크립터 위의 master 측 staging”

DML 한 번이 하나 이상의 복제 레코드를 만들어 내지만, 그 레코드 들이 곧바로 WAL 스트림에 추가되지는 않는다. commit 까지는 어딘 가에 살아 있어야 한다. 표준은 트랜잭션 디스크립터에 달아 둔 per-transaction 배열이다 (CUBRID 의 tdes, MySQL 의 binlog_cache, PostgreSQL 의 ReorderBuffer 항목). commit 시 그 배열을 걸어가며 각 entry 를 실제 WAL 레코드로 바꾸어 commit 레코드와 함께 atomically 적재하고, abort 시는 배열을 통째로 버린다. CUBRID 의 tdes->repl_records 가 정확히 이것이다. commit 까지 staging 된 LOG_REPL_RECORD 의 배열이다.

복제 레코드와 commit 레코드의 atomic 발행

섹션 제목: “복제 레코드와 commit 레코드의 atomic 발행”

slave 측 apply 알고리즘은 로그를 forward 로 걷다가 LOG_COMMIT 을 만나면 그 트랜잭션의 staging 된 복제 레코드들을 모두 flush 한다. slave 가 의지하는 interleaving 규칙은 엄격 하다. 트랜잭션 T 의 마지막 복제 레코드와 commit 레코드 사이 에는 다른 트랜잭션의 commit 레코드가 끼어 들면 안 된다. 만약 다른 peer 트랜잭션의 commit 이 가운데로 새 들어왔는데 slave 가 바로 그 두 레코드 사이에서 죽었다가 재기동하면, T 의 레코드를 적용하지 않은 채 T 를 commit 된 것으로 잘못 표시할 수 있다는 점이다. 이를 막기 위한 처방은 prior-LSA mutex 한 번을 들고 있 는 동안 T 의 staging 된 복제 레코드와 commit 레코드를 함께 emit 하는 것이다. CUBRID 의 log_append_repl_info_and_commit_log 가 정확히 이 idiom 이다. 잠그고, 모든 repl 레코드를 추가하고, commit 레코드를 추가하고, 푼다.

slave 측 push 또는 pull, 그리고 daemon 의 위치

섹션 제목: “slave 측 push 또는 pull, 그리고 daemon 의 위치”

slave 의 로그 fetch 는 slave 의 로그 apply 와 별개의 관심사다. 로그 가져오기는 slave 서버 자기 안의 thread 가 할 수도 있고 (PostgreSQL 의 walreceiver), out-of-process daemon 이 할 수도 있다 (CUBRID 의 copylogdb, 옛 MySQL 의 replication SQL thread). apply 측도 마찬가지로 분리 가능하다. 두 측면을 분리하는 것은 보편적이다. 둘의 실패 양상이 독립이기 때문이다. 느린 apply 가 slave 의 새 로그 수신을 막아서는 안 된다. 그러면 slave 가 무한 정 뒤처지고, master 의 archive 가 그 발 밑에서 결국 사라져 버 릴 것이다.

위치 cursor 와 내구성 있는 bookmark 기반 forward 로그 walking

섹션 제목: “위치 cursor 와 내구성 있는 bookmark 기반 forward 로그 walking”

applier 는 LSA cursor 를 들고 다닌다. 한 이벤트가 적용되고 다 운스트림에 ack 된 후에야 cursor 를 전진시킨다. 재기동 시에는 cursor 를 영구 위치 (slave 측 시스템 테이블, 로그 디렉토리의 파일, 컨트롤 데이터베이스 안의 entry) 로부터 읽어 들인다. CUBRID 의 _db_ha_apply_info 시스템 테이블이 정확히 이것이다. committed_lsa, committed_rep_lsa, required_lsala_log_commit 가 갱신한다. daemon 이 재기동하면 이전 실행이 멈춘 자리에서 다시 시작한다.

slave 측 commit 까지의 트랜잭션별 buffering

섹션 제목: “slave 측 commit 까지의 트랜잭션별 buffering”

논리 이벤트는 interleaved 순서로 발행된다 (T1 의 INSERT, T2 의 UPDATE, T1 의 INSERT, T2 의 COMMIT, T1 의 COMMIT). 그러나 slave 는 그 이벤트를 commit 순서로, 트랜잭션 단위로, T2 전체 다음에 T1 전체 같은 식으로 보고 싶어한다. applier 는 이를 trid 별 pending 이벤트 hash 로 푼다. LOG_COMMIT 을 만나면 그 trid 의 bucket 을 순서대로 디스패치하고, LOG_ABORT 면 그 bucket 을 버린다. CUBRID 의 la_Info.repl_lists[] (트랜잭션별 LA_APPLY 배열) 와 la_Info.commit_head (commit 된 트랜잭션의 LA_COMMIT queue) 가 이 프로토콜을 구체화한다.

이론적 개념CUBRID 명칭
보조 논리 이벤트 로그 레코드LOG_REPLICATION_DATA = 39LOG_REPLICATION_STATEMENT = 40 (log_record.hpp:116-117)
레코드 종류 (recovery index)RVREPL_DATA_INSERT/UPDATE/DELETE/STATEMENT/UPDATE_START/UPDATE_END (recovery.h:149-154)
master 측 트랜잭션별 staging 항목LOG_REPL_RECORD (replication.h:78) — repl_type, rcvindex, inst_oid, lsa, repl_data, length, must_flush, tde_encrypted 보유
디스크립터 위의 트랜잭션별 배열LOG_TDES::repl_records[], num_repl_records, cur_repl_record, fl_mark_repl_recidx (log_impl.h:522-526)
UPDATE 의 LSA back-patchLOG_TDES::repl_insert_lsa, repl_update_lsa (log_impl.h:527-528); repl_add_update_lsa (replication.c:229)
staging 으로의 insertrepl_log_insert (replication.c:293)
statement 단위 emissionrepl_log_insert_statement (replication.c:512)
시스템 DDL 의 flush markrepl_start_flush_mark (replication.c:606), repl_end_flush_mark (replication.c:635)
master 측 commit 시점 emissionlog_append_repl_info_internal (log_manager.c:4555), log_append_repl_info_and_commit_log (log_manager.c:4647)
atomic repl + commit emissionlog_append_repl_info_and_commit_log 가 두 append 를 한 번의 prior_lsa_mutex 보유 안에서 수행
복사 프로토콜의 server 측xlogwr_get_log_pages (log_writer.c:2571)
slave 측 daemon (copylogdb)logwr_copy_log_file (log_writer.c:1659/1960); logwr_flush_all_append_pages (1016) 와 logwr_archive_active_log (1275) 로 기록
slave 측 daemon (applylogdb)la_apply_log_file (log_applier.c:8074)
slave 의 레코드별 dispatchla_log_record_process (log_applier.c:6101)
slave 의 trid별 pending 리스트LA_INFO::repl_lists[] of LA_APPLY (log_applier.c:255-264, 298)
slave 의 commit queueLA_INFO::commit_head / commit_tail of LA_COMMIT (log_applier.c:266-276, 304-305)
slave 의 dispatch 분기la_apply_repl_logitem->item_type 으로 la_apply_insert/update/delete/statement_log 분기
slave 의 내구성 있는 bookmark_db_ha_apply_infoLA_HA_APPLY_INFO 행 (log_applier.c:393)
slave 의 retry 가능 에러 마스크LA_RETRY_ON_ERROR (log_applier.h:34)
slave 의 테이블 단위 필터REPL_FILTER_TYPELA_REPL_FILTER (log_applier.h:48, log_applier.c:206)

CUBRID HA 복제에는 네 개의 이동 부품이 있다. master 측 emission 경로 는 DML 도중, 카탈로그 단위로 보이는 행 mutation 마다 LOG_REPL_RECORD 하나를 트랜잭션 디스크립터에 적재한다. master 측 flush 경로 는 commit 시 그 staging 된 레코드들을 LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT 로 변환해 commit 레코드와 atomic 하게 로그 스트림에 빼낸다. copylogdb 는 slave 호스트에서 client 모드로 도는 daemon 이다. 한 번의 net 요청으로 master 의 active 와 archive 로그 볼륨을 끌어와 로컬 저장소에 적는다. applylogdb 는 또 다른 client 모드 daemon 으로, 그 로컬 로 그 볼륨을 forward 로 걸으며 레코드 종류별로 dispatch 하고, slave 서버의 정상 client API 로 DML 을 재생한다. 이 순서로 본다.

flowchart LR
  subgraph M["Master cub_server"]
    DML["DML 트랜잭션\n(qexec_execute_∗)"]
    LOC["locator_∗_force\nlocator_attribute_info_force"]
    HEAP["heap_∗_logical\nbtree_update"]
    REPL["repl_log_insert\nrepl_add_update_lsa"]
    TDES["tdes->repl_records[]\n(LOG_REPL_RECORD)"]
    COMMIT["log_commit ->\nlog_append_repl_info_and_commit_log"]
    PRIOR["prior_lsa list +\nlog_append_repl\n(LOG_REPLICATION_DATA)"]
    LGAT["active 로그\n(lgat) + archive 볼륨"]
    XLW["xlogwr_get_log_pages\n(NET_SERVER_LOGWR_GET_LOG_PAGES)"]
    DML --> LOC --> HEAP --> REPL --> TDES
    DML --> COMMIT --> PRIOR --> LGAT
    LGAT --> XLW
  end
  subgraph S["Slave 호스트"]
    CLDB["copylogdb\nlogwr_copy_log_file"]
    SLOG["slave 측 로그 볼륨\n(active + archive)"]
    APPL["applylogdb\nla_apply_log_file"]
    REC["la_log_record_process\n레코드별 dispatch"]
    PEND["la_Info.repl_lists[]\ntrid별 LA_APPLY"]
    CQ["la_Info.commit_head\nLA_COMMIT queue"]
    AP["la_apply_repl_log\nla_apply_insert/update/delete/statement_log"]
    SLAVE["slave cub_server\n(client 모드 연결)"]
    HA["_db_ha_apply_info 행\nLA_HA_APPLY_INFO"]
    XLW -- "log pages" --> CLDB --> SLOG --> APPL
    APPL --> REC
    REC -- "REPL records" --> PEND
    REC -- "COMMIT/ABORT" --> CQ
    CQ --> AP
    AP --> SLAVE
    AP --> HA
  end

이 그림은 네 경계를 인코딩한다. (emit / flush) master 는 DML 도중 tdes->repl_records[] 를 채우지만, 실제 LOG_REPLICATION_DATA 로그 레코드는 commit 시 log_append_repl_info 가 비로소 적는 다는 점이다. (commit atomicity) log_append_repl_info_and_commit_log 는 prior-LSA mutex 를 한 번만 들고, 그 보유 동안 repl append 와 commit append 를 모두 수행해, peer 의 commit 이 그 사이에 끼어 들지 못하게 한다. (copy / apply) copylogdbapplylogdb 는 별개 프로세스다. on-disk 로그 볼륨만 공유한다. apply 의 느림 이 copy 에 backpressure 를 줄 수 없다. (client 모드의 slave 서버) apply daemon 은 일반 client/server 프로토콜로 slave 의 cub_server 에 붙는다. 페이지를 직접 쓰지 않는다는 뜻 이다. 테이블 단위 필터링, 스키마 분기 허용, 행 단위 에러 retry 가 가능해지는 이유가 여기에 있다.

Master 측 — LOG_REPL_RECORD 와 staging 배열

섹션 제목: “Master 측 — LOG_REPL_RECORD 와 staging 배열”

master 의 DML 동작은 비-HA 서버와 같은 실행 경로를 탄다.

sqmgr_execute_query
→ xqmgr_execute_query
→ qmgr_process_query
→ qexec_execute_main_block / qexec_execute_mainblock_internal
→ qexec_execute_<insert|update|delete>
→ locator_attribute_info_force
→ locator_insert_force / locator_update_force / locator_delete_force

locator_*_force 안에서, 모든 행 연산은 두 부수 효과를 같이 일 으킨다. 하나는 physiological 로그 레코드의 발행이다. heap_insert_logical / heap_update_logical / heap_delete_logical (또는 인덱스 측의 btree_update) 가 그것 이다. 다른 하나는 복제 레코드의 적재다. repl_log_insert 가 호출된다. 이 시점에 복제 레코드는 WAL 스트림에 들어가지 않는다. 트랜잭션 디스크립터의 per-transaction staging 배열에 추가될 뿐이 다.

// LOG_TDES replication fields — src/transaction/log_impl.h:522
int num_repl_records; /* # of replication records (capacity) */
int cur_repl_record; /* # of replication records used so far */
int append_repl_recidx; /* cursor used at commit-time emission */
int fl_mark_repl_recidx; /* index of flush-marked record (DDL) */
struct log_repl *repl_records; /* the array */
LOG_LSA repl_insert_lsa; /* insert-or-MVCC-update target LSA */
LOG_LSA repl_update_lsa; /* in-place-update target LSA */

staging entry 는 LOG_REPL_RECORD 이다.

// LOG_REPL_RECORD — src/transaction/replication.h:78
typedef struct log_repl LOG_REPL_RECORD;
struct log_repl
{
LOG_RECTYPE repl_type; /* LOG_REPLICATION_DATA or LOG_REPLICATION_STATEMENT */
LOG_RCVINDEX rcvindex; /* RVREPL_DATA_INSERT / UPDATE / DELETE /
UPDATE_START / UPDATE_END / STATEMENT */
OID inst_oid; /* OID of the row being changed */
LOG_LSA lsa; /* LSA of the related "real" log record
(filled in later for UPDATE) */
char *repl_data; /* | pkey size | class_name | pkey dbvalue | */
int length; /* repl_data length */
LOG_REPL_FLUSH must_flush; /* DONT_NEED_FLUSH=-1, COMMIT_NEED_FLUSH=0,
NEED_FLUSH=1 */
bool tde_encrypted; /* class is TDE-encrypted */
};

repl_data payload 는 의도적으로 최소다. 4바이트 packed-key 길 이, or-packed class 이름, or-packed primary-key DB_VALUE 만 들어 간다. 이 시점에 slave 는 전체 행 이미지를 필요로 하지 않는 다. apply 시점에 master 의 heap log 로부터 행을 다시 가져 올 것 이기 때문이다. staging entry 를 작게 유지함으로써 DML 이 수백만 행을 건드릴 때의 트랜잭션별 메모리 비용을 한정한다.

// repl_log_insert — src/transaction/replication.c:293 (condensed)
int
repl_log_insert (THREAD_ENTRY *thread_p, const OID *class_oid, const OID *inst_oid,
LOG_RECTYPE log_type, LOG_RCVINDEX rcvindex,
DB_VALUE *key_dbvalue, REPL_INFO_TYPE repl_info)
{
int tran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);
LOG_TDES *tdes = LOG_FIND_TDES (tran_index);
LOG_REPL_RECORD *repl_rec;
if (tdes->suppress_replication != 0) {
LSA_SET_NULL (&tdes->repl_insert_lsa);
LSA_SET_NULL (&tdes->repl_update_lsa);
return NO_ERROR;
}
/* Allocate / grow tdes->repl_records as needed. */
if (REPL_LOG_IS_NOT_EXISTS (tran_index))
repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, false);
else if (REPL_LOG_IS_FULL (tran_index))
repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, true); /* realloc +100 */
repl_rec = (LOG_REPL_RECORD *) (&tdes->repl_records[tdes->cur_repl_record]);
repl_rec->repl_type = log_type;
repl_rec->rcvindex = rcvindex;
/* RBR_START / RBR_END refine UPDATE into UPDATE_START / UPDATE_END */
if (rcvindex == RVREPL_DATA_UPDATE) { /* ... map repl_info → rcvindex ... */ }
COPY_OID (&repl_rec->inst_oid, inst_oid);
if (log_type == LOG_REPLICATION_DATA) {
/* Build | packed_key_size | class_name | pkey_dbvalue | */
repl_rec->length = OR_INT_SIZE
+ or_packed_string_length (class_name, &strlen)
+ OR_VALUE_ALIGNED_SIZE (key_dbvalue);
repl_rec->repl_data = malloc (repl_rec->length);
/* ... pack class_name + key_dbvalue, fill packed_key_size ... */
}
repl_rec->must_flush = LOG_REPL_COMMIT_NEED_FLUSH;
/* Bookkeeping: link the LSA of the heap log to the repl record. */
switch (rcvindex) {
case RVREPL_DATA_INSERT:
if (!LSA_ISNULL (&tdes->repl_insert_lsa)) {
LSA_COPY (&repl_rec->lsa, &tdes->repl_insert_lsa);
LSA_SET_NULL (&tdes->repl_insert_lsa);
LSA_SET_NULL (&tdes->repl_update_lsa);
}
break;
case RVREPL_DATA_UPDATE:
/* For update, the heap log is written *after* the repl record;
repl_add_update_lsa back-patches repl_rec->lsa later. */
LSA_SET_NULL (&repl_rec->lsa);
break;
case RVREPL_DATA_DELETE:
/* For delete, no after-image is needed — pkey is enough. */
break;
}
tdes->cur_repl_record++;
tdes->must_flush = LOG_REPL_NEED_FLUSH;
return NO_ERROR;
}

세 가지 짚을 점이 있다. (a) 기본 배열 크기는 100 (REPL_LOG_INFO_ALLOC_SIZE) 이다. overflow 시 realloc 으로 100 씩 키운다. 행 mutation 이 100 건을 넘기지 않는 트랜잭션은 realloc 비용을 한 번도 지불하지 않는다는 점이다. (b) RVREPL_DATA_UPDATEREPL_INFO_TYPE 에 따라 세 sub-kind 중 하나로 사상된다. UPDATE, UPDATE_START, UPDATE_END. system op 안에서의 multi-statement update 를 slave 가 식별할 수 있도록 하기 위함이다 (START / END 쌍이 변경 구간의 괄호 역할을 한다). (c) 복제 레코드의 lsa 필드와 heap 로그 사이의 관계는 연산 별로 비대칭이다. INSERT 의 경우, repl_log_insert 호출 에 heap 로그가 이미 적혀 있다. 그래서 tdes->repl_insert_lsa 가 이미 그 값을 들고 있고, 함수가 그것을 그대로 복사한다. UPDATE 의 경우, heap 로그는 repl_log_insert 를 trigger 한 인덱스 갱 신 경로 이후 에 적힌다. 그래서 필드를 일단 null 로 두고, locator_update_force 가 heap LSA 를 손에 쥔 다음 단계에서 repl_add_update_lsa 가 back-patch 한다. DELETE 의 경우 lsa 가 무의미하다. slave 는 primary key 만 있으면 된다.

repl_add_update_lsa — UPDATE 용 back-patch

섹션 제목: “repl_add_update_lsa — UPDATE 용 back-patch”
// repl_add_update_lsa — src/transaction/replication.c:229 (condensed)
int
repl_add_update_lsa (THREAD_ENTRY *thread_p, const OID *inst_oid)
{
LOG_TDES *tdes = LOG_FIND_TDES (LOG_FIND_THREAD_TRAN_INDEX (thread_p));
if (tdes->suppress_replication != 0) return NO_ERROR;
/* Walk backwards through repl_records; the last one matching this
* OID with a non-null repl_update_lsa is the one we just inserted. */
for (int i = tdes->cur_repl_record - 1; i >= 0; i--) {
LOG_REPL_RECORD *repl_rec = &tdes->repl_records[i];
if (OID_EQ (&repl_rec->inst_oid, inst_oid)
&& !LSA_ISNULL (&tdes->repl_update_lsa)) {
assert (repl_rec->rcvindex == RVREPL_DATA_UPDATE
|| repl_rec->rcvindex == RVREPL_DATA_UPDATE_START
|| repl_rec->rcvindex == RVREPL_DATA_UPDATE_END);
LSA_COPY (&repl_rec->lsa, &tdes->repl_update_lsa);
LSA_SET_NULL (&tdes->repl_update_lsa);
LSA_SET_NULL (&tdes->repl_insert_lsa);
return NO_ERROR;
}
}
return NO_ERROR; /* not found is not an error — debug log only */
}

이 함수는 locator_update_force 가 heap update 를 로깅한 직후 에 호출된다. tdes->repl_update_lsa 가 그 heap 로그의 LSA 를 들고 있고, 그에 매칭되는 LOG_REPL_RECORD (방금 btree_update → repl_log_insert 로 만들어진 것) 가 그 LSA 를 자기 lsa 필드에 받게 된다. 뒤에서부터 걸어가는 것이 옳다. 같은 OID 의 가장 최근 매칭이 항상 방금 insert 한 항목이기 때문이다. 같은 행이 한 트 랜잭션 안에서 반복적으로 갱신되어도, 이전 update 의 lsa 는 이미 non-null 이다 (그 자기의 repl_add_update_lsa 호출에서 패치된 바 있다). 따라서 자연스럽게 건너뛰어진다.

master 경로 위에서 INSERT 하나를 따라 걷기

섹션 제목: “master 경로 위에서 INSERT 하나를 따라 걷기”

raw deck 은 이 패턴을 t1(c1 PK, c2)INSERT (1, 가), INSERT (2, 나), COMMIT 으로 예시한다. 각 insert 의 작업 순 서는 다음과 같다.

  1. heap_insert_logicalheap_insert_physical 가 행을 slotted page 에 적는다. heap_log_insert_physical 가 prior list 에 LOG_UNDOREDO_DATA 를 추가한다.
  2. locator_add_or_remove_indexbtree_insert 가 primary-key B-tree 인덱스를 갱신한다. 인덱스 경로 안에서 repl_log_insert 가 호출되어 tdes->repl_records[] 에 새 LOG_REPL_RECORD 하나가 추가된다. rcvindex = RVREPL_DATA_INSERT, inst_oid = 새 행의 OID, lsa = heap 로그의 LSA, payload 는 packed (pk_size, class_name, pk_dbvalue) 다.

두 번째 insert 가 끝난 시점에 tdes->repl_records[] 는 두 entry 를 가진다.

idxrcvindexinst_oidlsarepl_data
0RVREPL_DATA_INSERT(oid_1)LSA(heap_1)t1 + 1
1RVREPL_DATA_INSERT(oid_2)LSA(heap_2)t1 + 2

이 시점까지 WAL 스트림에는 LOG_REPLICATION_DATA 레코드가 존재하지 않는다. 디스크립터 위에만 살아 있다. COMMIT 이 그 변환의 trigger 다.

Commit 시점의 emission — log_append_repl_info_and_commit_log

섹션 제목: “Commit 시점의 emission — log_append_repl_info_and_commit_log”

log_commit 은 (HA 설정이 그것을 요구할 때) log_append_repl_info_and_commit_log 를 호출한다. 이것이 atomic emission idiom 이다.

// log_append_repl_info_and_commit_log — src/transaction/log_manager.c:4647
static void
log_append_repl_info_and_commit_log (THREAD_ENTRY *thread_p, LOG_TDES *tdes,
LOG_LSA *commit_lsa)
{
if (tdes->has_supplemental_log) {
log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER,
strlen (tdes->client.get_db_user ()),
tdes->client.get_db_user ());
tdes->has_supplemental_log = false;
}
log_Gl.prior_info.prior_lsa_mutex.lock ();
log_append_repl_info_with_lock (thread_p, tdes, true);
log_append_commit_log_with_lock (thread_p, tdes, commit_lsa);
log_Gl.prior_info.prior_lsa_mutex.unlock ();
}

mutex 가 두 append 모두를 가로질러 보유된다. 이것이 slave 가 의지하는 atomicity 보장이다. log_append_repl_info_internal 안에서 staging 된 각 레코드는 진짜 prior-list 노드로 변환된다.

// log_append_repl_info_internal — src/transaction/log_manager.c:4555 (condensed)
static void
log_append_repl_info_internal (THREAD_ENTRY *thread_p, LOG_TDES *tdes,
bool is_commit, int with_lock)
{
if (tdes->append_repl_recidx == -1 || is_commit)
tdes->append_repl_recidx = 0;
while (tdes->append_repl_recidx < tdes->cur_repl_record) {
LOG_REPL_RECORD *repl_rec = &tdes->repl_records[tdes->append_repl_recidx];
if ((repl_rec->repl_type == LOG_REPLICATION_DATA
|| repl_rec->repl_type == LOG_REPLICATION_STATEMENT)
&& ((is_commit && repl_rec->must_flush != LOG_REPL_DONT_NEED_FLUSH)
|| repl_rec->must_flush == LOG_REPL_NEED_FLUSH)) {
LOG_PRIOR_NODE *node =
prior_lsa_alloc_and_copy_data (thread_p, repl_rec->repl_type,
RV_NOT_DEFINED, NULL,
repl_rec->length, repl_rec->repl_data,
0, NULL);
LOG_REC_REPLICATION *log = (LOG_REC_REPLICATION *) node->data_header;
if (repl_rec->rcvindex == RVREPL_DATA_DELETE
|| repl_rec->rcvindex == RVREPL_STATEMENT)
LSA_SET_NULL (&log->lsa);
else
LSA_COPY (&log->lsa, &repl_rec->lsa);
log->length = repl_rec->length;
log->rcvindex = repl_rec->rcvindex;
prior_lsa_next_record_with_lock (thread_p, node, tdes);
repl_rec->must_flush = LOG_REPL_DONT_NEED_FLUSH;
}
tdes->append_repl_recidx++;
}
}

이 함수는 LOG_REPLICATION_DATA (= 39) 또는 LOG_REPLICATION_STATEMENT (= 40) 를 발행한다. 둘 다 정상 로그 레코드 타입과 함께 log_record.hpp 에 정의된 진짜 LOG_RECTYPE 값이다. 발행되는 레코드의 LOG_REC_REPLICATION data-header 는 rcvindex (slave 가 INSERT/UPDATE/DELETE/STATEMENT 중 무엇인지 알 수 있게), 참조되는 heap 로그의 lsa (slave 가 행 이미지를 가 져 올 수 있게), 그리고 repl_data payload 의 length (inline class-name + primary-key 바이트 — prior node 의 body 로 복사됨) 를 들고 다닌다.

INSERT 와 STATEMENT 의 경우 발행되는 로그의 lsa 필드는 null 로 설정된다. slave 가 직전의 heap 로그로부터 행을 재구성하기 때문 이다. DELETE 의 경우 lsa 는 null 이다. slave 가 primary key 만 필요로 하기 때문이다. UPDATE 의 경우 repl_add_update_lsa 에서 back-patch 된 lsa 가 그대로 복사된다.

루프가 끝나면 발행된 모든 레코드의 must_flushLOG_REPL_DONT_NEED_FLUSH 로 바뀐다. 후속 abort 경로가 같은 레 코드를 다시 발행하지 못하게 막기 위함이다.

log_append_repl_info_and_commit_log 가 반환되면, prior list 에 는 staging 되어 있던 모든 repl 레코드가 차례로 들어가 있고 그 뒤에 LOG_COMMIT 이 있다. logpb_prior_lsa_append_all_list (cubrid-log-manager.md 참조) 의 drain 이 이 list 를 걸어가며 log page buffer 로 레코드를 복사하고, logpb_flush_all_append_pages 가 그것을 active log file 로 기록한다. slave 로 향하는 emission 이 이로써 master 위에서 내구성을 갖게 되며, copylogdb 의 폴링 시야에 들어 온다.

Slave 측 — copylogdb, 로그 볼륨 puller

섹션 제목: “Slave 측 — copylogdb, 로그 볼륨 puller”

copylogdb 는 client 모드 CUBRID 유틸리티다 (util_service.c 에 등록되어 있고, cubrid hb start 가 나머지 HA 토폴로지와 함께 띄운다). 그 일은 master 의 active 와 archive 로그 볼륨에 대한 slave 측 로컬 사본을 최신 상태로 유지하는 것이다.

프로토콜은 루프 한 회당 one-shot 이다. NET_SERVER_LOGWR_GET_LOG_PAGES 요청을 보낸다. 본문은 slave 가 누락하고 있는 첫 페이지의 LSA (first_pageid_torecv = last_recv_pageid) 다. master 의 xlogwr_get_log_pages (log_writer.c:2571) 가 최대 LOGWR_COPY_LOG_BUFFER_NPAGES * LOG_PAGESIZE 바이트 (기본 128 페이지 × 16 KiB = 2 MiB) 의 연속 로그 페이지로 응답한다. 요청된 페이지가 master 위에 아직 존재하지 않으면 master 는 블록 하고, 존재하게 되면 그때 응답한다. 폴링이 사실상 이벤트 기반 push 로 바뀐다는 점이다.

요청을 처리하는 master 측 골격은 다음과 같다.

// xlogwr_get_log_pages — src/transaction/log_writer.c (high-level)
xlogwr_get_log_pages (THREAD_ENTRY *thread_p, LOG_PAGEID first_pageid, LOGWR_MODE mode)
{
/* For each page from first_pageid to eof_lsa.pageid,
* - if !logpb_is_page_in_archive: read from active via
* logpb_copy_page_from_file → logpb_read_page_from_file → fileio_read
* - else: locate the right archive via logpb_get_guess_archive_num,
* logpb_arv_page_info_table search, then fetch via
* logpb_fetch_from_archive
* Pack via logwr_pack_log_pages, send via xlog_send_log_pages_to_client. */
}

archive 를 찾을 때 master 는 logpb_arv_page_info_table 을 본 다. 이것은 (arv_num, fpageid, lpageid) 레코드를 들고 있는 메모 리 캐시로, archive 가 새로 만들어질 때마다 갱신된다. 캐시가 cold 일 경우에는 Log_Nname_info 파일 스캔으로 fallback 한다. 함수 이름의 guess 는 산술적 추정기를 가리킨다. active 로그 헤더가 가용할 때, 함수는 요청된 pageid 를 LOGPB_ACTIVE_NPAGES 로 나누어 archive 번호를 추정한다. 그렇지 않은 경우 archive 0 부터 시작해 forward 로 스캔한다. 후보 archive 를 잡고 나서는 그 후보의 Arv_hdr->fpageid 를 요청된 pageid 와 비교해 forward (direction = +1) 또는 backward (direction = -1) 로 archive 시퀀스를 따라 걷는다.

slave 측에서는 logwr_copy_log_file (log_writer.c:1659/1960) 가 요청을 보내고, 도착한 페이지로 자기 Logwr_Gl 구조체를 채우 며, logwr_flush_all_append_pages (1016) 로 slave 로컬 active log 에 기록한다. active log 가 자기 크기 경계를 넘어가면 logwr_archive_active_log (1275) 가 현재 active log 의 내용 을 새 archive 볼륨으로 페이지 단위 복사 (fileio_read_pages + fileio_write_pages) 하고, slave 로컬 active log 는 reset 된다. 이름과 구조는 일반 CUBRID 로그 볼륨과 동일하다. applylogdb 가 master 의 recovery 가 읽었을 방식 그대로 읽는다.

Slave 측 — applylogdb, 레코드별 dispatcher

섹션 제목: “Slave 측 — applylogdb, 레코드별 dispatcher”

applylogdb 의 진입점은 la_apply_log_file (log_applier.c:8074) 이다. 장기 daemon 으로 동작한다. main loop 는 la_Info.final_lsa 부터 forward 로 로그 레코드를 가져와 각 레코드를 la_log_record_process 에 넘긴다.

// la_log_record_process — src/transaction/log_applier.c:6101 (condensed)
static int
la_log_record_process (LOG_RECORD_HEADER *lrec, LOG_LSA *final, LOG_PAGE *pg_ptr)
{
/* Defensive: a non-EOL record must have non-null prev_tranlsa. */
if (lrec->trid == NULL_TRANID || LSA_GT (&lrec->prev_tranlsa, final)
|| LSA_GT (&lrec->back_lsa, final)) {
if (lrec->type != LOG_END_OF_LOG) return ER_LOG_PAGE_CORRUPTED;
}
/* First time we see this trid — register an LA_APPLY for it. */
if ((lrec->type != LOG_END_OF_LOG && lrec->type != LOG_DUMMY_HA_SERVER_STATE)
&& lrec->trid != LOG_SYSTEM_TRANID
&& LSA_ISNULL (&lrec->prev_tranlsa)) {
LA_APPLY *apply = la_add_apply_list (lrec->trid);
/* ... start_lsa bookkeeping ... */
}
switch (lrec->type) {
case LOG_END_OF_LOG:
/* Reached end of currently-known log. Set is_end_of_record and
* return ER_INTERRUPTED so the caller waits for more pages. */
return ER_INTERRUPTED;
case LOG_REPLICATION_DATA:
case LOG_REPLICATION_STATEMENT:
/* Buffer this event in the trid's apply list. */
return la_set_repl_log (pg_ptr, lrec->type, lrec->trid, final);
case LOG_SYSOP_END:
case LOG_COMMIT:
/* Flush the trid's apply list onto the slave. */
if (LSA_GT (final, &la_Info.committed_lsa)) {
eot_time = (lrec->type == LOG_SYSOP_END) ? 0
: la_retrieve_eot_time (pg_ptr, final);
la_add_node_into_la_commit_list (lrec->trid, final, lrec->type, eot_time);
do {
error = la_apply_commit_list (&lsa_apply, final_pageid);
/* ... handle ER_NET_CANT_CONNECT_SERVER, ER_HA_LA_EXCEED_MAX_MEM_SIZE,
LA_IS_FLUSH_ERROR, ER_TDE_CIPHER_IS_NOT_LOADED ... */
if (!LSA_ISNULL (&lsa_apply)) {
LSA_COPY (&la_Info.committed_lsa, &lsa_apply);
if (lrec->type == LOG_COMMIT) la_Info.commit_counter++;
}
} while (!LSA_ISNULL (&lsa_apply));
} else {
la_free_repl_items_by_tranid (lrec->trid); /* already past committed */
}
break;
case LOG_ABORT:
la_add_node_into_la_commit_list (lrec->trid, final, LOG_ABORT, 0);
break;
case LOG_DUMMY_HA_SERVER_STATE:
/* Detect master role change; if state != ACTIVE && != TO_BE_STANDBY,
* the slave's role has changed → set is_role_changed and return
* ER_INTERRUPTED so the daemon shuts down cleanly. */
break;
default: break;
}
/* ... handle out-of-bounds forw_lsa / type → ER_LOG_PAGE_CORRUPTED ... */
return NO_ERROR;
}

dispatch 는 빡빡하다. 모든 레코드 종류는 buffer (두 REPL 종류), flush 의 trigger (COMMIT, SYSOP_END, ABORT), 또는 제어용 소비 (DUMMY_HA_SERVER_STATE, END_OF_LOG, DUMMY_CRASH_RECOVERY, END_CHKPT) 중 하나에 속한다. 그 외의 종류는 default 분기로 빠져 버려진다. apply 와 무관하기 때문이다.

la_set_repl_log — REPL 레코드의 buffering

섹션 제목: “la_set_repl_log — REPL 레코드의 buffering”
// la_set_repl_log — src/transaction/log_applier.c:3419
static int
la_set_repl_log (LOG_PAGE *log_pgptr, int log_type, int tranid, LOG_LSA *lsa)
{
LA_APPLY *apply = la_find_apply_list (tranid);
if (apply == NULL) return NO_ERROR;
/* Long transaction: bypass the per-item buffer; just remember last_lsa. */
if (apply->is_long_trans) { LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; }
/* Cap per-trid items at LA_MAX_REPL_ITEMS (1000) — overflow degrades
* the trid into "long transaction" mode (re-fetch from log on apply). */
if (apply->num_items >= LA_MAX_REPL_ITEMS) {
la_free_all_repl_items_except_head (apply);
apply->is_long_trans = true;
LSA_COPY (&apply->last_lsa, lsa);
return NO_ERROR;
}
LA_ITEM *item = la_make_repl_item (log_pgptr, log_type, tranid, lsa);
la_add_repl_item (apply, item);
return NO_ERROR;
}

bucket 구조는 LA_INFO::repl_lists[] 다. trid 의 hash 로 색인되 는 LA_APPLY 포인터의 배열이다. 각 LA_APPLY 는 trid 별 LA_ITEM 의 linked list 를 들고 있다.

// LA_APPLY and LA_ITEM — src/transaction/log_applier.c:236-264
struct la_item {
LA_ITEM *next, *prev;
int log_type; /* LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT */
int item_type; /* RVREPL_DATA_INSERT / UPDATE / DELETE / STATEMENT */
char *class_name; /* unpacked from the REPL record */
char *db_user;
char *ha_sys_prm;
int packed_key_value_length;
char *packed_key_value; /* disk image of pkey value */
DB_VALUE key; /* unpacked from packed_key_value on demand */
LOG_LSA lsa; /* LSA of the LOG_REPLICATION_* record itself */
LOG_LSA target_lsa; /* LSA of the target heap/btree log record */
};
struct la_apply {
int tranid;
int num_items;
bool is_long_trans; /* exceeded LA_MAX_REPL_ITEMS — re-walk on apply */
LOG_LSA start_lsa;
LOG_LSA last_lsa;
LA_ITEM *head;
LA_ITEM *tail;
};

is_long_trans 플래그는 백만 행 update 문제의 escape hatch 다. LA_ITEM 백만 개를 메모리에 들고 있는 대신, daemon 은 start_lsalast_lsa 만 기억하는 모드로 바뀐다. commit 시 점에는 start_lsa 부터 last_lsa 까지 로그를 forward 로 다시 걸어가며 각 REPL 레코드를 다시 읽어 온다. trade-off 는 long transaction trid 당 한 번의 추가 로그 walk 이며, 그 대가로 메모 리가 한정된다.

la_apply_commit_listla_apply_repl_log — apply fan-out

섹션 제목: “la_apply_commit_list 와 la_apply_repl_log — apply fan-out”

LOG_COMMIT 이 도착하면 dispatcher 가 la_Info.commit_head / commit_tailLA_COMMIT 노드를 enqueue 하고, head 가 빌 때까지 la_apply_commit_list 를 루프로 호출한다.

// la_apply_commit_list / la_apply_repl_log — src/transaction/log_applier.c:5920, 5739
static int
la_apply_commit_list (LOG_LSA *lsa, LOG_PAGEID final_pageid) {
LA_COMMIT *commit = la_Info.commit_head;
if (commit && (commit->type == LOG_COMMIT || commit->type == LOG_SYSOP_END
|| commit->type == LOG_ABORT)) {
error = la_apply_repl_log (commit->tranid, commit->type,
&commit->log_lsa, &la_Info.total_rows,
final_pageid);
LSA_COPY (lsa, &commit->log_lsa);
/* ... unlink commit, advance head, update _db_ha_apply_info ... */
}
return error;
}
static int
la_apply_repl_log (int tranid, int rectype, LOG_LSA *commit_lsa,
int *total_rows, LOG_PAGEID final_pageid) {
LA_APPLY *apply = la_find_apply_list (tranid);
if (rectype == LOG_ABORT) { la_clear_applied_info (apply); return NO_ERROR; }
for (LA_ITEM *item = apply->head; item != NULL; item = next) {
if (LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa)
&& la_need_filter_out (item) == false) {
if (item->log_type == LOG_REPLICATION_DATA) {
switch (item->item_type) {
case RVREPL_DATA_UPDATE_START:
case RVREPL_DATA_UPDATE_END:
case RVREPL_DATA_UPDATE: error = la_apply_update_log (item); break;
case RVREPL_DATA_INSERT: error = la_apply_insert_log (item); break;
case RVREPL_DATA_DELETE: error = la_apply_delete_log (item); break;
}
} else if (item->log_type == LOG_REPLICATION_STATEMENT) {
error = la_apply_statement_log (item);
}
if (error == NO_ERROR) LSA_COPY (&la_Info.committed_rep_lsa, &item->lsa);
else if (LA_RETRY_ON_ERROR (error)) { LA_SLEEP (10, 0); continue; }
/* ... handle ER_NET_CANT_CONNECT_SERVER, log error, advance ... */
}
next = la_get_next_repl_item (item, apply->is_long_trans, &apply->last_lsa);
la_free_repl_item (apply, item);
item = next;
}
/* ... end-of-trid bookkeeping; clear or free per LOG_SYSOP_END semantics ... */
return error;
}

la_apply_insert_log, la_apply_update_log, la_apply_delete_log 가 세 worker 다. 같은 모양을 공유한다.

  1. 클래스 해석. LA_ITEMclass_name 을 slave 의 카탈로 그를 해석해 DB_OBJECT* 를 얻는다.
  2. 행 이미지 재구성. INSERT/UPDATE 의 경우, item 의 target_lsa 가 master 의 heap 로그 레코드를 가리킨다. daemon 은 그것을 la_get_log_data 로 읽고, helper 들을 사용한다. la_get_overflow_recdes (BIGONE / link 변경), la_get_relocation_recdes (REC_RELOCATION + REC_NEWHOME), la_get_next_update_log (REC_ASSIGN_ADDRESS deferred update). 결과는 after-image 만 담은 RECDES 다. CUBRID 은 복제용으로 before-image 를 운반하지 않는다는 점이다.
  3. 행 적용. la_repl_add_object 가 재구성된 행을 들고 slave 서버의 정상 client API (db_create, db_otmpl_*) 로 들어간 다. slave 서버는 이 동작을 일반 DML 처럼 수행한다. 자기 락 을 잡고, 자기 MVCC ID 를 만들고, 자기 WAL 을 적는다.
  4. 추적과 retry. 성공 시 committed_rep_lsa 가 전진한다. retry 가능한 에러 (deadlock, lock timeout, page latch abort, TDE cipher not loaded — LA_RETRY_ON_ERROR 마스크) 일 때는 daemon 이 10 초 sleep 한 뒤 재시도한다. retry 불가 에러일 때는 그 동작을 로그로 남기고 daemon 이 그것을 지나친다.

DELETE 는 단순하다. primary key 만 있으면 되고 행 이미지 fetch 가 없다. la_repl_add_objectrecdes = NULL 로 호출된다.

la_get_recdesitem->target_lsa 로부터 after-image RECDES 를 만들어 내는 dispatcher 다. 다섯 가지 레코드 타입 case 가 핵심이다.

1. Normal heap record (REC_HOME).
La_get_log_data() — header + redo + undo, copy redo into recdes.
2. RVOVF_CHANGE_LINK — the record is BIGONE, but only the linkage
to overflow pages changed.
La_get_overflow_recdes(..., RVOVF_PAGE_UPDATE) — walk forward
collecting overflow-page redo until the dummy/anchor record.
3. recdes->type == REC_BIGONE — the record is a fresh BIGONE.
La_get_overflow_recdes(..., RVOVF_NEWPAGE_INSERT) — collect the
freshly-inserted overflow chain.
4. RVHF_INSERT && recdes->type == REC_ASSIGN_ADDRESS — the heap
reserved a slot first, then the actual data update was deferred.
La_get_next_update_log() — chase forw_lsa within the same trid
to find the deferred update record.
5. (RVHF_UPDATE || RVHF_UPDATE_NOTIFY_VACUUM) && recdes->type ==
REC_RELOCATION — the record is the REC_RELOCATION pointer; the
actual REC_NEWHOME lives elsewhere.
La_get_relocation_recdes() — chase prev_tranlsa within the same
trid to find the REC_NEWHOME companion.

chase 함수들은 모두 세 헤더 LSA (forw_lsa, prev_tranlsa, back_lsa) 중 하나를 따라 forward 또는 backward 로 로그를 읽는 다. physiological 로그 레코드를 디코딩하고, daemon 의 인스턴스 별 LOG_ZIP 컨텍스트 (la_Info.undo_unzip_ptr, la_Info.redo_unzip_ptr) 로 압축을 푼다.

la_log_commit — 내구성 있는 bookmark

섹션 제목: “la_log_commit — 내구성 있는 bookmark”

매 apply 배치 이후 la_log_commit 은 slave 의 _db_ha_apply_info 시스템 테이블에 새로운 committed_lsa, committed_rep_lsa, final_lsa, 그리고 누적 카운터를 기록한다. 행은 master 의 db_name / copied_log_path 로 키된다. daemon 재기동 시 그 행 을 다시 읽어 la_Info.final_lsa 의 시드로 사용한다. apply cursor 의 내구성 있는 한 끝이다.

master → slave 의 commit 한 번, 처음부터 끝까지

섹션 제목: “master → slave 의 commit 한 번, 처음부터 끝까지”
sequenceDiagram
  participant TX as Master DML thread
  participant LOC as locator_*_force
  participant REPL as repl_log_insert
  participant TDES as tdes->repl_records
  participant FLUSH as log_append_repl_info_<br/>and_commit_log
  participant PRIOR as prior_lsa list
  participant LGAT as master active log
  participant XLW as xlogwr_get_log_pages
  participant CL as copylogdb (slave host)
  participant SLOG as slave-local log
  participant AL as applylogdb
  participant DISP as la_log_record_process
  participant AP as la_apply_<insert|update|delete>_log
  participant SS as slave cub_server
  participant HA as _db_ha_apply_info

  TX->>LOC: INSERT (1, "가")
  LOC->>LOC: heap_insert_logical → log_undoredo
  LOC->>REPL: repl_log_insert (RVREPL_DATA_INSERT)
  REPL->>TDES: append LOG_REPL_RECORD
  Note over TX,TDES: caller continues — no WAL emission yet
  TX->>FLUSH: COMMIT
  FLUSH->>FLUSH: prior_lsa_mutex.lock()
  loop each LOG_REPL_RECORD
    FLUSH->>PRIOR: append LOG_REPLICATION_DATA
  end
  FLUSH->>PRIOR: append LOG_COMMIT
  FLUSH->>FLUSH: prior_lsa_mutex.unlock()
  PRIOR->>LGAT: drain + flush (logpb_flush_all_append_pages)

  CL->>XLW: NET_SERVER_LOGWR_GET_LOG_PAGES (last_recv_pageid)
  XLW->>LGAT: read pages
  XLW-->>CL: up to 128 × LOG_PAGESIZE bytes
  CL->>SLOG: write pages (logwr_flush_all_append_pages)

  AL->>SLOG: la_get_page_buffer (la_Info.final_lsa)
  AL->>DISP: lrec = LOG_REPLICATION_DATA
  DISP->>DISP: la_set_repl_log → repl_lists[trid]
  AL->>DISP: lrec = LOG_COMMIT
  DISP->>DISP: la_add_node_into_la_commit_list
  DISP->>AP: la_apply_commit_list → la_apply_repl_log
  AP->>SS: db_otmpl_create / db_template_*
  SS-->>AP: success / retryable / fatal
  AP->>HA: la_log_commit (committed_rep_lsa)

이 파이프라인은 두 개의 interleaved ordering 을 동시에 들고 간 다. LSA 순서 는 master 측에서 attach 시점에 prior-LSA mutex 로 강제된다. 발행된 LOG_REPLICATION_DATALOG_COMMIT 은 엄격하게 단조다. apply 순서 는 slave 측에서 trid 별 buffer 와 commit queue 의 조합으로 강제된다. 이벤트는 등장하는 대로 buffer 되지만, LOG_COMMIT 에 도달했을 때 비로소, LOG_COMMIT 의 도착 순서대로 적용된다. 두 ordering 이 일치하는 이유는 slave 가 LSA 순으로 forward 로 걷고, 한 번에 한 commit 의 이벤트만 fan-out 하기 때문이다.

심볼 이름 에 닻을 내려라. 라인 번호가 아니다.

  • LOG_REPL_RECORD (replication.h) — 행 mutation 당 staging entry.
  • LOG_REPL_FLUSH enum (replication.h) — DONT_NEED_FLUSH = -1, COMMIT_NEED_FLUSH = 0, NEED_FLUSH = 1.
  • REPL_INFO_TYPE enum (replication.h) — SBR, RBR_START, RBR_NORMAL, RBR_END.
  • LOG_TDES::repl_records / num_repl_records / cur_repl_record / append_repl_recidx / fl_mark_repl_recidx / repl_insert_lsa / repl_update_lsa / must_flush (log_impl.h).
  • REPL_LOG_INFO_ALLOC_SIZE, REPL_LOG_IS_NOT_EXISTS, REPL_LOG_IS_FULL (replication.c).
  • repl_log_insert (replication.c) — tdes->repl_records[]LOG_REPL_RECORD 를 추가.
  • repl_log_insert_statement (replication.c) — DDL / 복제 대상 세션 statement 의 statement-based 발행.
  • repl_add_update_lsa (replication.c) — UPDATE 의 heap 로그 이후 repl_rec->lsa back-patch.
  • repl_log_info_alloc (replication.c) — 초기 alloc + 100씩 realloc.
  • repl_start_flush_mark / repl_end_flush_mark (replication.c) rollback 시에도 flush 되어야 하는 DDL emission 의 괄호.
  • repl_log_abort_after_lsa (replication.c) — savepoint LSA 이 후의 staging 레코드를 떨궈 냄.
  • locator_attribute_info_force / locator_insert_force / locator_update_force / locator_delete_force (locator_sr.c).
  • locator_add_or_remove_index (locator_sr.c) — INSERT 경로, btree_insert 호출.
  • locator_update_index (locator_sr.c) — UPDATE 경로, btree_update 호출.
  • heap_insert_logical / heap_update_logical / heap_delete_logical / heap_log_insert_physical / heap_log_update_physical (heap_file.c).
  • btree_update (btree.c) — 인덱스 측에서 repl_log_insert 를 호출.
  • log_append_repl_info_internal (log_manager.c) — staging 레 코드를 prior-list 노드로 변환.
  • log_append_repl_info (log_manager.c) — 락 없는 공개 진입.
  • log_append_repl_info_with_lock (log_manager.c) — 호출자가 prior mutex 를 이미 들고 있을 때의 변종.
  • log_append_repl_info_and_commit_log (log_manager.c) — repl 과 commit 의 atomic emission.
  • log_commit / log_commit_local (log_manager.c) — top-level commit driver.
  • LOG_REC_REPLICATION (log_record.hpp) — LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT 의 on-disk data header.

Master — 복사 프로토콜의 wire 측

섹션 제목: “Master — 복사 프로토콜의 wire 측”
  • xlogwr_get_log_pages (log_writer.c) — NET_SERVER_LOGWR_GET_LOG_PAGES 의 server 진입.
  • logwr_pack_log_pages (log_writer.c) — 연속 범위 packing.
  • xlog_send_log_pages_to_client (server-support 측) — wire 쓰기.
  • logpb_copy_page_from_file / logpb_read_page_from_file (log_page_buffer.c) — physical fetch.
  • logpb_fetch_from_archive / logpb_get_guess_archive_num (log_page_buffer.c) — archive lookup.
  • logpb_arv_page_info_table — 메모리 캐시. archive 생성 시 갱신.
  • logwr_initialize (log_writer.c) — Logwr_Gl init, active 로그 open.
  • logwr_copy_log_file (log_writer.c) — main loop: fetch → write → archive.
  • logwr_set_hdr_and_flush_info (log_writer.c) — 매 batch 후 헤더 reconciliation.
  • logwr_writev_append_pages / logwr_flush_all_append_pages (log_writer.c) — slave 로컬 active 로그 쓰기.
  • logwr_archive_active_log (log_writer.c) — active 가 가득 차면 archive 로 roll.
  • logwr_flush_header_page / logwr_flush_bgarv_header_page (log_writer.c) — header writeback.
  • logwr_to_physical_pageid (log_writer.c) — logical → physical page id.
  • logwr_check_page_checksum (log_writer.c) — 페이지별 무결성 검사.
  • LA_INFO 전역 (log_applier.c) — 프로세스별 상태.
  • LA_APPLY / LA_ITEM / LA_COMMIT / LA_HA_APPLY_INFO / LA_CACHE_PB / LA_CACHE_BUFFER / LA_REPL_FILTER / LA_OVF_PAGE_LIST / LA_RECDES_POOL (log_applier.c) — 내부 타입.
  • LA_RETRY_ON_ERROR 마스크 (log_applier.h) — retry 가능 apply 에러.
  • REPL_FILTER_TYPE enum (log_applier.h) — NONE / INCLUDE_TBL / EXCLUDE_TBL.
  • LA_MAX_REPL_ITEMS (1000), LA_MAX_REPL_ITEM_WITHOUT_RELEASE_PB (50), LA_STATUS_BUSY / LA_STATUS_IDLE (log_applier.c).

Slave — 로그 fetch 와 페이지 캐시

섹션 제목: “Slave — 로그 fetch 와 페이지 캐시”
  • la_log_fetch / la_log_fetch_from_archive (log_applier.c) active 또는 archive 로부터 로그 페이지 한 장을 읽는다.
  • la_get_page_buffer / la_release_page_buffer (log_applier.c) — refcount 가진 페이지 캐시 접근.
  • la_init_cache_pb / la_init_cache_log_buffer (log_applier.c) — 페이지 캐시 init.
  • la_cache_buffer_replace / la_invalidate_page_buffer / la_decache_page_buffers (log_applier.c) — eviction.
  • la_init_recdes_pool / la_assign_recdes_from_pool (log_applier.c) — 사전 할당된 RECDES pool.
  • la_apply_log_file (log_applier.c) — daemon main. cubrid hb start 가 호출하는 진입.
  • la_init (log_applier.c) — 전역 init, 캐시 할당, helper thread spawn.
  • la_apply_pre (log_applier.c) — pre-flight: 락, 헤더 fetch, 중복 검사.
  • la_change_state (log_applier.c) — slave 상태 변경 처리.
  • la_log_commit (log_applier.c) — _db_ha_apply_info 체크포인트.
  • la_force_shutdown (log_applier.h) — 외부 shutdown 훅.
  • la_log_record_process (log_applier.c) — lrec->type 에 대한 switch.
  • la_set_repl_log (log_applier.c) — LOG_REPLICATION_* 레코 드 buffer.
  • la_make_repl_item / la_add_repl_item (log_applier.c) — log 페이지로부터 LA_ITEM 을 빌드.
  • la_find_apply_list / la_add_apply_list (log_applier.c) trid bucket lookup.
  • la_init_repl_lists (log_applier.c) — bucket 배열 init / realloc.
  • la_add_node_into_la_commit_list / la_retrieve_eot_time (log_applier.c) — commit queue.
  • la_log_copy_fromlog (log_applier.c) — log-page 경계를 가 로지르는 byte 복사.
  • la_apply_commit_list (log_applier.c) — LA_COMMIT queue drain. 한 호출당 한 trid dispatch.
  • la_apply_repl_log (log_applier.c) — LA_APPLY::head 위 per-item dispatch.
  • la_apply_insert_log / la_apply_update_log / la_apply_delete_log / la_apply_statement_log (log_applier.c) — kind 별 네 applier.
  • la_repl_add_object (log_applier.c) — slave 서버를 호출하 는 공통 종단.
  • la_get_recdes (log_applier.c) — after-image 다섯-case 재구 성기.
  • la_get_log_data (log_applier.c) — heap 로그 한 개를 읽고 압축을 푼다.
  • la_get_overflow_recdes (log_applier.c) — BIGONE 사슬 walker.
  • la_get_relocation_recdes (log_applier.c) — REC_RELOCATION → REC_NEWHOME 추적.
  • la_get_next_update_log (log_applier.c) — REC_ASSIGN_ADDRESS deferred-update 추적.
  • la_get_undoredo_diff / la_get_zipped_data (log_applier.c) — diff 와 zlib unzip.
  • la_make_room_for_mvcc_insid / la_make_room_for_mvcc_delid_and_prev_ver (log_applier.c) slave 측 적용 행에 MVCC 헤더 주입.
  • la_disk_to_obj (log_applier.c) — RECDES → DB_OTMPL 변환.
  • la_need_filter_out / la_create_repl_filter / la_print_repl_filter_info (log_applier.c) — 테이블 단위 필터.
  • la_init_ha_apply_info (log_applier.c) — LA_HA_APPLY_INFO zero-fill.
  • la_get_ha_apply_info (log_applier.c) — _db_ha_apply_info read.
  • la_insert_ha_apply_info / la_update_ha_last_applied_info / la_update_ha_apply_info_start_time / la_update_ha_apply_info_log_record_time (log_applier.c) write back.
  • la_delete_ha_apply_info (log_applier.c) — full reset 시 cleanup.
  • la_get_last_ha_applied_info (log_applier.c) — 재기동 시 bookmark.
  • la_find_required_lsa (log_applier.c) — 최소 필요 LSA 계산.
  • la_remove_archive_logs (log_applier.c) — apply 후 slave 로컬 archive trim.
  • util_service.ccubrid hb start / cubrid heartbeat startcub_master 와 호스트별 copylogdb, applylogdb 를 함께 묶어 띄운다.
  • commdb.ccub_commdb 를 통한 운영자 측 HA 활성화.
  • connection/heartbeat.ccopylogdb / applylogdb 시작 시 호출되는 hb_register_to_master 가 있어 cub_master 가 그들 의 생존을 인지한다.

이 개정 시점의 위치 힌트 (2026-05-01)

섹션 제목: “이 개정 시점의 위치 힌트 (2026-05-01)”
심볼파일라인
LOG_REPLICATION_DATAlog_record.hpp116
LOG_REPLICATION_STATEMENTlog_record.hpp117
RVREPL_DATA_INSERT/UPDATE/DELETE/STATEMENTrecovery.h149-154
LOG_REPL_RECORD (struct log_repl)replication.h78
LOG_REPL_FLUSH enumreplication.h70
REPL_INFO_TYPE enumreplication.h43
LOG_TDES::repl_records grouplog_impl.h522-528
REPL_LOG_INFO_ALLOC_SIZEreplication.c49
repl_log_info_allocreplication.c165
repl_add_update_lsareplication.c229
repl_log_insertreplication.c293
repl_log_insert_statementreplication.c512
repl_start_flush_markreplication.c606
repl_end_flush_markreplication.c635
repl_log_abort_after_lsareplication.c673
log_append_repl_info_internallog_manager.c4555
log_append_repl_infolog_manager.c4623
log_append_repl_info_with_locklog_manager.c4629
log_append_repl_info_and_commit_loglog_manager.c4647
locator_insert_forcelocator_sr.c4938
locator_update_forcelocator_sr.c5396
locator_delete_forcelocator_sr.c6116
locator_attribute_info_forcelocator_sr.c7461
locator_add_or_remove_indexlocator_sr.c7695
locator_update_indexlocator_sr.c8260
xlogwr_get_log_pageslog_writer.c2571
logwr_initializelog_writer.c428
logwr_set_hdr_and_flush_infolog_writer.c639
logwr_writev_append_pageslog_writer.c838
logwr_flush_all_append_pageslog_writer.c1016
logwr_flush_header_pagelog_writer.c1207
logwr_archive_active_loglog_writer.c1275
logwr_write_log_pageslog_writer.c1512
logwr_copy_log_filelog_writer.c1659/1960
LA_RETRY_ON_ERROR (macro)log_applier.h34
REPL_FILTER_TYPE (enum)log_applier.h48
LA_CACHE_BUFFER/LA_CACHE_PBlog_applier.c177-204
LA_REPL_FILTERlog_applier.c206
LA_ITEMlog_applier.c236
LA_APPLYlog_applier.c254
LA_COMMITlog_applier.c266
LA_INFOlog_applier.c279
LA_HA_APPLY_INFOlog_applier.c393
la_init_ha_apply_infolog_applier.c606
la_get_page_bufferlog_applier.c1297
la_get_ha_apply_infolog_applier.c1514
la_init_recdes_poollog_applier.c2416
la_init_cache_pblog_applier.c2474
la_init_cache_log_bufferlog_applier.c2528
la_init_repl_listslog_applier.c2773
la_find_apply_listlog_applier.c2860
la_log_copy_fromloglog_applier.c2960
la_add_repl_itemlog_applier.c3050
la_make_repl_itemlog_applier.c3092
la_set_repl_loglog_applier.c3419
la_add_node_into_la_commit_listlog_applier.c3473
la_get_log_datalog_applier.c3949
la_get_overflow_recdeslog_applier.c4249
la_get_next_update_loglog_applier.c4393
la_get_relocation_recdeslog_applier.c4552
la_get_recdeslog_applier.c4604
la_repl_add_objectlog_applier.c4882
la_apply_delete_loglog_applier.c5000
la_apply_update_loglog_applier.c5110
la_apply_insert_loglog_applier.c5311
la_apply_statement_loglog_applier.c5496
la_apply_repl_loglog_applier.c5739
la_apply_commit_listlog_applier.c5920
la_log_record_processlog_applier.c6101
la_change_statelog_applier.c6397
la_log_commitlog_applier.c6531
la_initlog_applier.c6917
la_apply_log_filelog_applier.c8074

raw deck (HA replication.pdf / .pptx) 은 그 시점 이전의 브 랜치를 대상으로 작성된 것이다. 그 안에 있는 대부분의 내용이 /data/hgryoo/references/cubrid 아래의 11.5.x 소스에 대해서도 여전히 성립한다. 주요 주장 하나하나를 updated: 시점의 소스 를 검증하는 일이 어렵지 않았다는 점이다. drift 가 있는 부 분만 여기에 기록한다.

  • repl_log_insert 시그니처는 변하지 않았다. deck 은 tdes->repl_records[ ], tdes->num_repl_records, tdes->cur_repl_record, 기본 크기 100, tdes->must_flush = LOG_REPL_NEED_FLUSH 를 보여 준다. 다섯 이름 모두 log_impl.h:522-528replication.c:293 에 그대 로 살아 있다. 현대 코드가 여기에 더한 것은 tdes->fl_mark_repl_recidx (log_impl.h:525), RVREPL_DATA_UPDATE_START / RVREPL_DATA_UPDATE_END sub-kind (recovery.h:152-154), 그리 고 struct 자체의 tde_encrypted 필드 (replication.h:88) 정 도다. deck 의 LOG_REPL_RECORD 열거는 이 추가들이 도입되기 전이다.

  • deck 에 적힌 LOG_REPL_RECORD::repl_data 레이아웃 — | pkey size | class_name | pkey dbvalue | — 은 현재 소스와 일치한다. replication.c:411-419 에서 검증함. 함수가 앞쪽 에 OR_INT_SIZE 만큼의 packed_key_value_size 자리를 예약한 뒤, class_name 을 or-pack 하고, pkey DB_VALUE 를 or-pack 하고, 마지막에 그 첫 4바이트에 실제 packed-key 길이를 채워 넣는다.

  • commit 시점의 emission 은 log_append_repl_info_* 를 통한 다. deck 의 Log_commit() → Log_commit_local() → Log_append_repl_info_and_commit_log() → Log_append_repl_info() → Log_append_repl_info_with_lock() → Log_append_repl_info_internal() 시퀀스가 현재 소스의 log_commit → log_append_repl_info_and_commit_log → log_append_repl_info_with_lock → log_append_repl_info_internal 에 그대로 대응된다. deck 은 with-lock 변종과 락 없는 변종을 호출자 수준에서 갈라 놓고 있고, 소스도 같다.

  • repl_info + commit_log 의 atomic idiom. deck 은 이 atomicity 를 명시적으로 짚지 않지만, 현재 소스 (log_manager.c:4642-4645) 는 이를 직접 적어 둔다. “Atomic write of replication log and commit log is crucial for replication consistencies. When a commit log of others is written in the middle of one’s replication and commit log, a restart of replication will break consistencies of slaves/replicas.”

  • copylogdb 요청 형식이 일치한다. deck 은 요청을 (ctx_ptr->last_error, mode, First_pageid_torecv) tuple 이 NET_SERVER_LOGWR_GET_LOG_PAGES 로 보내지는 형태로 묘사한다. 현재 소스의 xlogwr_get_log_pageslog_writer.c:2571 에 서 (THREAD_ENTRY*, LOG_PAGEID first_pageid, LOGWR_MODE mode) 를 받고, slave 측의 logwr_copy_log_filelog_writer.c:1659/1960 에서 그 요청 루프를 돌린다. buffer 크기 상수 LOGWR_COPY_LOG_BUFFER_NPAGES = 128 도 그대로다.

  • la_log_record_process 안의 applylogdb 레코드별 dispatch 가 deck 과 일치한다. log_applier.c:6101 에서 검증함. switch 분기들 — LOG_END_OF_LOG, LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT, LOG_SYSOP_END / LOG_COMMIT, LOG_ABORT, LOG_DUMMY_CRASH_RECOVERY, LOG_END_CHKPT, LOG_DUMMY_HA_SERVER_STATE — 이 모두 존재한다. deck 은 REPL

    • COMMIT 경로에 집중하기에 분기를 더 적게 적는다.
  • la_apply_repl_log dispatch 표는 같은 모양이다. log_applier.c:5797-5826 에서 검증함. deck 은 RVREPL_DATA_INSERT → la_apply_insert_log, RVREPL_DATA_UPDATE → la_apply_update_log, RVREPL_DATA_DELETE → la_apply_delete_log 를 적는다. 현재 소스는 RVREPL_DATA_UPDATE_STARTRVREPL_DATA_UPDATE_ENDla_apply_update_log 로 dispatch 한다 (row-based-replication 경계의 START/END 괄호). deck 은 이 sub-kind 들을 언급하지 않 는다.

  • la_get_recdes 의 다섯 case 가 일치한다. log_applier.c:4604+ 에서 검증함. case 1-5 — normal, RVOVF_CHANGE_LINK, REC_BIGONE, RVHF_INSERT + REC_ASSIGN_ADDRESS, (RVHF_UPDATE | RVHF_UPDATE_NOTIFY_VACUUM)

    • REC_RELOCATION — 모두 존재한다.
  • LA_RETRY_ON_ERROR 의 폭이 넓어졌다. deck 은 에러 마스크 를 열거하지 않는다. 현재 소스 (log_applier.h:34-46) 는 ER_LK_UNILATERALLY_ABORTED, ER_LK_OBJECT_TIMEOUT 의 세 변종, ER_LK_PAGE_TIMEOUT, ER_PAGE_LATCH_* 의 두 변종, ER_LK_OBJECT_DL_TIMEOUT 의 세 변종, ER_TDE_CIPHER_IS_NOT_LOADED, ER_LK_DEADLOCK_CYCLE_DETECTED — 합쳐 12 개 코드를 나열한다. TDE 가 가장 최근 추가다.

  • REPL_FILTER_TYPE 와 테이블 단위 필터링. deck 은 필터를 보여 주지 않지만, 현재 소스의 log_applier.h:48-53 에 (NONE, INCLUDE_TBL, EXCLUDE_TBL) 가 있고, LA_REPL_FILTERla_apply_repl_log (log_applier.c:5797) 안의 la_need_filter_out 에 의해 소비된다. 필터는 master 의 emit 시점이 아니라 slave 의 apply 시점에 item 별로 평가된다는 점 이다. 즉, 필터 out 된 이벤트도 slave 에서 la_get_recdes walk 비용은 그대로 치른다. 마지막의 la_repl_add_object 만 건너뛴 다.

  • slave 측에서의 MVCC 주입. deck 은 이 부분을 다루지 않는다. la_make_room_for_mvcc_insidla_make_room_for_mvcc_delid_and_prev_ver (log_applier.c, 503-504 부근에 선언) 가 재구성된 RECDES 안에 자리를 비워 둔 다. slave 서버의 MVCC 계층이 apply 시점에 slave 자기의 MVCCID 를 찍을 수 있도록 하기 위함이다. master 의 MVCC ID 는 운반되지 않고, slave 가 새 MVCC ID 를 만들어 낸다.

  • la_log_record_processLOG_DUMMY_HA_SERVER_STATE 를 통 해 role-change 를 감지한다. log_applier.c:6292+ 에서 검증 함. ha_server_state->stateHA_SERVER_STATE_ACTIVEHA_SERVER_STATE_TO_BE_STANDBY 도 아닐 때, daemon 은 is_role_changed = true 로 설정하고 ER_INTERRUPTED 를 반환 해 호출자가 daemon 을 깨끗하게 종료할 수 있게 한다. deck 은 이 경로를 다루지 않지만, cub_master 의 heartbeat failover 가 유발하는 master-to-slave demote (cubrid-heartbeat.md 참조) 가 applier 까지 전파되는 메커니즘이 바로 이것이다.

  • is_long_trans overflow 처리는 변하지 않았다. log_applier.c:3437-3443 에서 검증함. apply->num_items >= LA_MAX_REPL_ITEMS (1000) 이면 daemon 이 head 를 제외한 모든 item 을 free 하고, is_long_trans = true 로 설정한 뒤, last_lsa 만 추적한다. 그런 trid 의 apply 는 start_lsalast_lsa 사이의 로그를 다시 walk 한다. deck 은 이를 다루지 않지만 상수와 분기가 현재 소스에 존재한다.

  • master 측 staging 항목의 TDE. deck 은 TDE 를 다루지 않는 다. 현재 LOG_REPL_RECORD::tde_encrypted (replication.h:88) 는 repl_log_insert 안에서 heap_get_class_tde_algorithm 의 결과로 설정된다. prior-list emission 측에서는, 그 플래그가 true 일 때 prior_set_tde_encrypted 가 호출된다 (log_manager.c:4585-4592). slave 에서는 daemon 의 la_load_tde (그리고 copy 측의 logwr_load_tde) 가 대칭적 복호화를 처리한다.

  1. 동기 복제 모드. 모델 절은 sync vs. async 를 거론하지만 현재 소스는 async 경로만 출하한다. LOGWR_MODE (logwr_copy_log_filexlogwr_get_log_pages 에 전달되는) 는 enum 으로 존재하 고, deck 은 요청 안에 mode 를 보여 준다. sync 값이 있는 가, 아니면 async 만인가? 조사 경로 — LOGWR_MODE enum 을 읽고 non-LOGWR_MODE_ASYNC writer 를 grep 한다.

  2. DDL 의 fl_mark_repl_recidx 의미. repl_start_flush_mark / repl_end_flush_mark 쌍이 DDL 레코드 구간을 괄호짓기 위해 fl_mark_repl_recidx 를 설정한다. 의도는 그 괄호 안의 레코 드들이 must_flush = LOG_REPL_NEED_FLUSH 를 가져, rollback 시에도 emit 되도록 하는 것이다 (CUBRID 에서 DDL 은 복제 목 적상 비-트랜잭션이다). 중첩 DDL 과 부분 rollback 를 이 의도가 강건함을 무엇이 보장하는가? 조사 경로 — must_flush writer 와 repl_log_abort_after_lsa 와의 상호 작용을 읽는다.

  3. repl_lists[] bucket 크기. LA_INFO::repl_cnt 는 bucket 수다. deck 은 그 sizing 을 명세하지 않고, la_init_repl_lists 는 on-demand realloc 패턴을 보여 준다. 초기 cap 은 얼마이며, 무엇이 regrow 를 trigger 하는가? 조사 경로 — la_init_repl_lists (log_applier.c:2773) 와 la_add_apply_list 를 읽는다.

  4. long-transaction 재 walk 의 성능. trid 가 is_long_trans 에 걸리면 la_get_next_repl_item_from_log 가 forward 로 로 그를 걸으며 같은 trid 의 다음 REPL 레코드를 찾는다. 비용은 item 당 O(records-since-start-lsa) 다. 거대한 트랜잭션에서 이것이 O(N²) 가 되는 것을 무엇이 막는가? 조사 경로 — la_get_next_repl_item_from_log 를 읽고 합성 백만 행 update 에서 측정한다.

  5. copylogdbapplylogdb 사이의 TDE 키 공유. log_applier.cUNSTABLE_TDE_FOR_REPLICATION_LOG 가드 (350-352 라인) 는 TDE data key 공유를 위한 unix-socket 프로 토콜이 copylogdb 와 apply 측 사이에 있음을 보여 준다. unstable 이라는 이름이 production 이 아님을 시사한다. TDE

    • 복제는 실제로 지원되는가, 아니면 내부 전용 feature flag 인가? 조사 경로 — release note 와 CMake feature flag 에서 해당 심볼을 검색한다.
  6. logpb_get_guess_archive_num 의 worst-case 동작. logpb_arv_page_info_table 캐시가 cold 인 경우 master 의 archive lookup 은 추정 + 스캔으로 fallback 한다. archive 가 수천 개 있는 노드에서, worst-case latency 는 얼마인가? 조사 경로 — 함수 본문을 읽고 미리 빌드된 archive 집합으로 측정 한다.

  7. _db_ha_apply_info 행의 recovery 시 의미. slave 서버가 apply 도중 죽으면, 행은 마지막으로 ack 된 committed_rep_lsa 를 들고 있고, 재기동 시 daemon 이 그 지점부터 다시 walk 한 다. 하지만 la_log_commit 가 그 행을 slave 서버 위에서 트 랜잭션 단위로 갱신하고, 그 서버 자기의 recovery 가 그 행을 roll back 할 수 있다. la_repl_add_objectla_log_commit 사이에서 slave 서버가 죽으면 무슨 일이 일어나는가? 레코드 가 재적용되는가 (PK 유일성을 idempotent 한가), 아니면 per-item ack 가 따로 있는가? 조사 경로 — la_log_commit (log_applier.c:6531) 을 읽고 slave 서버의 recovery 상호작 용을 추적한다.

  8. statement-based 복제와 비결정성. LOG_REPLICATION_STATEMENT 경로는 la_apply_statement_log 로 SQL 텍스트를 재생한 다. CUBRID 은 master emit 시점에 비결정 함수 (NOW(), RAND()) 를 막지 않는다. master 와 slave 사이에서 그런 statement 의 drift 를 무엇이 막는가? 조사 경로 — la_apply_statement_log (log_applier.c:5496) 를 읽고 사 전 바인딩된 파라미터 치환이 있는지 확인한다.

  9. 컨슈머 reconfigure 시의 필터 race. LA_REPL_FILTER 는 daemon 시작 시 la_create_repl_filter 가 로드하고 item 별 la_need_filter_out 가 참조한다. daemon 이 도는 동안 운영 자가 필터 리스트를 바꾸면, 새 필터는 언제 적용되는가? 조사 경로 — la_create_repl_filter 를 읽고 SIGHUP handler 를 확 인한다.

  10. replica 와 slave 의 구분. heartbeat 모듈은 HB_NSTATE_SLAVEHB_NSTATE_REPLICA 를 구분한다. 둘 다 applylogdb 를 돌리지만, replica 는 master 가 될 수 없다. apply 경로가 slave 와 replica 사이에서 다른가, 아니 면 두 역할이 순전히 cluster 측 FSM 에 관한 것일 뿐인가? 조사 경로 — cubrid-heartbeat.md 의 HB_NSTATE_REPLICA 를 apply 측 분기 (la_check_replica_info 등) 와 cross- reference 한다.

  • raw/code-analysis/cubrid/distributed/HA replication.pdf — deck 의 PDF 렌더.
  • raw/code-analysis/cubrid/distributed/HA replication.pptx — 원본 슬라이드 deck.
  • raw/code-analysis/cubrid/distributed/_converted/ha-replication.pdf.md PDF 의 pdftotext 추출.
  • raw/code-analysis/cubrid/distributed/_converted/ha-replication.pptx.md PPTX 의 markitdown 추출.
  • knowledge/code-analysis/cubrid/cubrid-log-manager.md — master 가 emit 하는 WAL 기계, 그리고 slave 의 applylogdb 가 walk 하는 그 기계.
  • knowledge/code-analysis/cubrid/cubrid-cdc.md — 현대 pull- style 대안. log_record.hpp 타입과 la_apply_* legacy 코드 경로를 공유한다.
  • knowledge/code-analysis/cubrid/cubrid-heartbeat.mdcub_master 의 cluster FSM. copylogdbapplylogdb 를 감독하며, apply daemon 이 LOG_DUMMY_HA_SERVER_STATE 를 통 해 감지하는 role 변경을 trigger 한다.
  • knowledge/code-analysis/cubrid/cubrid-recovery-manager.md — master 측 analysis/redo/undo pass 가 record decoding 과 log_reader 인프라를 applylogdb 와 공유한다.
  • Designing Data-Intensive Applications (Kleppmann), 5장 Replication — primary/standby, sync vs async, statement vs row vs WAL shipping.
  • Database Internals (Petrov), 13장 Replication — leader- follower, fail-over 와 일관성 보장, log shipping.
  • Database System Concepts (Silberschatz, Korth, Sudarshan), 19장 “Recovery System + 23장 Distributed Databases” — 복제 일관성 모델, 분산 commit, replica 위에서의 recovery.

CUBRID 소스 (/data/hgryoo/references/cubrid/)

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/transaction/replication.c / replication.h — master 측 staging primitive.
  • src/transaction/log_manager.c / log_manager.hlog_append_repl_info_* 패밀리.
  • src/transaction/log_record.hppLOG_REPLICATION_DATALOG_REPLICATION_STATEMENT record-type enum entry.
  • src/transaction/recovery.hRVREPL_DATA_* recovery index.
  • src/transaction/log_impl.hLOG_TDES 의 복제 필드.
  • src/transaction/log_writer.c / log_writer.h — master 측의 xlogwr_* server endpoint 와 slave 측의 logwr_* daemon (즉 copylogdb).
  • src/transaction/log_applier.c / log_applier.h — slave 측 la_* daemon (즉 applylogdb).
  • src/storage/heap_file.cheap_*_logical / heap_log_*_physical 의 emission site.
  • src/storage/btree.cbtree_update / btree_insert 의 인 덱스 측. 인덱스 op 에서 repl_log_insert 가 호출되는 곳.
  • src/transaction/locator_sr.clocator_*_forcelocator_attribute_info_force. heap 로그와 복제 staging 을 함께 driving 하는 상위 진입점.
  • src/executables/util_service.ccubrid hb start / cubrid heartbeat startcub_master, copylogdb, applylogdb 를 함께 묶는다.
  • src/connection/heartbeat.c — 두 daemon 모두가 시작 시 호출 하는 process 측 hb_register_to_master.