콘텐츠로 이동

(KO) CUBRID Heartbeat — 클러스터 생존 신호, failover, failback

목차

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의 디스크) 장애가 대표적인 트리거다.

이 모델 위에서 모든 실제 엔진은 다음 세 가지 결정을 내려야 한다. 그 결정이 본 문서의 뼈대를 만든다.

  1. 합의(consensus)인가, 지역적 결정인가? Raft (Ongaro & Ousterhout, USENIX ATC 2014) 또는 ZooKeeper의 ZAB는 정 족수(quorum)로 리더를 뽑는다. 즉, 다수가 동의해야 비로소 자기를 리더로 인정한다. CUBRID는 정반대 선택을 했다 — 각 cub_master가 자기 peer 표만 보고 독자적으로 결론 을 낸다는 점이다. 비용은 싸다. 다만 비대칭 단절과 split-brain 같은, 합의 기반 시스템이 피하는 문제들을 그 대로 떠안게 된다.
  2. 푸시(push)인가, 풀(pull)인가? 어떤 시스템은 “나 살아 있다” 를 주기적으로 보낸다. 어떤 시스템은 감시자가 감시되는 대상에 묻는다. CUBRID는 푸시 방식을 택했다 — cub_master가 알고 있는 모든 다른 cub_master에게 UDP heartbeat을 broadcast 하며, 받은 패킷으로 각 peer의 상 태에 대한 자기 인식을 갱신한다.
  3. 네트워크 단절된 slave가 승격하지 못하게 막는 장치는 무엇인가? 단절되어 master가 닿지 않는 slave가 timeout 만 보고 자기를 승격시키면 split-brain이 생긴다. CUBRID 에는 두 안전 장치가 있다. 하나는 ha_ping_hosts (외부 주소 목록. 이쪽이 닿지 않으면 자기 승격을 신뢰하지 않는다는 정책)이고, 다른 하나는 is_isolated 술어(나 를 제외한 비-replica peer 모두가 HB_NSTATE_UNKNOWN 이 면 나는 고립이라고 판단한다는 뜻이다.

이 세 답이 보이고 나면, 본 문서의 모든 CUBRID 구조는 그 답 중 하나를 구현하거나 그로부터 만들어지는 상태 기계를 내구하게 만들기 위해 존재함이 분명해진다.

primary/standby 클러스터를 운영하는 모든 엔진 — MySQL/Galera, PostgreSQL+Patroni 또는 repmgr, Oracle Data Guard, MongoDB replica set, CUBRID — 은 교과서적 장애 감지기 위에 동일한 패턴을 얹는다. 이 패턴들은 Chandra-Toueg 원본 논문에는 없는 공통 어휘다. 이론과 코드 사이의 빈자리를 채운다는 점이다.

감지기는 단순히 peer마다 boolean을 내놓는 것으로는 부족하 다. 승격이 결정적이려면 peer들 사이에 순서가 잡혀야 한다는 점이다. 표준은 (state, priority) 를 한 정수로 묶어 비교 가능하게 만드는 것이다. 상위 비트는 역할(master, to-be-master, slave, replica, unknown)을 인코딩하고, 하위 비트는 같은 역 할 안에서의 priority를 인코딩한다. PostgreSQL의 synchronous_standby_names 우선순위 리스트, MongoDB 멤버 필드 priority, CUBRID의 node->score 가 같은 발상이 다.

서로 독립인 두 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_thcluster_worker_th 가 같은 모양이다.

자원 측 — 클러스터 결정과 프로세스 관리의 분리

섹션 제목: “자원 측 — 클러스터 결정과 프로세스 관리의 분리”

heartbeat 모듈은 또한 로컬 프로세스(데이터베이스 서버, 복제 리더)를 시작하고 종료하고 모드 변경하는 책임도 진다. 이 일을 클러스터 가십(gossip)과 섞으면 데드락이 생긴다 — 가 령 heartbeat을 보내려는데 프로세스 표 락이 잡혀 있어서 막히 는 식이다. 표준 분리는 두 보호 영역에 두 락이다. 하나는 클러스터 상태(peer 표), 다른 하나는 자원 상태(로컬 프로세 스 표)다. CUBRID는 이를 hb_Cluster / hb_Resource 로 구 체화하고, 각각 자기 pthread_mutex_t lock 을 갖는다.

이론적 개념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 상태로부터 유도된 scorenode->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)
대칭 손실 stalenessHB_NODE_ENTRY::heartbeat_gap + ha_max_heartbeat_gap (기본 5)
비대칭 손실 stalenessHB_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 enumHB_CLUSTER_JOB { INIT, HEARTBEAT, CALC_SCORE, CHECK_PING, FAILOVER, FAILBACK, CHECK_VALID_PING_SERVER, DEMOTE } (master_heartbeat.h:62)
Resource 잡 FSM enumHB_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)

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:86
enum 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_MASTERTO_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_scorecase : failover 분기 가 먼저 TO_BE_MASTER 를 쓰고, 뒤이어 failover 잡이 hb_cluster_calc_score 를 한 번 더 돌려 보고 결과가 달라졌 으면 SLAVE 로 되돌릴 수 있다.

