(KO) CUBRID 복제와 HA — 섹션 개요
목차
이 섹션의 범위
섹션 제목: “이 섹션의 범위”CUBRID 의 고가용성 그림은 한 줄로 요약된다. 비동기 논리 복제
위에 얹은 primary / standby 클러스터, 그리고 노드별 로컬 점수
로 결정하는 리더 선출이다. 노드마다 cub_master 프로세스가
한 대 떠서, 알고 있는 peer 들에게 UDP heartbeat 을 쏘고, 받은
패킷만 가지고 혼자 “누가 master 인가” 라는 결론을 낸다. Raft
도, Paxos 도, quorum 왕복도 없다. 이 한 가지가 CUBRID HA 를
다른 시스템과 구분 짓는 결정이다.
이 생존 기반 위에서 master 엔진이 보조 레코드 두 종류
(LOG_REPLICATION_DATA 와 LOG_REPLICATION_STATEMENT) 를
짜 넣는다. 이 레코드들은 평소 crash recovery 에 쓰는 그 WAL
스트림 안에 그대로 들어간다. slave 호스트의 copylogdb 데몬이
그 볼륨을 통째로 TCP 로 받아 가고, applylogdb 워커
(la_apply_log_file) 가 받은 아카이브를 따라가며 행 이벤트를
레코드 종류별로 스토리지 레이어에 다시 적용한다. 같은 WAL 은
두 번째 소비자에게도 열려 있다. 새 cdc_* API 가
LOG_SUPPLEMENTAL_INFO 레코드를 외부 구독자 (Kafka, 검색 인덱스,
감사 파이프라인 등) 로 흘려 보낸다.
이 섹션의 세부 문서 셋이 위 세 레이어를 하나씩 풀어 둔다. 이 개요는 그 지도다.
HA 스택
섹션 제목: “HA 스택”replication-ha 묶음은 세 레이어가 위에서 아래로 쌓인 구조로 보면 가장 깔끔하다. 각 레이어에는 자기 세부 문서가 있고, 바로 아래 레이어가 만들어 놓은 결과를 입력으로 받는다. 셋을 관통하는 약속이 하나 있다. WAL 이 노드 사이를 오가는 유일한 영속 계약 이라는 것이다. 같은 디스크 바이트 스트림이 한 노드의 crash recovery 를 굴리고, peer 노드의 slave 따라잡기를 굴리고, 외부 CDC 소비자까지 굴린다. 아래에서 흐르는 것은 같고, 받아 가는 쪽만 다를 뿐이다.
-
생존 레이어,
cubrid-heartbeat.md. 노드마다cub_master가 알고 있는 모든 peer 에게 UDP heartbeat 패킷을 쏘고, 들어오는 패킷으로 peer 마다의(state, priority)점수를 갱신한다. 장애 감지는 두 신호를 함께 본다. 양방향 손실을 잡는 gap 카운터와 한쪽 방향 손실을 잡는 last-heard 시각이다. 리더 선출은 전적으로 로컬이다. 각 노드가 자기 peer 표만 들여다보고 결론을 내며, 어디에도 전역 합의 단계가 없다. 작은 잡 큐 FSM 이 워커 스레드 네 개를 돌려slave → to-be-master → master와master → slave전이를 실행하고, failover, failback, 자원 장애 시의 강등이 모두 이 전이로 풀린다. split-brain 을 막는 안전 장치도 여기에 살아 있다. 외부 도달 가능성을 증인 삼는ha_ping_hosts가 하나, 비-replica peer 가 모두HB_NSTATE_UNKNOWN이면 승격을 거부하는is_isolated술어가 다른 하나다. -
복제 레이어,
cubrid-ha-replication.md. 생존 레이어가 master 와 slave 를 짚어 두면, 실제 데이터 흐름이 시작된다. 그 흐름은 물리적 WAL 옆에서 같이 달리는 논리 로그 파이프라인이다. DML 시점에 master 는 두 종류의 레코드를 같은 로그 파일 에 함께 적는다. 하나는 페이지·바이트 단위 물리 undo/redo 레코드 (복구 관리자가 다시 돌리는 것) 이고, 다른 하나는 보조 복제 레코드 (LOG_REPLICATION_DATA는 행 이벤트,LOG_REPLICATION_STATEMENT는 DDL 과 트리거 연동 구문) 다. slave 호스트의copylogdb데몬이 master 에 TCP 로 붙어 로그 볼륨을 그대로 자기 로컬 아카이브로 끌어간다. 별도로 도는applylogdb데몬 (log_applier.c의la_apply_log_file) 이 그 아카이브를 앞으로 훑으며 레코드 종류마다 정해진 길 (heap_*,btree_*,locator_*) 로 직렬화된 재적용을 돌린다. slave 는 같은 워크 로드 아래에서 master 와 기능적으로 같지만, 바이트 단위로는 같지 않다. 페이지 레이아웃, 빈 공간 분포, B-tree 분기 모양이 갈라질 수 있다. -
변경 캡처 레이어,
cubrid-cdc.md. 같은 WAL 이 두 번째 소비자에게도 열린다. 새cdc_*API (src/api/cubrid_log.c) 는 풀 방식 요청-응답 인터페이스다. 하류 소비자가 “LSA X 부터 다음 배치를 달라” 고 요청하면 서버가log_reader를 그만큼 앞으로 옮기며LOG_SUPPLEMENTAL_INFO레코드를 돌려준다. 이 레코드는 DML 시점에 엔진이 일부러 풍부하게 적어 두는 논리 레코드라, 소비자가 카탈로그를 다시 두드리지 않아도 될 만큼 정보가 충분하다. HA 복제의applylogdb를 굴리는 그la_*적용자 코드가 새 API 와 한 코드베이스 안에 공존한다. 둘 다 같은 디스크 로그를 읽고, 커서와 프레이밍 의미만 다르다. DDL 은LOG_SUPPLEMENT_DDL로 인라인으로 함께 흘러가기 때문에 CDC 소비자는 카탈로그로 콜백을 보내지 않고도 자기 스키마 캐시를 유지할 수 있다.
flowchart TB
subgraph Master["Master 노드 — cub_master"]
MWAL[("WAL (물리 + LOG_REPL + LOG_SUPPL)")]
MHB[heartbeat 발신]
MWAL --> SHIP[copylogdb 발신]
end
subgraph Slave["Slave 노드 — cub_master"]
SHB[heartbeat 수신]
SCOPY[copylogdb 수신]
SAPPLY[applylogdb / la_apply_log_file]
SCOPY --> SARCH[(로컬 로그 아카이브)]
SARCH --> SAPPLY
SAPPLY --> SSTORAGE[(스토리지: heap, btree, locator)]
end
subgraph Peer["Replica / witness — cub_master"]
PHB[heartbeat peer]
end
subgraph CDC["외부 CDC 소비자"]
CDCAPI[cdc_make_loginfo via cubrid_log.c]
end
MHB <-. UDP heartbeat .-> SHB
MHB <-. UDP heartbeat .-> PHB
SHB <-. UDP heartbeat .-> PHB
SHIP -- TCP 로그 전송 --> SCOPY
MWAL -- LSA 커서 풀 --> CDCAPI
classDef liveness fill:#fde,stroke:#a36
classDef repl fill:#dfe,stroke:#3a6
classDef capture fill:#def,stroke:#36a
class MHB,SHB,PHB liveness
class SHIP,SCOPY,SAPPLY,SARCH,SSTORAGE repl
class CDCAPI capture
다이어그램이 층 구조를 그대로 보여준다. 분홍 층 (heartbeat)
은 누가 master 인가 를 정한다. 초록 층 (복제) 은 master 의
WAL 을 slave 에 실어 보내고 다시 적용한다. 파란 층 (CDC) 은
외부 시스템이 다른 커서로 같은 WAL 을 따라 읽게 해준다. 초록과
파랑은 분홍 층을 직접 호출하지 않는다. master 가 누구인지는
평범한 클라이언트 라우팅 길로 알아 간다 (failover 가 일어나면
broker 가 연결을 다시 라우팅한다.
cubrid-architecture-overview.md 와 cubrid-broker.md 를
참조).
읽는 순서
섹션 제목: “읽는 순서”세부 문서는 아래에서 위로, 즉 생존 레이어부터 읽는 편이 가장 매끄럽다. 아래 층의 결과가 그대로 위 층의 입력이 되기 때문이다.
-
cubrid-heartbeat.md를 먼저 읽는다. 노드가 순전히 자기가 가진 정보만으로 “나는 master 다” 또는 “나는 slave 다” 라는 결론을 어떻게 내는지가 여기에 있다.(state, priority)점수, gap 카운터와 last-heard 라는 두 신호,ha_ping_hosts안전 장치, 상태 전이를 굴리는 네 스레드 FSM 까지, 이 어휘가 그대로 다른 두 문서에서 다시 도는 도구다. 이 문서는 또 CUBRID 의 설계 결정 (합의 대신 로컬 결정) 을 Raft / ZAB 와 나란히 놓고 그 트레이드오프를 분명히 적어 둔다 (비용은 싸다, 그러나 합의 시스템이 알아서 처리해 주는 비대칭 단절 위험은 본인이 떠안는다). -
cubrid-ha-replication.md를 다음에 읽는다. master 가 누구인지 알고 들어가야 이 문서가 풀린다. 실제 데이터 흐름이 여기에 들어 있다. 세 축 (물리 vs 논리, 구문 vs 행, 동기 vs 비동기) 이라는 틀로, CUBRID 의 선택 (비동기, 논리, 구문+행 혼합) 이 MySQL 행 기반 binlog, PostgreSQL 논리 디코딩, Oracle GoldenGate 와 어떻게 갈리는지를 짚어 준다. 생산자 쪽 (heap_*·btree_*안에서LOG_REPLICATION_DATA가 박히는 자리), 전송 쪽 (copylogdb), 소비자 쪽 (la_apply_log_file과 그 안의 레코드 종류별 디스패치) 에 어떤 심볼이 어디 사는지 빠짐없이 호명한다. -
cubrid-cdc.md가 마지막이다. HA 파이프라인 전체를 한 번 본 뒤에 들어가면, CDC 가 결국 같은 WAL 을 다른 커서로 읽고 다른 프레임에 담는 일이라는 점이 단번에 보인다. 새 풀 방식cdc_*API 와 옛 푸시 방식la_*데몬을 나란히 놓고, 둘이 왜 한동안 같이 살아야 하는지 (점진적 마이그레이션) 를 설명 한다.LOG_SUPPLEMENTAL_INFO레코드 가족 (SUPPLEMENT_REC_TYPE열거형, 커밋 경계에서tran_user로 묶는 트랜잭션 그루핑, 소비자가 아직 보지 못한 로그 볼륨을 아카이브 제거기가 못 지우게 막는cdc_min_log_pageid_to_keep워터마크) 까지 차례로 정리한다.
이 셋 가운데 한 편만 시간이 된다면 cubrid-heartbeat.md 를
고르면 된다. 로컬에서 결정짓는 생존 모델이야말로 CUBRID HA 를
교과서적 설명과 갈라놓는 단 하나의 결정이고, 이 모델만 머릿속에
들어와도 이 섹션의 나머지는 윤곽으로 이해된다.
횡단 관심사
섹션 제목: “횡단 관심사”세 문서를 가로질러 흐르는 약속들이 있다. 이 셋이 따로 떠도는 문서가 아니라 한 섹션으로 묶이는 이유가 여기 있다.
로그는 하나, 소비자는 둘
섹션 제목: “로그는 하나, 소비자는 둘”복제와 CDC 모두 같은 디스크 WAL 위에 얹혀 있다. 그 WAL은 crash restart 시 복구 관리자가 읽는 바이트 스트림과 동일하다(cubrid-recovery-manager.md와 cubrid-log-manager.md 참고). HA 복제는 LOG_REPLICATION_DATA와 LOG_REPLICATION_STATEMENT를, CDC는 LOG_SUPPLEMENTAL_INFO를 가져간다. 이 레코드들은 복구 관리자가 필요로 하는 물리 undo/redo 레코드와 섞여서 들어가 있다. 생산자 어디에서도 누가 이걸 받아 갈지 알지 못하고, 신경 쓰지도 않는다. 세부 문서들이 로그 관리자 · 복구 관리자 문서와 source walkthrough 심볼 셋을 같이 쓰는 이유가 여기에 있다. 생산자 쪽은 자기가 사는 WAL 과 떼어놓고 보기 어렵기 때문이다.
로컬 생존을 고른 대가는 split-brain 안전 장치
섹션 제목: “로컬 생존을 고른 대가는 split-brain 안전 장치”cub_master 마다 master/slave 판정을 혼자 내리기 때문에,
진짜 master 를 가리는 네트워크 단절이 생기면 slave 는 timeout
만 보고 자기 자신을 승격시켜 split-brain 을 만들 위험이 있다.
heartbeat 문서가 생존 레이어 안에 박아 둔 안전 장치 두 개를
명시한다.
ha_ping_hosts는 외부 주소 목록 (보통은 게이트웨이나 sibling 클러스터의 sentinel) 이다. 로컬 노드가 자기 승격 결정을 신뢰하려면 먼저 이 주소들에 닿을 수 있어야 한다. 닿지 못하면 모든 peer 가 죽어 보이더라도 승격을 거부한다. 자기가 단절된 쪽일 가능성을 배제하지 못하기 때문이다.is_isolated술어는 비-replica peer 전부가HB_NSTATE_UNKNOWN상태이면 로컬 노드를 고립 으로 본다. 고립된 노드는 승격하지 않는다. “다 죽었다” 와 “내가 잘려 나갔다” 를 가릴 방법이 없기 때문이다.
복제 레이어와 CDC 레이어는 이 split-brain 이야기를 다시 풀지 않는다. 둘 다 자기가 보고 있는 master가 단 하나라는 가정 위에서 쓰여 있다.
Failover는 backup-restore와 맞물린다
섹션 제목: “Failover는 backup-restore와 맞물린다”failover와 failback은 역할 이름표만 옮길 뿐, 미디어 복구를 직접 해 주지는 않는다. slave의 로컬 아카이브가 끊긴 경우 — 예컨대 노드가 너무 오래 떨어져 있어서, master 쪽 아카이브 제거기가 slave가 아직 필요로 하는 로그 볼륨을 지워 버린 경우 — HA 복제 레이어는 더 이상 로그 파일을 따라가는 것만으로 따라잡을 수 없다. 이때 복구는 master에서 새 백업을 떠서 복원한 뒤, 복원이 끝난 시점의 LSA부터 로그 전송을 다시 시작하는 길로 빠진다. 이 갈림길은 txn-recovery 섹션에 들어가 있다 — 복원 절차는 cubrid-backup-restore.md, LSA 커서와 복원 지점이 어떻게 이어지는지는 cubrid-checkpoint.md에서 본다. 복제 문서는 이 갈림길을 호명만 하고 복원 절차를 다시 적지는 않는다. CDC 문서도 cdc_min_log_pageid_to_keep이 관리하는 같은 아카이브 보존 압력을 공유한다.
논리 재적용은 바이트 단위로 멱등하지 않다
섹션 제목: “논리 재적용은 바이트 단위로 멱등하지 않다”applylogdb도 옛 la_* 적용자도 논리적 행 이벤트를 스토리지 레이어에 다시 흘려 보낸다. 페이지를 통째로 memcpy하지 않는다. 세부 문서를 읽을 때 머리에 두고 가야 할 결과가 두 가지 따라 나온다. 첫째, slave 페이지가 master 페이지와 물리 레이아웃에서 갈릴 수 있다 — 같은 행, 다른 빈 공간 분포, 다른 B-tree 분기점. 둘째, 재적용은 복구 관리자가 쓰는 의미의 crash-멱등성 (같은 redo 레코드를 다시 적용해도 같은 결과를 보장하는 그 의미) 을 갖지 않는다. 적용자는 영속 커서 last_committed_lsa를 따로 들고 다니다가, 재시작 시 그 커서를 넘긴 지점부터 건너뛰어 다시 시작하는 식으로 풀어낸다. 두 세부 문서 모두 이 커서를 호명만 하고, 복구 관리자의 redo 규율 전체는 다시 풀지 않는다(cubrid-recovery-manager.md로 가야 한다).
개별 문서 요약
섹션 제목: “개별 문서 요약”| 세부 문서 | 레이어 | 주요 심볼 / 소스 파일 | 한 줄 요약 |
|---|---|---|---|
cubrid-heartbeat.md | 생존 | master_heartbeat.c/h, connection/heartbeat.c/h, util_service.c, commdb.c; HB_NODE_ENTRY, HB_NSTATE_*, node->score, 네 스레드 잡 큐 FSM | cub_master 사이의 UDP heartbeat. priority+state로 매기는 peer별 점수. gap 카운터와 last-heard 두 신호로 staleness를 본다. master 선출은 로컬만으로 끝낸다(합의 없음). ha_ping_hosts와 is_isolated가 split-brain을 막는다. FSM이 slave→to-be-master→master, master→slave 전이를 굴려 failover, failback, 강등을 처리한다. |
cubrid-ha-replication.md | 복제 | log_applier.c/h, log_writer.c/h, replication.c/h, log_record.hpp, heap_file.c, btree.c, locator_sr.c; LOG_REPLICATION_DATA, LOG_REPLICATION_STATEMENT, la_apply_log_file, copylogdb | DML 시점에 master가 물리 WAL과 함께 보조 LOG_REPL*을 적는다. copylogdb가 로그 볼륨을 slave로 옮기고, applylogdb(la_apply_log_file)가 레코드 종류별로 스토리지 레이어에 직렬 재적용을 돌린다. 비동기, 논리, 구문+행 혼합. slave는 워크로드상 같지만 바이트 단위로는 같지 않다. |
cubrid-cdc.md | 변경 캡처 | log_manager.c/h, log_applier.c/h, log_applier_sql_log.c/h, log_reader.cpp/hpp, api/cubrid_log.c; LOG_SUPPLEMENTAL_INFO, SUPPLEMENT_REC_TYPE, cdc_make_loginfo, cdc_min_log_pageid_to_keep | 같은 WAL, 두 번째 소비자. 새 cdc_* API는 풀 방식이다(소비자가 LSA 커서를 들고 다음 배치를 요청). 옛 la_* 데몬 경로는 푸시 방식이고 한동안 공존한다. DDL은 LOG_SUPPLEMENT_DDL로 인라인으로 흐른다. 커밋 경계에서 tran_user로 트랜잭션을 묶고, 아카이브 제거는 cdc_min_log_pageid_to_keep이 잡아 둔다. |
인접 섹션
섹션 제목: “인접 섹션”replication-ha 섹션은 이 code-analysis 트리에서 다른 두 묶음 사이에 끼여 있다. 여러 문서를 가로지르는 읽기 경로는 거의 그 경계를 넘어 다닌다.
-
트랜잭션과 복구 측면. 이 섹션의 모든 레이어가 입력으로 쓰는 WAL 이 만들어지는 곳이다.
cubrid-log-manager.md에서 디스크 레코드 형식 (이 섹션의 소비자들이 파싱하는LOG_REPL*·LOG_SUPPLEMENTAL_INFO가족 포함) 을,cubrid-recovery-manager.md에서 같은 WAL 이 한 노드 안에서 굴리는 redo/undo 규율을,cubrid-checkpoint.md에서 LSA 커서와 영속 재시작 지점이 어떻게 이어지는지를,cubrid-backup-restore.md에서 slave 로그가 따라잡기 지점 이전에 잘려 나갔을 때의 미디어 복구 갈림길을 본다. 복제와 CDC 문서 둘 다 이 인접점을 가리키되, WAL 의미를 거기서 다시 풀지는 않는다. -
서버 아키텍처 측면. heartbeat 을 짊어진
cub_master프로세스 는cubrid-architecture-overview.md의 서버 프로세스 트리에 들어 있다. broker (cubrid-broker.md) 는 failover 결과를 받아 쓰는 쪽이다. heartbeat 이 master 이름표를 새 호스트로 옮기면, 클라이언트 연결을 다시 거는 일은 broker 가 한다. heartbeat 문서가 역할이 어떻게 정해지는지를 답하고, broker 문서가 클라이언트가 새 역할을 어떻게 알아채는지를 답한다. 둘이 합쳐지면 master 가 죽은 시점부터 새 master 로 클라이언트 트래픽이 도달하는 시점까지의 고리가 닫힌다.
레코드가 어디서 오는지 알고 싶으면 cubrid-log-manager.md, 어디로 가는지 알고 싶으면 이 섹션, failover 뒤 클라이언트가 어떻게 따라가는지 알고 싶으면 cubrid-broker.md로 들어가면 된다.