(KO) CUBRID Heartbeat — 클러스터 생존 신호, failover, failback
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- CUBRID의 구현
- Source Walkthrough
- Source verification — 2026-05-01 기준
- CUBRID를 넘어서 — 비교 설계와 연구 프론티어
- Sources
학술적 배경
섹션 제목: “학술적 배경”heartbeat 기계는 클러스터 생존(liveness)의 계약을 들고 있는 부품이다. 그 아래에는 두 개의 교과서적 문제가 있다. 하나는 장애 감지(failure detection) — 어느 peer가 사라 졌다고 판단하는 일이다. 다른 하나는 리더 선출(leader election) — 사라진 자리를 누가 대신할지 결정하는 일이다. Designing Data-Intensive Applications (Kleppmann) 5장 “Replication 과 9장 Consistency and Consensus” 가 현대적 인 정리를 준다. Chandra와 Toueg의 Unreliable Failure Detectors for Reliable Distributed Systems (PODC 1996) 는 “비동기 네트워크에서는 어떤 감지기든 안전하지 않거나(거짓 양성) 또는 적시에 작동하지 않거나(느림) 둘 중 하나일 수밖에 없다” 라는 형식적 결과를 준다.
primary/standby 데이터베이스 클러스터에서는 같은 기계가 세 가지 운영 사건을 다룬다.
- Failover — slave가 자신을 master로 승격한다. 기존 master가 더 이상 닿지 않기 때문이다.
- Failback — master가 자신을 slave로 강등한다. 다른 노드가 정당한 master임이 드러난 경우(split-brain의 loser)이거나, 자기 쪽 자원이 망가진 경우다.
- Demote — master가 새 master 선출이 끝날 때까지 일시
적으로 자리를 비키는 것을 말한다. master 측 자원(보통
cub_server의 디스크) 장애가 대표적인 트리거다.
이 모델 위에서 모든 실제 엔진은 다음 세 가지 결정을 내려야 한다. 그 결정이 본 문서의 뼈대를 만든다.
- 합의(consensus)인가, 지역적 결정인가? Raft (Ongaro &
Ousterhout, USENIX ATC 2014) 또는 ZooKeeper의 ZAB는 정
족수(quorum)로 리더를 뽑는다. 즉, 다수가 동의해야 비로소
자기를 리더로 인정한다. CUBRID는 정반대 선택을 했다 —
각
cub_master가 자기 peer 표만 보고 독자적으로 결론 을 낸다는 점이다. 비용은 싸다. 다만 비대칭 단절과 split-brain 같은, 합의 기반 시스템이 피하는 문제들을 그 대로 떠안게 된다. - 푸시(push)인가, 풀(pull)인가? 어떤 시스템은 “나 살아
있다” 를 주기적으로 보낸다. 어떤 시스템은 감시자가
감시되는 대상에 묻는다. CUBRID는 푸시 방식을 택했다 —
cub_master가 알고 있는 모든 다른cub_master에게 UDP heartbeat을 broadcast 하며, 받은 패킷으로 각 peer의 상 태에 대한 자기 인식을 갱신한다. - 네트워크 단절된 slave가 승격하지 못하게 막는 장치는
무엇인가? 단절되어 master가 닿지 않는 slave가 timeout
만 보고 자기를 승격시키면 split-brain이 생긴다. CUBRID
에는 두 안전 장치가 있다. 하나는
ha_ping_hosts(외부 주소 목록. 이쪽이 닿지 않으면 자기 승격을 신뢰하지 않는다는 정책)이고, 다른 하나는is_isolated술어(나 를 제외한 비-replica peer 모두가HB_NSTATE_UNKNOWN이 면 나는 고립이라고 판단한다는 뜻이다.
이 세 답이 보이고 나면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나를 구현하거나 그로부터 만들어지는 상태 기계를 내구하게 만들기 위해 존재함이 분명해진다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”primary/standby 클러스터를 운영하는 모든 엔진 — MySQL/Galera, PostgreSQL+Patroni 또는 repmgr, Oracle Data Guard, MongoDB replica set, CUBRID — 은 교과서적 장애 감지기 위에 동일한 패턴을 얹는다. 이 패턴들은 Chandra-Toueg 원본 논문에는 없는 공통 어휘다. 이론과 코드 사이의 빈자리를 채운다는 점이다.
priority와 state를 묶은 peer 점수
섹션 제목: “priority와 state를 묶은 peer 점수”감지기는 단순히 peer마다 boolean을 내놓는 것으로는 부족하
다. 승격이 결정적이려면 peer들 사이에 순서가 잡혀야 한다는
점이다. 표준은 (state, priority) 를 한 정수로 묶어 비교
가능하게 만드는 것이다. 상위 비트는 역할(master, to-be-master,
slave, replica, unknown)을 인코딩하고, 하위 비트는 같은 역
할 안에서의 priority를 인코딩한다. PostgreSQL의
synchronous_standby_names 우선순위 리스트, MongoDB
멤버 필드 priority, CUBRID의 node->score 가 같은 발상이
다.
heartbeat gap과 last-heard 시각
섹션 제목: “heartbeat gap과 last-heard 시각”서로 독립인 두 staleness 신호가 공존한다. 두 신호
가 서로 다른 장애 양상을 잡아내기 때문이다. gap 카운터
(보낼 때마다 +1, 받을 때마다 -1)는 대칭 손실 을 잡는다 —
양방향이 똑같이 패킷을 떨어뜨리는 경우다. last-heard 시각
은 비대칭 손실 을 잡는다 — 받기는 잘 받지만 우리가 보내
는 쪽이 사라지는 경우다. CUBRID는 두 신호 모두를
HB_NODE_ENTRY 안에 둔다 (heartbeat_gap,
last_recv_hbtime). 둘 중 하나라도 임계값을 넘기면 그 peer
는 HB_NSTATE_UNKNOWN으로 강등된다.
split-brain을 막는 별도의 ping 채널
섹션 제목: “split-brain을 막는 별도의 ping 채널”순수한 peer 대 peer 생존 신호만으로는 master가 죽었다 와
내가 master로부터 단절되었다 를 구별할 수 없다. 이 둘을
틀리게 답하는 노드가 승격하면 split-brain이 발생한다는 점이
다. 표준적 처방은 제3의 기준점을 두는 것이다 — 게이트웨이,
DNS 서버, witness 노드 같은 외부 호스트 목록을 정해 두고,
승격 직전에 이 호스트에 ping을 보내 본다. Pacemaker는 이를
fencing, ZooKeeper는 witness, CUBRID는 ha_ping_hosts
라고 부른다. 의미는 같다 — 자기 peer로부터도 끊기고 witness
까지 닿지 않는 노드라면 진짜로 절단된 것이며, 승격해서는
안 된다.
잡 큐 + 워커 쓰레드
섹션 제목: “잡 큐 + 워커 쓰레드”감지기는 I/O 경로 위에 있을 수 없다. send/recv가
fork()/execv() 같은 느린 동작에 막히면 안 되기 때문이라
는 점이다. 표준은 최소 두 쓰레드로 분리하는 것이다 — 하나는
선(wire)을 읽고, 다른 하나는 디퍼된 잡으로 FSM 전이를 실행
한다. Galera의 gcs 쓰레드와 applier, etcd의 raft.Node.Tick
과 applier, CUBRID의 cluster_reader_th 와
cluster_worker_th 가 같은 모양이다.
자원 측 — 클러스터 결정과 프로세스 관리의 분리
섹션 제목: “자원 측 — 클러스터 결정과 프로세스 관리의 분리”heartbeat 모듈은 또한 로컬 프로세스(데이터베이스 서버, 복제
리더)를 시작하고 종료하고 모드 변경하는 책임도 진다. 이
일을 클러스터 가십(gossip)과 섞으면 데드락이 생긴다 — 가
령 heartbeat을 보내려는데 프로세스 표 락이 잡혀 있어서 막히
는 식이다. 표준 분리는 두 보호 영역에 두 락이다. 하나는
클러스터 상태(peer 표), 다른 하나는 자원 상태(로컬 프로세
스 표)다. CUBRID는 이를 hb_Cluster / hb_Resource 로 구
체화하고, 각각 자기 pthread_mutex_t lock 을 갖는다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 장애 감지 + 리더 선출 | hb_Cluster 전역과 peer 표 (HB_NODE_ENTRY 연결 리스트) |
| 로컬 노드 FSM 상태 | HB_NODE_STATE { UNKNOWN, SLAVE, TO_BE_MASTER, TO_BE_SLAVE, MASTER, REPLICA } (heartbeat.h:86) |
| peer 상태로부터 유도된 score | node->score = node->priority | HB_NODE_SCORE_<state> in hb_cluster_calc_score |
| score의 역할 비트마스크 | HB_NODE_SCORE_MASTER 0x8000, HB_NODE_SCORE_TO_BE_MASTER 0xF000, HB_NODE_SCORE_SLAVE 0x0000, HB_NODE_SCORE_UNKNOWN 0x7FFF (master_heartbeat.h:122-125) |
| 대칭 손실 staleness | HB_NODE_ENTRY::heartbeat_gap + ha_max_heartbeat_gap (기본 5) |
| 비대칭 손실 staleness | HB_NODE_ENTRY::last_recv_hbtime + ha_calc_score_interval_in_msecs (기본 3000) |
| Witness 호스트 채널 | HB_PING_HOST_ENTRY 리스트 (hb_Cluster->ping_hosts); 게이트 플래그 is_ping_check_enabled |
| 고립 술어 | hb_cluster_is_isolated (master_heartbeat.c:762) |
| Split-brain 두 master 동시 존재 감지 | num_master > 1 분기 (hb_cluster_job_calc_score, master_heartbeat.c:867) |
| Cluster 잡 FSM enum | HB_CLUSTER_JOB { INIT, HEARTBEAT, CALC_SCORE, CHECK_PING, FAILOVER, FAILBACK, CHECK_VALID_PING_SERVER, DEMOTE } (master_heartbeat.h:62) |
| Resource 잡 FSM enum | HB_RESOURCE_JOB { PROC_START, PROC_DEREG, CONFIRM_START, CONFIRM_DEREG, CHANGE_MODE, DEMOTE_START_SHUTDOWN, DEMOTE_CONFIRM_SHUTDOWN, CLEANUP_ALL, CONFIRM_CLEANUP_ALL } (master_heartbeat.h:76) |
| 자원 측 프로세스 상태 | HB_PROC_STATE { DEAD, DEREGISTERED, STARTED, REGISTERED_AND_STANDBY, REGISTERED_AND_TO_BE_STANDBY, REGISTERED_AND_ACTIVE, REGISTERED_AND_TO_BE_ACTIVE } (master_heartbeat.h:93) |
| 와이어 헤더 | HBP_HEADER { type, r:1, len, seq, group_id, orig_host_name, dest_host_name } (heartbeat.h:114) |
| 와이어 본문 | or_pack_int 으로 직렬화된 한 개의 HB_NODE_STATE_TYPE (master_heartbeat.c:1719) |
| Reader 쓰레드 | hb_thread_cluster_reader (master_heartbeat.c:4704) |
| Cluster 워커 쓰레드 | hb_thread_cluster_worker (master_heartbeat.c:4659) |
| Resource 워커 쓰레드 | hb_thread_resource_worker (master_heartbeat.c:4769) |
| 서버 hang 감지 쓰레드 | hb_thread_check_disk_failure (master_heartbeat.c:4814) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”heartbeat 모듈에는 네 개의 이동 부품이 있다. 클러스터 측 FSM 은 peer 상태를 가십하고 master를 선출한다. 자원 측 FSM 은 로컬 프로세스를 등록하고 감독한다. 잡 큐 + 워커 쌍 은 두 FSM을 모두 굴린다. 와이어 프로토콜 은 둘이 주고받는 형식이다. 이 순서로 본다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph WIRE["UDP 클러스터 가십"]
PEER1["cub_master @ Node1"]
PEER2["cub_master @ Node2"]
PEER3["cub_master @ Node3"]
end
subgraph LOCAL["로컬 cub_master 프로세스"]
R["cluster_reader_th\nhb_thread_cluster_reader"]
CW["cluster_worker_th\nhb_thread_cluster_worker"]
RW["resource_worker_th\nhb_thread_resource_worker"]
DK["check_disk_failure_th\nhb_thread_check_disk_failure"]
CJQ["cluster_Jobs\n(CJOB 큐)"]
RJQ["resource_Jobs\n(RJOB 큐)"]
HC["hb_Cluster\n(peer 표)"]
HR["hb_Resource\n(로컬 proc 표)"]
end
subgraph PROCS["로컬 HA 프로세스"]
SVR["cub_server"]
CL["copylogdb"]
AL["applylogdb"]
end
PEER2 -- HBP_CLUSTER_HEARTBEAT --> R
PEER3 -- HBP_CLUSTER_HEARTBEAT --> R
R --> HC
CW --> CJQ
CJQ --> CW
CW --> HC
CW --> RJQ
RW --> RJQ
RJQ --> RW
RW --> HR
RW --> SVR
RW --> CL
RW --> AL
DK --> HR
DK --> SVR
PEER1 <-- HBP_CLUSTER_HEARTBEAT --> R
CW -. broadcast .-> PEER1
CW -. broadcast .-> PEER2
CW -. broadcast .-> PEER3
이 그림은 세 경계를 보여 준다. (reader / worker) 와이어
는 한 쓰레드(cluster_reader_th)가 읽고, FSM은 다른 쓰레드
(cluster_worker_th)가 굴린다는 점이다. 둘 사이의 큐
(cluster_Jobs)가 유일한 동기화 지점이다. (cluster /
resource) peer 표 변경과 프로세스 표 변경은 별도 락으로
보호된다 (hb_Cluster->lock, hb_Resource->lock). failover
같은 전이 시 워커 쓰레드가 두 락을 모두 만지지만 각 락은
필요한 시간만 잠시 잡는다는 점이다. (cub_master / 관리되
는 프로세스들) 로컬 cub_master 자체는 데이터베이스 상태
를 갖지 않는다. 그 상태를 가진 프로세스들을 감독할 뿐이다.
클러스터 측 FSM — 노드 상태 전이
섹션 제목: “클러스터 측 FSM — 노드 상태 전이”peer 상태 공간은 여섯 값이다.
// HB_NODE_STATE — src/connection/heartbeat.h:86enum HB_NODE_STATE{ HB_NSTATE_UNKNOWN = 0, HB_NSTATE_SLAVE = 1, HB_NSTATE_TO_BE_MASTER = 2, HB_NSTATE_TO_BE_SLAVE = 3, HB_NSTATE_MASTER = 4, HB_NSTATE_REPLICA = 5, HB_NSTATE_MAX};UNKNOWN 은 정보가 없는 상태다. 모든 노드는 SLAVE 로
시작한다 (replica-only 호스트로 설정된 경우 REPLICA).
peer의 gap 또는 last-heard 시각이 임계값을 넘긴 경우에만
UNKNOWN 으로 떨어진다. TO_BE_MASTER 와 TO_BE_SLAVE 는
전이 도중 상태다. 단, 코드를 보면 TO_BE_SLAVE 는 외부에
서만 도달 가능한 상태다 — 즉, 다른 peer가 자기가 slave로
가고 있다고 알려 주는 경우에만 우리 표에 기록된다.
로컬에서의 MASTER → SLAVE 전이는 이 중간 단계를 거치지
않고 곧장 간다.
stateDiagram-v2 [*] --> SLAVE : start [*] --> REPLICA : ha_replica_list SLAVE --> TO_BE_MASTER : calc_score elects me TO_BE_MASTER --> MASTER : failover confirms TO_BE_MASTER --> SLAVE : failover cancelled MASTER --> SLAVE : failback (split-brain loser) MASTER --> SLAVE : demote (resource fail) MASTER --> UNKNOWN : demote (transient, before SLAVE) UNKNOWN --> SLAVE : demote step 2 SLAVE --> UNKNOWN : peer view only MASTER --> UNKNOWN : peer view only REPLICA --> REPLICA : never elected
가장 중요한 성질은 그림에 없는 것이다 — SLAVE 에서
MASTER 로 직행하는 전이는 없다. 모든 승격은
TO_BE_MASTER 를 거친다. 이 중간 상태가 존재하는 이유는,
이 시점이 새 점수 계산이 승격을 취소 시킬 수 있는 마지막
지점이라는 데 있다. calc_score 의 case : failover 분기
가 먼저 TO_BE_MASTER 를 쓰고, 뒤이어 failover 잡이
hb_cluster_calc_score 를 한 번 더 돌려 보고 결과가 달라졌
으면 SLAVE 로 되돌릴 수 있다.
Score 계산 — 지역적 리더 선출
섹션 제목: “Score 계산 — 지역적 리더 선출”각 cub_master는 자기 타이머 위에서 독립적으로
hb_cluster_calc_score 를 돌린다 (기본
ha_calc_score_interval_in_msecs = 3000). 이 함수는 알고 있
는 모든 peer를 short 점수로 사상한 뒤, 가장 작은 점수가 이긴
다는 규칙으로 선출한다.
// hb_cluster_calc_score — src/executables/master_heartbeat.c:1556static inthb_cluster_calc_score (void){ int num_master = 0; short min_score = HB_NODE_SCORE_UNKNOWN; HB_NODE_ENTRY *node; struct timeval now;
hb_Cluster->myself->state = hb_Cluster->state; gettimeofday (&now, NULL);
for (node = hb_Cluster->nodes; node; node = node->next) { /* Demote stale peers to UNKNOWN — symmetric or asymmetric loss. */ if (node->heartbeat_gap > prm_get_integer_value (PRM_ID_HA_MAX_HEARTBEAT_GAP) || (!HB_IS_INITIALIZED_TIME (node->last_recv_hbtime) && HB_GET_ELAPSED_TIME (now, node->last_recv_hbtime) > prm_get_integer_value (PRM_ID_HA_CALC_SCORE_INTERVAL_IN_MSECS))) { // ... condensed: save peer name if it was master, then ... node->state = HB_NSTATE_UNKNOWN; }
switch (node->state) { case HB_NSTATE_MASTER: case HB_NSTATE_TO_BE_SLAVE: node->score = node->priority | HB_NODE_SCORE_MASTER; /* 0x8000 */ break; case HB_NSTATE_TO_BE_MASTER: node->score = node->priority | HB_NODE_SCORE_TO_BE_MASTER; /* 0xF000 */ break; case HB_NSTATE_SLAVE: node->score = node->priority | HB_NODE_SCORE_SLAVE; /* 0x0000 */ break; case HB_NSTATE_REPLICA: case HB_NSTATE_UNKNOWN: default: node->score = node->priority | HB_NODE_SCORE_UNKNOWN; /* 0x7FFF */ break; }
if (node->score < min_score) { hb_Cluster->master = node; min_score = node->score; } if (node->score < (short) HB_NODE_SCORE_TO_BE_MASTER) num_master++; } return num_master;}분명하지 않은 점이 둘 있다. 첫째, 역할 비트의 값이 MASTER (0x8000) 가 가장 작은 short 가 되도록 의도적으로 배치
되어 있다는 점이다 (0x8000 을 부호 있는 short로 읽으면
-32768 이다). 그래서 min_score 비교가 자연스럽게 기존
master를 선호하게 되고, 두 master가 동률이면 priority가
타이브레이커가 된다. 둘째, TO_BE_SLAVE 는 master 역할
비트를 공유한다. demote 도중인 peer는 후계자가 확
정되기 전까지는 여전히 권위 있는 상태이기 때문이다.
num_master 카운터(점수가 HB_NODE_SCORE_TO_BE_MASTER 보
다 작은 peer 수)는 split-brain 감지기다. 동시에 자기를
master라 여기는 노드가 둘 이상이면 hb_cluster_job_calc_score
호출자가 loser 쪽에 FAILBACK 잡을 큐잉한다.
Cluster 잡 FSM — 워커가 무엇을 디스패치하는가
섹션 제목: “Cluster 잡 FSM — 워커가 무엇을 디스패치하는가”cluster 워커는 cluster_Jobs 에서 잡을 꺼내, cluster 잡
테이블로 디스패치한다.
// hb_cluster_jobs — src/executables/master_heartbeat.c:259static HB_JOB_FUNC hb_cluster_jobs[] = { hb_cluster_job_init, hb_cluster_job_heartbeat, hb_cluster_job_calc_score, hb_cluster_job_check_ping, hb_cluster_job_failover, hb_cluster_job_failback, hb_cluster_job_check_valid_ping_server, hb_cluster_job_demote, NULL};배열 인덱스가 HB_CLUSTER_JOB enum 값이다. 테이블과 enum
은 보조를 맞춘다. 마지막 NULL 은 길이를 명시적으로 끝맺기
위한 것이다.
cluster 잡 사이의 전이는 형식적 상태도로 표현되어 있지 않다. 각 잡이 다음 잡을 자기 손으로 큐잉하면서 떠오르는 흐름이다. 정상 운영의 척추는 다음과 같다.
flowchart LR INIT["INIT"] HB["HEARTBEAT (every 0.5s)"] CV["CHECK_VALID_PING_SERVER"] CS["CALC_SCORE (every 3s)"] CP["CHECK_PING"] FO["FAILOVER"] FB["FAILBACK"] DM["DEMOTE"] INIT --> HB INIT --> CV INIT --> CS HB --> HB CV --> CV CS --> CS CS --> CP CP --> FO CP --> CS CP --> FB FO --> CS FB --> CS DM --> DM
각 화살표는 그 잡이 hb_cluster_job_queue 로 후속 잡을 거
는 동작이다. 가장 의미 있는 분기는
hb_cluster_job_calc_score 안에 있다. 로컬 노드의 상태,
고립 술어, split-brain 카운트에 따라 다음 중 하나로 간다.
다시 CALC_SCORE 로 이어 가거나, 고립 검증을 위해
CHECK_PING 으로 가거나, 직접 FAILBACK 으로 가는 것이다.
Failover 한 번, 처음부터 끝까지
섹션 제목: “Failover 한 번, 처음부터 끝까지”sequenceDiagram
participant M as Old master (Node1)
participant N as Slave (Node2, will promote)
participant P as Slave (Node3)
participant W as ha_ping_hosts witnesses
Note over M: cub_master crash or partition
loop every 0.5s
N->>M: HBP_CLUSTER_HEARTBEAT (req)
N->>P: HBP_CLUSTER_HEARTBEAT (req)
P-->>N: HBP_CLUSTER_HEARTBEAT (resp, state=SLAVE)
Note over N: node[Node1].heartbeat_gap++
end
Note over N: heartbeat_gap > ha_max_heartbeat_gap
N->>N: hb_cluster_calc_score:<br/>node[Node1].state = UNKNOWN
N->>N: min score is myself (priority|SLAVE)
N->>N: state = TO_BE_MASTER, broadcast hb
N->>W: ping ha_ping_hosts
W-->>N: at least one replies
N->>N: hb_cluster_job_failover:<br/>recompute score → still me
N->>N: state = MASTER<br/>hb_Resource->state = MASTER
N->>N: queue HB_RJOB_CHANGE_MODE
N->>P: HBP_CLUSTER_HEARTBEAT (state=MASTER)
P->>P: peer table updated, master=Node2
타이밍 측면에서 두 성질이 중요하다. (a) TO_BE_MASTER 는
ha_failover_wait_time_in_msecs (기본 3초) 동안 유지된다.
witness가 없거나 모든 peer로부터 응답이 도착하지 않은 경우
가 그렇다. 이 시간이 살아 있지만 느린 master가 자기를 다시
주장할 창문이다. (b) TO_BE_MASTER → MASTER 전이
는 hb_cluster_job_failover 안에서 두 번째 score 계산을
거쳐야 일어난다. 첫 계산은 hb_cluster_job_calc_score 에서
이미 했지만 그것만으로는 부족하다. 대기 동안 peer 표가 바
뀌었다면 failover는 취소된다.
Failover — hb_cluster_job_failover
섹션 제목: “Failover — hb_cluster_job_failover”// hb_cluster_job_failover — src/executables/master_heartbeat.c:1163static voidhb_cluster_job_failover (HB_JOB_ARG * arg){ int num_master;
pthread_mutex_lock (&hb_Cluster->lock); num_master = hb_cluster_calc_score ();
if (hb_Cluster->master && hb_Cluster->myself && hb_Cluster->master->priority == hb_Cluster->myself->priority) { /* I am still the highest-priority master after the wait. */ hb_Cluster->state = HB_NSTATE_MASTER; hb_Resource->state = HB_NSTATE_MASTER; hb_resource_job_set_expire_and_reorder (HB_RJOB_CHANGE_MODE, HB_JOB_TIMER_IMMEDIATELY); } else { /* A new master appeared during the wait — abort. */ hb_Cluster->state = HB_NSTATE_SLAVE; } hb_cluster_request_heartbeat_to_all (); pthread_mutex_unlock (&hb_Cluster->lock); // ... condensed: re-queue CALC_SCORE ...}승격은 두 측에서 동시에 일어난다는 점이다.
hb_Cluster->state 가 클러스터 측 FSM을 다음 상태로 옮기
고, hb_Resource->state 가 자원 측 FSM을 다음 상태로 옮긴
다. 자원 측 상태를 읽는 쪽이 HB_RJOB_CHANGE_MODE 잡이며,
이 잡이 로컬 cub_server 의 모드를
HA_SERVER_STATE_STANDBY 에서 HA_SERVER_STATE_ACTIVE 로
바꾼다.
Failback — hb_cluster_job_failback
섹션 제목: “Failback — hb_cluster_job_failback”failback은 무조건적이다. 이 잡이 시작될 때는 호출자가 이미 이 노드는 더 이상 master가 아니라고 결정한 뒤다. 이 잡의 책임은 정리(cleanup)뿐이다.
// hb_cluster_job_failback — src/executables/master_heartbeat.c:1351 (condensed)static voidhb_cluster_job_failback (HB_JOB_ARG * arg){ HB_PROC_ENTRY *proc; pid_t *pids = NULL; int count = 0;
pthread_mutex_lock (&hb_Cluster->lock); hb_Cluster->state = HB_NSTATE_SLAVE; hb_Cluster->myself->state = hb_Cluster->state; hb_cluster_request_heartbeat_to_all (); /* announce SLAVE */ pthread_mutex_unlock (&hb_Cluster->lock);
pthread_mutex_lock (&hb_Resource->lock); hb_Resource->state = HB_NSTATE_SLAVE; for (proc = hb_Resource->procs; proc; proc = proc->next) if (proc->type == HB_PTYPE_SERVER) { pids = (pid_t *) realloc (pids, sizeof (pid_t) * (count + 1)); pids[count++] = proc->pid; } pthread_mutex_unlock (&hb_Resource->lock);
hb_kill_process (pids, count); /* SIGTERM, then SIGKILL on timeout */ // ... condensed: re-queue CALC_SCORE ...}여기서 HB_NSTATE_TO_BE_SLAVE 쓰기가 빠져 있다는 점에 주
의해야 한다. failback은 MASTER 에서 SLAVE 로 한 번에
간다. enum 값으로 TO_BE_SLAVE 가 존재하긴 하지만 도달 가
능한 경로는 외부 peer가 알려 주는 경우뿐이다.
cub_server 프로세스를 mode-change RPC가 아니라 죽이는 방
식을 쓰는 이유는, slave 측 cub_server 의 in-process 설정
(복구 의미론, 로그 적용기)이 master 측과 다르기 때문이다.
재기동이 가장 단순한 방법으로 올바른 구성으로 다시 올라오
게 만든다는 점이다.
Demote — hb_cluster_job_demote
섹션 제목: “Demote — hb_cluster_job_demote”demote는 master가 직접 자리를 비키는 경로다. 보통은 로컬
자원이 망가졌을 때 (hb_thread_check_disk_failure 가 감지)
사용된다. failback과 달리 demote는 새 master가 나타나기를
기다린다 — 한 번씩 1초 간격으로 최대
HB_MAX_WAIT_FOR_NEW_MASTER (60) 회 잡을 다시 큐잉한다는
점이다. 그 시간이 지나면 master를 다시 자기 자신이라 주장
한다.
상태 시퀀스는 MASTER → UNKNOWN → SLAVE 이며, 그 동안
hide_to_demote 플래그가 켜져 있다. 이 플래그가 켜져 있는
동안 로컬 노드는 heartbeat을 broadcast하지 않고 calc_score
참여도 하지 않는다. 그래서 다른 peer 입장에서 보면 이 노드
는 gap 창문 너머로 UNKNOWN 이고, 경합 없이 새 master를
뽑을 수 있다.
와이어 프로토콜 — UDP HBP_HEADER + 상태 한 바이트
섹션 제목: “와이어 프로토콜 — UDP HBP_HEADER + 상태 한 바이트”heartbeat 한 패킷은 HBP_HEADER 한 개와, or-pack된 송신자
의 HB_NODE_STATE_TYPE 정수 한 개로 구성된다.
// HBP_HEADER — src/connection/heartbeat.h:114struct hbp_header{ unsigned char type; /* HBP_CLUSTER_HEARTBEAT */ /* (bit-field portability — bigendian/littleendian variants) */ char reserved:7; char r:1; /* 1 = request, 0 = response */ unsigned short len; /* body length */ unsigned int seq; char group_id[HB_MAX_GROUP_ID_LEN]; /* HA group filter */ char orig_host_name[CUB_MAXHOSTNAMELEN]; char dest_host_name[CUB_MAXHOSTNAMELEN];};헤더에 group_id 가 있는 이유는 한 호스트 위에 여러 HA 클
러스터를 함께 운영할 수 있기 때문이다. group_id 가
hb_Cluster->group_id 와 다른 패킷은 조용히 버려진다는 점
이다. r 비트는 요청과 응답을 구별한다. 요청은 응답을 기
대하며, 송신 시에만 gap 카운터가 +1 된다. 수신 측은 “응답을
보냈다” 같은 카운터를 따로 두지 않는다. 그래서 고립된 노드
의 gap은 받은 요청에 답을 한다 하더라도 단조 증가한다.
// hb_cluster_send_heartbeat_internal — src/executables/master_heartbeat.c:1702 (condensed)static inthb_cluster_send_heartbeat_internal (struct sockaddr_in *saddr, socklen_t saddr_len, char *dest_host_name, bool is_req){ HBP_HEADER *hbp_header; char buffer[HB_BUFFER_SZ], *p;
hbp_header = (HBP_HEADER *) (&buffer[0]); hb_set_net_header (hbp_header, HBP_CLUSTER_HEARTBEAT, is_req, OR_INT_SIZE, 0, dest_host_name);
p = (char *) (hbp_header + 1); p = or_pack_int (p, hb_Cluster->state);
return sendto (hb_Cluster->sfd, buffer, sizeof (HBP_HEADER) + OR_INT_SIZE, 0, (struct sockaddr *) saddr, saddr_len) > 0 ? NO_ERROR : ER_FAILED;}들어오는 heartbeat을 읽고 디스패치
섹션 제목: “들어오는 heartbeat을 읽고 디스패치”hb_thread_cluster_reader 만 hb_Cluster->sfd 위에서
recvfrom 을 호출한다. 본문은 의도적으로 짧다 — 작업을
hb_cluster_receive_heartbeat 에 넘기고 다음 루프로 간다는
점이다.
// hb_thread_cluster_reader — src/executables/master_heartbeat.c:4704 (condensed)static void *hb_thread_cluster_reader (void *arg){ SOCKET sfd = hb_Cluster->sfd; char buffer[HB_BUFFER_SZ + MAX_ALIGNMENT]; char *aligned_buffer = PTR_ALIGN (buffer, MAX_ALIGNMENT); struct pollfd po[1] = { {0, 0, 0} };
while (hb_Cluster->shutdown == false) { po[0].fd = sfd; po[0].events = POLLIN; if (poll (po, 1, 1) <= 0) continue;
if ((po[0].revents & POLLIN) && sfd == hb_Cluster->sfd) { struct sockaddr_in from; socklen_t from_len = sizeof (from); int len = recvfrom (sfd, aligned_buffer, HB_BUFFER_SZ, 0, (struct sockaddr *) &from, &from_len); if (len > 0) hb_cluster_receive_heartbeat (aligned_buffer, len, &from, from_len); } } return NULL;}들어온 패킷을 hb_Cluster 에 반영하는 일은
hb_cluster_receive_heartbeat 에서 일어난다.
dest_host_name이hb_Cluster->host_name과 일치하는지 검사한다 (틀리면 버린다).- 본문 길이가
hbp_header->len과 일치하는지 검사한다. hb_is_heartbeat_valid가 거부하면 (모르는 호스트, 그 룹 불일치, IP 불일치) 진단용HB_UI_NODE_ENTRY에 기 록하고 버린다.r == 1이고hide_to_demote == false이면hb_cluster_send_heartbeat_resp로 응답한다.- peer 표에서 송신자를 찾아
node->state를 갱신하고,heartbeat_gap을 -1 (0 미만으로는 내려가지 않는다), 그리고last_recv_hbtime에 현재 시각을 찍는다. - 이전에 알고 있던 master가 이번 메시지에서 자기를 강등
했다면 (
old.state == MASTER && new.state != MASTER)is_state_changed = true로 표시하고, 락을 푼 뒤hb_cluster_job_set_expire_and_reorder로CALC_SCORE의 만료 시각을 즉시로 당긴다.
마지막 단계가 중요하다. peer 측 상태 변화(예: master가 자
기 디스크 장애 타이머에서 demote 된 경우)가 다음 주기적
CALC_SCORE 를 기다리지 않고 한 heartbeat 간격 안에 우리
시야로 전파되게 만드는 경로다.
자원 측 — 로컬 프로세스 감독
섹션 제목: “자원 측 — 로컬 프로세스 감독”hb_Resource 는 로컬 cub_master 가 책임지는 cub_server,
copylogdb, applylogdb 프로세스를 추적한다. 각 프로세스
당 하나의 HB_PROC_ENTRY 가 할당된다.
// HB_PROC_ENTRY — src/executables/master_heartbeat.h:272 (excerpt)struct HB_PROC_ENTRY{ HB_PROC_ENTRY *next; HB_PROC_ENTRY **prev;
unsigned char state; /* HB_PROC_STATE — REGISTERED_AND_ACTIVE etc. */ unsigned char type; /* HB_PTYPE_SERVER / COPYLOGDB / APPLYLOGDB */ int sfd; /* TCP socket cub_master ↔ process */ int pid; char exec_path[HB_MAX_SZ_PROC_EXEC_PATH]; char args[HB_MAX_SZ_PROC_ARGS];
struct timeval frtime, rtime, dtime, ktime, stime;
unsigned short changemode_rid; unsigned short changemode_gap;
LOG_LSA prev_eof; /* previous server-reported EOF LSA */ LOG_LSA curr_eof; /* current server-reported EOF LSA */ bool is_curr_eof_received;
CSS_CONN_ENTRY *conn; bool being_shutdown; bool server_hang; /* set when prev_eof == curr_eof */};두 LOG_LSA 필드와 server_hang 플래그가
hb_thread_check_disk_failure 가 master 측 로컬 cub_server
의 hang 여부를 판정하는 근거다. 감지기는
ha_check_disk_failure_interval_in_secs (기본 15) 마다 서
버에 현재 EOF LSA를 묻는다. 두 번 연속 같은 답이 오면 서
버가 멈춘 것으로 본다는 점이다. 그러면
HB_RJOB_DEMOTE_START_SHUTDOWN 잡이 큐잉되어
MASTER → SLAVE demote 사슬을 시작한다.
서버 hang 감지
섹션 제목: “서버 hang 감지”// hb_thread_check_disk_failure — src/executables/master_heartbeat.c:4814 (condensed)static void *hb_thread_check_disk_failure (void *arg){ while (hb_Resource->shutdown == false) { int interval = prm_get_integer_value (PRM_ID_HA_CHECK_DISK_FAILURE_INTERVAL_IN_SECS); if (interval > 0 && remaining_time_msecs <= 0) { pthread_mutex_lock (&css_Master_socket_anchor_lock); pthread_mutex_lock (&hb_Cluster->lock); pthread_mutex_lock (&hb_Resource->lock);
if (hb_Cluster->is_isolated == false && hb_Resource->state == HB_NSTATE_MASTER) { if (hb_resource_check_server_log_grow () == false) { /* Two equal EOF LSAs → server wedged → demote. */ hb_Resource->state = HB_NSTATE_SLAVE; // ... condensed: drop locks ... hb_resource_job_queue (HB_RJOB_DEMOTE_START_SHUTDOWN, NULL, HB_JOB_TIMER_IMMEDIATELY); continue; } } if (hb_Resource->state == HB_NSTATE_MASTER) hb_resource_send_get_eof (); /* ask server for fresh EOF */ // ... condensed: unlock ... remaining_time_msecs = interval * 1000; } SLEEP_MILISEC (0, HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS); remaining_time_msecs -= HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS; } return NULL;}감지기는 master 이고 고립이 아닌 경우에만 실제로 작동한
다. 고립 가드가 결정적이다 — 고립된 master는 peer 측에서
오는 is_state_changed 신호를 신뢰할 수 없고, 그래서 자기
서버가 멈춘 것인지 자기 hb 메시지가 빠져나가지 못하고 있는
것인지 구분할 수 없다.
프로세스 등록과 HB_PSTATE_*
섹션 제목: “프로세스 등록과 HB_PSTATE_*”cub_server, copylogdb, applylogdb 는 시작 시 자기를
cub_master 의 제어 소켓으로 등록한다. 등록 메시지는
HBP_PROC_REGISTER 다. 핸들러는 hb_register_new_process
(master_heartbeat.c:4238) 로, args 키로 기존 엔트리를
찾는다 (같은 설정의 재기동이면 슬롯을 재사용한다는 의미다).
없으면 hb_alloc_new_proc 으로 새 엔트리를 할당하고, 엔트
리의 state 필드를 다음 값들 사이로 옮긴다.
// HB_PROC_STATE — src/executables/master_heartbeat.h:93enum HB_PROC_STATE{ HB_PSTATE_UNKNOWN = 0, HB_PSTATE_DEAD = 1, HB_PSTATE_DEREGISTERED = 2, HB_PSTATE_STARTED = 3, HB_PSTATE_NOT_REGISTERED = 4, HB_PSTATE_REGISTERED = 5, HB_PSTATE_REGISTERED_AND_STANDBY = HB_PSTATE_REGISTERED, HB_PSTATE_REGISTERED_AND_TO_BE_STANDBY = 6, HB_PSTATE_REGISTERED_AND_ACTIVE = 7, HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE = 8, HB_PSTATE_MAX};이 enum과 클러스터 측의 관계가 본질이다.
hb_Cluster->state 가 MASTER 로 옮겨 가면,
HB_RJOB_CHANGE_MODE 잡이 HB_PSTATE_REGISTERED_AND_STANDBY
서버 엔트리를 HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE 로 옮
기고 서버 자체에는 HA_SERVER_STATE_ACTIVE 가 되라고 지시
한다. 서버의 응답(hb_resource_receive_changemode,
master_heartbeat.c:4444)이 도착하면 엔트리는
HB_PSTATE_REGISTERED_AND_ACTIVE 로 확정된다.
초기화 흐름
섹션 제목: “초기화 흐름”활성화는 hb_master_init (master_heartbeat.c:5250) 를 통
해 일어난다.
// hb_master_init — src/executables/master_heartbeat.c (sketch)inthb_master_init (void){ hb_cluster_initialize (ha_node_list, ha_replica_list); hb_cluster_job_initialize (); /* queues HB_CJOB_INIT */ hb_resource_initialize (); hb_resource_job_initialize (); hb_thread_initialize (); /* spawns the four threads */ return NO_ERROR;}HB_CJOB_INIT 가 부트스트랩이다. 세 주기 잡(HEARTBEAT,
CHECK_VALID_PING_SERVER, CALC_SCORE)을 큐잉한 뒤 자기는
끝난다. 그 시점부터 클러스터는 자생한다 — 모든 주기 잡이
스스로 자기 후속 잡을 큐잉하기 때문이다.
운영자에게 노출된 활성화 진입점은 hb_activate_heartbeat
(master_heartbeat.c:6599) 다. 비활성화 대칭은
hb_deactivate_heartbeat (6557) 다. 둘 다 cub_commdb (운
영자 측 유틸)로부터 ACTIVATE_HEARTBEAT /
DEACTIVATE_HEARTBEAT 제어 메시지로 도달 가능하며, 핸들러
는 css_process_activate_heartbeat 다.
Source Walkthrough
섹션 제목: “Source Walkthrough”심볼 이름 에 닻을 내려라. 라인 번호가 아니다. CUBRID 소스는 움직인다. 본 절 끝의 위치 표는 문서의
updated:일자에 한정된다.
와이어 프로토콜과 헤더
섹션 제목: “와이어 프로토콜과 헤더”HBP_HEADER(heartbeat.h) — UDP 와이어 헤더.HBP_CLUSTER_HEARTBEAT(heartbeat.h) — 현재 코드에서HBP_HEADER::type의 유일한 값.HBP_PROC_REGISTER(heartbeat.h) — TCP 등록 페이로드.HB_NODE_STATE(heartbeat.h) — 6값 클러스터 FSM.HB_PROC_TYPE(heartbeat.h) — server / copylogdb / applylogdb.hb_set_net_header(master_heartbeat.c) —group_id와orig_host_name등 헤더를 채운다.
클러스터 측 전역과 표
섹션 제목: “클러스터 측 전역과 표”hb_Cluster(master_heartbeat.h) — 전역 포인터.HB_CLUSTER구조체 (master_heartbeat.h) — peer 표, myself / master 커서, 고립 플래그, ping 리스트.HB_NODE_ENTRY(master_heartbeat.h) — peer당 1개.HB_PING_HOST_ENTRY(master_heartbeat.h) — witness 호스트.HB_UI_NODE_ENTRY(master_heartbeat.h) — 미식별 송신 자 진단용.
Cluster 잡
섹션 제목: “Cluster 잡”HB_CLUSTER_JOBenum (master_heartbeat.h) — 8값.hb_cluster_jobs[](master_heartbeat.c) — 함수 테이블.hb_cluster_job_init— HEARTBEAT, CHECK_VALID_PING_SERVER, CALC_SCORE 큐잉.hb_cluster_job_heartbeat— broadcast 후 자기 큐잉.hb_cluster_job_calc_score— 점수 계산, 분류, 분기.hb_cluster_calc_score— 점수 계산 자체.hb_cluster_is_isolated— 모든 비-replica peer가 UNKNOWN인지.hb_cluster_is_received_heartbeat_from_all— failover 대 기 시 사용되는 peer 수신 신선도 술어.hb_cluster_job_check_ping— witness 조회.hb_cluster_check_valid_ping_server— 주기적 witness 갱 신 (실제 ping 사용 가능 여부 플래그만 갱신).hb_cluster_job_check_valid_ping_server— 그 잡 래퍼.hb_cluster_job_failover— 두 번째 점수 계산, 승격.hb_cluster_job_failback— cub_server를 죽이고 강등.hb_cluster_job_demote—hide_to_demote와 함께 새 master 대기 루프.
와이어 I/O
섹션 제목: “와이어 I/O”hb_cluster_receive_heartbeat— 들어오는 패킷 디스패처.hb_cluster_send_heartbeat_internal— 나가는 패킷 빌더.hb_cluster_send_heartbeat_req/_resp— 방향 래퍼.hb_cluster_request_heartbeat_to_all— broadcast 루프; peer마다heartbeat_gap을 +1 한다.
자원 측 전역과 표
섹션 제목: “자원 측 전역과 표”hb_Resource(master_heartbeat.h) — 로컬 proc 표.HB_RESOURCE구조체 — proc 리스트, FSM 상태, shutdown 플래그.HB_PROC_ENTRY— server / copylogdb / applylogdb 당 1개.HB_PROC_STATE— registered / standby / active 등.
Resource 잡
섹션 제목: “Resource 잡”HB_RESOURCE_JOBenum (master_heartbeat.h).hb_resource_jobs[](master_heartbeat.c) — 함수 테이블.hb_resource_job_proc_start/_confirm_start— 죽은 프로세스를 fork+execv 로 살리고 확인.hb_resource_job_proc_dereg/_confirm_dereg— SIGTERM 으로 정상 종료, timeout 시 SIGKILL.hb_resource_job_change_mode— cub_server를 STANDBY ↔ ACTIVE 사이에서 옮긴다.hb_resource_job_demote_start_shutdown/_demote_confirm_shutdown— 디스크-장애 demote 경로.hb_resource_job_cleanup_all/_confirm_cleanup_all—cubrid hb stop시 사용.hb_resource_demote_start_shutdown_server_proc— demote 시 cub_server stop을 시작하는 보조 함수.
프로세스 등록과 changemode
섹션 제목: “프로세스 등록과 changemode”hb_register_new_process—HBP_PROC_REGISTER핸들러.hb_alloc_new_proc/hb_remove_proc—hb_Resource->procs리스트 연산.hb_resource_receive_changemode— 서버의 CHANGE_MODE 응답.hb_resource_receive_get_eof— 서버의 EOF 프로브 응답.hb_resource_send_changemode— 외부로의 changemode RPC.hb_resource_send_get_eof— 외부로의 EOF 프로브.hb_resource_check_server_log_grow—prev_eof == curr_eof술어.
쓰레드와 라이프사이클
섹션 제목: “쓰레드와 라이프사이클”hb_thread_initialize— 4개 워커 쓰레드 생성.hb_thread_cluster_reader— UDP 리더.hb_thread_cluster_worker— CJOB 디스패처.hb_thread_resource_worker— RJOB 디스패처.hb_thread_check_disk_failure— 서버 hang 감지기.hb_master_init— 클러스터 초기화 + 쓰레드 생성 진입점.hb_activate_heartbeat/hb_deactivate_heartbeat— 운영자 측 on/off.hb_resource_shutdown_and_cleanup/hb_cluster_shutdown_and_cleanup— shutdown.
잡 큐 원시 기능
섹션 제목: “잡 큐 원시 기능”HB_JOB,HB_JOB_ENTRY,HB_JOB_ARG(master_heartbeat.h).hb_job_queue/hb_job_dequeue/hb_job_set_expire_and_reorder— cluster, resource 양쪽 이 공유하는 정렬 큐.hb_cluster_job_queue/hb_resource_job_queue— 타입화 된 래퍼.
프로세스 측 다리 (connection/heartbeat.{c,h})
섹션 제목: “프로세스 측 다리 (connection/heartbeat.{c,h})”hb_register_to_master— 서버 / 복제 프로세스가 시작 시 자기를 등록한다.hb_deregister_from_master— 대칭.hb_process_init— 연결, 등록, reader 시작.hb_process_master_request— 프로세스 측 master 소켓 수 신 루프.hb_thread_master_reader— master 측 끊김을 감지하면 자 신을 SIGTERM 한다.
위치 힌트 — 2026-05-01 기준
섹션 제목: “위치 힌트 — 2026-05-01 기준”| Symbol | File | Line |
|---|---|---|
HB_NODE_STATE enum | heartbeat.h | 86 |
HBP_HEADER struct | heartbeat.h | 114 |
HBP_PROC_REGISTER struct | heartbeat.h | 138 |
HBP_CLUSTER_HEARTBEAT | heartbeat.h | 75 |
HB_CLUSTER_JOB enum | master_heartbeat.h | 62 |
HB_RESOURCE_JOB enum | master_heartbeat.h | 76 |
HB_PROC_STATE enum | master_heartbeat.h | 93 |
HB_NODE_SCORE_* macros | master_heartbeat.h | 122 |
HB_NODE_ENTRY struct | master_heartbeat.h | 200 |
HB_CLUSTER struct | master_heartbeat.h | 242 |
HB_PROC_ENTRY struct | master_heartbeat.h | 272 |
HB_RESOURCE struct | master_heartbeat.h | 307 |
hb_cluster_jobs[] | master_heartbeat.c | 259 |
hb_resource_jobs[] | master_heartbeat.c | 272 |
hb_cluster_job_init | master_heartbeat.c | 708 |
hb_cluster_job_heartbeat | master_heartbeat.c | 734 |
hb_cluster_is_isolated | master_heartbeat.c | 762 |
hb_cluster_is_received_heartbeat_from_all | master_heartbeat.c | 785 |
hb_cluster_job_calc_score | master_heartbeat.c | 812 |
hb_cluster_job_check_ping | master_heartbeat.c | 992 |
hb_cluster_job_failover | master_heartbeat.c | 1163 |
hb_cluster_job_demote | master_heartbeat.c | 1236 |
hb_cluster_job_failback | master_heartbeat.c | 1351 |
hb_cluster_check_valid_ping_server | master_heartbeat.c | 1463 |
hb_cluster_job_check_valid_ping_server | master_heartbeat.c | 1500 |
hb_cluster_calc_score | master_heartbeat.c | 1556 |
hb_cluster_request_heartbeat_to_all | master_heartbeat.c | 1646 |
hb_cluster_send_heartbeat_req | master_heartbeat.c | 1677 |
hb_cluster_send_heartbeat_resp | master_heartbeat.c | 1696 |
hb_cluster_send_heartbeat_internal | master_heartbeat.c | 1702 |
hb_cluster_receive_heartbeat | master_heartbeat.c | 1750 |
hb_set_net_header | master_heartbeat.c | 1914 |
hb_cluster_load_group_and_node_list | master_heartbeat.c | 2730 |
hb_resource_demote_start_shutdown_server_proc | master_heartbeat.c | 3307 |
hb_resource_job_demote_confirm_shutdown | master_heartbeat.c | 3416 |
hb_resource_job_demote_start_shutdown | master_heartbeat.c | 3494 |
hb_resource_job_confirm_start | master_heartbeat.c | 3552 |
hb_resource_job_confirm_dereg | master_heartbeat.c | 3702 |
hb_resource_job_change_mode | master_heartbeat.c | 3791 |
hb_alloc_new_proc | master_heartbeat.c | 3925 |
hb_register_new_process | master_heartbeat.c | 4238 |
hb_resource_send_changemode | master_heartbeat.c | 4356 |
hb_resource_receive_changemode | master_heartbeat.c | 4444 |
hb_resource_check_server_log_grow | master_heartbeat.c | 4518 |
hb_resource_send_get_eof | master_heartbeat.c | 4577 |
hb_resource_receive_get_eof | master_heartbeat.c | 4605 |
hb_thread_cluster_worker | master_heartbeat.c | 4659 |
hb_thread_cluster_reader | master_heartbeat.c | 4704 |
hb_thread_resource_worker | master_heartbeat.c | 4769 |
hb_thread_check_disk_failure | master_heartbeat.c | 4814 |
hb_thread_initialize | master_heartbeat.c | 5146 |
hb_master_init | master_heartbeat.c | 5250 |
hb_deactivate_heartbeat | master_heartbeat.c | 6557 |
hb_activate_heartbeat | master_heartbeat.c | 6599 |
css_send_heartbeat_request | connection/heartbeat.c | 160 |
hb_register_to_master | connection/heartbeat.c | 298 |
hb_process_init | connection/heartbeat.c | 691 |
Source verification — 2026-05-01 기준
섹션 제목: “Source verification — 2026-05-01 기준”검증된 사실들
섹션 제목: “검증된 사실들”-
각
cub_master는 자기 master를 독자적으로 결정한다 — 합의 프로토콜은 없다.hb_cluster_calc_score(master_heartbeat.c:1556) 에서 검증했다. 함수가hb_Cluster->nodes를 순회하며 peer마다 로컬score를 계산하고,hb_Cluster->master에 가장 작은 점수의 peer를 쓴다는 점이다. quorum 도,cub_master사이의 투표 도 없 다. 수렴은 모든 노드가 같은 입력(heartbeat에 의해 갱신된 peer 상태)을 보는 데 의존한다. 입력이 갈라지면 결론도 갈 라진다 (split-brain 분기에서 처리). -
역할 비트 상수는 의도적으로
short로 읽었을 때 음수가 되도록 잡혀 있다.master_heartbeat.h:122-125에서 검 증했다.HB_NODE_SCORE_MASTER 0x8000은SHORT_MIN이 며, 부호 있는 short의 최솟값이다. 그래서hb_cluster_calc_score의min_score비교가 자연스럽게 master를 선호한다. priority는 같은 역할 비트 안 에서만 타이브레이커 역할을 한다. 같은 priority의 두 master 는 발생 불가능하다. priority가 설정 순서 1, 2, 3 … 으 로hb_cluster_load_group_and_node_list(master_heartbeat.c:2730) 에서 부여되기 때문이다. -
HB_NSTATE_TO_BE_SLAVE는 로컬 전이로는 도달할 수 없 다.hb_Cluster->state에 대한 모든 쓰기를 검사해 검증 했다. failback은SLAVE를 직접 쓴다 (master_heartbeat.c:1364). demote는MASTER → UNKNOWN → SLAVE로 걸어간다 (1259,1267). failover는MASTER또는SLAVE를 쓴다 (1180,1192). 노드가TO_BE_SLAVE로 들어가는 유일한 경로는hb_cluster_receive_heartbeat가 peer가 보낸 상태를 읽 어 표에 기록하는 경우뿐이다 — peer 측 시야지, 로컬 측 행위가 아니다. -
hide_to_demote == true는 외부 heartbeat 송신과 점수 참여를 모두 막는다.hb_cluster_job_heartbeat(master_heartbeat.c:740,if (hb_Cluster->hide_to_demote == false)가 broadcast을 가두고) 와hb_cluster_job_calc_score(828,goto calc_end가hide_to_demote시 master / split-brain 분기를 건너뛴다) 에서 검증했다. 이 플래그는hb_cluster_job_demote안에서만 켜진다. 새 master를 발견 하면 (1310) 또는 대기 시간이 만료되면 (1274) 해제된다. -
와이어 프로토콜은 헤더 외에 한 정수(송신자 상태)만 싣 는다.
hb_cluster_send_heartbeat_internal(master_heartbeat.c:1702) 에서 검증했다.hb_set_net_header다음에 본문은 정확히or_pack_int (p, hb_Cluster->state)한 줄이고, 길이는OR_INT_SIZE(4 바이트) 다. 수신 측 (1802) 도 같은 필 드를or_unpack_int한다. 다른 본문 내용은 없으며, HBP_HEADER의len필드는 항상 4 다. -
리더는
recvfrom으로 단일 UDP 소켓을 사용한다 — 즉, UDP 손실은 설계상 허용된다.hb_thread_cluster_reader(master_heartbeat.c:4704) 에서 검증했다. 루프마다hb_Cluster->sfd위에서recvfrom을 한 번 호출한다는 점이다. UDP 수준의 재시도는 gap 카운터가 대신한다 — 송 신 시node->heartbeat_gap이 +1, 수신 시 -1 (0 미만으로 떨어지지 않는다)이므로 한 패킷의 손실은 보이지 않는다. 반면ha_max_heartbeat_gap만큼의 연속 손실은 peer를UNKNOWN으로 강등시킨다. -
디스크 장애 감지기는 비-master 노드에서는 가드에 의해 아무 일도 하지 않는다.
hb_thread_check_disk_failure(master_heartbeat.c:4840) 에서 검증했다.is_isolated == false && hb_Resource->state == HB_NSTATE_MASTER가드가 EOF 프로브와 demote 검사 모두를 slave에서는 건너 뛴다는 점이다. 쓰레드는 slave에서도HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS(100 ms) 마다 깨 어나기는 한다. 그 비용은 mutex try-lock 한 번과 다시 잠 드는 일이다. -
cluster 잡 테이블과 cluster 잡 enum의 크기 일치는 컴파 일 시 정적 보증이 없고 관행에만 의존한다.
master_heartbeat.c:259-269(hb_cluster_jobs[]) 와master_heartbeat.h:62(HB_CLUSTER_JOB) 에서 검증했 다.static_assert는 없다. 새 enum 값을 추가하고 테이블 엔트리를 잊은 기여자는 첫 디스패치에서 크래시를 본다. 배열 끝의NULL은 진단용이다 — 디스패치 가드는 이미hb_cluster_job_queue의HB_CJOB_MAX경계 검사가 한다. -
HB_MAX_NUM_NODES = 8은 하드코딩이다 — 8 노드를 넘는 클러스터는 지원되지 않는다.master_heartbeat.h:128에 서 검증했다. 이 값은ha_node_list상한으로 매뉴얼에도 나온다. 런타임 파라미터가 아니다. 늘리려면 소스 변경이 필요하다. -
peer 표와 로컬 proc 표는 별도 mutex를 사용한다.
master_heartbeat.h:244(HB_CLUSTER::lock) 와master_heartbeat.h:309(HB_RESOURCE::lock) 에서 검증 했다. failover와 failback 경로는 두 락을 모두 만진다. failback은SLAVE선언과 서버 kill 사이에 cluster 락을 내려놓는다 (1390,1392). 즉, 죽이는 루프 동안 cluster 락은 잡혀 있지 않으며, 그 덕분에hb_kill_process가 cluster reader를 막지 않을 수 있다. -
활성화 진입점은
cub_commdb운영자가 원격으로 부를 수 있다.master_heartbeat.c:6599(hb_activate_heartbeat) 와commdb.c안의ACTIVATE_HEARTBEAT/DEACTIVATE_HEARTBEAT명령 핸들 러로 검증했다. 운영자 경로는cubrid hb start→cub_commdb --activate-heartbeat→css_process_activate_heartbeat→hb_activate_heartbeat→hb_master_init이다.
미해결 질문
섹션 제목: “미해결 질문”-
HB_MAX_WAIT_FOR_NEW_MASTER = 60의 의도. demote 루 프는 60회 (1초 간격) 후에 자기 자신을 종료하면서hide_to_demote = false만 풀고 (master_heartbeat.c:1274)hb_Cluster->state는MASTER로 되돌리지 않는다. 의 도가 후계자가 안 보이면 원래 master로 복귀하는 것인지, 아니면 SLAVE 로 머무르되 (이때cub_server는 이미 죽었 다) 두는 누수인지 분명하지 않다. 조사 경로는hb_cluster_job_demote종료와 자원 측 후속의 관계를 따 라가는 것이다. -
changemode_rid와changemode_gap는 덱에서 “미사 용” 으로 표기되어 있다.HB_PROC_ENTRY::changemode_rid/_gap의 정의는 여전히master_heartbeat.h:292-293에 있다. 현재 소스 어디에서도 참조되지 않는지, 아니면 죽은 상태로 남아 있는지 확인이 필요하다. 조사 경로 는git grep changemode_rid와git grep changemode_gap를 트리 전체에 돌리는 것이다. -
UDP 패킷 인증은
group_id만 본다.hb_is_heartbeat_valid는group_id와 호스트 해석만 확인하고 암호화 서명은 없다. 위조 UDP 패킷이 올바른group_id와 등록된 호스트 이름 중 하나가 있다 면 받아진다는 점이다. 이것이 HA 위협 모델에서 허용 가능 한지, 아니면 외부 가정(방화벽, 사설망)을 명시해야 하는 지 결정이 필요하다. -
HBP_HEADER의 비트필드 바이트 정렬.heartbeat.h:117-122에_AIX,sparc용r:1과reserved:7의 재배치가 있다. 현대 빌드는 대체로 Linux x86_64 다. 다른 분기는 아마 시험되지 않았을 것이다. 조 사 경로는 가능하다면_AIX/sparc툴체인으로 빌드해 엔디안이 섞인 클러스터에서도 와이어가 상호 운용되는지 확인하는 것이다. -
HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE→_ACTIVE완료 경로. 덱은 전이 자체는 그렸지만 어떤cub_server응답이 이 값을 뒤집는지는 자세히 짚지 않았다. 조사 경로 는hb_resource_receive_changemode(master_heartbeat.c:4444) 와src/connection/server_support.c안의 송신 쪽 짝을 같이 읽는 것이다. -
ha_ping_hosts가 없는 고립 master의 failback. witness가 없는 고립 master는hb_cluster_job_check_ping의ping_check_cancel분기 (master_heartbeat.c:1011)로 빠진다 — 즉, 그대로 master로 머문다. 덱은 이를 “Master 지위 유지 -> 고립 해 소 전 무한 반복” 이라고 적었다. 이것이 영구적 정책으로 의도된 것인지, 아니면 고립이 일정 시간 이상 지속되면 강 등하는 max-isolation 타임아웃이 필요한지 확인이 필요하 다는 점이다. 조사 경로는is_ping_check_enabled기본값 에 닿는 RND 티켓들을 찾는 것이다.
CUBRID를 넘어서 — 비교 설계와 연구 프론티어
섹션 제목: “CUBRID를 넘어서 — 비교 설계와 연구 프론티어”포인터일 뿐이다. 분석이 아니다.
-
Raft (Ongaro & Ousterhout, USENIX ATC 2014) — 정족수 기반 리더 선출. 명시적 term 번호와 로그 비교 규칙이 있다. CUBRID의 노드별
calc_score는 정반대 설계점이다. 후속 문서는 합의를 돌리지 않음으로써 CUBRID이 무엇을 포기하는 지를 정량화할 수 있을 것이다. 특히 미해결 질문 §6 의 고 립 master 무한 보유 행동을 다뤄야 한다. -
ZooKeeper / ZAB (Hunt et al., USENIX ATC 2010) — 리 더가 지정된 원자 broadcast 와, 데이터 경로 외부의 witness 서비스. Pacemaker, etcd, Kubernetes 가 모두 선출을 여기 에 위임한다. CUBRID의
ha_ping_hosts는 비슷한 witness 역할을 훨씬 싸게 달성하지만, ZAB의 안전성 보장은 없다. -
MySQL Group Replication / Galera Cluster — 인증 (certification) 기반 peer-to-peer 복제. 선출은 인증 프로 토콜의 부산물이다. 중앙 코디네이터 없음 선택은 CUBRID 과 비슷하지만, 장애 감지 방식은 다르다 (벡터 시계 vs. heartbeat gap).
-
Patroni (PostgreSQL HA 오케스트레이터) — 리더 선출을 외부 DCS (etcd / Consul / ZooKeeper) 에 위임한다. 데이터 베이스 엔진 자체는 수동 참여자 역할만 한다. CUBRID은 그 역할을
cub_master안에 끼워 넣었다. 비교 문서는 장애 주 입 표면이 어떻게 다른지 추적할 수 있을 것이다. -
Pacemaker + Corosync — Linux-HA 스택의 active/passive 클러스터 도구다. STONITH (Shoot The Other Node In The Head) 가 fence 보장을 제공하는데, CUBRID의 failback에서
kill (proc->pid, SIGKILL)이 근사하는 것이 이것이다. 단, Pacemaker는 노드 전체를 fence 한다 — 데이터베이스 프로세 스만이 아니다. -
MongoDB replica set — heartbeat 기반 선출, priority, 10초 timeout. 상태 기계 (PRIMARY / SECONDARY / RECOVERING / ROLLBACK / FATAL) 가 CUBRID의 6값 FSM 보다 크고, 데이 터 측 상태를 포함한다 — CUBRID는 이 데이터 측 상태를 클 러스터 측 범위 밖에 의도적으로 두었다.
-
Designing Data-Intensive Applications (Kleppmann), 5장 “Replication + 9장 Consistency and Consensus” — CUBRID heartbeat이 내린 선택들의 교과서적 틀이다. 9장의 split-brain 다룸이 위 미해결 질문 §6 의 동기다.
Sources
섹션 제목: “Sources”Raw 분석 자료 (raw/code-analysis/cubrid/distributed/heartbeat/)
섹션 제목: “Raw 분석 자료 (raw/code-analysis/cubrid/distributed/heartbeat/)”heartbeat 코드 분석.pdfheartbeat 코드 분석.pptx_converted/heartbeat-code-analysis.pdf.txt— pdftotext 로 추출한 PDF 텍스트._converted/heartbeat-code-analysis.pptx.md— markitdown 으로 추출한 PPTX 텍스트.
인접 문서
섹션 제목: “인접 문서”knowledge/code-analysis/cubrid/cubrid-recovery-manager.md— failover 시 새 master 위에서 in-doubt 복구가 시작된다.knowledge/code-analysis/cubrid/cubrid-2pc.md— 분산 2PC가 같은 제어 표면(cub_commdb의 XA 경로) 으로 클러 스터 FSM과 상호 작용한다.
교과서 / 논문
섹션 제목: “교과서 / 논문”- Designing Data-Intensive Applications (Kleppmann) 5장 Replication — primary/standby 틀. 동기 vs. 비동기.
- Designing Data-Intensive Applications (Kleppmann) 9장 Consistency and Consensus — split-brain, fencing.
- Chandra & Toueg, Unreliable Failure Detectors for Reliable Distributed Systems, PODC 1996 — 비동기 감지기는 안전성 과 적시성 중 하나를 포기해야 한다는 형식적 결과.
- Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm (Raft), USENIX ATC 2014 — CUBRID의 지역적 결정 설계와의 대조점.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/executables/master_heartbeat.{c,h}—cub_master측. 모듈의 본체.src/connection/heartbeat.{c,h}— 프로세스 측.cub_server/copylogdb/applylogdb가 등록하고 master-reader를 돌리는 코드.src/executables/util_service.c—cubrid hb start|stop|...유틸리티 진입점.src/executables/commdb.c—cub_commdb운영자 유틸리티.cub_master로 명령을 보낸다.src/connection/server_support.c— 서버 측 프로세스 등록.hb_register_to_master가net_server_start→css_init에서 호출된다.