cub_master는 자기 타이머 위에서 독립적으로 hb_cluster_calc_score 를 돌린다 (기본 ha_calc_score_interval_in_msecs = 3000). 이 함수는 알고 있 는 모든 peer를 short 점수로 사상한 뒤, 가장 작은 점수가 이긴 다는 규칙으로 선출한다.

// hb_cluster_calc_score — src/executables/master_heartbeat.c:1556
static int
hb_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:259
static 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 으로 가는 것이다.

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_MASTERha_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는 취소된다.

// hb_cluster_job_failover — src/executables/master_heartbeat.c:1163
static void
hb_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은 무조건적이다. 이 잡이 시작될 때는 호출자가 이미 이 노드는 더 이상 master가 아니라고 결정한 뒤다. 이 잡의 책임은 정리(cleanup)뿐이다.

// hb_cluster_job_failback — src/executables/master_heartbeat.c:1351 (condensed)
static void
hb_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는 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:114
struct 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_idhb_Cluster->group_id 와 다른 패킷은 조용히 버려진다는 점 이다. r 비트는 요청과 응답을 구별한다. 요청은 응답을 기 대하며, 송신 시에만 gap 카운터가 +1 된다. 수신 측은 “응답을 보냈다” 같은 카운터를 따로 두지 않는다. 그래서 고립된 노드 의 gap은 받은 요청에 답을 한다 하더라도 단조 증가한다.

// hb_cluster_send_heartbeat_internal — src/executables/master_heartbeat.c:1702 (condensed)
static int
hb_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_readerhb_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 에서 일어난다.

  1. dest_host_namehb_Cluster->host_name 과 일치하는지 검사한다 (틀리면 버린다).
  2. 본문 길이가 hbp_header->len 과 일치하는지 검사한다.
  3. hb_is_heartbeat_valid 가 거부하면 (모르는 호스트, 그 룹 불일치, IP 불일치) 진단용 HB_UI_NODE_ENTRY 에 기 록하고 버린다.
  4. r == 1 이고 hide_to_demote == false 이면 hb_cluster_send_heartbeat_resp 로 응답한다.
  5. peer 표에서 송신자를 찾아 node->state 를 갱신하고, heartbeat_gap 을 -1 (0 미만으로는 내려가지 않는다), 그리고 last_recv_hbtime 에 현재 시각을 찍는다.
  6. 이전에 알고 있던 master가 이번 메시지에서 자기를 강등 했다면 (old.state == MASTER && new.state != MASTER) is_state_changed = true 로 표시하고, 락을 푼 뒤 hb_cluster_job_set_expire_and_reorderCALC_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 사슬을 시작한다.

// 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 메시지가 빠져나가지 못하고 있는 것인지 구분할 수 없다.

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:93
enum 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->stateMASTER 로 옮겨 가면, 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)
int
hb_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 다.

