(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 고유 구조는 이 세 축 위에서의 한 좌표 선택을 구현하는 것이거나, 그 선택으로 만들어지는 상태 기계를 내구하게 만들기 위한 것임이 드러난다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서적 추상화의 한 단계 아래에는, 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_lsa 를
la_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 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 보조 논리 이벤트 로그 레코드 | LOG_REPLICATION_DATA = 39 와 LOG_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-patch | LOG_TDES::repl_insert_lsa, repl_update_lsa (log_impl.h:527-528); repl_add_update_lsa (replication.c:229) |
| staging 으로의 insert | repl_log_insert (replication.c:293) |
| statement 단위 emission | repl_log_insert_statement (replication.c:512) |
| 시스템 DDL 의 flush mark | repl_start_flush_mark (replication.c:606), repl_end_flush_mark (replication.c:635) |
| master 측 commit 시점 emission | log_append_repl_info_internal (log_manager.c:4555), log_append_repl_info_and_commit_log (log_manager.c:4647) |
| atomic repl + commit emission | log_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 의 레코드별 dispatch | la_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 queue | LA_INFO::commit_head / commit_tail of LA_COMMIT (log_applier.c:266-276, 304-305) |
| slave 의 dispatch 분기 | la_apply_repl_log 가 item->item_type 으로 la_apply_insert/update/delete/statement_log 분기 |
| slave 의 내구성 있는 bookmark | _db_ha_apply_info 의 LA_HA_APPLY_INFO 행 (log_applier.c:393) |
| slave 의 retry 가능 에러 마스크 | LA_RETRY_ON_ERROR (log_applier.h:34) |
| slave 의 테이블 단위 필터 | REPL_FILTER_TYPE 와 LA_REPL_FILTER (log_applier.h:48, log_applier.c:206) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”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) copylogdb 와 applylogdb
는 별개 프로세스다. 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_forcelocator_*_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:522int 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:78typedef 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 — staging primitive
섹션 제목: “repl_log_insert — staging primitive”// repl_log_insert — src/transaction/replication.c:293 (condensed)intrepl_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_UPDATE 는 REPL_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)intrepl_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 의 작업 순
서는 다음과 같다.
heap_insert_logical→heap_insert_physical가 행을 slotted page 에 적는다.heap_log_insert_physical가 prior list 에LOG_UNDOREDO_DATA를 추가한다.locator_add_or_remove_index→btree_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 를 가진다.
| idx | rcvindex | inst_oid | lsa | repl_data |
|---|---|---|---|---|
| 0 | RVREPL_DATA_INSERT | (oid_1) | LSA(heap_1) | t1 + 1 |
| 1 | RVREPL_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:4647static voidlog_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 voidlog_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_flush 가
LOG_REPL_DONT_NEED_FLUSH 로 바뀐다. 후속 abort 경로가 같은 레
코드를 다시 발행하지 못하게 막기 위함이다.
Master flush 경로
섹션 제목: “Master flush 경로”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 intla_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:3419static intla_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-264struct 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_lsa 와 last_lsa 만 기억하는 모드로 바뀐다. commit 시
점에는 start_lsa 부터 last_lsa 까지 로그를 forward 로 다시
걸어가며 각 REPL 레코드를 다시 읽어 온다. trade-off 는 long
transaction trid 당 한 번의 추가 로그 walk 이며, 그 대가로 메모
리가 한정된다.
la_apply_commit_list 와 la_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_tail 에 LA_COMMIT 노드를 enqueue 하고, head 가 빌
때까지 la_apply_commit_list 를 루프로 호출한다.
// la_apply_commit_list / la_apply_repl_log — src/transaction/log_applier.c:5920, 5739static intla_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 intla_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 다. 같은 모양을 공유한다.
- 클래스 해석.
LA_ITEM의class_name을 slave 의 카탈로 그를 해석해DB_OBJECT*를 얻는다. - 행 이미지 재구성. 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 를 운반하지 않는다는 점이다. - 행 적용.
la_repl_add_object가 재구성된 행을 들고 slave 서버의 정상 client API (db_create,db_otmpl_*) 로 들어간 다. slave 서버는 이 동작을 일반 DML 처럼 수행한다. 자기 락 을 잡고, 자기 MVCC ID 를 만들고, 자기 WAL 을 적는다. - 추적과 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_object 가 recdes = NULL 로 호출된다.
행 이미지 재구성 — case 분석
섹션 제목: “행 이미지 재구성 — case 분석”la_get_recdes 는 item->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_DATA 와 LOG_COMMIT 은
엄격하게 단조다. apply 순서 는 slave 측에서 trid 별 buffer
와 commit queue 의 조합으로 강제된다. 이벤트는 등장하는 대로
buffer 되지만, LOG_COMMIT 에 도달했을 때 비로소, LOG_COMMIT
의 도착 순서대로 적용된다. 두 ordering 이 일치하는 이유는 slave
가 LSA 순으로 forward 로 걷고, 한 번에 한 commit 의 이벤트만
fan-out 하기 때문이다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼 이름 에 닻을 내려라. 라인 번호가 아니다.
Master — staging 구조
섹션 제목: “Master — staging 구조”LOG_REPL_RECORD(replication.h) — 행 mutation 당 staging entry.LOG_REPL_FLUSHenum (replication.h) —DONT_NEED_FLUSH = -1,COMMIT_NEED_FLUSH = 0,NEED_FLUSH = 1.REPL_INFO_TYPEenum (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).
Master — staging primitive
섹션 제목: “Master — staging primitive”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->lsaback-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 레코드를 떨궈 냄.
Master — DML emission site
섹션 제목: “Master — DML emission site”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를 호출.
Master — commit 시점 emission
섹션 제목: “Master — commit 시점 emission”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 생성 시 갱신.
Slave — copylogdb / logwr_*
섹션 제목: “Slave — copylogdb / logwr_*”logwr_initialize(log_writer.c) —Logwr_Glinit, 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) — 페이지별 무결성 검사.
Slave — applylogdb infrastructure
섹션 제목: “Slave — applylogdb infrastructure”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_TYPEenum (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) — 사전 할당된RECDESpool.
Slave — daemon 진입과 main loop
섹션 제목: “Slave — daemon 진입과 main loop”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 훅.
Slave — 레코드 dispatch
섹션 제목: “Slave — 레코드 dispatch”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 복사.
Slave — apply 와 행 재구성
섹션 제목: “Slave — apply 와 행 재구성”la_apply_commit_list(log_applier.c) —LA_COMMITqueue 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) — 테이블 단위 필터.
Slave — 내구성 있는 부기
섹션 제목: “Slave — 내구성 있는 부기”la_init_ha_apply_info(log_applier.c) —LA_HA_APPLY_INFOzero-fill.la_get_ha_apply_info(log_applier.c) —_db_ha_apply_inforead.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.
유틸리티 수준의 wire-up
섹션 제목: “유틸리티 수준의 wire-up”util_service.c—cubrid hb start/cubrid heartbeat start가cub_master와 호스트별copylogdb,applylogdb를 함께 묶어 띄운다.commdb.c—cub_commdb를 통한 운영자 측 HA 활성화.connection/heartbeat.c—copylogdb/applylogdb시작 시 호출되는hb_register_to_master가 있어cub_master가 그들 의 생존을 인지한다.
이 개정 시점의 위치 힌트 (2026-05-01)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-05-01)”| 심볼 | 파일 | 라인 |
|---|---|---|
LOG_REPLICATION_DATA | log_record.hpp | 116 |
LOG_REPLICATION_STATEMENT | log_record.hpp | 117 |
RVREPL_DATA_INSERT/UPDATE/DELETE/STATEMENT | recovery.h | 149-154 |
LOG_REPL_RECORD (struct log_repl) | replication.h | 78 |
LOG_REPL_FLUSH enum | replication.h | 70 |
REPL_INFO_TYPE enum | replication.h | 43 |
LOG_TDES::repl_records group | log_impl.h | 522-528 |
REPL_LOG_INFO_ALLOC_SIZE | replication.c | 49 |
repl_log_info_alloc | replication.c | 165 |
repl_add_update_lsa | replication.c | 229 |
repl_log_insert | replication.c | 293 |
repl_log_insert_statement | replication.c | 512 |
repl_start_flush_mark | replication.c | 606 |
repl_end_flush_mark | replication.c | 635 |
repl_log_abort_after_lsa | replication.c | 673 |
log_append_repl_info_internal | log_manager.c | 4555 |
log_append_repl_info | log_manager.c | 4623 |
log_append_repl_info_with_lock | log_manager.c | 4629 |
log_append_repl_info_and_commit_log | log_manager.c | 4647 |
locator_insert_force | locator_sr.c | 4938 |
locator_update_force | locator_sr.c | 5396 |
locator_delete_force | locator_sr.c | 6116 |
locator_attribute_info_force | locator_sr.c | 7461 |
locator_add_or_remove_index | locator_sr.c | 7695 |
locator_update_index | locator_sr.c | 8260 |
xlogwr_get_log_pages | log_writer.c | 2571 |
logwr_initialize | log_writer.c | 428 |
logwr_set_hdr_and_flush_info | log_writer.c | 639 |
logwr_writev_append_pages | log_writer.c | 838 |
logwr_flush_all_append_pages | log_writer.c | 1016 |
logwr_flush_header_page | log_writer.c | 1207 |
logwr_archive_active_log | log_writer.c | 1275 |
logwr_write_log_pages | log_writer.c | 1512 |
logwr_copy_log_file | log_writer.c | 1659/1960 |
LA_RETRY_ON_ERROR (macro) | log_applier.h | 34 |
REPL_FILTER_TYPE (enum) | log_applier.h | 48 |
LA_CACHE_BUFFER/LA_CACHE_PB | log_applier.c | 177-204 |
LA_REPL_FILTER | log_applier.c | 206 |
LA_ITEM | log_applier.c | 236 |
LA_APPLY | log_applier.c | 254 |
LA_COMMIT | log_applier.c | 266 |
LA_INFO | log_applier.c | 279 |
LA_HA_APPLY_INFO | log_applier.c | 393 |
la_init_ha_apply_info | log_applier.c | 606 |
la_get_page_buffer | log_applier.c | 1297 |
la_get_ha_apply_info | log_applier.c | 1514 |
la_init_recdes_pool | log_applier.c | 2416 |
la_init_cache_pb | log_applier.c | 2474 |
la_init_cache_log_buffer | log_applier.c | 2528 |
la_init_repl_lists | log_applier.c | 2773 |
la_find_apply_list | log_applier.c | 2860 |
la_log_copy_fromlog | log_applier.c | 2960 |
la_add_repl_item | log_applier.c | 3050 |
la_make_repl_item | log_applier.c | 3092 |
la_set_repl_log | log_applier.c | 3419 |
la_add_node_into_la_commit_list | log_applier.c | 3473 |
la_get_log_data | log_applier.c | 3949 |
la_get_overflow_recdes | log_applier.c | 4249 |
la_get_next_update_log | log_applier.c | 4393 |
la_get_relocation_recdes | log_applier.c | 4552 |
la_get_recdes | log_applier.c | 4604 |
la_repl_add_object | log_applier.c | 4882 |
la_apply_delete_log | log_applier.c | 5000 |
la_apply_update_log | log_applier.c | 5110 |
la_apply_insert_log | log_applier.c | 5311 |
la_apply_statement_log | log_applier.c | 5496 |
la_apply_repl_log | log_applier.c | 5739 |
la_apply_commit_list | log_applier.c | 5920 |
la_log_record_process | log_applier.c | 6101 |
la_change_state | log_applier.c | 6397 |
la_log_commit | log_applier.c | 6531 |
la_init | log_applier.c | 6917 |
la_apply_log_file | log_applier.c | 8074 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”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-528와replication.c:293에 그대 로 살아 있다. 현대 코드가 여기에 더한 것은tdes->fl_mark_repl_recidx(log_impl.h:525),RVREPL_DATA_UPDATE_START/RVREPL_DATA_UPDATE_ENDsub-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 하고, pkeyDB_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_pages는log_writer.c:2571에 서(THREAD_ENTRY*, LOG_PAGEID first_pageid, LOGWR_MODE mode)를 받고, slave 측의logwr_copy_log_file가log_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_logdispatch 표는 같은 모양이다.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_START와RVREPL_DATA_UPDATE_END도la_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_FILTER가la_apply_repl_log(log_applier.c:5797) 안의la_need_filter_out에 의해 소비된다. 필터는 master 의 emit 시점이 아니라 slave 의 apply 시점에 item 별로 평가된다는 점 이다. 즉, 필터 out 된 이벤트도 slave 에서la_get_recdeswalk 비용은 그대로 치른다. 마지막의la_repl_add_object만 건너뛴 다. -
slave 측에서의 MVCC 주입. deck 은 이 부분을 다루지 않는다.
la_make_room_for_mvcc_insid와la_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_process가LOG_DUMMY_HA_SERVER_STATE를 통 해 role-change 를 감지한다.log_applier.c:6292+에서 검증 함.ha_server_state->state가HA_SERVER_STATE_ACTIVE도HA_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_transoverflow 처리는 변하지 않았다.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_lsa와last_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) 가 대칭적 복호화를 처리한다.
미해결 질문
섹션 제목: “미해결 질문”-
동기 복제 모드. 모델 절은 sync vs. async 를 거론하지만 현재 소스는 async 경로만 출하한다.
LOGWR_MODE(logwr_copy_log_file와xlogwr_get_log_pages에 전달되는) 는 enum 으로 존재하 고, deck 은 요청 안에mode를 보여 준다. sync 값이 있는 가, 아니면 async 만인가? 조사 경로 —LOGWR_MODEenum 을 읽고 non-LOGWR_MODE_ASYNCwriter 를 grep 한다. -
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_flushwriter 와repl_log_abort_after_lsa와의 상호 작용을 읽는다. -
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를 읽는다. -
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 에서 측정한다. -
copylogdb와applylogdb사이의 TDE 키 공유.log_applier.c의UNSTABLE_TDE_FOR_REPLICATION_LOG가드 (350-352 라인) 는 TDE data key 공유를 위한 unix-socket 프로 토콜이copylogdb와 apply 측 사이에 있음을 보여 준다. unstable 이라는 이름이 production 이 아님을 시사한다. TDE- 복제는 실제로 지원되는가, 아니면 내부 전용 feature flag 인가? 조사 경로 — release note 와 CMake feature flag 에서 해당 심볼을 검색한다.
-
logpb_get_guess_archive_num의 worst-case 동작.logpb_arv_page_info_table캐시가 cold 인 경우 master 의 archive lookup 은 추정 + 스캔으로 fallback 한다. archive 가 수천 개 있는 노드에서, worst-case latency 는 얼마인가? 조사 경로 — 함수 본문을 읽고 미리 빌드된 archive 집합으로 측정 한다. -
_db_ha_apply_info행의 recovery 시 의미. slave 서버가 apply 도중 죽으면, 행은 마지막으로 ack 된committed_rep_lsa를 들고 있고, 재기동 시 daemon 이 그 지점부터 다시 walk 한 다. 하지만la_log_commit가 그 행을 slave 서버 위에서 트 랜잭션 단위로 갱신하고, 그 서버 자기의 recovery 가 그 행을 roll back 할 수 있다.la_repl_add_object와la_log_commit사이에서 slave 서버가 죽으면 무슨 일이 일어나는가? 레코드 가 재적용되는가 (PK 유일성을 idempotent 한가), 아니면 per-item ack 가 따로 있는가? 조사 경로 —la_log_commit(log_applier.c:6531) 을 읽고 slave 서버의 recovery 상호작 용을 추적한다. -
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) 를 읽고 사 전 바인딩된 파라미터 치환이 있는지 확인한다. -
컨슈머 reconfigure 시의 필터 race.
LA_REPL_FILTER는 daemon 시작 시la_create_repl_filter가 로드하고 item 별la_need_filter_out가 참조한다. daemon 이 도는 동안 운영 자가 필터 리스트를 바꾸면, 새 필터는 언제 적용되는가? 조사 경로 —la_create_repl_filter를 읽고 SIGHUP handler 를 확 인한다. -
replica 와 slave 의 구분. heartbeat 모듈은
HB_NSTATE_SLAVE와HB_NSTATE_REPLICA를 구분한다. 둘 다applylogdb를 돌리지만, replica 는 master 가 될 수 없다. apply 경로가 slave 와 replica 사이에서 다른가, 아니 면 두 역할이 순전히 cluster 측 FSM 에 관한 것일 뿐인가? 조사 경로 — cubrid-heartbeat.md 의HB_NSTATE_REPLICA를 apply 측 분기 (la_check_replica_info등) 와 cross- reference 한다.
Raw 분석
섹션 제목: “Raw 분석”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.mdPDF 의 pdftotext 추출.raw/code-analysis/cubrid/distributed/_converted/ha-replication.pptx.mdPPTX 의 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.md—cub_master의 cluster FSM.copylogdb와applylogdb를 감독하며, 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.h—log_append_repl_info_*패밀리.src/transaction/log_record.hpp—LOG_REPLICATION_DATA와LOG_REPLICATION_STATEMENTrecord-type enum entry.src/transaction/recovery.h—RVREPL_DATA_*recovery index.src/transaction/log_impl.h—LOG_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.c—heap_*_logical/heap_log_*_physical의 emission site.src/storage/btree.c—btree_update/btree_insert의 인 덱스 측. 인덱스 op 에서repl_log_insert가 호출되는 곳.src/transaction/locator_sr.c—locator_*_force와locator_attribute_info_force. heap 로그와 복제 staging 을 함께 driving 하는 상위 진입점.src/executables/util_service.c—cubrid hb start/cubrid heartbeat start가cub_master,copylogdb,applylogdb를 함께 묶는다.src/connection/heartbeat.c— 두 daemon 모두가 시작 시 호출 하는 process 측hb_register_to_master.