CUBRID HA Replication — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-ha-replication.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 하나의 변경이 복제되는 전체 생명주기 — 마스터 로그 생성, copylogdb 전송, 레플리카의 applylogdb 적용 — 를 순서대로 따라갈 수 있다.
목차:
Chapter 1: 자료구조 지도
섹션 제목: “Chapter 1: 자료구조 지도”복제되는 변경 사항은 마스터의 DML 문에서 슬레이브의 행 재실행에 이르기까지 다섯 번 재포장된다. 이 챕터는 모든 구조체와 필드, 그리고 마스터 스테이징 엔트리와 슬레이브 적용 아이템 사이의 바이트 수준 대응 관계를 목록으로 정리한다. 구조의 이유 — DML 시점의 발행, 커밋 시점의 플러시, 슬레이브에서의 트랜잭션 단위 버퍼링 — 는 상위 수준 문서(cubrid-ha-replication.md)에서 논증한다. 여기서는 구조체 목록 자체만을 다룬다.
다섯 개의 홉은 각각 하나의 구조체가 담당한다: (1) DML 스레드 — LOG_TDES.repl_records[], 원소 타입 LOG_REPL_RECORD; (2) 온디스크 로그 — LOG_REC_REPLICATION + 패킹된 repl_data; (3) copylogdb — 로그 페이지 원본, 디코드 없음; (4) applylogdb 디코드 — LA_ITEM; (5) applylogdb 버퍼 — 트랜잭션별 LA_APPLY + LA_COMMIT 큐. 상태 전이: repl_log_insert (스테이지) → log_append_repl_info_internal (플러시) → 전송 → la_make_repl_item (디코드) → la_set_repl_log (버퍼). Figure 1-1은 다섯 홉에 걸친 필드 수준 연결을 나타낸다.
1.1 마스터 스테이징 — LOG_TDES 복제 그룹
섹션 제목: “1.1 마스터 스테이징 — LOG_TDES 복제 그룹”LOG_TDES (log_impl.h)는 realloc으로 증가하는 스테이징 레코드 배열, 이를 구동하는 두 개의 커서, 플러시 마크 인덱스, 그리고 두 개의 LSA 역추적 슬롯을 포함한다:
// log_tdes -- src/transaction/log_impl.hint num_repl_records; /* # of replication records */int cur_repl_record; /* # of replication records */int append_repl_recidx; /* index of append replication records */int fl_mark_repl_recidx; /* index of flush marked replication record at first */struct log_repl *repl_records; /* replication records */LOG_LSA repl_insert_lsa; /* insert or mvcc update target lsa */LOG_LSA repl_update_lsa; /* in-place update target lsa */// ... condensed ...int suppress_replication; /* suppress writing replication logs when flag is set */두 int 필드는 주석이 거의 동일하지만 역할이 다르다 — 주석이 아니라 실제 사용처를 기준으로 읽어야 한다. repl_log_insert는 repl_records[cur_repl_record]에 쓴 후 cur_repl_record++를 실행하고, 커밋 시점의 log_append_repl_info_internal 루프는 while (append_repl_recidx < cur_repl_record) 조건으로 반복한다. 따라서 cur_repl_record는 스테이징 쓰기 커서이고, append_repl_recidx는 플러시 읽기 커서다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
repl_records | LOG_REPL_RECORD의 힙 배열. repl_log_info_alloc으로 증가. | 트랜잭션의 스테이징 레코드를 보관하며 커밋 시 로그에 플러시된다. |
num_repl_records | 할당된 용량(슬롯 수). REPL_LOG_IS_FULL ≡ num_repl_records == cur_repl_record + 1. | 스테이징 확장 vs. 배열 재사용을 제어하고 realloc 트리거의 기준이 된다. |
cur_repl_record | 스테이징 쓰기 커서 / 스테이징된 레코드 수. 다음 추가는 repl_records[cur_repl_record]에 이뤄진 후 cur_repl_record++. | 모든 스테이징 루프가 0 .. cur_repl_record-1을 순회하는 상한선. |
append_repl_recidx | 플러시 읽기 커서. 첫 플러시 이전에는 -1; 첫 플러시 또는 커밋 시 0으로 설정되고 cur_repl_record까지 전진. | 점진적 플러시는 마지막 위치에서 재개하며, 커밋 시 0으로 되감아 전체를 재스캔한다. |
fl_mark_repl_recidx | 플러시 마킹 시작 인덱스 (= repl_start_flush_mark 호출 시의 cur_repl_record); 열린 마크가 없으면 -1. | repl_end_flush_mark가 undo 시 [fl_mark_repl_recidx, cur_repl_record) 범위를 되돌릴 수 있도록 한다 (Ch 4). |
repl_insert_lsa | 최신 INSERT/MVCC-update 힙 레코드의 대상 LSA. | 힙 LSA가 확정되면 repl 레코드의 lsa를 역추적 패치한다. |
repl_update_lsa | 최신 in-place UPDATE 힙 레코드의 대상 LSA. | 동일한 역추적 패치, in-place 경로 (repl_add_update_lsa, Ch 3). |
suppress_replication | 인접 카운터. 0이 아니면 repl_log_insert가 조기 반환. | 카탈로그/내부 작업이 복제를 건너뛸 수 있으며, 두 repl_*_lsa 슬롯도 함께 초기화한다. |
불변 조건 — 스테이징 vs. 플러시 커서. 스테이징 중에는
0 <= fl_mark_repl_recidx <= cur_repl_record < num_repl_records(마크가 열려 있지 않으면fl_mark_repl_recidx == -1)가 유지되며,repl_log_insert의 모든 추가 직전에 수행되는REPL_LOG_IS_FULLrealloc 검사가 이를 보장한다. 플러시 커서append_repl_recidx는 독립적으로 동작하며, 첫 플러시 이전에는-1을 유지하다가log_append_repl_info_internal에 의해cur_repl_record까지 전진하고 그 값을 절대 넘지 않는다(루프 조건이<). 만약 미래의 수정으로cur_repl_record가 realloc 없이num_repl_records에 도달한다면, 다음 추가는 배열 범위를 벗어난 슬롯에 쓰이고, 플러시 루프는 해당 슬롯의repl_data쓰레기 값을 슬레이브에 전송하게 된다.
1.2 스테이징 엔트리 — LOG_REPL_RECORD
섹션 제목: “1.2 스테이징 엔트리 — LOG_REPL_RECORD”각 repl_records 슬롯은 하나의 LOG_REPL_RECORD (struct log_repl)이며, 로그에 기록되기 전 변경 사항의 인메모리 형태다.
// log_repl -- src/transaction/replication.hstruct log_repl{ LOG_RECTYPE repl_type; /* LOG_REPLICATION_DATA or LOG_REPLICATION_SCHEMA */ LOG_RCVINDEX rcvindex; OID inst_oid; LOG_LSA lsa; char *repl_data; /* the content of the replication log record */ int length; LOG_REPL_FLUSH must_flush; bool tde_encrypted; /* if it contains user data of tde-class */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
repl_type | LOG_REPLICATION_DATA (행) 또는 LOG_REPLICATION_STATEMENT (DDL). | 플러시 시 로그 레코드 헤더와 적용 분기를 결정한다. |
rcvindex | 연산 인덱스: RVREPL_DATA_* / RVREPL_STATEMENT (1.6). | LOG_REC_REPLICATION.rcvindex로 복사되고, 이후 LA_ITEM.item_type이 된다 (1.7). |
inst_oid | 영향받은 인스턴스의 OID. | 힙 행과 연관되며, in-place 브래킷 병합에 사용된다. |
lsa | 레코드가 참조하는 LSA / 대상 힙 LSA. | rcvindex에 따라 repl_*_lsa 역추적 슬롯에서 채워진다. |
repl_data | 패킹된 페이로드 (길이 + 클래스 + pkey, 또는 DDL 필드). 레이아웃은 1.9 참조. | repl_log_insert에서 한 번 생성되어 그대로 전송되며, STATEMENT이면 NULL. |
length | repl_data의 바이트 길이. | malloc, 온디스크 length, 슬레이브 읽기의 기준. |
must_flush | LOG_REPL_FLUSH 값 (1.3). | 스테이징 시 COMMIT_NEED_FLUSH로 설정되며, 플러시 루프가 DONT_NEED_FLUSH로 리셋한다. |
tde_encrypted | 소스 클래스가 TDE 보호 대상. | 이전 노드를 TDE 암호화 경로로 라우팅한다 (Ch 11). |
교차 검증 — 오래된 주석.
repl_type필드 주석은LOG_REPLICATION_SCHEMA를 언급하지만, 그런LOG_RECTYPE값은 존재하지 않는다.log_type으로 실제 설정되는 값은LOG_REPLICATION_DATA/LOG_REPLICATION_STATEMENT이다 (1.5). 주석은 이름 변경 이전의 잔재이며, 위 표는 현재 살아있는 enum을 기준으로 작성했다.
1.3 LOG_REPL_FLUSH — 레코드별 플러시 정책
섹션 제목: “1.3 LOG_REPL_FLUSH — 레코드별 플러시 정책”// log_repl_flush -- src/transaction/replication.henum log_repl_flush{ LOG_REPL_DONT_NEED_FLUSH = -1, /* no flush */ LOG_REPL_COMMIT_NEED_FLUSH = 0, /* log must be flushed at commit */ LOG_REPL_NEED_FLUSH = 1 /* log must be flushed at commit and rollback */};| 값 | 역할 |
|---|---|
LOG_REPL_DONT_NEED_FLUSH (-1) | 플러시 건너뜀. repl_log_abort_after_lsa (세이브포인트 롤백 시 해당 세이브포인트 LSA 이후의 레코드를 퇴역시킴)와 log_append_repl_info_internal이 레코드를 추가한 후 설정하여, 이후의 점진적 패스가 같은 레코드를 재추가하지 않도록 한다. |
LOG_REPL_COMMIT_NEED_FLUSH (0) | 스테이징된 행의 기본값: 트랜잭션이 커밋될 때만 추가. |
LOG_REPL_NEED_FLUSH (1) | 커밋과 롤백 모두에서 추가. 슬레이브에 반드시 전달되어야 하는 레코드용으로 플러시 마크 내부에서 설정된다 (Ch 4). |
log_append_repl_info_internal의 플러시 게이트 조건은 정확히 다음과 같다:
(is_commit && must_flush != DONT_NEED_FLUSH) || must_flush == NEED_FLUSH.
1.4 REPL_INFO_TYPE, REPL_INFO, REPL_INFO_SBR — 문장 기반 복제 캐리어
섹션 제목: “1.4 REPL_INFO_TYPE, REPL_INFO, REPL_INFO_SBR — 문장 기반 복제 캐리어”문장 기반 복제(DDL, 스키마, 카탈로그 변경)는 힙 변경으로부터 LOG_REPL_RECORD를 구성하지 않는다. 대신 REPL_INFO_TYPE으로 태그된 클라이언트 제공 캐리어로 전달된다:
// REPL_INFO_TYPE -- src/transaction/replication.htypedef enum{ REPL_INFO_TYPE_SBR, REPL_INFO_TYPE_RBR_START, REPL_INFO_TYPE_RBR_NORMAL, REPL_INFO_TYPE_RBR_END } REPL_INFO_TYPE;| 값 | 역할 |
|---|---|
REPL_INFO_TYPE_SBR | REPL_INFO_SBR에 담긴 문장 페이로드. SQL을 재실행하여 복제. |
REPL_INFO_TYPE_RBR_START | in-place UPDATE 브래킷의 첫 번째 레코드. repl_log_insert에서 RVREPL_DATA_UPDATE → RVREPL_DATA_UPDATE_START로 매핑. |
REPL_INFO_TYPE_RBR_NORMAL | 독립적인 행 변경 (INSERT/DELETE/MVCC-update). rcvindex를 변경하지 않음. |
REPL_INFO_TYPE_RBR_END | 브래킷을 닫음. RVREPL_DATA_UPDATE → RVREPL_DATA_UPDATE_END로 매핑. |
REPL_INFO는 일반 래퍼이며, info 필드를 REPL_INFO_SBR 등의 페이로드로 다운캐스트한다:
// repl_info -- src/transaction/replication.hstruct repl_info { char *info; int repl_info_type; bool need_replication; };// repl_info_statement -- src/transaction/replication.hstruct repl_info_statement{ int statement_type; char *name; char *stmt_text; char *db_user; char *sys_prm_context; };REPL_INFO 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
info | 페이로드로의 다운캐스트 포인터 (예: REPL_INFO_SBR). | 하나의 래퍼가 클라이언트 경계를 넘어 어떤 종류의 정보든 전달한다. |
repl_info_type | info를 해석하는 방법을 선택하는 REPL_INFO_TYPE. | info를 해석하기 위한 판별자. |
need_replication | 스테이징 전에 검사되는 게이트. | 클라이언트가 특정 문장에 대한 복제를 억제할 수 있게 한다. |
REPL_INFO_SBR 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
statement_type | CUBRID_STMT_* 종류. | 먼저 패킹되어 LA_ITEM.item_type에 도달하며 필터링에 사용된다 (1.7). |
name | 영향받는 객체 이름. | DDL이 대상으로 하는 스키마 객체를 식별한다. |
stmt_text | 재실행할 DDL SQL. | 슬레이브가 문장을 그대로 재실행 (LA_ITEM.key로 전달). |
db_user | 소유 DB 사용자. | 슬레이브가 마스터의 신원으로 재실행한다. |
sys_prm_context | 세션 파라미터 컨텍스트. | 슬레이브가 마스터의 파라미터 환경에서 재실행한다. |
1.5 온디스크 헤더 — LOG_REC_REPLICATION
섹션 제목: “1.5 온디스크 헤더 — LOG_REC_REPLICATION”플러시 시 로그는 고정 헤더 다음에 repl_data 바이트를 기록한다:
// log_rec_replication -- src/transaction/log_record.hppstruct log_rec_replication{ LOG_LSA lsa; int length; int rcvindex;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
lsa | 대상 LSA (LOG_REPL_RECORD.lsa에서). DELETE/STATEMENT이면 NULL 설정. | 슬레이브가 데이터 변경 사항과 순서 및 연관관계를 맺는다 (LA_ITEM.target_lsa가 됨). |
length | 뒤따르는 repl_data의 바이트 길이. | 슬레이브가 정확히 이 바이트 수를 읽는다 (1.9). |
rcvindex | RVREPL_DATA_* / RVREPL_STATEMENT (1.6). | 슬레이브에게 디코드 방법과 적용 분기를 알린다. |
부모 LOG_RECTYPE은 LOG_REPLICATION_DATA = 39 또는 LOG_REPLICATION_STATEMENT = 40 (log_record.hpp)이며, repl_type에서 복사된다.
1.6 RVREPL_DATA_* — 복구 인덱스 디스패치 키
섹션 제목: “1.6 RVREPL_DATA_* — 복구 인덱스 디스패치 키”// LOG_RCVINDEX (replication subset) -- src/transaction/recovery.hRVREPL_DATA_INSERT = 98, RVREPL_DATA_UPDATE = 99, RVREPL_DATA_DELETE = 100,RVREPL_STATEMENT = 101, RVREPL_DATA_UPDATE_START = 102, RVREPL_DATA_UPDATE_END = 103,| 값 | 역할 |
|---|---|
RVREPL_DATA_INSERT | 행 INSERT. 슬레이브가 재구성된 이미지를 삽입한다. |
RVREPL_DATA_UPDATE | MVCC (out-of-place) UPDATE, 단일 레코드. 슬레이브가 pkey로 업데이트. |
RVREPL_DATA_DELETE | 행 DELETE. 슬레이브가 pkey로 삭제 (페이로드는 키만 포함). |
RVREPL_STATEMENT | DDL/스키마. 슬레이브가 SQL을 재실행. |
RVREPL_DATA_UPDATE_START | in-place UPDATE 브래킷 개시 (before-image). |
RVREPL_DATA_UPDATE_END | 브래킷 종료 (after-image). 원자적으로 적용됨 (Ch 9). |
이 여섯 값은 조인 키다: rcvindex는 LOG_REPL_RECORD에서 설정되고, LOG_REC_REPLICATION.rcvindex에 기록되며, 슬레이브에서 LA_ITEM.item_type으로 복사된다.
1.7 슬레이브 디코드 — LA_ITEM
섹션 제목: “1.7 슬레이브 디코드 — LA_ITEM”la_make_repl_item은 LOG_REC_REPLICATION 하나와 페이로드를 LA_ITEM으로 언패킹한다 — 마스터 패킹의 역연산이다.
// la_item -- src/transaction/log_applier.cstruct la_item{ LA_ITEM *next; LA_ITEM *prev; int log_type; /* LOG_REPLICATION_DATA or _STATEMENT */ int item_type; /* rcvindex (DATA) or CUBRID_STMT_* (STATEMENT) */ char *class_name; char *db_user; char *ha_sys_prm; int packed_key_value_length; char *packed_key_value; /* disk image of pkey value */ DB_VALUE key; /* it will be unpacked from packed_key_value on demand */ LOG_LSA lsa; /* the LSA of the replication log record */ LOG_LSA target_lsa; /* the LSA of the target log record */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
next / prev | LA_APPLY 목록 내의 링크 포인터 (1.8). | 커밋 레코드가 도착할 때까지 아이템이 누적된다. |
log_type | LOG_REPLICATION_DATA 또는 _STATEMENT. | 언패킹 분기와 적용 경로를 선택한다. |
item_type | DATA: 헤더 rcvindex에서 온 RVREPL_DATA_*. STATEMENT: 먼저 언패킹된 CUBRID_STMT_*. | 적용 / 문장 필터 판별자 (Ch 11). |
class_name | 대상 클래스 이름. | 테이블을 식별하며 클래스 수준 필터를 구동한다. |
db_user | DB 사용자 (STATEMENT 전용). | DDL을 원래 사용자로 재실행. |
ha_sys_prm | 세션 파라미터 (STATEMENT 전용). | 동일한 파라미터 환경에서 DDL을 재실행. |
packed_key_value_length | 패킹된 pkey 바이트 길이 (DATA 전용). | malloc/memcpy의 기준. |
packed_key_value | pkey DB_VALUE의 디스크 이미지 (DATA 전용). | 패킹 상태로 유지되며, key로 지연 언패킹. |
key | 언패킹된 pkey DB_VALUE (DATA). 또는 db_make_string을 통한 문장 텍스트 (STATEMENT). | 슬레이브 조회 키 또는 재실행할 SQL. |
lsa | 이 복제 레코드의 LSA. | 위치 추적 / 중복 제거. |
target_lsa | 데이터 레코드의 LSA (repl_log->lsa), la_new_repl_item에서 설정. | 아이템을 해당 데이터 변경과 연관짓는다. |
불변 조건 — 지연 키 언패킹. DATA 아이템의 경우
key는packed_key_value가 언패킹되기 전에는 유효하지 않다. 적용 코드는log_type == LOG_REPLICATION_DATA && DB_IS_NULL (&item->key)조건으로 첫 사용 시점에 언패킹을 수행한다. 그 전에packed_key_value를 해제하면 적용 코드가 댕글링 버퍼를 언패킹하게 된다. (STATEMENT 아이템은 관계가 역전된다:key는 디코드 시점에 채워지고packed_key_value는NULL로 유지된다.)
1.8 슬레이브 버퍼링 — LA_APPLY, LA_COMMIT, LA_INFO
섹션 제목: “1.8 슬레이브 버퍼링 — LA_APPLY, LA_COMMIT, LA_INFO”LA_ITEM은 트랜잭션별로 LA_APPLY에 버퍼링되며, 커밋은 LA_COMMIT 큐에 쌓이고, 전역 싱글톤 LA_INFO (la_Info)가 두 구조를 모두 소유한다.
// la_apply -- src/transaction/log_applier.cstruct la_apply{ int tranid; int num_items; bool is_long_trans; LOG_LSA start_lsa; LOG_LSA last_lsa; LA_ITEM *head; LA_ITEM *tail; };// la_commit -- src/transaction/log_applier.cstruct la_commit{ LA_COMMIT *next; LA_COMMIT *prev; int type; int tranid; LOG_LSA log_lsa; time_t log_record_time; };LA_APPLY 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
tranid | 이 목록의 마스터 트랜잭션 ID. | 레코드들은 tranid를 키로 트랜잭션별 목록에 분배된다 (la_find_apply_list). |
num_items | 버퍼링된 LA_ITEM 수. | LA_MAX_REPL_ITEMS에서 long-transaction 전환을 트리거한다. |
is_long_trans | 목록이 상한선을 초과했음 (불변 조건 참조). | 버퍼링된 아이템 재실행 대신 로그 재페치 방식으로 전환한다. |
start_lsa | 트랜잭션의 첫 번째 레코드 LSA. | long transaction의 재스캔 시작점. |
last_lsa | 마지막으로 확인된 레코드의 LSA. | is_long_trans 상태에서도 계속 업데이트된다. |
head / tail | LA_ITEM 목록의 양 끝. | tail에 추가하고 head부터 재실행. |
LA_COMMIT 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
next / prev | 전역 커밋 큐를 연결. | 커밋 레코드는 트랜잭션에 걸쳐 FIFO 순으로 적용된다. |
type | 종료 종류 (LOG_COMMIT). | 커밋을 다른 종료 방식과 구분한다. |
tranid | 플러시할 LA_APPLY를 선택. | 커밋을 해당 버퍼와 연결한다. |
log_lsa | 커밋 레코드의 LSA. | 내구성 있는 적용 북마크가 이 값으로 전진한다 (Ch 10). |
log_record_time | 마스터 커밋 타임스탬프 (eot_time). | 복제 지연 보고에 사용. |
불변 조건 — 제한된 버퍼링 (단방향 래치). 목록은 최대
LA_MAX_REPL_ITEMS(1000)개의 아이템을 보관한다. 이 한도에 도달하면la_set_repl_log가la_free_all_repl_items_except_head를 호출하고is_long_trans = true로 설정한 뒤, 이후로는last_lsa만 갱신할 뿐 새 아이템을 추가하지 않는다. 이 래치는 트랜잭션이 완전히 적용된 시점에만 해제된다 (is_long_trans = false). 이 메커니즘이 없다면 하나의 거대한 트랜잭션이 디코딩된 아이템을 보관하면서 슬레이브 메모리를 고갈시킬 수 있다.
LA_INFO는 싱글톤 (la_Info)이다. 복제와 관련된 필드들:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
repl_lists | LA_APPLY * 배열 (용량 repl_cnt, 커서 cur_repl). | tranid를 키로 검색되는 트랜잭션별 버퍼. la_init_repl_lists로 확장. |
commit_head | LA_COMMIT 큐의 헤드. | 커밋이 헤드에서부터 FIFO 순으로 디큐된다. |
commit_tail | LA_COMMIT 큐의 테일. | 새로운 커밋이 테일에 인큐된다. |
나머지 LA_INFO 필드는 사용처에서 각각 분석한다: LSA 북마크 그룹 (final_lsa, committed_lsa, required_lsa)은 Ch 10에서, 페치/캐시 (act_log, cache_pb, log_data)는 Ch 7에서, 필터/역할 (repl_filter, is_role_changed)은 Ch 11에서 다룬다.
1.9 필드 수준 대응: repl_data ↔ LA_ITEM
섹션 제목: “1.9 필드 수준 대응: repl_data ↔ LA_ITEM”가장 오류가 발생하기 쉬운 홉은 페이로드 레이아웃이다. 마스터는 repl_log_insert에서 repl_data를 패킹하고, 슬레이브는 la_make_repl_item에서 이를 언패킹하는데, 두 쪽이 바이트 단위로 일치해야 한다. LOG_REPLICATION_DATA 레코드의 경우:
// repl_log_insert (pack) -- src/transaction/replication.cptr_to_packed_key_value_size = ptr; ptr += OR_INT_SIZE; /* reserve len */ptr = or_pack_string_with_length (ptr, class_name, strlen);ptr = or_pack_mem_value (ptr, key_dbvalue, &packed_key_len);or_pack_int (ptr_to_packed_key_value_size, packed_key_len); /* back-fill len */
// la_make_repl_item (unpack) -- src/transaction/log_applier.cptr = or_unpack_int (area, &item->packed_key_value_length);ptr = or_unpack_string (ptr, &item->class_name);item->packed_key_value = (char *) malloc (item->packed_key_value_length);ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); /* 8 bytes alignment. see or_pack_mem_value */memcpy (item->packed_key_value, ptr, item->packed_key_value_length);item->item_type = repl_log->rcvindex;repl_data 영역 (마스터) | LA_ITEM 필드 (슬레이브) | 비고 |
|---|---|---|
선행 OR_INT_SIZE int = packed_key_len | packed_key_value_length | 먼저 자리를 예약하고, 값 패킹 후 역추적 채움. |
or_pack_string_with_length(class_name) | class_name (or_unpack_string으로 언팩) | 길이 접두사가 붙은 문자열. |
or_pack_mem_value(key_dbvalue) (8바이트 정렬) | packed_key_value (PTR_ALIGN + memcpy), 이후 → key | PTR_ALIGN의 MAX_ALIGNMENT가 마스터 or_pack_mem_value의 패딩과 반드시 일치해야 하며, 그렇지 않으면 pkey가 잘못 디코드된다. |
(헤더) rcvindex | item_type | repl_log->rcvindex에서 복사됨. 적용 판별자. |
불변 조건 — pack/unpack 대칭. (int 길이, 클래스 문자열, 8바이트 정렬 pkey 값) 순서는 내장 스키마 없이 두 파일에 수동으로 동일하게 코딩되어 있다. 마스터는
OR_INT_SIZE + or_packed_string_length + OR_VALUE_ALIGNED_SIZE로 버퍼 크기를 계산하고, 슬레이브는PTR_ALIGN(ptr, MAX_ALIGNMENT)으로 pkey 오프셋을 재유도한다.repl_log_insert의 패킹 순서나 정렬 방식이 변경되고 이것이la_make_repl_item에 반영되지 않으면, 복제되는 모든 행이 조용히 손상된다.
LOG_REPLICATION_STATEMENT 레코드는 대신 다음 순서로 패킹한다: item_type (CUBRID_STMT_* int), class_name, 문장 텍스트 (or_unpack_string → db_make_string으로 key에 저장), db_user, ha_sys_prm — REPL_INFO_SBR의 다섯 필드를 언패킹한 이미지들이다 (1.4).
1.10 챕터 요약 — 핵심 정리
섹션 제목: “1.10 챕터 요약 — 핵심 정리”- 두 호스트에 걸쳐 변경 사항을 보관하는 다섯 개의 구조체:
LOG_TDES.repl_records[]→LOG_REC_REPLICATION+ 패킹된repl_data→ copylogdb 원본 페이지 →LA_ITEM→LA_APPLY/LA_COMMIT. - 마스터 배열은 두 개의 독립된 커서를 사용한다:
cur_repl_record는 스테이징 쓰기 커서(모든 스테이징 루프는0 .. cur_repl_record-1을 순회)이고,append_repl_recidx는 플러시 읽기 커서(첫 플러시 이전-1,cur_repl_record를 향해 전진).repl_*_lsa슬롯은 힙 LSA가 확정된 후 레코드의lsa를 역추적 패치한다. must_flush(LOG_REPL_FLUSH)가 생존을 결정한다: 행에 대해서는 커밋 전용 (COMMIT_NEED_FLUSH), 플러시 마크 내부에서는 커밋과 롤백 모두 (NEED_FLUSH), 세이브포인트 롤백 후 또는 추가 완료 후에는 퇴역 (DONT_NEED_FLUSH).rcvindex(RVREPL_DATA_*/RVREPL_STATEMENT)는 조인 키다:LOG_REPL_RECORD에서 설정되고,LOG_REC_REPLICATION에 기록되며,LA_ITEM.item_type으로 복사되어 모든 적용 분기에서 스위치된다.- 슬레이브는 지연 디코드한다 (
LA_ITEM.key는 적용 시점에만 언패킹).is_long_trans는 단방향 래치다:LA_MAX_REPL_ITEMS(1000)를 초과하면 목록이 헤드만 남기고 해제되며, 해당 트랜잭션은 로그에서start_lsa..last_lsa를 재스캔하여 재실행한다. repl_data레이아웃 (int 길이, 클래스 문자열, 8바이트 정렬 pkey) 은repl_log_insert/la_make_repl_item두 파일에 대칭적으로 수동 코딩되어 있다. 자기 서술 스키마가 없으므로, 패킹 순서나 정렬 방식이 한쪽에만 변경되면 복제되는 행이 조용히 손상된다.
Chapter 2: 마스터 측 초기화와 스테이징 배열
섹션 제목: “Chapter 2: 마스터 측 초기화와 스테이징 배열”어떤 DML이든 행 이미지를 레플리카에 전달하기 전에, 마스터는 트랜잭션별 복제 레코드 스트림을 어딘가에 스테이징해야 한다 — 모든 트랜잭션 디스크립터의 tdes->repl_records[] 배열이 바로 그 공간이다. 이 챕터는 그 메모리의 생애주기를 추적한다: 첫 번째 쓰기 시점에 지연 생성(lazy allocation)되고, 100-슬롯 단위로 증가하며, 트랜잭션 경계마다 초기화된다. 또한 suppress_replication이 이 과정을 어떻게 단락(short-circuit)시키는지, 그리고 마스터가 커밋 전까지 WAL에 복제 관련 내용을 전혀 기록하지 않는다는 불변 조건(Chapter 5)을 다룬다. 스테이징 배열 개요와 마스터/레플리카 역할은 상위 개요 문서(cubrid-ha-replication.md, “마스터 측 로그 생성” 절)에서 이미 다뤘다고 가정하며, 여기서는 배열이 왜 사용되는지가 아니라 어떻게 관리되는지를 추적한다.
2.1 스테이징 필드와 그 위치
섹션 제목: “2.1 스테이징 필드와 그 위치”스테이징 배열은 LOG_TDES(log_impl.h) 위의 네 개 정수 필드와 포인터 하나로 기술된다. 이들은 Chapter 1의 자료구조 맵에서 소개됐다. 슬롯 페이로드는 LOG_REPL_RECORD(struct log_repl, replication.h)이며, 여기서는 할당과 초기화를 제어하는 디스크립터 측 커서 의미론(cursor semantics)에 집중한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
repl_records | 힙 LOG_REPL_RECORD 배열을 가리키는 포인터 (또는 NULL) | 스테이징 버퍼; NULL이면 해당 디스크립터에서 아직 쓰기가 발생하지 않았거나, 막 초기화/해제된 상태임 |
num_repl_records | 할당된 용량 (슬롯 수) | “크기”와 “채워진 정도”를 분리; 증가 여부 결정에 사용 |
cur_repl_record | 사용 중인 슬롯 수 — 동시에 쓰기 커서 역할 | repl_log_insert가 repl_records[cur_repl_record]에 쓴 후 후위 증가 |
append_repl_recidx | 커밋 시 WAL 방출 패스의 커서 | 방출을 배열 중간에서 재개할 수 있게 함; -1 = 이 트랜잭션에서 아직 방출 미실행 |
fl_mark_repl_recidx | 구문 flush 마킹이 시작된 인덱스, 또는 -1 | must_flush 업그레이드 로직(Chapter 4)을 구동; -1 = flush 마크 없음 |
이 중 세 개는 인덱스 커서다 — cur_repl_record(채우기), append_repl_recidx(방출), fl_mark_repl_recidx(flush 마크); repl_records는 버퍼 포인터이고 num_repl_records는 용량이다. 배열은 cur_repl_record를 기준으로 나뉜다: 슬롯 [0 .. cur-1]은 사용 중, [cur .. num-1]은 여유 공간이다. num_repl_records까지의 여유분이 다음 삽입이 소비할 공간이며, 이 여유가 없어지면 배열이 “가득 찬” 상태이므로 증가가 필요하다.
불변 조건 (fill 커서는 용량을 절대 초과하지 않는다). 항상
0 <= cur_repl_record <= num_repl_records가 성립하며, 이는 구조적으로 강제된다:repl_log_insert는 배열이 가득 찼을 때 쓰기 전에 반드시repl_log_info_alloc을 먼저 호출하므로, 후위 증가 전에repl_records[cur_repl_record]에 슬롯이 항상 존재한다. 이 조건을 위반하면&tdes->repl_records[tdes->cur_repl_record]쓰기가 할당 범위를 벗어나 힙을 오염시킨다. “가득 찬” 판정(Section 2.3)이 한 슬롯 일찍 발생하는 것은 정확히 이 안전성을 보장하기 위해서다.
2.2 생성과 초기화: 트랜잭션 경계를 가로지르는 스테이징 필드
섹션 제목: “2.2 생성과 초기화: 트랜잭션 경계를 가로지르는 스테이징 필드”세 개의 함수에서 세 가지 생애주기 이벤트가 발생한다. 핵심 구분은 포인터를 null로 만드는 것과 커서만 되감는 것의 차이 — 이것이 재사용 디스크립터에서 버퍼가 트랜잭션을 가로질러 살아남는 이유다.
(a) 디스크립터 생성 — logtb_initialize_tdes (log_tran_table.c). LOG_TDES 슬롯 설정 시 딱 한 번 실행된다 — repl_records를 null로 만들고 동시에 용량을 0으로 만드는 유일한 위치:
// logtb_initialize_tdes -- src/transaction/log_tran_table.ctdes->num_repl_records = 0;tdes->cur_repl_record = 0;tdes->append_repl_recidx = -1;tdes->fl_mark_repl_recidx = -1;tdes->repl_records = NULL;LSA_SET_NULL (&tdes->repl_insert_lsa);LSA_SET_NULL (&tdes->repl_update_lsa);// ... first_save_entry / suppress_replication = 0 ...(b) 트랜잭션 종료/재사용 — logtb_clear_tdes (log_tran_table.c). 커밋 또는 롤백 시마다 디스크립터를 재활용하기 위해 실행된다. 각 레코드의 페이로드를 해제하지만, 배열 할당 자체는 유지해 다음 트랜잭션이 재사용할 수 있게 한다 — repl_records나 num_repl_records를 건드리지 않는다:
// logtb_clear_tdes -- src/transaction/log_tran_table.cfor (i = 0; i < tdes->cur_repl_record; i++) { if (tdes->repl_records[i].repl_data) { free_and_init (tdes->repl_records[i].repl_data); } }// ... bind_history cleanup condensed ...tdes->cur_repl_record = 0;tdes->append_repl_recidx = -1;tdes->fl_mark_repl_recidx = -1;LSA_SET_NULL (&tdes->repl_insert_lsa);LSA_SET_NULL (&tdes->repl_update_lsa);// ... later: tdes->suppress_replication = 0 ...포인터와 용량이 유지되므로, 배열을 300 슬롯까지 키운 트랜잭션은 그 상태를 다음 트랜잭션에 물려준다: REPL_LOG_IS_NOT_EXISTS(Section 2.3)는 num_repl_records != 0을 읽어 지연 초기 할당을 건너뛴다. 해제된 슬롯의 repl_data 포인터는 repl_log_info_alloc(Section 2.4)이 새로 증가된 슬롯에 대해서만 null로 초기화한다; cur_repl_record 이하의 슬롯에는 이전 트랜잭션이 채운 dangling repl_data 포인터가 남아 있지만, 여기서 cur_repl_record가 0으로 되감겼으므로 repl_log_insert가 해당 필드를 덮어쓰기 전에는 어떤 읽기도 이를 참조하지 않는다.
(c) 디스크립터 해제 — logtb_free_tran_index (log_tran_table.c). 트랜잭션 인덱스가 반환될 때 호출된다 — 배열 메모리가 힙으로 돌아가는 유일한 위치. logtb_clear_tdes를 먼저 호출해 페이로드를 해제한 다음 배열 껍데기를 해제한다는 점에 주목하라:
// logtb_free_tran_index -- src/transaction/log_tran_table.clogtb_clear_tdes (thread_p, tdes);if (tdes->repl_records) { free_and_init (tdes->repl_records); }tdes->num_repl_records = 0;불변 조건 (용량과 포인터는 함께 움직인다).
repl_records == NULL인 것과num_repl_records == 0인 것은 동치다.logtb_initialize_tdes,logtb_free_tran_index, 그리고repl_log_info_alloc(성공 시)은 항상 둘을 일관되게 설정한다;logtb_clear_tdes는 둘 다 건드리지 않는다. 포인터만 null로 만들고 용량을 0으로 만들지 않거나, 반대로 하면REPL_LOG_IS_NOT_EXISTS가 버퍼를 잘못 분류해 메모리 누수나NULL역참조를 유발한다.
네 번째 이벤트 — append_repl_recidx가 0으로 초기화되는 것 — 는 여기가 아니라 커밋 시 방출 단계인 log_append_repl_info_internal(log_manager.c)에서 발생한다. if (tdes->append_repl_recidx == -1 || is_commit) 조건이 방출 커서를 되감는다(Chapter 5). 생애주기를 완성하기 위해 언급만 한다(Figure 2-1).
stateDiagram-v2 [*] --> 미초기화: logtb_initialize_tdes 미초기화 --> 할당됨: 첫 repl_log_insert -> malloc 할당됨 --> 할당됨: 배열 가득 참 -> realloc 증가 할당됨 --> 방출됨: 커밋 -> log_append_repl_info_internal 방출됨 --> 재활용됨: logtb_clear_tdes 커서 되감기 버퍼 유지 재활용됨 --> 할당됨: 다음 트랜잭션이 warm 버퍼 재사용 재활용됨 --> 해제됨: logtb_free_tran_index 버퍼 해제 미초기화 --> 해제됨: logtb_free_tran_index 해제됨 --> [*]
Figure 2-1. 디스크립터 재사용을 가로지르는 스테이징 배열 생애주기. 재활용됨 -> 할당됨 엣지가 대부분의 트랜잭션이 malloc을 건너뛰는 이유다.
2.3 두 매크로 가드: NOT_EXISTS와 FULL
섹션 제목: “2.3 두 매크로 가드: NOT_EXISTS와 FULL”두 매크로가 모든 삽입 시점에 배열 상태를 분류한다. tran_index로 전역 trantable에서 디스크립터를 읽는다:
// REPL_LOG_IS_NOT_EXISTS / REPL_LOG_IS_FULL -- src/transaction/replication.c#define REPL_LOG_IS_NOT_EXISTS(tran_index) \ (log_Gl.trantable.all_tdes[(tran_index)]->num_repl_records == 0)#define REPL_LOG_IS_FULL(tran_index) \ (log_Gl.trantable.all_tdes[(tran_index)]->num_repl_records \ == log_Gl.trantable.all_tdes[(tran_index)]->cur_repl_record+1)
static const int REPL_LOG_INFO_ALLOC_SIZE = 100;REPL_LOG_IS_NOT_EXISTS는 용량이 0일 때 참이다 — 이 디스크립터에 버퍼가 한 번도 할당되지 않았거나(신규) 방금 해제된 상태 — 그리고 지연 초기 malloc을 트리거한다. REPL_LOG_IS_FULL은 fill 커서가 용량보다 하나 부족할 때(num_repl_records == cur_repl_record+1) 참이다; +1은 Section 2.1의 “한 슬롯 일찍” 판정으로, 곧 발생할 삽입을 위한 여유 슬롯을 보장한다. 두 매크로는 상호 배타적이다 — 용량이 0인 배열은 “가득 찰” 수 없다 — 따라서 repl_log_insert는 if / else if로 순서대로 검사한다.
2.4 repl_log_info_alloc — 모든 분기
섹션 제목: “2.4 repl_log_info_alloc — 모든 분기”repl_log_info_alloc은 repl_records와 num_repl_records를 함께 다루는 유일한 함수다. need_realloc 파라미터가 초기 malloc인지 realloc을 통한 증가인지를 선택한다.
// repl_log_info_alloc -- src/transaction/replication.cstatic intrepl_log_info_alloc (LOG_TDES * tdes, int arr_size, bool need_realloc){ int i = 0, k; int error = NO_ERROR;
if (need_realloc == false) { i = arr_size * DB_SIZEOF (LOG_REPL_RECORD); tdes->repl_records = (LOG_REPL_RECORD *) malloc (i); if (tdes->repl_records == NULL) { error = ER_REPL_ERROR; er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_REPL_ERROR, 1, "can't allocate memory"); return error; } tdes->num_repl_records = arr_size; k = 0; } else { i = tdes->num_repl_records + arr_size; tdes->repl_records = (LOG_REPL_RECORD *) realloc (tdes->repl_records, i * DB_SIZEOF (LOG_REPL_RECORD)); // ... same NULL check / ER_REPL_ERROR / er_set / return as the malloc path ... k = tdes->num_repl_records; tdes->num_repl_records = i; }
for (i = k; i < tdes->num_repl_records; i++) { tdes->repl_records[i].repl_data = NULL; }
return error;}네 가지 결과가 모든 분기를 커버한다. A (need_realloc == false): malloc; 성공하면 용량 = arr_size, k = 0. A1 (malloc 실패): ER_REPL_ERROR를 설정하고 반환 — num_repl_records는 0으로, 포인터는 NULL로 남으므로 NOT_EXISTS가 여전히 성립한다. B (need_realloc == true): 용량을 old + arr_size로 증가, k = 이전 용량. B1 (realloc 실패): ER_REPL_ERROR를 설정하고 반환; 이전 블록이 누수된다(아래 참조). 성공 경로 둘 다 꼬리 루프 for (i = k; ...) repl_records[i].repl_data = NULL;을 실행한다. k 변수가 핵심이다: 신규 malloc에서는 0이므로 모든 슬롯을 초기화하고, realloc에서는 이전 용량이므로 증가된 꼬리 부분만 초기화해 이미 스테이징된 레코드를 건드리지 않는다. repl_data만 null로 초기화해도 충분한 이유는, repl_log_insert가 다른 모든 필드(repl_type, rcvindex, inst_oid, lsa, length, must_flush, tde_encrypted)를 읽기 전에 채우며, repl_data의 NULL 여부가 logtb_clear_tdes의 해제를 제어하는 가드이기 때문이다.
불변 조건 (모든 슬롯의
repl_data는 유효한 힙 포인터이거나NULL이다). 여기서 할당 시 null로 초기화되고,repl_log_insert에서malloc된 버퍼 또는NULL로 설정되며,logtb_clear_tdes에서if (repl_records[i].repl_data)조건 하에 해제된다 — 초기화되지 않은 증가 슬롯이 있으면 그 해제가 garbage를 건드리게 된다. 꼬리 루프가 증가된 슬롯의 이 조건을 보장하고, 삽입별 대입이 채워진 슬롯을 보장한다. 주의: 분기 B1은 전형적인p = realloc(p, …)안티패턴이다 — 실패 시 원본 블록은 살아 있지만 그 유일한 포인터가NULL로 덮어써져 누수가 발생한다. OOM 이후에는 어차피 abort와 프로세스 정리가 뒤따르므로 실제 해악은 없지만, 이 패턴을 보존하지 않도록 한다.
2.5 repl_log_insert — 할당 분기와 suppress 단락
섹션 제목: “2.5 repl_log_insert — 할당 분기와 suppress 단락”repl_log_insert의 방출 부분은 Chapter 3에서 해부한다; 여기서는 진입 가드 — suppress 단락과 레코드 쓰기 이전의 할당 분기만 다룬다.
// repl_log_insert -- src/transaction/replication.ctran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);tdes = LOG_FIND_TDES (tran_index);if (tdes == NULL) { return ER_FAILED; }
/* If suppress_replication flag is set, do not write replication log. */if (tdes->suppress_replication != 0) { /* clear repl lsa in tdes since no replication log will be written */ LSA_SET_NULL (&tdes->repl_insert_lsa); LSA_SET_NULL (&tdes->repl_update_lsa);
return NO_ERROR; }
if (thread_p->no_logging && tdes->fl_mark_repl_recidx == -1) { return NO_ERROR; }
/* check the replication log array status, if we need to alloc? */if (REPL_LOG_IS_NOT_EXISTS (tran_index) && ((error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, false)) != NO_ERROR)) { return error; }/* the replication log array is full? re-alloc? */else if (REPL_LOG_IS_FULL (tran_index) && (error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, true)) != NO_ERROR) { return error; }
repl_rec = (LOG_REPL_RECORD *) (&tdes->repl_records[tdes->cur_repl_record]);// ... record fill (Chapter 3) ...Figure 2-2가 모든 분기를 추적한다. 핵심적인 비자명한 점: 두 할당 가드는 repl_log_info_alloc을 최대 한 번만 호출하는데, 매크로가 상호 배타적이기 때문이다(Section 2.3). &&는 해당 가드가 성립할 때만 할당자를 호출하며, G3/G4는 실패를 전파하고, 할당이 불필요한 경우는 fall-through하며, 여유 슬롯이 이미 있는 버퍼는 둘 다 건너뛴다. 동일한 할당 프롤로그가 repl_log_insert_statement(Chapter 4)에도 그대로 등장한다; no_logging early-out(G2)은 repl_log_insert에만 있다.
flowchart TD
A["repl_log_insert 진입"] --> B{"tdes == NULL?"}
B -- 예 --> R0["return ER_FAILED (G0)"]
B -- 아니오 --> C{"suppress_replication != 0?"}
C -- 예 --> S["repl LSA 둘 다 NULL 설정<br/>return NO_ERROR (G1)"]
C -- 아니오 --> D{"no_logging AND flush 마크 없음?"}
D -- 예 --> R2["return NO_ERROR (G2)"]
D -- 아니오 --> E{"NOT_EXISTS?"}
E -- 예 --> F["alloc(malloc)"]
F --> F2{"오류?"}
F2 -- 예 --> R3["return error (G3)"]
F2 -- 아니오 --> J["slot = repl_records[cur_repl_record]"]
E -- 아니오 --> G{"IS_FULL?"}
G -- 예 --> H["alloc(realloc 증가)"]
H --> H2{"오류?"}
H2 -- 예 --> R4["return error (G4)"]
H2 -- 아니오 --> J
G -- 아니오 --> J
J --> K["레코드 채우기 (Chapter 3)"]
Figure 2-2. repl_log_insert의 진입 가드; 두 할당 분기 모두 동일한 안전한 슬롯 접근으로 수렴한다.
불변 조건 (suppress는 스테이징 LSA를 null로 만들 뿐, 배열을 건드리지 않는다).
suppress_replication != 0일 때repl_log_insert는repl_insert_lsa/repl_update_lsa를 초기화하지만repl_records와 커서는 그대로 둔다. suppress는 트랜잭션 내에서 토글 가능한 per-operation 게이트이므로, LSA를 null로 만드는 것은 같은 트랜잭션의 이후 비-suppress 삽입이 낡은repl_insert_lsa를 물려받아 잘못된 WAL 주소로 레코드를 스탬핑하는 것을 막는다(Chapter 3: 삽입 경로가repl_insert_lsa를repl_rec->lsa에 복사한다). 동일한 플래그가repl_log_insert_statement(Chapter 4)와repl_add_update_lsa를 제어한다 — 후자는 suppress 하에서 LSA를 null로 만들지 않고 즉시 반환하는데, 백패치할 스테이징된 update 레코드가 없기 때문이다.
2.6 커밋 전까지 WAL에 기록하지 않는다는 불변 조건
섹션 제목: “2.6 커밋 전까지 WAL에 기록하지 않는다는 불변 조건”불변 조건 (커밋 전까지 WAL에 LOG_REPLICATION_DATA가 없다). 이 챕터의 모든 작업은 로그 버퍼나 디스크가 아닌 프로세스 사유 힙 메모리(
tdes->repl_records[])에만 쓴다. 스테이징된 레코드는 커밋 시log_append_repl_info_internal이append_repl_recidx부터 배열을 순회할 때 비로소 실제 복제 WAL 레코드가 된다(Chapter 5); 이 경로 밖에서 레코드를 append하는 호출 지점은 없으며, 이는 구조적 강제다. abort하는 트랜잭션은logtb_clear_tdes에서 스테이징 페이로드가 해제되므로, 그 행 이미지는 WAL에 도달하지 않았고 따라서 레플리카에도 절대 도달할 수 없다. 복제는 이로써 커밋과 원자적으로 구성된다; DML 측에서 즉시 append를 수행했다면 abort 시 phantom 변경이 누출되어 레플리카가 조용히 diverge했을 것이다.
2.7 챕터 요약 — 핵심 정리
섹션 제목: “2.7 챕터 요약 — 핵심 정리”- 지연 할당, warm 재사용.
repl_records는 첫 번째 쓰기가repl_log_info_alloc(..., false)를 트리거할 때까지NULL이다;logtb_clear_tdes는 커서를 되감지만 버퍼를 유지하므로, 이후 트랜잭션은malloc을 건너뛴다. - 세 함수가 세 가지 생애주기 이벤트를 소유한다.
logtb_initialize_tdes는 포인터를 null로 만들고 용량을 0으로 만든다;logtb_clear_tdes는 페이로드를 해제하고 커서를 되감되 할당을 유지한다;logtb_free_tran_index만이 (logtb_clear_tdes호출 후) 배열 껍데기를 해제한다. - 용량과 채우기는 분리된다.
num_repl_records가 용량,cur_repl_record가 fill 커서; “가득 참” 매크로는 한 슬롯 일찍(num == cur+1) 발생해 진행 중인 삽입을 위한 슬롯을 보장한다. repl_log_info_alloc은 100씩 증가하며, 새 슬롯만 초기화한다.k변수가repl_data = NULL꼬리 루프의 범위를 한정해 증가 시 스테이징된 레코드가 살아남게 한다; OOM 시 realloc 경로는 이전 블록을 누수시킨다.suppress_replication은 어떤 할당보다 먼저 단락된다. 스테이징 LSA를 null로 만들어 이후 비-suppress 삽입이 낡은 LSA를 물려받지 않도록 한다.- 커밋 전까지 WAL에 아무것도 도달하지 않는다 — 스테이징은 사유 힙 메모리이며, 커밋 시 방출 패스(Chapter 5)에 의해서만 WAL 레코드로 변환된다. 이로써 복제는 커밋과 원자적이 된다.
Chapter 3: DML 실행 중 복제 레코드 기록
섹션 제목: “Chapter 3: DML 실행 중 복제 레코드 기록”이 챕터는 하나의 질문에 답한다. 마스터에서 행이 삽입, 수정, 삭제될 때, 정확히 어느 호출 경로가 LOG_REPL_RECORD를 스테이징하며, 작업 종류에 따라 스테이징된 레코드는 어떻게 달라지는가? 상위 개요 문서(cubrid-ha-replication.md, “Row-based replication” 절)는 CUBRID가 물리적 행 이미지 대신 기본 키(primary key)를 복제하는 이유를 설명한다. 여기서는 키가 어떻게 캡처되는지, UPDATE에서 역방향 패치(back-patch)가 어디서 발생하는지, 그리고 기록 코드가 취하는 모든 분기를 추적한다.
스테이징 배열(tdes->repl_records[])과 LOG_REPL_RECORD 구조는 Chapter 1에서 다루었고, Chapter 2에서는 할당을 설명했다. 이 챕터는 배열 자체가 아니라 배열에 쓰는 경로를 추적한다.
3.1 기록 토폴로지 — 하나의 입구, 세 가지 호출 경로
섹션 제목: “3.1 기록 토폴로지 — 하나의 입구, 세 가지 호출 경로”서버 측 DML은 모두 locator_attribute_info_force에 도달한다. 이 함수는 attribute-info copyarea로부터 레코드 이미지를 재구성하고 플러시 작업 종류에 따라 분기하여 locator_insert_force, locator_update_force(호출자가 넘긴 repl_info 포함), 또는 locator_delete_force로 디스패치한다. locator_delete_force는 locator_delete_force_internal로 전달하는 공개 래퍼이며, 알 수 없는 작업은 ER_LC_BADFORCE_OPERATION을 발생시킨다.
핵심 아키텍처 사실: 복제 레코드는 힙(heap) 레이어가 아니라 인덱스(index) 레이어에서 스테이징된다. 세 force 루틴 모두 locator_add_or_remove_index / locator_update_index를 호출하고, 이 함수들이 repl_log_insert를 호출한다. B-트리 작업을 위해 기본 키 DB_VALUE가 이미 구체화되어 있는 지점이므로, 이를 캡처하는 추가 비용이 없다.
flowchart TB AIF["locator_attribute_info_force"] AIF --> INS["locator_insert_force"] --> HIL["heap_insert_logical"] AIF --> UPD["locator_update_force"] AIF --> DEL["locator_delete_force"] --> DELI["locator_delete_force_internal"] --> HDL["heap_delete_logical"] INS --> ARI["locator_add_or_remove_index\n(is_insert true/false)"] DELI --> ARI UPD --> LUI["locator_update_index"] UPD --> HUL["heap_update_logical"] UPD --> RAU["repl_add_update_lsa\n(back-patch lsa)"] ARI -->|"PRIMARY_KEY, INSERT 또는 DELETE"| RLI["repl_log_insert\n(LOG_REPL_RECORD 스테이징)"] LUI -->|"PRIMARY_KEY, UPDATE"| RLI
Figure 3-1 — 기록 깔때기(emission funnel). INSERT와 DELETE는 locator_add_or_remove_index 내부에서 스테이징되고, UPDATE는 locator_update_index 내부에서 스테이징한 뒤 heap_update_logical 이후 repl_add_update_lsa로 LSA를 역방향 패치한다.
3.2 세 가지 force 루틴 — 복제 관련 분기
섹션 제목: “3.2 세 가지 force 루틴 — 복제 관련 분기”force 루틴들은 repl_log_insert를 직접 호출하지 않는다. 대신 힙 로그와 need_replication 상태를 확립하여, 이후 인덱스 패스가 의존하도록 한다. INSERT와 DELETE는 힙 패스를 인덱스 패스 이전에 실행하여(힙 LSA가 스테이징 시점에 이미 확보된 상태로), UPDATE는 순서를 반전하여 인덱스 패스 먼저 힙 패스 나중에 실행한다. 이 역전이 역방향 패치를 필요하게 만드는 원인이다. 세 루틴 전반에서 복제와 관련된 분기들을 정리하면 다음과 같다.
| 루틴 / 분기 | 복제에 대한 영향 |
|---|---|
insert: partition_prune_insert 실패 후 goto error2 | 아무것도 스테이징되지 않음 |
insert: heap_insert_logical 성공 | 힙 로그(RVHF_INSERT/RVHF_MVCC_INSERT)가 로그 추가 경로에서 repl_insert_lsa를 기록; 오류 시 LSA 없음, 인덱스 패스 없음 |
insert: has_index && add_or_remove_index (... true ...) | is_insert = true 인덱스 패스에서 INSERT 레코드 스테이징 |
delete: isold_object == false | OID_SET_NULL; 인스턴스 블록 건너뜀 — 아무것도 스테이징되지 않음 |
delete: isold_object && OID_IS_ROOTOID | 클래스 삭제 — 카탈로그 경로, 인스턴스 인덱스 패스 아님 |
delete: heap_delete_logical 성공 | 힙 삭제 로그 추가, tdes->tail_lsa 전진; MVCC 비활성화 건너뜀은 힙 로그를 기록하지 않아 tail_lsa 변화 없음(3.4의 prior_lsa 폴백); 오류 시 아무것도 스테이징되지 않음 |
delete: idx_action_flag == FOR_INSERT_OR_DELETE에서 add_or_remove_index (... false ...) | is_insert = false 인덱스 패스에서 DELETE 레코드 스테이징 |
delete: 그 외 ..._index_for_moving (... false ...) | 파티션 이동 — 인덱스 항목은 삭제하지만 스테이징하지 않음 |
update: locator_update_index(본문 앞부분) | NULL lsa로 UPDATE 레코드 스테이징; repl_info.need_replication 설정/해제 |
update: heap_update_logical 성공 | RVHF_UPDATE 계열 힙 로그가 로그 추가 경로에서 repl_update_lsa를 기록; 오류 시 NULL-lsa 레코드는 패치되지 않음 |
update: !LOG_CHECK_LOG_APPLIER && log_does_allow_replication () && repl_info.need_replication | repl_add_update_lsa (thread_p, oid) 호출을 제어하는 가드 |
3.3 INSERT와 DELETE — locator_add_or_remove_index 내부에서의 스테이징
섹션 제목: “3.3 INSERT와 DELETE — locator_add_or_remove_index 내부에서의 스테이징”locator_insert_force와 locator_delete_force_internal 모두 locator_add_or_remove_index_internal에 도달한다. 이 함수는 클래스의 모든 B-트리를 순회하면서 물리적 인덱스 작업을 수행하고, 기본 키에 한해서만 복제 레코드를 스테이징한다.
// locator_add_or_remove_index_internal -- src/transaction/locator_sr.c if (need_replication && index->type == BTREE_PRIMARY_KEY && error_code == NO_ERROR && !LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true) { error_code = repl_log_insert (thread_p, class_oid, inst_oid, datayn ? LOG_REPLICATION_DATA : LOG_REPLICATION_STATEMENT, is_insert ? RVREPL_DATA_INSERT : RVREPL_DATA_DELETE, /* <- op kind */ key_dbvalue, REPL_INFO_TYPE_RBR_NORMAL); /* <- always NORMAL here */ }네 가지 조건이 모두 충족되어야 한다. need_replication이 false이면 클래스가 복제 대상이 아니다(예: PK 없음). BTREE_PRIMARY_KEY가 아닌 인덱스는 다음 반복에서 처리된다. error_code != NO_ERROR는 B-트리 작업이 실패했음을 뜻하며 실패한 변경을 스테이징해서는 안 된다. LOG_CHECK_LOG_APPLIER / log_does_allow_replication () 쌍은 현재 프로세스가 applier로서 재현 중이거나 복제가 전역적으로 비활성화된 경우 단락(short-circuit)된다. INSERT와 DELETE는 REPL_INFO_TYPE_RBR_NORMAL을 하드코딩한다(단일 행 작업은 START/END 괄호로 묶이지 않는다 — 3.5절). 차이는 rcvindex 하나뿐이다(RVREPL_DATA_INSERT 대 RVREPL_DATA_DELETE).
불변 조건 — 행 복제 레코드는 행당 정확히 한 번, 기본 키 인덱스에 대해서만 스테이징된다. per-index 루프 내의
index->type == BTREE_PRIMARY_KEY가드가 이를 강제한다. 이 가드를 제거하면 N개 인덱스를 가진 테이블은 N개의 복사본을 스테이징하고, PK 없는 테이블은 아무것도 스테이징하지 않아(need_replication/PK단락) 조용히 복제 대상에서 제외된다.
3.4 repl_log_insert — 스테이징 핵심 함수, 분기별 분석
섹션 제목: “3.4 repl_log_insert — 스테이징 핵심 함수, 분기별 분석”repl_log_insert는 tdes->repl_records[]에 쓰는 유일한 함수다. 시그니처에는 작업 종류(rcvindex)와 다중 업데이트 정제 인수(repl_info)가 담긴다. 다섯 단계를 거치며, Figure 3-2는 모든 분기를 추적한다.
flowchart TB A["tdes == NULL?"] -->|yes| RET1["return ER_FAILED"] A -->|no| B["suppress_replication?"] B -->|yes| C["대기 중인 LSA 두 개를 null로; return"] B -->|no| D["no_logging이고 flush-mark 없음?"] D -->|yes| E["return NO_ERROR"] D -->|no| F["배열 없음/가득 참 -> repl_log_info_alloc"] F -->|실패| RET2["return error"] F -->|성공| G["P3: rcvindex 정제\nP4: 페이로드 + tde\nP5: lsa 설정; cur_repl_record++"] G --> Q["flush-mark 열림? must_flush 업그레이드"]
Figure 3-2 — repl_log_insert 제어 흐름, 모든 조기 반환 표시.
Phase 1–2 — 조기 반환과 배열 확장. 세 가지 단락 조건은 절대 스테이징하지 않는다. tdes == NULL; suppress_replication != 0(Chapter 11 — 대기 중인 LSA 두 개를 null로 설정하여 이후 레코드가 억제된 변경에 역방향 패치하지 못하도록 막는다); no_logging && fl_mark_repl_recidx == -1. 이후 REPL_LOG_IS_NOT_EXISTS / REPL_LOG_IS_FULL 조건이 repl_log_info_alloc을 호출한다(Chapter 2). 할당 실패 시 cur_repl_record를 건드리지 않고 반환하여 cur_repl_record <= capacity 불변 조건을 유지한다.
Phase 3 — rcvindex 정제. 작업 종류가 최종 결정되는 지점이다.
// repl_log_insert -- src/transaction/replication.c repl_rec->rcvindex = rcvindex; if (rcvindex == RVREPL_DATA_UPDATE) /* INSERT/DELETE skip this block */ switch (repl_info) { case REPL_INFO_TYPE_RBR_START: repl_rec->rcvindex = RVREPL_DATA_UPDATE_START; break; case REPL_INFO_TYPE_RBR_END: repl_rec->rcvindex = RVREPL_DATA_UPDATE_END; break; default: break; /* NORMAL -> stays RVREPL_DATA_UPDATE */ }외부 if는 RVREPL_DATA_UPDATE일 때만 실행되므로, 3.3절에서 INSERT/DELETE에 전달하는 REPL_INFO_TYPE_RBR_NORMAL 인수는 무시된다.
Phase 4 — 페이로드와 tde_encrypted. LOG_REPLICATION_DATA일 때만 패킹된다(datayn 인수). LOG_REPLICATION_STATEMENT 레코드는 repl_data = NULL, length = 0이 된다.
// repl_log_insert -- src/transaction/replication.c if (log_type == LOG_REPLICATION_DATA) { // ... condensed: heap_get_class_name / heap_get_class_tde_algorithm -> ER_REPL_ERROR on failure ... repl_rec->tde_encrypted = tde_algo != TDE_ALGORITHM_NONE; /* <- per-class TDE flag */ repl_rec->length = OR_INT_SIZE + or_packed_string_length (class_name, &strlen) + OR_VALUE_ALIGNED_SIZE (key_dbvalue); ptr = (char *) malloc (repl_rec->length); repl_rec->repl_data = ptr; /* NULL -> ER_REPL_ERROR */ ptr_to_packed_key_value_size = ptr; ptr += OR_INT_SIZE; /* reserve 4 bytes, fill last */ ptr = or_pack_string_with_length (ptr, class_name, strlen); /* class name */ ptr = or_pack_mem_value (ptr, key_dbvalue, &packed_key_len); /* primary key DB_VALUE */ or_pack_int (ptr_to_packed_key_value_size, packed_key_len); /* back-fill size header */ } else { repl_rec->repl_data = NULL; repl_rec->length = 0; }tde_encrypted는 상단에서 false로 초기화되고 이 Phase에서만 올라가므로, STATEMENT 레코드와 TDE 미사용 레코드는 false를 유지한다.
불변 조건 — 4바이트 크기 헤더는 그 뒤에 이어지는 패킹된 키의 바이트 길이와 같아야 한다.
or_pack_mem_value가packed_key_len을 보고한 뒤 마지막에 기록된다.OR_VALUE_ALIGNED_SIZE상한값으로 미리 채우고 슬레이브가 언팩하면 키를 넘어 읽어 행이 손상된다.
Phase 5 — 비대칭 LSA 설정. must_flush = LOG_REPL_COMMIT_NEED_FLUSH 이후, 함수는 작업 종류에 따라 repl_rec->lsa를 선택한다.
// repl_log_insert -- src/transaction/replication.c switch (rcvindex) { case RVREPL_DATA_INSERT: /* heap_insert already ran */ 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: LSA_SET_NULL (&repl_rec->lsa); break; /* <- heap update not done yet; back-patch later */ case RVREPL_DATA_DELETE: /* tail_lsa if set, else next prior_lsa */ LSA_COPY (&repl_rec->lsa, LSA_ISNULL (&tdes->tail_lsa) ? &log_Gl.prior_info.prior_lsa : &tdes->tail_lsa); break; default: break; } tdes->cur_repl_record++;INSERT는 repl_insert_lsa를 복사하고 대기 중인 LSA 두 개를 null로 설정한다. UPDATE는 힙 업데이트가 인덱스 패스 이후에 실행되어 repl_add_update_lsa로 역방향 패치할 것이므로 NULL을 저장한다. DELETE는 자체 tail_lsa를 사용하되, MVCC 비활성화 건너뜀(3.2)으로 tail_lsa가 변하지 않은 경우 다음 prior_lsa로 폴백한다. UPDATE에서만 역방향 패치가 필요한 이유는 인덱스 패스를 힙 패스 이전에 실행하는 이 역전 때문이며, repl_add_update_lsa 헤더 주석에도 기록되어 있다.
3.5 flush-mark 꼬리와 다중 업데이트 괄호
섹션 제목: “3.5 flush-mark 꼬리와 다중 업데이트 괄호”cur_repl_record++ 이후, flush-mark 괄호가 열려 있을 때만(fl_mark_repl_recidx != -1; repl_start_flush_mark로 열림, Chapter 4) 마지막 블록이 실행된다.
// repl_log_insert -- src/transaction/replication.c if (tdes->fl_mark_repl_recidx != -1) { if (strcmp (class_name, CT_SERIAL_NAME) != 0) { for (i = 0; i < tdes->fl_mark_repl_recidx; i++) /* same instance already staged? */ if (recsp[i].must_flush == LOG_REPL_COMMIT_NEED_FLUSH && OID_EQ (&recsp[i].inst_oid, &repl_rec->inst_oid)) break; /* -> do NOT upgrade */ if (i >= tdes->fl_mark_repl_recidx) repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* <- flush at commit AND rollback */ } else repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* serial changes always upgrade */ }기본 must_flush는 LOG_REPL_COMMIT_NEED_FLUSH(커밋 시에만 플러시)다. 열린 괄호 안에서는 LOG_REPL_NEED_FLUSH(커밋 및 롤백 시 플러시)로 업그레이드된다. 단, 같은 inst_oid를 대상으로 하는 이전 괄호 레코드가 있으면 이중 플러시를 방지하기 위해 업그레이드하지 않는다. CT_SERIAL_NAME(_db_serial)은 예외다. 시리얼 변경은 롤백 후에도 유지되므로 항상 업그레이드된다. repl_log_insert는 마크를 적용하고, 마크가 존재하는 이유는 Chapter 4에서 다룬다.
3.6 UPDATE — locator_update_index에서의 스테이징 후 역방향 패치
섹션 제목: “3.6 UPDATE — locator_update_index에서의 스테이징 후 역방향 패치”locator_force_for_multi_update가 정제 값을 계산한다(i == first_update_obj이면 REPL_INFO_TYPE_RBR_START, i == last_update_obj이면 REPL_INFO_TYPE_RBR_END, 그 외에는 NORMAL). locator_update_force는 이를 REPL_INFO(need_replication = true)로 감싸 locator_update_index에 전달하고, locator_update_index는 PK를 찾아(pk_btid_index), 이전 키를 구체화하고, RVREPL_DATA_UPDATE로 스테이징한다.
// locator_update_index -- src/transaction/locator_sr.c if (pk_btid_index != -1) { // ... condensed: heap_attrvalue_get_key materializes repl_old_key, MIDXKEY domain fixup ... error_code = repl_log_insert (thread_p, class_oid, oid, LOG_REPLICATION_DATA, RVREPL_DATA_UPDATE, repl_old_key, (REPL_INFO_TYPE) repl_info->repl_info_type); /* START/NORMAL/END */ } else { LSA_SET_NULL (&tdes->repl_insert_lsa); /* no PK -> FK action can't steal it */ if (repl_info != NULL) repl_info->need_replication = false; /* <- mark class non-replicated */ }UPDATE는 이전 PK(repl_old_key)를 복제하여, PK가 변경된 경우에도 슬레이브가 행을 찾을 수 있게 한다. PK가 없으면 need_replication이 해제되고 호출자는 역방향 패치를 건너뛴다. locator_update_force로 돌아오면, heap_update_logical이 RVHF_UPDATE 계열 힙 로그를 추가하고, log_manager.c의 복제 인식(replication-aware) 로그 추가 헬퍼가 부수 효과로 tail_lsa에서 tdes->repl_update_lsa를 기록한다. 그 후에야 세 조건 가드(!LOG_CHECK_LOG_APPLIER && log_does_allow_replication () && repl_info.need_replication)가 repl_add_update_lsa (thread_p, oid)를 호출하여 Phase 5에서 NULL로 남겨둔 lsa를 채운다.
3.7 repl_add_update_lsa — 역방향 탐색을 통한 역방향 패치
섹션 제목: “3.7 repl_add_update_lsa — 역방향 탐색을 통한 역방향 패치”repl_add_update_lsa는 스테이징 배열을 역방향으로 순회하며 inst_oid가 일치하는 가장 가까운 UPDATE 레코드를 채운다.
// repl_add_update_lsa -- src/transaction/replication.c if (tdes->suppress_replication != 0) return NO_ERROR; /* <- suppressed: nothing to patch */ for (i = tdes->cur_repl_record - 1; i >= 0; i--) { /* newest first */ 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 || ... _UPDATE_START || ... _UPDATE_END); LSA_COPY (&repl_rec->lsa, &tdes->repl_update_lsa); /* patch the NULL lsa */ LSA_SET_NULL (&tdes->repl_update_lsa); LSA_SET_NULL (&tdes->repl_insert_lsa); find = true; break; /* patch only the nearest match */ } } if (find == false && prm_get_bool_value (PRM_ID_DEBUG_REPLICATION_DATA)) _er_log_debug (ARG_FILE_LINE, "can't find out the UPDATE LSA");분기별로: **억제됨(suppressed)**은 즉시 반환; 일치(OID_EQ, non-null repl_update_lsa)는 힙 업데이트 LSA를 복사하고 대기 중인 LSA 두 개를 null로 설정한 뒤 가장 가까운 레코드에서 멈춘다(두 번 업데이트된 인스턴스는 가장 최근 레코드에 가장 최근 LSA를 얻는다); 일치 없음은 PRM_ID_DEBUG_REPLICATION_DATA가 설정된 경우에만 로그를 남기고 그렇지 않으면 NO_ERROR를 조용히 반환한다(복제 대상이 아닌 클래스의 레코드는 스테이징된 적이 없다). assert는 INSERT/DELETE가 여기에 null LSA를 가지고 도달하지 않음을 문서화한다 — 이들은 Phase 5에서 이미 LSA를 확정했다.
불변 조건 —
tdes->repl_update_lsa는 UPDATE 당 정확히 한 번 소비된다.heap_update_logical이 설정하고,repl_add_update_lsa가 레코드를 패치하는 즉시 해제된다. 성공 시 대기 중인 LSA 두 개를 모두 null로 설정하면 이후 FK 캐스케이드가 잘못된 레코드에 역방향 패치하는 것을 막는다. PK에 FK가 함께 있을 때 FK 검사 전후의preserved_repl_lsa저장/복원(locator_update_index내)이 이를 보호한다.
3.8 챕터 요약 — 핵심 정리
섹션 제목: “3.8 챕터 요약 — 핵심 정리”- 모든 DML은
locator_attribute_info_force를 거쳐 세 force 루틴으로 도달한다. 레코드는 한 단계 더 깊은 인덱스 패스에서 스테이징되는데, 이 시점에 PKDB_VALUE가 이미 준비되어 있다. - force 루틴들은 힙 패스와 인덱스 패스의 순서를 서로 다르게 정한다. INSERT/DELETE는 힙 로그를 먼저 기록하고(로그 추가 경로가 대기 중인 LSA를 즉시 기록), UPDATE는 인덱스 패스를 먼저 실행하고 LSA를 나중에 패치한다. 모든 오류
goto는 스테이징을 건너뛰며, 파티션 이동과 MVCC 비활성화 분기는 아무것도 스테이징하지 않는다. repl_log_insert는tdes->repl_records[]에 쓰는 유일한 함수다. 배열을 확장하기 전에tdes == NULL,suppress_replication, no-logging/no-flush-mark 경우를 단락한다.- 작업 종류는
rcvindex인수다. INSERT/DELETE는 고정REPL_INFO_TYPE_RBR_NORMAL과 함께RVREPL_DATA_INSERT/_DELETE를 전달한다. UPDATE는RVREPL_DATA_UPDATE와 함께 START/NORMAL/END 정제 값을 전달하며, 이는RVREPL_DATA_UPDATE_START/_END로 재작성된다. - 기본 키 인덱스만 레코드를 스테이징하며, 행당 한 번이다. DATA 페이로드는 클래스 이름과 역방향 채움 4바이트 크기 헤더 뒤에 패킹된 PK로 구성되며,
tde_encrypted는heap_get_class_tde_algorithm에서 가져온다. - LSA 처리는 비대칭이다. INSERT는
repl_insert_lsa를 복사하고, DELETE는tail_lsa(또는 다음prior_lsa)를 사용하며, UPDATE는 힙 업데이트가 아직 발생하지 않았으므로 NULL을 저장한다. 대기 중인 LSA 자체는log_manager.c의 복제 인식 로그 추가 헬퍼가RVHF_INSERT/RVHF_MVCC_INSERT및RVHF_UPDATE힙 복구 인덱스를 키로 삼아 기록한다.heap_*_logical루틴이 직접 기록하는 것이 아니다. - UPDATE만 역방향 패치를 수행한다.
repl_add_update_lsa는 배열을 역방향으로 탐색하여 가장 가까운 일치 레코드를repl_update_lsa로 채우고, 대기 중인 LSA 두 개를 null로 설정하여 FK 캐스케이드가 이전 레코드의 대상을 손상시키지 못하도록 한다.
Chapter 4: 구문 기반 방출(Statement-Based Emission)과 플러시 마크(Flush Mark)
섹션 제목: “Chapter 4: 구문 기반 방출(Statement-Based Emission)과 플러시 마크(Flush Mark)”3장에서는 must_flush = LOG_REPL_COMMIT_NEED_FLUSH로 repl_records에 row 이벤트를 스테이징하는 경로를 다뤘다. 이 장은 그 경로에서 벗어나는 두 가지 경우를 다룬다: (1) row 이미지 대신 SQL 텍스트를 담는 구문 기반(statement-based) 레코드, (2) 트랜잭션이 롤백되더라도 레코드가 살아남도록 강제하는 플러시 마크(flush mark). 독자가 가져야 할 질문: DDL과 복제 대상 세션 구문은 row 이벤트와 어떻게 다르게 스테이징되며, 플러시 마크는 어떻게 롤백 시에도 방출을 강제하는가? SBR 경로의 설계 근거는 cubrid-ha-replication.md의 “Statement-based vs row-based” 및 Open Question 8을 참고하라. 이 장은 코드를 따라간다.
4.1 구문 레코드의 출처
섹션 제목: “4.1 구문 레코드의 출처”구문 레코드는 heap/index 기계가 아닌, SQL 실행기가 DDL 구문(및 일부 복제 대상 세션 구문)을 완료할 때 명시적으로 생성한다. 경로는 do_replicate_statement(execute_statement.c)로, 파싱된 구문으로부터 스택 상주 REPL_INFO_SBR repl_stmt를 채운다. 이어서 locator_flush_replication_info → 서버 측 디스패처 xrepl_set_info(locator_sr.c) 순서로 진행된다. 이 게이트에는 오직 REPL_INFO_TYPE_SBR만 repl_log_insert_statement에 도달하고, 다른 repl_info_type은 모두 거부된다:
// xrepl_set_info -- src/transaction/locator_sr.cif (!LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true) { switch (repl_info->repl_info_type) { case REPL_INFO_TYPE_SBR: error_code = repl_log_insert_statement (thread_p, (REPL_INFO_SBR *) repl_info->info); break; default: /* <- RBR types never arrive here */ error_code = ER_REPL_ERROR; er_set (..., "can't make repl sbr info"); break; } }row 기반 타입(REPL_INFO_TYPE 열거형의 REPL_INFO_TYPE_RBR_*)은 heap/index 경로(3장)에서 repl_log_insert가 처리하며, 이 함수에는 도달하지 않는다.
REPL_INFO_SBR 페이로드 필드
섹션 제목: “REPL_INFO_SBR 페이로드 필드”REPL_INFO_SBR(replication.h의 struct repl_info_statement alias)은 진행 중인 하나의 구문을 기술한다. 이것은 스테이징된 레코드가 아니라 repl_log_insert_statement가 repl_rec->repl_data에 직렬화하는 입력이다. 각 필드:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
statement_type | int — CUBRID_STMT_* 코드(예: CUBRID_STMT_CREATE_CLASS, CUBRID_STMT_GRANT). | applier에게 구문 범주를 알린다. do_replicate_statement가 PT 노드 타입으로 분기해 설정한다. PT_DROP_VARIABLE 및 미처리 타입은 조기에 return NO_ERROR — 의도적으로 복제하지 않는다. |
name | 대상 오브젝트 이름(클래스/시리얼/인덱스), 또는 파서가 이름을 산출하지 못할 때의 센티널 unknown_name(리터럴 문자열 "-"). | applier가 오브젝트 단위로 로깅·라우팅할 수 있게 한다. 센티널 덕분에 미설정 포인터가 생기지 않는다. |
stmt_text | 레플리카에서 재실행할 SQL 텍스트. | 이것이 핵심 페이로드다 — 레플리카는 이를 다시 파싱해 실행한다. CREATE/ALTER SERVER의 경우 비밀번호를 제거하기 위해 pt_print_bytes 출력으로 대체된다. 호스트 변수가 있으면 ? 플레이스홀더로 치환하여 재구성된다(sbr_text). |
db_user | 실행 DB 사용자(db_get_user_name ()). | 레플리카가 동일 사용자로 DDL을 적용해야 권한이 일치한다. assert_release (repl_stmt.db_user != NULL)이 이를 보장한다. |
sys_prm_context | HA 관련 시스템 파라미터(sysprm_print_parameters_for_ha_repl ()), DDL이 아닌 경우 NULL. | DDL 의미론은 세션 파라미터에 따라 달라질 수 있다. pt_is_ddl_statement (statement) 시에만 채워진다. |
불변 조건 — SBR은 row 이미지가 아닌 텍스트를 담는다. 구문 레코드의 repl_data는 {statement_type, name, stmt_text, db_user, sys_prm_context} 튜플을 패킹하며, inst_oid는 항상 NULL(OID_SET_NULL (&repl_rec->inst_oid))이다. row 레코드는 패킹된 PK와 실제 inst_oid를 가진다. repl_type(LOG_REPLICATION_STATEMENT 대 LOG_REPLICATION_DATA)이 shipper와 applier가 분기하는 판별자다. 여기서 inst_oid가 NULL이 아니면, repl_log_insert의 충돌 병합 로직(inst_oid를 키로 사용)이 이를 row 업데이트로 오인할 수 있다.
4.2 repl_log_insert_statement — 분기별 분석
섹션 제목: “4.2 repl_log_insert_statement — 분기별 분석”flowchart TB
A["LOG_FIND_TDES"] --> B{"tdes == NULL?"}
B -->|yes| RF["return ER_FAILED"]
B -->|no| C{"suppress_replication?"}
C -->|yes| OK0["return NO_ERROR\n레코드 스테이징 안 함"]
C -->|no| D{"배열 상태"}
D -->|NOT_EXISTS| AL1["alloc(false)"]
D -->|IS_FULL| AL2["alloc(true) realloc"]
D -->|여유 있음| E
AL1 --> E["식별자 스탬핑\ntype/rcvindex/must_flush/inst_oid"]
AL2 --> E
E --> G["malloc repl_data"]
G --> H{"malloc OK?"}
H -->|no| ME["ER_REPL_ERROR, return"]
H -->|yes| I["5개 필드 or_pack\nlsa = tdes->tail_lsa"]
I --> J{"플러시 마크 윈도우 안?"}
J -->|yes| K["must_flush = NEED_FLUSH"]
J -->|no| L["COMMIT_NEED_FLUSH 유지"]
K --> M["cur_repl_record++"]
L --> M
Figure 4-1 — repl_log_insert_statement의 제어 흐름. 두 개의 조기 반환과 malloc 오류 경로 모두 cur_repl_record를 건드리지 않으므로, 반쯤 완성된 레코드가 게시되지 않는다.
전처리 부분은 repl_log_insert(3장)와 동일하다: LOG_TDES 확인, suppress_replication 처리, repl_records 할당 또는 확장. 두 가지 할당 분기가 중요하다:
// repl_log_insert_statement -- src/transaction/replication.cif (REPL_LOG_IS_NOT_EXISTS (tran_index) /* <- never allocated */ && ((error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, false)) != NO_ERROR)) return error;else if (REPL_LOG_IS_FULL (tran_index) /* <- last free slot used */ && (error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, true)) != NO_ERROR) return error;REPL_LOG_IS_FULL은 num_repl_records == cur_repl_record + 1이므로, 오버플로우 직전 슬롯에서 확장이 발동된다 — repl_records[cur_repl_record]에 대한 쓰기는 항상 범위 안에 있다. 레코드에는 이후 식별자와 LSA가 스탬핑된다:
// repl_log_insert_statement -- src/transaction/replication.crepl_rec->repl_type = LOG_REPLICATION_STATEMENT;repl_rec->tde_encrypted = false; /* <- SBR text is never TDE data */repl_rec->rcvindex = RVREPL_STATEMENT;repl_rec->must_flush = LOG_REPL_COMMIT_NEED_FLUSH; /* <- default: flush at commit only */OID_SET_NULL (&repl_rec->inst_oid); /* <- statements have no instance */// ... condensed: length, malloc repl_data, or_pack the 5 fields ...LSA_COPY (&repl_rec->lsa, &tdes->tail_lsa); /* <- last log this txn wrote */페이로드 길이는 OR_INT_SIZE 하나(statement_type용)와 네 개의 패킹된 문자열 길이를 합산한다. malloc으로 repl_data를 생성한 뒤 or_pack_int / or_pack_string_with_length로 튜플을 직렬화한다. malloc 실패 시 ER_REPL_ERROR를 발생시키고, cur_repl_record를 증가시키기 전에 반환하므로 배열은 변경되지 않는다. lsa는 repl_log_abort_after_lsa가 롤백 LSA와 비교할 때(§4.4)와 copylogdb/applylogdb가 순서를 정할 때 사용하는 앵커다. repl_log_insert의 RVREPL_DATA_INSERT/DELETE 케이스(repl_insert_lsa 또는 prior_info.prior_lsa 사용)와 달리, 구문 경로에는 before/after 이미지가 없으므로 단순한 tail_lsa — 해당 트랜잭션이 마지막으로 기록한 로그 — 가 올바른 앵커다.
끝부분의 플러시 마크 확인은 row 확인과 다르다. row 경로는 repl_records[0 .. fl_mark_repl_recidx)를 스캔하여 동일한 inst_oid를 가진 기존 COMMIT_NEED_FLUSH 레코드가 없을 때만 승격하는 반면(충돌 병합), 구문 경로는 그러한 스캔을 수행하지 않는다:
// repl_log_insert_statement -- src/transaction/replication.cif (tdes->fl_mark_repl_recidx != -1 && tdes->cur_repl_record >= tdes->fl_mark_repl_recidx) { /* statement replication does not check log conflicts, so * use repl_start_flush_mark with caution. */ repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* <- unconditional promotion */ }tdes->cur_repl_record++;소스 내 주석이 핵심 경고다: inst_oid로 중복 제거를 할 수 없으므로, 플러시 마크 인덱스 이후의 모든 구문 레코드는 무조건 NEED_FLUSH로 강제된다.
4.3 must_flush 상태 기계와 플러시 마크 브래킷
섹션 제목: “4.3 must_flush 상태 기계와 플러시 마크 브래킷”LOG_REPL_FLUSH(replication.h)는 스테이징된 레코드별로 언제 발송될 수 있는지를 결정하는 세 가지 값의 열거형이다:
| 값 | 숫자 | 의미 |
|---|---|---|
LOG_REPL_DONT_NEED_FLUSH | -1 | 절대 발송하지 않음 — 논리적으로 폐기됨(예: repl_log_abort_after_lsa에 의해). |
LOG_REPL_COMMIT_NEED_FLUSH | 0 | 트랜잭션이 커밋될 때만 발송(모든 row·구문 레코드의 기본값). 롤백 시 폐기. |
LOG_REPL_NEED_FLUSH | 1 | 커밋과 롤백 모두에서 발송 — 트랜잭션이 데이터를 되돌리더라도 레플리카에 도달해야 한다. 플러시 마크 브래킷 안에서만 설정. |
브래킷은 tdes->fl_mark_repl_recidx로, repl_records의 int 인덱스다(LOG_TDES 필드, 1장). repl_start_flush_mark로 열리고 repl_end_flush_mark로 닫힌다.
stateDiagram-v2 [*] --> Closed: fl_mark_repl_recidx == -1 Closed --> Open: repl_start_flush_mark\n cur_repl_record 기록 Open --> Open: 윈도우 내 삽입 \n 새 레코드는 NEED_FLUSH Open --> Closed: repl_end_flush_mark need_undo=false\n 레코드 유지, 마크 해제 Open --> RolledBack: repl_end_flush_mark need_undo=true\n 마크까지 해제 및 잘라내기 RolledBack --> Closed: 마크 해제
Figure 4-2 — 하나의 트랜잭션 디스크립터 위의 플러시 마크 상태 기계. “Open”은 마크가 유효한 인덱스를 보유한 상태, “Closed”는 -1인 상태를 의미한다.
repl_start_flush_mark
섹션 제목: “repl_start_flush_mark”// repl_start_flush_mark -- src/transaction/replication.ctdes = LOG_FIND_CURRENT_TDES (thread_p);if (tdes == NULL) { er_set (ER_FATAL_ERROR_SEVERITY, ..., ER_LOG_UNKNOWN_TRANINDEX, ...); return; /* <- fatal: no current txn */ }if (tdes->fl_mark_repl_recidx == -1) tdes->fl_mark_repl_recidx = tdes->cur_repl_record; /* <- remember where window opened *//* else already started, return */ /* <- idempotent, not nested */불변 조건 — 플러시 마크는 재진입(re-entrant)이 불가능하다. 마크가 열려 있는 동안 두 번째 repl_start_flush_mark를 호출하면 no-op이 된다. if (== -1) 가드가 첫 번째 호출 시 시작 인덱스를 고정하므로, 브래킷은 중첩되거나 시작 위치가 이동될 수 없다. 마크를 쌓으면 내부 start가 외부 윈도우에 묶이고 내부 end가 모든 것을 조기에 닫아버릴 수 있다. 따라서 qexec_execute_selupd_list는 start 하나와 end 하나를 정확히 쌍으로 맞춘다.
repl_end_flush_mark
섹션 제목: “repl_end_flush_mark”// repl_end_flush_mark -- src/transaction/replication.cif (need_undo) { LOG_REPL_RECORD *recsp = tdes->repl_records; for (i = tdes->fl_mark_repl_recidx; i < tdes->cur_repl_record; i++) free_and_init (recsp[i].repl_data); /* <- free payloads staged in window */ tdes->cur_repl_record = tdes->fl_mark_repl_recidx; /* <- truncate array back to mark */ }tdes->fl_mark_repl_recidx = -1; /* <- always close the window */두 가지 분기가 있다:
need_undo == false(성공). 윈도우의 레코드는 보존된다(여전히LOG_REPL_NEED_FLUSH). 마크 정수만-1로 리셋된다.need_undo == true(실패).fl_mark_repl_recidx부터cur_repl_record직전까지 각 레코드의repl_data가 해제되고,cur_repl_record가 마크 위치로 되감겨 해당 레코드들을 지운다. 이후 마크가 해제된다.
코드 트리 전체에서 정확히 세 곳이 브래킷을 열고 닫으며, 모두 start 하나와 end 하나를 쌍으로 맞춘다:
xchksum_insert_repl_log_and_demote_table_lock(locator_sr.c) — 이 장의 DDL 경로를 감싸는 정식 구문 호출자.log_sysop_start→repl_start_flush_mark→xrepl_set_info(→repl_log_insert_statement) →repl_end_flush_mark (..., false)순서로 실행된다. 브래킷이 DDL 구문 레코드를LOG_REPL_NEED_FLUSH로 승격시켜, 감싸는 트랜잭션이 나중에 데이터 변경을 롤백하더라도 레플리카에 도달한다.error != NO_ERROR이면log_sysop_abort로 떨어진다.qexec_execute_selupd_list(query_executor.c) —lock_is_instant_lock_mode하에서 일괄 증분(batch of increments)을 처리하는 정식 row 호출자. 성공 시repl_end_flush_mark (..., false),exit_on_error시repl_end_flush_mark (..., true)를 호출하여 배치가 원자적으로 방출된다.serial_update_serial_object(serial.c) —lock_mode != X_LOCK인 경우에만 해당.X_LOCK(커밋되지 않은 create/alter)은 sysop와 플러시 마크를 의도적으로 건너뛴다. 마킹하면 복제 로그 순서가 어긋나기 때문이다.
세 곳 모두 !LOG_CHECK_LOG_APPLIER와 log_does_allow_replication () 뒤에 start/end 쌍을 배치한다 — applier는 재실행 중에 브래킷을 열지 않는다.
4.4 repl_log_abort_after_lsa — 부분 롤백 생존
섹션 제목: “4.4 repl_log_abort_after_lsa — 부분 롤백 생존”롤백 시에도 레코드를 플러시하도록 강제하는 것(§4.3)은 이야기의 절반에 불과하다. 부분적 롤백 — 시스템 오퍼레이션(sysop) 중단이나 세이브포인트 롤백 — 은 해당 시점 이후에 스테이징된 레코드를 폐기하되, 그 이전의 플러시 마크된 레코드는 살아남아야 한다. 그 폐기를 담당하는 것이 repl_log_abort_after_lsa다:
// repl_log_abort_after_lsa -- src/transaction/replication.crepl_rec_arr = tdes->repl_records;for (i = 0; i < tdes->cur_repl_record; i++) { if (LSA_GT (&repl_rec_arr[i].lsa, start_lsa)) /* <- staged strictly after the rollback point */ repl_rec_arr[i].must_flush = LOG_REPL_DONT_NEED_FLUSH; }return NO_ERROR;스테이징된 모든 레코드를 순회하며 lsa가 start_lsa보다 엄격히 큰 것에 LOG_REPL_DONT_NEED_FLUSH 툼스톤을 찍는다. cur_repl_record를 되감거나 repl_data를 해제하지는 않는다 — 레코드는 배열에 남지만 shipper가 건너뛴다. start_lsa 이하의 레코드는 플러시 상태(LOG_REPL_NEED_FLUSH 포함)를 유지하므로, 롤백 지점 이전의 플러시 마크된 작업은 살아남는다.
유일한 호출자는 log_sysop_abort(log_manager.c)로, applier와 비워커 트랜잭션에서는 실행되지 않도록 세 가지 가드를 배치한다:
// log_sysop_abort -- src/transaction/log_manager.cif (!LOG_CHECK_LOG_APPLIER (thread_p) /* <- not the applier replaying */ && tdes->is_active_worker_transaction () /* <- a real worker txn, not sysop-main */ && log_does_allow_replication () == true) /* <- HA replication enabled */ { repl_log_abort_after_lsa (tdes, LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes)); }이 코드는 log_sysop_abort의 LSA_ISNULL || LSA_LE 가드의 else 분기에 위치하므로, sysop가 실제로 부모 LSA를 넘어 로그를 기록했을 때만 실행된다. no-op sysop는 툼스톤 순회를 건너뛴다.
불변 조건 — LSA_GT는 엄격한 비교이므로 롤백 경계 레코드는 보존된다. lsa가 start_lsa와 같은 레코드는 툼스톤 처리되지 않는다(>, >= 아님). 경계 레코드는 살아남는 접두 구간에 속한다. 두 메커니즘은 직교하는 기준으로 조합된다 — 플러시 마크는 발송 시점의 플러시 상태로 보호하고, repl_log_abort_after_lsa는 플러시 상태와 무관하게 LSA 위치로 툼스톤을 찍는다. 롤백 지점 이전의 플러시 마크된 레코드는 NEED_FLUSH를 유지하고 발송된다. 롤백 지점 이후의 것은 폐기된다. 세이브포인트나 sysop 로직을 건드리는 독자는 이 조합을 보존해야 한다.
4.5 구문 결정론 — 미해결 문제에 대한 포인터
섹션 제목: “4.5 구문 결정론 — 미해결 문제에 대한 포인터”구문 경로는 SQL 텍스트를 스테이징하고 레플리카에서 재실행한다 — 간결하지만 비결정론적이다. NOW(), RAND(), 또는 시리얼에 의존하는 표현식이 포함된 구문은, 행 이미지가 아닌 구문이 재실행되므로 결과가 달라질 수 있다. CUBRID는 시리얼 비결정론을 완화하고(repl_log_insert 3장의 CT_SERIAL_NAME 분기) sys_prm_context를 전송하지만, 일반적인 재실행 비결정론은 아직 해결되지 않았다 — cubrid-ha-replication.md의 Open Question 8. 이 장이 추가하는 세부 사항은 어디서 드러나는가이다: 전송되는 필드는 stmt_text이며 row 이미지가 아니다.
4.6 챕터 요약 — 핵심 정리
섹션 제목: “4.6 챕터 요약 — 핵심 정리”-
구문 레코드는 별도의 진입점을 사용한다 —
do_replicate_statement→xrepl_set_info. 여기서는 오직REPL_INFO_TYPE_SBR만repl_log_insert_statement로 디스패치되며, 그 외는 모두ER_REPL_ERROR다. row 이벤트는 이 함수에 도달하지 않는다. -
REPL_INFO_SBR은 row가 아닌 텍스트를 발송한다. 다섯 개 필드가repl_data에 패킹되며, 스테이징된 레코드는LOG_REPLICATION_STATEMENT,rcvindex = RVREPL_STATEMENT이고inst_oid는 항상 NULL이다. -
must_flush는 세 값짜리 발송 게이트다 —DONT_NEED_FLUSH(-1)는 절대 발송하지 않고,COMMIT_NEED_FLUSH(0)는 커밋 시에만 발송하며(기본값),NEED_FLUSH(1)는 커밋 및 롤백 모두에서 발송한다. -
구문 플러시 마크 승격은 무조건적이다 —
repl_log_insert(inst_oid로 병합)와 달리,repl_log_insert_statement는 윈도우 내의 모든 레코드를NEED_FLUSH로 강제한다(“use with caution”). -
플러시 마크 브래킷은 단일, 재진입 불가 정수다.
repl_start_flush_mark는-1일 때만fl_mark_repl_recidx를 설정하며,repl_end_flush_mark는 윈도우를 유지(need_undo=false)하거나 해제 후 잘라낸다(need_undo=true). 세 호출자가 start와 end를 쌍으로 맞춘다 —xchksum_insert_repl_log_and_demote_table_lock(구문/DDL),qexec_execute_selupd_list(row 배치),serial.c업데이트 경로. -
repl_log_abort_after_lsa는 플러시 마크가 아닌 LSA 위치로 툼스톤을 찍는다 —log_sysop_abort에서lsa > start_lsa인 모든 레코드를DONT_NEED_FLUSH로 강등한다(엄격 비교이므로 경계는 살아남음). 부분 롤백이 되돌린 작업에 대해서는 플러시 마크를 덮어쓴다. -
구문 결정론이 미해결 접합부다 —
stmt_text가 레플리카에서 재실행되므로, 비결정론적 함수는 결과가 달라진다(Open Question 8).
Chapter 5: 커밋 시점 플러시와 원자적 방출
섹션 제목: “Chapter 5: 커밋 시점 플러시와 원자적 방출”Chapter 4가 끝날 시점에 트랜잭션 디스크립터는 tdes->repl_records[0 .. cur_repl_record-1]
스테이징 배열을 완전히 채운 상태다. 각 원소는 LOG_REPL_RECORD로, repl_type,
rcvindex, 역산출된 lsa, 패킹된 repl_data, must_flush 플래그를
담고 있으나 아직 WAL 스트림에는 기록되지 않았다. 이 챕터는 커밋 시점의
핵심 질문에 답한다: 스테이징된 레코드들이 어떻게 실제
LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT WAL 레코드로
전환되는가, 그리고 피어 트랜잭션 T’의 커밋 레코드가 트랜잭션 T의 복제
레코드와 T의 커밋 레코드 사이에 끼어들지 않도록 보장하는 것은 무엇인가?
동반 고수준 문서(cubrid-ha-replication.md, “Commit-time emission —
log_append_repl_info_and_commit_log” 절)는 원자성 계약을 명시한다. 이
챕터는 그것을 구현하는 다섯 함수를 분기별로 해부하고, 슬레이브의 재시작
로직이 의존하는 불변 조건을 명명한다. 이 노드들을 로그 페이지 버퍼에 쓰는
prior-list 드레인은 여기서 재도출하지 않는다 — cubrid-log-manager-detail.md,
“Prior-LSA list and the append pipeline” 절을 참조하라.
트랜잭션 종료 결과는 세 가지로 범위가 한정된다: 커밋하는 HA 트랜잭션은 스테이징된 레코드와 커밋 레코드를 하나의 mutex 아래 방출한다(§5.2); 일반 커밋(가드 조건 중 하나라도 거짓)은 커밋 레코드만 방출하고 스테이징 레코드를 조용히 버린다(§5.1); 중단(abort)하는 트랜잭션은 복제 레코드를 전혀 방출하지 않는다 — abort는 스테이징 배열을 전혀 순회하지 않으므로, 레코드들은 트랜잭션 종료 정리 시 디스크립터 리셋에 의해 폐기된다(§5.1, abort 주석).
5.1 log_commit_local에서의 커밋 결정
섹션 제목: “5.1 log_commit_local에서의 커밋 결정”log_commit(공개 진입점)은 2PC 부기 작업을 수행한 뒤, 로컬 또는
참여자 트랜잭션에 log_commit_local을 호출한다. 복제 분기는
log_commit_local의 말미, “트랜잭션이 데이터를 갱신한” 분기
(is_local_tran) 안에 존재한다:
// log_commit_local -- src/transaction/log_manager.cif (is_local_tran) { LOG_LSA commit_lsa; // ... condensed ... if (!LOG_CHECK_LOG_APPLIER (thread_p) && tdes->is_active_worker_transaction () && log_does_allow_replication () == true) log_append_repl_info_and_commit_log (thread_p, tdes, &commit_lsa); /* <- atomic path */ else log_append_commit_log (thread_p, tdes, &commit_lsa); /* <- plain commit, no repl */ if (retain_lock != true) lock_unlock_all (thread_p); /* <- AFTER the commit log is appended; see ordering note */ log_change_tran_as_completed (thread_p, tdes, LOG_COMMIT, &commit_lsa); }가드의 각 분기는 모두 의미를 가진다:
| 분기 | 조건 | 결과 |
|---|---|---|
| 원자적 복제 경로 | 복제 허용 and 활성 워커 트랜잭션 and 호출자가 log applier가 아님 | log_append_repl_info_and_commit_log — 하나의 mutex 아래 복제 레코드 + 커밋 |
| 일반 커밋 | 가드 조건 중 하나라도 거짓 | log_append_commit_log — 커밋 레코드만, 스테이징된 복제 레코드는 방출되지 않음 |
| 갱신 없음 분기 | LSA_ISNULL (&tdes->tail_lsa) (함수 상단) | 둘 다 호출되지 않음; 상태가 바로 TRAN_UNACTIVE_COMMITTED로 전환 |
LOG_CHECK_LOG_APPLIER는 슬레이브 자체의 apply 연결을 배제한다(자신의
복제를 재방출해서는 안 된다); is_active_worker_transaction()은 사용자
가시적 DML이 아닌 시스템 워커 트랜잭션(vacuum, checkpoint)을 배제한다;
log_does_allow_replication()(log_comm.c)은 SA_MODE/WINDOWS 환경,
log-copier/applier 클라이언트일 때 거짓을 반환하며, 그 외에는 HA 서버 상태를
조회한다. 조건 중 하나라도 거짓이면 스테이징된 레코드는 디스크립터와
함께 폐기된다.
동일한 방출자에 도달하는 두 번째 경로도 존재한다. log_complete(비-2PC
완료)는 일반 커밋만 추가한다; log_complete_for_2pc(2PC 경로)는
!LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true
라는 두 조건 변형으로 복제+커밋 방출자를 호출한다 — 2PC 트랜잭션은
구조적으로 활성 워커이므로 is_active_worker_transaction() 조건은 여기서
중복이다. 이 챕터의 나머지 부분은 두 경로를 하나로 취급한다.
순서 주석. 복제+커밋 추가는 lock_unlock_all보다 앞서 발생한다
(소스 블록 주석이 이유를 설명한다): T1이 대기 중인 T2를 재개시켰고, T2가
커밋했으며, T2의 커밋 이후 T1의 커밋 이전에 크래시가 발생한다면, 순서가
뒤바뀐 unlock 로그로 인해 복구는 T1을 중단시키면서 T2는 이미 커밋된 상태가
된다. 커밋 로그를 먼저 기록함으로써 그 취약 구간을 닫는다.
Abort는 방출하지 않음으로써 폐기한다. 대응하는 abort 함수
log_abort_local에는 복제 분기가 전혀 없다: log_rollback을 호출하고,
lock_unlock_all을 호출하며, TRAN_UNACTIVE_ABORTED를 반환한다 —
log_append_repl_info*를 전혀 호출하지 않는다. 스테이징된
tdes->repl_records는 순전히 방출되지 않음으로써 폐기된다: 아무것도
방출하지 않고, 트랜잭션 종료 정리 시 logtb_clear_tdes가 스테이징 배열을
리셋한다(cur_repl_record = 0, append_repl_recidx = -1,
fl_mark_repl_recidx = -1). abort 시점에 게이트를 순회하며 레코드를
건너뛰는 처리는 없다; 게이트(§5.4.2)는 커밋 경로와 system-op 경로에서만
진입된다.
5.2 원자성 관용구 — log_append_repl_info_and_commit_log
섹션 제목: “5.2 원자성 관용구 — log_append_repl_info_and_commit_log”// log_append_repl_info_and_commit_log -- src/transaction/log_manager.cstatic voidlog_append_repl_info_and_commit_log (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ if (tdes->has_supplemental_log) /* <- SUPPLEMENTAL_LOG > 0 set a user-name marker */ { 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 (); /* <- single hold spans BOTH appends */ log_append_repl_info_with_lock (thread_p, tdes, true); /* is_commit = true */ log_append_commit_log_with_lock (thread_p, tdes, commit_lsa); /* LOG_COMMIT donetime */ log_Gl.prior_info.prior_lsa_mutex.unlock ();}불변 조건 5-A — T의 복제 레코드와 T의 커밋 사이의 엄격한 인접성. 트랜잭션 T의 마지막
LOG_REPLICATION_*레코드와 T의LOG_COMMIT레코드 사이에는 다른 어떤 트랜잭션의 커밋 레코드도 나타날 수 없다. 보장 방법:log_append_repl_info_with_lock과log_append_commit_log_with_lock양쪽에 걸쳐log_Gl.prior_info.prior_lsa_mutex를 유지한다 — 다른 모든 커밋 주체도 자신의 커밋 donetime을 추가하려면 동일한 mutex를 취득해야 하므로, 어떤 피어 커밋도 사이에 끼어들 수 없다. 위반 시: 두 추가 사이에 피어 커밋이 삽입되어[T repl...][T' commit][T commit]형태가 생긴다; 내구적 북마크(Chapter 10)가[T repl...]와[T' commit]사이를 가리키고 있다가 크래시 후 재시작한 슬레이브(applylogdb)는 T의 커밋을 순서 밖에서 읽고, 플러시를 잘못 귀속하거나 순서를 잘못 처리하여 T의 행 변경을 조용히 누락할 수 있다. 문서 주석이 이를 명확히 명시한다: “Atomic write of replication log and commit log is crucial for replication consistencies.”
비-HA 경로는 다르다: log_append_commit_log(§5.5)는 명시적 mutex를 전혀
취득하지 않는다 — log_append_donetime_internal이 LOG_PRIOR_LSA_WITHOUT_LOCK으로
실행되며 단일 커밋 추가를 위해 내부적으로 mutex를 취득한다. HA 경로만이
두 번의 추가에 걸쳐 하나의 lock을 유지하므로, HA 경로만이 호출자 측에서
mutex를 취득한다.
has_supplemental_log 접두 처리. PRM_ID_SUPPLEMENTAL_LOG > 0이고
tdes->has_supplemental_log가 설정된 경우, 커밋 중인 DB 사용자 이름을
담은 LOG_SUPPLEMENT_TRAN_USER 하위 타입의 LOG_SUPPLEMENTAL_INFO
레코드가 mutex 밖에서 먼저 추가되고 플래그가 초기화된다. 이는 커밋당 최대
한 번만 방출된다. CDC / supplemental 소비자에게 트랜잭션의 사용자 신원을
제공하기 위한 것으로, HA 원자성 보장과는 무관하며 의도적으로 lock 취득
이전에 위치한다.
5.3 log_append_repl_info와 log_append_repl_info_with_lock
섹션 제목: “5.3 log_append_repl_info와 log_append_repl_info_with_lock”두 함수는 공유 내부 함수의 one-line 디스패처로, with_lock 인수의 전달
방식만 다르다:
// log_append_repl_info / log_append_repl_info_with_lock -- src/transaction/log_manager.cvoid log_append_repl_info (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit){ log_append_repl_info_internal (thread_p, tdes, is_commit, LOG_PRIOR_LSA_WITHOUT_LOCK); }
static void log_append_repl_info_with_lock (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit){ log_append_repl_info_internal (thread_p, tdes, is_commit, LOG_PRIOR_LSA_WITH_LOCK); }with_lock은 prior_lsa_next_record_with_lock(호출자가 이미 mutex를 보유)
또는 prior_lsa_next_record(노드당 mutex를 취득)를 선택한다.
두 호출자의 목적은 서로 다르다:
| 호출자 | is_commit | with_lock | 목적 |
|---|---|---|---|
log_append_repl_info_and_commit_log → _with_lock | true | WITH_LOCK | 커밋 시점 플러시; 보유 중인 mutex 아래 아직 플러시되지 않은 모든 레코드를 방출 (원자적 경로) |
log_sysop_commit_internal → log_append_repl_info | false | WITHOUT_LOCK | System-op 커밋, §5.1과 동일한 세 조건 가드 아래; 트랜잭션 중간에 LOG_REPL_NEED_FLUSH 레코드만 방출 (DDL flush 마킹), 각각 자체적으로 mutex를 취득 |
system-op 호출자는 Chapter 4의 flush-mark 메커니즘이다: DDL 서브-오퍼레이션은
사용자 커밋까지 지연하지 않고 system-op 경계에서 복제되며, 해당 레코드에
LOG_REPL_NEED_FLUSH를 표시하고 log_sysop_commit_internal이 is_commit = false로
조기 플러시한다. is_commit 플래그는 “지금 NEED_FLUSH 레코드만 플러시”와
“커밋 시점에 아직 남은 모든 것을 플러시”를 구분한다; §5.4에서 게이트가
이를 어떻게 사용하는지 보여준다.
5.4 log_append_repl_info_internal — 분기별 해부
섹션 제목: “5.4 log_append_repl_info_internal — 분기별 해부”핵심 함수다: 커서부터 스테이징 배열을 순회하며 조건을 충족하는 레코드를 prior-list 노드로 변환한다.
// log_append_repl_info_internal -- src/transaction/log_manager.cstatic voidlog_append_repl_info_internal (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit, int with_lock){ LOG_REPL_RECORD *repl_rec; LOG_REC_REPLICATION *log; LOG_PRIOR_NODE *node;
if (tdes->append_repl_recidx == -1 || is_commit) /* first time, OR commit rescans from start */ tdes->append_repl_recidx = 0;
while (tdes->append_repl_recidx < tdes->cur_repl_record) { repl_rec = (LOG_REPL_RECORD *) (&(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)) /* <- the gate, §5.4.2 */ { 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); if (node == NULL) { assert (false); continue; } /* <- does NOT advance recidx */
if (repl_rec->tde_encrypted) if (prior_set_tde_encrypted (node, repl_rec->rcvindex) != NO_ERROR) { assert (false); continue; } /* <- same non-advancing continue */
log = (LOG_REC_REPLICATION *) node->data_header; if (repl_rec->rcvindex == RVREPL_DATA_DELETE || repl_rec->rcvindex == RVREPL_STATEMENT) LSA_SET_NULL (&log->lsa); /* <- DELETE/STATEMENT carry no target LSA */ else LSA_COPY (&log->lsa, &repl_rec->lsa); /* <- INSERT/UPDATE copy the heap LSA through */ log->length = repl_rec->length; log->rcvindex = repl_rec->rcvindex;
// ... with_lock ? prior_lsa_next_record_with_lock : prior_lsa_next_record (both arms in §5.3) ...
repl_rec->must_flush = LOG_REPL_DONT_NEED_FLUSH; /* <- §5.4.4 re-emit guard */ } tdes->append_repl_recidx++; }}5.4.1 커서 리셋
섹션 제목: “5.4.1 커서 리셋”append_repl_recidx는 재개 커서로, 두 경우에 진입 시 0으로 설정된다:
최초 진입 (== -1, 새로 초기화된 디스크립터)이거나 is_commit이
true일 때 — 커밋은 배열 전체를 처음부터 재스캔한다. 트랜잭션 중간
flush-mark 패스(§5.3)가 커서를 이미 검사한 레코드들 너머로 전진시켰기
때문에, 커밋은 해당 패스가 건너뛴 COMMIT_NEED_FLUSH 레코드들을 잡아내기
위해 모든 레코드를 재검사해야 한다. is_commit이 false이고 커서가 이미
>= 0이면, 비-커밋 패스는 중단된 지점부터 재개하여 마지막 패스 이후
추가된 레코드만 스캔한다.
5.4.2 must_flush 게이트 — 모든 조합
섹션 제목: “5.4.2 must_flush 게이트 — 모든 조합”if 조건은 타입이 복제 타입이고 그리고 플러시 조건이 성립할 때
레코드를 통과시킨다. LOG_REPL_FLUSH 세 값(replication.h)에 대한 조합은
다음과 같다:
must_flush | 값 | 커밋 패스 (is_commit=true) | flush-mark 패스 (is_commit=false) |
|---|---|---|---|
LOG_REPL_DONT_NEED_FLUSH | -1 | 건너뜀 (!= DONT_NEED_FLUSH 거짓) | 건너뜀 (!= NEED_FLUSH) |
LOG_REPL_COMMIT_NEED_FLUSH | 0 | 방출 (!= DONT_NEED_FLUSH 참) | 건너뜀 (!= NEED_FLUSH) |
LOG_REPL_NEED_FLUSH | 1 | 방출 | 방출 (== NEED_FLUSH) |
외부 타입 검사(LOG_REPLICATION_DATA || LOG_REPLICATION_STATEMENT)는
이중 안전장치다 — 스테이징 배열은 오직 이 두 타입만을 담는다.
5.4.3 LOG_REC_REPLICATION 노드 구성 — lsa null 규칙
섹션 제목: “5.4.3 LOG_REC_REPLICATION 노드 구성 — lsa null 규칙”prior_lsa_alloc_and_copy_data는 data_header가 LOG_REC_REPLICATION
(Chapter 1에서 다룬 {lsa, length, rcvindex} 3-필드 구조체)이고 본문이
repl_rec->repl_data(length 바이트 — 패킹된
pkey_size | class_name | pkey_dbvalue)를 복사한 prior 노드를 할당한다.
length와 rcvindex는 스테이징 레코드에서 직접 채워지고, lsa는
rcvindex에 따라 설정된다:
rcvindex | 헤더 lsa | 이유 |
|---|---|---|
RVREPL_DATA_DELETE | LSA_SET_NULL | 슬레이브는 위치 지정 및 삭제에 primary key만 필요; after-image 없음 |
RVREPL_STATEMENT | LSA_SET_NULL | 문장 텍스트 자체가 페이로드; 가리킬 힙 레코드가 없음 |
RVREPL_DATA_INSERT | repl_rec->lsa에서 LSA_COPY | 힙 insert 로그를 가리킴; 슬레이브가 삽입된 행 이미지를 fetch |
RVREPL_DATA_UPDATE | LSA_COPY | repl_add_update_lsa(Chapter 3)에서 역산출; 힙 update 로그를 가리킴 |
RVREPL_DATA_UPDATE_START / UPDATE_END | LSA_COPY | UPDATE와 동일; START/END 쌍이 다중 행 update를 괄호로 묶음 |
여기서 복사되는 값들은 스테이징 측(Chapter 3)에서 설정된 것이다.
5.4.4 TDE 암호화와 방출 후 flush-flag 뒤집기
섹션 제목: “5.4.4 TDE 암호화와 방출 후 flush-flag 뒤집기”레코드별 처리를 완성하는 두 가지 부대 조건이 있다:
tde_encrypted. 스테이징된 레코드가 TDE 암호화 클래스에서 온 경우 (스테이징 시repl_rec->tde_encrypted설정 — Chapter 3),prior_set_tde_encrypted(log_append.cpp)가tde_is_loaded()를 확인한다; 마스터 키가 로드되지 않은 경우ER_TDE_CIPHER_IS_NOT_LOADED를 발생시키고 레코드는assert(false); continue;분기로 건너뛴다 (커서를 전진시키지 않음 — 불변 조건 5-B 및 Figure 5-1 참조), 마스터 키가 로드된 경우node->tde_encrypted = true로 설정하여 포함 페이지가 rest 상태에서 암호화되도록 한다. Chapter 11이 apply 측을 다룬다.- 방출 성공 후
must_flush = LOG_REPL_DONT_NEED_FLUSH— 재방출 방지 가드: 레코드가 prior 리스트에 들어가면DONT_NEED_FLUSH로 뒤집어 이후 커밋 패스 재스캔(§5.4.1)이 건너뛰게 만든다. 게이트가DONT_NEED_FLUSH를 배제하기 때문이다. 트랜잭션 중간 flush-mark 패스 (§5.3)가 이미NEED_FLUSH레코드를 방출한 경우, 인덱스 0부터 재스캔하는 커밋 재스캔이 같은 레코드를 두 번 방출하지 않도록 하는 것이 핵심이다. Abort는 이 루프에 전혀 진입하지 않으므로(§5.1), 이 가드는 순전히 커밋 대 flush-mark 간의 중복 제거 장치이지 abort 필터가 아니다.
불변 조건 5-B — 스테이징된 레코드는 최대 한 번만 방출된다. 보장 방법: 노드가
prior_lsa_next_record*에 전달된 직후must_flush를DONT_NEED_FLUSH로 뒤집는다. 위반 시: 조기 flush-mark 패스가 방출한 레코드가 (인덱스 0에서 재시작하는, §5.4.1) 커밋 재스캔에서 다시 방출되어, 하나의 논리적 변경에LOG_REPLICATION_*레코드가 두 개 생성되고 슬레이브가 같은 INSERT/UPDATE를 두 번 적용하게 된다. 이 가드는 커밋 대 flush-mark 중첩만을 관장한다; abort 경로는 아무것도 방출하지 않으므로(§5.1) 이런 필터가 필요 없다. 두 에러 분기(node == NULL,prior_set_tde_encrypted != NO_ERROR)는append_repl_recidx를 전진시키지 않고continue한다; 양쪽 모두 “불가능한” 상태(할당 실패, TDE 키 누락)에 대한assert(false)막다른 길로, 정상적인 복구 경로가 아니다 — 수정자는 성공 경로의 커서 전진을 반드시 보존해야 한다.
5.4.5 커밋 시점 패스 한 번의 흐름
섹션 제목: “5.4.5 커밋 시점 패스 한 번의 흐름”루프의 각 반복은 4단계 결정 연쇄다(위의 실제 코드가 동일한 분기를 인라인으로 담는다):
flowchart TD
C{"recidx < cur_repl_record ?"}
C -- 아니오 --> Z["반환"]
C -- 예 --> D{"REPL 타입이고 게이트 통과 ?"}
D -- 아니오 --> INC["recidx++"] --> C
D -- 예 --> G{"alloc NULL, 또는 tde 설정 실패 ?"}
G -- 예 --> C2["continue, recidx++ 없음"] --> C
G -- 아니오 --> K{"rcvindex DELETE/STATEMENT ?"}
K -- 예 --> L["lsa = NULL"] --> N
K -- 아니오 --> M["lsa = repl_rec->lsa"] --> N
N["next_record, 이후 must_flush = DONT_NEED_FLUSH"] --> INC
Figure 5-1 — log_append_repl_info_internal 루프 본문: 게이트, 커서를
전진시키지 않는 에러 continue, 그리고 lsa-null 분기.
5.5 커밋 donetime — log_append_commit_log_with_lock
섹션 제목: “5.5 커밋 donetime — log_append_commit_log_with_lock”보유 중인 mutex 아래의 두 번째 추가는 커밋 레코드 자체다:
// log_append_commit_log_with_lock / log_append_commit_log -- src/transaction/log_manager.cstatic void log_append_commit_log_with_lock (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ log_append_donetime_internal (thread_p, tdes, commit_lsa, LOG_COMMIT, LOG_PRIOR_LSA_WITH_LOCK); }
static void log_append_commit_log (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ if (tdes->has_supplemental_log) /* <- non-HA path emits the user-name marker here (cf. §5.2) */ { /* ... condensed: same log_append_supplemental_info + clear flag as §5.2 ... */ } log_append_donetime_internal (thread_p, tdes, commit_lsa, LOG_COMMIT, LOG_PRIOR_LSA_WITHOUT_LOCK);}log_append_donetime_internal은 LOG_COMMIT 노드를 할당하고
donetime->at_time = time (NULL)을 스탬핑한다. HA 호출자가 WITH_LOCK을
전달했기 때문에 mutex를 재취득하지 않고 prior_lsa_next_record_with_lock을
호출한다 — 방출자가 두 추가에 걸쳐 하나의 lock을 유지하는 이유가 바로 이것이다.
할당된 LSA는 commit_lsa로 반환되어 log_commit_local에서
log_change_tran_as_completed로 흘러간다(logpb_flush_pages를 구동하고
TRAN_UNACTIVE_COMMITTED로 전환). prior 리스트는 이제 엄격한 순서로
[복제 레코드 0 .. n] 다음에 [LOG_COMMIT]을 담고 있다; 이것이 로그
페이지 버퍼로 드레인되는 과정은 cubrid-log-manager.md,
“Prior-LSA list and the append pipeline” 절에 상세히 설명된다.
5.6 챕터 요약 — 핵심 정리
섹션 제목: “5.6 챕터 요약 — 핵심 정리”log_commit_local은 세 조건으로 분기한다 (!LOG_CHECK_LOG_APPLIER,is_active_worker_transaction,log_does_allow_replication); 셋 모두 성립할 때만 원자적 방출자를 호출하고, 그렇지 않으면 복제 없이log_append_commit_log만 호출한다. 2PC 경로(log_complete_for_2pc)는 중복인is_active_worker_transaction()검사를 생략한 두 조건 변형을 사용한다.- 원자성은 두 추가에 걸친 하나의 mutex다.
log_append_repl_info_and_commit_log는log_append_repl_info_with_lock과log_append_commit_log_with_lock양쪽에 걸쳐log_Gl.prior_info.prior_lsa_mutex를 유지한다(불변 조건 5-A); 이를 분리하면[T repl...][T' commit][T commit]이 출현하여 슬레이브가 재시작 시 T의 변경을 누락하거나 순서를 잘못 처리할 수 있다. must_flush게이트는is_commit을 인식한다. 커밋 시에는!= DONT_NEED_FLUSH인 모든 레코드가 방출된다(COMMIT_NEED_FLUSHDML 행과 미처리NEED_FLUSH모두); flush-mark 패스(is_commit=false)는NEED_FLUSHDDL 레코드만 방출한다.- lsa-null은
rcvindex별로 다르다. DELETE와 STATEMENT는 헤더lsa를 null로 설정하고; INSERT와 세 UPDATE 변형은 역산출된 힙 LSA를 복사하여 슬레이브가 after-image를 fetch할 수 있게 한다. - 각 레코드는 최대 한 번만 방출된다 (불변 조건 5-B): 노드가 추가된 후
must_flush가DONT_NEED_FLUSH로 뒤집혀, (인덱스 0에서 재시작하는) 커밋 재스캔이 조기 flush-mark 패스가 이미 기록한 레코드를 재방출하지 않는다. Abort는 아무것도 방출하지 않는다 —log_abort_local에는 복제 분기가 없으며; 스테이징된 레코드는 게이트에 의해서가 아니라logtb_clear_tdes디스크립터 리셋에 의해 폐기된다. 두 에러continue분기는 커서를 전진시키지 않는assert(false)막다른 길이다. log_append_repl_info(WITHOUT_LOCK,is_commit=false)와log_append_repl_info_with_lock(WITH_LOCK,is_commit=true) 는 하나의 내부 함수의 두 얼굴이다: system-op flush-mark 호출자와 커밋-원자적 호출자가 각각 사용한다.
Chapter 6: copylogdb로 로그 페이지 전송하기
섹션 제목: “Chapter 6: copylogdb로 로그 페이지 전송하기”슬레이브 측 copylogdb 데몬은 파싱된 레코드가 아니라 원시 로그 페이지를 가져와서
슬레이브 로컬 active-log 볼륨에 기록한다. 결과적으로 마스터의 active log에 거의 근접한
복사본이 유지된다. 동반 문서 Slave side — copylogdb에서는
puller가 존재하는 이유와 페이지를 복사하는 이유를 다뤘다. 이 챕터는 어떻게 동작하는지를
추적한다 — 모든 구조체 필드, fetch 루프의 모든 분기, 물리 페이지 wraparound 수식, 그리고
페이지가 존재할 때까지 block하는 마스터 측 응답자까지.
두 절반은 log_writer.c 안에서 서로 반대되는 컴파일 가드로 구분된다. 클라이언트
절반 (#if defined(CS_MODE))은 슬레이브의 copylogdb 프로세스이고, 서버 절반
(SERVER_MODE)은 마스터 cub_server의 xlogwr_get_log_pages다. 두 절반은
NET_SERVER_LOGWR_GET_LOG_PAGES 프로토콜로 통신한다.
6.1 두 가지 전역 역할 — LOGWR_GLOBAL vs LOGWR_INFO
섹션 제목: “6.1 두 가지 전역 역할 — LOGWR_GLOBAL vs LOGWR_INFO”log_writer.h는 컴파일 가드에 따라 선택되는 두 가지 서로소(disjoint) 구조체 집합을 정의한다.
graph TB
subgraph CS["CS_MODE (copylogdb process, the slave puller)"]
G["LOGWR_GLOBAL logwr_Gl<br/>프로세스 전역 싱글턴"]
C["LOGWR_CONTEXT ctx<br/>복사 세션 하나당 하나"]
G -.active-log fd, 버퍼 소유.-> C
end
subgraph SV["SERVER_MODE (inside master cub_server)"]
I["LOGWR_INFO log_Gl.writer_info<br/>마스터 측 코디네이터"]
E["LOGWR_ENTRY<br/>연결된 슬레이브 하나당 하나"]
I -->|writer_list| E
end
C -.NET_SERVER_LOGWR_GET_LOG_PAGES.-> E
Figure 6-1. 클라이언트 구조체(LOGWR_GLOBAL, LOGWR_CONTEXT)는 슬레이브 puller에,
서버 구조체(LOGWR_INFO, LOGWR_ENTRY)는 마스터에 존재한다. 두 구조체 집합은 같은
바이너리에 절대 공존하지 않는다.
LOGWR_MODE는 양쪽이 공유하는 유일한 타입이며 wire로 전달된다:
| 열거자 | 값 | 의미 |
|---|---|---|
LOGWR_MODE_ASYNC | 1 | 페이지가 준비되는 즉시 마스터가 응답한다. 마스터 flush를 차단하지 않는다. |
LOGWR_MODE_SEMISYNC | 2 | 마스터 flush는 슬레이브가 fetch할 때까지 대기한다(apply 아님). |
LOGWR_MODE_SYNC | 3 | 마스터 flush는 슬레이브 fetch 왕복이 완료될 때까지 대기한다. |
정확한 대기 의미론은 동반 문서의 Open Question에서 다루며,
§6.8에서 메커니즘(thread_suspend_with_other_mutex)을 설명한다. 비트
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK(0x80000000)는 새로운 copylogdb -f 실행 시
wire mode에 OR되어 “가장 오래된 물리 페이지부터 복사”를 지시한다(§6.8).
LOGWR_GLOBAL — 슬레이브 puller의 전체 상태
섹션 제목: “LOGWR_GLOBAL — 슬레이브 puller의 전체 상태”logwr_Gl은 하나의 정적 인스턴스다. 필드를 그룹별로 정리하면 다음과 같다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
hdr | 슬레이브가 갖고 있는 마스터 LOG_HEADER의 작업 복사본. | 물리 페이지 수식 전체(fpageid, npages, nxarv_*)를 구동한다. 응답 page 0에서 매번 갱신된다. |
loghdr_pgptr | 헤더 슬롯의 logpg_area 내 포인터. | logwr_flush_header_page가 복사 없이 헤더를 쓴다. |
db_name / hostname | @host를 분리한 DB 이름(hostname은 @ 이후를 가리킨다). | 볼륨 이름 생성 및 마스터 식별에 사용된다. logwr_initialize에서 분리된다. |
log_path / loginf_path / active_name | 슬레이브 로그 디렉토리, loginfo 경로, active-log 경로. | 볼륨이 저장되는 위치. active_name은 logwr_writev_append_pages가 쓰는 대상이다. |
append_vdes | 슬레이브 active log의 fd(또는 NULL_VOLDES). | 유일하게 변경 가능한 출력 핸들. |
logpg_area / logpg_area_size / logpg_fill_size | (NPAGES+1)*LOG_PAGESIZE 수신 버퍼, 크기, 수신된 바이트 수. | 응답 하나를 담는다(page 0 = 헤더, 1..N = 데이터). logwr_set_hdr_and_flush_info가 [0,fill_size) 구간을 순회한다. |
toflush / max_toflush / num_toflush | LOG_PAGE* 데이터 포인터들, 용량(= NPAGES), 유효한 수. | flush 작업 목록(§6.5). num_toflush가 루프 상한이다. |
mode | 현재 LOGWR_MODE. | 요청마다 저장·복원되며, catchup 중에는 ASYNC로 다운그레이드된다. |
action | LOGWR_ACTION_* 비트마스크. | DELAYED_WRITE / ASYNC_WRITE / HDR_WRITE / ARCHIVING을 루프 턴 사이에 전달한다. |
last_recv_pageid | 수신된 가장 높은 논리 pageid. | 커서. 다음 요청의 first_pageid_torecv가 여기서 파생된다(§6.3). |
last_arv_fpageid / last_arv_lpageid / last_arv_num | 조립 중인 아카이브의 첫/마지막 pageid와 번호. | logwr_archive_active_log의 복사 범위와 파일 이름을 결정한다. |
force_flush / last_flush_time | SEMISYNC flush를 타이머와 무관하게 강제; 마지막 flush 타임스탬프. | SEMISYNC는 약 1초에 한 번씩 throttle한다. force_flush(종료/크래시)는 이를 우회한다. |
bg_archive_info / bg_archive_name | bg-archive 스크래치 상태와 임시 경로. | LOG_BACKGROUND_ARCHIVING 활성 시 이중 쓰기. 임시 파일은 롤 시점에 실제 아카이브 이름으로 rename된다. |
ori_nxarv_pageid | 이번 응답 적용 전의 nxarv_pageid 스냅샷. | 아카이브 경계를 감지하여 요청 모드가 다운그레이드되지 않도록 한다(§6.3). |
start_pageid | 일반 시 -2; -f일 때 >= NULL_PAGEID. | 원샷 전체 복사와 데몬을 구별한다. |
reinit_copylog | 마스터 DB가 우리 모르게 재빌드됨. | logwr_reinit_copylog 호출을 트리거한다. |
(last_chkpt_pageid는 진단용으로만 보존되며, 코어 루프에서는 읽지 않는다.)
불변식 — last_recv_pageid는 단조 증가하며 다음 요청의 상한을 결정한다.
logwr_set_hdr_and_flush_info는 아직 수신할 페이지가 있을 때 마지막 수신 페이지로,
따라잡은 경우 hdr.eof_lsa.pageid로 설정한다. 다음 요청은 여기서 first_pageid_torecv를
파생한다(§6.3). logwr_get_log_pages는 last_recv_pageid <= hdr.eof_lsa.pageid를
assert한다 — EOF를 넘는 페이지를 요청하면 루프가 마스터의 poll-to-push에서 영구적으로
대기하게 된다(§6.8).
LOGWR_CONTEXT — 세션 단위 제어
섹션 제목: “LOGWR_CONTEXT — 세션 단위 제어”// struct logwr_context -- src/transaction/log_writer.hstruct logwr_context { int rc; int last_error; bool shutdown; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
rc | 네트워크 요청 핸들(> 0이면 열려 있음). | net_client_logwr_send_end_msg(ctx.rc,...) 호출로 서버 스레드를 정리한다. |
last_error | 마지막 에러 코드로, 마스터에게 echo된다. | 마스터가 writer 엔트리를 유지할지 결정한다. |
shutdown | 세션을 종료해야 함. | 루프를 탈출한다. 크래시, 치명적 에러, 또는 -f 완료 시 설정된다. |
LOGWR_ENTRY와 LOGWR_INFO — 마스터 측 대응 구조체
섹션 제목: “LOGWR_ENTRY와 LOGWR_INFO — 마스터 측 대응 구조체”연결된 슬레이브마다 writer_info->writer_list에 LOGWR_ENTRY 하나가 존재한다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
thread_p | 이 슬레이브를 담당하는 LOGWR 스레드. | suspend/resume의 대상이다. |
fpageid | 이 슬레이브가 요청하는 첫 번째 pageid. | logwr_pack_log_pages의 입력이다. |
mode | 슬레이브가 요청한 LOGWR_MODE. | flush 스레드의 대기 여부를 결정한다. |
status | LOGWR_STATUS_* (WAIT/FETCH/DONE/DELAY/ERROR). | Log Flush Thread와의 핸드셰이크 상태. |
eof_lsa | 이 엔트리를 위한 마스터 EOF 스냅샷. | 팩 범위를 제한한다. LOG_CS 하에서 설정된다. |
last_sent_eof_lsa | 이미 전송한 최고 EOF. | logwr_is_delayed가 지연 감지 시 비교한다. |
tmp_last_sent_eof_lsa | last_sent_eof_lsa의 스테이징 값. | 전송 왕복이 완료된 이후에만 커밋된다. |
start_copy_time | fetch 시작 시각. | 슬레이브가 flush 스레드를 얼마나 지연시켰는지 측정한다. |
copy_from_first_phy_page | -f 플래그. | 팩을 가장 오래된 페이지 분기로 라우팅한다. |
next | 리스트 연결. | writer_list는 단방향 연결 리스트다. |
LOGWR_INFO(log_Gl.writer_info)는 단일 Log Flush Thread(LFT)와 슬레이브별 LOGWR
스레드(LWT) 사이의 랑데부 지점이다. 모든 멤버를 정리하면:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
writer_list | LOGWR_ENTRY 리스트의 head. | LFT가 깨워야 할 슬레이브 집합이다. |
wr_list_mutex | register/unregister 시 writer_list 변경을 직렬화. | 모든 LWT와 LFT가 공유하는 리스트다. 아래 세 condvar 쌍과는 별개다. |
flush_start_cond / flush_start_mutex | LWT가 요청한 페이지를 기다리며 여기서 suspend. | poll-to-push 대기 지점(§6.8). LFT가 페이지 생성 후 신호를 보낸다. |
flush_wait_cond / flush_wait_mutex | LWT가 팩 이후 flush 완료를 기다리며 여기서 대기. | 팩된 LWT가 LFT의 디스크 flush 완료 후 진행하도록 순서를 맞춘다. |
flush_end_cond / flush_end_mutex | LFT가 LWT들의 전송 완료를 기다리며 여기서 대기. | LFT가 관심 있는 슬레이브들이 페이지를 소비할 때까지 블록할 수 있게 한다. |
skip_flush | LFT가 이번 사이클에서 writer 대기를 건너뜀. | 조율할 내용이 없을 때 불필요한 stall을 방지한다. |
flush_completed | LFT가 현재 flush를 완료함. | flush_wait_cond에서 깨어난 LWT가 검사하는 조건이다. |
is_init | writer_info가 초기화되었음. | 설정 전에 핸드셰이크가 실행되는 것을 막는다. |
trace_last_writer | 마지막 writer 타이밍 계측 활성화. | 지연 원인 분석을 위한 진단용. |
last_writer_client_info | 마지막으로 지연을 일으킨 writer의 CLIENTIDS. | 어느 슬레이브가 flush 지연을 유발했는지 식별한다. |
last_writer_elapsed_time | 해당 writer가 LFT를 대기시킨 시간. | HA 모니터링에 노출되는 측정된 지연 값. |
6.2 logwr_initialize — 슬레이브 로컬 active log 열기
섹션 제목: “6.2 logwr_initialize — 슬레이브 로컬 active log 열기”데몬 시작 시 한 번 실행된다:
// logwr_initialize -- src/transaction/log_writer.cstrncpy (logwr_Gl.db_name, db_name, PATH_MAX - 1);if ((at_char = strchr (logwr_Gl.db_name, '@')) != NULL) { *at_char = '\0'; logwr_Gl.hostname = at_char + 1; /* <- split db@host */}log_nbuffers = LOGWR_COPY_LOG_BUFFER_NPAGES + 1; /* <- 128 data + 1 header */LOGWR_COPY_LOG_BUFFER_NPAGES는 LOGPB_BUFFER_NPAGES_LOWER(128)로, 응답당 페이지
상한이다. 이후 분기별로 진행한다:
- 버퍼 할당 —
logpg_area == NULL이면log_nbuffers * LOG_PAGESIZE를 malloc한다. 실패 시logpg_area_size = 0으로 리셋하고ER_OUT_OF_VIRTUAL_MEMORY를 반환한다. toflush할당 — NULL이면max_toflush = log_nbuffers - 1개의 포인터를 calloc한다. 동일한 OOM 처리를 한다.logwr_read_log_header— 슬레이브 active log가 존재하면 마운트하고 page 0을 읽는다. 없으면 생성을 지연한다(마스터의npages가 먼저 필요하다). 에러는 전파한다.- “이미 존재” 가드 —
start_pageid >= NULL_PAGEID(즉-f)인데 해당 디렉토리에 이미 복제 로그가 있으면(hdr.nxarv_pageid != NULL_PAGEID)ER_HA_GENERIC_ERROR를 반환한다. - 아카이브 커서 초기화 —
last_arv_fpageid = hdr.nxarv_pageid,last_arv_num = hdr.nxarv_num;force_flush/last_flush_time/ori_nxarv_pageid를 리셋한다. - bg-archiving 분기 (
PRM_ID_LOG_BACKGROUND_ARCHIVING이 활성화된 경우에만) — 임시 볼륨을 마운트하고 헤더를 읽는다.ER_LOG_INCOMPATIBLE_DATABASE시 재생성하고, 없으면fileio_format으로 새로 만들고 헤더를 flush한다. 마운트/포맷 실패 시ER_IO_MOUNT_FAIL/ER_HA_GENERIC_ERROR를 반환한다.
일반적인 경우 active log는 여기서 생성되지 않는다. 생성은 첫 번째
logwr_write_log_pages(§6.5)로 미뤄지며, 그때 hdr.npages를 알게 된 후
fileio_format된다.
6.3 logwr_copy_log_file — 데몬 메인 루프
섹션 제목: “6.3 logwr_copy_log_file — 데몬 메인 루프”copylogdb의 생명주기. logwr_initialize 이후(초기화 실패 시 반환), 다음을 반복한다:
flowchart TD
init["logwr_initialize"] -->|ok| loop{shutdown<br/>또는 need_shutdown?}
init -->|err| fin["logwr_finalize; return"]
loop -->|no| get["logwr_get_log_pages(ctx)<br/>= 요청/응답 한 번"]
get -->|error| errb{reinit_copylog?}
errb -->|yes| reinit["logwr_reinit_copylog<br/>goto end"]
errb -->|no| clr["action &= DELAYED_WRITE"]
get -->|NO_ERROR| asy{action & ASYNC_WRITE?}
asy -->|yes| wlp["logwr_write_log_pages"]
asy -->|no| clr
wlp --> clr
clr --> loop
loop -->|yes| sig{need_shutdown<br/>and SEMISYNC?}
sig -->|yes| flush["force_flush=true<br/>logwr_write_log_pages"]
sig -->|no| endlbl["end:"]
flush --> endlbl
reinit --> endlbl
endlbl --> endmsg["if rc>0: net_client_logwr_send_end_msg"]
endmsg --> fin
Figure 6-2. logwr_copy_log_file 제어 흐름. 네트워크 왕복은 logwr_get_log_pages
안에 있다. 이 루프는 write-back, reinit, shutdown을 조율한다.
주요 분기: logwr_get_log_pages 에러 시 ctx.last_error를 기록하고,
ER_HA_LW_FAILED_GET_LOG_PAGE는 heartbeat에서 deregister한다(복구 불가). reinit_copylog가
설정되어 있으면 logwr_reinit_copylog를 실행하고 goto end. 성공 시
action & LOGWR_ACTION_ASYNC_WRITE가 있으면 즉시 logwr_write_log_pages를 호출한다
(SEMISYNC/SYNC 쓰기는 다른 경로). 매 턴, action &= LOGWR_ACTION_DELAYED_WRITE로
DELAYED_WRITE를 제외한 모든 비트를 제거한다. DELAYED_WRITE는 클라이언트가 남은 페이지를
계속 가져오게 하므로 반드시 유지되어야 한다. SEMISYNC로 신호 종료 시 force_flush를
설정하고 마지막 쓰기 후 ER_HA_LW_STOPPED_BY_SIGNAL을 반환한다. end: 에서
ctx.rc > 0이면 end 메시지를 전송한다. 완료된 -f 복사는 의도적으로 ER_FAILED를
보내 서버 스레드를 종료시키며, 항상 logwr_finalize한다.
logwr_get_log_pages(network_interface_cl.c)는 커서에서 요청을 파생한다:
// logwr_get_log_pages -- src/communication/network_interface_cl.cif (logwr_Gl.last_recv_pageid == logwr_Gl.hdr.eof_lsa.pageid) { /* caught up */ first_pageid_torecv = logwr_Gl.last_recv_pageid; mode = (logwr_Gl.last_recv_pageid == NULL_PAGEID) ? LOGWR_MODE_ASYNC : logwr_Gl.mode;} else { /* still draining */ if (logwr_Gl.last_recv_pageid == NULL_PAGEID) first_pageid_torecv = LOGPB_HEADER_PAGE_ID; /* <- prove DB identity */ else if (logwr_Gl.hdr.ha_file_status == LOG_HA_FILESTAT_SYNCHRONIZED) first_pageid_torecv = logwr_Gl.last_recv_pageid; /* re-fetch tail page */ else first_pageid_torecv = logwr_Gl.last_recv_pageid + 1; mode = (first_pageid_torecv == logwr_Gl.ori_nxarv_pageid) ? logwr_Gl.mode : LOGWR_MODE_ASYNC;}세 가지 pack된 필드: int64 첫 pageid(-f 마스크가 OR될 수 있음), int mode, int last_error.
드레이닝 중에는 마스터가 catch-up 중인 슬레이브 때문에 block하지 않도록 mode를 강제로
ASYNC로 설정한다. 아카이브 경계(first_pageid_torecv == ori_nxarv_pageid)에서만 설정된
mode를 유지한다. ER_NET_SERVER_CRASHED / ER_HA_LW_FAILED_GET_LOG_PAGE 시 SEMISYNC
슬레이브는 force-flush 후 ctx->shutdown을 설정한다. ER_NET_SERVER_CRASHED 분기는
추가로 로컬 헤더에 HA_SERVER_STATE_DEAD를 기록한 후 flush한다
(ER_HA_LW_FAILED_GET_LOG_PAGE 분기는 기록된 서버 상태를 그대로 둔다).
6.4 logwr_set_hdr_and_flush_info — 응답 데이터 정합
섹션 제목: “6.4 logwr_set_hdr_and_flush_info — 응답 데이터 정합”응답이 logpg_area에 도착하면, 이 함수는 toflush를 재구성하고 마스터 헤더를
logwr_Gl.hdr에 복사하고 아카이브 여부를 결정한 후 커서를 전진시킨다. 먼저 데이터
페이지를 순회한다(첫 LOG_PAGESIZE 헤더 슬롯 이후):
// logwr_set_hdr_and_flush_info -- src/transaction/log_writer.cp = logwr_Gl.logpg_area + LOG_PAGESIZE; /* <- skip header page */while (p < (logwr_Gl.logpg_area + logwr_Gl.logpg_fill_size)) { logwr_Gl.toflush[num_toflush++] = (LOG_PAGE *) p; p += LOG_PAGESIZE;}logwr_Gl.ori_nxarv_pageid = logwr_Gl.hdr.nxarv_pageid; /* <- snapshot BEFORE overwrite */분기 A — num_toflush > 0 (데이터 도착): wire page 0을 hdr에 복사한다. 아카이브
정보가 미설정 상태이면 초기값을 채운다. 두 가지 하위 케이스에서 LOGWR_ACTION_ARCHIVING을
설정한다 — 지연(last_arv_num+1 < hdr.nxarv_num이고 ha_file_status == ARCHIVED이고
last_arv_fpageid <= last_recv_pageid → last_arv_lpageid = last_recv_pageid) 또는
경계 도달(last_arv_num+1 == hdr.nxarv_num이고 마지막 페이지가
hdr.nxarv_pageid에 도달 → last_arv_lpageid = hdr.nxarv_pageid - 1). 이후 커서
전진: 마지막 페이지가 hdr.eof_lsa.pageid보다 낮으면 더 받을 것이 있다 —
last_recv_pageid = 마지막 페이지이고 DELAYED_WRITE를 설정한다. 다 받았으면
last_recv_pageid = hdr.eof_lsa.pageid이고 DELAYED_WRITE를 제거한다.
분기 B — num_toflush == 0 (헤더 전용 응답): 마스터에 새 내용이 없다.
재빌드 감지 — 서버 상태가 non-active이고 프로모션 시각이 일치하지만
db_restore_time이 다르면 마스터가 다른 이미지에서 복원된 것이다:
reinit_copylog = true를 설정하고 ER_LOG_DOESNT_CORRESPOND_TO_DATABASE를 반환한다.
db_creation 불일치도 동일한 에러를 반환한다(완전히 다른 DB). 커서 되감기로
tail을 재전송 강제: 로컬 상태가 SYNCHRONIZED가 아니면 last_recv_pageid를
hdr.append_lsa.pageid - 1로, SYNCHRONIZED이면 hdr.eof_lsa.pageid - 1로 설정한다.
꼬리 처리 (두 분기 공통): 로컬 헤더가 SYNCHRONIZED가 아니면, 로컬 append_lsa를
last_recv_pageid에 NULL_OFFSET으로 고정한다. 이는 슬레이브의 쓰기 가능 로그 끝
위치를 기록하여 재시작 시 그 지점부터 재개할 수 있게 한다(Chapter 10).
6.5 logwr_writev_append_pages와 logwr_flush_all_append_pages
섹션 제목: “6.5 logwr_writev_append_pages와 logwr_flush_all_append_pages”logwr_write_log_pages(호출자)는 active log가 없으면 hdr.npages + 1 크기로
fileio_format한 뒤, LOGWR_ACTION_ARCHIVING이 설정되어 있으면 flush 전에
logwr_archive_active_log를 호출한다(곧 덮일 페이지를 먼저 아카이브한다). 그다음
logwr_flush_all_append_pages, 그다음 logwr_flush_header_page를 실행한다.
SEMISYNC는 force_flush이거나 아카이브 중간이 아닌 한 약 1초 타이머로 throttle한다.
logwr_flush_all_append_pages는 toflush를 논리적으로도 물리적으로도 연속한 run으로
합쳐서 run마다 logwr_writev_append_pages를 한 번씩 호출한다:
// logwr_flush_all_append_pages -- src/transaction/log_writer.cif ((pageid != prv_pageid + 1) || (logwr_to_physical_pageid (pageid) != logwr_to_physical_pageid (prv_pageid) + 1)) { if (logwr_writev_append_pages (&logwr_Gl.toflush[idxflush], i - idxflush) == NULL) return er_errid (); /* not contiguous: flush the run, restart */ need_sync = true; flush_page_count += i - idxflush; idxflush = -1;}두 번째 조건이 원형 로그를 올바르게 처리하게 한다: 논리적으로 인접한 두 페이지가
물리 pageid가 1로 리셋되는 wraparound 경계에 걸쳐 있으면 두 번의 쓰기로 분리된다.
루프 이후 마지막 run을 flush하고, fileio_synchronize 한 번으로 모든 데이터를 영속화한다
(dual-write가 활성화되어 있으면 bg-archive 볼륨도 포함). 그 후 toflush를 초기화한다.
logwr_writev_append_pages는 fileio_write를 두 단계로 수행한다. bg-archive 쓰기
(if LOG_BACKGROUND_ARCHIVING) — bg_arv_info->vdes를 검증하고 logwr_copy_necessary_log로
빈 구간을 채운 뒤 각 페이지를 phy_pageid = fpageid - start_page_id + 1 위치에 쓴다.
active 쓰기 — phy_pageid = logwr_to_physical_pageid(fpageid)를 계산하고 각 페이지를
phy_pageid + i 위치에 쓴다. 두 경우 모두 ER_IO_WRITE_OUT_OF_SPACE를
ER_LOG_WRITE_OUT_OF_SPACE로 매핑하고 to_flush = NULL을 반환해 실패를 알린다.
첫 번째 데이터 페이지는 쓰기 전에 체크섬이 검증된다. #ifdef UNSTABLE_TDE_FOR_REPLICATION_LOG
블록은 TDE 마킹된 페이지를 재암호화하며 출하 빌드에서는 컴파일 제외된다 — Chapter 11의
TDE Open Question 참조.
불변식 — 기록되는 내용은 마스터와 바이트 단위로 동일하다. wire 페이지 영역은 그대로
복사된다. 슬레이브는 레코드를 재직렬화하지 않는다. 유일한 변경은 물리적 배치
(logwr_to_physical_pageid)와 선택적 TDE 재암호화뿐이다. 이것이 copylogdb가 레코드가
아닌 페이지를 복사하는 이유다.
6.6 logwr_to_physical_pageid — 원형 로그 주소 수식
섹션 제목: “6.6 logwr_to_physical_pageid — 원형 로그 주소 수식”active log는 hdr.npages 페이지의 고정 링이다. 단조 증가하는 논리 pageid는 물리 슬롯으로
매핑된다:
// logwr_to_physical_pageid -- src/transaction/log_writer.cif (logical_pageid == LOGPB_HEADER_PAGE_ID) /* header page is always slot 0 */ phy_pageid = 0;else { tmp_pageid = logical_pageid - logwr_Gl.hdr.fpageid; if (tmp_pageid >= logwr_Gl.hdr.npages) tmp_pageid %= logwr_Gl.hdr.npages; else if (tmp_pageid < 0) tmp_pageid = (logwr_Gl.hdr.npages - ((-tmp_pageid) % logwr_Gl.hdr.npages)); /* negative-safe */ tmp_pageid++; /* slot 0 reserved -> data starts at 1 */ if (tmp_pageid > logwr_Gl.hdr.npages) tmp_pageid %= logwr_Gl.hdr.npages; phy_pageid = (LOG_PHY_PAGEID) tmp_pageid;}fpageid(데이터 슬롯 1에 매핑되는 논리 pageid)를 빼고, npages로 모듈로 연산한다
(음수 케이스는 오래된 pageid를 올바르게 매핑한다). +1로 헤더 슬롯을 건너뛰고,
+1이 링을 넘어섰으면 다시 모듈로 연산한다. -f 복사는
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK를 사용하여 마스터가 오래된 fpageid를 신뢰하는
대신 가장 오래된 물리 페이지부터 다시 팩하게 한다.
6.7 logwr_archive_active_log — active log를 아카이브로 롤
섹션 제목: “6.7 logwr_archive_active_log — active log를 아카이브로 롤”logwr_set_hdr_and_flush_info가 아카이브를 봉인하기로 결정하면, flush 전에 이 함수가
실행된다: 아카이브 헤더 페이지 생성, 목적지 열기, 페이지 범위 복사, 커서 갱신. 각 단계는
실패 시 goto error로 떨어진다.
- 헤더 페이지 — 페이지 하나를 malloc한다.
LOG_ARV_HEADER를 채운다(db_creation,fpageid = last_arv_fpageid,arv_num = last_arv_num,npages = last_arv_lpageid - fpageid + 1). - 목적지 — bg-archiving이면
bg_arv_info->vdes를 재사용한다. 기존 파일이 있으면fileio_mount한다. 없으면npages + 1크기의 새 아카이브를fileio_format한다. - 헤더 쓰기 — 물리 페이지 0에 쓴다(
FILEIO_WRITE_NO_COMPENSATE_WRITE). - 범위 복사 — bg이면
current_page_id에서, 아니면last_arv_fpageid에서 시작하여last_arv_lpageid까지LOGPB_IO_NPAGES청크 단위로 반복한다. 청크가 링 경계를 넘지 않도록 크기를 제한한다. 각 청크는fileio_read_pages(논리 pageid 불일치 →ER_LOG_PAGE_CORRUPTED) 후fileio_write_pages로 아카이브에 쓴다. - Bg-archive 롤 (활성화된 경우) — 임시 볼륨을 dismount하고
fileio_rename으로 실제 아카이브 이름으로 변경한 뒤, 새 임시 파일을 format하고 헤더를 reset·flush한다. - 커서 + 헤더 —
last_arv_num++,last_arv_fpageid = last_arv_lpageid + 1. 동시 applier가 미완성 active 페이지를 읽지 못하도록 일시적으로hdr.append_lsa.pageid = last_arv_lpageid로 고정하고 헤더를 flush한 후append_lsa를 복원하고ER_LOG_ARCHIVE_CREATED를 발행한다.
불변식 — 덮어쓰기 전에 아카이브한다. logwr_write_log_pages는
logwr_flush_all_append_pages 전에 logwr_archive_active_log를 실행하므로,
링 wraparound로 곧 덮일 슬롯은 이미 내구성 있는 아카이브에 들어가 있다. 순서를
위반하면 슬레이브 로그를 잃고 point-in-time 복구가 깨진다(Chapter 10).
6.8 xlogwr_get_log_pages — 마스터 측 응답자
섹션 제목: “6.8 xlogwr_get_log_pages — 마스터 측 응답자”각 슬레이브는 xlogwr_get_log_pages에서 spin하는 스레드 하나로 서비스된다
(slogwr_get_log_pages에서 dispatch됨). LOGWR_COPY_LOG_BUFFER_NPAGES * LOG_PAGESIZE
크기의 send 버퍼를 malloc하고 while (true) 루프를 돌며 반복마다 요청 하나를 처리한다.
stateDiagram-v2 [*] --> Register : -f 마스크 제거\n logwr_register_writer_entry Register --> Wait : entry->status == WAIT Register --> Delay : entry->status == DELAY Wait --> Timeout : ASYNC이고 10초 경과 Timeout --> Register : unregister, continue Wait --> Interrupted : 인터럽트로 resume Interrupted --> Register : 지연 상태이면 continue Wait --> Pack : THREAD_LOGWR_RESUMED Delay --> Pack : LOG_CS_ENTER Pack --> Send : logwr_pack_log_pages 성공 Send --> NextReq : xlog_send_log_pages_to_client NextReq --> Register : xlog_get_page_request_with_reply Pack --> Err : pack 실패 Err --> [*]
Figure 6-3. xlogwr_get_log_pages 요청별 상태 머신. Wait는 poll-to-push 블록 지점으로,
스레드가 요청한 페이지가 존재할 때까지 Log Flush Thread가 깨워줄 때까지 잠든다.
entry->status == WAIT 시 poll-to-push suspend:
// xlogwr_get_log_pages -- src/transaction/log_writer.cif (mode == LOGWR_MODE_ASYNC) { timeout = LOGWR_THREAD_SUSPEND_TIMEOUT; /* 10s */ to.tv_sec = time (NULL) + timeout; to.tv_nsec = 0; }else { timeout = INF_WAIT; to.tv_sec = to.tv_nsec = 0; } /* SYNC/SEMISYNC block forever */rv = thread_suspend_with_other_mutex (thread_p, &writer_info->flush_start_mutex, timeout, &to, THREAD_LOGWR_SUSPENDED);SYNC/SEMISYNC writer는 LFT가 append 후 신호를 보낼 때까지 무한정 대기한다. ASYNC
writer는 최대 10초 대기 후 ER_CSS_PTHREAD_COND_TIMEDOUT 시 unregister하고 continue한다.
다른 분기: 인터럽트/종료 → ER_INTERRUPTED. THREAD_RESUME_DUE_TO_INTERRUPT +
logwr_is_delayed이면 백로그를 전송하기 위해 루프를 반복한다. mutex 에러 → ER_FAILED.
LOGWR_STATUS_DELAY 엔트리는 대기를 건너뛰고 즉시 팩한다. 팩 후 flush_wait_cond에서
대기하고, xlog_send_log_pages_to_client로 전송한 뒤,
xlog_get_page_request_with_reply가 다음 요청을 block하며 기다린다.
ASYNC 엔트리는 전송 전에 unregister하고, SYNC는 전송 후에 한다. 이후 요청에 -f 마스크가
있으면 불법이다(assert_release(false)).
logwr_pack_log_pages는 범위를 선택한다: is_hdr_page_only면 page 0만 팩한다.
copy_from_first_phy_page(-f)이면 logpb_find_oldest_available_page_id에서 시작한다.
일반적으로 fpageid = entry->fpageid를 unflushed 경계인 nxio_lsa.pageid로 clamp한다.
그다음 active-vs-archive 분기:
// logwr_pack_log_pages -- src/transaction/log_writer.cif (!logpb_is_page_in_archive (fpageid)) /* still in active log */ lpageid = eof_lsa.pageid;else { /* already archived */ logpb_fetch_from_archive (thread_p, fpageid, NULL, NULL, &arvhdr, false); lpageid = arvhdr.fpageid + arvhdr.npages - 1; /* bounded by that archive */ if (fpageid == arvhdr.fpageid) ha_file_status = LOG_HA_FILESTAT_ARCHIVED;}if (((size_t)(lpageid - fpageid + 1)) > (LOGWR_COPY_LOG_BUFFER_NPAGES - 1)) lpageid = fpageid + (LOGWR_COPY_LOG_BUFFER_NPAGES - 1) - 1; /* cap at 127 pages */logpb_fetch_from_archive 내부의 아카이브 조회는 guess estimator와 아카이브 페이지
캐시가 있는 곳이다: logpb_get_archive_num_from_info_table이 인메모리
logpb_Arv_page_info_table(start/end/arv_num 트리플의 링)에서 O(1) hit를 시도하고,
실패 시 logpb_get_guess_archive_num이 loginfo 파일로부터 보간하여 — 모든 아카이브를
스캔하지 않고도 한참 뒤처진 슬레이브의 페이지가 어느 아카이브에 있는지 찾는다.
팩은 page 0을 채운 뒤 fpageid..lpageid를 logpb_copy_page_from_log_buffer
(인터럽트된 전송이면 _from_file)로 순회하며, nxio_lsa.pageid 이후의 아직 변경
가능한 페이지는 체크섬을 재계산(logpb_set_page_checksum)하고, 이미 flush된
페이지는 검증한다. *status는 EOF가 포함되면 DONE, 아니면 DELAY다.
logwr_check_page_checksum은 샘플링 CRC다: 4096바이트 블록마다 처음과 마지막 16바이트를
hdr.checksum과 CRC를 비교한다(저장된 값이 0이면 “미설정”). hot path에서 저렴하면서도
찢긴 페이지나 bit-rot를 잡아낸다.
6.9 챕터 요약 — 핵심 정리
섹션 제목: “6.9 챕터 요약 — 핵심 정리”copylogdb는 레코드가 아닌 페이지를 전송한다. 슬레이브는 마스터의 로그 페이지를 바이트 단위로 복사한다. 레코드 파싱은applylogdb쪽의 역할이다(Chapter 7–9).- 두 개의 독립적인 구조체 패밀리가 하나의 파일을 공유한다.
LOGWR_GLOBAL/LOGWR_CONTEXT(CS_MODE)와LOGWR_INFO/LOGWR_ENTRY(SERVER_MODE)는 절대 공존하지 않는다. wire를 넘는 것은LOGWR_MODE뿐이다. last_recv_pageid가 내구성 있는 커서다.logwr_set_hdr_and_flush_info가 전진시키고, 다음 요청은 여기서 파생된다.logwr_get_log_pages의 단조성 assert가 이를 보호한다.- 원형 로그는 두 축의 연속성 검사를 강제한다.
logwr_flush_all_append_pages는 논리적 또는 물리적 인접성이 끊기는 곳에서 run을 분리한다.logwr_to_physical_pageid가hdr.npages에서 wrap하면서 슬롯 0을 헤더에 고정하기 때문이다. - 덮어쓰기 전에 아카이브한다.
logwr_write_log_pages는 flush 전에logwr_archive_active_log를 실행하여 링 wraparound로 곧 덮일 페이지를 봉인한다 — 슬레이브 측 point-in-time 복구를 뒷받침하는 불변식이다. - 마스터 응답자는 poll-to-push 방식이다.
xlogwr_get_log_pages는 SYNC/SEMISYNC writer를flush_start에서 무한정(ASYNC: 10초) suspend하며, LFT가 신호를 보낼 때까지 기다린다. 한참 뒤처진 슬레이브는logpb_Arv_page_info_table과logpb_get_guess_archive_num으로 가속된다. - 체크섬은 전체 페이지가 아닌 샘플링 방식이다.
logwr_check_page_checksum은 4KB 블록마다 처음/마지막 16바이트의 CRC로 flush된 페이지를 검증하고,logpb_set_page_checksum은 전송 전에 아직 변경 가능한 tail 페이지의 체크섬을 재계산한다.
Chapter 7: applylogdb 초기화와 로그 페치
섹션 제목: “Chapter 7: applylogdb 초기화와 로그 페치”HA 노드가 복사된 로그를 *적용(apply)*하도록 지시받으면, apply 데몬은 하나의 전역 상태 객체를 부트스트랩하고, 내구성 있는 _db_ha_apply_info 북마크에서 중단 지점을 복구한 뒤, 슬레이브 로컬에 복사된 볼륨에서 원시 로그 페이지를 읽어야 한다. 이론적 배경(로그 쉬핑, 마스터/슬레이브 분리, copylogdb/applylogdb 쌍)은 상위 레벨 문서인 cubrid-ha-replication.md에서 다루며, 이 챕터는 메커니즘만 추적한다. 챕터 8~10은 여기서 설정된 내용을 토대로 진행된다.
모든 것은 두 개의 파일 스코프 전역 변수인 la_Info(LA_INFO)와 la_recdes_pool(LA_RECDES_POOL)에 의존한다. 각각 정확히 하나씩 존재하며(데몬은 데이터베이스당 싱글 스레드이므로), init 함수는 컨텍스트 포인터를 스레딩하지 않고 직접 이 변수에 쓴다.
7.1 구조체 맵
섹션 제목: “7.1 구조체 맵”flowchart TD info["LA_INFO la_Info<br/>(단일 전역 변수)"] act["LA_ACT_LOG act_log"] arv["LA_ARV_LOG arv_log"] filt["LA_REPL_FILTER repl_filter"] pb["LA_CACHE_PB *cache_pb"] ht["MHT_TABLE *hash_table<br/>pageid에서 버퍼로"] arr["LA_CACHE_BUFFER **log_buffer"] area["LA_CACHE_BUFFER_AREA *buffer_area"] buf["LA_CACHE_BUFFER<br/>fix_count + LOG_PAGE"] info --> act info --> arv info --> filt info --> pb pb --> ht pb --> arr pb --> area arr -.포인팅.-> buf area -.스토리지 소유.-> buf
Figure 7-1. LA_INFO와 그것이 소유하는 캐시들.
LA_INFO — 전역 apply 컨텍스트. 필드는 로그 볼륨 정보, 진행 중인 apply 맵, 슬레이브 측 임시 공간, 마스터 측 캐시, 그리고 영속화된 _db_ha_apply_info 미러로 구분된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
log_path / loginf_path | 복사된 볼륨 디렉터리; *_lginf info-file 경로 | 모든 볼륨 이름이 여기서 파생됨; 아카이브 삭제 북키핑 |
act_log / arv_log | 활성 / 아카이브 볼륨 핸들 | LA_ACT_LOG, LA_ARV_LOG 참조 |
last_file_state | 마지막 ha_file_status | 포맷/동기화 전환 감지 |
start_vsize / start_time | 시작 시 VSZ / 벽시계 시각 | max_mem_size watchdog; 아카이브 삭제 클럭 |
final_lsa | 읽기 커서 — 마지막으로 처리된 LSA | 북마크에서 시드; apply 루프가 전진시킴 |
committed_lsa / committed_rep_lsa | 마지막으로 적용된 LOG_COMMIT / repl 레코드의 LSA | 내구성 있는 apply 워터마크(챕터 10); repl-record 진행 상황 |
last_committed_lsa / last_committed_rep_lsa | 시작 시 스냅샷 | 재시작 시 이미 적용된 작업 건너뜀 |
repl_lists / repl_cnt / cur_repl | 트랜잭션별 LA_APPLY 버킷, 카운트, 커서 | 인플라이트 버퍼링(챕터 8); LA_REPL_LIST_COUNT(50) 단위로 증가 |
total_rows / prev_total_rows / log_record_time | 행 카운터; 마지막 커밋의 서버 시각 | 처리량 및 복제 지연 보고 |
commit_head / commit_tail | 보류 중인 LA_COMMIT의 FIFO | 커밋 적용 순서 지정(챕터 9) |
last_deleted_archive_num / last_time_archive_deleted | 마지막으로 제거된 아카이브 번호, 시각 | la_remove_archive_logs 구동/스로틀 |
log_data / rec_type | 레코드 언팩 임시 공간 | rec_type = 2바이트 슬롯형 타입; la_apply_pre에서 지연 할당 |
undo_unzip_ptr / redo_unzip_ptr | LOG_ZIP 압축 해제 임시 공간 | 압축된 undo/redo; la_apply_pre에서 할당 |
apply_state | HA_LOG_APPLIER_STATE_* | 마스터에 보고됨; dbname-lock 해제 게이팅 |
max_mem_size | VSZ 상한(MB) | 초과 시 데몬 자체 재시작 |
cache_pb / cache_buffer_size | 페이지 캐시; 확장당 버퍼 수 | LA_CACHE_PB 참조; 크기 = LA_DEFAULT_CACHE_BUFFER_SIZE(100) |
last_is_end_of_record / is_end_of_record | 로그 EOF 플래그 | ”따라잡음” vs. “페이지 없음” |
last_server_state / is_role_changed | 마스터 HA 상태 추적 | 역할 변경 처리(챕터 11) |
append_lsa / eof_lsa | 활성 로그 헤더 미러 | 커서 범위 지정; 영속화됨 |
required_lsa / required_lsa_changed | 적용되지 않은 가장 오래된 트랜잭션의 시작 LSA; dirty 플래그 | 재확보 하한선: 이 아래 아카이브는 제거 가능; 플래그는 불필요한 카탈로그 쓰기 방지 |
insert/update/delete/schema/commit/fail_counter / log_commit_time | 연산 통계; 마지막 커밋 시각 | _db_ha_apply_info에 영속화; 지연 보고 |
status / is_apply_info_updated | 행 status, 업데이트 중 플래그 | IDLE/BUSY; 업데이트 중간 크래시 힌트 |
num_unflushed | 버퍼링됐지만 flush되지 않은 항목 수 | LA_MAX_UNFLUSHED_REPL_ITEMS(200)에서 flush |
log_path_lockf_vdes / db_lockf_vdes | path-lock / dbname-lock fd | 중복 데몬 방지(7.4) vs. apply 시간 락 |
repl_filter / reinit_copylog | 테이블 필터; “copylogdb가 재초기화해야 함” 신호 | LA_REPL_FILTER 참조(챕터 11); 아카이브 읽기에서 리셋 볼륨 감지 시 설정됨 |
tde_sock_for_dks / maxslotted_reclength | TDE data-key 소켓; heap_Maxslotted_reclength | UNSTABLE_TDE_FOR_REPLICATION_LOG 하에서만(챕터 11); 행 재구성 크기 지정(챕터 9) |
불변 조건 —
final_lsa는 단일 읽기 커서이며 한 실행 내에서 절대 뒤로 이동하지 않는다.la_apply_pre는 바깥쪽 이터레이션마다committed_lsa에서 이를 다시 시드하고, 안쪽 루프는 오직 전진만 한다. fetch 레이어는append_lsa.pageid < pageid인 페이지를 거부하므로 마스터가 생성한 것을 초과하여 커서가 이동할 수 없다 —eof_lsa앞으로 시드되면la_get_page_buffer가 무한 스핀한다.
LA_CACHE_PB / LA_CACHE_BUFFER / LA_CACHE_BUFFER_AREA — 고정 크기 버퍼의 플랫 풀 위에 구축된 해시 테이블로, 스토리지는 하나 이상의 슬랩(slab)에서 할당된다.
LA_CACHE_PB 필드 | 역할 | 존재 이유 |
|---|---|---|
hash_table | MHT_TABLE LOG_PAGEID → LA_CACHE_BUFFER* | 페치된 페이지의 O(1) 조회 |
log_buffer | 버퍼 포인터 배열 | 클럭 피해자 스캔이 순회 |
num_buffers | log_buffer의 길이 | 피해자를 찾지 못하면 증가 |
buffer_area | 슬랩 리스트의 헤드 | 종료 시 일괄 해제 |
LA_CACHE_BUFFER 필드 | 역할 | 존재 이유 |
|---|---|---|
fix_count | 핀 카운트 | > 0이면 사용 중, 축출 불가 |
recently_free | 세컨드 찬스 비트 | 해제 시 설정; 클럭 첫 번째 패스에서 지워짐 |
in_archive | 출처 플래그 | 아카이브 볼륨에서 채워졌으면 true |
pageid | 내용의 논리적 페이지 id | 해시 키; 0 = 빈 슬롯 |
phy_pageid | 볼륨 내 물리적 페이지 | la_log_phypageid에서 가져옴 |
logpage | LOG_PAGE 페이로드 | 페이지 이미지; SIZEOF_LA_CACHE_LOG_BUFFER(io_pagesize) |
LA_CACHE_BUFFER_AREA(슬랩 하나): area 헤더 뒤에 버퍼들이 연속으로 배치되며, next는 이전 슬랩을 가리키는 단방향 연결이다 — 기존 버퍼를 이동시키지 않으므로 핀된 페이지가 있을 때도 안전하게 확장할 수 있다.
불변 조건 — 살아 있는 모든 버퍼는
log_buffer[]인덱스로 도달 가능하고,pageid != 0이면hash_table키로도 도달 가능하며, 두 경로가 일치해야 한다.la_cache_buffer_replace는 재사용 전에 피해자의 이전pageid로mht_rem을 호출하고,la_get_page_buffer는 새pageid로 다시 삽입한다. 오래된 키가 남아 있으면mht_get이 잘못된 내용을 반환 — 무성한 데이터 손상이 발생한다.
LA_RECDES_POOL — apply 중 레코드당 malloc을 피하기 위해 한 번 할당하는 RECDES 디스크립터와 하나의 연속 아레나.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
recdes_arr | RECDES 배열 | 행 재구성 시 라운드 로빈으로 배분 |
area | page_size * num_recdes 아레나 | 모든 recdes_arr[i].data의 백킹 |
next_idx | 라운드 로빈 커서 | 각 apply 패스마다 0으로 초기화 |
db_page_size | recdes당 용량 | 재초기화가 필요한 페이지 크기 변경 감지 |
num_recdes | 풀 크기 | 호출 지점에서 LA_MAX_UNFLUSHED_REPL_ITEMS(200) |
is_initialized | 가드 | la_init_recdes_pool을 멱등성 있게 만듦 |
LA_ACT_LOG / LA_ARV_LOG — 볼륨 핸들.
LA_ACT_LOG 필드 | 역할 | 존재 이유 |
|---|---|---|
path[PATH_MAX] | 활성 로그 경로 | fileio_make_log_active_name에서 가져옴 |
log_vdes | 열린 fd | 열리기 전까지 NULL_VOLDES |
hdr_page | 헤더 페이지 버퍼 | 마스터 추적을 위해 루프마다 재읽기 |
log_hdr | hdr_page->area 안의 LOG_HEADER* | 모든 append_lsa/nxarv_* 읽기 |
db_iopagesize | 헤더에서 가져온 IO 페이지 크기 | 압축 해제/recdes 임시 공간 크기 지정 |
db_logpagesize | 헤더에서 가져온 로그 페이지 크기 | 모든 캐시 버퍼 크기 지정; 헤더 읽기 전까지 4096 |
LA_ARV_LOG 필드 | 역할 | 존재 이유 |
|---|---|---|
path[PATH_MAX] | 아카이브 경로 | 아카이브 번호에 따라 온디맨드로 구성 |
log_vdes | 열린 아카이브 fd | 아카이브 번호가 바뀌면 재오픈 |
hdr_page | 아카이브 헤더 버퍼 | 첫 아카이브 읽기 시 지연 malloc |
log_hdr | hdr_page 안의 LOG_ARV_HEADER* | fpageid/npages 제공 |
arv_num | 현재 열린 아카이브 번호 | 필요한 번호와 비교하여 재오픈 여부 결정 |
LA_REPL_FILTER(list/list_size/num_filters/type)는 include/exclude 테이블 목록으로, 그 동작 원리는 챕터 11에서 다루며, 여기서는 REPL_FILTER_NONE으로의 초기화만 나타난다.
7.2 진입점: cubrid hb start에서 la_apply_log_file까지
섹션 제목: “7.2 진입점: cubrid hb start에서 la_apply_log_file까지”cubrid heartbeat start가 applylogdb를 생성하면, main은 util_cs.c에 진입하고, 이는 log_applier.c의 la_apply_log_file (database_name, log_path, max_mem_size)를 호출한다. 이 호출은 정상 동작 하에서는 절대 반환하지 않는다 — 이 함수가 곧 데몬 그 자체이다. 프롤로그는 고정된 순서로 진행된다: 시그널 핸들러 설치 및 슬레이브 db-name/peer host 설정; la_init; PRM_ID_HA_SQL_LOGGING이 설정된 경우 선택적으로 sl_init; la_check_duplicated(path lock — 오류 시 반환); la_init_cache_pb(NULL이면 OUT_OF_MEMORY); la_find_log_pagesize(활성 볼륨 열기, 헤더 읽기); 그제야 실제 db_logpagesize로 la_init_cache_log_buffer; la_init_recdes_pool (db_iopagesize, 200); la_get_last_ha_applied_info; committed_lsa := required_lsa; apply 루프 진입(챕터 8+).
리팩터링 시 쉽게 깨지는 두 가지 순서 제약:
- 헤더를 읽기 전에 캐시 버퍼를 할당할 수 없다.
la_init_cache_pb는 컨테이너(LA_CACHE_PB, 필드 NULL/0)만 할당한다. 버퍼당 크기는la_find_log_pagesize이후에야 알게 되는db_logpagesize를 필요로 하므로,la_init_cache_log_buffer는 그 이후에 실행된다. la_check_duplicated는 볼륨 작업 이전에 실행된다, 따라서 두 번째 데몬은 캐시를 두고 경합하기 전에 거부된다.
7.3 la_init — 전역 변수 초기화와 기본값 설정
섹션 제목: “7.3 la_init — 전역 변수 초기화와 기본값 설정”la_init은 순수한 상태 설정이며, VSZ 프로브 단 한 번을 제외하면 실패 경로가 없다. 필드 의미는 7.1 표에 있으며, 비직관적인 의도만 주석으로 표시한다:
// la_init -- src/transaction/log_applier.cmemset (&la_Info, 0, sizeof (la_Info)); /* <- everything zeroed */strncpy (la_Info.log_path, log_path, PATH_MAX - 1);la_Info.cache_buffer_size = LA_DEFAULT_CACHE_BUFFER_SIZE; /* 100 */la_Info.act_log.db_iopagesize = LA_DEFAULT_LOG_PAGE_SIZE; /* 4096, provisional */la_Info.act_log.db_logpagesize = LA_DEFAULT_LOG_PAGE_SIZE;la_Info.act_log.log_vdes = la_Info.arv_log.log_vdes = NULL_VOLDES; /* no volume open */LSA_SET_NULL (&la_Info.committed_lsa); /* all cursors start NULL */// ... committed_rep_lsa, append_lsa, eof_lsa, required_lsa, final_lsa,// last_committed_lsa, last_committed_rep_lsa all SET_NULL ...la_Info.last_deleted_archive_num = -1; /* nothing purged yet */la_Info.max_mem_size = max_mem_size;if (!start_vsize) start_vsize = la_get_mem_size (); /* probe VSZ once per process */la_Info.start_vsize = start_vsize;la_recdes_pool.is_initialized = false; /* force pool (re)alloc */la_Info.repl_filter.type = REPL_FILTER_NONE;la_Info.reinit_copylog = false;db_logpagesize = 4096은 플레이스홀더이다; la_find_log_pagesize 이전에 이 값으로 크기를 결정하면 조용히 4096을 사용하게 된다 — la_init_cache_pb가 버퍼를 할당하지 않는 이유가 바로 이것이다. 기본값은 첫 헤더 읽기 시 malloc으로 임시 hdr_page를 할당할 수 있도록만 존재한다. repl_lists 배열은 여기서 건드리지 않는다; la_init_repl_lists가 LA_REPL_LIST_COUNT(50) 버킷 단위로 지연 구축한다.
7.4 la_apply_pre와 path lock
섹션 제목: “7.4 la_apply_pre와 path lock”적용 전 설정은 두 단계로 나뉜다: la_check_duplicated(프롤로그에서 한 번)와 la_apply_pre(바깥쪽 이터레이션마다 최상단에서).
**la_check_duplicated**는 중복 데몬으로부터 보호한다. "<logpath>/<dbname>_lgla__lock"(LA_LOCK_SUFFIX = "_lgla__lock")을 구성하여 O_RDWR|O_CREAT로 열고, fileio_lock_la_log_path로 쓰기 락을 건다:
// la_check_duplicated -- src/transaction/log_applier.c*lockf_vdes = fileio_open (lock_path, O_RDWR | O_CREAT, 0644);if (*lockf_vdes == NULL_VOLDES) return ER_IO_MOUNT_FAIL; /* branch: cannot open */lockf_type = fileio_lock_la_log_path (dbname, lock_path, *lockf_vdes, last_deleted_arv_num);if (lockf_type == FILEIO_NOT_LOCKF) { /* branch: another daemon holds it */ fileio_close (*lockf_vdes); *lockf_vdes = NULL_VOLDES; return ER_FAILED;}return NO_ERROR; /* branch: lock acquired */락 파일에는 last_deleted_arv_num도 내구성 있게 저장된다 — fileio_lock_la_log_path가 정수를 다시 읽어 오므로, 이후 la_update_last_deleted_arv_num 쓰기를 위해 fd를 log_path_lockf_vdes에 보관한다. dbname 락(db_lockf_vdes, la_lock_dbname 경유)은 별개로, apply 중에만 보유한다.
불변 조건 — (log_path, dbname)당 최대 하나의 applylogdb만 path lock을 보유한다. 두 데몬이 동시에 실행되면 행을 이중 적용하여
committed_lsa가 손상된다. OS 쓰기 락이 단일 쓰기를 강제하며, 패배한 쪽은FILEIO_NOT_LOCKF를 받아 종료한다.
**la_apply_pre**는 이터레이션별 커서를 시드하고 임시 공간을 지연 할당한다:
// la_apply_pre -- src/transaction/log_applier.cLSA_COPY (&la_Info.final_lsa, &la_Info.committed_lsa); /* <- restart cursor at the watermark */if (la_Info.rec_type == NULL) { /* 2-byte slotted rectype scratch, once */ la_Info.rec_type = (char *) malloc (DB_SIZEOF (INT16)); if (la_Info.rec_type == NULL) return false; /* branch: OOM */}if (la_Info.undo_unzip_ptr == NULL) { /* LOG_ZIP undo scratch */ la_Info.undo_unzip_ptr = log_zip_alloc (la_Info.act_log.db_iopagesize); if (la_Info.undo_unzip_ptr == NULL) return false; /* branch: OOM */}if (la_Info.redo_unzip_ptr == NULL) { /* LOG_ZIP redo scratch */ la_Info.redo_unzip_ptr = log_zip_alloc (la_Info.act_log.db_iopagesize); if (la_Info.redo_unzip_ptr == NULL) return false; /* branch: OOM */}return true;세 개의 == NULL 가드는 이터레이션 간 할당을 멱등성 있게 만든다. 첫 번째 줄 — final_lsa를 committed_lsa로 재설정 — 은 일시적 정지 후 루프가 마지막 내구성 있는 커밋에서 재개하는 방식이다.
7.5 북마크에서 final_lsa 시드하기
섹션 제목: “7.5 북마크에서 final_lsa 시드하기”la_get_last_ha_applied_info는 _db_ha_apply_info 행을 읽고(la_get_ha_apply_info 경유), 영속화된 LSA들을 la_Info에 복사한다.
flowchart TD
A["la_get_last_ha_applied_info"] --> B["la_get_ha_apply_info -> res"]
B --> C{"res > 0?<br/>행 발견"}
C -->|yes| D["LSA_COPY committed/rep/append/eof/final/required + 카운터"]
D --> E{"db_creation이 헤더와 일치?"}
E -->|no| Rx["return ER_FAILED"]
E -->|yes| F{"행의 required_lsa가 NULL?"}
F -->|yes| Rx2["return ER_FAILED"]
F -->|no| G
C -->|res == 0| H["insert_apply_info := true"]
C -->|res < 0| Rz["return res"]
H --> G["NULL 폴백 픽스업"]
G --> G1["required NULL -> eof; committed/rep/final NULL -> required"]
G1 --> I{"insert_apply_info?"}
I -->|yes| J["la_insert_ha_apply_info"]
I -->|no| K["la_update_ha_apply_info_start_time"]
J --> L
K --> L{"res == 0?"}
L -->|yes| Rw["return ER_FAILED"]
L -->|res < 0| Rv["return res"]
L -->|ok| M["db_commit_transaction"]
M --> N["last_committed_lsa/rep_lsa := committed_lsa/rep_lsa"]
N --> O["return NO_ERROR"]
Figure 7-2. la_get_last_ha_applied_info의 모든 분기. NULL 폴백 픽스업(G/G1)은 행 발견 경로와 새 행 경로 양쪽에서 모두 도달한다.
행 발견(res > 0) 경로는 행에서 여섯 개의 LSA와 여섯 개의 카운터를 복사한 뒤, 두 가지를 검증한다: db_creation 불일치는 북마크가 다른 데이터베이스 인카네이션에 속한다는 의미이므로 ER_FAILED를 반환하고; 기존 행에서 required_lsa가 NULL이면 이 역시 치명적(ER_FAILED) — 기록된 행에는 반드시 하한선이 있어야 한다. 새 데이터베이스(res == 0)는 복사를 건너뛰고 insert_apply_info만 설정한다. NULL 폴백 블록은 살아남은 두 경로 모두에서 실행된다: required_lsa가 NULL이면 헤더의 eof_lsa로 폴백하고, 이후 committed_lsa / committed_rep_lsa / final_lsa 각각이 NULL이면 required_lsa로 폴백한다. (행 발견 경로에서는 위의 가드가 NULL을 이미 잡았으므로 required_lsa는 이미 비NULL — 따라서 폴백은 세 커서만 패치한다.) 행을 삽입하거나 시작 시각을 업데이트하고, 성공 시에만 db_commit_transaction으로 영속화한다. 마지막으로 last_committed_lsa/last_committed_rep_lsa가 시작 워터마크를 스냅샷하여 챕터 8이 이미 적용된 레코드를 건너뛸 수 있게 한다.
la_apply_log_file로 돌아오면, 한 줄로 시드가 완성된다: LSA_COPY (&la_Info.committed_lsa, &la_Info.required_lsa). la_apply_pre의 final_lsa := committed_lsa와 합산하면, 첫 번째 읽기 커서는 required_lsa — 적용이 필요한 가장 오래된 트랜잭션 — 에 놓인다.
7.6 캐시 할당
섹션 제목: “7.6 캐시 할당”la_init_cache_pb는 단순하다: LA_CACHE_PB를 malloc하고, 모든 필드를 NULL로 설정하며, 반환한다(OOM이면 NULL). 버퍼는 아직 없다.
la_init_cache_log_buffer는 첫 번째 슬랩을 할당한 뒤 해시 테이블을 만든다:
// la_init_cache_log_buffer -- src/transaction/log_applier.cerror = la_expand_cache_log_buffer (cache_pb, slb_cnt, slb_size); /* slb_cnt=100 buffers */if (error != NO_ERROR) return error; /* branch: OOM */cache_pb->hash_table = mht_create ("...", cache_pb->num_buffers * 8, /* <- 8x headroom */ log_pageid_hash, mht_compare_logpageids_are_equal);if (cache_pb->hash_table == NULL) error = ER_OUT_OF_VIRTUAL_MEMORY;return error;la_expand_cache_log_buffer는 LA_CACHE_BUFFER_AREA 헤더와 slb_cnt개의 연속 버퍼를 포함하는 하나의 malloc을 수행하고, log_buffer[]를 realloc한다. 슬랩은 앞에 추가되며(area->next = cache_pb->buffer_area), 기존 버퍼는 절대 이동하지 않으므로 페이지가 핀된 상태에서도 확장이 안전하다. log_pageid_hash는 LOGPB_HEADER_PAGE_ID를 버킷 0에 특수 처리한다.
la_init_recdes_pool은 멱등성이 있으며 크기 변경 시 자체 복구한다:
// la_init_recdes_pool -- src/transaction/log_applier.cif (la_recdes_pool.is_initialized == false) { la_recdes_pool.area = malloc (page_size * num_recdes); /* one arena + OOM branches */ la_recdes_pool.recdes_arr = malloc (sizeof (RECDES) * num_recdes); p = la_recdes_pool.area; for (i = 0; i < num_recdes; i++) { recdes = &la_recdes_pool.recdes_arr[i]; recdes->data = p; recdes->area_size = page_size; recdes->length = 0; /* carve from arena */ p += page_size; } la_recdes_pool.is_initialized = true;}else if (la_recdes_pool.db_page_size != page_size /* branch: size changed */ || la_recdes_pool.num_recdes != num_recdes) { la_clear_recdes_pool (); return la_init_recdes_pool (page_size, num_recdes); /* recurse once with new size */}la_recdes_pool.next_idx = 0; /* always reset cursor */7.7 로그 페치 레이어
섹션 제목: “7.7 로그 페치 레이어”캐시 위의 호출 스택: la_get_page -> la_get_page_buffer -> (la_cache_buffer_replace -> la_log_fetch). la_get_page는 성공할 때까지 la_get_page_buffer를 반복 호출하고 &cache_buffer->logpage를 반환한다.
la_get_page_buffer — 해시 조회, 미스 시 페치, 핀:
// la_get_page_buffer -- src/transaction/log_applier.ccache_buffer = (LA_CACHE_BUFFER *) mht_get (cache_pb->hash_table, &pageid);if (cache_buffer == NULL) { /* MISS */ cache_buffer = la_cache_buffer_replace (cache_pb, pageid, la_Info.act_log.db_logpagesize, la_Info.cache_buffer_size); if (cache_buffer == NULL || cache_buffer->logpage.hdr.logical_pageid != pageid) /* fetch failed / wrong page */ return NULL; (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); /* drop stale key */ if (mht_put (cache_pb->hash_table, &cache_buffer->pageid, cache_buffer) == NULL) return NULL; /* hash insert failed */}else if (cache_buffer->logpage.hdr.logical_pageid != pageid) { /* stale HIT -> evict, retry */ (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); return NULL;}cache_buffer->fix_count++; /* <- pin before returning */return cache_buffer;“오래된 히트” 분기(해시에서는 찾았지만 내용이 더 이상 일치하지 않음)는 호출자가 재페치하도록 NULL을 반환한다; 성공적인 모든 반환은 핀을 건다.
해제 vs. 무효화. la_release_page_buffer는 언핀하되 페이지를 유지하며, 세컨드 찬스 비트를 세운다:
// la_release_page_buffer -- src/transaction/log_applier.ccache_buffer = (LA_CACHE_BUFFER *) mht_get (cache_pb->hash_table, (void *) &pageid);if (cache_buffer != NULL) { if ((--cache_buffer->fix_count) <= 0) cache_buffer->fix_count = 0; /* <- clamp, never negative */ cache_buffer->recently_free = true; /* <- the bit Figure 7-3's clock reads */}la_invalidate_page_buffer는 축출한다 — 해시 키를 제거하고, 핀과 비트를 초기화하며, 슬롯이 비어 있음을 나타내도록 식별자를 0으로 초기화한다:
// la_invalidate_page_buffer -- src/transaction/log_applier.cif (cache_buffer->pageid != 0) (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); /* drop old key */cache_buffer->fix_count = 0;cache_buffer->recently_free = false; /* <- it is gone, not a candidate */cache_buffer->pageid = 0;la_cache_buffer_replace — la_log_fetch에 공급하는 클럭 방식 피해자 선택:
flowchart TD
A["la_cache_buffer_replace"] --> B["found = -1"]
B --> C["log_buffer[]를 'last'부터 스캔"]
C --> D{"fix_count == 0?"}
D -->|no, 핀됨| C
D -->|yes| E{"recently_free?"}
E -->|yes| F["비트 지움, num_recently_free++"]
F --> C
E -->|no| G["found = 슬롯; break"]
C -->|전체 스캔 완료| H{"found >= 0?"}
G --> H
H -->|yes| I["pageid!=0이면 mht_rem 이전 키<br/>fix_count=0; la_log_fetch"]
I --> J{"페치 성공?"}
J -->|no| K["victim->pageid=0; return NULL"]
J -->|yes| L["return victim"]
H -->|no| M{"num_recently_free > 0?"}
M -->|yes| C
M -->|no| N["la_expand_cache_log_buffer"]
N --> O{"확장 성공?"}
O -->|no| P["return NULL"]
O -->|yes| C
Figure 7-3. la_cache_buffer_replace 피해자 선택.
static unsigned int last는 클럭 핸드로, 호출 간에 유지된다. recently_free 버퍼는 한 번의 유예를 받는다(비트 지워짐, num_recently_free++ — 정확히 la_release_page_buffer가 세운 비트); fix_count == 0이고 recently_free가 아닌 버퍼만 선택된다. 피해자를 찾지 못했지만 비트를 지운 스윕은 다시 루프하고; 비트를 찾지도 지우지도 못한 스윕(모두 핀됨)은 풀을 확장하고 재시도한다. 피해자의 이전 키는 la_log_fetch가 덮어쓰기 전에 제거된다.
**la_log_fetch**는 활성 vs. 아카이브를 선택하여 물리적 페이지를 읽는다:
// la_log_fetch -- src/transaction/log_applier.cphy_pageid = la_log_phypageid (pageid);if (la_Info.act_log.log_hdr->append_lsa.pageid < pageid) { /* page beyond what master wrote? */ error = la_fetch_log_hdr (&la_Info.act_log); /* re-read header, recheck */ if (error != NO_ERROR) return error; if (la_Info.act_log.log_hdr->append_lsa.pageid < pageid) return ER_LOG_NOTIN_ARCHIVE; /* genuinely not there yet */}do { if (LA_LOG_IS_IN_ARCHIVE (pageid)) { /* pageid < nxarv_pageid */ error = la_log_fetch_from_archive (pageid, (char *) &cache_buffer->logpage); if (error != NO_ERROR) { la_applier_need_shutdown = true; return error; } cache_buffer->in_archive = true; } else { error = la_log_io_read (la_Info.act_log.path, la_Info.act_log.log_vdes, &cache_buffer->logpage, phy_pageid, la_Info.act_log.db_logpagesize); if (error != NO_ERROR) return ER_LOG_READ; cache_buffer->in_archive = false; } if (cache_buffer->logpage.hdr.logical_pageid == pageid) break; /* got the right page */ usleep (100 * 1000); /* master may be archiving; retry */ error = la_fetch_log_hdr (&la_Info.act_log); if (error != NO_ERROR) return error;} while (error == NO_ERROR && --retry > 0); /* up to 5 tries */if (retry <= 0 || la_Info.act_log.log_hdr->append_lsa.pageid < pageid) return ER_LOG_NOTIN_ARCHIVE;cache_buffer->pageid = pageid; /* <- stamp identity only on success */cache_buffer->phy_pageid = phy_pageid;return error;LA_LOG_IS_IN_ARCHIVE(pageid)는 pageid < nxarv_pageid를 검사한다 — 다음 아카이브 경계 아래의 페이지들은 이미 아카이브 볼륨에 있다. la_log_fetch_from_archive는 아카이브 번호를 찾고(la_find_archive_num), arv_log.arv_num과 다르면 fd를 재오픈하고, 헤더를 읽은 뒤 pageid - fpageid + 1 오프셋의 페이지를 읽는다; ER_PB_BAD_PAGEID 분기는 오래된 fd를 복구하기 위해 log_reopen으로 goto한다. 재시도 루프는 마스터가 테스트와 읽기 사이에 페이지를 아카이브하는 경쟁 조건을 흡수한다.
불변 조건 —
cache_buffer->pageid는 검증된 읽기 후에만 스탬프된다. 어떠한 실패에서도0을 유지하거나la_cache_buffer_replace의 오류 분기에서 초기화되므로, 반쯤 채워진 버퍼는 절대 해시 테이블에 게시되지 않는다.
7.8 페이지 경계를 넘는 레코드 복사: la_log_copy_fromlog
섹션 제목: “7.8 페이지 경계를 넘는 레코드 복사: la_log_copy_fromlog”레코드는 페이지 경계에 걸쳐 있다. la_log_copy_fromlog는 캐시된 페이지들에서 레코드의 타입 워드와 데이터를 복사하며, LA_LOG_READ_* 매크로로 페이지를 넘나든다:
// la_log_copy_fromlog -- src/transaction/log_applier.cint rec_length = (int) sizeof (INT16); /* the 2-byte slotted rectype */pg = log_pgptr;while (rec_type != NULL && rec_length > 0) { /* phase 1: copy the rectype */ LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT (error, 0, log_offset, log_pageid, pg); /* hop if at edge */ if (pg == NULL) break; /* defensive: page vanished */ copy_length = (log_offset + rec_length <= LA_LOGAREA_SIZE) ? rec_length : LA_LOGAREA_SIZE - log_offset; /* clip to page end */ memcpy (rec_type + area_offset, (char *) pg->area + log_offset, copy_length); rec_length -= copy_length; area_offset += copy_length; log_offset += copy_length; *length = *length - DB_SIZEOF (INT16); /* rectype is not part of the data */}area_offset = 0; t_length = *length;while (t_length > 0) { /* phase 2: copy the record data */ LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT (error, 0, log_offset, log_pageid, pg); if (pg == NULL) break; copy_length = (log_offset + t_length <= LA_LOGAREA_SIZE) ? t_length : LA_LOGAREA_SIZE - log_offset; memcpy (area + area_offset, (char *) pg->area + log_offset, copy_length); t_length -= copy_length; area_offset += copy_length; log_offset += copy_length;}LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT는 offset + length >= LA_LOGAREA_SIZE — 페이지당 사용 가능한 바이트 수, la_Info.act_log.db_logpagesize - SSIZEOF(LOG_HDRPAGE)로 정의됨 — 를 검사하고, 그 경우 la_get_page (++pageid)로 다음 페이지를 페치하며 offset을 0으로 초기화한다. la_get_page가 la_get_page_buffer로 재귀하므로, 다음 페이지에 핀을 건다; 이 핀들은 나중에 la_release_all_page_buffers가 해제한다. 형제 매크로 LA_LOG_READ_ALIGN / LA_LOG_READ_ADD_ALIGN은 챕터 8에서 레코드 간 이동에 사용된다.
불변 조건 —
*length는 페이즈 1 이후 rectype을 제외한 값이다. 페이즈 1이*length를sizeof(INT16)만큼 감소시키므로 페이즈 2는 정확히 데이터만 복사한다.rec_type == NULL(오버플로우 페이지)인 경우 페이즈 1은 건너뛰어지므로*length는 전체 데이터 길이를 유지한다.
7.9 챕터 요약 — 핵심 정리
섹션 제목: “7.9 챕터 요약 — 핵심 정리”- 전역 변수 하나, 두 단계 초기화.
la_init은 임시 페이지 크기로la_Info를 초기화한다; 캐시 버퍼와 recdes 풀은la_find_log_pagesize가 실제db_logpagesize를 읽기 전까지 크기를 결정할 수 없다.la_init_cache_pb는 그 사이에 빈 컨테이너만 할당한다. - Path lock(
_lgla__lock,LA_LOCK_SUFFIX)이 단일 쓰기 보장.la_check_duplicated는fileio_lock_la_log_path로 두 번째 데몬을 거부하며, 같은 파일이last_deleted_archive_num을 영속화한다. dbname 락은 별개이며 apply 시간에만 보유된다. final_lsa는 읽기 커서이며,la_apply_pre가 바깥쪽 이터레이션마다committed_lsa에서 재시드한다. 시작 시 체인:required_lsa -> committed_lsa -> final_lsa.- 북마크가 복구를 주도한다.
la_get_last_ha_applied_info는 행에서 여섯 개의 LSA와 여섯 개의 카운터를 복사하고, 불일치하는db_creation인카네이션(및 기존 행의 NULLrequired_lsa)을 검증하며, 행 발견과 새 행 경로 양쪽에서 NULL 폴백을 적용하여 모든 커서가required_lsa를 거쳐 헤더eof_lsa까지 기본값이 되도록 하고, 재시작 워터마크를 스냅샷한다. - 페이지 캐시는 클럭 교체 플랫 풀 위의 해시 테이블이다.
la_get_page_buffer가 핀(fix_count++);la_release_page_buffer가 언핀하고recently_free를 세움;la_invalidate_page_buffer가 축출;la_cache_buffer_replace가recently_free를 세컨드 찬스로 소비하며, 모든 버퍼가 핀됐을 때 풀을 확장한다. la_log_fetch는nxarv_pageid경계(LA_LOG_IS_IN_ARCHIVE)로 활성 vs. 아카이브를 선택하고, 마스터 아카이빙 경쟁 조건을 흡수하기 위해 헤더를 재읽고 최대 5회 재시도하며, 검증된 읽기 후에만pageid를 스탬프한다.- 레코드는 페이지에 걸쳐 있으며,
LA_LOG_READ_*매크로와la_log_copy_fromlog가 경계를 투명하게 넘나들고, 페이즈 1에서 2바이트 rectype을 빼서 페이즈 2가 정확히 페이로드만 복사하도록 한다.
Chapter 8: 레코드별 디스패치와 트랜잭션별 버퍼링
섹션 제목: “Chapter 8: 레코드별 디스패치와 트랜잭션별 버퍼링”이 챕터는 applylogdb 내부 루프의 핵심을 해부한다. 데몬이 로그 레코드를 하나씩 앞으로 순회할 때마다, la_log_record_process는 해당 레코드의 종류와 원본 트랜잭션의 커밋이 도착할 때까지 그 효과를 어디에 쌓아둘지 결정한다. 상위 문서(cubrid-ha-replication.md, “applylogdb apply pipeline”)는 슬레이브가 트랜잭션 단위로 아이템을 버퍼링하고 커밋 순서대로 재생하는 이유를 설명한다. 이 챕터는 branch별로 어떻게 이루어지는지를 추적한다. 호출자(Chapter 7)는 이미 페이지를 가져와 final 위치의 LOG_RECORD_HEADER를 찾아 놓은 상태다. 여기서 우리가 다룰 것은 switch (lrec->type)이다. 디스패치된 레코드는 두 가지 보조 구조에 흡수된다.
- 트랜잭션별 apply 리스트
la_Info.repl_lists[]— 살아있는 각trid마다 하나의LA_APPLY버킷이 존재하며, 각 버킷은LA_ITEM의 이중 연결 리스트다. - 커밋 큐
la_Info.commit_head / commit_tail— 트랜잭션 종료 마커마다 하나씩 생성되는LA_COMMIT노드의 FIFO로, 재생 순서를 확정한다.
8.1 트랜잭션별 apply 구조체
섹션 제목: “8.1 트랜잭션별 apply 구조체”세 가지 구조체(log_applier.c 상단에 정의됨)가 버퍼링된 상태를 담는다.
LA_APPLY — 살아있는 트랜잭션마다 하나의 버킷:
| 필드 | 역할 | 이유 |
|---|---|---|
tranid | 소유 트랜잭션 ID; 0은 재활용된 슬롯을 표시 | la_find_apply_list의 검색 키; 0이면 la_add_apply_list가 배열을 늘리기 전에 완료된 슬롯을 재사용한다 |
num_items | 버퍼링된 LA_ITEM 개수 | 롱 트랜잭션 제어기가 이 값을 LA_MAX_REPL_ITEMS와 비교한다 |
is_long_trans | 버킷이 tail을 버린 이후에 고정되는 플래그 | 플립되면, la_set_repl_log는 더 이상 할당하지 않고 last_lsa만 추적한다 |
start_lsa | 트랜잭션의 첫 번째 레코드 LSA | 아직 끝나지 않은 트랜잭션이 소유한 가장 낮은 LSA; Chapter 10의 북마크는 이 지점을 넘어갈 수 없다 |
last_lsa | 롱 트랜잭션의 가장 최근 레코드 LSA | Chapter 9의 롱 트랜잭션 재생이 어디까지 재스캔해야 하는지 알려준다 |
head / tail | 이중 연결 LA_ITEM 체인의 양 끝 | head는 shed 이후에도 보존되는 anchor이며, tail은 추가 지점이다 |
LA_ITEM — 버퍼링된 복제 레코드 하나:
| 필드 | 역할 | 이유 |
|---|---|---|
next / prev | 트랜잭션 내 체인 링크 | 롱 트랜잭션에서 살아남은 head는 둘 다 null이며, la_unlink_repl_item은 이를 허용한다 |
log_type | LOG_REPLICATION_DATA 또는 _STATEMENT | la_make_repl_item의 unpack branch와 la_get_item_pk_value의 lazy-key branch를 결정한다 |
item_type | 마스터의 rcvindex(DATA) 또는 statement 유형(STATEMENT) | 재생할 구체적인 연산을 식별한다 |
class_name | 대상 클래스 이름 | 슬레이브에서 heap/index를 조회한다 |
packed_key_value / _length | PK의 디스크 이미지, packed 상태 유지 | 아직 abort될 수 있는 키를 위해 DB_VALUE 없이 on-demand로만 key로 unpack된다 |
key | unpack된 PK(DATA, on-demand) 또는 statement 텍스트(STATEMENT) | 슬레이브가 apply하는 값; STATEMENT는 need_clear = true로 즉시 채워진다 |
db_user / ha_sys_prm | statement 복제 컨텍스트 | 올바른 사용자와 시스템 파라미터 세트로 재생한다 |
lsa / target_lsa | 복제 레코드 LSA와 마스터 target LSA | 아이템의 순서와 출처를 나타낸다 |
LA_COMMIT — FIFO 내의 트랜잭션 종료 마커 하나:
| 필드 | 역할 | 이유 |
|---|---|---|
next / prev | FIFO 링크 | repl_lists[]가 아닌 이 큐가 재생 순서를 확정한다 |
type | LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT | ABORT 노드는 la_apply_commit_list에게 해당 버킷을 재생하지 말고 버리라고 지시한다 |
tranid | 소유 트랜잭션 | 노드를 해당 LA_APPLY 버킷과 연결한다 |
log_lsa | EOT 마커의 LSA | 성공적인 drain 이후 committed_lsa로 복사되는 커밋 지점 |
log_record_time | 벽시계 커밋 시각, sysop은 0 | 시간 제한 복제 지연(replica delay)에 사용된다 |
8.2 switch 이전의 두 가지 프롤로그 가드
섹션 제목: “8.2 switch 이전의 두 가지 프롤로그 가드”모든 레코드는 la_log_record_process가 타입을 살피기 전에 두 개의 게이트를 통과한다.
// la_log_record_process -- src/transaction/log_applier.cif (lrec->trid == NULL_TRANID || LSA_GT (&lrec->prev_tranlsa, final) || LSA_GT (&lrec->back_lsa, final)) { if (lrec->type != LOG_END_OF_LOG) { /* EOF sentinel is the only tolerated exception */ la_applier_need_shutdown = true; /* poison pill: caller exits the apply loop */ er_set (..., ER_HA_LA_INVALID_REPL_LOG_RECORD, 10, ...); return ER_LOG_PAGE_CORRUPTED; }}가드 1 — 손상 / 미기록 감지. 레코드는 다음 중 하나에 해당하면 의심 레코드로 판단한다: trid가 NULL_TRANID이거나, 역방향 포인터(prev_tranlsa — 동일 트랜잭션 내 직전 레코드; back_lsa — 전역 로그 내 직전 레코드) 중 하나가 final보다 앞을 가리키는 경우. 역방향 포인터는 이미 기록된 것만 합법적으로 가리킬 수 있다. 따라서 앞을 가리킨다는 것은 마스터가 flush하기 직전에 페이지를 가져온, 아직 유효하지 않은 레코드임을 의미한다. LOG_END_OF_LOG는 허용된다(활성 로그 tail의 sentinel은 null/쓰레기 포인터를 가진다). 그 외에는 복구 불가능하므로 데몬을 종료한다.
INVARIANT — 역방향 포인터는 읽히는 레코드 위치를 절대 초과할 수 없다. 가드 1의
LSA_GT검사(back_lsa,prev_tranlsa)로 강제된다. 위반 시 데몬은 해당 바이트 해석을 거부한다(초기화되지 않은 페이지 메모리를 역참조하면 SEGV가 발생하거나 쓰레기 데이터가 apply될 수 있다).LOG_END_OF_LOG예외만이 forward/null 포인터로 이 게이트를 통과할 수 있는 유일한 합법적 경로다.
가드 2 — 최초 인식 등록. LSA_ISNULL (&lrec->prev_tranlsa)이고 레코드가 예외 대상이 아닌 경우(type != LOG_END_OF_LOG && type != LOG_DUMMY_HA_SERVER_STATE && trid != LOG_SYSTEM_TRANID — 이 타입들은 버킷을 소유하지 않는다) 이 가드가 동작한다. prev_tranlsa == NULL은 트랜잭션의 첫 번째 로그 레코드(트랜잭션 내 직전 레코드 없음)를 표시하므로, 가드는 apply = la_add_apply_list (lrec->trid)를 호출하고, apply->start_lsa가 처음으로 null인 경우 final을 복사하여 Chapter 10의 북마크가 미완료 트랜잭션이 소유한 가장 낮은 LSA를 알 수 있게 한다. NULL 반환은 OOM을 의미한다: la_applier_need_shutdown을 설정하고 캡처된 er_errid ()(또는 ER_FAILED)를 반환한다.
두 가드를 통과한 후에야 함수는 la_Info.is_end_of_record = false를 설정하고 타입 switch에 진입한다.
8.3 디스패치 switch, 분기별 추적
섹션 제목: “8.3 디스패치 switch, 분기별 추적”모든 분기를 아래에 정리한다. ER_INTERRUPTED는 실패가 아니다 — 호출자(Chapter 7)는 이를 “이 페이지 워크를 멈추고 재평가하라”로 읽어, end-of-log, 역할 변경, 충돌 복구 마커에서 내부 루프를 깔끔하게 중단한다.
LOG_END_OF_LOG. 로그 tail이다. 두 가지 하위 경우 모두 ER_INTERRUPTED를 반환한다: 다음 페이지가 존재하고 현재 페이지가 아카이브된 상태(la_does_page_exist(pageid+1) && la_does_page_exist(pageid) == LA_PAGE_EXST_IN_ARCHIVE_LOG)라면 아카이브 파일의 조기 EOF다 — 경고 후 final->pageid++; final->offset = 0으로 건너뛴다. 그 외에는 진짜 active 로그 tail이다 — la_Info.is_end_of_record = true로 설정한다.
LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT. 실제 작업이 이루어지는 분기다. 둘 다 la_set_repl_log (pg_ptr, lrec->type, lrec->trid, final)에 공급되어 레코드를 트랜잭션 버킷에 추가한다(섹션 8.4). NO_ERROR가 아닌 반환은 데몬을 종료하고 전파된다.
LOG_SYSOP_END / LOG_COMMIT. flush 트리거 — 버퍼링된 아이템을 슬레이브로 drain하는 분기다.
// la_log_record_process -- src/transaction/log_applier.ccase LOG_SYSOP_END:case LOG_COMMIT: if (LSA_GT (final, &la_Info.committed_lsa)) { /* idempotency gate vs durable bookmark */ eot_time = (lrec->type == LOG_SYSOP_END) ? 0 : la_retrieve_eot_time (pg_ptr, final); error = la_add_node_into_la_commit_list (lrec->trid, final, lrec->type, eot_time); if (eot_time != 0) { error = la_delay_replica (eot_time); ... } /* time-bound replica */ if (la_Info.status == LA_STATUS_IDLE) la_Info.status = LA_STATUS_BUSY; do { error = la_apply_commit_list (&lsa_apply, final_pageid); /* drains buckets in commit order */ ... /* error triage below; on success advance committed_lsa, bump commit_counter for LOG_COMMIT */ } while (!LSA_ISNULL (&lsa_apply)); } else { la_free_repl_items_by_tranid (lrec->trid); } /* already applied across a restart: discard */ break;외부의 LSA_GT는 멱등성 게이트다(LSA가 내구적 북마크 위치 이하이면 이미 지난 실행에서 적용되었음을 의미하므로 버킷을 버린다). 각 성공적인 la_apply_commit_list 패스는 lsa_apply를 committed_lsa로 복사하고 실제 커밋에 한해 commit_counter를 증가시킨다. 비직관적인 분기는 do/while 내부의 오류 분류 처리다: la_apply_commit_list가 ER_NET_CANT_CONNECT_SERVER를 반환할 때 동작하며, er_errid ()를 검사한다 — 복구 가능한 서버 다운 abort(ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED, ER_LK_UNILATERALLY_ABORTED)는 삼켜지고 루프가 재시도된다. 그 외 er_errid ()는 ER_NET_SERVER_COMM_ERROR를 다시 발생시키고 반환한다. 독립적으로, ER_HA_LA_EXCEED_MAX_MEM_SIZE와 ER_TDE_CIPHER_IS_NOT_LOADED(Chapter 11의 TDE 경로)는 데몬을 종료하고 반환하며, flush 오류(LA_IS_FLUSH_ERROR)는 즉시 반환된다.
LOG_ABORT. 롤백된 트랜잭션도 재생 순서에 빈 자리가 생기지 않도록 커밋 큐 슬롯이 필요하지만, 아이템은 없다. 이 분기는 la_add_node_into_la_commit_list (lrec->trid, final, LOG_ABORT, 0)을 호출한다. 나중에 la_apply_commit_list가 ABORT 노드를 디큐할 때 해당 trid의 버킷을 버린다.
LOG_DUMMY_CRASH_RECOVERY. 스스로 충돌에서 복구한 마스터가 기록한다. 이 지점을 가로질러 연속성을 신뢰할 수 없다 — 알림을 로그에 남기고 final을 lrec->forw_lsa로 점프시킨 뒤 재앵커링을 강제하기 위해 ER_INTERRUPTED를 반환한다. LOG_END_CHKPT. 체크포인트 종료 마커 — break(아무것도 하지 않음).
LOG_DUMMY_HA_SERVER_STATE. HA 하트비트이자 역할 변경 트립와이어다. ha_server_state = la_get_ha_server_state (pg_ptr, final) 이후 세 가지 결과: (a) ha_server_state == NULL(읽기 불가) → 로그 후 break; (b) state != HA_SERVER_STATE_ACTIVE && state != HA_SERVER_STATE_TO_BE_STANDBY이면서 DB lock 파일을 보유 중(la_Info.db_lockf_vdes != NULL_VOLDES) → la_Info.is_role_changed = true 설정 후 ER_INTERRUPTED 반환 — 페일오버 트립와이어(Chapter 11); (c) 상태가 여전히 active이고 la_is_repl_lists_empty () → la_update_ha_apply_info_log_record_time (ha_server_state->at_time) 후 la_log_commit (true)로 북마크를 flush(오류 시 반환). 리스트가 비어있는지 확인하는 것이 중요하다: 트랜잭션이 아직 버퍼링된 상태에서 하트비트에 북마크를 flush하면 적용되지 않은 작업을 건너뛰게 된다.
default. 그 외 모든 레코드 타입 — break(무시됨).
flowchart TD
SW{"lrec->type"} --> INT["EOL / CRASH_RECOVERY / HA 역할 변경\n-> ER_INTERRUPTED, 페이지 워크 중단"]
SW --> RD["REPL_DATA/STATEMENT -> la_set_repl_log 버퍼링"]
SW --> CM["SYSOP_END / COMMIT -> LA_COMMIT 큐 추가, drain 루프"]
SW --> AB["ABORT -> LA_COMMIT 큐 추가, type=ABORT"]
SW --> NOP["END_CHKPT / HA active / default -> break"]
Figure 8-1: switch 분기를 결과별로 묶은 그림. 세 개의 ER_INTERRUPTED 분기는 페이지 워크를 중단하고, 나머지는 버퍼링, flush, abort 플레이스홀더, 또는 아무것도 하지 않는다.
switch 이후 에필로그는 아카이브 페이지 마지막 레코드에 대한 두 번째 손상 검사다: lrec->forw_lsa.pageid == -1 || lrec->type <= LOG_SMALLER_LOGREC_TYPE || lrec->type >= LOG_LARGER_LOGREC_TYPE(경계 sentinel 자체도 유효하지 않음으로 포함하는 <=/>=) 조건 성립 시 ER_LOG_PAGE_CORRUPTED를 반환한다(해당하는 경우 아카이브 페이지를 건너뜀). 그 외에는 NO_ERROR.
8.4 la_set_repl_log — 롱 트랜잭션 우회를 포함한 버퍼링
섹션 제목: “8.4 la_set_repl_log — 롱 트랜잭션 우회를 포함한 버퍼링”la_set_repl_log는 apply = la_find_apply_list (tranid)로 버킷을 조회한 뒤 네 가지 분기를 따른다.
// la_set_repl_log -- src/transaction/log_applier.c (condensed branch heads)if (apply == NULL) { er_log_debug (...); return NO_ERROR; } /* (1) never registered: silently drop */if (apply->is_long_trans) { LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; } /* (2) already long: track last_lsa */if (apply->num_items >= LA_MAX_REPL_ITEMS) { /* (3) crossing the 1000-item threshold */ la_free_all_repl_items_except_head (apply); apply->is_long_trans = true; LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR;}item = la_make_repl_item (log_pgptr, log_type, tranid, lsa); /* (4) normal: build (NULL -> ER_OUT_OF_VIRTUAL_MEMORY) + la_add_repl_item */비직관적인 분기들: (1)은 applier의 시작 지점 이전에 시작된, 첫 번째 레코드를 본 적 없는 트랜잭션을 조용히 버린다. (2)/(3)은 메모리 제어기다 — 임계값에서 la_free_all_repl_items_except_head는 첫 번째 아이템을 제외한 모든 아이템을 해제하고(리스트 anchor와 start_lsa를 보존), is_long_trans를 플립한다. 이후 (2)는 apply->last_lsa만 전진시켜 롱 트랜잭션 재생(Chapter 9)이 마스터 로그에서 어디까지 직접 재스캔해야 하는지 알 수 있게 한다.
INVARIANT — 롱 트랜잭션은 최대 하나의 버퍼링된 아이템(head)만 보유한다. 크로스오버 시
la_free_all_repl_items_except_head와 already-long 분기(할당을 전혀 하지 않음)로 강제된다. 메모리는 제한되지만, 그 대가로 Chapter 9는start_lsa와last_lsa사이를 마스터 로그에서 직접 다시 읽어야 한다 — 정확히LA_MAX_REPL_ITEMS가 방지하고자 했던 팽창이다.
la_free_all_repl_items_except_head는 apply->head->next부터 순회하며 각 아이템을 la_free_repl_item으로 해제한다. head가 null이면 즉시 반환된다. la_free_repl_item은 먼저 아이템을 unlink하고(la_unlink_repl_item — 롱 트랜잭션의 unlink된 head를 허용함), class_name을 해제하고(설정된 경우에만 pr_clear_value로 key를 정리), db_user, ha_sys_prm, packed_key_value를 해제한 후 노드를 free한다.
8.5 la_make_repl_item / la_add_repl_item — unpack과 연결
섹션 제목: “8.5 la_make_repl_item / la_add_repl_item — unpack과 연결”la_make_repl_item은 채워진 LA_ITEM을 생성한다: 레코드 헤더를 지나 정렬/전진하고(LA_LOG_READ_ALIGN, LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT — 실패 시 NULL), repl_log->length 바이트를 힙 area에 복사하고, la_new_repl_item (lsa, &repl_log->lsa)에서 초기화된 skeleton을 가져온 뒤(lsa와 마스터의 target_lsa를 복사), switch (log_type)을 실행한다.
// la_make_repl_item -- src/transaction/log_applier.ccase LOG_REPLICATION_DATA: /* layout: [int key_len][string class_name][aligned key] */ ptr = or_unpack_int (area, &item->packed_key_value_length); ptr = or_unpack_string (ptr, &item->class_name); item->packed_key_value = malloc (item->packed_key_value_length); if (item->packed_key_value == NULL) goto error_return; ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); /* 8-byte align, see or_pack_mem_value */ memcpy (item->packed_key_value, ptr, item->packed_key_value_length); item->item_type = repl_log->rcvindex; break; /* rcvindex identifies the DML op */case LOG_REPLICATION_STATEMENT: /* layout: [int type][string class][string stmt][string user][string prm] */ ptr = or_unpack_int (area, &item->item_type); ptr = or_unpack_string (ptr, &item->class_name); ptr = or_unpack_string (ptr, &str_value); db_make_string (&item->key, str_value); item->key.need_clear = true; ptr = or_unpack_string (ptr, &item->db_user); ptr = or_unpack_string (ptr, &item->ha_sys_prm); break;default: er_set (..., ER_GENERIC_ERROR, 0); goto error_return;LOG_REPLICATION_DATA(Chapter 3)는 PK를 packed 상태로 유지하며, la_get_item_pk_value의 on-demand 요청 시에만 item->key로 unpack된다. item_type에는 rcvindex가 담긴다. LOG_REPLICATION_STATEMENT(Chapter 4)는 statement 텍스트를 item->key에 넣고(need_clear = true), db_user / ha_sys_prm도 채운다. default 분기와 모든 할당 실패는 error_return으로 점프한다(area와 반쯤 만들어진 아이템을 해제하고 NULL 반환). 성공 시 item->log_type을 stamping하고, area를 해제한 뒤 아이템을 반환한다. la_add_repl_item은 단순한 tail append이며 apply->num_items를 증가시킨다.
8.6 트랜잭션별 버킷: 조회, 추가, on-demand 재할당
섹션 제목: “8.6 트랜잭션별 버킷: 조회, 추가, on-demand 재할당”la_Info.repl_lists[]는 LA_APPLY *의 평탄한 배열이다. 재활용되지 않은 슬롯은 트랜잭션의 tranid와 이중 연결 LA_ITEM 체인(head에서 tail까지)을 보유하며, 재활용된 슬롯은 tranid == 0이다.
la_find_apply_list (tranid)는 일치하는 tranid를 찾기 위해 repl_lists[0 .. cur_repl)을 선형 스캔하며, 없으면 NULL을 반환한다. la_add_apply_list (tranid)는 찾기 또는 할당: (1) la_find_apply_list에 이미 있으면 반환; (2) 없으면 첫 번째 재활용 슬롯(tranid == 0, 완료된 트랜잭션이 남긴 것)을 재사용하여 tranid를 stamping; (3) cur_repl == repl_cnt(배열 가득 참)이면 la_init_repl_lists (true)로 확장(실패 시 NULL); (4) repl_lists[cur_repl]을 할당하고 cur_repl을 증가시킨 뒤 반환한다.
la_init_repl_lists (need_realloc)은 배열 스토리지를 소유한다.
// la_init_repl_lists -- src/transaction/log_applier.c (condensed)if (need_realloc == false) { /* first call: malloc LA_REPL_LIST_COUNT (50) pointers; repl_cnt = 50, cur_repl = 0, j = 0 */ ...} else { /* grow: realloc to repl_cnt + 50; j = old repl_cnt marks first NEW slot, then repl_cnt += 50 */ ...}for (i = j; i < la_Info.repl_cnt; i++) { /* init ONLY [j, repl_cnt); each malloc OOM -> free + ER_OUT_OF_VIRTUAL_MEMORY */ la_Info.repl_lists[i] = malloc (DB_SIZEOF (LA_APPLY)); ...->tranid = 0; num_items = 0; is_long_trans = false; start_lsa = last_lsa = NULL; head = tail = NULL;}루프를 j = old_repl_cnt에서 시작하는 것은 realloc이 새로 추가된 슬롯만 초기화한다는 의미다 — 기존 버킷과 아이템 체인은 그대로 유지된다.
INVARIANT —
cur_repl <= repl_cnt이며,[0, cur_repl)슬롯만이 유효한 버킷이다.la_add_apply_list는cur_repl < repl_cnt를 먼저 보장하지 않고서는repl_lists[cur_repl]을 절대 할당하지 않으며(확장 분기는 정확히 둘이 같을 때 동작한다), 배열을 늘리기 전에 재활용tranid == 0슬롯을 우선 재사용한다. 이를 위반하면 배열 밖을 인덱싱하여 힙 손상이 발생한다.
8.7 커밋 큐: la_add_node_into_la_commit_list와 la_retrieve_eot_time
섹션 제목: “8.7 커밋 큐: la_add_node_into_la_commit_list와 la_retrieve_eot_time”la_add_node_into_la_commit_list (tranid, lsa, type, eot_time)은 LA_COMMIT을 malloc하고, type(LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT), log_record_time = eot_time, log_lsa(EOT 마커에서), tranid를 채운 뒤 la_Info.commit_tail의 FIFO에 추가한다(비어있으면 head+tail을 초기화; OOM → ER_OUT_OF_VIRTUAL_MEMORY). repl_lists[]가 아닌 이 큐가 재생 순서를 확정한다. Chapter 9의 la_apply_commit_list는 head부터 순서대로 걷기 때문에, DML 레코드들이 로그에서 아무리 뒤섞여 있어도 트랜잭션들은 마스터 커밋 순서 그대로 슬레이브에 적용된다.
la_retrieve_eot_time (pgptr, lsa)는 벽시계 커밋 시각을 읽는다: 레코드 헤더를 지나 위치를 잡고, LOG_REC_DONETIME이 맞게 들어오도록 정렬/전진한 뒤, 읽기 실패 시 0(“시각 없음” sentinel — 커밋 분기가 la_delay_replica를 건너뛰는 데 사용)을 반환하거나 성공 시 donetime->at_time을 반환한다. la_retrieve_eot_time은 LOG_COMMIT에서만 호출된다. LOG_SYSOP_END는 EOT 타임스탬프가 없으므로 0을 하드코딩한다.
8.8 챕터 요약 — 핵심 정리
섹션 제목: “8.8 챕터 요약 — 핵심 정리”la_log_record_process는 모든 레코드를 두 개의 가드로 검사한다 — EOF sentinel을 제외하고 applier를 종료시키는 손상/forward 포인터 검사, 그리고 첫 번째 레코드(prev_tranlsa == NULL)를 감지해 버킷을 할당하고start_lsa를 stamp하는 최초 인식 등록.- 디스패치 switch에는 도달 가능한 분기가 여덟 개 있다. 그 중 세 개(
LOG_END_OF_LOG,LOG_DUMMY_CRASH_RECOVERY,LOG_DUMMY_HA_SERVER_STATE의 역할 변경 케이스)는 실패가 아닌 페이지 워크 중단을 위해ER_INTERRUPTED를 반환한다. 레코드는LOG_COMMIT/LOG_SYSOP_END분기가la_apply_commit_list를 동작시킬 때까지 버퍼링된다.LOG_ABORT는 아이템 없이 순서 보존용 플레이스홀더를 큐에 추가한다. la_set_repl_log는 롱 트랜잭션 상한을 강제한다:LA_MAX_REPL_ITEMS(1000)에 도달하면 버킷은 head를 제외한 모든 것을 버리고,is_long_trans를 설정한 뒤 이후에는last_lsa만 추적한다 — 나중에 로그를 재스캔하는 비용으로 메모리를 제한한다.la_make_repl_item은LOG_REPLICATION_DATA(lazy packed-key +rcvindex)와LOG_REPLICATION_STATEMENT(statement +db_user+ha_sys_prm)를 다르게 unpack하며, 반쯤 만들어진 아이템을 해제하는 공유error_return을 사용한다.repl_lists[]는 재활용 우선, 50개씩 확장하는 배열이다.la_add_apply_list는la_init_repl_lists (true)로 realloc하기 전에tranid == 0슬롯을 재사용하여cur_repl <= repl_cnt를 유지한다.la_add_node_into_la_commit_list가 생성하는LA_COMMITFIFO — 실제 커밋은la_retrieve_eot_time으로 타임스탬핑되고, sysop은0— 은 슬레이브에서의 재생 순서를 마스터 커밋 순서에 고정한다.
Chapter 9: 커밋된 트랜잭션 적용과 행 이미지 재구성
섹션 제목: “Chapter 9: 커밋된 트랜잭션 적용과 행 이미지 재구성”8장에서는 트랜잭션 단위 그림을 살펴봤다. 각 trid는 하나의 LA_APPLY를 소유하며, head .. tail 체인에 버퍼링된 LA_ITEM이 연결되어 있고, la_Info.commit_head .. commit_tail에는 LOG_COMMIT/LOG_ABORT/LOG_SYSOP_END마다 큐에 쌓인 LA_COMMIT 레코드가 대기 중이다. 이 장은 적용(apply) 생애주기의 마지막 단계를 다룬다. 메인 루프가 커밋 레코드를 꺼낼 때, 버퍼링된 이벤트가 어떤 순서로 재실행되는지, 마스터의 힙 로그에서 after-image가 어떻게 재구성되는지, 그리고 행이 어떻게 슬레이브 서버에 도달하는지를 추적한다. “pull → 커밋 전까지 버퍼 → 재실행” 이론은 동반 문서(cubrid-ha-replication.md §“슬레이브의 커밋 전 트랜잭션 단위 버퍼링”과 §“행 이미지 재구성 — 경우 분석”)에 있으며, 이 장에서는 각 분기를 추적한다.
9.1 la_apply_commit_list — 호출당 커밋 레코드 하나
섹션 제목: “9.1 la_apply_commit_list — 호출당 커밋 레코드 하나”메인 루프는 커밋을 하나 지나칠 때마다 이 함수를 호출한다. 큐의 맨 앞에 있는 커밋(la_Info.commit_head의 type이 LOG_COMMIT/LOG_SYSOP_END/LOG_ABORT인 것) 딱 하나를 처리하고, la_apply_repl_log를 호출한 뒤 꺼낸 log_lsa를 호출자의 *lsa에 LSA_COPY하며, 노드를 연결 해제하고 free_and_init한다(Figure 9-1 참고).
commit_head == NULL이거나 head가 커밋 계열 레코드가 아니면 본문은 건너뛰고 *lsa는 NULL 상태로 남는다. 적용 성공·실패 모두 노드를 해제한다 — 함수가 적용 오류 시 중단하지 않고 오류를 상위로 전파하여 호출자가 재연결 여부를 판단하게 한다. log_record_time(딜레이 리포트가 now()에서 빼는 마스터의 커밋 타임스탬프)은 LOG_COMMIT일 때만 복사된다.
불변 조건: 커밋 큐는 엄격한 FIFO 순서로, 호출당 정확히 하나의 노드만 꺼낸다. commit_head는 정확히 한 노드 앞으로 이동하고 head만 해제된다. 미래에 누군가 여러 커밋을 한 번에 루프 처리하도록 바꾸면, 호출당 *lsa 반환값(10장의 호출자 내구성 북마크)이 더 이상 단일한 안전 재시작 지점을 가리키지 않게 된다.
flowchart TB
A["la_apply_commit_list(lsa)"] --> C{"commit_head != NULL\n그리고 type이 COMMIT/SYSOP_END/ABORT?"}
C -- 아니오 --> Z["NO_ERROR 반환, lsa는 NULL 유지"]
C -- 예 --> D["la_apply_repl_log(tranid, type, log_lsa)"]
D --> E["LSA_COPY(lsa, commit.log_lsa)"]
E --> F{"type == LOG_COMMIT?"}
F -- 예 --> G["log_record_time = commit.log_record_time"]
F -- 아니오 --> H["커밋 노드 연결 해제 + 해제"]
G --> H
H --> I["오류 반환"]
Figure 9-1 — la_apply_commit_list: 호출당 커밋 노드 하나 소비.
9.2 la_apply_repl_log — 하나의 트랜잭션 아이템 체인 소비
섹션 제목: “9.2 la_apply_repl_log — 하나의 트랜잭션 아이템 체인 소비”트랜잭션 단위 팬아웃은 trid에 해당하는 LA_APPLY를 조회한 뒤 apply->head를 순회하며 각 아이템을 재실행한다.
루프 진입 전 분기 (la_lock_dbname과 루프 이전):
la_find_apply_list(tranid) == NULL이면 NO_ERROR 반환(버퍼링된 적 없음); rectype == LOG_ABORT면 la_clear_applied_info를 호출하고 반환한다. 이미 적용 완료된 경우 가드 apply->head == NULL || LSA_LE(commit_lsa, &la_Info.last_committed_lsa) — 버퍼가 비어 있거나 commit_lsa <= 10장의 재시작 북마크인 경우 — 는 이전 실행이 처리했음을 의미한다. 이때 LOG_SYSOP_END는 la_free_all_repl_items(아이템만 해제; 긴 트랜잭션의 나머지를 위해 리스트 노드는 재사용)를, 다른 타입은 전체 리셋을 위해 la_clear_applied_info를 호출한다. 그 외에는 슬레이브 DB 이름 락을 잡고 루프에 진입한다.
재실행 루프 — 아이템별 디스패치:
// la_apply_repl_log -- src/transaction/log_applier.c (loop, condensed)while (item) { /* ... periodic la_release_all_page_buffers every LA_MAX_REPL_ITEM_WITHOUT_RELEASE_PB ... */ if (LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa) && la_need_filter_out (item) == false) { /* <- skip already-done + filtered */ 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; default: assert_release (false); } /* <- corruption */ else if (item->log_type == LOG_REPLICATION_STATEMENT) error = la_apply_statement_log (item); else assert_release (false); if (error == NO_ERROR) LSA_COPY (&la_Info.committed_rep_lsa, &item->lsa); /* advance bookmark */ else { /* error sub-branches -- below */ } } next_item = la_get_next_repl_item (item, apply->is_long_trans, &apply->last_lsa); la_free_repl_item (apply, item); item = next_item; if (item != NULL && LSA_GT (&item->lsa, commit_lsa)) { assert (rectype == LOG_SYSOP_END); has_more_commit_items = true; break; }}log_type은 데이터 vs. 구문 경로를 선택하고, 데이터의 경우 item_type이 동사를 선택한다. RVREPL_DATA_UPDATE_START와 _END 모두 la_apply_update_log로 흘러간다 — START/END 브라켓은 마스터에서만 의미가 있었다(4장). 그 외 데이터 item_type이나 알 수 없는 log_type은 assert_release(false)를 발생시킨다.
불변 조건: committed_rep_lsa는 성공적인 아이템 적용에서만, 오직 앞 방향으로만 전진한다. error == NO_ERROR 이후에만 item->lsa를 LSA_COPY하며, 건너뛰기 가드 LSA_GT(&item->lsa, &last_committed_rep_lsa)는 재시작 기준선을 읽어 재실행 시 이미 처리된 아이템을 중복 적용하지 않도록 한다. 이를 어기면 배치 도중 크래시 발생 시 행이 누락되거나 이중 적용된다.
오류 하위 분기 (error != NO_ERROR일 때의 else):
- 플러시 오류 (
LA_IS_FLUSH_ERROR(error)):goto end— 호출자가 재연결 후 해당 트랜잭션 전체를 다시 시도한다. - 서버 다운 (
ER_IS_SERVER_DOWN_ERROR(er_errid())):ER_NET_CANT_CONNECT_SERVER로 변환 후goto end. - 재시도 가능, 무시 불가 (
la_ignore_on_error가 false이고la_retry_on_error가 true):er_set으로ER_HA_GENERIC_ERROR알림을 설정하고LA_SLEEP(10, 0)후continue— 아이템을 전진하거나 해제하지 않고while로 돌아가 슬립 후 동일 아이템을 재시도한다(LA_RETRY_ON_ERROR루프). - 무시 가능 (else): 진행되며 아이템 해제, 루프 전진 — 실패가 삼켜진다.
루프 전진 / 긴 트랜잭션 break. la_get_next_repl_item은 apply->is_long_trans 여부에 따라 다르게 동작한다: 일반 트랜잭션은 item->next를 반환(la_get_next_repl_item_from_list); is_long_trans일 때만 apply->last_lsa로 로그를 다시 읽는다(la_get_next_repl_item_from_log). 다음 아이템의 LSA가 commit_lsa를 초과하면 LOG_SYSOP_END의 부분 커밋임을 의미하므로(assert 검증), 루프를 has_more_commit_items = true로 설정한 뒤 break한다. end 이후: LOG_SYSOP_END의 경우 commit_lsa 이후에 남은 아이템은 la_free_and_add_next_repl_item으로 재연결되며(그 외 타입은 la_free_all_repl_items로 리스트를 비우되 노드는 유지); 일반 LOG_COMMIT은 la_clear_applied_info로 전체 리셋한다.
9.3 세 가지 데이터 DML 적용자 — 공통 구조
섹션 제목: “9.3 세 가지 데이터 DML 적용자 — 공통 구조”la_apply_insert_log, la_apply_update_log, la_apply_delete_log는 모두 la_flush_repl_items(false)로 시작하고 동일한 두 갈래 꼬리로 끝난다: 오류 → la_log_apply_error(fail_counter 증가), 성공 → 동사별 카운터와 num_unflushed 증가. 그 사이에서 두 가지 실질적인 차이가 있다(Figure 9-2): insert와 update는 로그 페이지를 고정하고 after-image를 재구성하지만 delete는 그렇지 않으며, 두 이미지 재구성자는 서로 다른 시점에 클래스를 결정한다.
flowchart TB
B["la_flush_repl_items(false)"] --> P["la_get_page(item->target_lsa.pageid)"]
P -- NULL --> XR["er_errid() 반환"]
P -- ok --> CIN{"insert: db_find_class 먼저\nupdate: 가드 이후로 클래스 지연"}
CIN --> E["la_get_recdes(&target_lsa, pgptr, recdes, &rcvindex, is_mvcc_class)"]
E --> F{"recdes.type이 ASSIGN_ADDRESS\n또는 RELOCATION?"}
F -- 예 --> X["ER_FAILED -> end"]
F -- 아니오 --> G{"rcvindex가 허용 집합에 있는가?"}
G -- 아니오 --> X
G -- 예 --> J["la_repl_add_object(class_obj, item, recdes)"]
J --> K["카운터++ ; num_unflushed++ ; la_release_page_buffer"]
Figure 9-2 — insert/update 공통 적용 흐름. delete는 P-G 단계와 page release를 건너뛴다.
동사별 클래스 결정 순서가 다르다. la_apply_insert_log는 la_get_recdes 이전에 db_find_class를 호출한다. is_mvcc_class = la_is_mvcc_class(ws_oid(class_obj))를 구해서 전달해야 하기 때문이다(이 플래그가 §9.5의 loaddb 업컨버전을 제어한다). la_apply_update_log는 반대: is_mvcc_class = false를 하드코딩하여 la_get_recdes를 먼저 호출하고, 가드를 거친 뒤 db_find_class를 호출한다 — update redo 로그가 이미 완성된 이미지를 담고 있기 때문에 이 순서가 맞다. 따라서 클래스 없음 오류는 insert에서는 일찍 종료되지만 update에서는 가드 이후에 종료된다.
타입 가드도 다르다. 둘 다 먼저 recdes->type == REC_ASSIGN_ADDRESS || REC_RELOCATION(la_get_recdes를 빠져나온 간접 주소)을 ER_FAILED로 거부한다. 이후 insert는 RVHF_INSERT/RVHF_MVCC_INSERT만 허용하고, update는 RVHF_UPDATE, RVOVF_CHANGE_LINK, RVHF_MVCC_INSERT, RVHF_UPDATE_NOTIFY_VACUUM, RVHF_INSERT_NEWHOME을 허용한다.
페이지 고정 생애주기는 insert/update에만 있다. 두 함수 모두 la_get_page(item->target_lsa.pageid)로 힙 redo 로그 페이지를 pin한다 — NULL 반환은 end 레이블 이전에 즉시 return er_errid()로 나간다(카운터 없음, release 없음) — 그리고 종료 시 la_release_page_buffer(old_pageid)를 호출한다.
delete는 기본 키(pkey)만 사용하는 경로다. la_apply_delete_log는 로그 페이지나 la_get_recdes를 건드리지 않는다 — item->packed_key_value에 있는 키만 필요하여 la_repl_add_object(class_obj, item, NULL)을 호출한다; 오류 시 la_log_apply_error(..., ER_HA_LA_FAILED_TO_APPLY_DELETE), 성공 시 delete_counter/num_unflushed를 증가시킨다.
// la_apply_insert_log -- src/transaction/log_applier.c (condensed)pgptr = la_get_page (item->target_lsa.pageid);if (pgptr == NULL) return er_errid (); /* page-fix failure: before end label */class_obj = db_find_class (item->class_name); /* insert: class resolved FIRST */recdes = la_assign_recdes_from_pool ();is_mvcc_class = la_is_mvcc_class (ws_oid (class_obj));error = la_get_recdes (&item->target_lsa, pgptr, recdes, &rcvindex, la_Info.rec_type, is_mvcc_class);// ... guards on recdes->type and rcvindex, then ...error = la_repl_add_object (class_obj, item, recdes);9.4 la_get_recdes — 다섯 가지 경우의 after-image 재구성자
섹션 제목: “9.4 la_get_recdes — 다섯 가지 경우의 after-image 재구성자”힙 redo 레코드의 target LSA를 받아 la_get_recdes는 온디스크 RECDES를 생성한다. is_mvcc_class 입력(§9.3에서 전달, update는 false)은 §9.5 MVCC 조정을 끝 부분에서만 제어한다. 기본 호출은 항상 la_get_log_data(§9.6)이며, recdes->data/length를 채우고 recdes->type = *(INT16 *) rec_type으로 설정한다. 이후 if/else if 분기가 아래 네 가지 간접 케이스 중 정확히 하나를 실행한다(다섯 번째는 암묵적인 “해당 없음” 케이스).
| 케이스 | 조건 | 의미 | 헬퍼 |
|---|---|---|---|
| 1 | RVOVF_CHANGE_LINK | 오버플로 레코드 업데이트; redo는 링크만 변경함. 재이어붙임, REC_BIGONE 강제. | la_get_overflow_recdes(RVOVF_PAGE_UPDATE) |
| 2 | type == REC_BIGONE | 신규 오버플로 insert: home 슬롯이 오버플로 페이지 체인을 가리킴. | la_get_overflow_recdes(RVOVF_NEWPAGE_INSERT) |
| 3 | RVHF_INSERT + REC_ASSIGN_ADDRESS | 지연 insert: 먼저 슬롯 예약, 실제 이미지는 같은 trid의 이후 UPDATE에 있음. | la_get_next_update_log (BIGONE이면 이후 케이스 2) |
| 4 | RVHF_UPDATE/_NOTIFY_VACUUM + REC_RELOCATION | home 슬롯이 전달; 실제 이미지는 재배치된 RVHF_INSERT_NEWHOME에 있음. | la_get_relocation_recdes |
각 분기는 헬퍼의 rec_type에서 recdes->type을 다시 읽는다; 케이스 3은 추적한 실제 행이 REC_BIGONE이면 케이스 2로 다시 진입한다. 해당하는 것이 없으면(일반 REC_HOME 경로) 기본 이미지가 이미 완성된 상태 — 암묵적인 다섯 번째 케이스. 이후 §9.5 MVCC 조정이 실행된다.
9.5 슬레이브에서의 MVCC 헤더 주입
섹션 제목: “9.5 슬레이브에서의 MVCC 헤더 주입”마스터 힙 레코드에는 슬롯 상주 MVCC insert id가 없을 수 있다; 슬레이브는 자신의 서버가 기대하는 레이아웃으로 재구성한다. RVHF_MVCC_INSERT에서는 la_make_room_for_mvcc_insid(recdes)를 바로 호출한다. 두 번째 분기(is_mvcc_class && *rcvindex == RVHF_INSERT && type != REC_BIGONE)는 CS 모드 마스터에서의 loaddb 벌크 insert를 위한 것으로, 이 경우 행은 MVCC 플래그 없이 일반 RVHF_INSERT로 로깅된다. 이 분기는 VALID_INSID를 설정하고 la_make_room_for_mvcc_insid를 호출하며, recdes->length > la_Info.maxslotted_reclength(레코드가 REC_BIGONE으로 오버플로될 것)이면 VALID_DELID|VALID_PREV_VERSION도 설정하고 la_make_room_for_mvcc_delid_and_prev_ver를 호출한다. 두 헬퍼 모두 플래그 전제조건을 assert하고, LA_MOVE_INSIDE_RECORD로 본문을 오른쪽으로 시프트한다; LA_MOVE_INSIDE_RECORD는 area_size >= length + delta를 assert하므로 버퍼가 작으면 메모리를 오염시키는 대신 assert가 발생한다.
9.6 la_get_log_data와 압축 해제 경로
섹션 제목: “9.6 la_get_log_data와 압축 해제 경로”la_get_log_data는 lrec->type을 분기하여 힙 로그 레코드 하나의 데이터 영역을 읽는다:
LOG_UNDOREDO_DATA/LOG_DIFF_UNDOREDO_DATA/ MVCC 변형 —MVCC_UNDOREDOvsUNDOREDO레이아웃을 선택한다.match_rcvindex == 0또는 레코드의rcvindex가 일치하면*rcvindex/*logs를 공개; 그렇지 않으면*logs = NULL(“원하는 레코드가 아님”). DIFF 타입은 undo 이미지를 위해la_get_undoredo_diff를 호출하고, 나머지는 그 앞을 정렬 이동한다.LOG_UNDO_DATA/LOG_MVCC_UNDO_DATA및LOG_REDO_DATA/LOG_MVCC_REDO_DATA—LOG_REC_UNDO/LOG_REC_REDO위에서 동일한 일치/공개 처리.default—*logs = NULL, 데이터 없음.
ZIP_CHECK(temp_length)이면 redo는 log_unzip되고 la_get_zipped_data가 최종 이미지를 생성한다 — DIFF의 경우 undo와 압축 해제된 redo를 log_diff(XOR)하고, 그 외에는 단순 복사 — 요청 시 앞의 INT16 rec_type을 제거하고 is_overflow이면 할당한다. 그 외에는 원시 페이로드를 직접 복사한다.
9.7 세 가지 체인 추적 헬퍼
섹션 제목: “9.7 세 가지 체인 추적 헬퍼”**la_get_overflow_recdes**는 prev_tranlsa를 역방향으로 순회하며 해당 trid의 모든 LOG_REDO 타입 레코드를 앞에 붙이고(head 끝이 먼저), trid 변경 또는 LOG_DUMMY_OVF_RECORD에서 멈춘 뒤, LA_OVF_FIRST_PART/LA_OVF_REST_PARTS 헤더를 제거하며 부분들을 연결한다. malloc 실패 시 부분 리스트를 해제하고 ER_OUT_OF_VIRTUAL_MEMORY를 반환한다.
la_get_next_update_log(지연 insert 추적자)는 prev_lrec->forw_lsa에서 순방향으로 스캔하며, 예약된 슬롯의 data.{pageid,offset,volid}와 일치하고 rcvindex가 RVHF_UPDATE/_NOTIFY_VACUUM인 UPDATE를 찾는다. 실패 모드: 해당 trid의 LOG_COMMIT/LOG_ABORT를 먼저 만나면 ER_GENERIC_ERROR를 반환한다 — 예상된 update가 끝내 오지 않은 것이다.
**la_get_relocation_recdes**는 prev_tranlsa를 한 홉 역방향으로 따라가 RVHF_INSERT_NEWHOME 레코드를 찾는다(match_rcvindex == RVHF_INSERT_NEWHOME으로 la_get_log_data를 호출); prev LSA가 null이거나 trid가 불일치하면 ER_LOG_PAGE_CORRUPTED를 발생시킨다.
9.8 la_disk_to_obj와 la_repl_add_object — 슬레이브 서버로
섹션 제목: “9.8 la_disk_to_obj와 la_repl_add_object — 슬레이브 서버로”la_disk_to_obj(SQL 로깅 경로 전용, 메인 플러시에서는 아님)는 RECDES를 DB_OTMPL로 변환하며, MVCC 헤더를 파싱하여 건너뛸 크기를 결정한다: mvcc_flags == 0이면 chn만 지나치고; 그 외에는 INSID/DELID(OR_MVCCID_SIZE), chn, 플래그가 있으면 PREV_VERSION LOG_LSA를 지나친 뒤 la_get_current로 컬럼을 읽는다. buf->ptr > buf->endptr 오버런은 ER_TF_BUFFER_OVERFLOW를 반환한다.
la_repl_add_object는 슬레이브 서버로 향하는 브릿지다. au_fetch_class 또는 sm_flush_objects가 실패하면 일찍 반환하고, delete의 경우 sm_partitioned_class_type 건너뜀 이후, item_type을 switch에서 연산으로 매핑한다 — UPDATE_* → LC_UPDATE_OPERATION_TYPE, INSERT → LC_INSERT_OPERATION_TYPE, DELETE → LC_FLUSH_DELETE, default: assert(false) — 최종적으로 __gv_loc_repl.ws_add_to_repl_obj_list(class_oid, item->packed_key_value, ..., recdes, operation, has_index)로 객체를 큐에 넣는다. 네트워크 플러시는 이후 la_flush_repl_items에서 발생한다.
9.9 la_need_filter_out — 아이템별 테이블 필터링
섹션 제목: “9.9 la_need_filter_out — 아이템별 테이블 필터링”데이터 아이템 처리 전에 la_apply_repl_log가 이 함수를 호출한다. item->class_name에서 [...] 인용 부호를 제거한 뒤, 다음 경우에는 false(필터링 안 함)를 반환한다: filter->type == REPL_FILTER_NONE이거나, 대부분의 구문 복제(log_type == LOG_REPLICATION_STATEMENT && item_type != CUBRID_STMT_TRUNCATE — truncate, 즉 테이블 전체 삭제만 필터링 가능), 또는 시리얼 시스템 클래스(CT_SERIAL_NAME, 시퀀스 상태 일관성을 위해 항상 복제해야 함). 그 외에는 filter->list에서 클래스를 스캔하여 REPL_FILTER_INCLUDE_TBL && !filter_found 또는 REPL_FILTER_EXCLUDE_TBL && filter_found이면 true를 반환한다.
9.10 la_apply_statement_log — 구문 재실행과 결정성
섹션 제목: “9.10 la_apply_statement_log — 구문 재실행과 결정성”이 경로는 행 이미지가 아닌 SQL 텍스트를 재실행한다: 대기 중인 행 객체를 강제로 플러시하고(la_flush_repl_items(true)), 필요하면 item->db_user(저장 프로시저와 사용자 DDL은 need_set_user = false로 건너뜀)와 item->ha_sys_prm을 호출 전후로 설정하며, res = la_update_query_execute(stmt_text, false)를 실행하고, 영향받은 행 수를 분류한다(DDL → schema_counter; INSERT/DELETE/UPDATE → 해당 카운터). TRUNCATE는 필터링된 경우 NO_ERROR로 단락 처리; CUBRID_STMT_DROP_LABEL/default는 NO_ERROR 반환(미지원, 무시).
비정상 경로 분기:
- 사용자 없음 —
need_set_user이고db_user가 설정된 경우,au_find_user(item->db_user) == NULL이면 서버 다운er_errid()는ER_NET_CANT_CONNECT_SERVER로 변환되고, 그 외에는error = er_errid()(0이 아님을 assert); 어느 경우든 switch에서 break한다. - 쿼리 실행 실패 —
res < 0이면error = er_errid()이며,ER_IS_SERVER_DOWN_ERROR(error)는 다시ER_NET_CANT_CONNECT_SERVER로 변환된다. - 최종 실패 처리 — switch 이후
error != NO_ERROR이면er_set으로ER_HA_LA_FAILED_TO_APPLY_STATEMENT(클래스, 구문, 오류, 메시지)를 설정하고 반환 전에la_Info.fail_counter++를 증가시킨다.
슬레이브에서 SQL을 실행하기 때문에, 구문 기반 DML은 비결정적 함수의 영향을 그대로 받는다(NOW(), RAND(), …) — CUBRID는 생성 시점에 이런 함수를 재작성하거나 차단하지 않는다. 이것이 동반 문서의 미결 질문에서 언급한 다이버전스 위험이다(cubrid-ha-replication.md §“Open Questions” 항목 8). 행 기반 복제(§9.3–9.8)는 레시피가 아닌 구체화된 after-image를 전송함으로써 이 문제를 피한다.
9.11 챕터 요약 — 핵심 정리
섹션 제목: “9.11 챕터 요약 — 핵심 정리”la_apply_commit_list는 호출당 커밋 하나를 소비하고 그 LSA를 내구성 북마크 후보로 반환한다;log_record_time(딜레이 클록)은LOG_COMMIT에서만 갱신된다.la_apply_repl_log는 아이템 체인을 LSA 순서대로 재실행하며,log_type후item_type에 따라 디스패치한다(UPDATE_START/_END모두 update로 라우팅);la_get_next_repl_item은 보통item->next를 반환하고is_long_trans일 때만last_lsa로 로그를 다시 읽는다.committed_rep_lsa는 성공적인 적용 후에만, 앞 방향으로만 전진한다 — 건너뛰기 가드가 재실행을 멱등으로 만든다. 재시도 가능 오류는LA_SLEEP(10,0)+continue로 전진 없이 처리되고; 플러시/서버 다운은 재연결을 위해goto end; 무시 가능 오류는 삼켜진다.- delete는 기본 키 전용 경로다(로그 페이지 없음,
la_repl_add_object(..., NULL)); insert와 update는 힙 redo 로그 페이지를 pin하고(la_get_page/la_release_page_buffer) after-image를 재구성하며, 허용되는rcvindex집합과 클래스 결정 순서에서 차이가 있다 — insert는la_get_recdes이전에 클래스(와la_is_mvcc_class)를 결정하고, update는 가드 이후에 결정한다. la_get_recdes는 다섯 가지 레코드 형태를 처리한다 — 일반REC_HOME,RVOVF_CHANGE_LINK오버플로 업데이트,REC_BIGONE오버플로 insert,RVHF_INSERT+REC_ASSIGN_ADDRESS지연 insert(la_get_next_update_log),RVHF_UPDATE*+REC_RELOCATION재배치(la_get_relocation_recdes).- 슬레이브는 MVCC 헤더를 재구성한다 —
la_make_room_for_mvcc_insid/..._delid_and_prev_ver로(특히 loaddb 행 업컨버전을 위해),la_get_log_data/la_get_undoredo_diff/la_get_zipped_data의 압축 해제 및 XOR diff 디코드 이후에 실행된다. la_repl_add_object와la_apply_statement_log모두 하드 오류 종료 지점이 있다 — 전자는au_fetch_class/sm_flush_objects실패 시 일찍 반환하고 알 수 없는 동사에 assert를 발생시키며; 후자는NO_ERROR가 아닌 모든 종료 시fail_counter를 증가시키고ER_HA_LA_FAILED_TO_APPLY_STATEMENT를er_set하며, 서버 다운을ER_NET_CANT_CONNECT_SERVER로 변환한다.
Chapter 10: 내구성 있는 적용 북마크와 재시작 복구
섹션 제목: “Chapter 10: 내구성 있는 적용 북마크와 재시작 복구”applylogdb(7~9장)는 부팅 시 어디까지 적용을 마쳤는가를 반드시 알아야 한다.
CUBRID는 이 북마크를 슬레이브 카탈로그 테이블 _db_ha_apply_info에 저장하고,
해당 배치의 행 적용과 동일한 슬레이브 트랜잭션 안에서 갱신한다 — 북마크 전진과
행 적용 커밋이 원자적으로 묶이는 것이다. 이 장에서는 구조체 미러(LA_HA_APPLY_INFO),
기록 함수(la_log_commit), 부팅 시 읽기(la_get_last_ha_applied_info),
SQL 헬퍼, 하한선(la_find_required_lsa), 아카이브 정리(la_remove_archive_logs)를 다룬다.
내구성 있는 커서가 왜 필요한지는 companion 문서의 “위치 커서와 내구성 북마크를 이용한
전방향 로그 탐색”과 “복제 레코드와 커밋 레코드의 원자적 방출” 절에서 설명하므로
여기서는 다시 유도하지 않는다.
10.1 LA_HA_APPLY_INFO — 카탈로그 행의 인메모리 미러
섹션 제목: “10.1 LA_HA_APPLY_INFO — 카탈로그 행의 인메모리 미러”LA_HA_APPLY_INFO는 (db_name, copied_log_path)를 키로 하는 _db_ha_apply_info
행 하나와 일대일로 대응하는 구조체다. 전달용 구조체로서: la_get_ha_apply_info가
SELECT로 채우고, 그 값들을 수명이 긴 전역 la_Info에 복사한 뒤,
la_update_ha_last_applied_info로 카탈로그에 다시 기록된다.
// struct la_ha_apply_info -- src/transaction/log_applier.cstruct la_ha_apply_info{ char db_name[256]; DB_DATETIME creation_time; char copied_log_path[4096]; LOG_LSA committed_lsa; /* last committed commit log lsa */ LOG_LSA committed_rep_lsa; /* last committed replication log lsa */ LOG_LSA append_lsa; /* append lsa of active log header */ LOG_LSA eof_lsa; /* eof lsa of active log header */ LOG_LSA final_lsa; /* last processed log lsa */ LOG_LSA required_lsa; /* start lsa of first txn to be applied */ DB_DATETIME log_record_time; DB_DATETIME log_commit_time; DB_DATETIME last_access_time; int status; INT64 insert_counter; INT64 update_counter; INT64 delete_counter; INT64 schema_counter; INT64 commit_counter; INT64 fail_counter; DB_DATETIME start_time;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
db_name | 행 키 1번; 마스터 DB 접두어. | 마스터별 행 구분. |
creation_time | 마스터 log_hdr->db_creation 스냅샷. | 로그 재생성 방어용 부팅 가드(10.3). |
copied_log_path | 행 키 2번; 슬레이브 log_path. | 두 copylog 스트림 구분. |
committed_lsa | 가장 높이 커밋된 commit LSA. | ”얼마나 복제됐는가”; 재적용 하한. |
committed_rep_lsa | 가장 높이 커밋된 REPL LSA. | 별도 스트림; REPL 필터 하한. |
append_lsa | 기록 시점 마스터 헤더 append_lsa. | 지연 진단. |
eof_lsa | 마스터 헤더 eof_lsa. | 지연 진단; 읽기 가능 경계. |
final_lsa | 마지막으로 처리(읽기+디스패치)한 LSA. | 재시작 재개 커서; la_Info.final_lsa 시드. |
required_lsa | 가장 오래된 열린 트랜잭션의 시작 LSA. | 아카이브 정리 하한(10.6~10.7). |
log_record_time | 마지막으로 적용된 커밋 레코드 시각(마스터). | 복제 지연 지표. |
log_commit_time | 슬레이브 커밋 벽시계 시각. | 지연 / 운영자 가시성. |
last_access_time | 마지막 기록 시점의 SYS_DATETIME. | 활성 상태 heartbeat. |
status | LA_STATUS_IDLE / _BUSY. | 읽기 시 강제 IDLE(10.2). |
insert_counter … fail_counter | 누적 행/실패 카운터. | 재시작 후에도 유지; 저장 값에서 재개. |
start_time | 이 인스턴스가 (재)시작한 시각. | ”이번 부팅 이후”와 누적값 구분. |
la_init_ha_apply_info는 구조체를 0으로 초기화한 뒤 LSA 필드 여섯 개에 NULL 마크를 찍는다:
// la_init_ha_apply_info -- src/transaction/log_applier.cmemset ((void *) ha_apply_info, 0, sizeof (LA_HA_APPLY_INFO));LSA_SET_NULL (&ha_apply_info->committed_lsa);/* ... committed_rep, append, eof, final ... */LSA_SET_NULL (&ha_apply_info->required_lsa);memset은 바이트를 0으로 채우지만, LSA_SET_NULL은 LSA_ISNULL이 검사하는
센티넬 (pageid = -1)을 기록한다 — 0인 LSA는 유효한 page-0 위치이지 “없음”이
아니다. 덕분에 la_get_last_ha_applied_info는 “컬럼이 SQL NULL이었다”와
“컬럼 값이 0이었다”를 구별할 수 있다.
불변 조건 — 이 구조체는 카탈로그 행 하나의 엄격한 미러다. 모든 필드는
_db_ha_apply_info컬럼 하나에 대응하며, LSA는_pageid/_offset쌍으로 분할된다.la_get_ha_apply_info는col_cnt == LA_OUT_VALUE_COUNT(SELECT가 정확히 23개 컬럼을 반환했는지)와 디코딩 후out_value_idx == LA_OUT_VALUE_COUNT를 모두 assert한다. 컬럼이 늘어난 스키마는 이 가드가 잡아낸다. INSERT 미러인la_insert_ha_apply_info는in_value_idx == LA_IN_VALUE_COUNT(15개 바인드 파라미터; 나머지 11개 컬럼은 SQL 리터럴)를 assert한다. 이 카운트를 올리지 않고 컬럼을 추가하면assert_release가 발화한다 — 스키마와 구조체는 조용히 벌어질 수 없다.
flowchart LR cat["_db_ha_apply_info 행\n(LSA 컬럼은 pageid/offset 분리)"] mem["LA_HA_APPLY_INFO\n(임시)"] glob["la_Info\n(상주)"] cat -->|"la_get_ha_apply_info SELECT"| mem mem -->|"la_get_last_ha_applied_info 복사"| glob glob -->|"la_update_ha_last_applied_info UPDATE"| cat
Figure 10-1. 카탈로그 행, 임시 구조체 이미지, 상주 전역 — 그리고 세 함수가 데이터를 이동하는 방향.
10.2 행 읽기 — la_get_ha_apply_info
섹션 제목: “10.2 행 읽기 — la_get_ha_apply_info”(prefix_name, log_path)를 키로 CT_HA_APPLY_INFO_NAME을 파라미터화된
SELECT로 조회하고, 영향받은 튜플 수를 res로 반환한다(>0 발견, 0 없음, <0 오류).
분기 흐름:
db_find_class (CT_HA_APPLY_INFO_NAME) == NULL→ 카탈로그 클래스 없음;assert (er_errid () != NO_ERROR), 오류 id 반환(“DB가 아직 HA 준비 안 됨” 경로).db_execute_with_values (...)→res.res > 0일 때만 튜플을 디코딩; 그 외엔 정리 후res반환(0 = 행 없음, 호출자의 INSERT 경로).db_query_first_tupleswitch —DB_CURSOR_SUCCESS이면 out-value 23개 전체 디코딩 (LSA 6개 컬럼은 양쪽 half 모두DB_IS_NULL검사, NULL이면LSA_SET_NULL+out_value_idx += 2;append_lsa/eof_lsa는 무조건 읽음;status는 강제로LA_STATUS_IDLE).END/ERROR/default→res = ER_FAILED.- 정리:
db_query_end+ 모든 in-value를db_value_clear, 항상 도달; out-value는SUCCESS분기에서만 정리.
NULL-허용 디코딩은 la_init_ha_apply_info를 반영한다: 합법적으로 NULL인 컬럼
(예: 첫 커밋 전 committed_lsa)은 엉뚱한 page-0 위치가 아닌 LSA_ISNULL로
디코딩된다.
10.3 부팅 시 커서 초기화 — la_get_last_ha_applied_info
섹션 제목: “10.3 부팅 시 커서 초기화 — la_get_last_ha_applied_info”재시작 복구 진입점으로, applylogdb 시작(7장) 시 한 번 실행되며
재개 LSA를 결정한다:
// la_get_last_ha_applied_info -- src/transaction/log_applier.clog_db_creation = act_log->log_hdr->db_creation;db_localdatetime (&log_db_creation, &log_db_creation_time);res = la_get_ha_apply_info (la_Info.log_path, act_log->log_hdr->prefix_name, &apply_info);if (res > 0) { LSA_COPY (&la_Info.committed_lsa, &apply_info.committed_lsa); /* ... committed_rep, append, eof, final, required, counters ... */ if ((log_db_creation_time.date != apply_info.creation_time.date) || (log_db_creation_time.time != apply_info.creation_time.time)) return ER_FAILED; /* <- 저장된 행이 다른 DB 인카네이션 */ if (LSA_ISNULL (&la_Info.required_lsa)) { er_set (...ER_HA_GENERIC_ERROR...); return ER_FAILED; } /* <- 비정상 행 */ }else if (res == 0) insert_apply_info = true; /* <- 최초 부팅: 아직 행 없음 */else return res; /* <- SELECT 오류, 전파 */최상위 분기는 세 가지 — 행 발견, 행 없음(INSERT 플래그), 오류(반환) —
그리고 발견 분기 내부의 두 가지 하드 실패: creation-time 불일치
(복사된 로그가 다른 DB 인카네이션으로 교체됨)와 NULL required_lsa
(기존 행은 반드시 이 값을 가져야 함). 이어서 네 개의 LSA_ISNULL 픽스업이
의존 순서에 따라 fresh-row 하한을 확립한다:
if (LSA_ISNULL (&la_Info.required_lsa)) LSA_COPY (&la_Info.required_lsa, &act_log->log_hdr->eof_lsa); /* anchor at master EOF */if (LSA_ISNULL (&la_Info.committed_lsa)) LSA_COPY (&la_Info.committed_lsa, &la_Info.required_lsa);/* same for committed_rep_lsa */if (LSA_ISNULL (&la_Info.final_lsa)) LSA_COPY (&la_Info.final_lsa, &la_Info.required_lsa); /* <- cursor seed */완전히 새 applier라면 네 값 모두 마스터의 현재 eof_lsa로 수렴한다(복제가 “지금”부터
시작됨). 재개 시에는 저장된 final_lsa가 non-NULL이므로 그대로 유지된다.
마지막으로 행을 영속화하고 시드를 고정한다:
if (insert_apply_info == true) res = la_insert_ha_apply_info (&log_db_creation_time);else res = la_update_ha_apply_info_start_time ();if (res == 0) return ER_FAILED; /* <- 0 affected rows == 실패 */else if (res < 0) return res;(void) db_commit_transaction ();LSA_COPY (&la_Info.last_committed_lsa, &la_Info.committed_lsa);LSA_COPY (&la_Info.last_committed_rep_lsa, &la_Info.committed_rep_lsa);불변 조건 —
last_committed_lsa는 역진을 막는 고수위 하한이다. 여기서 저장된committed_lsa로 시드되며,la_update_ha_last_applied_info가 기록 전에LSA_GE로 비교하는 값이다(10.4). 이것이 없으면 재시작 시 이전에 내구성을 확보한 것보다 낮은 committed LSA를 영속화해 마크를 조용히 되감을 수 있다.
10.3a 최초 부팅 INSERT — la_insert_ha_apply_info 분기별 분석
섹션 제목: “10.3a 최초 부팅 INSERT — la_insert_ha_apply_info 분기별 분석”la_get_last_ha_applied_info가 행을 찾지 못했을 때(res == 0) 호출된다.
26컬럼 행을 INSERT한다: 15개 바인드 ? 파라미터(db_name, creation time,
copied path, 6개 LSA pageid/offset 쌍)와 11개 SQL 리터럴(3개 시각 및
start_time에 SYS_DATETIME; status와 6개 카운터에 0). 파라미터 바인딩 후:
// la_insert_ha_apply_info -- src/transaction/log_applier.cassert_release (in_value_idx == LA_IN_VALUE_COUNT); /* <- 정확히 15개 바인드, 아니면 abort */
res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true);for (i = 0; i < in_value_idx; i++) db_value_clear (&in_value[i]);
if (res <= 0) { return res; /* <- INSERT 실패: log-info 파일 미기록 */ }
/* create log info */msg = msgcat_message (MSGCAT_CATALOG_CUBRID, MSGCAT_SET_LOG, MSGCAT_LOG_LOGINFO_COMMENT);if (msg == NULL) { msg = "COMMENT: %s for database %s\n"; /* <- msgcat 미스: 하드코딩 폴백 */ }fp = fopen (la_Info.loginf_path, "w");if (fp == NULL) { er_set (...ER_LOG_MOUNT_FAIL..., la_Info.loginf_path); /* <- 경고만, 치명적 오류 아님 */ }else { (void) fprintf (fp, msg, CUBRID_MAGIC_LOG_INFO, la_Info.loginf_path); fflush (fp); fclose (fp); /* <- .log_info에 magic comment 기록 */ }return res;바인딩 이후 네 개의 분기: assert_release 가드(10.1 불변 조건의 드리프트 검사);
INSERT 실패 시 .log_info 파일 미기록 상태로 res <= 0 조기 반환; msgcat 미스 시
리터럴 포맷 문자열 대체; 그리고 fopen 결과 — fp == NULL이면 비치명적
ER_LOG_MOUNT_FAIL 경고, 아니면 CUBRID_MAGIC_LOG_INFO 코멘트 기록 후
fflush/fclose. .log_info 파일은 최선 노력(best-effort)이다: res(영향받은 행 수)는
이미 > 0이며 어느 경로든 반환되므로, 이 파일 실패가 INSERT를 중단시키지 않는다.
10.4 행 기록 — la_update_ha_last_applied_info
섹션 제목: “10.4 행 기록 — la_update_ha_last_applied_info”la_log_commit에서 호출되며, 12개 LSA half, 2개 선택적 시각, 6개 카운터,
last_access_time = SYS_DATETIME을 (db_name, copied_log_path) 키로 UPDATE한다.
두 가지 핵심 로직:
// la_update_ha_last_applied_info -- src/transaction/log_applier.cif (LSA_GE (&la_Info.committed_lsa, &la_Info.last_committed_lsa)) { /* bind committed_lsa */ }else { /* bind last_committed_lsa instead */ } /* <- 마크 역진 방지 *//* committed_rep_lsa도 동일한 LSA_GE 가드; 시각은 IFNULL(?, col) 사용 */
res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true);if (res == 0) /* <- 0 rows matched: 행이 우리 모르게 삭제됨 */ { db_localdatetime (&la_Info.act_log.log_hdr->db_creation, &log_db_creation_time); res = la_insert_ha_apply_info (&log_db_creation_time); if (res > 0) res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true); }커밋된 컬럼은 MAX(현재값, last_committed) 방식으로 기록되므로 인메모리 값이
일시적으로 낮더라도 내구성 있는 마크가 역진되지 않는다. log_record_time /
log_commit_time은 IFNULL(?, col)을 사용해 커밋 레코드가 없는 배치는
저장된 타임스탬프를 그대로 둔다. 행이 삭제된 경우(UPDATE 0 rows) 함수는
la_insert_ha_apply_info로 재INSERT 후 UPDATE를 재시도해 la_Info 상태를
잃지 않는다(la_update_ha_apply_info_log_record_time에도 동일한 패턴). 커밋은
하지 않는다 — la_log_commit이 적용된 행과 북마크를 함께 커밋할 수 있도록
트랜잭션을 열어둔다.
10.5 커밋 지점 — la_log_commit
섹션 제목: “10.5 커밋 지점 — la_log_commit”la_log_commit (bool update_commit_time)은 슬레이브 적용 배치가 내구성을 얻는
단일 병목 지점이다. 디스패치 경로(8장)의 커밋 레코드, repl 목록이 비었을 때의
server-state 레코드, 그리고 메인 적용 루프에서 호출된다. required_lsa 재산정,
마스터 헤더 LSA 스냅샷, 버퍼링된 행 플러시, 북마크 UPDATE 스테이징, 커밋을
순서대로 수행하며 각 단계가 다음 단계를 조건화한다:
// la_log_commit -- src/transaction/log_applier.c(void) la_find_required_lsa (&la_Info.required_lsa); /* recompute floor */LSA_COPY (&la_Info.append_lsa, &la_Info.act_log.log_hdr->append_lsa);LSA_COPY (&la_Info.eof_lsa, &la_Info.act_log.log_hdr->eof_lsa);if (update_commit_time) la_Info.log_commit_time = time (0);error = la_flush_repl_items (true); /* push buffered rows */if (error != NO_ERROR) return error; /* <- 미플러시: 아무것도 커밋 안 됨 */res = la_update_ha_last_applied_info (); /* stage bookmark UPDATE */if (res > 0) error = la_commit_transaction (); /* <- 원자적: 행 + 북마크 */else { la_Info.fail_counter++; if (ER_IS_SERVER_DOWN_ERROR (res)) error = ER_NET_CANT_CONNECT_SERVER; /* loop reconnects */ else { er_set (...); error = NO_ERROR; } /* <- 삼킴, 동일 final_lsa로 재시도 */ }return error;flowchart TD
A["la_log_commit(update_commit_time)"] --> B["la_find_required_lsa"]
B --> C["헤더에서 append_lsa, eof_lsa 복사"]
C --> D{update_commit_time?}
D -->|yes| E["log_commit_time = time(0)"]
D -->|no| F["la_flush_repl_items(true)"]
E --> F
F --> G{플러시 오류?}
G -->|"!= NO_ERROR"| H["return error -- 아무것도 커밋 안 됨"]
G -->|NO_ERROR| I["la_update_ha_last_applied_info -> res"]
I --> J{res > 0?}
J -->|yes| K["la_commit_transaction -- 적용 + 북마크 원자적 커밋"]
J -->|no| L["fail_counter++"]
L --> M{ER_IS_SERVER_DOWN_ERROR?}
M -->|yes| N["error = ER_NET_CANT_CONNECT_SERVER"]
M -->|no| O["er_set HA_GENERIC; error = NO_ERROR -- 삼킴"]
K --> P["return error"]
N --> P
O --> P
Figure 10-2. la_log_commit 분기 지도. 모든 종료 경로는 행과 북마크를 함께 커밋하거나
아무것도 커밋하지 않는다.
불변 조건 — 적용과 북마크는 하나의 트랜잭션이다.
la_flush_repl_items는 행 변경을 수행하고la_update_ha_last_applied_info는 북마크 UPDATE를 스테이징한다. 둘 다 동일한 커넥션에서 중간 커밋 없이 실행되며, 단 하나의la_commit_transaction(db_commit_transaction호출)으로 함께 내구성을 얻는다. 스테이징 후 커밋 전에 크래시가 나면 둘 다 롤백된다 — 북마크가 “X까지 적용됨”이라고 기록했지만 X에 해당하는 행이 없는 상황이 생기지 않는다.
북마크 UPDATE 실패(res <= 0) 시 서버 다운 오류는 루프 재접속을 위해
ER_NET_CANT_CONNECT_SERVER로 변환되고, 그 외 실패는 fail_counter++ 후
NO_ERROR로 삼켜진다. 행은 커밋되지 않았으므로 다음 반복에서 동일한
final_lsa부터 재시도한다 — 비용은 멱등적 재적용 한 번이다(10.8).
10.6 하한선 계산 — la_find_required_lsa
섹션 제목: “10.6 하한선 계산 — la_find_required_lsa”required_lsa는 데몬이 여전히 다시 읽을 수 있는 가장 낮은 LSA다:
트랜잭션별 목록(8장)에서 가장 오래된 열린 트랜잭션의 start_lsa.
la_log_commit은 매번 커밋 전에 이를 재산정한다.
// la_find_required_lsa -- src/transaction/log_applier.cLSA_SET_NULL (&lowest_lsa);for (i = 0; i < la_Info.cur_repl; i++) { if (la_Info.repl_lists[i]->tranid <= 0) continue; /* <- 빈/닫힌 슬롯 */ if (LSA_ISNULL (&lowest_lsa) || LSA_GT (&lowest_lsa, &la_Info.repl_lists[i]->start_lsa)) LSA_COPY (&lowest_lsa, &la_Info.repl_lists[i]->start_lsa); }if (LSA_ISNULL (&lowest_lsa)) LSA_COPY (required_lsa, &la_Info.final_lsa); /* <- 열린 트랜잭션 없음: 하한 = 커서 */else LSA_COPY (required_lsa, &lowest_lsa);세 가지 분기: 닫힌 슬롯 건너뛰기(tranid <= 0), 최솟값 추적, 그리고
열린 트랜잭션이 없으면 final_lsa로 폴백해 유휴 기간에도 하한이 전진하고
아카이브가 정리될 수 있도록 한다.
불변 조건 —
required_lsa <= final_lsa이며, 열린 트랜잭션의 start보다 높을 수 없다.required_lsa엄밀히 하의 모든 것은 절대 다시 읽히지 않는다 — 이것이la_remove_archive_logs가 아카이브를 안전하게 삭제하기 위해 전제하는 조건이다.
10.7 슬레이브 로컬 아카이브 정리 — la_remove_archive_logs
섹션 제목: “10.7 슬레이브 로컬 아카이브 정리 — la_remove_archive_logs”copylogdb는 슬레이브 log_path에 아카이브 로그를 계속 추가한다(6장).
la_remove_archive_logs는 최대 개수 상한과 required_lsa 하한 모두에
의해 제어되며, 새 last_deleted_arv_num(삭제된 것이 없으면 기존 값)을 반환한다.
// la_remove_archive_logs -- src/transaction/log_applier.cif (LA_LOG_IS_IN_ARCHIVE (la_Info.required_lsa.pageid)) /* 하한이 아카이브 안에 있는가? */ { error = la_find_archive_num (&required_arv_num, la_Info.required_lsa.pageid); if (error != NO_ERROR) return last_deleted_arv_num; /* <- 위치 불명: 전부 보존 */ max_arv_count = MAX (log_max_archives, nxarv_num - required_arv_num); /* 하한 보호 */ }else max_arv_count = log_max_archives; /* 하한이 활성 로그에 있음: 상한만 적용 */if (current_arv_count > max_arv_count) /* nxarv_num - last_deleted - 1 */ { /* first = last_deleted+1; last = nxarv_num - max_arv_count - 1 */ if (last_arv_num_to_delete < 0 || last_arv_num_to_delete < first_arv_num_to_delete) return last_deleted_arv_num; /* <- 삭제 대상 없음 */ /* throttle: cap (last-first+1) at max_arv_count_to_delete */ for (i = first ...; i <= last_arv_num_to_delete; i++) fileio_unformat (NULL, archive_name); /* <- 물리 삭제 */ return last_arv_num_to_delete; }return last_deleted_arv_num; /* 상한 이하: 전부 보존 */분기 요약:
- 하한이 아카이브 안에 있을 때 (
pageid < nxarv_pageid):required_arv_num부터 위쪽 아카이브를 보존하도록max_arv_count를 올린다;la_find_archive_num실패 시 전부 보존하고 반환. - 하한이 활성 로그에 있을 때:
HA_COPY_LOG_MAX_ARCHIVES만 보존 개수를 제한. - 상한 이하이거나 삭제 대상 범위가 비었거나 음수: 변경 없음.
max_arv_count_to_delete스로틀: 호출자가 구간별 페이스 조절 시1을, 새 아카이브가 방금 나타났을 때는INT_MAX를 전달.
호출자는 반환값을 la_Info.last_deleted_archive_num에 반영하고,
la_update_last_deleted_arv_num으로 영속화한 뒤 la_Info.last_time_archive_deleted를
기록한다.
불변 조건 —
required_lsa아래의 아카이브는la_log_commit이 그 하한을 영속화한 이후에만 삭제된다.required_lsa는 적용 배치 트랜잭션 내부에서 (10.5) 루프가 정리 단계에 도달하기 전에 기록되므로, 영속화된 하한은 적용된 것보다 높을 수 없다. 삭제 후 다음 커밋 전에 크래시가 나도 안전하다: 내구성 있는required_lsa가 이미 삭제된 범위를 제외하고 있기 때문이다.
10.8 크래시 창과 재적용 멱등성
섹션 제목: “10.8 크래시 창과 재적용 멱등성”순서가 중요한 구간은 la_repl_add_object가 행을 bulk-flush 목록에 스테이징하는
시점(9장)과 la_log_commit이 이를 커밋하는 시점 사이뿐이다. 이 구간에서 크래시가
나면 트랜잭션이 롤백된다 — 스테이징된 행과 북마크 UPDATE 모두 사라진다(10.5 불변 조건).
재시작 시 la_get_last_ha_applied_info는 이전에 커밋된 final_lsa를 읽고
데몬은 그 범위를 재적용한다.
재적용은 멱등적이며, 두 가드 모두 la_apply_repl_log 안에 있다 — 단순히 대기 중인
커밋들을 꺼내 아래로 넘기는 la_apply_commit_list 호출자가 아니다.
la_apply_repl_log는 LSA_LE (commit_lsa, &la_Info.last_committed_lsa)인
커밋 전체를 건너뛰고(9장), 진행되는 커밋에 대해서는 각 REPL 항목을
LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa)인 것만 적용한다.
크래시 전에 커밋된 레코드는 이미 적용됨으로 버려지고, 스테이징만 됐던 레코드는
깨끗하게 재적용된다. 남은 엣지 케이스 — 재적용을 관찰하는 비멱등적 사용자
트리거 — 는 companion의 “Open Questions”에서 다루므로 그쪽을 참고하라.
10.9 챕터 요약 — 핵심 정리
섹션 제목: “10.9 챕터 요약 — 핵심 정리”LA_HA_APPLY_INFO는(db_name, copied_log_path)키를 갖는_db_ha_apply_info행 하나의 임시 일대일 이미지다. 상주 상태는la_Info에 필드별로 흩어져 있으며,LA_*_VALUE_COUNTassert가 구조체와 스키마의 드리프트를 차단한다.final_lsa는 재시작 재개 커서,required_lsa는 최소 필요 하한이다. 새 applier에서 둘 다 마스터의eof_lsa로 수렴하고,committed_lsa/committed_rep_lsa는 단조 증가하는 이미 적용된 마크다.la_insert_ha_apply_info는 최초 부팅 시(그리고 UPDATE 경로 내 자가 치유 단계로) 15-바인드 행을 한 번 기록한다. assert된 바인드 카운트 이후 INSERT 실패를 그대로 전파하며,.log_info파일은 최선 노력으로 취급해ER_LOG_MOUNT_FAIL에서 중단하지 않는다.la_log_commit은 단일 원자적 병목 지점이다: 버퍼링된 행을 플러시하고 북마크 UPDATE를 하나의 커넥션에서 스테이징한 뒤, 단 하나의la_commit_transaction으로 함께 커밋한다 — 함께 내구성을 얻거나 아무것도 얻지 않는다.la_update_ha_last_applied_info는LSA_GE가드로 커밋 마크의 역진을 차단하고, 삭제된 행을 재INSERT 후 UPDATE 재시도로 자가 치유한다.la_get_last_ha_applied_info는 creation-time 불일치나 NULLrequired_lsa시 낡은 북마크를 거부하며, 역진 방지 하한으로last_committed_lsa/last_committed_rep_lsa를 시드한다.la_find_required_lsa와la_remove_archive_logs가 함께 영속화된required_lsa위쪽의 슬레이브 로컬 아카이브 공간만 회수하므로,la_repl_add_object→la_log_commit크래시 창은 멱등적 재적용으로 안전하게 처리된다.
Chapter 11: 특수 경로 — 장기 트랜잭션, 역할 전환, 필터, TDE
섹션 제목: “Chapter 11: 특수 경로 — 장기 트랜잭션, 역할 전환, 필터, TDE”8장부터 10장까지는 정상 상태(steady-state)의 apply 생명주기를 다뤘다. 이 장은 그 흐름을 깨는 네 가지 경로를 모은다: 스테이징 한도를 초과하는 트랜잭션, 슬레이브를 강등하는 계획된 역할 전환, 이벤트를 버리는 테이블 수준 필터, 그리고 소켓으로 키를 공유해야 하는 TDE 암호화 페이지. 정상 상태 메커니즘과 master/slave 토폴로지에 대해서는 함께 참조하는 cubrid-ha-replication.md의 “Apply pipeline” 및 “Failover” 절을 전제한다.
11.1 장기 트랜잭션: 메모리를 아끼되 O(records) 재탐색을 감수하다
섹션 제목: “11.1 장기 트랜잭션: 메모리를 아끼되 O(records) 재탐색을 감수하다”8장은 la_set_repl_log가 각 REPL 레코드를 apply->head .. apply->tail 리스트에 추가하되, LA_MAX_REPL_ITEMS 한도 안에서만 유지함을 보였다. 이 한도를 초과하는 트랜잭션은 LA_APPLY 항목을 장기 트랜잭션 모드로 전환하고 스테이징된 아이템을 버린다:
// la_set_repl_log -- src/transaction/log_applier.c if (apply->is_long_trans) /* already long: track last_lsa only */ { LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; } if (apply->num_items >= LA_MAX_REPL_ITEMS) /* cap hit: switch to long mode */ { la_free_all_repl_items_except_head (apply); /* <- discard the staged list */ apply->is_long_trans = true; LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; }이제 항목은 start_lsa(la_log_record_process가 첫 레코드에서 설정)와 last_lsa(이후 모든 REPL 레코드에서 갱신)만 추적한다. 아이템 리스트는 head 하나만 남기고 사라진다.
불변 조건 — 장기 트랜잭션은 스테이징된 아이템 리스트가 아니라 로그 내의 start_lsa .. last_lsa로 완전히 기술된다. la_set_repl_log는 is_long_trans가 설정된 이후 추가를 거부하고, 매 레코드마다 last_lsa를 최신으로 유지함으로써 이를 강제한다.
커밋 시점에 la_apply_repl_log는 la_get_next_repl_item으로 아이템을 순회하는데, 이 함수는 플래그에 따라 분기한다: is_long_trans 가 true이면 → la_get_next_repl_item_from_log (item, last_lsa) (로그 재독); false이면 → la_get_next_repl_item_from_list (item), 즉 return item->next. 로그 재독 경로가 비용이 크다:
// la_get_next_repl_item_from_log -- src/transaction/log_applier.c LSA_COPY (&curr_lsa, &item->lsa); while (!LSA_ISNULL (&curr_lsa)) { curr_log_record = LOG_GET_LOG_RECORD_HEADER (la_get_page (curr_lsa.pageid), &curr_lsa); // ... condensed: snapshot start-record trid into prev_repl_log_record ... if (!LSA_EQ (&curr_lsa, &prev_repl_lsa) && prev_repl_log_record->trid == curr_log_record->trid) { if (LSA_GT (&curr_lsa, last_lsa) || curr_log_record->type == LOG_COMMIT || curr_log_record->type == LOG_ABORT || LSA_GE (&curr_lsa, &la_Info.act_log.log_hdr->eof_lsa)) break; if (curr_log_record->type == LOG_REPLICATION_DATA || curr_log_record->type == LOG_REPLICATION_STATEMENT) { next_item = la_make_repl_item (...); break; } } la_release_page_buffer (curr_lsa.pageid); LSA_COPY (&curr_lsa, &curr_log_record->forw_lsa); /* follow forward chain */ }루프의 모든 분기가 실제로 실행된다:
- 첫 번째 반복 —
prev_repl_log_record == NULL절(위에서 생략)에서 시작 레코드의 trid를prev_repl_log_record에 저장한다. - 시작 LSA와 동일 (
LSA_EQ) — 건너뜀; 루프가 넘겨받은 아이템을 자신이 반환하지 않는 이유다. - 다른 trid — 인터리브된 트랜잭션; 페이지를 해제하고
forw_lsa를 따른다. - 동일 trid,
last_lsa초과 / COMMIT / ABORT / EOF — 소진;next_item == NULL로break. - 동일 trid, REPL 레코드 —
la_make_repl_item,break.
각 조회는 forw_lsa를 앞 방향으로 따르므로, 전체 트랜잭션 적용 비용은 O(스팬된 로그 레코드 수)다 — 핵심 정리 2의 트레이드오프.
11.2 역할 전환: 계획된 페일오버에서 슬레이브 강등하기
섹션 제목: “11.2 역할 전환: 계획된 페일오버에서 슬레이브 강등하기”마스터는 모든 HA 서버 상태 변경(cub_master가 페일오버 시 구동 — cubrid-heartbeat.md 교차 참조)마다 LOG_DUMMY_HA_SERVER_STATE 마커를 기록한다. 어플라이어는 la_log_record_process에서 이를 처리한다:
// la_log_record_process, case LOG_DUMMY_HA_SERVER_STATE -- src/transaction/log_applier.c ha_server_state = la_get_ha_server_state (pg_ptr, final); /* NULL -> log + break */ if (ha_server_state->state != HA_SERVER_STATE_ACTIVE && ha_server_state->state != HA_SERVER_STATE_TO_BE_STANDBY) /* demotion */ { if (la_Info.db_lockf_vdes != NULL_VOLDES) /* <- still hold the db lock */ { la_Info.is_role_changed = true; return ER_INTERRUPTED; } /* <- clean stop */ } else if (la_is_repl_lists_empty ()) /* benign: la_log_commit (true) */분기 설명:
- ACTIVE/TO_BE_STANDBY를 벗어났고 db 락을 보유 중 — 더 이상 따를 마스터가 없다;
is_role_changed를 설정하고ER_INTERRUPTED(에러가 아닌 합의된 정상 종료 신호)를 반환한다. - 같은 강등 상태, 락 미보유 — 해제할 것이 없다; 그냥 통과.
- ACTIVE/TO_BE_STANDBY, 리스트 비어 있음 — 무해; 타임스탬프를 기록하고 북마크를 최신으로 유지하기 위해 강제 커밋.
- ACTIVE/TO_BE_STANDBY, 리스트 비어 있지 않음 — 여기서는 아무것도 하지 않는다; 대기 중인 트랜잭션이 정상적으로 적용된다.
ER_INTERRUPTED는 la_apply_log_file 메인 루프로 전파되고, is_role_changed == true이면 la_unlock_dbname (..., clear_owner = true)를 호출해 db 락의 소유권을 해제함으로써 새 마스터가 이를 획득할 수 있게 한다.
(LOG_DUMMY_CRASH_RECOVERY는 바로 위의 케이스인데 ER_INTERRUPTED를 반환하지만 is_role_changed는 설정하지 않는다 — 이 경우 인터럽트는 재fetch를 강제하면서 락은 유지한다.)
la_change_state는 보완적인 나머지 절반이다: 메인 루프 이터레이션마다 로그 헤더의 ha_server_state / ha_file_status를 기반으로 어플라이어 자신의 HA_LOG_APPLIER_STATE를 보고한다. 첫 번째 분기는 조기 반환이다 — last_server_state, last_file_state, last_is_end_of_record가 모두 현재 값과 같으면 return NO_ERROR. 그렇지 않으면 이중 switch로 목표 상태를 결정한다:
- 따라잡음 (
is_end_of_record && ha_file_status == LOG_HA_FILESTAT_SYNCHRONIZED):ACTIVE/TO_BE_STANDBY/TO_BE_ACTIVE→WORKING;DEAD/STANDBY/MAINTENANCE→DONE(그리고DEAD가 아닌 한, 대기 마스터는 드레인할 커밋을 생성하지 않으므로la_clear_all_repl_and_commit_list로 대기 리스트를 플러시한다). - 아직 따라잡는 중 (else): 모든 활성 상태 →
RECOVERING(두 switch 케이스가 이미-상태 가드만 다르고 둘 다RECOVERING을 목표로 한다). - 알 수 없는
ha_server_state(어느 switch에서든):er_log_debug ("BUG ..."),return ER_FAILED.
실제 전환 — new_state != HA_LOG_APPLIER_STATE_NA이고, 두 번째 apply_state == new_state 조기 반환 이후에도 새 상태가 실제로 다를 때 — la_change_state는 boot_notify_ha_log_applier_state 전에 강제 커밋(la_log_commit (true))을 수행해 보고되는 북마크가 내구적임을 보장한다.
불변 조건 — 어플라이어는 마지막으로 적용한 북마크를 내구적으로 만들기 전에 새 상태를 외부에 알리지 않는다. boot_notify_ha_log_applier_state 앞의 무조건적인 la_log_commit (true)가 이를 강제한다.
11.3 테이블 수준 필터링: REPL_FILTER_TYPE과 아이템별 평가
섹션 제목: “11.3 테이블 수준 필터링: REPL_FILTER_TYPE과 아이템별 평가”슬레이브는 일부 테이블만(INCLUDE) 복제하거나 일부를 제외하고(EXCLUDE) 복제할 수 있다. 정책은 하나의 열거형과 병렬 배열 필터 구조체로 표현된다.
// REPL_FILTER_TYPE -- src/transaction/log_applier.htypedef enum { REPL_FILTER_NONE, REPL_FILTER_INCLUDE_TBL, REPL_FILTER_EXCLUDE_TBL } REPL_FILTER_TYPE;| 열거값 | 역할 | 존재 이유 |
|---|---|---|
REPL_FILTER_NONE | la_need_filter_out이 즉시 false를 반환한다. | 기본값; 모든 것을 복제할 때 오버헤드 없음. |
REPL_FILTER_INCLUDE_TBL | 허용 목록: 클래스가 목록에 없으면 버린다. | ”이 테이블만 복제한다.” |
REPL_FILTER_EXCLUDE_TBL | 거부 목록: 클래스가 목록에 있으면 버린다. | ”이 테이블을 제외하고 모두 복제한다.” |
런타임 상태 LA_REPL_FILTER(la_Info.repl_filter)는 병렬 배열이다:
// struct la_repl_filter -- src/transaction/log_applier.cstruct la_repl_filter { char **list; int list_size; int num_filters; REPL_FILTER_TYPE type; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
list | 필터 파일에서 strdup된 클래스 이름들. | 허용/거부 집합; la_add_repl_filter가 LA_NUM_REPL_FILTER 단위로 늘린다. |
list_size | list의 할당된 용량. | la_add_repl_filter가 realloc 시점을 판단하는 기준; 사용 중인 항목 수와 별개. |
num_filters | 채워진 항목 수 (<= list_size). | la_need_filter_out의 반복 상한; 용량이 아닌 카운트. |
type | REPL_FILTER_TYPE 정책. | 허용/거부를 선택하거나 “필터 없음”으로 단락한다. |
la_create_repl_filter가 시작 시 한 번 로드한다. 분기 맵:
type=PRM_ID_HA_REPL_FILTER_TYPE;REPL_FILTER_NONE→ 즉시 반환(파일 읽지 않음).- 필터 파일 =
PRM_ID_HA_REPL_FILTER_FILE; 비어 있거나fopen실패 →ER_HA_LA_REPL_FILTER_GENERIC; 상대 경로는envvar_confdir_file로 해석. - 줄별 처리:
trim, 개행 제거, 빈 줄 건너뜀; user/식별자 길이 검증 (초과 →goto error_return);sm_user_specified_name으로 정규화 후locator_find_class로 해석. 클래스 미존재는 비치명적 — 에러는er_stack_push/er_stack_pop쌍 안에서 발생(발생 후 즉시 스택에서 제거)하고, 테이블이 나중에 생성될 수 있으므로la_add_repl_filter로 항목을 추가한다. error_return: 파일을 닫고la_destroy_repl_filter를 호출한다.
필터는 스테이징 시점이 아니라 apply 시점에 아이템별로 평가된다 — la_apply_repl_log 루프에서 apply 디스패치는
LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa) && la_need_filter_out (item) == false일 때만 실행된다.
la_need_filter_out 함수 자체는:
// la_need_filter_out -- src/transaction/log_applier.c if (filter->type == REPL_FILTER_NONE || (item->log_type == LOG_REPLICATION_STATEMENT && item->item_type != CUBRID_STMT_TRUNCATE) || strcasecmp (class_name, CT_SERIAL_NAME) == 0) return false; /* never filter these */ for (i = 0; i < filter->num_filters; i++) if (strcasecmp (filter->list[i], class_name) == 0) { filter_found = true; break; } if ((filter->type == REPL_FILTER_INCLUDE_TBL && filter_found == false) || (filter->type == REPL_FILTER_EXCLUDE_TBL && filter_found == true)) return true; return false;즉, 구문 복제(TRUNCATE 제외)와 시리얼 카탈로그(CT_SERIAL_NAME)는 절대 필터링되지 않는다. 그 외에는 INCLUDE가 목록에 없는 클래스를 버리고, EXCLUDE가 목록에 있는 클래스를 버린다.
수정자를 위한 비용 참고사항: 클래스 이름은 la_need_filter_out이 실행되기 전에 la_apply_* 내의 la_get_recdes 탐색에서 가져오므로, 필터링된 이벤트도 레코드 디스크립터 디코딩 비용은 치른다 — 필터가 아끼는 것은 슬레이브 apply이지 로그 읽기가 아니다.
11.4 TDE: 유닉스 소켓으로 데이터 키 공유하기
섹션 제목: “11.4 TDE: 유닉스 소켓으로 데이터 키 공유하기”마스터가 로그 페이지를 TDE로 암호화하면, copylogdb는 암호문을 전달하고 applylogdb는 fetch된 각 페이지를 복호화한다 — 둘 다 UNSTABLE_TDE_FOR_REPLICATION_LOG 가드 뒤에 위치한다(기본값: 비활성).
la_load_tde에 대한 주석.la_load_tde라는 이름의 심벌은 존재하지 않는다; “어플라이어가 키를 로드하고 서빙한다”는 개념적 역할은la_start_dk_sharing(리스너 설정)과 서버 스레드la_process_dk_request(copylogdb의logwr_load_tde의 실제 대응체)가 담당한다. 이 두 심벌로 grep하라.
복호화 지점은 la_log_fetch(및 아카이브 경로)다: 가드 하에서
LOG_IS_PAGE_TDE_ENCRYPTED (&cache_buffer->logpage)가 참이면
tde_decrypt_log_page (..., logwr_get_tde_algorithm (...), ...)를 그 자리에서 호출한다.
키는 어플라이어에서 온다 — 어느 데몬도 본질적으로 키를 소유하지 않는다.
la_apply_log_file에서 tde_get_data_keys ()를 호출하고, tde_Cipher.is_loaded를 설정하고,
la_start_dk_sharing을 호출한다. la_start_dk_sharing은 fileio_make_ha_sock_name (..., TDE_HA_SOCK_NAME)으로
경로를 만들고, 오래된 노드를 unlink하고, socket/bind/listen한 뒤
pthread_create (la_process_dk_request)를 수행한다(각 실패 → 고유한 ER_TDE_DK_SHARING_*).
클라이언트 logwr_load_tde는 연결하고, 핸드셰이크로 log_path를 기록하고, 상태 워드를 읽고,
성공 시 TDE_DATA_KEY_SET을 읽는다:
// logwr_load_tde -- src/transaction/log_writer.c fileio_make_ha_sock_name (sock_path, logwr_Gl.log_path, TDE_HA_SOCK_NAME); // ... socket / connect / write(log_path) / read(err_msg) ... if (err_msg != NO_ERROR) { close (client_sockfd); return err_msg; } /* refused */ // ... read TDE_DATA_KEY_SET dks; memcpy perm_key / temp_key / log_key ... tde_Cipher.is_loaded = true;서버 스레드 la_process_dk_request는 키를 넘기기 전에 각 클라이언트를 검증한다:
// la_process_dk_request -- src/transaction/log_applier.c if (error == NO_ERROR) /* prior read may set ER_TDE_DK_SHARING_SOCK_READ */ { if (!tde_is_loaded ()) error = ER_TDE_CIPHER_IS_NOT_LOADED; /* <- keys not ready yet */ else if (memcmp (buf, la_Info.log_path, PATH_MAX) != 0) error = ER_TDE_WRONG_DK_REQUEST; /* <- client's log_path mismatch */ } // ... write `error` to client first; only if NO_ERROR send TDE_DATA_KEY_SET ...상태 워드는 항상 먼저 전송되고(따라서 logwr_load_tde가 거부 이유를 알 수 있고), TDE_DATA_KEY_SET은 오직 정상 경로에서만 기록된다. 이후 la_log_fetch가 페이지를 복호화할 수 있다. 전체 핸드셰이크: 어플라이어 tde_get_data_keys + listen → 클라이언트 연결, log_path 기록 → 어플라이어 상태 워드 반환 → NO_ERROR이면 TDE_DATA_KEY_SET 전송.
제한 사항 — 복제용 TDE는 컴파일 시점에 가드되며 기본적으로 비활성이다. 이 절의 모든 코드 경로는 #ifdef UNSTABLE_TDE_FOR_REPLICATION_LOG 뒤에 있다; 일반 빌드는 평문 로그 페이지를 복제하며 키 소켓을 열지 않는다.
마스터 측에서는 tde_encrypted가 스테이징된 각 LOG_REPL_RECORD의 멤버로 탑승한다(struct log_repl의 bool tde_encrypted, replication.h). log_manager.c의 emit 지점에서 if (repl_rec->tde_encrypted)이면 prior_set_tde_encrypted (node, repl_rec->rcvindex)가 실행되어 redo 노드가 플래그를 상속한다. tde_encrypted가 처음 설정되는 위치는 2장에서 다루며, 여기서는 그것이 prior-list 노드까지 살아남는다는 점만 확인한다.
11.5 Abort와 종료 정리
섹션 제목: “11.5 Abort와 종료 정리”abort는 읽히는 곳에서 해제되지 않는다. la_log_record_process에서 LOG_ABORT는 — commit처럼 — la_add_node_into_la_commit_list (lrec->trid, final, LOG_ABORT, 0)으로 큐에 삽입될 뿐이며, la_apply_commit_list가 LSA 순서대로 도달할 때 처리된다. 스테이징된 아이템은 나중에 la_apply_repl_log에서 해제된다 — 첫 번째 분기가 abort를 단락시킨다:
// la_apply_repl_log -- src/transaction/log_applier.c if (rectype == LOG_ABORT) { la_clear_applied_info (apply); /* free items + reset LSAs, then return */ return NO_ERROR; } if (apply->head == NULL || LSA_LE (commit_lsa, &la_Info.last_committed_lsa)) { if (rectype == LOG_SYSOP_END) la_free_all_repl_items (apply); /* preserve slot LSAs */ else la_clear_applied_info (apply); /* full reset */ return NO_ERROR; }la_clear_applied_info(LA_APPLY별 리셋: la_free_all_repl_items 후 start_lsa / last_lsa / tranid null 처리)는 abort 시, 그리고 비어 있거나 이미 커밋된 apply 리스트일 때 도달한다. 반면 같은 가드의 LOG_SYSOP_END 분기는 la_free_all_repl_items를 호출해 슬롯 LSA를 보존한다. la_free_all_repl_items가 is_long_trans도 초기화하므로, 재사용된 슬롯은 이전 점유자의 장기 트랜잭션 플래그를 물려받지 않는다.
la_free_repl_items_by_tranid는 abort 경로와 별개인 또 다른 정리 진입점이다. 이 함수는 (a) la_change_state가 non-DEAD standby로 전환될 때 la_clear_all_repl_and_commit_list에서, (b) la_log_record_process의 LOG_COMMIT / LOG_SYSOP_END else 분기에서 레코드 LSA가 <= committed_lsa인 경우(오래된, 이미 적용된 EOT)에 호출된다. 이 함수는 스테이징된 아이템을 초기화하고 해당 trid의 커밋 리스트 노드도 언링크한다:
// la_free_repl_items_by_tranid -- src/transaction/log_applier.c apply = la_find_apply_list (tranid); if (apply) la_clear_applied_info (apply); /* free items + reset LSAs */ // ... walk commit_head; for the node with commit->tranid == tranid, unlink + free_and_init ... if (la_Info.commit_head == NULL) la_Info.commit_tail = NULL; /* keep head/tail consistent */불변 조건 — 커밋 리스트의 head/tail은 항상 상호 일관성을 유지한다. la_free_repl_items_by_tranid는 마지막 노드를 언링크할 때 commit_tail을 수정하고(commit->next == NULL 분기: commit_tail = commit->prev), commit_head가 NULL이 될 때마다 commit_tail = NULL로 리셋함으로써 이를 강제한다.
LOG_SYSOP_END vs commit 디스패치의 전체 내용은 9장의 몫이다; 여기서는 어느 리프가 슬롯을 해제하고 어느 쪽이 보존하는지만 확인한다.
11.6 챕터 요약 — 핵심 정리
섹션 제목: “11.6 챕터 요약 — 핵심 정리”LA_MAX_REPL_ITEMS를 초과하는 트랜잭션은is_long_trans를 설정하고 아이템 리스트를 버린다. 이후start_lsa .. last_lsa만으로 기술되며,la_set_repl_log는 플래그가 설정된 후 추가를 거부한다.- 장기 트랜잭션 적용은
la_get_next_repl_item_from_log로 REPL 레코드를 재독하고,forw_lsa를 따르며 외래 trid / 비-REPL 레코드를 건너뛴다 — 메모리 풋프린트를 최소화하는 대신 O(스팬된 레코드 수)의 비용을 치른다. - 강등은
LOG_DUMMY_HA_SERVER_STATE로 도달한다; db 락을 보유한 non-ACTIVE/TO_BE 상태에서 어플라이어는is_role_changed를 설정하고ER_INTERRUPTED를 반환하며, 메인 루프가clear_owner = true로 락을 해제한다. la_change_state는 로그 헤더로부터 어플라이어 자체 상태를 보고하며,boot_notify_ha_log_applier_state전에 반드시 내구적 커밋을 강제한다.- 필터링은 아이템별 결정(
la_need_filter_out)으로,REPL_FILTER_TYPE허용/거부 목록을 기준으로 한다. 구문 복제(TRUNCATE제외)와 시리얼 카탈로그는 절대 필터링되지 않으며, 필터링된 이벤트도la_get_recdes디코딩 비용을 치른다. - TDE는 컴파일 가드(
UNSTABLE_TDE_FOR_REPLICATION_LOG)로 비활성:applylogdb가 키를 서빙하고(la_start_dk_sharing/la_process_dk_request),copylogdb가 가져가며(logwr_load_tde), 페이지 복호화는la_log_fetch에서 이뤄지고,tde_encrypted는prior_set_tde_encrypted를 거쳐 redo 노드까지 전달된다. - abort는 커밋처럼
la_add_node_into_la_commit_list로 큐에 삽입되고 나중에la_apply_repl_log에서 해제된다(rectype == LOG_ABORT→la_clear_applied_info).la_free_repl_items_by_tranid는 standby 종료 및 이미 적용된 EOT 처리에 쓰이는 별개의 경로이며, 커밋 리스트의 head/tail 일관성을 유지한다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
logwr_get_log_pages | src/communication/network_interface_cl.c | 9153 |
slogwr_get_log_pages | src/communication/network_interface_sr.cpp | 8500 |
la_apply_log_file (call site) | src/executables/util_cs.c | 3170 |
CT_SERIAL_NAME | src/object/schema_system_catalog_constants.h | 45 |
ws_add_to_repl_obj_list | src/object/work_space.c | 5360 |
do_replicate_statement | src/query/execute_statement.c | 16136 |
qexec_execute_selupd_list | src/query/query_executor.c | 13832 |
serial_update_serial_object | src/query/serial.c | 918 |
btree_update | src/storage/btree.c | 14696 |
btree_insert | src/storage/btree.c | 26876 |
heap_get_class_name | src/storage/heap_file.c | 9718 |
heap_get_class_tde_algorithm | src/storage/heap_file.c | 11083 |
heap_insert_logical | src/storage/heap_file.c | 23460 |
heap_delete_logical | src/storage/heap_file.c | 23676 |
heap_update_logical | src/storage/heap_file.c | 23867 |
locator_insert_force | src/transaction/locator_sr.c | 4938 |
locator_insert_force heap_insert_logical call | src/transaction/locator_sr.c | 5065 |
locator_insert_force add_or_remove_index call | src/transaction/locator_sr.c | 5184 |
locator_update_force | src/transaction/locator_sr.c | 5396 |
locator_update_force heap_update_logical call | src/transaction/locator_sr.c | 6028 |
locator_update_force repl_add_update_lsa guard | src/transaction/locator_sr.c | 6052 |
locator_update_force repl_add_update_lsa call | src/transaction/locator_sr.c | 6055 |
locator_delete_force | src/transaction/locator_sr.c | 6116 |
locator_delete_force_internal | src/transaction/locator_sr.c | 6172 |
locator_delete_force_internal isold_object skip | src/transaction/locator_sr.c | 6251 |
locator_delete_force_internal heap_delete_logical call | src/transaction/locator_sr.c | 6327 |
locator_delete_force_internal add_or_remove_index call | src/transaction/locator_sr.c | 6349 |
locator_delete_force_internal add_or_remove_index_for_moving call | src/transaction/locator_sr.c | 6355 |
locator_force_for_multi_update repl_info flag set | src/transaction/locator_sr.c | 6634 |
locator_force_for_multi_update REPL_INFO_TYPE_RBR_START assignment | src/transaction/locator_sr.c | 6636 |
locator_attribute_info_force | src/transaction/locator_sr.c | 7461 |
locator_attribute_info_force dispatch switch | src/transaction/locator_sr.c | 7569 |
locator_add_or_remove_index | src/transaction/locator_sr.c | 7695 |
locator_add_or_remove_index_internal | src/transaction/locator_sr.c | 7760 |
locator_add_or_remove_index_internal repl_log_insert guard | src/transaction/locator_sr.c | 8038 |
locator_add_or_remove_index_internal repl_log_insert call | src/transaction/locator_sr.c | 8042 |
locator_update_index | src/transaction/locator_sr.c | 8260 |
locator_update_index pk_btid_index discovery | src/transaction/locator_sr.c | 8416 |
locator_update_index repl_log_insert call | src/transaction/locator_sr.c | 8804 |
xrepl_set_info | src/transaction/locator_sr.c | 11707 |
xchksum_insert_repl_log_and_demote_table_lock | src/transaction/locator_sr.c | 12620 |
prior_set_tde_encrypted | src/transaction/log_append.cpp | 1565 |
enum LOG_PRIOR_LSA_LOCK | src/transaction/log_append.hpp | 66 |
LA_DEFAULT_CACHE_BUFFER_SIZE | src/transaction/log_applier.c | 80 |
LA_REPL_LIST_COUNT | src/transaction/log_applier.c | 85 |
LA_PAGE_EXST_IN_ARCHIVE_LOG | src/transaction/log_applier.c | 89 |
LA_LOCK_SUFFIX | src/transaction/log_applier.c | 94 |
LA_MAX_REPL_ITEMS | src/transaction/log_applier.c | 98 |
LA_LOG_IS_IN_ARCHIVE | src/transaction/log_applier.c | 112 |
LA_LOGAREA_SIZE | src/transaction/log_applier.c | 118 |
LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT | src/transaction/log_applier.c | 119 |
LA_MOVE_INSIDE_RECORD | src/transaction/log_applier.c | 156 |
LA_IS_FLUSH_ERROR | src/transaction/log_applier.c | 174 |
struct la_cache_buffer | src/transaction/log_applier.c | 178 |
struct la_cache_buffer_area | src/transaction/log_applier.c | 191 |
struct la_cache_pb | src/transaction/log_applier.c | 198 |
struct la_repl_filter | src/transaction/log_applier.c | 207 |
la_repl_filter | src/transaction/log_applier.c | 207 |
struct la_act_log | src/transaction/log_applier.c | 216 |
struct la_arv_log | src/transaction/log_applier.c | 227 |
la_item | src/transaction/log_applier.c | 237 |
la_item (struct) | src/transaction/log_applier.c | 237 |
la_apply | src/transaction/log_applier.c | 255 |
la_apply (struct) | src/transaction/log_applier.c | 255 |
la_commit | src/transaction/log_applier.c | 267 |
la_commit (struct) | src/transaction/log_applier.c | 267 |
la_info | src/transaction/log_applier.c | 280 |
struct la_info | src/transaction/log_applier.c | 280 |
repl_lists | src/transaction/log_applier.c | 298 |
commit_head | src/transaction/log_applier.c | 304 |
commit_tail | src/transaction/log_applier.c | 305 |
struct la_recdes_pool | src/transaction/log_applier.c | 383 |
LA_HA_APPLY_INFO | src/transaction/log_applier.c | 393 |
la_init_ha_apply_info | src/transaction/log_applier.c | 606 |
la_log_phypageid | src/transaction/log_applier.c | 630 |
la_log_fetch_from_archive | src/transaction/log_applier.c | 912 |
la_log_fetch | src/transaction/log_applier.c | 1063 |
la_expand_cache_log_buffer | src/transaction/log_applier.c | 1177 |
la_cache_buffer_replace | src/transaction/log_applier.c | 1232 |
la_get_page_buffer | src/transaction/log_applier.c | 1297 |
la_get_page | src/transaction/log_applier.c | 1336 |
la_release_page_buffer | src/transaction/log_applier.c | 1364 |
la_invalidate_page_buffer | src/transaction/log_applier.c | 1418 |
la_find_required_lsa | src/transaction/log_applier.c | 1471 |
la_get_ha_apply_info | src/transaction/log_applier.c | 1514 |
la_insert_ha_apply_info | src/transaction/log_applier.c | 1716 |
la_update_ha_apply_info_start_time | src/transaction/log_applier.c | 1860 |
la_update_ha_apply_info_log_record_time | src/transaction/log_applier.c | 1897 |
la_get_last_ha_applied_info | src/transaction/log_applier.c | 1965 |
la_update_ha_last_applied_info | src/transaction/log_applier.c | 2074 |
la_assign_recdes_from_pool | src/transaction/log_applier.c | 2381 |
la_init_recdes_pool | src/transaction/log_applier.c | 2416 |
la_init_cache_pb | src/transaction/log_applier.c | 2474 |
log_pageid_hash | src/transaction/log_applier.c | 2500 |
la_init_cache_log_buffer | src/transaction/log_applier.c | 2528 |
la_apply_pre | src/transaction/log_applier.c | 2693 |
la_init_repl_lists | src/transaction/log_applier.c | 2773 |
la_is_repl_lists_empty | src/transaction/log_applier.c | 2837 |
la_find_apply_list | src/transaction/log_applier.c | 2860 |
la_add_apply_list | src/transaction/log_applier.c | 2889 |
la_log_copy_fromlog | src/transaction/log_applier.c | 2960 |
la_new_repl_item | src/transaction/log_applier.c | 3012 |
la_add_repl_item | src/transaction/log_applier.c | 3050 |
la_get_item_pk_value | src/transaction/log_applier.c | 3073 |
la_make_repl_item | src/transaction/log_applier.c | 3092 |
la_unlink_repl_item | src/transaction/log_applier.c | 3228 |
la_free_repl_item | src/transaction/log_applier.c | 3266 |
la_free_all_repl_items_except_head | src/transaction/log_applier.c | 3300 |
la_free_and_add_next_repl_item | src/transaction/log_applier.c | 3327 |
la_free_all_repl_items | src/transaction/log_applier.c | 3358 |
la_clear_applied_info | src/transaction/log_applier.c | 3378 |
la_clear_all_repl_and_commit_list | src/transaction/log_applier.c | 3392 |
la_set_repl_log | src/transaction/log_applier.c | 3419 |
la_add_node_into_la_commit_list | src/transaction/log_applier.c | 3473 |
la_retrieve_eot_time | src/transaction/log_applier.c | 3515 |
la_make_room_for_mvcc_insid | src/transaction/log_applier.c | 3669 |
la_make_room_for_mvcc_delid_and_prev_ver | src/transaction/log_applier.c | 3700 |
la_disk_to_obj | src/transaction/log_applier.c | 3732 |
la_get_zipped_data | src/transaction/log_applier.c | 3803 |
la_get_undoredo_diff | src/transaction/log_applier.c | 3880 |
la_get_log_data | src/transaction/log_applier.c | 3949 |
la_get_overflow_recdes | src/transaction/log_applier.c | 4249 |
la_get_next_update_log | src/transaction/log_applier.c | 4393 |
la_get_relocation_recdes | src/transaction/log_applier.c | 4552 |
la_get_recdes | src/transaction/log_applier.c | 4604 |
la_repl_add_object | src/transaction/log_applier.c | 4882 |
la_apply_delete_log | src/transaction/log_applier.c | 5000 |
la_apply_update_log | src/transaction/log_applier.c | 5110 |
la_is_mvcc_class | src/transaction/log_applier.c | 5218 |
la_apply_insert_log | src/transaction/log_applier.c | 5311 |
la_apply_statement_log | src/transaction/log_applier.c | 5496 |
la_apply_repl_log | src/transaction/log_applier.c | 5739 |
la_apply_commit_list | src/transaction/log_applier.c | 5920 |
la_free_repl_items_by_tranid | src/transaction/log_applier.c | 5973 |
la_get_next_repl_item | src/transaction/log_applier.c | 6024 |
la_get_next_repl_item_from_list | src/transaction/log_applier.c | 6036 |
la_get_next_repl_item_from_log | src/transaction/log_applier.c | 6043 |
la_log_record_process | src/transaction/log_applier.c | 6101 |
la_change_state | src/transaction/log_applier.c | 6397 |
la_log_commit | src/transaction/log_applier.c | 6531 |
la_commit_transaction | src/transaction/log_applier.c | 6647 |
la_check_duplicated | src/transaction/log_applier.c | 6796 |
la_init | src/transaction/log_applier.c | 6917 |
la_remove_archive_logs | src/transaction/log_applier.c | 7490 |
la_need_filter_out | src/transaction/log_applier.c | 7723 |
la_add_repl_filter | src/transaction/log_applier.c | 7784 |
la_create_repl_filter | src/transaction/log_applier.c | 7831 |
la_apply_log_file | src/transaction/log_applier.c | 8074 |
la_start_dk_sharing | src/transaction/log_applier.c | 8743 |
la_process_dk_request | src/transaction/log_applier.c | 8796 |
REPL_FILTER_TYPE | src/transaction/log_applier.h | 48 |
log_does_allow_replication | src/transaction/log_comm.c | 272 |
log_tdes | src/transaction/log_impl.h | 475 |
num_repl_records | src/transaction/log_impl.h | 522 |
cur_repl_record | src/transaction/log_impl.h | 523 |
append_repl_recidx | src/transaction/log_impl.h | 524 |
fl_mark_repl_recidx | src/transaction/log_impl.h | 525 |
repl_records | src/transaction/log_impl.h | 526 |
repl_insert_lsa | src/transaction/log_impl.h | 527 |
repl_update_lsa | src/transaction/log_impl.h | 528 |
suppress_replication | src/transaction/log_impl.h | 531 |
log_append_undoredo_crumbs repl_insert/update_lsa stamp | src/transaction/log_manager.c | 2198 |
log_append_redo_crumbs repl_insert/update_lsa stamp | src/transaction/log_manager.c | 2463 |
log_sysop_commit_internal | src/transaction/log_manager.c | 3825 |
log_sysop_abort | src/transaction/log_manager.c | 4038 |
log_append_repl_info_internal | src/transaction/log_manager.c | 4555 |
prior_set_tde_encrypted | src/transaction/log_manager.c | 4587 |
log_append_repl_info | src/transaction/log_manager.c | 4623 |
log_append_repl_info_with_lock | src/transaction/log_manager.c | 4629 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_donetime_internal | src/transaction/log_manager.c | 4679 |
log_append_commit_log | src/transaction/log_manager.c | 4779 |
log_append_commit_log_with_lock | src/transaction/log_manager.c | 4802 |
log_append_supplemental_info | src/transaction/log_manager.c | 4837 |
log_commit_local | src/transaction/log_manager.c | 5159 |
log_abort_local | src/transaction/log_manager.c | 5277 |
log_commit | src/transaction/log_manager.c | 5352 |
log_complete | src/transaction/log_manager.c | 5653 |
log_complete_for_2pc | src/transaction/log_manager.c | 5758 |
logpb_Arv_page_info_table | src/transaction/log_page_buffer.c | 282 |
logpb_get_guess_archive_num | src/transaction/log_page_buffer.c | 4558 |
logpb_get_archive_num_from_info_table | src/transaction/log_page_buffer.c | 6386 |
LOG_REPLICATION_DATA | src/transaction/log_record.hpp | 116 |
LOG_REPLICATION_STATEMENT | src/transaction/log_record.hpp | 117 |
log_rec_replication | src/transaction/log_record.hpp | 228 |
struct log_rec_replication | src/transaction/log_record.hpp | 228 |
logtb_free_tran_index | src/transaction/log_tran_table.c | 1202 |
logtb_clear_tdes | src/transaction/log_tran_table.c | 1493 |
logtb_initialize_tdes | src/transaction/log_tran_table.c | 1614 |
LOGWR_COPY_LOG_BUFFER_NPAGES | src/transaction/log_writer.c | 70 |
logwr_to_physical_pageid | src/transaction/log_writer.c | 195 |
logwr_initialize | src/transaction/log_writer.c | 428 |
logwr_set_hdr_and_flush_info | src/transaction/log_writer.c | 639 |
logwr_writev_append_pages | src/transaction/log_writer.c | 838 |
logwr_flush_all_append_pages | src/transaction/log_writer.c | 1016 |
logwr_archive_active_log | src/transaction/log_writer.c | 1275 |
logwr_write_log_pages | src/transaction/log_writer.c | 1512 |
logwr_copy_log_file | src/transaction/log_writer.c | 1659 |
logwr_load_tde | src/transaction/log_writer.c | 1834 |
logwr_check_page_checksum | src/transaction/log_writer.c | 1978 |
logwr_pack_log_pages | src/transaction/log_writer.c | 2263 |
xlogwr_get_log_pages | src/transaction/log_writer.c | 2571 |
LOGWR_CONTEXT | src/transaction/log_writer.h | 41 |
LOGWR_MODE | src/transaction/log_writer.h | 48 |
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK | src/transaction/log_writer.h | 55 |
LOGWR_GLOBAL | src/transaction/log_writer.h | 69 |
LOGWR_ENTRY | src/transaction/log_writer.h | 142 |
LOGWR_INFO | src/transaction/log_writer.h | 157 |
wr_list_mutex | src/transaction/log_writer.h | 160 |
RVREPL_DATA_INSERT | src/transaction/recovery.h | 149 |
RVREPL_DATA_UPDATE | src/transaction/recovery.h | 150 |
RVREPL_DATA_DELETE | src/transaction/recovery.h | 151 |
RVREPL_STATEMENT | src/transaction/recovery.h | 152 |
RVREPL_DATA_UPDATE_START | src/transaction/recovery.h | 153 |
RVREPL_DATA_UPDATE_END | src/transaction/recovery.h | 154 |
REPL_LOG_IS_NOT_EXISTS | src/transaction/replication.c | 43 |
REPL_LOG_IS_FULL | src/transaction/replication.c | 45 |
REPL_LOG_INFO_ALLOC_SIZE | src/transaction/replication.c | 49 |
repl_log_info_alloc | src/transaction/replication.c | 165 |
repl_add_update_lsa | src/transaction/replication.c | 229 |
repl_log_insert | src/transaction/replication.c | 293 |
repl_log_insert rcvindex refinement | src/transaction/replication.c | 344 |
repl_log_insert payload packing | src/transaction/replication.c | 363 |
repl_log_insert lsa switch | src/transaction/replication.c | 428 |
repl_log_insert flush-mark tail | src/transaction/replication.c | 466 |
repl_log_insert_statement | src/transaction/replication.c | 512 |
repl_start_flush_mark | src/transaction/replication.c | 606 |
repl_end_flush_mark | src/transaction/replication.c | 635 |
repl_log_abort_after_lsa | src/transaction/replication.c | 673 |
REPL_INFO_TYPE | src/transaction/replication.h | 43 |
REPL_INFO_TYPE | src/transaction/replication.h | 49 |
repl_info | src/transaction/replication.h | 52 |
repl_info_statement | src/transaction/replication.h | 60 |
log_repl_flush | src/transaction/replication.h | 70 |
enum log_repl_flush | src/transaction/replication.h | 70 |
log_repl | src/transaction/replication.h | 79 |
log_repl (LOG_REPL_RECORD) | src/transaction/replication.h | 79 |
repl_data | src/transaction/replication.h | 85 |
tde_encrypted | src/transaction/replication.h | 88 |
cubrid-ha-replication.md— 상위 수준 동반 문서. 관련 문서로cubrid-log-manager-detail.md(전송 대상 로그)와cubrid-heartbeat.md(failover 제어)도 함께 참조한다.- 원본 분석 자료:
raw/code-analysis/cubrid/distributed/하위 파일들. - 코드:
src/transaction/log_applier.{c,h},log_writer.{c,h},replication.{c,h}; 마스터 로그는log_manager.c. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md.