콘텐츠로 이동

(KO) CUBRID 복제와 HA — 섹션 개요

목차

CUBRID 의 고가용성 그림은 한 줄로 요약된다. 비동기 논리 복제 위에 얹은 primary / standby 클러스터, 그리고 노드별 로컬 점수 로 결정하는 리더 선출이다. 노드마다 cub_master 프로세스가 한 대 떠서, 알고 있는 peer 들에게 UDP heartbeat 을 쏘고, 받은 패킷만 가지고 혼자 “누가 master 인가” 라는 결론을 낸다. Raft 도, Paxos 도, quorum 왕복도 없다. 이 한 가지가 CUBRID HA 를 다른 시스템과 구분 짓는 결정이다.

이 생존 기반 위에서 master 엔진이 보조 레코드 두 종류 (LOG_REPLICATION_DATALOG_REPLICATION_STATEMENT) 를 짜 넣는다. 이 레코드들은 평소 crash recovery 에 쓰는 그 WAL 스트림 안에 그대로 들어간다. slave 호스트의 copylogdb 데몬이 그 볼륨을 통째로 TCP 로 받아 가고, applylogdb 워커 (la_apply_log_file) 가 받은 아카이브를 따라가며 행 이벤트를 레코드 종류별로 스토리지 레이어에 다시 적용한다. 같은 WAL 은 두 번째 소비자에게도 열려 있다. 새 cdc_* API 가 LOG_SUPPLEMENTAL_INFO 레코드를 외부 구독자 (Kafka, 검색 인덱스, 감사 파이프라인 등) 로 흘려 보낸다.

이 섹션의 세부 문서 셋이 위 세 레이어를 하나씩 풀어 둔다. 이 개요는 그 지도다.

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 → mastermaster → 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.cla_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.mdcubrid-broker.md 를 참조).

세부 문서는 아래에서 위로, 즉 생존 레이어부터 읽는 편이 가장 매끄럽다. 아래 층의 결과가 그대로 위 층의 입력이 되기 때문이다.

  1. cubrid-heartbeat.md 를 먼저 읽는다. 노드가 순전히 자기가 가진 정보만으로 “나는 master 다” 또는 “나는 slave 다” 라는 결론을 어떻게 내는지가 여기에 있다. (state, priority) 점수, gap 카운터와 last-heard 라는 두 신호, ha_ping_hosts 안전 장치, 상태 전이를 굴리는 네 스레드 FSM 까지, 이 어휘가 그대로 다른 두 문서에서 다시 도는 도구다. 이 문서는 또 CUBRID 의 설계 결정 (합의 대신 로컬 결정) 을 Raft / ZAB 와 나란히 놓고 그 트레이드오프를 분명히 적어 둔다 (비용은 싸다, 그러나 합의 시스템이 알아서 처리해 주는 비대칭 단절 위험은 본인이 떠안는다).

  2. cubrid-ha-replication.md 를 다음에 읽는다. master 가 누구인지 알고 들어가야 이 문서가 풀린다. 실제 데이터 흐름이 여기에 들어 있다. 세 축 (물리 vs 논리, 구문 vs 행, 동기 vs 비동기) 이라는 틀로, CUBRID 의 선택 (비동기, 논리, 구문+행 혼합) 이 MySQL 행 기반 binlog, PostgreSQL 논리 디코딩, Oracle GoldenGate 와 어떻게 갈리는지를 짚어 준다. 생산자 쪽 (heap_* · btree_* 안에서 LOG_REPLICATION_DATA 가 박히는 자리), 전송 쪽 (copylogdb), 소비자 쪽 (la_apply_log_file 과 그 안의 레코드 종류별 디스패치) 에 어떤 심볼이 어디 사는지 빠짐없이 호명한다.

  3. 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.mdcubrid-log-manager.md 참고). HA 복제는 LOG_REPLICATION_DATALOG_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, 네 스레드 잡 큐 FSMcub_master 사이의 UDP heartbeat. priority+state로 매기는 peer별 점수. gap 카운터와 last-heard 두 신호로 staleness를 본다. master 선출은 로컬만으로 끝낸다(합의 없음). ha_ping_hostsis_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, copylogdbDML 시점에 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로 들어가면 된다.