심볼 이름 에 닻을 내려라. 라인 번호가 아니다. 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_idorig_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) — 미식별 송신 자 진단용.
  • HB_CLUSTER_JOB enum (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_demotehide_to_demote 와 함께 새 master 대기 루프.
  • 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 등.
  • HB_RESOURCE_JOB enum (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_allcubrid hb stop 시 사용.
  • hb_resource_demote_start_shutdown_server_proc — demote 시 cub_server stop을 시작하는 보조 함수.
  • hb_register_new_processHBP_PROC_REGISTER 핸들러.
  • hb_alloc_new_proc / hb_remove_prochb_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_growprev_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 한다.
SymbolFileLine
HB_NODE_STATE enumheartbeat.h86
HBP_HEADER structheartbeat.h114
HBP_PROC_REGISTER structheartbeat.h138
HBP_CLUSTER_HEARTBEATheartbeat.h75
HB_CLUSTER_JOB enummaster_heartbeat.h62
HB_RESOURCE_JOB enummaster_heartbeat.h76
HB_PROC_STATE enummaster_heartbeat.h93
HB_NODE_SCORE_* macrosmaster_heartbeat.h122
HB_NODE_ENTRY structmaster_heartbeat.h200
HB_CLUSTER structmaster_heartbeat.h242
HB_PROC_ENTRY structmaster_heartbeat.h272
HB_RESOURCE structmaster_heartbeat.h307
hb_cluster_jobs[]master_heartbeat.c259
hb_resource_jobs[]master_heartbeat.c272
hb_cluster_job_initmaster_heartbeat.c708
hb_cluster_job_heartbeatmaster_heartbeat.c734
hb_cluster_is_isolatedmaster_heartbeat.c762
hb_cluster_is_received_heartbeat_from_allmaster_heartbeat.c785
hb_cluster_job_calc_scoremaster_heartbeat.c812
hb_cluster_job_check_pingmaster_heartbeat.c992
hb_cluster_job_failovermaster_heartbeat.c1163
hb_cluster_job_demotemaster_heartbeat.c1236
hb_cluster_job_failbackmaster_heartbeat.c1351
hb_cluster_check_valid_ping_servermaster_heartbeat.c1463
hb_cluster_job_check_valid_ping_servermaster_heartbeat.c1500
hb_cluster_calc_scoremaster_heartbeat.c1556
hb_cluster_request_heartbeat_to_allmaster_heartbeat.c1646
hb_cluster_send_heartbeat_reqmaster_heartbeat.c1677
hb_cluster_send_heartbeat_respmaster_heartbeat.c1696
hb_cluster_send_heartbeat_internalmaster_heartbeat.c1702
hb_cluster_receive_heartbeatmaster_heartbeat.c1750
hb_set_net_headermaster_heartbeat.c1914
hb_cluster_load_group_and_node_listmaster_heartbeat.c2730
hb_resource_demote_start_shutdown_server_procmaster_heartbeat.c3307
hb_resource_job_demote_confirm_shutdownmaster_heartbeat.c3416
hb_resource_job_demote_start_shutdownmaster_heartbeat.c3494
hb_resource_job_confirm_startmaster_heartbeat.c3552
hb_resource_job_confirm_deregmaster_heartbeat.c3702
hb_resource_job_change_modemaster_heartbeat.c3791
hb_alloc_new_procmaster_heartbeat.c3925
hb_register_new_processmaster_heartbeat.c4238
hb_resource_send_changemodemaster_heartbeat.c4356
hb_resource_receive_changemodemaster_heartbeat.c4444
hb_resource_check_server_log_growmaster_heartbeat.c4518
hb_resource_send_get_eofmaster_heartbeat.c4577
hb_resource_receive_get_eofmaster_heartbeat.c4605
hb_thread_cluster_workermaster_heartbeat.c4659
hb_thread_cluster_readermaster_heartbeat.c4704
hb_thread_resource_workermaster_heartbeat.c4769
hb_thread_check_disk_failuremaster_heartbeat.c4814
hb_thread_initializemaster_heartbeat.c5146
hb_master_initmaster_heartbeat.c5250
hb_deactivate_heartbeatmaster_heartbeat.c6557
hb_activate_heartbeatmaster_heartbeat.c6599
css_send_heartbeat_requestconnection/heartbeat.c160
hb_register_to_masterconnection/heartbeat.c298
hb_process_initconnection/heartbeat.c691
  • 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 0x8000SHORT_MIN 이 며, 부호 있는 short의 최솟값이다. 그래서 hb_cluster_calc_scoremin_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_endhide_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_queueHB_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 startcub_commdb --activate-heartbeatcss_process_activate_heartbeathb_activate_heartbeathb_master_init 이다.

  1. HB_MAX_WAIT_FOR_NEW_MASTER = 60 의 의도. demote 루 프는 60회 (1초 간격) 후에 자기 자신을 종료하면서 hide_to_demote = false 만 풀고 (master_heartbeat.c:1274) hb_Cluster->stateMASTER 로 되돌리지 않는다. 의 도가 후계자가 안 보이면 원래 master로 복귀하는 것인지, 아니면 SLAVE 로 머무르되 (이때 cub_server 는 이미 죽었 다) 두는 누수인지 분명하지 않다. 조사 경로는 hb_cluster_job_demote 종료와 자원 측 후속의 관계를 따 라가는 것이다.

  2. changemode_ridchangemode_gap 는 덱에서 “미사 용” 으로 표기되어 있다. HB_PROC_ENTRY::changemode_rid / _gap 의 정의는 여전히 master_heartbeat.h:292-293 에 있다. 현재 소스 어디에서도 참조되지 않는지, 아니면 죽은 상태로 남아 있는지 확인이 필요하다. 조사 경로 는 git grep changemode_ridgit grep changemode_gap 를 트리 전체에 돌리는 것이다.

  3. UDP 패킷 인증은 group_id 만 본다. hb_is_heartbeat_validgroup_id 와 호스트 해석만 확인하고 암호화 서명은 없다. 위조 UDP 패킷이 올바른 group_id 와 등록된 호스트 이름 중 하나가 있다 면 받아진다는 점이다. 이것이 HA 위협 모델에서 허용 가능 한지, 아니면 외부 가정(방화벽, 사설망)을 명시해야 하는 지 결정이 필요하다.

  4. HBP_HEADER 의 비트필드 바이트 정렬. heartbeat.h:117-122_AIX, sparcr:1reserved:7 의 재배치가 있다. 현대 빌드는 대체로 Linux x86_64 다. 다른 분기는 아마 시험되지 않았을 것이다. 조 사 경로는 가능하다면 _AIX / sparc 툴체인으로 빌드해 엔디안이 섞인 클러스터에서도 와이어가 상호 운용되는지 확인하는 것이다.

  5. HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE_ACTIVE 완료 경로. 덱은 전이 자체는 그렸지만 어떤 cub_server 응답이 이 값을 뒤집는지는 자세히 짚지 않았다. 조사 경로 는 hb_resource_receive_changemode (master_heartbeat.c:4444) 와 src/connection/server_support.c 안의 송신 쪽 짝을 같이 읽는 것이다.

  6. ha_ping_hosts 가 없는 고립 master의 failback. witness가 없는 고립 master는 hb_cluster_job_check_pingping_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 의 동기다.

Raw 분석 자료 (raw/code-analysis/cubrid/distributed/heartbeat/)

섹션 제목: “Raw 분석 자료 (raw/code-analysis/cubrid/distributed/heartbeat/)”
  • heartbeat 코드 분석.pdf
  • heartbeat 코드 분석.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.ccubrid hb start|stop|... 유틸리티 진입점.
  • src/executables/commdb.ccub_commdb 운영자 유틸리티. cub_master 로 명령을 보낸다.
  • src/connection/server_support.c — 서버 측 프로세스 등록. hb_register_to_masternet_server_startcss_init 에서 호출된다.