CUBRID Heartbeat — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-heartbeat.md는 설계 의도와 이론적 배경을 다룬다. 본 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 HA 노드의 전체 생애주기 — 참여(join)와 heartbeat 교환부터 장애 감지, failover까지 — 를 흐름대로 따라갈 수 있다.
목차:
| Ch | 제목 | 상태 |
|---|---|---|
| 1 | 자료 구조 지도 | ✅ |
| 2 | 초기화와 메모리 관리 | ✅ |
| 3 | 잡 큐와 워커 스레드 | ✅ |
| 4 | 네트워크를 통한 하트비트 교환 | ✅ |
| 5 | 점수 계산과 로컬 리더 선출 | ✅ |
| 6 | 페일오버와 핑 위트니스 체크 | ✅ |
| 7 | 피어가 마스터 자리를 되찾을 때의 페일백 | ✅ |
| 8 | 로컬 리소스 장애 시 역할 강등 | ✅ |
| 9 | 리소스 측과 프로세스 감시 | ✅ |
| 10 | 운영자 명령 처리 브리지와 종료 경로 | ✅ |
Chapter 1: 자료 구조 지도
섹션 제목: “Chapter 1: 자료 구조 지도”이 챕터는 다음 질문에 답한다: heartbeat 모듈이 메모리에서 유지하는 상태는 무엇이고, 각 필드는 어떤 이유로 존재하는가? 순수한 저장 구조의 지도 — 왜 CUBRID가 로컬에서 리더를 선출하는가의 서사는 cubrid-heartbeat.md(“Local leader election”)에 맡기고, 이 챕터는 구조만 다룬다. 상태는 클러스터 패밀리(hb_Cluster 루트), 리소스 패밀리(hb_Resource 루트), 와이어 패밀리(HBP_HEADER, HBP_PROC_REGISTER), 공용 리스트/잡 기계류(HB_LIST, HB_JOB, HB_JOB_ENTRY, HB_JOB_ARG)로 나뉜다. 클러스터/리소스/잡 타입은 src/executables/master_heartbeat.h에, 와이어 타입과 HB_NODE_STATE는 src/connection/heartbeat.h에 정의된다. 각 필드 테이블은 역할 / 이유를 하나의 셀에 담았다. 레이아웃이 자명한 구조체는 재인용 없이 테이블만 제시하며, 모든 심볼은 뒤에 나오는 위치 힌트 테이블에 앵커된다.
1.1 네 개의 전역 변수와 그 연결 구조
섹션 제목: “1.1 네 개의 전역 변수와 그 연결 구조”master_heartbeat.h의 extern 포인터 네 개가 전체 상태의 루트다: hb_Cluster(멤버십 + 선출), hb_Resource(감시 프로세스), 타이머 큐 cluster_Jobs / resource_Jobs.
flowchart TD C["hb_Cluster<br/>HB_CLUSTER"] C -->|nodes| N1["HB_NODE_ENTRY<br/>(침투형 리스트)"] C -->|myself / master 커서| N1 C -->|ping_hosts| P1["HB_PING_HOST_ENTRY"] C -->|ui_nodes| U1["HB_UI_NODE_ENTRY"] R["hb_Resource<br/>HB_RESOURCE"] R -->|procs| PR["HB_PROC_ENTRY"] PR -->|conn| CONN["CSS_CONN_ENTRY"] CJ["cluster_Jobs<br/>HB_JOB"] CJ -->|jobs| JE1["HB_JOB_ENTRY<br/>(expire 순 정렬)"] JE1 -->|arg| JA["HB_JOB_ARG (union)"] RJ["resource_Jobs<br/>HB_JOB"] RJ -->|jobs| JE2["HB_JOB_ENTRY"] JE2 -->|arg| JA
Figure 1-1. 네 개의 전역 변수와 그것들이 소유하는 리스트. myself와 master는 별도로 할당된 객체가 아니라 nodes 리스트 안의 커서다.
불변식 — myself와 master는 nodes에 대한 별칭이다. 둘 다 이미 nodes에 연결된 노드를 가리키며(master는 알 수 없을 때 NULL), 독립적으로 할당되지 않는다. 살아 있는 커서가 참조하는 노드를 해제하면, 이후의 점수 계산이나 failover 결정에서 해제된 메모리를 역참조하게 된다.
1.2 HB_CLUSTER — 멤버십과 선출의 루트
섹션 제목: “1.2 HB_CLUSTER — 멤버십과 선출의 루트”// hb_cluster -- src/executables/master_heartbeat.hstruct hb_cluster { pthread_mutex_t lock; SOCKET sfd; HB_NODE_STATE_TYPE state; char group_id[HB_MAX_GROUP_ID_LEN]; char host_name[CUB_MAXHOSTNAMELEN]; int num_nodes; HB_NODE_ENTRY *nodes; HB_NODE_ENTRY *myself; HB_NODE_ENTRY *master; /* cursors into nodes */ bool shutdown, hide_to_demote, is_isolated, is_ping_check_enabled; HB_PING_HOST_ENTRY *ping_hosts; int num_ping_hosts; int ping_timeout; /* TCP ping only */ HB_UI_NODE_ENTRY *ui_nodes; int num_ui_nodes;};| 필드 | 역할 / 이유 |
|---|---|
lock | 구조체 뮤텍스; 워커와 리더를 직렬화한다. |
sfd | UDP 소켓, 송수신용; 초기화 전에는 -1. |
state | 나의 HB_NODE_STATE; failover 시 변경된다. |
group_id | HA 그룹 (HB_MAX_GROUP_ID_LEN=64); 다른 그룹의 패킷은 거부된다. |
host_name | 나의 호스트 이름; 자신을 식별하고 orig_host_name에 스탬프로 사용된다. |
num_nodes | nodes 길이의 캐시 값. |
nodes | HB_NODE_ENTRY 리스트 헤드; 멤버십(피어 + 자신). |
myself | nodes 안의 자신을 가리키는 커서; 검색 없이 자신의 점수에 접근. |
master | nodes 안의 마스터를 가리키는 커서 또는 NULL; failover/failback 시 재작성된다. |
shutdown | 클러스터 스레드 종료 플래그. |
hide_to_demote | demote 중 불필요한 로그 출력을 억제한다. |
is_isolated | 어떤 피어도 닿지 않음; failover를 제어한다. |
is_ping_check_enabled | ping 검사 실행 여부; ping 호스트가 없으면 off. |
ping_hosts | ping 증인(witness) 리스트 헤드 (외부 IP). |
num_ping_hosts | ping_hosts 길이의 캐시 값. |
ping_timeout | ping당 타임아웃, TCP 전용. |
ui_nodes | 미확인 노드(unidentified node) 리스트 헤드; 오류 로그 빈도를 제어한다. |
num_ui_nodes | ui_nodes 길이의 캐시 값. |
불변식 — num_nodes는 lock 하에 nodes 길이와 같다. 삽입/삭제 시 동일한 임계 구역에서 둘을 함께 조정해야 한다. 값이 어긋나면 점수 루프가 피어를 건너뛰거나 끝을 넘어 실행된다.
1.3 HB_NODE_ENTRY — 설정된 피어 하나
섹션 제목: “1.3 HB_NODE_ENTRY — 설정된 피어 하나”struct hb_node_entry (master_heartbeat.h): §1.8의 next/prev 링크, 그 다음에 host_name, priority (unsigned short), state, score (short), heartbeat_gap (short), last_recv_hbtime (struct timeval).
| 필드 | 역할 / 이유 |
|---|---|
next / prev | nodes에 대한 침투형 링크 (§1.8). |
host_name | 피어 호스트 이름; 수신 패킷의 orig_host_name과 매칭 키로 사용된다. |
priority | 선출 가중치, 낮을수록 강함; HB_REPLICA_PRIORITY=0x7FFF는 마스터가 될 수 없음을 의미한다. |
state | 피어의 최근 알려진 HB_NODE_STATE; 선출에 반영된다. |
score | 선출 점수; Ch 5에서 HB_NODE_SCORE_*로 설정되며, 가장 작은 값이 이긴다. |
heartbeat_gap | 누락된 박동 수; HB_DEFAULT_MAX_HEARTBEAT_GAP=5를 초과하면 down으로 간주. |
last_recv_hbtime | 마지막 박동 수신 시각; 현재 시간과의 차이로 gap/고립 판단, {0,0}은 수신한 적 없음을 의미. |
1.4 HB_PING_HOST_ENTRY — 외부 ping 증인
섹션 제목: “1.4 HB_PING_HOST_ENTRY — 외부 ping 증인”struct hb_ping_host_entry (master_heartbeat.h): §1.8 링크, 그 다음에 host_name, port (int), ping_result (int).
| 필드 | 역할 / 이유 |
|---|---|
next / prev | ping_hosts에 대한 침투형 링크 (§1.8). |
host_name | 증인의 IP/호스트 이름; 탐침 대상. |
port | TCP 포트, TCP ping 전용; ICMP는 무시한다. |
ping_result | 마지막 탐침 결과; HB_PING_RESULT (§1.12). |
1.5 HB_UI_NODE_ENTRY — 캐시된 미확인 피어
섹션 제목: “1.5 HB_UI_NODE_ENTRY — 캐시된 미확인 피어”검증에 실패한 heartbeat는 여기에 캐시된다. 덕분에 오류 로그가 매 패킷마다 쏟아지는 대신 빈도가 제어된다. struct hb_ui_node_entry (master_heartbeat.h): §1.8 링크, 그 다음에 host_name, group_id, saddr (struct sockaddr_in), last_recv_time (struct timeval), v_result (int).
| 필드 | 역할 / 이유 |
|---|---|
next / prev | ui_nodes에 대한 침투형 링크 (§1.8). |
host_name | 주장된 호스트 이름; “이미 기록됐는가?” 판단 키. |
group_id | 주장된 그룹; 매칭 키의 일부. |
saddr | 출처 sockaddr_in; HB_VALID_IP_ADDR_MISMATCH 탐지에 사용된다. |
last_recv_time | 마지막 수신 시각; 캐시 유지 기간 및 퇴거(..._CACHE_/..._CLEANUP_TIME_...) 기준. |
v_result | 검증 결과; HB_VALID_RESULT (§1.13). |
1.6 HB_RESOURCE — 로컬 감시 프로세스의 루트
섹션 제목: “1.6 HB_RESOURCE — 로컬 감시 프로세스의 루트”struct hb_resource (master_heartbeat.h): lock (pthread_mutex_t), state (HB_NODE_STATE_TYPE, 모드), num_procs (int), procs (HB_PROC_ENTRY *), shutdown (bool).
| 필드 | 역할 / 이유 |
|---|---|
lock | 구조체 뮤텍스; 워커와 커맨드 핸들러가 procs를 변경한다. |
state | 리소스 측 역할; 클러스터 판정을 미러링하며, failover 시 changemode를 전파한다. |
num_procs | procs 길이의 캐시 값; 최대 HB_MAX_NUM_RESOURCE_PROC=16. |
procs | HB_PROC_ENTRY 리스트 헤드; 등록된 프로세스마다 하나. |
shutdown | 리소스 스레드 종료 플래그. |
불변식 — HB_RESOURCE::state는 HB_CLUSTER::state를 추적한다. 리소스 측은 선출에 참여하지 않는다. 클러스터 판정을 미러링하며, Ch 8에서 demote 시 모든 proc에 changemode를 푸시한다. 값이 어긋나면 클러스터는 마스터인데 서버는 standby로 계속 동작하는 상황이 발생한다.
1.7 HB_PROC_ENTRY — 감시 대상 프로세스 하나
섹션 제목: “1.7 HB_PROC_ENTRY — 감시 대상 프로세스 하나”가장 밀도 높은 구조체로, 상태 기계(state machine)에 더해 재시작/확인/changemode 부기(bookkeeping)를 담는다.
// HB_PROC_ENTRY -- src/executables/master_heartbeat.hstruct HB_PROC_ENTRY { HB_PROC_ENTRY *next; HB_PROC_ENTRY **prev; unsigned char state; /* HB_PROC_STATE */ unsigned char type; /* HB_PROC_TYPE */ int sfd; 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; /* first-reg/reg/dereg/kill/start */ unsigned short changemode_rid; unsigned short changemode_gap; LOG_LSA prev_eof; LOG_LSA curr_eof; bool is_curr_eof_received; CSS_CONN_ENTRY *conn; bool being_shutdown; bool server_hang;};| 필드 | 역할 / 이유 |
|---|---|
next / prev | procs에 대한 침투형 링크 (§1.8). |
state | HB_PROC_STATE (§1.11); unsigned char는 broker.c의 SERVER_STATE와 일치한다. |
type | HB_PROC_TYPE (§1.12); 감시 정책을 결정한다. |
sfd | 소켓 fd; hb_return_proc_state_by_fd의 키. |
pid | OS pid; TERM/KILL, 시작/종료 확인에 사용된다. |
exec_path | 실행 파일 경로 (128); 재시작에 사용된다. |
args | argv (HB_MAX_SZ_PROC_ARGS=1024); 재시작 및 dereg-by-args 키. |
frtime | 최초 등록 시각; 재시작이 너무 빠른지 판단하는 기준점. |
rtime | 가장 최근 등록 시각. |
dtime | 등록 해제 시각; {0,0}은 아직 등록된 상태임을 의미한다. |
ktime | kill/shutdown 시각; 등록 해제 확인 기준점. |
stime | 시작 시각; 시작 확인 재시도에 사용된다. |
changemode_rid | 진행 중인 changemode 요청 id; 응답 연결에 사용된다. |
changemode_gap | changemode 실패 횟수; TERM 에스컬레이션(..._TO_TERM=12), KILL(=24) 기준. |
prev_eof | 이전 폴링 로그 끝 LSA; hang 탐지에 사용된다. |
curr_eof | 최신 로그 끝 LSA (LOG_LSA, §1.7.1). |
is_curr_eof_received | curr_eof 응답 수신 여부; hang 검사를 보호한다. |
conn | CSS_CONN_ENTRY; changemode 전송 및 EOF 취득에 사용된다. |
being_shutdown | 종료 진행 중; 재시작을 억제한다. |
server_hang | hang 플래그; EOF가 멈추면 설정되고 hb_is_hang_process가 읽는다. |
1.7.1 LOG_LSA 요약
섹션 제목: “1.7.1 LOG_LSA 요약”prev_eof/curr_eof는 LOG_LSA (struct log_lsa, log_lsa.hpp)로, pageid:48+offset:16으로 팩킹된 구조체이며 불투명하게(opaque) 취급된다. 값이 전진하면 살아 있는 것이고, 멈추면 server_hang을 무장시킨다. LSA의 전체 의미는 recovery/log 모듈에 속한다.
1.8 침투형 리스트 관용구 — HB_LIST
섹션 제목: “1.8 침투형 리스트 관용구 — HB_LIST”HB_NODE_ENTRY, HB_PING_HOST_ENTRY, HB_UI_NODE_ENTRY, HB_PROC_ENTRY, HB_JOB_ENTRY는 모두 동일한 next/prev 쌍을 내장하며, 이를 총칭하여 HB_LIST라 한다.
// hb_list -- src/executables/master_heartbeat.hstruct hb_list { HB_LIST *next; HB_LIST **prev; /* address of the predecessor's `next` slot */};prev는 포인터 투 포인터로, 현재 노드를 가리키는 슬롯의 주소를 담는다(첫 번째 원소라면 &head, 그렇지 않으면 &predecessor->next). Linux hlist 관용구와 동일하여, 연결 해제는 *prev = next; if (next) next->prev = prev;로 처리되며 헤드에 대한 예외 처리가 필요 없다.
불변식 — 연결된 모든 노드에서 *(entry->prev) == entry 가 성립한다. 이 조건이 깨지면, 연결 해제 시 dangling prev로 무관한 next를 오염시킨다.
1.9 타이머 잡 기계류 — HB_JOB, HB_JOB_ENTRY, HB_JOB_ARG
섹션 제목: “1.9 타이머 잡 기계류 — HB_JOB, HB_JOB_ENTRY, HB_JOB_ARG”클러스터와 리소스 양쪽 모두 데드라인 순으로 정렬된 큐 하나로 미래 작업을 스케줄한다(워커 실행은 Ch 3에서 다룬다). HB_JOB_FUNC는 typedef void (*)(HB_JOB_ARG *)다.
struct hb_job (큐): lock (pthread_mutex_t), num_jobs (unsigned short), jobs (HB_JOB_ENTRY *), job_funcs (HB_JOB_FUNC *), shutdown (bool):
HB_JOB 필드 | 역할 / 이유 |
|---|---|
lock | 큐 뮤텍스; 워커가 팝하는 동안 스케줄러가 푸시한다. |
num_jobs | 캐시된 잡 개수. |
jobs | expire 기준 오름차순 정렬된 HB_JOB_ENTRY 헤드; 가장 임박한 것이 맨 앞. |
job_funcs | 디스패치 테이블, 잡 type → 핸들러; 클러스터/리소스가 각각 다르다. |
shutdown | 워커 종료 플래그. |
struct hb_job_entry (잡 하나): §1.8의 next/prev, 그 다음:
HB_JOB_ENTRY 필드 | 역할 / 이유 |
|---|---|
type (unsigned int) | HB_CLUSTER_JOB/HB_RESOURCE_JOB (§1.14); 핸들러를 선택한다. |
expire (timeval) | 절대 데드라인; 워커는 헤드의 expire까지 슬립한다. |
func (HB_JOB_FUNC) | 핸들러; 엔트리 자체로 자급자족 가능. |
arg (HB_JOB_ARG *) | 핸들러가 소유하고 해제하는 힙 인수. |
불변식 — jobs는 expire 기준 오름차순으로 정렬된다. 워커는 헤드만 검사하므로, 순서가 어긋난 삽입이 발생하면 failover 확인이 늦게 실행된다.
// hb_cluster_job_arg / hb_resource_job_arg / hb_job_arg -- master_heartbeat.hstruct hb_cluster_job_arg { unsigned int ping_check_count; unsigned int retries; };struct hb_resource_job_arg { int pid; int sfd; char args[HB_MAX_SZ_PROC_ARGS]; unsigned int retries; unsigned int max_retries; struct timeval ftime, ltime; /* first / last job execution time */};union hb_job_arg { HB_CLUSTER_JOB_ARG cluster_job_arg; HB_RESOURCE_JOB_ARG resource_job_arg;};HB_JOB_ARG는 union이다: 잡은 클러스터이거나 리소스이지, 둘 다일 수 없으므로, 두 형태가 하나의 할당 위에 겹쳐진다. 활성 arm은 어느 큐가 잡을 보유하는지(그리고 HB_JOB_ENTRY::type)로 암묵적으로 결정된다.
HB_CLUSTER_JOB_ARG | 역할 / 이유 | HB_RESOURCE_JOB_ARG | 역할 / 이유 | |
|---|---|---|---|---|
ping_check_count | 실행된 ping 라운드 수, HB_MAX_PING_CHECK=3 기준 | pid / sfd | 대상 프로세스 / 연결 fd | |
retries | 범용 재시도 카운터 | args | 재시작용 argv | |
retries / max_retries | 지금까지 재시도 횟수 / 상한 | |||
ftime / ltime | 최초 실행 시각(백오프) / 최근 실행 시각 |
1.10 와이어 패밀리 — HBP_HEADER와 HBP_PROC_REGISTER
섹션 제목: “1.10 와이어 패밀리 — HBP_HEADER와 HBP_PROC_REGISTER”이 구조체들은 네트워크로 직렬화되므로 바이트 레이아웃이 중요하다.
// hbp_header -- src/connection/heartbeat.hstruct hbp_header { unsigned char type; /* HBP_CLUSTER_MESSAGE */#if defined(HPUX) || defined(_AIX) || defined(sparc) char r:1; /* is request? -- MSB-first hosts */ char reserved:7;#else char reserved:7; /* LSB-first hosts (x86): order flipped */ char r:1;#endif unsigned short len; unsigned int seq; char group_id[HB_MAX_GROUP_ID_LEN]; char orig_host_name[CUB_MAXHOSTNAMELEN]; char dest_host_name[CUB_MAXHOSTNAMELEN];};| 필드 | 역할 / 이유 |
|---|---|
type | 메시지 종류; HBP_CLUSTER_MESSAGE (§1.13). |
r:1 | 요청-vs-응답 비트; reserved와 같은 바이트를 공유한다. |
reserved:7 | 플래그 바이트 패딩; 미래 플래그를 위한 예약. |
len | 헤더 이후 본문 길이. |
seq | 시퀀스 번호; 중복/오래된 박동 탐지. |
group_id | 송신자의 HA 그룹; HB_CLUSTER::group_id와 검증된다. |
orig_host_name | 송신자 호스트 이름; nodes에서의 매칭 키. |
dest_host_name | 수신자; 여기로 오지 않은 박동은 무시한다. |
불변식 — r:1/reserved:7 비트 순서는 엔디언 조건부다. HPUX || _AIX || sparc 경우에는 r이 먼저 오고, 나머지(리틀-엔디언) 호스트에서는 reserved가 먼저 온다. 둘 다 동일한 와이어 바이트를 인코딩한다. #if를 제거하면 빅-엔디언과 리틀-엔디언 노드가 서로 반대쪽 끝에서 요청 비트를 읽게 된다.
// hbp_proc_register -- src/connection/heartbeat.hstruct hbp_proc_register { int pid; int type; /* type = HB_PROC_TYPE */ char exec_path[HB_MAX_SZ_PROC_EXEC_PATH]; char args[HB_MAX_SZ_PROC_ARGS];};| 필드 | 역할 / 이유 |
|---|---|
pid | 등록하는 프로세스의 pid; HB_PROC_ENTRY::pid가 된다. |
type | 프로세스 종류 (HB_PROC_TYPE); HB_PROC_ENTRY::type이 된다. |
exec_path | 경로; 재시작을 위해 복사된다. |
args | argv; 복사되며, dereg-by-args 키로 사용된다. |
HBP_PROC_REGISTER는 자식 프로세스가 등록 시 전송하는 본문이다. 마스터는 이를 새로운 HB_PROC_ENTRY에 복사한다(Ch 9). 이 메시지는 HBP_HEADER를 운반하는 UDP 소켓이 아니라, 리소스 연결(CSS_CONN_ENTRY)로 전달된다.
1.11 노드와 프로세스 상태 열거형
섹션 제목: “1.11 노드와 프로세스 상태 열거형”HB_NODE_STATE (typedef HB_NODE_STATE_TYPE)는 HB_CLUSTER::state, HB_NODE_ENTRY::state, HB_RESOURCE::state가 공유한다.
| 값 | 의미 | 값 | 의미 | |
|---|---|---|---|---|
HB_NSTATE_UNKNOWN (0) | 미결정 | HB_NSTATE_TO_BE_SLAVE (3) | demote 진행 중 | |
HB_NSTATE_SLAVE (1) | standby | HB_NSTATE_MASTER (4) | 활성 마스터 | |
HB_NSTATE_TO_BE_MASTER (2) | 승격 진행 중 | HB_NSTATE_REPLICA (5) | 절대 마스터가 될 수 없는 replica | |
HB_NSTATE_MAX (6) | 센티넬 |
HB_PROC_STATE는 HB_PROC_ENTRY::state의 프로세스별 상태 기계다.
| 값 | 의미 | 값 | 의미 | |
|---|---|---|---|---|
HB_PSTATE_UNKNOWN (0) | 초기화되지 않음 | HB_PSTATE_REGISTERED (5) | 등록됨 | |
HB_PSTATE_DEAD (1) | 실행 중이 아님 | HB_PSTATE_REGISTERED_AND_STANDBY | 별칭 = 5 | |
HB_PSTATE_DEREGISTERED (2) | 정상 등록 해제 | HB_PSTATE_REGISTERED_AND_TO_BE_STANDBY (6) | demote 대기 중 | |
HB_PSTATE_STARTED (3) | 스폰됨, 등록 대기 | HB_PSTATE_REGISTERED_AND_ACTIVE (7) | 마스터로 서비스 중 | |
HB_PSTATE_NOT_REGISTERED (4) | 실행 중, 아직 미등록 | HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE (8) | 승격 대기 중 | |
HB_PSTATE_MAX (9) | 센티넬 |
REGISTERED == REGISTERED_AND_STANDBY 별칭에 주의. 새로 등록된 프로세스는 standby이므로, 두 이름이 값 5를 공유한다. switch에서 둘을 동시에 case로 나열할 수 없고(중복 case), 값만으로는 구분이 불가능하다. 헤더 주석에 따르면 이 넘버링은 broker.c의 SERVER_STATE를 미러링한다.
1.12 프로세스 타입과 ping 결과 열거형
섹션 제목: “1.12 프로세스 타입과 ping 결과 열거형”HB_PROC_TYPE (hb_proc_type)과 HB_PING_RESULT (참고: HB_PING_UNKNOWN은 -1):
HB_PROC_TYPE | 의미 | HB_PING_RESULT | 의미 | |
|---|---|---|---|---|
HB_PTYPE_SERVER (0) | DB 서버 | HB_PING_UNKNOWN (-1) | 아직 탐침 전 | |
HB_PTYPE_COPYLOGDB (1) | 로그 복사기 | HB_PING_SUCCESS (0) | 도달 가능 | |
HB_PTYPE_APPLYLOGDB (2) | 로그 적용기 | HB_PING_USELESS_HOST (1) | 건너뜀 (예: 자기 자신) | |
HB_PTYPE_MAX (3) | 센티넬 | HB_PING_SYS_ERR (2) | 로컬 탐침 오류 | |
HB_PING_FAILURE (3) | 도달 불가 |
1.13 검증 결과와 클러스터 메시지 열거형
섹션 제목: “1.13 검증 결과와 클러스터 메시지 열거형”HB_VALID_RESULT (HB_UI_NODE_ENTRY::v_result에 사용)와 HBP_CLUSTER_MESSAGE (HBP_HEADER::type에 사용):
HB_VALID_RESULT | 의미 | HBP_CLUSTER_MESSAGE | 의미 | |
|---|---|---|---|---|
HB_VALID_NO_ERROR (0) | 유효 | HBP_CLUSTER_HEARTBEAT (0) | 현재 유일한 메시지 타입 | |
HB_VALID_UNIDENTIFIED_NODE (1) | 송신자가 nodes에 없음 | HBP_CLUSTER_MSG_MAX (1) | 센티넬 | |
HB_VALID_GROUP_NAME_MISMATCH (2) | group_id 불일치 | |||
HB_VALID_IP_ADDR_MISMATCH (3) | 호스트 이름/IP 불일치 | |||
HB_VALID_CANNOT_RESOLVE_HOST (4) | DNS 실패 |
1.14 잡 타입 열거형과 점수 매크로
섹션 제목: “1.14 잡 타입 열거형과 점수 매크로”HB_CLUSTER_JOB | 잡 | HB_RESOURCE_JOB | 잡 | |
|---|---|---|---|---|
HB_CJOB_INIT (0) | 일회성 초기화 | HB_RJOB_PROC_START (0) | 프로세스 스폰 | |
HB_CJOB_HEARTBEAT (1) | 박동 송수신 (Ch 4) | HB_RJOB_PROC_DEREG (1) | 프로세스 등록 해제 | |
HB_CJOB_CALC_SCORE (2) | 재계산 및 선출 (Ch 5) | HB_RJOB_CONFIRM_START (2) | 스폰 확인 | |
HB_CJOB_CHECK_PING (3) | ping 검사 (Ch 6) | HB_RJOB_CONFIRM_DEREG (3) | 등록 해제 확인 | |
HB_CJOB_FAILOVER (4) | 승격 (Ch 6) | HB_RJOB_CHANGE_MODE (4) | changemode 푸시 (Ch 8) | |
HB_CJOB_FAILBACK (5) | 마스터 양보 (Ch 7) | HB_RJOB_DEMOTE_START_SHUTDOWN (5) | demote-shutdown 시작 | |
HB_CJOB_CHECK_VALID_PING_SERVER (6) | ping 호스트 재검증 | HB_RJOB_DEMOTE_CONFIRM_SHUTDOWN (6) | demote-shutdown 확인 | |
HB_CJOB_DEMOTE (7) | demote (Ch 8) | HB_RJOB_CLEANUP_ALL (7) | 정리 (Ch 10) | |
HB_CJOB_MAX (8) | 센티넬 | HB_RJOB_CONFIRM_CLEANUP_ALL (8) | 정리 확인 | |
HB_RJOB_MAX (9) | 센티넬 |
선출(Ch 5)에서 HB_NODE_ENTRY::score에 기록되는 점수 매크로:
// score macros -- src/executables/master_heartbeat.h#define HB_REPLICA_PRIORITY 0x7FFF#define HB_NODE_SCORE_MASTER 0x8000 /* read as a NEGATIVE short */#define HB_NODE_SCORE_TO_BE_MASTER 0xF000#define HB_NODE_SCORE_SLAVE 0x0000#define HB_NODE_SCORE_UNKNOWN 0x7FFF0x8000은 의도적으로 음수로 읽힌다. HB_NODE_ENTRY::score는 부호 있는 short이므로: 0x8000 = -32768, 0xF000 = -4096 (둘 다 음수), 반면 SLAVE(0)와 UNKNOWN(0x7FFF = +32767)은 양수다. 선출은 가장 작은 점수를 고르므로, 마스터/to-be-master가 예외 처리 없이 모든 slave보다 앞에 정렬된다. unsigned short로 읽으면 0x8000은 +32768이 되어 마스터가 매번 선출에서 진다. Ch 5는 이 부호 트릭에 의존한다.
1.15 챕터 요약 — 핵심 정리
섹션 제목: “1.15 챕터 요약 — 핵심 정리”- 네 개의 전역 변수가 모든 것의 루트:
hb_Cluster,hb_Resource,cluster_Jobs,resource_Jobs; 나머지 모든 구조체는 이 중 하나에 매달린다. myself/master는nodes안의 커서이지, 독립적인 할당이 아니다 (master는NULL일 수 있다).- 다섯 개의 구조체가 하나의 침투형 리스트 관용구를 공유한다 — 포인터 투 포인터
prev(*(entry->prev) == entry), 연결 해제에 헤드 예외 처리 불필요. HB_PROC_ENTRY는 상태 기계에 재시작/확인/changemode 부기를 더한 것 — timeval 다섯 개, changemode 쌍,LOG_LSAEOF 스냅샷 두 개.HB_JOB_ARG는 큐로 구분되는 union;jobs는expire기준 정렬 상태를 유지한다.HBP_HEADER의 요청 비트는#if로 엔디언 조건부 — 두 호스트 클래스가 동일한 와이어 바이트에 동의한다.- 두 가지 숫자 함정:
HB_PSTATE_REGISTERED == HB_PSTATE_REGISTERED_AND_STANDBY(별칭 5), 그리고HB_NODE_SCORE_MASTER=0x8000은 부호 있는 short로 음수로 읽혀 마스터가 “가장 작은 값 우선” 선출에서 앞서게 된다.
Chapter 2: 초기화와 메모리 관리
섹션 제목: “Chapter 2: 초기화와 메모리 관리”세 개의 heartbeat 전역 변수(hb_Cluster, hb_Resource, 두 개의 HB_JOB 큐)는
단 한 건의 heartbeat 패킷이 네트워크를 타기 전에 어떤 순서로 할당되고 채워지는가?
구조체 레이아웃은 Chapter 1에서 다뤘다. 여기서는 bring-up 코드를 — 모든 분기와
오류 경로까지 — hb_thread_initialize에 도달하는 순간까지 추적한다(스레드 생성은
Chapter 3). 노드, 우선순위, replica, ping witness의 이유는 cubrid-heartbeat.md
(「클러스터 멤버십」, 「Split-brain과 ping witness」)에서 다루며 여기서 재유도하지
않는다.
2.1 진입점: hb_master_init
섹션 제목: “2.1 진입점: hb_master_init”hb_master_init은 공유 goto error_return 블록으로 보호되는 엄격한 5단계
파이프라인으로 heartbeat을 초기화한다.
// hb_master_init -- src/executables/master_heartbeat.chb_enable_er_log ();sysprm_reload_and_init (NULL, NULL); /* <- re-read cubrid.conf so HA params are fresh */error = hb_cluster_initialize (prm_get_string_value (PRM_ID_HA_NODE_LIST), prm_get_string_value (PRM_ID_HA_REPLICA_LIST));if (error != NO_ERROR) goto error_return; /* <- step 1 */error = hb_cluster_job_initialize (); if (...) goto error_return; /* <- step 2 */error = hb_resource_initialize (); if (...) goto error_return; /* <- step 3 */error = hb_resource_job_initialize (); if (...) goto error_return; /* <- step 4 */error = hb_thread_initialize (); if (...) goto error_return; /* <- step 5: Chapter 3 */hb_Deactivate_immediately = false; /* <- arm: deactivation no longer a no-op */return NO_ERROR;순서는 의미를 갖는다. 각 전역 변수는 자신의 잡 큐가 참조하기 전에 반드시 존재해야
한다. NO_ERROR가 아닌 반환값은 모두 정리 단계로 점프한다.
불변 조건 — 부분 초기화 정리는 shutdown == false를 기준으로 한다. 정리 블록은
완료된 전역 변수만 정확히 해제한다:
// hb_master_init (error_return) -- src/executables/master_heartbeat.cerror_return: if (hb_Cluster && hb_Cluster->shutdown == false) hb_cluster_cleanup (); if (cluster_Jobs && cluster_Jobs->shutdown == false) hb_cluster_job_shutdown (); if (hb_Resource && hb_Resource->shutdown == false) hb_resource_cleanup (); if (resource_Jobs && resource_Jobs->shutdown == false) hb_resource_job_shutdown (); return error;각 초기화 함수는 성공 직전에 shutdown = false를 마지막으로 설정하므로, 이 guard는
완료된 전역 변수에 대해서만 참이 된다. 이 불변 조건이 깨지면 3단계 실패 시
워커가 한 번도 생성되지 않은 큐를 해제하거나, 이미 해제된 전역 변수를 double-free할
수 있다.
flowchart TD A["hb_master_init"] --> C["hb_cluster_initialize"] C -->|오류| Z["error_return:\nshutdown==false 정리"] C -->|NO_ERROR| D["hb_cluster_job_initialize"] D -->|오류| Z D -->|NO_ERROR| E["hb_resource_initialize"] E -->|오류| Z E -->|NO_ERROR| F["hb_resource_job_initialize"] F -->|오류| Z F -->|NO_ERROR| G["hb_thread_initialize\n=> Chapter 3"] G -->|오류| Z G -->|NO_ERROR| H["hb_Deactivate_immediately = false"]
2.2 1단계 — hb_cluster_initialize: hb_Cluster 전역 변수
섹션 제목: “2.2 1단계 — hb_cluster_initialize: hb_Cluster 전역 변수”가장 무거운 초기화 함수 — hb_Cluster를 할당하고, 호스트명을 알아내고, 상태를
결정하고, 노드 및 ping-host 목록을 구성하고, UDP 소켓을 연다.
// hb_cluster_initialize -- src/executables/master_heartbeat.cif (hb_Cluster == NULL) { hb_Cluster = (HB_CLUSTER *) malloc (sizeof (HB_CLUSTER)); if (hb_Cluster == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; } /* <- nothing to unlock yet */ pthread_mutex_init (&hb_Cluster->lock, NULL); /* <- mutex init only on first alloc */}if (GETHOSTNAME (host_name, ...)) { ...; return ER_BO_UNABLE_TO_FIND_HOSTNAME; } /* <- pre-lock early return *//* ... pthread_mutex_lock; scalars default; host_name copied ... */if (HA_GET_MODE () == HA_MODE_REPLICA) hb_Cluster->state = HB_NSTATE_REPLICA; /* <- never elects */else hb_Cluster->state = HB_NSTATE_SLAVE; /* <- everyone boots SLAVE */malloc은 멱등적(== NULL 가드)이므로, 재활성화 시 기존 구조체와 mutex를 재사용한다.
두 조기 반환(malloc 실패, GETHOSTNAME)은 pthread_mutex_lock 이전에 발생하므로
어느 쪽도 held lock을 누수하지 않는다. 이후의 모든 오류 경로는 unlock한다. lock
아래에서 스칼라 초기값이 설정된다 — shutdown, hide_to_demote, is_isolated는
false; is_ping_check_enabled는 true; sfd는 INVALID_SOCKET;
master/myself/nodes는 NULL — 그리고 위의 상태 선택이 이루어진다.
불변 조건 — 새로 초기화된 노드는 결코 MASTER가 아니다. master/myself가 NULL인
SLAVE(또는 REPLICA)로 부팅되며, 마스터 지위는 나중에 election 잡으로만 획득된다
(Chapter 5). 이 불변 조건이 깨지면 두 노드가 첫 heartbeat 라운드 전에 MASTER로
부팅되어 split-brain이 발생한다.
hb_Cluster->num_nodes = hb_cluster_load_group_and_node_list (...) 다음에 오며
(§2.6), if (hb_Cluster->num_nodes < 1)은 치명적 — unlock 후 ER_PRM_BAD_VALUE를
반환한다. ping-host 로드는 먼저 ICMP를 시도하고, ICMP 호스트가 없을 때만 TCP로
폴백한다:
// hb_cluster_initialize -- src/executables/master_heartbeat.chb_Cluster->num_ping_hosts = hb_cluster_load_ping_host_list (prm_get_string_value (PRM_ID_HA_PING_HOSTS));if (hb_cluster_check_valid_ping_server () == false) { pthread_mutex_unlock (...); return ER_FAILED; }if (hb_Cluster->num_ping_hosts == 0) { /* <- no ICMP hosts -> try TCP */ hb_Cluster->num_ping_hosts = hb_cluster_load_tcp_ping_host_list (prm_get_string_value (PRM_ID_HA_TCP_PING_HOSTS)); hb_Cluster->ping_timeout = prm_get_integer_value (PRM_ID_HA_PING_TIMEOUT) * 1000; /* secs -> msecs */ if (hb_cluster_check_valid_ping_server () == false) { pthread_mutex_unlock (...); return ER_FAILED; }}/* ... sfd = socket(AF_INET, SOCK_DGRAM, 0); on <0 unlock, return ERR_CSS_TCP_DATAGRAM_SOCKET ... *//* ... bind to INADDR_ANY:ha_port_id; on <0 unlock, return ERR_CSS_TCP_DATAGRAM_BIND ... */pthread_mutex_unlock (&hb_Cluster->lock); return NO_ERROR; /* <- success: single tail unlock */hb_cluster_check_valid_ping_server는 ping 호스트가 하나도 설정되지 않은 경우
true를 반환한다(witness 없는 구성은 유효); 호스트가 설정되어 있지만 아무도
응답하지 않을 때만 실패한다(Chapter 6). 소켓 열기는 두 개의 실패 분기(socket() < 0,
bind() < 0)를 가지며, 둘 다 unlock하고 성공 시 tail unlock 하나로 끝난다. 이
소켓 위의 wire 프로토콜은 Chapter 4에서 다룬다.
2.3 2단계 — hb_cluster_job_initialize
섹션 제목: “2.3 2단계 — hb_cluster_job_initialize”§2.2와 동일한 == NULL malloc + pthread_mutex_init 멱등 패턴으로 할당하고, lock
아래에서 큐 헤드를 초기화하고, job_funcs를 정적 dispatch 배열에 연결하고, 첫
번째 잡을 큐에 넣는다:
// hb_cluster_job_initialize -- src/executables/master_heartbeat.c/* ... idempotent malloc cluster_Jobs + pthread_mutex_init on first alloc; lock ... */cluster_Jobs->shutdown = false; cluster_Jobs->num_jobs = 0; cluster_Jobs->jobs = NULL;cluster_Jobs->job_funcs = &hb_cluster_jobs[0]; /* <- index -> function dispatch table; unlock */error = hb_cluster_job_queue (HB_CJOB_INIT, NULL, HB_JOB_TIMER_IMMEDIATELY);if (error != NO_ERROR) { assert (false); return ER_FAILED; } /* <- queue failure is a logic bug */return NO_ERROR;job_funcs는 잡 타입 enum을 핸들러에 매핑하는 파일 스코프 dispatch 테이블에
바인딩된다 — hb_cluster_jobs[]는 인덱스 0에 hb_cluster_job_init
(HB_CJOB_INIT)을 두고, 이어서 _heartbeat, _calc_score, _check_ping,
_failover, _failback, _check_valid_ping_server, _demote, 그리고 NULL
sentinel이 온다. 시드 잡 HB_CJOB_INIT(enum 0)은 HB_JOB_TIMER_IMMEDIATELY로
큐에 들어가지만 워커가 존재할 때까지 휴면 상태다(Chapter 3).
2.4 3·4단계 — resource 쪽
섹션 제목: “2.4 3·4단계 — resource 쪽”hb_resource_initialize는 §2.3의 malloc-and-reset 패턴을 그대로 따르며,
hb_Resource->state = HB_NSTATE_SLAVE, procs = NULL, num_procs = 0으로 부팅한다
(아직 감시 대상 프로세스 없음). hb_resource_job_initialize는 §2.3의 큐 초기화와
동일한 구조이되 &hb_resource_jobs[0]을 바인딩하고, 시드 잡에 중요한 차이가 있다:
// hb_resource_job_initialize -- src/executables/master_heartbeat.cerror = hb_resource_job_queue (HB_RJOB_CHANGE_MODE, NULL, prm_get_integer_value (PRM_ID_HA_INIT_TIMER_IN_MSECS) + prm_get_integer_value (PRM_ID_HA_FAILOVER_WAIT_TIME_IN_MSECS));if (error != NO_ERROR) { assert (false); return ER_FAILED; }시드 잡은 HB_RJOB_CHANGE_MODE(enum 4, hb_resource_jobs[4] 인덱싱)이며
타이머는 즉시가 아니다 — ha_init_timer_in_msecs + ha_failover_wait_time_in_msecs
이후에 발화하여, resource 쪽이 로컬 프로세스 트리에 역할을 부여하기 전에 클러스터가
리더를 먼저 확정할 시간을 준다.
2.5 침입형 리스트 기본 연산과 엔트리별 할당자
섹션 제목: “2.5 침입형 리스트 기본 연산과 엔트리별 할당자”모든 동적 컬렉션(노드, ping 호스트, 감시 프로세스)은 동일한 침입형(intrusive)
이중 연결 리스트다. 각 엔트리는 HB_LIST 헤더를 첫 번째 멤버로 내장하며, prev는
포인터-투-포인터 방식이어서 삽입과 unlink가 헤드에 관계없이 동일하게 동작한다:
// struct hb_list -- src/executables/master_heartbeat.hstruct hb_list { HB_LIST *next; HB_LIST **prev; }; /* prev points at the slot that points to us */아래 세 기본 연산은 분기가 빠짐없이 나와 있다. hb_list_add는 헤드에 push하며
(if (n->next) guard로 기존 헤드를 역으로 연결); hb_list_remove는 if (n->prev)
/ 내부 if (*(n->prev))로 splice; hb_list_move는 소스가 비었을 때 if (*dest_pp)
를 건너뛰면서 헤드 전체를 옮긴다.
// hb_list_add -- src/executables/master_heartbeat.cn->next = *(p);if (n->next) n->next->prev = &(n->next); /* <- old head back-points into new node */n->prev = p; *(p) = n; /* <- new node back-points at head slot */
// hb_list_remove -- src/executables/master_heartbeat.cif (n->prev) { *(n->prev) = n->next; /* <- slot that pointed to n now points past it */ if (*(n->prev)) n->next->prev = n->prev; }n->next = NULL; n->prev = NULL; /* <- detached node has no dangling links */
// hb_list_move -- src/executables/master_heartbeat.c*dest_pp = *source_pp;if (*dest_pp) (*dest_pp)->prev = dest_pp; /* <- re-home back-ptr; skipped if source empty */*source_pp = NULL;불변 조건 — prev는 항상 해당 엔트리를 가리키는 포인터를 가리킨다. 첫 번째
엔트리의 경우 그 슬롯은 리스트 헤드(&hb_Cluster->nodes)이고, 중간 엔트리의 경우
&(predecessor->next)이다. 이 덕분에 hb_list_remove는 헤드를 특수처리하지 않고
splice할 수 있다. 이 불변 조건이 깨지면 첫 번째 노드를 제거했을 때
hb_Cluster->nodes가 해제된 메모리를 가리키게 된다.
엔트리별 할당자는 malloc, 기본값 채우기, 적절한 헤드에 hb_list_add를 수행한다.
hb_alloc_new_proc은 proc 리스트용 할당자다:
// hb_alloc_new_proc -- src/executables/master_heartbeat.cp = (HB_PROC_ENTRY *) malloc (sizeof (HB_PROC_ENTRY));if (p) { memset (p, 0, sizeof (HB_PROC_ENTRY)); /* <- zero first, then override non-zero defaults */ p->state = HB_PSTATE_UNKNOWN; LSA_SET_NULL (&p->prev_eof); LSA_SET_NULL (&p->curr_eof); /* <- log addresses start null */ hb_list_add ((HB_LIST **) &hb_Resource->procs, (HB_LIST *) p);}return (p);hb_alloc_new_proc은 bring-up 중에 호출되지 않는다 — proc 리스트는 빈 상태로
시작하며(§2.4), 엔트리는 관리 대상 프로세스가 등록할 때만 나타난다(Chapter 9).
할당자 관용구를 공유하기 때문에 여기서 보인다.
flowchart LR
subgraph Cluster["hb_Cluster"]
N0["nodes (head slot)"] --> N1["HB_NODE_ENTRY"] --> N2["HB_NODE_ENTRY"]
P0["ping_hosts (head slot)"] --> P1["HB_PING_HOST_ENTRY"]
end
R0["hb_Resource->procs (head slot)"] --> R1["HB_PROC_ENTRY"]
2.6 노드 테이블 구성: hb_cluster_load_group_and_node_list와 hb_add_node_to_cluster
섹션 제목: “2.6 노드 테이블 구성: hb_cluster_load_group_and_node_list와 hb_add_node_to_cluster”ha_node_list는 group_id@host1:host2:host3 형식이다. 로더는 먼저 @로 토큰화하여
group id를 분리하고, 이어서 ,:로 호스트를 분리하며, priority 카운터 하나를
토큰 인덱스이자 할당 우선순위 양쪽으로 활용한다.
// hb_cluster_load_group_and_node_list -- src/executables/master_heartbeat.cif (ha_node_list == NULL || ha_node_list[0] == '\0') { ...; goto error; } /* <- empty list fatal */for (priority = 0, p = strtok_r (tmp_string, "@", &savep); p; priority++, p = strtok_r (NULL, " ,:", &savep)) { if (priority == 0) strncpy_bufsize (hb_Cluster->group_id, p); /* <- token 0 is group id */ else { node = hb_add_node_to_cluster (p, (priority)); /* <- priority = 1, 2, 3, ... by order */ if (node && are_hostnames_equal (node->host_name, hb_Cluster->host_name)) { if (hb_Cluster->state == HB_NSTATE_REPLICA) { ...; goto error; } /* <- replica must NOT be here */ else hb_Cluster->myself = node; /* <- found myself */ } }}if (hb_Cluster->state != HB_NSTATE_REPLICA && hb_Cluster->myself == NULL) { ...; goto error; }num_nodes = priority;두 노드가 결코 같은 우선순위를 가질 수 없는 이유가 여기 있다. 우선순위는 설정 파일 순서대로 엄격하게 배정된다(첫 번째 호스트 1, 두 번째 2, 세 번째 3); 카운터는 단조 증가하므로 설정 위치와 우선순위는 전단사(bijection)가 된다. election(Chapter 5)은 이에 의존한다 — 동점이 불가능하므로 우선순위가 가장 낮은 생존자는 항상 유일하다.
hb_add_node_to_cluster는 NULL 토큰을 가드하고, 엔트리를 malloc하고, localhost
재작성을 적용하고, 기본값을 설정한다:
// hb_add_node_to_cluster -- src/executables/master_heartbeat.cif (host_name == NULL) return NULL; /* <- NULL token yields NULL entry */p = (HB_NODE_ENTRY *) malloc (sizeof (HB_NODE_ENTRY));if (p) { /* localhost -> hb_Cluster->host_name via are_hostnames_equal, else strncpy host_name as-is */ p->priority = priority; p->state = HB_NSTATE_UNKNOWN; /* <- peers UNKNOWN until first heartbeat */ /* ... score, heartbeat_gap, last_recv_hbtime, next/prev each set to 0 individually ... */ hb_list_add ((HB_LIST **) &hb_Cluster->nodes, (HB_LIST *) p);}return (p);host_name이 NULL이면 NULL을 반환하고, 호출 측의 if (node)가 이를 건너뛴다(malloc
실패 시도 마찬가지). hb_alloc_new_proc(§2.5)과 달리 이 할당자는 memset을 하지
않으며, 각 필드를 개별적으로 할당한다.
replica 루프 이전에 ha_replica_list가 tmp_string에 복사되거나(파라미터가 NULL이면
tmp_string을 비운다); 노드가 REPLICA 상태인데 replica 리스트가 비어 있으면 치명적
— if (hb_Cluster->state == HB_NSTATE_REPLICA && tmp_string[0] == '\0') goto error.
replica 패스는 그 이후 ha_replica_list를 노드 패스와 동일한 구조로 처리하되
두 가지 추가 검사가 있다: priority == 0 토큰이 hb_Cluster->group_id와 같아야
하며 — if (strcmp (hb_Cluster->group_id, p) != 0) goto error — 각 호스트는
hb_add_node_to_cluster (p, HB_REPLICA_PRIORITY)로 추가된다. 센티널
HB_REPLICA_PRIORITY(0x7FFF)는 일반 노드보다 훨씬 높은 값이므로 replica는 절대
election에서 이기지 못한다. 역할 유효성 검사 매트릭스(빈 목록 가드 및 myself 배치
goto error 분기들):
hb_Cluster->state | Replica 목록 비어있음 | 노드 목록에 포함 | Replica 목록에 포함 |
|---|---|---|---|
| SLAVE / 잠재적 MASTER | 허용 | myself 포함 필수; 아니면 goto error | 포함되면 안 됨; 아니면 goto error |
| REPLICA | 치명적 goto error | 포함되면 안 됨; 아니면 goto error | myself 포함 필수; 아니면 goto error |
성공 시 num_nodes + priority(노드 수 + replica 수)를 반환한다. goto error는
ER_PRM_BAD_VALUE를 설정하고 ER_FAILED(음수)를 반환하며, §2.2의 num_nodes < 1
가드에서 잡힌다.
상한 제한. 헤더는 HB_MAX_NUM_NODES = 8을 정의한다. 루프는 이를 강제하지 않지만
(제한은 들어오는 heartbeat에 노드를 매칭할 때 검사), 하위 고정 크기 버퍼는 최대 8개
노드를 가정하므로 이 상한을 확장하려면 하위 코드 전체를 검토해야 한다.
2.7 ping-host 목록 구성: ICMP와 TCP 로더
섹션 제목: “2.7 ping-host 목록 구성: ICMP와 TCP 로더”hb_cluster_load_ping_host_list는 ha_ping_hosts(공백/쉼표/콜론 구분)를 파싱하고
hb_add_ping_host로 각 호스트를 추가한다. 분기: if (ha_ping_host_list == NULL) return 0(파라미터 미설정은 유효 — 호스트 0개); 토큰별로 if (host_p == NULL) break
로 루프 종료; if (strcmp (host_p, "0.0.0.0") == 0)이면 로그 후 건너뜀(미계산);
그 외 hb_add_ping_host (host_p); num_hosts++.
hb_add_ping_host는 hb_add_node_to_cluster(§2.6)와 동일한 두 분기 — if (host_name == NULL) return NULL 가드, 그 다음 if (p) malloc 성공 본문 — 를 가지며,
저장하는 기본값만 다르다: p->port = -1(ICMP는 포트 없음)과 p->ping_result = HB_PING_UNKNOWN, 이후 hb_Cluster->ping_hosts에 hb_list_add.
hb_cluster_load_tcp_ping_host_list는 ICMP 호스트가 0개일 때만 도달한다(§2.2).
,로 토큰화하고 각 토큰을 :에서 호스트와 포트로 분리하며(hb_port_str_to_num으로
검증), 0.0.0.0 제외 규칙을 동일하게 적용한다. 이 함수의 특징적인 분기는 잘못된
입력에서의 break들이다:
// hb_cluster_load_tcp_ping_host_list -- src/executables/master_heartbeat.cport_p = strstr (host_p, ":");if (port_p == NULL || port_p == host_p) break; /* <- no ':' or empty host -> stop parsing */else { *port_p = '\0'; port_p++; port = hb_port_str_to_num (port_p); if (port == -1) break; } /* <- bad port -> stop parsing */잘못된 host:port 형식은 파싱 전체를 break하므로, 그 이후의 내용은 조용히
무시된다. hb_add_tcp_ping_host는 hb_add_ping_host와 동일한 두 분기를 가지며,
정확히 한 줄만 다르다: -1 대신 p->port = port를 저장한다. 둘 다 동일한
hb_Cluster->ping_hosts 헤드에 push하지만, TCP 폴백은 ICMP가 0개를 반환했을 때만
실행되므로 ICMP 엔트리와 TCP 엔트리가 혼재하는 일은 없다.
2.8 챕터 요약 — 핵심 정리
섹션 제목: “2.8 챕터 요약 — 핵심 정리”hb_master_init은 엄격한 5단계 파이프라인이다 (cluster, cluster jobs, resource, resource jobs, threads). 공유error_returnfunnel은shutdown필드가false인 전역 변수만 해제한다.hb_cluster_initialize가 핵심 작업을 수행한다hb_Cluster->lock아래에서: 멱등 malloc,GETHOSTNAME,HA_GET_MODE에서 SLAVE/REPLICA 결정, 노드/ping-host 로딩,ha_port_id에INADDR_ANYUDP 소켓 바인딩. lock 이후의 모든 오류 경로는 unlock하며, lock 이전의 두 조기 반환은 unlock하지 않는다.- 노드는 항상 non-MASTER로 부팅된다 (
master/myselfNULL). 마스터 지위는 나중에 election 잡으로만 획득되므로 부팅 시 split-brain을 막는다. - 각 잡 큐는
job_funcs를 정적 배열에 바인딩하고 시드 잡 하나를 넣는다 — clusterHB_CJOB_INIT은 즉시, resourceHB_RJOB_CHANGE_MODE는init + failover_wait지연으로 — 워커가 존재하기 전에는 어느 쪽도 실행되지 않는다. - 세 컬렉션 모두 포인터-투-포인터
prev를 가진 침입형 이중 연결 리스트 하나다; 기본 연산은 헤드에 무관하며, 할당자는 모두 NULL 가드 → malloc → 기본값 채우기 →hb_list_add순서를 따른다. - 우선순위는 설정 파일 순서대로 1, 2, 3으로 배정된다 (루프의 단조 카운터) —
동점이 불가능한 이유. REPLICA 모드 노드에서
ha_replica_list가 비어 있으면 치명적; replica는0x7FFF센티널을 받아ha_replica_list에만 나타나며(group id는ha_node_list와 일치해야 함),HB_MAX_NUM_NODES = 8이 테이블 상한을 제한한다. - ping witness는 ICMP를 우선하며, ICMP 호스트가 0개일 때만 TCP로 폴백한다;
두 로더 모두
0.0.0.0을 제외하고 잘못된 TCPhost:port에서break한다.
Chapter 3: 잡 큐와 워커 스레드
섹션 제목: “Chapter 3: 잡 큐와 워커 스레드”1장에서 구조체를 정의하고, 2장에서 할당과 초기화를 마쳤다. 다음 질문은 자연스럽게 이어진다: FSM을 실제로 구동하는 기계 장치는 무엇인가? 답은 하나의 범용 타이머-정렬 잡 큐(HB_JOB)이며, 이것을 두 인스턴스(cluster_Jobs, resource_Jobs)로 생성하고, 분리된(detached) pthread 4개가 비워낸다. 5~10장에서 다루는 모든 동작은 만료 시각과 함께 이 큐 중 하나에 등록된 함수 포인터다. cluster/resource 분리의 의미와 FSM 이론은 동반 문서인 cubrid-heartbeat.md(“Two planes”, “The job model”)를 참조하라. 이 장은 큐 메커니즘과 스레드 본체를 해부하며, 그 이론을 다루지 않는다.
3.1 두 큐 인스턴스와 디스패치 테이블
섹션 제목: “3.1 두 큐 인스턴스와 디스패치 테이블”HB_JOB(전체 필드는 1장 참조)은 mutex, 만료 시각 순으로 정렬된 침입형(intrusive) 엔트리 리스트, 디스패치 테이블 포인터, shutdown 플래그를 하나로 묶는다. 파일 스코프의 인스턴스가 두 개 존재한다 — cluster_Jobs와 resource_Jobs. 이 둘은 NULL로 정의되어 있다가 hb_*_initialize(2장)가 할당하면 초기화된다. 각 인스턴스의 job_funcs는 두 enum 인덱스 디스패치 테이블 중 하나를 가리킨다. 큐에 등록되는 잡은 type(unsigned int)만 보유하며, 이 값이 enqueue 시점에 테이블에서 함수 포인터로 변환된다.
HB_JOB 필드 | 역할 | 이유 |
|---|---|---|
lock | 큐 전용 mutex | 리스트 변경 전체를 직렬화한다. 데이터 락(3.9)과 분리되며 중첩(nesting)되지 않는다. |
num_jobs | unsigned short | 잔재 필드 — hb_*_job_initialize에서 0으로 초기화된 뒤 한 번도 변경되지 않는다. enqueue/dequeue 어디서도 갱신하지 않으므로 실시간 깊이(depth)로 읽으면 안 된다. |
jobs | 정렬된 HB_JOB_ENTRY 리스트의 헤드 | 만료 오름차순 큐 본체. hb_job_dequeue는 이 헤드만 들여다본다. |
job_funcs | 디스패치 테이블 포인터 | 인스턴스를 hb_cluster_jobs[] 또는 hb_resource_jobs[]에 바인딩하고, type을 핸들러로 해석한다. |
shutdown | latch 플래그 | hb_job_shutdown이 설정한다. true가 되면 dequeue/reorder가 no-op이 되고, 대응하는 워커 루프가 종료된다. |
큐에 담기는 잡 하나는 HB_JOB_ENTRY 한 개다:
HB_JOB_ENTRY 필드 | 역할 | 이유 |
|---|---|---|
next | 순방향 링크 | 단방향으로 순회. 삽입과 dequeue 모두 이 링크를 따라 이동한다. |
prev | HB_JOB_ENTRY **, 이전 노드의 next 슬롯 주소 | 뒤로 순회하지 않고 O(1) splice-out을 가능하게 한다(*(n->prev) = n->next). 역방향 순회는 없지만 “이중 연결” 리스트라 부르는 이유다. |
type | unsigned int, enum 서수 | enqueue 시 디스패치 테이블 인덱스이자, reorder 시 일치 키로 사용된다. |
expire | 절대값 struct timeval 기한 | 정렬 키. enqueue 시 now + msec으로 한 번 계산된다. |
func | 캐시된 HB_JOB_FUNC | enqueue 시 미리 해석(job_funcs[type])되어, 디스패치는 단순 함수 호출이 된다. 재조회 없음. |
arg | HB_JOB_ARG * 페이로드 | 핸들러 소유. 워커가 엔트리를 해제하고, 핸들러가 arg를 해제한다. |
INVARIANT —
num_jobs는 실시간 카운트가 아니다. 초기화 시 0으로 세팅되고 이후 어디서도 증가 또는 감소하지 않는다.num_jobs를 보고 “큐가 비어 있는가/가득 찼는가”를 판단하는 코드는 항상0을 읽는다. 비어 있는지 여부는num_jobs가 아니라jobs->jobs == NULL로 확인해야 한다.
// hb_cluster_jobs[] -- src/executables/master_heartbeat.cstatic HB_JOB_FUNC hb_cluster_jobs[] = { hb_cluster_job_init, /* HB_CJOB_INIT = 0 */ hb_cluster_job_heartbeat, /* = 1 */ hb_cluster_job_calc_score, /* = 2 */ hb_cluster_job_check_ping, /* = 3 */ hb_cluster_job_failover, /* = 4 */ hb_cluster_job_failback, /* = 5 */ hb_cluster_job_check_valid_ping_server, /* = 6 */ hb_cluster_job_demote, /* = 7 */ NULL /* sentinel at index HB_CJOB_MAX */};hb_resource_jobs[]도 구조는 동일하다: HB_RJOB_* 순서대로 핸들러 9개(hb_resource_job_proc_start … …_confirm_cleanup_all)를 나열하고, HB_RJOB_MAX 위치에 NULL을 둔다. type 값이 곧 배열 인덱스이므로(new_job->func = jobs->job_funcs[job_type]), 순서는 반드시 enum 서수와 일치해야 한다.
INVARIANT — 테이블 순서는 enum 순서와 동일해야 하며, 컴파일 타임 검증이 없다.
ARRAY_SIZE(hb_cluster_jobs) == HB_CJOB_MAX + 1을 보장하는 static_assert가 존재하지 않는다. 유일한 방어선은 런타임의>= HB_*_MAX래퍼 검사(3.6)와NULL센티넬뿐이다. 새HB_CJOB_*를 enum 중간에 추가하면서 같은 슬롯에 함수 포인터를 추가하지 않으면, 그 이후의 모든 잡이 조용히 잘못된 핸들러를 실행한다 — 크래시 없음. enum과 테이블은 반드시 같은 diff에서 함께 수정해야 한다. 센티넬은job_type == HB_*_MAX케이스만 잡을 뿐, 불일치는 잡지 못한다.
flowchart LR J["HB_JOB 인스턴스\n(cluster_Jobs / resource_Jobs)"] -->|job_funcs| T["디스패치 테이블\nhb_cluster_jobs[] / hb_resource_jobs[]"] J -->|jobs| L["만료 오름차순 이중 연결\nHB_JOB_ENTRY 리스트"]
Figure 3-1. 각 HB_JOB 인스턴스는 정렬된 엔트리 리스트를 디스패치 테이블에 바인딩한다. cluster_Jobs와 resource_Jobs는 구조가 동일하지만 완전히 독립적이다 — 별도의 리스트, 테이블, mutex.
3.2 hb_job_queue — 만료 시각 순 삽입
섹션 제목: “3.2 hb_job_queue — 만료 시각 순 삽입”유일한 enqueue 원시 함수: 메모리를 할당하고, 절대 기한을 계산하고, 정렬 위치를 찾아 삽입한다.
// hb_job_queue -- src/executables/master_heartbeat.c (condensed) new_job = malloc (sizeof (HB_JOB_ENTRY)); if (new_job == NULL) return ER_OUT_OF_VIRTUAL_MEMORY; /* <- 분기: malloc 실패 -> ER */ gettimeofday (&now, NULL); hb_add_timeval (&now, msec); /* now := 현재 시각 + msec == 기한 */ new_job->type = job_type; new_job->func = jobs->job_funcs[job_type]; /* <- enum->함수포인터 변환은 여기서 */ new_job->arg = arg; memcpy (&new_job->expire, &now, sizeof (struct timeval)); pthread_mutex_lock (&jobs->lock); for (job = &(jobs->jobs); *job; job = &((*job)->next)) { if (hb_compare_timeval (&((*job)->expire), &now) <= 0) continue; /* <- 더 일찍 만료: 계속 스캔 */ break; /* <- 첫 번째로 늦은 엔트리: 그 앞에 삽입 */ } hb_list_add ((HB_LIST **) job, (HB_LIST *) new_job); /* 루프 소진 -> 꼬리에 추가 */ pthread_mutex_unlock (&jobs->lock); return NO_ERROR;hb_add_timeval은 tv_usec 올림(carry)을 1e6 이상으로 정규화하지 않는다. 여기서 사용하는 작은 msec 값 범위에서는 실제로 1e6을 넘지 않으므로 gettimeofday 결과와의 비교가 어긋나지 않는다(관찰 사항이며 보장 사항은 아님). prev/next는 hb_list_add가 위의 엔트리 필드 표에 따라 설정한다.
INVARIANT —
jobs->jobs는 항상expire오름차순으로 정렬된다. 삽입 경로는hb_job_queue와hb_job_set_expire_and_reorder두 가지뿐이며, 둘 다 올바른 슬롯을 찾아 삽입하므로hb_job_dequeue는 헤드만 검사하면 된다. 순서가 깨지면, 실행 시각이 한참 뒤인 헤드가 그 뒤에 쌓인 준비된 잡들을 모두 막는다. 정렬 자체가 스케줄러다.
3.3 hb_job_dequeue — 헤드가 만료된 경우에만 반환
섹션 제목: “3.3 hb_job_dequeue — 헤드가 만료된 경우에만 반환”워커가 tight loop에서 호출하며, 기한이 지난 경우에만 헤드를 반환하고 그 외에는 NULL을 반환한다.
// hb_job_dequeue -- src/executables/master_heartbeat.c (condensed) gettimeofday (&now, NULL); pthread_mutex_lock (&jobs->lock); if (jobs->shutdown == true) { unlock; return NULL; } /* <- 분기 1: 종료 중 */ job = jobs->jobs; if (job == NULL) { unlock; return NULL; } /* <- 분기 2: 빈 큐 */ if (hb_compare_timeval (&now, &job->expire) >= 0) hb_list_remove ((HB_LIST *) job); /* <- 분기 3a: now >= expire -> 헤드 분리 */ else { unlock; return NULL; } /* <- 분기 3b: 헤드 아직 미준비 */ pthread_mutex_unlock (&jobs->lock); return job; /* 호출자가 소유, free_and_init 필수 */리스트가 정렬되어 있으므로(3.2) 헤드만 검사하면 충분하다. hb_list_remove는 prev 관용구(3.1 표)로 O(1) 분리한다. NULL이 아닌 엔트리를 받은 워커는 소유권을 가지며 반드시 free_and_init을 호출해야 한다. hb_job_dequeue는 분리만 하고 해제하지 않는다.
3.4 hb_job_set_expire_and_reorder — 즉시 실행 끌어올리기 원시 함수
섹션 제목: “3.4 hb_job_set_expire_and_reorder — 즉시 실행 끌어올리기 원시 함수”이미 큐에 있는 잡을 재스케줄하는 유일한 방법이다. 실제 사용처(5장)는 주기적인 HB_CJOB_CALC_SCORE 잡을 큐 맨 앞으로 끌어당겨서, 하트비트가 클러스터 상황을 바꾼 직후 즉시 재계산하도록 만드는 것이다.
// hb_job_set_expire_and_reorder -- src/executables/master_heartbeat.c (condensed) gettimeofday (&now, NULL); hb_add_timeval (&now, msec); /* msec은 보통 HB_JOB_TIMER_IMMEDIATELY (0) */ pthread_mutex_lock (&jobs->lock); if (jobs->shutdown == true) { unlock; return; } /* <- 분기 1: 종료 중 -> no-op */ for (job = &(jobs->jobs); *job; job = &((*job)->next)) if ((*job)->type == job_type) { target_job = *job; break; } /* 해당 type의 첫 번째 엔트리 */ if (target_job == NULL) { unlock; return; } /* <- 분기 2: 큐에 없음 -> no-op */ memcpy (&target_job->expire, &now, sizeof (struct timeval)); hb_list_remove ((HB_LIST *) target_job); /* 분리 후 정렬 슬롯 재탐색 */ for (job = &(jobs->jobs); *job; job = &((*job)->next)) /* (3.2와 동일한 삽입 루프) */ if (hb_compare_timeval (&((*job)->expire), &(target_job->expire)) > 0) break; hb_list_add ((HB_LIST **) job, (HB_LIST *) target_job); /* 재삽입. 정렬 불변식 유지 */ pthread_mutex_unlock (&jobs->lock);대상이 없을 때의 no-op은 정상 동작이다 — 핸들러가 이미 잡을 재등록한 상태에서 reorder가 경쟁할 수 있기 때문이다. 제자리 수정이 아닌 분리-후-재삽입 방식으로 3.2의 정렬 불변식을 유지한다. type이 일치하는 첫 번째 엔트리만 처리해도 충분한 이유는, FSM이 각 주기적 type의 인스턴스를 최대 하나만 살아 있도록 관리하기 때문이다.
3.5 hb_job_shutdown — 드레인 후 latch
섹션 제목: “3.5 hb_job_shutdown — 드레인 후 latch”// hb_job_shutdown -- src/executables/master_heartbeat.c (condensed; jobs->lock 획득 상태) for (job = jobs->jobs; job; job = job_next) { job_next = job->next; hb_list_remove (job); free_and_init (job); } /* <- next를 먼저 저장 */ jobs->shutdown = true; /* <- latch: 이후 dequeue/reorder는 no-op */latch가 설정되면 hb_job_dequeue와 hb_job_set_expire_and_reorder가 즉시 반환하고, 큐 드레인 워커 두 개는 루프 조건을 통과하지 못한다(cluster_Jobs->shutdown == false, resource_Jobs->shutdown == false). 범위에 주목하라: 이 latch는 4개 스레드 중 2개만 멈춘다. 리더 스레드는 별도 플래그인 hb_Cluster->shutdown을, 디스크 장애 스레드는 hb_Resource->shutdown을 종료 조건으로 사용한다 — 이 둘은 hb_job_shutdown이 건드리지 않으며, 종료 절차(10장)의 다른 단계에서 설정된다.
3.6 타입 래퍼 함수와 HB_*_MAX 범위 검사
섹션 제목: “3.6 타입 래퍼 함수와 HB_*_MAX 범위 검사”호출자는 hb_job_*를 직접 호출하지 않는다. 올바른 HB_JOB 인스턴스를 바인딩하고 타입 범위를 검사하는 6개의 단순 래퍼로 접근한다. hb_cluster_job_queue가 그 템플릿이다:
// hb_cluster_job_queue -- src/executables/master_heartbeat.c (condensed) if (job_type >= HB_CJOB_MAX) return ER_FAILED; /* <- 범위 초과에 대한 유일한 방어선 */ return hb_job_queue (cluster_Jobs, job_type, arg, msec);hb_cluster_job_dequeue는 단순히 hb_job_dequeue (cluster_Jobs)이다(타입을 받지 않으므로 범위 검사 없음). hb_cluster_job_set_expire_and_reorder도 동일한 >= HB_CJOB_MAX 검사 후 위임한다. 리소스 3종(hb_resource_job_queue / _dequeue / _set_expire_and_reorder)은 resource_Jobs와 HB_RJOB_MAX를 쓰는 것만 다르고 구조가 동일하다. 검사가 >=이므로 유효한 최대 인덱스는 HB_*_MAX - 1이며, HB_*_MAX 위치의 NULL 센티넬은 정상 경로에서 도달할 수 없고 방어적 꼬리 표시자일 뿐이다.
3.7 hb_cluster_job_init — 부트스트랩 잡
섹션 제목: “3.7 hb_cluster_job_init — 부트스트랩 잡”클러스터 쪽에서 가장 먼저 dequeue되는 것은 HB_CJOB_INIT으로, hb_cluster_job_initialize(2장)가 등록한다. 이 핸들러는 세 가지 주기적 클러스터 잡을 초기 등록한다:
// hb_cluster_job_init -- src/executables/master_heartbeat.c (condensed; each queue call assert'd NO_ERROR) hb_cluster_job_queue (HB_CJOB_HEARTBEAT, NULL, HB_JOB_TIMER_IMMEDIATELY); hb_cluster_job_queue (HB_CJOB_CHECK_VALID_PING_SERVER, NULL, HB_JOB_TIMER_IMMEDIATELY); hb_cluster_job_queue (HB_CJOB_CALC_SCORE, NULL, prm_get_integer_value (PRM_ID_HA_INIT_TIMER_IN_MSECS)); if (arg) free_and_init (arg); /* <- INIT은 NULL arg로 등록되므로 방어적 처리 */HB_JOB_TIMER_IMMEDIATELY는 (0)이므로, heartbeat와 ping 서버 유효성 검사는 다음 워커 패스에서 즉시 실행된다. CALC_SCORE는 PRM_ID_HA_INIT_TIMER_IN_MSECS만큼 지연되어, 첫 번째 점수 계산이 몇 번의 하트비트 후에 이루어진다. 각 핸들러는 스스로를 재등록하거나 3.4로 재활성화된다(4장·5장). 정형 패턴: 잡의 마지막 동작은 자기 자신을 재등록하는 것이다.
3.8 4개의 스레드와 hb_thread_initialize
섹션 제목: “3.8 4개의 스레드와 hb_thread_initialize”hb_thread_initialize는 4개의 스레드를 생성한다. 모두 분리(detached) 상태(PTHREAD_CREATE_DETACHED, system scope)이며, pthread_attr_t 하나를 공유한다. join 핸들은 보관하지 않는다.
// hb_thread_initialize -- src/executables/master_heartbeat.c (condensed; each step returns its own ER_CSS_* on failure)pthread_attr_setdetachstate (&thread_attr, PTHREAD_CREATE_DETACHED); /* detached: join 없음 */pthread_attr_setscope (&thread_attr, PTHREAD_SCOPE_SYSTEM); /* + 스택이 작으면 증가 */pthread_create (&cluster_worker_th, &thread_attr, hb_thread_cluster_reader, NULL);pthread_create (&cluster_worker_th, &thread_attr, hb_thread_cluster_worker, NULL); /* <- 동일 로컬 재사용 */pthread_create (&resource_worker_th, &thread_attr, hb_thread_resource_worker, NULL);pthread_create (&check_disk_failure_th, &thread_attr, hb_thread_check_disk_failure, NULL);return NO_ERROR;무해한 quirk: reader와 cluster-worker 모두 **같은 로컬 변수 cluster_worker_th**에 생성된다(TID 로컬 3개 중 하나). 분리 상태이고 이후 참조하지 않으므로 덮어쓰기는 무관하다. pthread_create 실패 시 즉시 반환하며, 이미 생성된 스레드는 계속 실행된다.
flowchart TD HTI["hb_thread_initialize"] --> R["hb_thread_cluster_reader\nhb_Cluster->sfd에서 recvfrom"] HTI --> CW["hb_thread_cluster_worker\ncluster_Jobs 드레인"] HTI --> RW["hb_thread_resource_worker\nresource_Jobs 드레인"] HTI --> DF["hb_thread_check_disk_failure\n100ms 틱, 디스크 장애 시 demote"] R -->|hb_cluster_receive_heartbeat\nCJOB 등록 가능| CQ["cluster_Jobs"] CW -->|dequeue + 디스패치| CQ RW -->|dequeue + 디스패치| RQ["resource_Jobs"]
Figure 3-2. 분리된 스레드 4개. 리더는 수신 패킷을 처리하며 클러스터 큐에 간접적으로 잡을 공급하고, 두 워커는 각자의 큐를 비우며, 디스크 장애 스레드는 큐가 아닌 타이머 기반으로 동작한다.
**hb_thread_cluster_reader**는 heartbeat UDP 소켓에서 I/O를 수행하는 유일한 스레드다. sfd = hb_Cluster->sfd를 한 번 캐시하고, 1ms timeout으로 poll한다:
// hb_thread_cluster_reader -- src/executables/master_heartbeat.c (condensed)sfd = hb_Cluster->sfd;while (hb_Cluster->shutdown == false) { po[0].fd = sfd; po[0].events = POLLIN; if (poll (po, 1, 1) <= 0) continue; /* <- timeout(0) 또는 poll 오류(-1): 재poll */ if ((po[0].revents & POLLIN) && sfd == hb_Cluster->sfd) /* <- sfd 변경 여부 재확인 */ { len = recvfrom (sfd, aligned_buffer, HB_BUFFER_SZ, 0, &from, &from_len); if (len > 0) /* <- 0 또는 음수 len은 조용히 버림 */ hb_cluster_receive_heartbeat (aligned_buffer, len, &from, from_len); } }sfd == hb_Cluster->sfd 재확인은 소켓이 스레드 실행 중에 교체되는 상황을 방어한다. 버퍼는 HB_BUFFER_SZ(4096) + MAX_ALIGNMENT 여유분이며, 루프 바깥에서 PTR_ALIGN으로 한 번 정렬된다. 디코딩 및 그에 따른 hb_cluster_job_queue 호출은 hb_cluster_receive_heartbeat(4장) 내에서 이루어진다.
**hb_thread_cluster_worker**와 **hb_thread_resource_worker**는 구조가 동일하다 — 큐 드레인 루프:
// hb_thread_cluster_worker -- src/executables/master_heartbeat.c (resource_worker는 resource_Jobs로 동일)while (cluster_Jobs->shutdown == false) { while ((job = hb_cluster_job_dequeue ()) != NULL) { job->func (job->arg); free_and_init (job); } /* <- 실행 후 엔트리 해제 */ SLEEP_MILISEC (0, 10); /* <- 준비된 잡 없음: 10ms 대기 */ }내부 루프는 준비된 잡을 모두 소진한다(헤드가 아직 만료되지 않았거나, 큐가 비었거나, shutdown이 latch된 경우 dequeue가 NULL을 반환한다). 외부 루프는 10ms 대기한다 — FSM의 스케줄링 단위가 이것이므로, “즉시” 등록된 잡도 최대 약 10ms 내에 실행된다. 워커가 엔트리를 해제하고, 핸들러가 job->arg를 소유하여 해제한다.
**hb_thread_check_disk_failure**는 HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS(100ms)마다 깨어나 PRM_ID_HA_CHECK_DISK_FAILURE_INTERVAL_IN_SECS까지 카운트다운하고, 세 가지 락(anchor → cluster → resource)을 모두 획득한 뒤 서버 로그 성장 실패 시 demote한다. demote 로직은 8장으로 미룬다. 여기서는 다음 사실만 주목한다: 이 스레드는 큐에 의존하지 않는 유일한 행위자이며, 큐 latch가 아닌 hb_Resource->shutdown을 루프 종료 조건으로 사용하고, 3-락 순서(3.9)가 존재하는 이유도 바로 이 스레드 때문이다.
3.9 두 mutex의 동기화 계약
섹션 제목: “3.9 두 mutex의 동기화 계약”데이터를 보호하는 mutex 두 개와, 커넥션 레이어에서 빌려 온 mutex 하나:
| Mutex | 보호 대상 | 보유 주체 |
|---|---|---|
hb_Cluster->lock | 클러스터 노드 리스트, 상태, 점수, is_isolated, sfd | 클러스터 잡, 수신 경로, 디스크 장애 스레드 |
hb_Resource->lock | 리소스 프로세스 리스트, hb_Resource->state | 리소스 잡, 디스크 장애 스레드 |
css_Master_socket_anchor_lock | 마스터의 소켓 anchor(커넥션 레이어) | 프로세스 I/O가 마스터를 통과할 때 HB 락 외부에서 획득 |
큐 mutex(cluster_Jobs->lock, resource_Jobs->lock)는 데이터 mutex와 별개이며, hb_job_* 원시 함수 내부에서만 획득한다. 데이터 락과 중첩되지 않는다. 이후 모든 장에서 경합이 발생하는 것은 데이터 락이다.
INVARIANT — 락 획득 순서는 anchor, 그다음
hb_Cluster->lock, 마지막hb_Resource->lock이다.hb_thread_check_disk_failure가 정확히 이 순서로 획득하고 역순으로 해제한다. 둘 이상의 락이 필요한 코드는 반드시 같은 순서를 따라야 한다. 그렇지 않으면 두 heartbeat plane이 디스크 장애 스레드와 교착 상태(deadlock)에 빠질 수 있다. 클러스터 워커와 리소스 워커는 보통 자신의 plane 데이터 락만 사용하므로 병렬 실행이 가능하다. 교착 위험은 크로스-plane 경로(failover, demote, disk-failure)에서만 발생한다. 6~8장이 이에 의존한다.
3.10 챕터 요약 — 핵심 정리
섹션 제목: “3.10 챕터 요약 — 핵심 정리”- 범용 큐 하나, 인스턴스 둘.
HB_JOB(mutex + 만료 오름차순 침입형 이중 연결 리스트 + 디스패치 테이블 포인터 +shutdown플래그, 1장)이cluster_Jobs와resource_Jobs로 인스턴스화된다. FSM의 모든 동작은 둘 중 하나에 등록된 잡이다. - enum 인덱스 테이블, static assert 없음.
hb_cluster_jobs[]/hb_resource_jobs[]는HB_CJOB_*/HB_RJOB_*enum과 일치해야 한다. 유일한 방어선은>= HB_*_MAX래퍼 검사와NULL센티넬이다. 불일치 시 잘못된 핸들러가 조용히 실행된다. - 삽입 시 정렬, dequeue 시 헤드만 확인.
hb_job_queue와 reorder 원시 함수가 리스트를expire오름차순으로 유지하고,hb_job_dequeue는now >= head->expire일 때만 헤드를 반환한다. 정렬 자체가 타이머다. hb_job_set_expire_and_reorder는 가속 원시 함수다. 첫 번째로 일치하는 잡의 만료 시각을 재설정(보통IMMEDIATELY)하고 큐 앞으로 재정렬한다 —CALC_SCORE를 끌어올리는 방법이다. 종료 중이거나 해당 잡이 없으면 조용히 no-op이다.- 잡은 스스로를 재등록한다.
hb_cluster_job_init이 heartbeat / ping-validity / calc-score를 초기 등록하고, 각 주기적 핸들러가 후속 잡을 재등록한다. 이 자기 영속(self-perpetuation)이 FSM의 시계다. - 분리된 스레드 4개, join 없음. 리더(POLLIN당 recvfrom 1회), 큐 드레인 워커 2개(10ms 단위), 100ms 디스크 장애 틱커 1개.
hb_job_shutdown의jobs->shutdownlatch는 워커 2개만 멈춘다. 리더와 디스크 장애 스레드는 종료 절차(10장)에서 설정되는 별도 플래그hb_Cluster->shutdown/hb_Resource->shutdown으로 종료한다. - 데이터 mutex 두 개, 엄격한 획득 순서.
hb_Cluster->lock과hb_Resource->lock(빌려 온 anchor 락 포함)이 두 plane을 보호한다. 정해진 순서 anchor → cluster → resource는 디스크 장애 스레드가 확립한 것이며, 크로스-plane 장(6~8장)이 반드시 준수해야 하는 교착 방지 계약이다.
Chapter 4: 네트워크를 통한 하트비트 교환
섹션 제목: “Chapter 4: 네트워크를 통한 하트비트 교환”이 챕터는 하나의 질문에 답한다: 노드는 어떻게 하트비트 패킷을 네트워크에 내보내고, 수신된 각 패킷이 피어 테이블을 정확히 어떻게 변경하는가? cubrid-heartbeat.md의 “UDP gossip”과 “split-brain 방지” 섹션이 왜를 설명한다면, 이 챕터는 분기 하나하나를 따라가며 어떻게를 추적한다. 전송 계층은 PRM_ID_HA_PORT_ID를 통한 비연결형 UDP다. 모든 동작은 단일 큰 잠금 hb_Cluster->lock 아래서 수행된다. 이 챕터에서는 챕터 5가 소비하는 두 가지 staleness 신호를 생성하며, 점수 계산은 하지 않는다.
4.1 와이어 포맷: HBP_HEADER
섹션 제목: “4.1 와이어 포맷: HBP_HEADER”모든 클러스터 데이터그램은 고정 크기의 HBP_HEADER로 시작한다(heartbeat.h에 선언). HBP_CLUSTER_HEARTBEAT 타입의 본문은 정확히 packed int 하나 — 송신자의 노드 상태 값이다.
// struct hbp_header -- src/connection/heartbeat.hstruct hbp_header{ unsigned char type;#if defined(HPUX) || defined(_AIX) || defined(sparc) char r:1; /* is request? */ /* <- on big-endian platforms r comes first */ char reserved:7;#else char reserved:7; /* both r:1 and reserved:7 pack into one byte; */ char r:1; /* physical order is endian/platform-gated */#endif unsigned short len; unsigned int seq; char group_id[HB_MAX_GROUP_ID_LEN]; char orig_host_name[CUB_MAXHOSTNAMELEN]; char dest_host_name[CUB_MAXHOSTNAMELEN];};아래의 모든 분기는 이 필드 의미론에서 출발하므로, 전체 역할 테이블을 먼저 확인한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
type | 메시지 식별자; HBP_CLUSTER_HEARTBEAT가 유일한 값 | 수신 측이 이 값으로 switch함; 그 외 값은 default 분기에서 로그 후 폐기 |
r | 요청 비트. 1 = “응답 요청”; 0 = 이것이 응답임 | 단일 프리미티브가 양방향 모두 처리; split-brain 방지를 위한 응답 여부를 결정 |
reserved | 비트필드 바이트를 채우는 패딩 | 컴파일러에 관계없이 헤더 바이트 레이아웃을 안정적으로 유지 |
len | 본문 길이, 네트워크 바이트 오더; 클러스터 HB에서는 항상 OR_INT_SIZE | 길이 검사로 잘리거나 과도하게 큰 데이터그램을 거부 |
seq | 시퀀스 번호, 네트워크 바이트 오더 | 예약됨; 클러스터 HB는 항상 0을 전달; 현재 중복 제거 없음 |
group_id | 클러스터 식별 문자열 | 동일 서브넷의 두 클러스터가 서로 간섭하지 않도록 방지 |
orig_host_name | 송신자 호스트명 | 피어 테이블 및 미확인 노드 테이블의 키 |
dest_host_name | 의도된 수신자 호스트명 | 첫 번째 유효성 검사 게이트; 나에게 오지 않은 데이터그램은 폐기 |
불변 조건 —
HBP_CLUSTER_HEARTBEAT의 본문 크기는 항상OR_INT_SIZE다. 송신자는len = OR_INT_SIZE로 정확히 int 하나를 담아 보내고, 수신자는sizeof(*hbp_header) + htons(hbp_header->len)을 재계산하여 실제 수신 바이트 수와 불일치하면 패킷을 버린다 — 의도적으로 실패 안전(fail-closed) 설계.
flowchart LR
subgraph 송신
JOB["hb_cluster_job_heartbeat<br/>~0.5s마다 재등록"]
ALL["hb_cluster_request_heartbeat_to_all<br/>피어마다 gap++"]
REQ["hb_cluster_send_heartbeat_req<br/>r=1"]
INT["hb_cluster_send_heartbeat_internal<br/>or_pack_int(state)"]
JOB --> ALL --> REQ --> INT
end
subgraph 수신
RDR["hb_thread_cluster_reader<br/>recvfrom 루프"]
RCV["hb_cluster_receive_heartbeat"]
RESP["hb_cluster_send_heartbeat_resp<br/>r=0"]
RDR --> RCV
RCV -->|r==1| RESP
end
INT -. UDP 데이터그램 .-> RDR
RESP -. UDP 데이터그램 .-> RDR
Figure 4-1. gossip 경로의 두 절반. 요청(r=1)이 응답(r=0)을 유발한다; 양쪽 모두 hb_cluster_send_heartbeat_internal을 공유한다.
4.2 송신: 자기 재등록 잡(job)
섹션 제목: “4.2 송신: 자기 재등록 잡(job)”hb_cluster_job_heartbeat는 메트로놈 역할이다 — 브로드캐스트 후 다시 자신을 재등록한다.
// hb_cluster_job_heartbeat -- src/executables/master_heartbeat.c pthread_mutex_lock (&hb_Cluster->lock); if (hb_Cluster->hide_to_demote == false) /* <- 게이트: demote 중에는 전송 건너뜀 */ hb_cluster_request_heartbeat_to_all (); pthread_mutex_unlock (&hb_Cluster->lock); error = hb_cluster_job_queue (HB_CJOB_HEARTBEAT, NULL, prm_get_integer_value (PRM_ID_HA_HEARTBEAT_INTERVAL_IN_MSECS)); // ... assert (error == NO_ERROR); if (arg) free_and_init (arg); ...분기는 세 가지다: (1) hide_to_demote == false → 브로드캐스트 실행; (2) hide_to_demote == true → 전송 건너뜀(챕터 8에서 다루는 demote 진행 중의 “조용히 하기” 상태 — 재등록은 계속하지만 패킷 발신은 중단); (3) arg != NULL → 잡 인수를 해제(방어적; 하트비트 잡은 NULL로 큐에 쌓인다). 재등록 주기는 PRM_ID_HA_HEARTBEAT_INTERVAL_IN_MSECS(기본값 500ms — HB_DEFAULT_HEARTBEAT_INTERVAL_IN_MSECS).
불변 조건 — 하트비트 잡은 항상 재등록된다.
hb_cluster_job_queue는 unlock 뒤, 모든if바깥에 위치하므로hide_to_demote가 전송을 억제하더라도 잡은 재등록된다. 이를 건너뛰면 클러스터가 gossip을 멈추고, 모든 피어는 이 노드의heartbeat_gap이PRM_ID_HA_MAX_HEARTBEAT_GAP(기본값HB_DEFAULT_MAX_HEARTBEAT_GAP= 5)을 초과하는 순간 이 노드를 dead로 표시한다.
브로드캐스트 루프
섹션 제목: “브로드캐스트 루프”// hb_cluster_request_heartbeat_to_all -- src/executables/master_heartbeat.c if (hb_Cluster == NULL) { ... return; } /* 방어적: 초기화 전 / 종료 후 */ for (node = hb_Cluster->nodes; node; node = node->next) { if (are_hostnames_equal (hb_Cluster->host_name, node->host_name)) continue; /* <- 자기 자신에게는 하트비트 보내지 않음 */ hb_cluster_send_heartbeat_req (node->host_name); node->heartbeat_gap++; /* <- 신호 #1, 낙관적으로 증가 */ }분기는 두 가지다: 자기 자신 항목은 건너뛰고; 그 외 모든 피어에는 요청을 보내며 무조건 heartbeat_gap++를 한다. 이 증가는 응답 여부가 확인되기 전에 발생하며(감소는 수신자에서 유효한 응답이 왔을 때만), sendto의 반환값은 무시된다 — 전송이 불가능한 피어가 바로 gap으로 stale 판정을 받아야 하는 대상이다.
4.3 송신: 세 가지 전송 프리미티브
섹션 제목: “4.3 송신: 세 가지 전송 프리미티브”_req와 _resp는 _internal의 얇은 단일 행 래퍼로, is_req 플래그만 다르다. _req는 먼저 hb_hostname_n_port_to_sockaddr로 dest_host_name을 새로운 sockaddr_in으로 변환하며, DNS/파싱 실패 시 해당 피어를 건너뛰고 조기 반환한다. _resp는 데이터그램이 도착한 from 주소를 재사용하므로 DNS 조회를 하지 않는다. _internal은 버퍼를 구성하고, 상태를 담은 int 하나를 pack해서 전송한다 — 핵심 코드:
// hb_cluster_send_heartbeat_internal -- src/executables/master_heartbeat.c // ... memset buffer; hbp_header = (HBP_HEADER *) &buffer[0] ... error_code = hb_set_net_header (hbp_header, HBP_CLUSTER_HEARTBEAT, is_req, OR_INT_SIZE, 0 /* seq */, dest_host_name); if (error_code != NO_ERROR) /* <- myself==NULL: 클러스터 비정상, 중단 */ return error_code; p = or_pack_int ((char *) (hbp_header + 1), hb_Cluster->state); /* <- 페이로드 int 하나 */ hb_len = sizeof (HBP_HEADER) + OR_INT_SIZE; if (hb_Cluster->sfd == INVALID_SOCKET) /* 소켓이 열려 있지 않음 */ return ERR_CSS_TCP_DATAGRAM_SOCKET; send_len = sendto (hb_Cluster->sfd, &buffer[0], hb_len, 0, (struct sockaddr *) saddr, saddr_len); if (send_len <= 0) /* 커널 거부 / 인터페이스 다운 */ return ER_FAILED; return NO_ERROR;오류 분기는 세 가지다 — (a) myself == NULL; (b) INVALID_SOCKET; (c) sendto <= 0 — 모두 호출자로 전파되며, 브로드캐스트 경로에서는 오류를 버리고 heartbeat_gap이 계속 올라가도록 둔다. 페이로드는 오직 hb_Cluster->state 하나다.
헤더 채우기: hb_set_net_header
섹션 제목: “헤더 채우기: hb_set_net_header”유일한 실패 조건은 myself == NULL(자기 항목이 없어 orig_host_name을 채울 수 없음)이며, 그 외에는 type/r/len/seq와 식별 문자열을 채운다:
// hb_set_net_header -- src/executables/master_heartbeat.c if (hb_Cluster->myself == NULL) return ER_FAILED; /* <- 유일한 실패 분기 */ header->type = type; /* HBP_CLUSTER_HEARTBEAT */ header->r = (is_req) ? 1 : 0; /* 요청/응답 비트 */ header->len = htons (len); /* 네트워크 바이트 오더 */ header->seq = htonl (seq); // ... strncpy_bufsize group_id / dest_host_name / myself->host_name -> orig_host_name ...len/seq는 네트워크 바이트 오더로 변환하고, 호스트명 문자열은 raw 복사이므로 수신 측은 단순 strcmp / are_hostnames_equal로 비교한다.
4.4 수신: hb_cluster_receive_heartbeat, 분기별 분석
섹션 제목: “4.4 수신: hb_cluster_receive_heartbeat, 분기별 분석”리더 스레드는 recvfrom 루프를 돌며 데이터그램마다 이 함수를 호출한다. 유효성 검사 게이트들을 통과하면 상태 변경이 수행된다. Figure 4-2는 모든 분기를 나타내고, 각 핵심 조건을 산문으로 설명한다.
flowchart TD
A["recvfrom 데이터그램"] --> B{"shutdown?"}
B -->|예| Z1["unlock, return"]
B -->|아니오| C{"dest_host_name == 나?"}
C -->|아니오| Z2["불일치 로그, unlock, return"]
C -->|예| D{"len == header+htons(len)?"}
D -->|아니오| Z3["크기 불일치 로그, unlock, return"]
D -->|예| E{"type == HBP_CLUSTER_HEARTBEAT?"}
E -->|아니오| Z4["default: 알 수 없음 로그, break"]
E -->|예| F["or_unpack_int -> state"]
F --> G{"state 범위 내?"}
G -->|아니오| Z5["알 수 없는 state 로그, unlock, return"]
G -->|예| H{"hb_is_heartbeat_valid == NO_ERROR?"}
H -->|아니오| UI["미확인 노드 처리"]
H -->|예| J{"group_id == 내 그룹?"}
UI --> J
J -->|아니오| Z6["unlock, return"]
J -->|예| K{"r == 1 이고 hide_to_demote 아님?"}
K -->|예| RESP["하트비트 응답 전송"]
K -->|아니오| L
RESP --> L["hb_return_node_by_name_except_me"]
L --> M{"노드 발견?"}
M -->|아니오| Z7["알 수 없는 호스트 로그, break"]
M -->|예| N{"이전==MASTER이고 새값!=MASTER?"}
N -->|예| O["is_state_changed=true, hb_Master_host_name 저장"]
N -->|아니오| P
O --> P["state=new; gap=MAX(0,gap-1); last_recv_hbtime 갱신"]
P --> Q{"is_state_changed?"}
Q -->|예| R["CALC_SCORE를 즉시 실행으로 재정렬"]
Q -->|아니오| Z8["return"]
Figure 4-2. hb_cluster_receive_heartbeat의 모든 분기. 왼쪽은 거부 경로; 오른쪽은 수락 후 상태 변경.
처음 네 개의 거부 게이트는 같은 패턴을 공유한다 — 검사, MASTER_ER_LOG_DEBUG, pthread_mutex_unlock, return(단, type 게이트는 switch를 break하여 아래쪽 동일한 unlock 코드로 흘러간다). 각 게이트의 조건:
- 게이트 0 — shutdown:
if (hb_Cluster->shutdown)— 종료 중에는 아무것도 처리하지 않는다. - 게이트 1 — 목적지:
if (!are_hostnames_equal (hb_Cluster->host_name, hbp_header->dest_host_name))— 나에게 오지 않은 브로드캐스트/오라우팅 패킷을 버린다. - 게이트 2 — 길이:
if (len != (int)(sizeof (*hbp_header) + htons (hbp_header->len)))— §4.1의 본문 크기 불변 조건을 수신 측에서 강제 검사. - 게이트 3 — 타입:
switch (hbp_header->type);HBP_CLUSTER_HEARTBEAT만 처리하며,default는 “unknown heartbeat message”를 로그하고 break. - 게이트 4 — state 범위: case 내에서
or_unpack_int로 본문을hb_state에 읽고,if (hb_state < HB_NSTATE_UNKNOWN || hb_state >= HB_NSTATE_MAX)이면 unlock 후 return — 손상되거나 호환되지 않는 state 값을 버린다.
게이트 5 — 유효성 검사 + 미확인 노드 처리. hb_is_heartbeat_valid(§4.6)는 HB_VALID_NO_ERROR 또는 네 가지 거부 이유 중 하나를 반환한다. 거부 시 코드는 조용히 버리지 않고 위반 노드를 기록한다:
rv = hb_is_heartbeat_valid (hbp_header->orig_host_name, hbp_header->group_id, from); if (rv != HB_VALID_NO_ERROR) { ui_node = hb_return_ui_node (hbp_header->orig_host_name, hbp_header->group_id, *from); if (ui_node && ui_node->v_result != rv) /* 이유가 바뀜 -> 오래된 항목 */ { hb_remove_ui_node (ui_node); ui_node = NULL; } if (ui_node == NULL) /* 첫 발견 (또는 이유 변경) */ { MASTER_ER_SET (..., ER_HB_NODE_EVENT, 1, error_string); (void) hb_add_ui_node (hbp_header->orig_host_name, hbp_header->group_id, *from, rv); } else gettimeofday (&ui_node->last_recv_time, NULL); /* 이미 알려진 위반자: 시간만 갱신 */ }세 가지 하위 분기가 있다: (a) 항목 없음 → 한 번만 로그 후 추가; (b) 항목 있고 v_result가 다름 → 제거 후 재추가(새로운 이유로 로그 재발생); (c) 항목 있고 이유 동일 → 조용히 last_recv_time만 갱신. 하위 분기 (c)가 속도 제한 역할을 한다 — 패킷을 마구 보내는 피어가 있더라도 ER_HB_NODE_EVENT는 한 번만 기록되고 패킷마다 중복 기록되지 않는다.
게이트 6 — 그룹 일치 (강제 폐기). if (strcmp (hbp_header->group_id, hb_Cluster->group_id)) 이면 unlock 후 return. hb_is_heartbeat_valid 내부의 그룹 검사를 의도적으로 중복 배치한 것으로, 다른 그룹의 패킷이 위에서 로그 처리됐더라도 상태 변경 코드에는 절대 도달해서는 안 되기 때문이다.
응답 분기 — split-brain 방지. if (hbp_header->r && hb_Cluster->hide_to_demote == false)이면 hb_cluster_send_heartbeat_resp를 호출한다. hide_to_demote 가드는 §4.2의 송신 쪽 가드와 대칭을 이루며, demote 중인 노드는 하트비트를 시작하지도 응답하지도 않아 피어들이 이 노드를 더 이상 살아있다고 카운트하지 않게 한다(상세 근거는 companion doc 참조).
상태 변경 — 피어 조회, state, gap, timestamp.
node = hb_return_node_by_name_except_me (hbp_header->orig_host_name); if (node) { if (node->state == HB_NSTATE_MASTER && node->state != hb_state) { /* 현재 마스터가 강등됨: 장애 조치 오류 메시지를 위해 이름 저장 */ is_state_changed = true; snprintf (hb_Master_host_name, strlen (node->host_name) + 1, node->host_name); } node->state = hb_state; /* 피어가 보고한 state 반영 */ node->heartbeat_gap = MAX (0, (node->heartbeat_gap - 1)); /* <- 신호 #1, 감소 */ gettimeofday (&node->last_recv_hbtime, NULL); /* <- 신호 #2, 시각 기록 */ } else MASTER_ER_LOG_DEBUG (..., "receive heartbeat have unknown host_name. ...");분기는 두 가지다. (1) node == NULL: 설정된 피어가 아님; 게이트 5에서 이미 HB_VALID_UNIDENTIFIED_NODE로 처리됨. (2) node 발견: 중첩된 old==MASTER && new!=MASTER 검사는 수신 경로에서 is_state_changed를 올리는 유일한 지점이며, 그 후 무조건 세 가지 변경(state 반영, gap 하한 감소, timestamp 기록)이 실행된다.
불변 조건 — 카운터는
hb_Cluster->lock아래서만 변경되고,heartbeat_gap은 0을 하한으로 한다. 수신자는 게이트 0부터 하단 unlock까지 잠금을 유지하고, 송신자도 전체 브로드캐스트 동안 잠금을 유지하므로, 피어별gap++와gap = MAX(0, gap-1)이 인터리빙되어 틱을 잃는 일은 없다. 하한은 응답이 많은 피어(요청 수보다 응답이 더 많은 경우)가gap을 음수로 만들어 이후의 침묵을 숨기는 것을 방지한다 — 음수gap이면 챕터 5가 확인하는PRM_ID_HA_MAX_HEARTBEAT_GAP임계값을 넘어서기까지 여분의 틱이 더 필요해진다.
꼬리 부분 — 지연된 점수 재계산. unlock 후, 마스터 강등이 감지된 경우에만:
pthread_mutex_unlock (&hb_Cluster->lock); if (is_state_changed == true) hb_cluster_job_set_expire_and_reorder (HB_CJOB_CALC_SCORE, HB_JOB_TIMER_IMMEDIATELY);수신자는 HB_CJOB_CALC_SCORE를 HB_JOB_TIMER_IMMEDIATELY(== 0)로 앞당긴다 — 챕터 5로의 인계이며, 잡 큐 호출 동안 잠금을 유지하지 않기 위해 unlock 이후에 수행된다.
4.5 이중 staleness 신호 — 챕터 5가 소비하는 것
섹션 제목: “4.5 이중 staleness 신호 — 챕터 5가 소비하는 것”| 신호 | 필드 | 증가/기록 주체 | 감소/갱신 주체 | 의미 |
|---|---|---|---|---|
| 미응답 요청 카운트 | heartbeat_gap | 송신자, 브로드캐스트마다 gap++ | 수신자, 유효한 응답마다 MAX(0, gap-1) | 응답 없이 쌓인 하트비트 수; PRM_ID_HA_MAX_HEARTBEAT_GAP을 초과하면 점수 경로가 피어를 HB_NSTATE_UNKNOWN으로 선언 |
| 마지막 수신 시각 | last_recv_hbtime | — | 수신자, 유효한 응답마다 현재 시각 기록 | 벽시계 기준 최신성; 점수 경로는 PRM_ID_HA_CALC_SCORE_INTERVAL_IN_MSECS를 사용하고, hb_cluster_is_received_heartbeat_from_all은 더 짧은 PRM_ID_HA_HEARTBEAT_INTERVAL_IN_MSECS를 사용 |
이중 구성은 의도적이다: gap은 이벤트를 세므로 시계 점프에서도 살아남고; timestamp는 경과 벽시계 시간을 측정하므로 억제된 전송에서도 살아남는다(예: hide_to_demote 상태에서는 gap이 더 이상 오르지 않지만, 실제 침묵은 여전히 감지돼야 한다). 피어가 건강하려면 두 신호 모두 임계값 이하여야 한다.
4.6 유효성 검사 및 조회 헬퍼
섹션 제목: “4.6 유효성 검사 및 조회 헬퍼”hb_is_heartbeat_valid는 첫 번째 실패 이유를 반환한다. 네 가지 거부 조건과 성공이 순서대로 확인된다:
// hb_is_heartbeat_valid -- src/executables/master_heartbeat.c if (hb_return_node_by_name_except_me (host_name) == NULL) return HB_VALID_UNIDENTIFIED_NODE; /* 설정된 피어가 아님 */ if (strcmp (group_id, hb_Cluster->group_id) != 0) return HB_VALID_GROUP_NAME_MISMATCH; /* 잘못된 클러스터 */ // ... hb_hostname_to_sin_addr 실패 -> HB_VALID_CANNOT_RESOLVE_HOST ... if (memcmp (&sin_addr, &from->sin_addr, sizeof (struct in_addr)) != 0) return HB_VALID_IP_ADDR_MISMATCH; /* <- 스푸핑 방지: 이름이 출발지 IP로 해석돼야 함 */ return HB_VALID_NO_ERROR;IP 검사가 스푸핑 방지 가드다. 열거형은 hb_valid_result_string이 HB_VALID_*_STR로 변환하여 로그 라인에 기록된다. hb_return_node_by_name과 _except_me가 두 가지 피어 테이블 조회 함수며, _except_me는 continue 조건에 || are_hostnames_equal (name, hb_Cluster->host_name)을 추가하여 자기 항목을 절대 반환하지 않는다. 수신자는 항상 _except_me를 사용한다 — 노드는 자신의 하트비트를 피어의 것으로 처리해서는 안 된다.
미확인 노드 테이블(hb_Cluster->ui_nodes, 이중 연결 HB_UI_NODE_ENTRY 목록)의 관리자는 세 가지다:
hb_return_ui_node—(host_name, group_id, 출발지 IP)세 값 모두가 일치하는 항목을 선형 탐색.hb_add_ui_node— 유효한 거부 코드를assert하고,hb_return_ui_node로 중복을 확인한 후 없으면malloc으로 할당하여 세 값과v_result를 채우고,last_recv_time을 기록한 뒤 연결 목록에 추가하고num_ui_nodes를 증가;malloc실패 시NULL반환.hb_cleanup_ui_nodes— GC 역할:HB_UI_NODE_CLEANUP_TIME_IN_MSECS(1시간)보다 오래된 항목을hb_remove_ui_node로 제거.
불변 조건 —
num_ui_nodes는ui_nodes목록의 길이와 같다. 추가 시 증가하고,hb_remove_ui_node는 연결 해제/해제/감소를 하며assert(0)가드와 함께 0을 하한으로 고정한다. 값이 어긋나면 운영자에게 보이는 카운트(챕터 10)가 거짓말을 하게 된다; 하한은 언더플로우로 인해 값이 매우 큰 양수로 감싸지는 것을 방지한다.
4.7 챕터 요약 — 핵심 정리
섹션 제목: “4.7 챕터 요약 — 핵심 정리”- 와이어 페이로드는 packed int 하나 —
hb_Cluster->state다. gap, timestamp, 점수는 모두 로컬에서 파생되며 와이어에는 liveness 관련 정보가 올라가지 않는다. heartbeat_gap과last_recv_hbtime이 두 가지 staleness 신호다(§4.5). 둘 다 유효한 응답이 왔을 때만 감소/기록되며, 챕터 5가 두 신호를 모두 소비한다.- 수신자는 실패 안전 게이트들의 연쇄다 — 목적지, 길이, 타입, state 범위, 유효성, 그룹 — 모두 상태 변경 전에 검사되고; 그룹 및 유효성 실패는 미확인 노드 이벤트로도 로그된다.
r과hide_to_demote가 split-brain 방지를 구현한다: 요청에는 응답(r == 0)하지만, 노드가 demote 중이면 양방향 모두 침묵한다.- 피어가 마스터 지위를 잃는 사건이 선거를 선점하는 유일한 이벤트다 —
old==MASTER && new!=MASTER분기가is_state_changed를 올리고HB_CJOB_CALC_SCORE를 즉시 실행으로 재정렬한다. - 미확인 호스트는 패킷마다 기록되는 것이 아니라 이유별로 한 번만 로그된다 —
ui_node중복 제거(호스트+그룹+IP, 동일 이유면 갱신만, 1시간 후 GC)로 구현된다. - 카운터는
hb_Cluster->lock아래서만 변경되고, 메트로놈은 멈추지 않는다 — 전송 잡은 억제 중에도 모든 가드 바깥에서 재등록된다.
Chapter 5: 점수 계산과 로컬 리더 선출
섹션 제목: “Chapter 5: 점수 계산과 로컬 리더 선출”4장에서 작성된 피어 테이블(last_recv_hbtime, heartbeat_gap, 각 HB_NODE_ENTRY의 state)은 원시 관측값이다. 이 장이 답하는 질문은 하나다: 노드는 그 테이블을 어떻게 단 하나의 마스터 선출 결과로 압축하고, 그 결과에 따라 실제로 행동할지를 결정하는가? hb_cluster_calc_score가 산술 단계(오래된 피어 만료, 각 상태 점수화, 최솟값 선택)를 담당하고, hb_cluster_job_calc_score가 의사결정 허브(결과를 이 노드 자신의 상태와 대조해 전이 잡(job)을 정확히 하나 큐에 넣거나 재무장)가 된다. 점수 모델과 선출 개념의 이론적 배경은 동반 문서(cubrid-heartbeat.md, “점수 기반 선출” 절)에 있다. 여기서는 모든 분기와 비트를 추적한다. 전이 잡이 큐에 올라가는 순간 이 장은 끝난다. 잡 자체 — HB_CJOB_CHECK_PING, HB_CJOB_FAILOVER, HB_CJOB_FAILBACK — 는 6~8장에서 다룬다.
아래 두 다이어그램은
stateDiagram-v2가 아닌flowchart를 사용한다. 따라서 노드 레이블 안의<br/>와 괄호는 허용된다(KB Mermaid 규칙 5). 전이 레이블에는 이를 사용하지 않는다.
5.1 점수 비트마스크 — MASTER가 최솟값 탐색에서 이기는 이유
섹션 제목: “5.1 점수 비트마스크 — MASTER가 최솟값 탐색에서 이기는 이유”선출은 최솟값 탐색이다. score가 가장 작은 노드가 hb_Cluster->master가 된다. master_heartbeat.h의 HB_NODE_SCORE_* 상수 네 개는 아래 표의 16진수 값에 해당한다.
최종 점수는 node->priority | HB_NODE_SCORE_*이며, priority는 작은 양의 unsigned short다(설정된 마스터가 1, ha_node_list 순으로 증가; 레플리카는 HB_REPLICA_PRIORITY = 0x7FFF). score는 signed short(HB_NODE_ENTRY의 short score;)이므로, 최상위 비트 0x8000을 OR하면 부호 비트가 설정되어 값이 음수가 된다. 음수는 모든 슬레이브·unknown보다 낮게 정렬된다.
| 점수 상수 | Hex | signed short로 해석 | 정렬 위치 |
|---|---|---|---|
HB_NODE_SCORE_MASTER (MASTER, TO_BE_SLAVE) | 0x8000 | 0x8000 = −32768 (SHRT_MIN); 0x8000|1 = −32767 | 가장 낮음 — 선출 승리 |
HB_NODE_SCORE_TO_BE_MASTER | 0xF000 | −4096 | 낮지만 MASTER보다 위 |
HB_NODE_SCORE_SLAVE | 0x0000 | 0 + priority | 중간 |
HB_NODE_SCORE_UNKNOWN (UNKNOWN, REPLICA) | 0x7FFF | +32767 | 가장 높음 — 절대 선출 불가 |
불변 조건 — 선출되는 마스터는 수치상 가장 작은 점수이며, 살아 있는 MASTER는 항상 SLAVE를 이긴다. 부호 비트 인코딩으로 강제된다: MASTER/TO_BE_SLAVE/TO_BE_MASTER만 비트 15를 설정하므로, 비음수인 SLAVE는 결코 이들을 능가할 수 없다.
score를unsigned short로 선언하면0x8000이 가장 큰 값이 되어 마스터가 선출되지 않는다 — 단 한 글자 변경으로 HA 클러스터 선출 논리 전체가 조용히 뒤집힌다.
TO_BE_SLAVE가 MASTER 비트(0x8000)를 공유하는 이유. HB_NSTATE_TO_BE_SLAVE 노드는 강등(demotion) 진행 중인 전(前) 마스터다(8장). 강등이 완료될 때까지 진짜 슬레이브보다 높은 순위를 유지해야 두 번째 마스터가 조기에 선출되는 상황을 막는다. HB_NODE_SCORE_TO_BE_MASTER = 0xF000은 반대 경우다 — 승격(promotion) 중인 슬레이브(6장): 음수(슬레이브/unknown 이김)이지만 0x8000보다 크므로, 진짜 MASTER가 여전히 이긴다.
5.2 hb_cluster_calc_score — 오래된 피어 만료, 그리고 점수 스위치
섹션 제목: “5.2 hb_cluster_calc_score — 오래된 피어 만료, 그리고 점수 스위치”이 함수는 hb_Cluster->nodes를 한 번 순회한다. 피어 점수를 계산하기 전에 오래된 피어를 HB_NSTATE_UNKNOWN으로 강제 강등한 뒤, switch 문으로 상태를 점수에 매핑한다.
// hb_cluster_calc_score -- src/executables/master_heartbeat.cint num_master = 0;short min_score = HB_NODE_SCORE_UNKNOWN; /* start at the worst possible */hb_Is_master_node_isolated = false;// ... NULL guard elided: if (hb_Cluster == NULL) return ER_FAILED; ...hb_Cluster->myself->state = hb_Cluster->state; /* sync own entry from cluster state */gettimeofday (&now, NULL);for (node = hb_Cluster->nodes; node; node = node->next) { if (node->heartbeat_gap > prm_get_integer_value (PRM_ID_HA_MAX_HEARTBEAT_GAP) /* count path */ || (!HB_IS_INITIALIZED_TIME (node->last_recv_hbtime) /* <- never-heard skipped */ && HB_GET_ELAPSED_TIME (now, node->last_recv_hbtime) /* time path */ > prm_get_integer_value (PRM_ID_HA_CALC_SCORE_INTERVAL_IN_MSECS))) { if (hb_Cluster->myself != node && node->state == HB_NSTATE_MASTER) { hb_Is_master_node_isolated = true; /* save node->host_name into hb_Master_host_name */ } node->heartbeat_gap = 0; /* reset staleness counters */ node->last_recv_hbtime.tv_sec = node->last_recv_hbtime.tv_usec = 0; node->state = HB_NSTATE_UNKNOWN; /* <- demote stale peer */ } switch (node->state) { case HB_NSTATE_MASTER: case HB_NSTATE_TO_BE_SLAVE: node->score = node->priority | HB_NODE_SCORE_MASTER; break; /* 0x8000 */ case HB_NSTATE_TO_BE_MASTER: node->score = node->priority | HB_NODE_SCORE_TO_BE_MASTER; break; /* 0xF000 */ case HB_NSTATE_SLAVE: node->score = node->priority | HB_NODE_SCORE_SLAVE; break; /* 0x0000 */ case HB_NSTATE_REPLICA: case HB_NSTATE_UNKNOWN: default: node->score = node->priority | HB_NODE_SCORE_UNKNOWN; break; /* 0x7FFF */ } if (node->score < min_score) { hb_Cluster->master = node; min_score = node->score; } /* <- running argmin */ if (node->score < (short) HB_NODE_SCORE_TO_BE_MASTER) /* < 0xF000 as short == MASTER bit */ num_master++; }return num_master;생략된 NULL 가드는 ER_FAILED를 반환하며, 호출자는 이를 비양수 num_master로 처리한다.
두 갈래 오래된 피어 판별 조건. 피어는 다음 둘 중 하나라도 해당하면 UNKNOWN으로 리셋된다. 카운트 경로 heartbeat_gap > ha_max_heartbeat_gap(gap은 송신 시 증가, 수신 시 0 초기화, 4장 참고)는 폴링 중에 응답이 멈춘 피어를 잡는다. 시간 경로 elapsed > ha_calc_score_interval은 heartbeat_gap이 증가하지 않았지만 실제 시간이 경과한, 즉 송신 루프가 멈춘 상황을 잡는다. HB_IS_INITIALIZED_TIME 가드({0,0})는 한 번도 들어본 적 없는 피어가 시간 절을 건너뛰게 만들어, “1970년 이후 경과 시간”이라는 거짓 값으로 판별되는 일을 방지하고 오직 증가하는 heartbeat_gap으로만 만료되도록 한다. 오래된 피어가 현재 MASTER일 때 hb_Is_master_node_isolated/hb_Master_host_name 전역 변수가 설정된다 — 이 값은 §5.4의 페일오버 진단에서만 읽히며, 점수 산술에는 영향을 주지 않는다.
switch와 num_master. switch는 완전(total)하다(default와 명시적 REPLICA/UNKNOWN이 모든 상태를 UNKNOWN으로 강등). 감소(reduction) 결과로 hb_Cluster->master가 가장 낮은 점수에 위치한다. num_master는 점수가 (short)0xF000(−4096)을 엄격히 하회하는 노드만 셈한다 — 이는 0x8000 계열(점수 ≤ −32767)에만 해당하며, TO_BE_MASTER(0xF000)는 포함되지 않는다. 호출자는 num_master > 1을 스플릿 브레인(split-brain) 검출기로 사용한다.
불변 조건 —
num_master는 안정된 마스터 계열 노드만 셈하며, 진행 중인 승격은 포함하지 않는다.(short)0xF000에 대한 엄격한<비교로 강제된다. 비교가<=이거나0x8000을 기준으로 삼으면, 진행 중인 TO_BE_MASTER 승격이num_master를 부풀려 §5.4의num_master > 1분기가 정당한 페일오버 도중에 있는 노드를 상대로 거짓 FAILBACK을 발동시킨다.
flowchart TD
A["시작: min_score = 0x7FFF, num_master = 0"] --> B["myself->state = cluster->state"]
B --> C{"nodes의 각 node에 대해"}
C -->|"오래됨: gap > max 또는 elapsed > interval"| D{"오래된 피어 == MASTER이고 myself 아님?"}
D -->|예| E["hb_Is_master_node_isolated = true, host_name 저장"]
D -->|아니오| F["gap, last_recv_hbtime 리셋; state = UNKNOWN"]
E --> F
C -->|최신| G["score = priority | 상태별 비트마스크"]
F --> G
G --> H{"score < min_score?"}
H -->|예| I["master = node; min_score = score"]
H -->|아니오| J["현재 master 유지"]
I --> K{"score < (short)0xF000?"}
J --> K
K -->|예| L["num_master++"]
K -->|아니오| M[" "]
L --> C
M --> C
C -->|완료| N["num_master 반환"]
Figure 5-1. hb_cluster_calc_score: 피어별 오래된 피어 만료, 점수화, 누적 argmin, 마스터 카운터 — 모든 분기.
5.3 두 보조 함수: hb_cluster_is_isolated와 hb_cluster_is_received_heartbeat_from_all
섹션 제목: “5.3 두 보조 함수: hb_cluster_is_isolated와 hb_cluster_is_received_heartbeat_from_all”hb_cluster_is_isolated는 “내가 모든 실제 피어로부터 단절되어 있는가?”를 답한다.
// hb_cluster_is_isolated -- src/executables/master_heartbeat.cfor (node = hb_Cluster->nodes; node; node = node->next) { if (node->state == HB_NSTATE_REPLICA) continue; /* <- replicas never count */ if (hb_Cluster->myself != node && node->state != HB_NSTATE_UNKNOWN) return false; /* a live non-replica peer exists */ }return true;세 가지 경로가 있다. REPLICA는 건너뜀(읽기 전용 어플라이어, 마스터 후보 아님). myself는 건너뜀(자신의 항목이 단절 여부를 가릴 수 있음). 그 외 UNKNOWN이 아닌 피어가 하나라도 있으면 false를 반환한다. 레플리카와 자신을 제외한 모든 피어가 UNKNOWN일 때만 true를 반환한다.
불변 조건 — 단절(isolation)은
hb_cluster_calc_score가 동일한 사이클에서 모든 비레플리카 피어를 UNKNOWN으로 만료시켰음을 의미한다. 호출자는 §5.4에서calc_score직후is_isolated를 실행하므로, 읽히는 UNKNOWN 상태는 이번 사이클의 결과다.calc_score없이is_isolated를 호출하면 건강한 노드를 단절로 잘못 판정할 수 있다.
hb_cluster_is_received_heartbeat_from_all은 페일오버 지연을 조정하는 데만 사용되는 더 엄격한 실시간 신선도(freshness) 검사다.
// hb_cluster_is_received_heartbeat_from_all -- src/executables/master_heartbeat.cheartbeat_confirm_time = prm_get_integer_value (PRM_ID_HA_HEARTBEAT_INTERVAL_IN_MSECS);gettimeofday (&now, NULL);for (node = hb_Cluster->nodes; node; node = node->next) { if (hb_Cluster->myself != node && HB_GET_ELAPSED_TIME (now, node->last_recv_hbtime) > heartbeat_confirm_time) return false; /* one peer is even one interval stale */ }return true;모든 비자신(non-self) 피어가 ha_heartbeat_interval 한 주기 안에 들렸을 때만 true를 반환한다. is_isolated와 달리 레플리카를 건너뛰지 않으며 {0,0} 가드도 없다 — 한 번도 들어본 적 없는 피어의 경과 시간은 heartbeat_confirm_time을 초과하므로 올바르게 false를 반환한다. 이 가장 엄격한 술어(predicate)는 §5.4에서 빠른 대기(500 ms)와 보수적 대기 중 하나를 고르는 데만 존재한다.
5.4 hb_cluster_job_calc_score — 의사결정 허브
섹션 제목: “5.4 hb_cluster_job_calc_score — 의사결정 허브”CALC_SCORE 잡 본문은 hb_Cluster->lock 하에서 실행되며, 위의 두 단계를 호출한 뒤 이 노드 자신의 hb_Cluster->state에 따라 분기한다. 전이 잡은 최대 하나만 발행하며(낙하 분기(fall-through)만 CALC_SCORE를 재무장한다).
// hb_cluster_job_calc_score -- src/executables/master_heartbeat.c (lock/unlock, free_and_init, malloc-arg setup condensed)num_master = hb_cluster_calc_score ();hb_Cluster->is_isolated = hb_cluster_is_isolated ();
if (hb_Cluster->state == HB_NSTATE_REPLICA || hb_Cluster->hide_to_demote == true) goto calc_end; /* Branch 0: replicas / demoting never elect */
if (hb_Cluster->state == HB_NSTATE_MASTER && hb_Cluster->is_isolated == true) { /* Branch 1: isolated master -> ping witness */ if (malloc ok) hb_cluster_job_queue (HB_CJOB_CHECK_PING, job_arg, HB_JOB_TIMER_IMMEDIATELY); return; /* malloc fail: NOT queued, stay master; NO re-queue */ }
if ((num_master > 1) && hb_Cluster->master && hb_Cluster->myself /* Branch 2: split-brain loser */ && hb_Cluster->myself->state == HB_NSTATE_MASTER && hb_Cluster->master->priority != hb_Cluster->myself->priority) /* <- loser test */ { hb_cluster_job_queue (HB_CJOB_FAILBACK, NULL, HB_JOB_TIMER_IMMEDIATELY); return; }
if ((hb_Cluster->state == HB_NSTATE_SLAVE) && hb_Cluster->master && hb_Cluster->myself /* Branch 3: winner */ && hb_Cluster->master->priority == hb_Cluster->myself->priority) /* <- winner == me */ { hb_Cluster->state = HB_NSTATE_TO_BE_MASTER; /* <- the election write */ hb_cluster_request_heartbeat_to_all (); // ... pick diagnostic from hb_Is_master_node_isolated / hb_Master_host_name ... if (malloc ok) hb_cluster_job_queue (HB_CJOB_CHECK_PING, job_arg, HB_JOB_TIMER_WAIT_100_MILLISECOND); else /* malloc failed: skip ping, pick FAILOVER delay */ { SLEEP_MILISEC (0, HB_JOB_TIMER_WAIT_100_MILLISECOND); failover_wait_time = hb_cluster_is_received_heartbeat_from_all () ? HB_JOB_TIMER_WAIT_500_MILLISECOND /* fast: all fresh */ : prm_get_integer_value (PRM_ID_HA_FAILOVER_WAIT_TIME_IN_MSECS); /* conservative */ hb_cluster_job_queue (HB_CJOB_FAILOVER, NULL, failover_wait_time); } return; }
calc_end: /* Branch 4: steady state */hb_cluster_job_queue (HB_CJOB_CALC_SCORE, NULL, /* <- only path that re-arms */ prm_get_integer_value (PRM_ID_HA_CALC_SCORE_INTERVAL_IN_MSECS));인라인 /* Branch N */ 주석과 Figure 5-2가 전체 흐름을 열거한다. 각 분기의 핵심 근거는 다음과 같다.
- Branch 0 —
REPLICA와hide_to_demote(강등 진행 중인 마스터, 8장)는 가시성(observability) 목적으로만 계산을 수행한다. - Branch 1 —
malloc실패 경로는 의도적인 페일세이프(fail-safe)다: 단절된 마스터는 메모리 압박 상황에서도 강등되지 않고 마스터 상태를 유지한다. - Branch 2 (패자) —
priority !=조건이 패자 판별식이다. 우선순위가 낮을수록 순위가 높으므로(낮은 값 = 높은 순위), 우선순위가 같다면 내가 승자이므로 이 분기를 통과한다. - Branch 3 (승자) — 대칭되는
priority ==가드는 argmin 승자가 내 항목임을 뜻한다.hb_Cluster->state = HB_NSTATE_TO_BE_MASTER대입이 이 장의 가장 중요한 한 줄이다. 진단은 §5.2의 전역 변수(hb_Is_master_node_isolated→ 하트비트 소실, 그 외의 경우hb_Master_host_name→ “서버 프로세스 문제”, 8장)를 읽는다.malloc실패 시에는hb_cluster_is_received_heartbeat_from_all(§5.3)로 조정된 지연을 선택하여 직접 FAILOVER로 강등한다. - Branch 4 — CALC_SCORE를 재큐(re-queue)하는 유일한 경로다. 슬레이브의
is_isolated는 저장되지만 여기서는 어떠한 동작도 트리거하지 않는다.
flowchart TD
A["lock; num_master = calc_score(); is_isolated = is_isolated()"] --> B{"state == REPLICA<br/>또는 hide_to_demote?"}
B -->|예| Z["goto calc_end"]
B -->|아니오| C{"state == MASTER<br/>이고 is_isolated?"}
C -->|예| C1["CHECK_PING 즉시 큐 삽입; return (재큐 없음)"]
C -->|아니오| D{"num_master > 1<br/>이고 내가 MASTER<br/>이고 master.prio != my.prio?"}
D -->|예| D1["FAILBACK 큐 삽입; return"]
D -->|아니오| E{"state == SLAVE<br/>이고 master.prio == my.prio?"}
E -->|예| E1["state = TO_BE_MASTER; request_heartbeat_to_all"]
E1 --> E2{"malloc job_arg 성공?"}
E2 -->|예| E3["CHECK_PING +100ms 큐 삽입; return"]
E2 -->|아니오| E4["100ms 대기; received_from_all로 대기 시간 선택; FAILOVER 큐 삽입; return"]
E -->|아니오| Z
Z --> ZZ["calc_end: unlock; CALC_SCORE 재큐; return"]
Figure 5-2. hb_cluster_job_calc_score: 점수 결과에서 단 하나의 전이 잡 큐 삽입까지의 전체 분기 트리.
5.5 챕터 요약 — 핵심 정리
섹션 제목: “5.5 챕터 요약 — 핵심 정리”- 선출은 signed
short최솟값 탐색이다.score = priority | HB_NODE_SCORE_*.0x8000을 OR하면 마스터의 점수가 음수가 되어score < min_score에서 승리한다.score를 unsigned로 선언하면 HA 선출 결과 전체가 뒤집힌다. hb_cluster_calc_score는 오래된 피어를 UNKNOWN으로 만료시킨다. 두 갈래 검사(heartbeat_gap > ha_max_heartbeat_gap또는 elapsed> ha_calc_score_interval,HB_IS_INITIALIZED_TIME가드로 한 번도 들어본 적 없는 피어 보호)를 거친 뒤, 단일 패스 argmin으로hb_Cluster->master를 결정하고num_master를 집계한다.num_master는0x8000계열 노드만 센다 (score < (short)0xF000). 따라서 진행 중인 TO_BE_MASTER 승격은 포함되지 않아 스플릿 브레인 검출기가 정당한 페일오버에 오발동하지 않는다.- TO_BE_SLAVE는 마스터 비트를 공유하고, TO_BE_MASTER는
0xF000을 사용한다. 강등 중인 전(前) 마스터는 여전히 슬레이브를 이기고, 승격 중인 슬레이브는 여전히 진짜 마스터에게 진다. hb_cluster_job_calc_score는 이 노드 자신의 상태를 기준으로 최대 하나의 액션을 발행한다. 안정 상태 낙하 분기만이 CALC_SCORE를 재무장하므로, 주기 루프와 전이 파이프라인이 이중 예약되는 일은 없다.- 우선순위 동일 == 승자 판별식, 우선순위 불일치 == 패자 판별식. 전역 argmin 승자의 우선순위가 내 것과 같다는 것은 그 항목이 곧 내 항목임을 의미한다. 우선순위는 설정된 순위를 인코딩하며(낮을수록 순위 높음), 단절은 MASTER에게만 액션을 강제한다.
Chapter 6: 페일오버와 핑 위트니스 체크
섹션 제목: “Chapter 6: 페일오버와 핑 위트니스 체크”이 챕터는 하나의 질문에 답한다: 로컬 선거(Chapter 5)에서 승리한 슬레이브가 실제로 어떻게 마스터가 되며, 마스터가 끊긴 링크 너머에 살아 있을 때 파티션된 슬레이브가 승격하지 못하도록 막는 메커니즘은 무엇인가?
동반 고수준 문서(cubrid-heartbeat.md, “Failover and split-brain avoidance”)는 쿼럼 없는 2-노드 HA 클러스터에 외부 위트니스가 왜 필요한지를 설명한다. 이 챕터는 코드 경로를 추적한다. Chapter 5의 점수 모델(hb_cluster_calc_score, priority/master 필드)과 Chapter 3의 잡 큐는 전제로 삼으며 재유도하지 않는다.
승격은 세 가지 상태를 거치는 이동이다 — SLAVE → TO_BE_MASTER → MASTER — 서로 다른 두 시점에 독립적인 두 가지 검사로 보호된다:
- 핑 위트니스 체크 (
hb_cluster_job_check_ping): 커밋 전, 외부 호스트에 “아직 나한테 닿을 수 있냐?”고 묻는다. 응답한 위트니스가 없으면 이 노드가 고립된 쪽이므로SLAVE로 복귀·중단한다. - 페일오버 재확인 (
hb_cluster_job_failover): 의도적인 대기 후, 점수를 한 번 더 재계산한다. 그사이 느리지만 살아 있는 마스터가 자신을 재주장했다면SLAVE로 복귀·중단한다.
이 두 가지 가드는 직교하는 장애를 방어한다 — 자기-파티션된 슬레이브 vs. 느리지만 살아 있는 마스터. 따라서 잘못된 승격이 일어나려면 두 신호가 동시에 실패해야 한다: 핑 체크가 중단하지 않아야 하고(위트니스가 도달 가능했거나, 위트니스 없음/판정 불가 케이스에서 고립 증거를 제공하지 않았거나), 그리고 마스터가 전체 대기 구간 동안 침묵을 유지해야 한다. 아래의 위트니스 없는 경로는 문서화된 가용성 우선·안전성 타협 탈출구다.
stateDiagram-v2
[*] --> SLAVE
SLAVE --> TO_BE_MASTER : calc_score: master.priority == myself.priority \n CHECK_PING을 100ms 후 큐에 추가
TO_BE_MASTER --> SLAVE : CHECK_PING 호스트 응답, 도달 불가 \n 고립 양성 증거, 중단
TO_BE_MASTER --> TO_BE_MASTER : CHECK_PING 여전히 모호 \n HB_MAX_PING_CHECK까지 재시도
TO_BE_MASTER --> WAIT : CHECK_PING 통과 또는 판정 불가 \n failover_wait_time 후 FAILOVER 큐에 추가
WAIT --> MASTER : FAILOVER 재확인에서도 최고 우선순위 \n 두 플레인 모두 MASTER로 전환, CHANGE_MODE 큐에 추가
WAIT --> SLAVE : FAILOVER 새 마스터 출현 \n 중단
MASTER --> [*]
Figure 6-1. 승격 상태 이동. WAIT는 저장된 상태가 아니다 — 큐에 추가된 FAILOVER 잡이 대기 중인 구간이며, 이 구간 내내 hb_Cluster->state는 TO_BE_MASTER를 유지한다.
이 이동은 hb_cluster_job_calc_score(Chapter 5)에서 진입된다: SLAVE가 master->priority == myself->priority를 발견하면(가장 높은 우선순위의 생존자가 됐을 때), hb_Cluster->state = HB_NSTATE_TO_BE_MASTER로 설정하고, ping_check_count를 0으로 초기화한 뒤, 100 ms 후 HB_CJOB_CHECK_PING을 큐에 추가한다 — 이 챕터가 시작되는 지점이다.
6.1 위트니스 체크: hb_cluster_job_check_ping 분기별 해설
섹션 제목: “6.1 위트니스 체크: hb_cluster_job_check_ping 분기별 해설”이 함수는 반대 목적을 가진 두 호출자가 공유한다: 고립된 마스터는 페일백(권한 반납)을 결정하고, to-be-master 슬레이브는 페일오버를 결정한다. hb_Cluster->state가 모든 분기에서 이 둘을 구분한다. 준비: clst_arg는 재시도 전반에 걸쳐 ping_check_count를 전달한다; ping_try_count는 명확한 판정을 내린 호스트의 수를 센다; ping_success는 도달 가능한 호스트가 하나라도 있었는지를 기록한다.
// hb_cluster_job_check_ping -- src/executables/master_heartbeat.cHB_CLUSTER_JOB_ARG *clst_arg = (arg) ? &(arg->cluster_job_arg) : NULL;if (clst_arg == NULL || hb_Cluster->num_ping_hosts == 0 || hb_Cluster->is_ping_check_enabled == false) { /* If Ping Host is either empty or marked invalid, MASTER->MASTER, SLAVE->MASTER. */ if (hb_Cluster->state == HB_NSTATE_MASTER) goto ping_check_cancel; /* <- master, no usable witness: keep mastership */ /* SLAVE: no goto, no else-block; skip past the whole if/else to the decision tail */ }분기 A — 유효한 위트니스 없음 (num_ping_hosts == 0 또는 is_ping_check_enabled == false). 물어볼 호스트가 없으면 클러스터는 “마스터가 죽었다”와 “내가 파티션됐다”를 구분할 수 없으며, 승격 방향으로 해소된다: MASTER는 goto ping_check_cancel로 마스터 역할을 유지하고, to-be-master SLAVE는 goto도 else 블록도 타지 않아 — 핑 I/O와 재시도 루프 전체를 건너뛰고 — 결정 꼬리 부분으로 직행해 페일오버를 진행한다.
INVARIANT — 위트니스 없는 상태에서 클러스터는 파티션 안전성보다 가용성을 우선한다. 소스 주석은 노골적이다: “It may cause split-brain problem.” 낡은 마스터와 승격 중인 슬레이브 모두 마스터 역할을 유지/획득하므로 파티션은 두 마스터를 낳을 수 있다. 하지만 위트니스 없는 2-노드 클러스터에는 다른 타이-브레이커가 없고, 사용 불가능한 클러스터가 일시적인 스플릿-브레인보다 더 나쁘다고 판단한다(이후 Chapter 5의
calc_score스플릿-브레인 분기와FAILBACK으로 조정된다).
분기 B — 위트니스 구성·활성화됨. else 블록은 hb_Cluster->ping_hosts의 모든 호스트에 핑(ICMP hb_check_ping 또는 TCP hb_check_tcp_ping)을 보낸 뒤 상태에 따라 분기한다:
// hb_cluster_job_check_ping -- src/executables/master_heartbeat.cfor (ping_host = hb_Cluster->ping_hosts; ping_host; ping_host = ping_host->next) { // ... ping_result = hb_check_ping(...) or hb_check_tcp_ping(...) ... ping_host->ping_result = ping_result; if (ping_result == HB_PING_SUCCESS) { ping_try_count++; ping_success = true; break; } /* <- one reachable witness is enough */ else if (ping_result == HB_PING_FAILURE) { ping_try_count++; } /* <- definite unreachable; counts */ /* HB_PING_USELESS_HOST / SYS_ERR / UNKNOWN: counted neither way, skip */ }HB_PING_SUCCESS/HB_PING_FAILURE만 ping_try_count를 증가시킨다. 잘못 구성됐거나 오류가 발생했거나 알 수 없는 호스트는 증거를 제공하지 않으므로, ping_try_count == 0은 “유효한 응답 없음”을 뜻하며 “모든 호스트 도달 불가”와는 다르다. 판정은 거울상 조건으로 goto ping_check_cancel한다: 마스터는 ping_try_count == 0 || ping_success == true일 때(무언가에 도달했거나 아무것도 알 수 없을 때 — 마스터 유지) 페일백을 취소하고; 슬레이브는 고립의 양성 증거가 있는 ping_try_count > 0 && ping_success == false일 때만(호스트가 응답했지만 이 노드에서 아무도 도달 불가 — 위트니스는 살아 있지만 이 노드가 닿지 못함) 페일오버를 취소한다.
분기 C — 재시도 루프. 어느 취소도 발동되지 않았다면 상황이 여전히 모호하다. 카운트를 올리고 잡을 즉시 재큐한다:
// hb_cluster_job_check_ping -- src/executables/master_heartbeat.cif ((++clst_arg->ping_check_count) < HB_MAX_PING_CHECK) /* HB_MAX_PING_CHECK == 3 */ { pthread_mutex_unlock (&hb_Cluster->lock); error = hb_cluster_job_queue (HB_CJOB_CHECK_PING, arg, HB_JOB_TIMER_IMMEDIATELY); return; /* <- same arg reused; ping_check_count persists across requeue */ }INVARIANT — 결정이 강제되기 전 최대
HB_MAX_PING_CHECK(3)회의 핑 라운드.ping_check_count는 힙에 할당된HB_JOB_ARG에 있으며(호출자가 0으로 초기화), 오직 여기서만 증가한다. 동일한arg가 재큐를 거쳐도 해제되지 않고 전달되므로 카운트는 리셋되지 않고 누적되며, 루프는 유계된다.
분기 D — 결정 꼬리 (루프 소진, 또는 슬레이브가 분기 A에서 직행). ping_check_count가 3에 도달했거나 슬레이브가 위트니스 없는 분기 A에서 직접 왔을 때, 노드는 결정을 내리고 arg를 해제한다:
// hb_cluster_job_check_ping -- src/executables/master_heartbeat.cif (hb_Cluster->state == HB_NSTATE_MASTER) error = hb_cluster_job_queue (HB_CJOB_FAILBACK, NULL, HB_JOB_TIMER_IMMEDIATELY); /* master -> Ch 7 */else { failover_wait_time = hb_cluster_is_received_heartbeat_from_all () ? HB_JOB_TIMER_WAIT_500_MILLISECOND /* all peers seen */ : prm_get_integer_value (PRM_ID_HA_FAILOVER_WAIT_TIME_IN_MSECS); /* a node silent: wait longer */ error = hb_cluster_job_queue (HB_CJOB_FAILOVER, NULL, failover_wait_time); }if (arg) free_and_init (arg); /* <- terminal path frees the carried arg */마스터는 FAILBACK(Chapter 7)으로 위임하고, 슬레이브는 FAILOVER를 지연 큐에 추가한다. 아직 침묵 중인 노드가 있을 때(hb_cluster_is_received_heartbeat_from_all == false) 더 길게 기다리는 것이 느린-마스터 방어의 전반부다 — 느리지만 살아 있는 마스터가 한 번 더 하트비트를 보낼 시간을 준다. 6.3의 재확인이 후반부다.
분기 E — ping_check_cancel 레이블. 위의 goto들이 도달하는 지점. 동일한 상태/ping_try_count 구분에 따른 진단 로그를 남기며, 결정적으로 슬레이브 분기만 상태를 변경한다 — hb_Cluster->state = HB_NSTATE_SLAVE로 진행 중인 TO_BE_MASTER를 되돌려 페일오버를 중단하고, 마스터는 자신의 상태를 그대로 둔다. 양쪽 모두 CALC_SCORE 사이클을 재무장하고 free_and_init (arg)를 호출한다.
6.2 위트니스 신뢰성 유지: hb_cluster_check_valid_ping_server와 그 잡
섹션 제목: “6.2 위트니스 신뢰성 유지: hb_cluster_check_valid_ping_server와 그 잡”6.1의 체크는 is_ping_check_enabled를 신뢰하며, 이 플래그는 느린 백그라운드 스윕으로 유지되어 죽은 위트니스가 페일오버를 막지 못하게 한다. hb_cluster_check_valid_ping_server는 순수 프로브 함수다:
// hb_cluster_check_valid_ping_server -- src/executables/master_heartbeat.cif (hb_Cluster->num_ping_hosts == 0) return true; /* <- "no ping host configured" counts as valid (see NOTE in source) */for (ping_host = hb_Cluster->ping_hosts; ping_host; ping_host = ping_host->next) // ... ping_host->ping_result = hb_check_ping/tcp_ping(...); if SUCCESS -> valid_ping_host_exists = true ...return valid_ping_host_exists; /* <- no break in the loop: pings ALL hosts to refresh every ping_result */6.1과 달리 첫 번째 성공 시 break하지 않는다 — 진단 덤프를 위해 각 ping_host->ping_result를 최신 상태로 찍어두려고 모든 호스트에 핑을 보내며, 하나라도 도달 가능하면(그리고 소스의 NOTE에 따르면 아무것도 구성되지 않았을 때도) true를 반환한다. 잡 래퍼 hb_cluster_job_check_valid_ping_server는 is_ping_check_enabled가 토글되는 유일한 장소다:
// hb_cluster_job_check_valid_ping_server -- src/executables/master_heartbeat.cint check_interval = HB_DEFAULT_CHECK_VALID_PING_SERVER_INTERVAL_IN_MSECS; /* 1 hour */if (hb_Cluster->num_ping_hosts == 0) goto check_end; /* <- nothing to validate; reschedule at the 1h default */valid_ping_host_exists = hb_cluster_check_valid_ping_server ();if (valid_ping_host_exists == false && hb_cluster_is_isolated () == false) { check_interval = HB_TEMP_CHECK_VALID_PING_SERVER_INTERVAL_IN_MSECS; /* 5 min, faster recheck */ hb_Cluster->is_ping_check_enabled = false; /* <- disable: witnesses unreachable but NOT isolated */ }else if (valid_ping_host_exists == true) { hb_Cluster->is_ping_check_enabled = true; /* <- re-enable when a witness comes back */ }check_end: pthread_mutex_unlock (&hb_Cluster->lock); error = hb_cluster_job_queue (HB_CJOB_CHECK_VALID_PING_SERVER, NULL, check_interval);비활성화 조건은 정밀하다: valid_ping_host_exists == false 그리고 hb_cluster_is_isolated() == false — 클러스터 하트비트는 여전히 흐르는데 위트니스에만 도달할 수 없다는 뜻이므로, 위트니스 자체가 고장났지 네트워크가 고장난 게 아니다. 플래그를 비활성화하면 6.1이 분기 A(승격)로 진입하게 되어, 고장난 위트니스가 합법적인 페일오버를 잘못 중단시키는 상황을 막는다. 주기: 비활성화 중에는 5분, 위트니스가 돌아오면 1시간. 이 잡은 플래그 토글과 재스케줄링만 수행한다.
INVARIANT — 런타임에
is_ping_check_enabled는hb_Cluster->lock하에서hb_cluster_job_check_valid_ping_server에 의해서만 토글된다. (유일한 다른 쓰기는 잡 루프 실행 전hb_cluster_initialize에서 이루어지는 일회성= true초기화뿐이다. config/info 경로는 이 플래그를 직접 쓰지 않으며, 불일치를 감지했을 때 이 잡을 즉시 실행되도록 재정렬할 뿐이다.) 모든 읽기 측(6.1)도 동일한 락을 취득하므로 플래그가 뒤집히는 도중에 읽히지 않는다. 느린 스윕과 빠른 결정 경로를 분리함으로써 일시적인 핑 미스가 죽은 위트니스로 오인되지 않도록 한다.
6.3 승격 확정: hb_cluster_job_failover
섹션 제목: “6.3 승격 확정: hb_cluster_job_failover”FAILOVER는 분기 D의 지연 후에만 실행된다. 첫 번째 동작은 대기 중에 도착한 하트비트를 대상으로 새로운 hb_cluster_calc_score를 수행하는 것이다:
// hb_cluster_job_failover -- src/executables/master_heartbeat.crv = pthread_mutex_lock (&hb_Cluster->lock);num_master = hb_cluster_calc_score (); /* <- SECOND recompute, after the wait window */if (hb_Cluster->master && hb_Cluster->myself && hb_Cluster->master->priority == hb_Cluster->myself->priority) { /* "[Failover] [Success] ... promoted to master" */ hb_Cluster->state = HB_NSTATE_MASTER; hb_Resource->state = HB_NSTATE_MASTER; /* <- BOTH planes flip together */ error = hb_resource_job_set_expire_and_reorder (HB_RJOB_CHANGE_MODE, HB_JOB_TIMER_IMMEDIATELY); }else /* "[Failover] [Cancelled] New master ... found" */ { hb_Cluster->state = HB_NSTATE_SLAVE; /* <- abort: someone else reasserted mastership */ }확정 분기는 대기와 두 번째 재계산 후에도 master->priority == myself->priority가 성립할 때 발동한다. hb_Cluster->state(하트비트 플레인)와 hb_Resource->state(리소스 플레인 — 감시 중인 CUBRID 프로세스, Chapter 9) 모두를 HB_NSTATE_MASTER로 전환한 뒤, hb_resource_job_set_expire_and_reorder로 HB_RJOB_CHANGE_MODE를 즉시 큐에 추가한다. 그 잡이 서버/복제 프로세스를 활성 마스터 모드로 전환하며(Chapter 9/10), 그 잡이 없으면 상태 필드는 장식에 불과하고 프로세스는 레플리카로 남는다.
INVARIANT — 클러스터와 리소스 플레인은
hb_Cluster->lock하에서CHANGE_MODE가 동일 임계 구역 내에 큐에 추가된 채 원자적으로 MASTER로 전환된다. 두 할당과 큐 추가 모두 언락 이전에 이루어지므로,hb_Cluster->state == MASTER이면서hb_Resource->state가 여전히 SLAVE인 상태를 관찰자가 보는 일은 없다 — 프로세스를 승격시키지 않고 와이어에서 마스터십을 주장하는 절반만 승격된 유령 노드가 생기지 않는다.
중단 분기는 등호가 더 이상 성립하지 않을 때 발동한다 — 이 슬레이브가 기다리는 동안 단순히 느렸던 마스터가 하트비트를 다시 보내 재계산에서 재선출되었으므로, hb_cluster_calc_score가 hb_Cluster->master를 다른 노드로 지목한다. 노드는 HB_NSTATE_SLAVE로 복귀하며, 리소스 플레인을 건드리지 않고, CHANGE_MODE를 큐에 추가하지 않는다 — 로컬 프로세스는 결코 승격되지 않는다.
두 분기 어느 쪽 이후든, 함수는 노드/핑-호스트 정보를 로그로 남기고, hb_cluster_request_heartbeat_to_all로 새 상태를 재브로드캐스트하고, 언락한 뒤, CALC_SCORE 사이클을 재무장한다(Figure 6-1의 WAIT --> MASTER / WAIT --> SLAVE 엣지).
6.4 챕터 요약 — 핵심 정리
섹션 제목: “6.4 챕터 요약 — 핵심 정리”- 승격은 세 가지 상태를 거치는 이동이다
SLAVE→TO_BE_MASTER→MASTER.hb_cluster_job_calc_score(Ch 5)에서 시작되어hb_cluster_job_failover에서 완료된다.TO_BE_MASTER는 검사가 실행되는 동안 유지되는 게이트다. hb_cluster_job_check_ping은 마스터(페일백)와 슬레이브(페일오버)가 공유하며, 모든 분기에서hb_Cluster->state가 이 둘을 구분한다. 마스터는 무언가에라도 도달하면 취소하고, 슬레이브는 고립의 양성 증거(ping_try_count > 0 && !ping_success)가 있을 때만 중단한다.- 재시도 루프는
HB_MAX_PING_CHECK(3)에 의해 유계된다. 힙에 할당된HB_JOB_ARG의ping_check_count가 재큐를 거쳐도 변경 없이 전달되며, 종단 경로에서만 해제된다. - 위트니스 없는 경로(
num_ping_hosts == 0또는!is_ping_check_enabled)는 파티션 안전성보다 가용성을 우선한다 — 마스터는 역할을 유지하고, 슬레이브는 승격한다 — 스플릿-브레인 가능성을 수용한다. is_ping_check_enabled는hb_cluster_job_check_valid_ping_server에 의해서만 토글된다(정상 시 1시간, 비활성화 중 5분). 위트니스에 도달할 수 없지만 클러스터 하트비트가 여전히 흐를 때 정확히 위트니스 체크를 비활성화한다.hb_cluster_job_failover는failover_wait_time이후 점수를 두 번째로 재계산한다. 확정 분기는hb_Cluster->state와hb_Resource->state를 락 하에서 원자적으로 MASTER로 전환하며HB_RJOB_CHANGE_MODE를 큐에 추가하고, 중단 분기는 느린 마스터가 자신을 재주장했을 때 SLAVE로 복귀한다. 두 가드는 직교하므로, 잘못된 승격이 일어나려면 둘 다 실패해야 한다.
Chapter 7: 피어가 마스터 자리를 되찾을 때의 페일백
섹션 제목: “Chapter 7: 피어가 마스터 자리를 되찾을 때의 페일백”스플릿 브레인이 치유되면, 두 마스터 중 정확히 하나가 물러나야 한다. 5장과 6장은 누가 결정하는지를 설명하고, 이 장은 기계적인 물음에 답한다: 노드가 스플릿 브레인 패자임을 알게 된 순간, MASTER에서 SLAVE로 어떻게 스스로를 되돌리며, 자신이 호스팅하던 cub_server 프로세스들에는 어떤 일이 일어나는가? 이 전환을 담당하는 것이 hb_cluster_job_failback이며, 의도적으로 무조건적(unconditional)으로 설계되어 있다 — 이 잡이 실행될 시점에는 이미 결정이 확정되어 있기 때문이다. 개념적 배경은 컴패니언 문서의 “페일오버, 페일백, 스플릿 브레인” 절을 참고하라.
7.1 페일백이 결정되는 위치, 그리고 잡이 무조건적인 이유
섹션 제목: “7.1 페일백이 결정되는 위치, 그리고 잡이 무조건적인 이유”hb_cluster_job_failback에는 “정말 강등해야 하나?” 같은 판단 로직이 없다 — 결정은 전적으로 호출자 쪽에 있다. 이 잡의 계약은 나는 패배한 마스터다; 정리하라이며, 그래서 본문이 이전 상태를 검사하는 대신 새 상태를 쓰는 것으로 시작한다.
불변 조건 — 페일백은 마스터 패자 전용 정리 경로다. hb_cluster_job_failback은 오직 호출자가 해당 노드를 패자로 판정했을 때, 그 노드가 HB_NSTATE_MASTER를 보유하고 있는 경우에만 실행된다. 강제는 구조적으로 이루어진다: 모든 hb_cluster_job_queue (HB_CJOB_FAILBACK, ...) 호출 지점은 hb_Cluster->myself->state == HB_NSTATE_MASTER 검사로 보호된다. 두 호출 지점은 각각 CHECK_PING 목격자 패자 분기(6장)와, (num_master > 1) && hb_Cluster->myself->state == HB_NSTATE_MASTER && hb_Cluster->master->priority != hb_Cluster->myself->priority 조건(이 노드가 우선순위가 다른 또 다른 마스터를 발견한 경우)으로 보호된 스플릿 브레인 감지 코드(약 867번 줄) 근방으로, 894번 줄에서 FAILBACK을 큐에 넣는다. 불변 조건이 깨질 경우 — 슬레이브에 페일백이 큐잉되면 — 함수가 해당 슬레이브의 cub_server 프로세스에 SIGTERM을 보내는 역할 변경도 없는 가용성 구멍이 생긴다.
7.2 상태 쓰기: 중간 상태 없이 곧바로 SLAVE
섹션 제목: “7.2 상태 쓰기: 중간 상태 없이 곧바로 SLAVE”페일백이 가장 먼저 하는 일은 클러스터 락 아래에서 노드 상태를 SLAVE로 직접 설정하고, 자신의 노드 엔트리에도 반영하며, 이를 공표하는 것이다:
// hb_cluster_job_failback -- src/executables/master_heartbeat.c pthread_mutex_lock (&hb_Cluster->lock);
hb_Cluster->state = HB_NSTATE_SLAVE; /* <- TO_BE_SLAVE 단계 없음 */ hb_Cluster->myself->state = hb_Cluster->state; /* keep self-node mirror in sync */
hb_cluster_request_heartbeat_to_all (); /* announce SLAVE on the wire now */없는 것을 주목하라: HB_NSTATE_TO_BE_SLAVE를 거치는 전이가 없다. 페일오버(6장)와 대조해 보면, 슬레이브는 HB_NSTATE_TO_BE_MASTER를 거쳐 승격한다 — 리소스가 가동되는 동안 노드를 확정-but-미활성 상태로 유지하는 중간 단계다. 페일백에는 대칭적인 중간 상태가 없는데, 강등은 파괴적이고 즉각적이기 때문이다. hb_Cluster->myself->state가 hb_Cluster->state와 함께 미러링되는 이유는, 노드의 nodes 리스트 엔트리가 스칼라 역할 필드와 일치해야 하기 때문이다; 5장의 점수 계산이 노드 엔트리를 읽으므로, myself가 동기화되지 않으면 CALC_SCORE가 오염된다.
공표 이후, 페일백은 제어 흐름에 전혀 영향을 주지 않는 진단 로그를 남긴다: ER_HB_NODE_EVENT 성공 메시지(HA_FAILBACK_SUCCESS_STRING), 노드 정보 덤프, 그리고 hb_Cluster->num_ping_hosts > 0인 경우에만 핑 호스트 정보 줄이 출력된다. 여기서 내부 분기는 PRM_ID_HA_PING_HOSTS가 설정되어 있으면 hb_help_sprint_ping_host_info를, 아니면 hb_help_sprint_tcp_ping_host_info를 선택한다. 두 경로 모두 로그 문자열만 포맷할 뿐이며, 이것이 §7.5의 킬 모드 분기를 제외하고 함수 내의 유일한 if/else 쌍이다. 상태 열거형 HB_NODE_STATE_TYPE(src/connection/heartbeat.h, 1장 참조)에는 TO_BE_SLAVE=3이 정의되어 있지만, 코드 어디서도 실제로 대입되지 않는다.
불변 조건 — HB_NSTATE_TO_BE_SLAVE는 결코 대입되지 않는다; 강등에는 워밍업 상태가 없다. 소스 트리 전체를 검색해도 state = HB_NSTATE_TO_BE_SLAVE 대입은 존재하지 않는다 — hb_Cluster->state에도, hb_Resource->state에도, 나가는 하트비트에 담기는 형태로도 없다. 실제로 쓰이는 역할 값은 MASTER, TO_BE_MASTER, SLAVE, REPLICA, UNKNOWN뿐이다. 두 가지 결과가 따른다. 첫째, hb_resource_job_change_mode는 hb_Resource->state == HB_NSTATE_TO_BE_SLAVE 조건에서 change-mode 브로드캐스트를 보내도록 되어 있지만 — 해당 필드가 TO_BE_SLAVE로 설정되는 일이 없으므로 — 그 분기는 사실상 죽은 코드다: change-mode 요청은 MASTER 분기에서만 발동된다. 둘째, 하트비트 수신 경로가 피어의 HB_NODE_ENTRY.state를 TO_BE_SLAVE로 스탬프할 수는 있지만(node->state = hb_state, 피어의 역할을 언패킹한 후), 송신자가 자신의 hb_Cluster->state(§7.3)를 패킹하며 그 값이 TO_BE_SLAVE가 되는 일은 없으므로, 그 피어 미러 경로 역시 실질적으로 죽은 코드다. 열거값은 존재하지만 실행 코드는 결코 거기에 이르지 못한다. 페일백이 SLAVE로 곧장 점프하는 것은 전이 상태를 건너뛰는 게 아니라 — 강등 측에는 전이 상태 자체가 없기 때문이며, 전이 상태(TO_BE_MASTER)는 승격 측에만 존재한다.
7.3 강등 공표: hb_cluster_request_heartbeat_to_all
섹션 제목: “7.3 강등 공표: hb_cluster_request_heartbeat_to_all”클러스터 락을 해제하기 전, 페일백은 새로운 SLAVE 상태를 브로드캐스트하여 승리한 피어가 파티션이 해소되었음을 알 수 있게 한다:
// hb_cluster_request_heartbeat_to_all -- src/executables/master_heartbeat.c if (hb_Cluster == NULL) { MASTER_ER_LOG_DEBUG (ARG_FILE_LINE, "hb_Cluster is null. \n"); return; } /* pre-init / shutdown race */
for (node = hb_Cluster->nodes; node; node = node->next) { if (are_hostnames_equal (hb_Cluster->host_name, node->host_name)) continue; /* skip myself; don't heartbeat the loopback */
hb_cluster_send_heartbeat_req (node->host_name); node->heartbeat_gap++; /* optimistic gap bump, decremented on reply */ }각 분기: hb_Cluster == NULL 가드는 초기화/셧다운 경쟁 상황에서 null 역참조 대신 로그를 남기고 반환한다; are_hostnames_equal(...) 검사는 자기 자신 엔트리를 건너뛴다; 그 외의 경우 hb_cluster_send_heartbeat_req가 요청을 전송한다. 이 요청의 페이로드는 hb_cluster_send_heartbeat_internal이 구성하며, hb_Cluster->state(이제 SLAVE)를 or_pack_int로 패킹하고 sendto로 발송한다. node->heartbeat_gap++은 수신 경로(4장)가 응답 수신 시 감소시킬 미처리 요청을 기록한다. 와이어 페이로드가 로컬 hb_Cluster->state이므로, 피어는 이 노드로부터 TO_BE_SLAVE를 수신하는 일이 없다(§7.2). 이 노드를 MASTER로 추적하던 피어는 SLAVE로의 전환을 보고 경쟁을 멈춘다. 이 함수는 페일백 전용이 아닌, “현재 상태를 게시한다”는 범용 프리미티브다.
7.4 락 순서: 킬 루프 전에 클러스터 락을 해제
섹션 제목: “7.4 락 순서: 킬 루프 전에 클러스터 락을 해제”중요한 세부 사항: 페일백은 킬 작업을 위해 hb_Resource->lock을 획득하기 전에 hb_Cluster->lock을 해제한다.
// hb_cluster_job_failback (lock handoff) -- src/executables/master_heartbeat.c pthread_mutex_unlock (&hb_Cluster->lock); /* <- cluster lock dropped first */
pthread_mutex_lock (&hb_Resource->lock); /* then take the resource lock */ hb_Resource->state = HB_NSTATE_SLAVE; // ... walk hb_Resource->procs, collect server pids ...불변 조건 — 클러스터 락과 리소스 락은 킬을 걸쳐 cluster-then-resource 순서로 중첩되지 않는다. 클러스터 뮤텍스는 노드/상태 뷰를 보호하고, 리소스 뮤텍스는 프로세스 테이블을 보호한다. 이후의 hb_kill_process는 서버가 죽을 때까지 대기하며(헤더 주석에 최대 1분 블록킹을 경고한다), 그 대기 동안 hb_Cluster->lock을 보유하면 하트비트 경로와 점수 잡이 멈추어 실제 노드 장애처럼 보이는 갭을 인위적으로 만들게 된다. 클러스터 락을 먼저 해제함으로써, 하트비트 기계가 리소스 락 단독 아래의 teardown이 진행되는 동안에도 계속 동작할 수 있다.
7.5 프로세스 테이블 순회 및 서버 pid 수집
섹션 제목: “7.5 프로세스 테이블 순회 및 서버 pid 수집”리소스 락 아래에서 hb_Resource->state가 SLAVE가 되고, 이후 procs를 순회하며 모든 HB_PTYPE_SERVER pid를 수집한다:
// hb_cluster_job_failback (collect loop) -- src/executables/master_heartbeat.c proc = hb_Resource->procs; while (proc) { if (proc->type != HB_PTYPE_SERVER) { proc = proc->next; continue; } /* (1) non-server helpers untouched */
if (emergency_kill_enabled == false) { size = sizeof (pid_t) * (count + 1); pids = (pid_t *) realloc (pids, size); if (pids == NULL) /* (2) OOM -> switch to inline SIGKILL */ { MASTER_ER_SET (..., ER_OUT_OF_VIRTUAL_MEMORY, 1, size); emergency_kill_enabled = true; proc = hb_Resource->procs; /* restart the walk from head */ continue; } pids[count++] = proc->pid; /* (3) normal: buffer the pid */ } else { assert (proc->pid > 0); if (proc->pid > 0) kill (proc->pid, SIGKILL); /* (4) emergency: kill in place */ } proc = proc->next; } pthread_mutex_unlock (&hb_Resource->lock);네 개의 분기: (1) 서버가 아닌 헬퍼 타입(copy/apply-log)은 건너뛴다 — 오직 cub_server 인스턴스만 이 경로에서 강등된다, 헬퍼들은 별도의 change-mode 경로로 슬레이브 역할을 따른다; (2) realloc 실패 시 강등이 중단되지 않고(마스터 측 서버가 살아남는 상황을 방지), emergency_kill_enabled를 뒤집고 proc을 헤드로 리셋하여 재순회함으로써 모든 서버가 분기 (4)에 걸리게 한다; (3) 정상 경로는 pid를 버퍼에 담으며, 아직 시그널은 보내지 않는다; (4) pids를 사용할 수 없을 때는 각 서버에 SIGKILL을 즉석에서 전송하며, if (proc->pid > 0) 재검사가 pid가 0인 경우 kill(0, ...)(프로세스 그룹 시그널)이 되는 것을 막는다.
순회 이후 리소스 락이 해제되고, 정상적인 킬이 락 밖에서 실행된다: if (emergency_kill_enabled == false) hb_kill_process (pids, count);는 버퍼링된 pid에 SIGTERM-then-SIGKILL 시퀀스를 실행한다. 긴급 경로가 이미 모든 것을 인라인 SIGKILL로 처리했다면 이 단계는 건너뛴다. 이후 정리 작업과 CALC_SCORE 재큐잉이 따른다(§7.8). 전체 제어 흐름은 Figure 7-1에서 확인할 수 있다.
flowchart TD
C["hb_Cluster 락 아래:\nstate=SLAVE, myself=SLAVE,\n공표, 클러스터 락 해제"] --> F["hb_Resource 락 아래:\nhb_Resource->state = SLAVE"]
F --> G{"procs 순회:\nproc->type == SERVER?"}
G -- "아니오" --> H["proc = proc->next"]
H --> G
G -- "예, emergency off" --> I["realloc pids"]
I -- "ok" --> J["pids[count++] = pid"]
J --> H
I -- "NULL" --> K["emergency_kill_enabled = true\nproc = head, 재순회"]
K --> G
G -- "예, emergency on" --> L["kill(pid, SIGKILL) 인라인"]
L --> H
G -- "리스트 끝" --> M["hb_Resource 락 해제"]
M --> N{"emergency off?"}
N -- "예" --> O["hb_kill_process(pids, count)"]
N -- "아니오" --> P["건너뜀; 이미 SIGKILL됨"]
O --> Q["pids 해제, CALC_SCORE 큐잉,\narg 해제, 반환"]
P --> Q
7.6 hb_kill_process: SIGTERM, 대기, SIGKILL로 에스컬레이션
섹션 제목: “7.6 hb_kill_process: SIGTERM, 대기, SIGKILL로 에스컬레이션”hb_kill_process는 부드러운-후-강제 방식의 킬러다: 3초 간격으로 최대 20회까지 SIGTERM을 재시도한 후(약 1분), 살아남은 프로세스에 SIGKILL을 보낸다:
// hb_kill_process -- src/executables/master_heartbeat.c int signum = SIGTERM; /* max_retries = 20, wait = 3s */ for (i = 0; i < max_retries; i++) { finished = true; for (j = 0; j < count; j++) { if (pids[j] > 0) { error = kill (pids[j], signum); if (error && errno == ESRCH) /* process already gone */ pids[j] = 0; /* mark slot dead, stop signalling it */ else finished = false; /* still alive (or signal landed) */ } } if (finished == true) return; /* all reaped -> done early */ signum = 0; /* <- later passes only probe, don't re-TERM */ SLEEP_MILISEC (wait_time_in_secs, 0); }
for (j = 0; j < count; j++) /* timeout -> force kill survivors */ if (pids[j] > 0) kill (pids[j], SIGKILL);분기 완전 해석: pids[j] <= 0인 슬롯은 건너뛴다; kill이 ESRCH로 실패하면 해당 슬롯을 영구적으로 0으로 만든다; 그 외의 결과(성공 또는 ESRCH 이외의 오류)는 finished를 클리어하여 루프가 잠들고 재확인하게 한다; finished가 내부 패스 전체를 살아남으면 함수가 즉시 반환한다. 핵심은 첫 번째 패스 이후 signum = 0이 되어 이후 모든 패스가 시그널 없는 kill(pid, 0) 존재 probe로 바뀐다는 점이다 — 즉 실제 SIGTERM은 정확히 한 번만 전달되며, 서버가 flush를 수행하는 동안 중복 신호를 받지 않는다. 20회 패스(약 60초) 후에도 살아있는 프로세스가 있다면 후속 루프가 SIGKILL로 처리한다.
불변 조건 — 마스터 측 cub_server는 페일백 이후 살아남지 못한다. hb_kill_process의 어떤 종료 경로도 pids[j] > 0인 프로세스를 남기지 않는다: 모든 서버는 SIGTERM 아래에서 종료되거나 후속 SIGKILL 스윕에 의해 제거된다. 마스터 모드 서버가 살아남으면 쓰기를 받아들이고 페일백이 닫으려던 스플릿 브레인을 다시 열 수 있다.
7.7 모드 변경이 아닌 킬-후-재시작을 하는 이유
섹션 제목: “7.7 모드 변경이 아닌 킬-후-재시작을 하는 이유”가장 놀라운 설계 선택은 페일백이 cub_server에 “너는 이제 슬레이브다”라고 알리는 대신 킬한다는 점이다. 이유는 설정 구조에 있다: CUBRID 서버는 HA 역할과 그에 맞는 인-프로세스 설정(log applier/copier 배선, 복제 방향, 버퍼 역할)을 시작 시에 로드하며, 실행 중인 마스터를 슬레이브로 안전하게 재타겟팅하는 라이브 경로가 없다.
따라서 페일백은 절반의 작업만 수행한다 — 오래된 마스터 모드 서버를 제거하는 것. 슬레이브 측 설정으로의 재시작은 리소스 측 죽음 핸들러가 담당한다: 킬된 cub_server가 연결을 끊으면, hb_cleanup_conn_and_start_process가 proc을 HB_PSTATE_DEAD로 표시하고 hb_resource_job_queue (HB_RJOB_PROC_START, ...)를 큐에 넣는다(9장). 그 시점에 hb_Resource->state는 이미 HB_NSTATE_SLAVE(§7.5)이므로, HB_RJOB_PROC_START가 슬레이브 측 HA 설정을 읽어 서버를 재fork/exec한다. 결과적으로 — 옛 마스터가 죽고, 새 슬레이브가 스폰되는 — 역할 변경은 프로세스 교체로 이루어지며, 내부 상태 변형이 아니다. (핸들러의 HB_RJOB_DEMOTE_START_SHUTDOWN을 거치는 안티-플랩 강등은 8장의 리소스 주도 경로다.)
7.8 CALC_SCORE 재큐잉과 정리
섹션 제목: “7.8 CALC_SCORE 재큐잉과 정리”마지막 동작(§7.5)이 루프를 닫는다: HB_CJOB_CALC_SCORE가 재큐잉되고, 잡의 arg가 해제된다(페일백은 NULL로 시작되었으므로 실질적으로 no-op이지만, 잡 ABI를 일관되게 유지한다). 페일백이 반환되면 노드는 완전히 강등된 슬레이브다: 클러스터 상태 SLAVE, 리소스 상태 SLAVE, 마스터 모드 서버 없음, 슬레이브 모드 서버 재스폰 중, 점수 잡 재가동. 승리한 피어는 SLAVE 하트비트를 수신하여 유일한 마스터가 되고, 스플릿 브레인이 닫힌다.
7.9 챕터 요약 — 핵심 정리
섹션 제목: “7.9 챕터 요약 — 핵심 정리”- 페일백은 계약상 무조건적이다. 두 호출 지점 — CHECK_PING 목격자 패자 분기(6장)와 894번 줄의
num_master > 1스플릿 브레인 감지 — 모두hb_Cluster->myself->state == HB_NSTATE_MASTER일 때만HB_CJOB_FAILBACK을 큐에 넣는다. 이 잡은 정리이지 결정이 아니다. - 강등은
SLAVE를 직접 쓴다 —TO_BE_SLAVE워밍업이 없다. 승격의TO_BE_MASTER단계와 달리, 강등은 즉각적이다.HB_NSTATE_TO_BE_SLAVE는 트리 어디에서도 대입되지 않으며, 이를 언급하는hb_resource_job_change_mode가드와 피어 미러 수신 경로는 모두 실질적으로 죽은 코드다. - 클러스터 락은 킬 루프 전에 해제된다.
hb_kill_process는 최대 1분간 블록킹하므로, 그 동안hb_Cluster->lock을 보유하면 하트비트가 멈추고 노드 장애처럼 보이는 갭이 인위적으로 발생한다. HB_PTYPE_SERVER프로세스만 킬된다. 순회는 copy/apply-log 헬퍼를 건너뛴다;realloc실패 시에는 인라인SIGKILL로 재순회하여 메모리 압박 아래서도 강등이 완료된다. 공표 이후의 핑 호스트 로깅 분기는 진단 전용이다.hb_kill_process는 SIGTERM을 SIGKILL로 에스컬레이션한다. 첫 번째 패스에서 SIGTERM을 보내고, 이후 패스는kill(pid, 0)probe만 수행하며, 약 60초 후 살아있는 프로세스에 SIGKILL을 전송한다 — 어떤 마스터 서버도 페일백을 살아남지 못한다.- 서버는 모드 변경이 아닌 킬-후-재시작된다. HA 역할은 시작 시 고정되므로, 안전한 재타겟팅은 프로세스 교체로 이루어진다: 페일백이 킬하고, 죽음 핸들러(
hb_cleanup_conn_and_start_process->HB_RJOB_PROC_START)가hb_Resource->state가SLAVE임을 확인하고 슬레이브 설정으로 재스폰한다. - 페일백은
CALC_SCORE를 재큐잉한다. 강등된 노드는 슬레이브로서 선거 주기에 재합류하고, SLAVE 하트비트를 받은 승리 피어가 유일한 마스터가 되어 스플릿 브레인이 닫힌다.
Chapter 8: 로컬 리소스 장애 시 역할 강등
섹션 제목: “Chapter 8: 로컬 리소스 장애 시 역할 강등”이 챕터는 하나의 질문에 답한다: 로컬 CUBRID 서버가 멈춘 상태에서 이 노드가 master일 때, 두 노드가 잠시 역할을 두고 충돌하지 않으면서 어떻게 자리를 양보해 피어가 인계받도록 하는가? 상위 수준의 동반 문서(cubrid-heartbeat.md, “Failover, failback, demote”)가 이유를 설명한다면, 여기서는 모든 분기를 추적한다.
이 경로는 두 개의 worker 컨텍스트에서 실행된다. detector(hb_thread_check_disk_failure)는 서버의 로그 EOF를 주기적으로 확인하고, 서버가 멈춘 것을 감지하면 리소스 역할을 SLAVE로 전환하고 teardown을 큐에 넣는다. teardown(hb_resource_job_demote_start_shutdown -> _confirm_shutdown)은 서버 프로세스를 종료한 뒤, 클러스터 역할 양보(hb_cluster_job_demote)에게 제어를 넘긴다. 클러스터 역할 양보는 UNKNOWN을 광고하고 SLAVE로 내려간 다음 피어가 master를 주장할 때까지 기다린다. 클러스터 절반 전체에서 hide_to_demote(챕터 1의 HB_CLUSTER)가 핵심 연결고리다: 이 플래그가 설정된 동안 노드는 heartbeat를 브로드캐스트하지도 않고 점수 계산에도 자신을 포함시키지 않으므로, 양보 창 동안 스스로를 다시 선출할 수 없다.
8.1 Detector: hb_thread_check_disk_failure
섹션 제목: “8.1 Detector: hb_thread_check_disk_failure”disk-failure 스레드는 hb_Resource->shutdown이 될 때까지 타이머 루프를 돈다. PRM_ID_HA_CHECK_DISK_FAILURE_INTERVAL_IN_SECS초마다 anchor, cluster, resource 락을 순서대로 잡고(job-queue 경로와의 데드락을 방지하기 위해 이 순서를 지킨다) 보호된 탐침을 실행한다.
// hb_thread_check_disk_failure -- src/executables/master_heartbeat.cif (interval > 0 && remaining_time_msecs <= 0) /* <- 0 disables the check entirely */ { /* ... lock anchor, cluster, resource ... */ if (hb_Cluster->is_isolated == false && hb_Resource->state == HB_NSTATE_MASTER) { /* <- only an attached, serving master demotes itself */ if (hb_resource_check_server_log_grow () == false) { MASTER_ER_SET (... HA_FAILBACK_DIAG_STRING " ... lost its role due to server process problem"); MASTER_ER_SET (... HA_FAILBACK_SUCCESS_STRING " ... successfully demoted to slave"); /* <- detector emits the demote diagnostics itself */ hb_disable_er_log (HB_NOLOG_DEMOTE_ON_DISK_FAIL, NULL); /* <- go quiet: logging may block on the bad disk */ hb_Resource->state = HB_NSTATE_SLAVE; /* ... unlock resource, cluster, anchor ... */ error = hb_resource_job_queue (HB_RJOB_DEMOTE_START_SHUTDOWN, NULL, HB_JOB_TIMER_IMMEDIATELY); continue; /* <- re-test guard; state is now SLAVE so probe won't re-fire */ } } if (hb_Resource->state == HB_NSTATE_MASTER) hb_resource_send_get_eof (); /* <- arm the NEXT interval's probe */ /* ... unlock all three ... */ remaining_time_msecs = interval * 1000; }SLEEP_MILISEC (0, HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS); /* 100 ms tick; decrement remaining when interval>0 */인라인 주석 이상으로 설명하면: interval <= 0이면 전체 본문을 건너뛴다. 격리된(isolated) 노드는 절대 강등하지 않는다 — 유일하게 서비스 중인 노드를 내리는 것은 wedge보다 더 나쁘며, 격리 상황은 failover 스레드의 관심사다(챕터 6). 제어는 두 번째 if로 떨어지고, 여전히 MASTER 상태인 노드는 다음 탐침을 준비한다.
불변 조건 — 탐침 판정과 강등 동작은 리소스 상태 변경과 원자적으로 묶인다. SLAVE로의 전환은 세 락을 모두 쥔 상태에서 이루어지며, 이후 락을 해제한다. continue는 상태가 이미 SLAVE인 채로 루프에 재진입하므로 send_get_eof 준비 단계를 건너뛴다. 락 안에서의 전환과 continue의 조합이 wedge당 정확히 하나의 teardown을 보장한다 — 동시 경로가 그렇지 않으면 두 번째 teardown을 큐에 넣을 수 있다.
8.2 EOF 탐침: 전송, 수신, 그리고 증가 판정
섹션 제목: “8.2 EOF 탐침: 전송, 수신, 그리고 증가 판정”wedge 테스트는 두 인터벌 간의 비교다: 각 인터벌마다 detector는 서버의 로그 EOF를 요청하고, 지난번 이후 도착한 값을 읽는다. 살아 있는 서버는 EOF를 전진시키지만, 멈춘 서버는 같은 LSA(Log Sequence Address)를 반복하거나 아예 응답하지 않는다. hb_resource_send_get_eof는 활성화된 모든 서버에 SERVER_GET_EOF를 팬아웃하면서, 각 서버의 응답 수신 여부를 미수신으로 표시한다.
// hb_resource_send_get_eof -- src/executables/master_heartbeat.cif (hb_Resource->state != HB_NSTATE_MASTER) return; /* <- only a master probes its servers */for (proc = hb_Resource->procs; proc; proc = proc->next) if (proc->state == HB_PSTATE_REGISTERED_AND_ACTIVE) { css_send_heartbeat_request (proc->conn, SERVER_GET_EOF); proc->is_curr_eof_received = false; /* <- set true only when a reply lands */ }hb_resource_receive_get_eof는 연결 스레드 콜백으로, 응답을 받아 LSA를 proc->curr_eof에 언팩한다.
// hb_resource_receive_get_eof -- src/executables/master_heartbeat.cif (css_receive_heartbeat_data (conn, reply, ...) != NO_ERRORS) return; /* <- transport error: curr_eof stays stale -> looks wedged *//* ... lock resource ... */proc = hb_return_proc_by_fd (conn->fd);if (proc == NULL) { /* unlock; return */ } /* <- reply for a process we no longer track */if (proc->state == HB_PSTATE_REGISTERED_AND_ACTIVE) { or_unpack_log_lsa (reply, &proc->curr_eof); proc->is_curr_eof_received = true; /* <- distinguishes "stale value" from "no answer" */ }hb_resource_check_server_log_grow는 detector가 호출하는 판정 함수로, 활성 상태이면서 hung 처리되지 않은 각 서버의 curr_eof와 prev_eof를 비교한다.
// hb_resource_check_server_log_grow -- src/executables/master_heartbeat.cfor (proc = hb_Resource->procs; proc; proc = proc->next) { if (proc->type != HB_PTYPE_SERVER || proc->state != HB_PSTATE_REGISTERED_AND_ACTIVE || proc->server_hang == true) continue; /* <- skip non-servers, non-active, already-flagged */ if (LSA_ISNULL (&proc->curr_eof) == true) continue; /* <- never received a first EOF; can't judge yet */ if (LSA_GT (&proc->curr_eof, &proc->prev_eof) == true) LSA_COPY (&proc->prev_eof, &proc->curr_eof); /* <- advanced: healthy, slide window forward */ else { proc->server_hang = true; /* <- did NOT advance: declare this server wedged */ dead_cnt++; if (proc->is_curr_eof_received) { /* syslog "no change to eof" */ } else { /* syslog "no response to eof request" */ } } }if (dead_cnt > 0) return false; /* <- at least one server wedged -> caller demotes */return true;is_curr_eof_received 하위 분기는 표면적인 처리에 불과하다 — syslog 메시지 문구만 바꿀 뿐, 판정 결과에는 영향을 주지 않는다. 하나라도 멈춘 서버가 있으면 dead_cnt > 0이 되어 false를 반환한다.
불변 조건 — prev_eof는 오직 앞으로만 이동하며, 절대 뒤로 가지 않는다. LSA_GT / LSA_COPY 쌍은 순서가 역전된 오래된 LSA를 “전진하지 않음”(최악의 경우 위양성 wedge)으로 처리하며, 절대 역행으로 처리하지 않는다. prev_eof가 뒤로 움직일 수 있다면, 잠시 지연된 서버가 기준선을 초기화하여 실제 미래 wedge를 은폐할 수 있다.
8.3 리소스 측 Teardown: 종료 시작 후 확인
섹션 제목: “8.3 리소스 측 Teardown: 종료 시작 후 확인”HB_RJOB_DEMOTE_START_SHUTDOWN은 hb_resource_job_demote_start_shutdown을 실행한다: 각 서버에 정지를 요청하고, 확인 작업을 스케줄링한다.
// hb_resource_job_demote_start_shutdown -- src/executables/master_heartbeat.chb_resource_demote_start_shutdown_server_proc (); /* under anchor+resource lock */job_arg = malloc (sizeof (HB_JOB_ARG));if (job_arg == NULL) { /* css_master_cleanup(SIGTERM); return; */ } /* <- can't continue */proc_arg = &(job_arg->resource_job_arg); /* <- confirm-job arg lives inside the fresh job_arg */proc_arg->retries = 0;proc_arg->max_retries = prm_get_integer_value (PRM_ID_HA_MAX_PROCESS_DEREG_CONFIRM);error = hb_resource_job_queue (HB_RJOB_DEMOTE_CONFIRM_SHUTDOWN, job_arg, /* interval */);hb_resource_demote_start_shutdown_server_proc는 서버별 종료를 발행한다 — 세 가지 분기로: 비활성은 건너뛰고, wedge된 서버는 SIGKILL, 나머지는 정상 종료 요청.
// hb_resource_demote_start_shutdown_server_proc -- src/executables/master_heartbeat.cfor (proc = hb_Resource->procs; proc; proc = proc->next) { if (proc->state != HB_PSTATE_REGISTERED_AND_TO_BE_ACTIVE && proc->state != HB_PSTATE_REGISTERED_AND_ACTIVE) continue; /* <- leave non-active processes alone */ if (proc->server_hang) { /* <- the wedged one: don't ask politely */ if (proc->pid > 0 && (kill (proc->pid, 0) == 0 || errno != ESRCH)) kill (proc->pid, SIGKILL); /* <- SIGKILL; can't drain. kill(,0) guards recycled PID */ continue; } sock_entq = css_return_entry_by_conn (proc->conn, &css_Master_socket_anchor); if (sock_entq != NULL && sock_entq->name != NULL) { css_process_start_shutdown (sock_entq, 0, buffer); /* <- graceful request */ proc->being_shutdown = true; /* <- mark so confirm knows we asked */ } }hb_resource_job_demote_confirm_shutdown은 재시도 상한을 두고 폴링한다.
// hb_resource_job_demote_confirm_shutdown -- src/executables/master_heartbeat.cif (++(proc_arg->retries) > proc_arg->max_retries) { hb_resource_demote_kill_server_proc (); /* <- patience exhausted: force-kill survivors */ goto demote_confirm_shutdown_end; }if (hb_resource_demote_confirm_shutdown_server_proc () == false) { /* <- some server still active: requeue and wait */ error = hb_resource_job_queue (HB_RJOB_DEMOTE_CONFIRM_SHUTDOWN, arg, /* ... interval ... */); return; /* <- NOTE: keeps arg; do not free on requeue */ }demote_confirm_shutdown_end: job_arg = malloc (sizeof (HB_JOB_ARG)); if (job_arg == NULL) { /* free arg; css_master_cleanup(SIGTERM); return; */ } clst_arg = &(job_arg->cluster_job_arg); /* <- the handoff arg is the FRESH job_arg, not the inbound arg */ clst_arg->ping_check_count = 0; clst_arg->retries = 0; /* <- retries==0 -> 8.4 takes its first-pass branch */ error = hb_cluster_job_queue (HB_CJOB_DEMOTE, job_arg, HB_JOB_TIMER_IMMEDIATELY); /* free arg */hb_resource_demote_confirm_shutdown_server_proc는 이미 SIGKILL된 hung 서버를 건너뛰고, 다른 활성/활성 예정 서버가 하나도 남지 않았을 때만 true를 반환한다. hb_resource_demote_kill_server_proc는 인내심이 바닥났을 때 살아남은 프로세스를 SIGKILL한다. 클러스터 핸드오프는 retries를 0으로 초기화한 새 job_arg로 만들어지므로, 8.4는 첫 번째 분기로 진입한다. 인바운드 arg는 큐 삽입 후 해제된다. 재큐 경로는 동일한 arg를 유지한 채 해제 없이 반환한다. 핸드오프 malloc이 실패하면 css_master_cleanup(SIGTERM)을 호출한다 — 안전하게 계속할 방법이 없다.
불변 조건 — arg는 정확히 한 번만 해제된다. 재큐 시에는 유지되어(큐에 그대로 전달되어) 해제되지 않고, 모든 종단 분기에서는 해제된다. 재큐 시 해제하면 use-after-free가 발생하고, 종단 분기에서 해제하지 않으면 메모리 누수가 된다.
8.4 클러스터 측 역할 양보: hb_cluster_job_demote
섹션 제목: “8.4 클러스터 측 역할 양보: hb_cluster_job_demote”챕터의 핵심: 이 작업은 노드가 master 역할을 떠나고 있음을 광고하고, SLAVE로 내려간 다음 hide_to_demote 플래그 아래 1초 간격으로 재큐하며 피어가 master를 주장하기를 기다린다.
// hb_cluster_job_demote -- src/executables/master_heartbeat.c/* lock cluster */if (clst_arg->retries == 0) /* <- first invocation only */ { assert (hb_Cluster->state == HB_NSTATE_MASTER); assert (hb_Resource->state == HB_NSTATE_SLAVE); /* <- resource already demoted in 8.1 */ hb_Cluster->state = HB_NSTATE_UNKNOWN; /* <- "I'm not master" so a peer steps up */ hb_cluster_request_heartbeat_to_all (); MASTER_ER_SET (... "Waiting for a new node to be elected as master"); }hb_Cluster->hide_to_demote = true; /* <- silence broadcast + score EVERY iteration */hb_Cluster->state = HB_NSTATE_SLAVE;hb_Cluster->myself->state = hb_Cluster->state;if (hb_Cluster->is_isolated == true || ++(clst_arg->retries) > HB_MAX_WAIT_FOR_NEW_MASTER) { /* <- give up: isolated OR waited 60 iterations */ MASTER_ER_SET (... "Failed to find a new master node and it changes its role back to master again"); hb_Cluster->hide_to_demote = false; /* <- re-enable participation; score may re-promote */ /* unlock; free arg; return */ }for (node = hb_Cluster->nodes; node; node = node->next) if (node->state == HB_NSTATE_MASTER) /* <- a peer took over */ { assert (node != hb_Cluster->myself); hb_Cluster->hide_to_demote = false; /* <- clean exit: we are now a real slave */ MASTER_ER_SET (... HA_FAILBACK_SUCCESS_STRING " ... successfully demoted to slave"); /* unlock; free arg; return */ }/* unlock */error = hb_cluster_job_queue (HB_CJOB_DEMOTE, arg, HB_JOB_TIMER_WAIT_A_SECOND); /* <- 1s, try again */분기 목록: 잘못된 작업(arg/clst_arg == NULL)은 로그를 남기고 반환한다. 첫 번째 패스(retries == 0)는 MASTER/SLAVE를 assert하고, UNKNOWN으로 설정한 다음 피어들이 경쟁하도록 브로드캐스트한다. 모든 패스에서 hide_to_demote = true로 설정하고 SLAVE를 강제한다 — 작업이 락을 놓고 1초씩 잠들기 때문에 매번 재단언한다. 이렇게 해야 동시에 실행되는 hb_cluster_job_calc_score가 여전히 점수를 매기는 SLAVE를 보지 않는다. 두 개의 종단 분기는 상호 배타적이며 둘 다 플래그를 해제하고 arg를 해제한다: 만료(is_isolated, 또는 ++retries > HB_MAX_WAIT_FOR_NEW_MASTER = 60)와 피어 발견(node->state == MASTER인 노드 존재). 그 외에는 HB_JOB_TIMER_WAIT_A_SECOND(1000 ms)로 재큐한다.
불변 조건 — hide_to_demote를 해제하는 두 지점 중 정확히 하나만 강등당 실행된다. 설정된 동안 이 플래그는 점수 참여(hb_cluster_job_calc_score, 챕터 5), 주기적 브로드캐스트 게이트(hb_cluster_job_heartbeat), 그리고 수신 경로의 split-brain 응답을 억제한다 — 노드는 자신의 선출 기계에서 보이지 않는 존재가 된다. 영구히 설정된 채로 두면 다시는 참여하지 않는 조용한 slave가 되고, 너무 일찍 해제되면 강등 도중 스스로를 다시 선출하여 이 경로가 방지하려는 바로 그 충돌을 만들 수 있다.
Figure 8-1 — 새 master 대기 루프.
stateDiagram-v2 [*] --> 첫_패스: retries==0 첫_패스 --> 대기_중: UNKNOWN 브로드캐스트\nhide_to_demote=true; SLAVE 대기_중 --> 만료: is_isolated OR retries GT 60 대기_중 --> 피어_발견: 어떤 node.state==MASTER 대기_중 --> 대기_중: 1s 재큐, retries++ 만료 --> [*]: 플래그 해제, 점수에 의해 master로 복귀 피어_발견 --> [*]: 플래그 해제, SLAVE로 안착
8.5 만료 분기와 미해결 문제
섹션 제목: “8.5 만료 분기와 미해결 문제”만료 분기는 hide_to_demote를 해제하고 SLAVE로 설정하는 것만 한다. 실제 재승격은 다음 hb_cluster_job_calc_score 패스에 맡긴다. 해당 패스는 플래그가 해제되고 피어 master도 없으므로 이 노드를 master로 선출한다.
미해결 문제 (유지보수자에게 표시). 만료 분기는 클러스터 역할을 master 방향으로 복원하지만, 리소스 측은 부활시키지 않는다: 8.3의 teardown이 이미 로컬 서버를 종료했으므로, 노드는 실행 중인 서버 없이 클러스터 master가 될 수 있다. 이것이 의도된 동작(운영자가 개입)인지 아니면 빠진 부분인지는 챕터 9의 프로세스 부활 로직에 달려 있으며, hb_cluster_job_demote만으로는 확인할 수 없다.
8.6 챕터 요약 — 핵심 정리
섹션 제목: “8.6 챕터 요약 — 핵심 정리”- 두 컨텍스트, 하나의 강등. disk-failure 스레드가 wedge를 감지하고 리소스 측을 tear down하면,
hb_cluster_job_demote가 노드를 역할에서 물러나게 한다 —HB_RJOB_DEMOTE_START_SHUTDOWN->_CONFIRM_SHUTDOWN->HB_CJOB_DEMOTE로 연결된다. - wedge 테스트는 단조 증가하는 EOF 비교다. 서버는
curr_eof가 단조 증가하는prev_eof를 초과하지 못할 때 hung으로 판정되므로, 늦게 도착한 응답이 기준선을 초기화하지 않는다. - detector 가드는 좁다: 격리되지 않은 MASTER만 강등하며, SLAVE 전환은 세 락을 모두 쥔 상태에서 이루어지고,
continue가 wedge당 정확히 하나의 teardown을 보장한다. - Teardown은 정상 종료 후 강제 종료 순이다. 활성 서버에는
css_process_start_shutdown을 요청하고, wedge된 서버는 즉시SIGKILL하며, confirm은max_retries후 살아남은 서버를 강제 종료한다. hide_to_demote가 역할 양보를 안전하게 만든다. 설정된 동안 브로드캐스트, split-brain 응답, 점수 참여를 억제하므로 노드는 스스로를 다시 선출할 수 없다. 매 반복마다 재단언되며, 두 분기 중 정확히 하나에서 해제된다.- 대기는 유한하다
HB_MAX_WAIT_FOR_NEW_MASTER(60)번의 1초 반복으로 제한된다: 피어 발견은 깔끔하게 종료하고, 만료/격리는 플래그를 해제하여 점수에 의한 재승격을 허용한다. - 미해결 문제: 만료 분기는 실행 중인 서버 없이 노드를 master로 되돌릴 수 있다. 챕터 9의 프로세스 부활 로직을 먼저 확인할 것.
Chapter 9: 리소스 측과 프로세스 감시
섹션 제목: “Chapter 9: 리소스 측과 프로세스 감시”클러스터 측 FSM(4~8장)은 이 노드가 어떤 역할을 맡아야 하는가를 결정하지만, 직접 fork·kill하거나 cub_server와 통신하지는 않는다. 그 역할은 리소스 측이 담당한다. 리소스 측은 별도의 잡 큐(resource_Jobs, 3장)를 가지며, 워커가 HB_RJOB_* 잡을 꺼내어 프로세스별 FSM을 구동한다. 이 장에서는 노드 상태 변화가 촉발할 때 cub_master가 cub_server·copylogdb·applylogdb 프로세스를 실제로 어떻게 시작하고, 등록하며, 모드를 전환하고, 종료하는지를 설명한다.
이 서브시스템은 전역 hb_Resource(HB_RESOURCE, 1장) 위에 걸려 있다. hb_Resource는 뮤텍스, 노드 state 미러, num_procs, 그리고 HB_PROC_ENTRY의 단방향 순회 리스트 procs로 구성된다. 각 슬롯의 모든 필드는 감시 경로에서 의미를 지닌다.
| 필드 | 역할 | 이유 |
|---|---|---|
next / prev | HB_LIST 링크 | procs는 이중 연결 리스트; hb_alloc_new_proc이 앞에 추가하고 hb_remove_proc이 연결을 끊음 |
state | HB_PROC_STATE FSM 커서 | §9.1의 신뢰·역할 척추; 모든 잡이 이 값으로 분기 |
type | HB_PROC_TYPE (SERVER/COPYLOGDB/APPLYLOGDB) | 서버는 소켓 종료·모드 전환 경로를 타고, 로그라이터는 SIGTERM을 받음 |
sfd | 소켓 fd | hb_return_proc_by_fd의 키; 연결이 끊기면 INVALID_SOCKET으로 초기화 |
pid | 자식 프로세스 pid | hb_return_proc_by_pid의 키; kill 생존 탐침과 시그널의 대상 |
exec_path | execv에 넘길 바이너리 | 재시작 시 재실행할 프로그램; 최초 등록 시 한 번만 기록 |
args | 서버 이름 + 로그 경로 | 식별자 키(hb_return_proc_by_args); 크래시·재시작 이후에도 유지 |
frtime | 최초 등록 시각 | HB_PROC_RECOVERY_DELAY_TIME 기반 조기 재시작 스로틀 게이트(§9.4) |
rtime | 마지막 등록 시각 | §9.7에서 측정하는 flapping 윈도우의 시작점 |
dtime | 등록 해제 시각 | dereg 경로의 부기용 |
ktime | 종료·사망 감지 시각 | flapping 윈도우의 끝(ktime - rtime, §9.7) |
stime | 시작 시각 | fork 후 부모가 기록(§9.4) |
changemode_rid | 예약된 요청 id | 선언만 되어 있고 읽거나 쓰는 코드가 없음 — 잔재(Cross-check Notes) |
changemode_gap | 모드 전환 누락 카운터 | SIGTERM/SIGKILL 에스컬레이션 판단에 읽힘(§9.6); 이 트리에서는 증가 코드가 없음 |
prev_eof / curr_eof | 복제 EOF LSA | 로그 프로세스의 진행 추적; 재시작 시 초기화(§9.7) |
is_curr_eof_received | EOF 수신 플래그 | EOF LSA와 쌍을 이룸; 재시작 시 초기화 |
conn | CSS_CONN_ENTRY * | 활성 소켓 연결; 연결 끊기면 NULL |
being_shutdown | 정상 종료 진행 플래그 | 이전 인스턴스가 사라지길 proc_start가 기다리게 함(§9.4) |
server_hang | 행 감지 플래그 | 데모트 경로에서 즉시 SIGKILL을 보낼 수 있게 함; 등록·재시작 시 초기화 |
공통 방어 가드(아래 함수 설명에서 반복하지 않음). 각 리소스 잡(hb_resource_job_proc_start, _proc_dereg, _confirm_start, _confirm_dereg)은 if (arg == NULL || proc_arg == NULL) return;으로 시작한다. hb_resource_job_change_mode는 설계상 arg를 NULL로 받으므로 이 가드가 없다. 각 핸들러(hb_register_new_process, hb_is_registered_process, hb_resource_receive_changemode, hb_cleanup_conn_and_start_process)는 hb_Resource->lock을 잡기 전에 if (hb_Resource == NULL) return ...;으로 시작한다.
9.1 프로세스 FSM과 조회 함수
섹션 제목: “9.1 프로세스 FSM과 조회 함수”HB_PROC_ENTRY.state(HB_PROC_STATE)가 척추 역할을 한다. 열거형은 HB_PSTATE_REGISTERED(5)를 신뢰 경계로 정의한다. 5 이상은 감시 대상, 미만은 과도 상태다. HB_PSTATE_REGISTERED_AND_STANDBY는 HB_PSTATE_REGISTERED의 별칭(둘 다 5)이며, 방금 등록된 서버는 정의상 standby다. 과도 상태를 순서대로 나열하면: DEAD(1), DEREGISTERED(2), STARTED(3, fork+execv 완료, 자식이 재등록 전), NOT_REGISTERED(4, 재시작 자식이 보고했으나 미확인). 경계 위: REGISTERED_AND_TO_BE_STANDBY(6), REGISTERED_AND_ACTIVE(7), REGISTERED_AND_TO_BE_ACTIVE(8).
세 가지 조회 헬퍼가 각각 다른 키로 procs를 인덱싱한다. 모두 “락 보유 상태에서 선형 탐색, 첫 일치 반환” 루프를 공유하며 비교식만 다르다. hb_return_proc_by_args는 strcmp(proc->args, args) == 0, hb_return_proc_by_pid는 proc->pid == pid, hb_return_proc_by_fd는 proc->sfd == sfd로 비교하며, 일치하는 HB_PROC_ENTRY * 또는 NULL을 반환한다. args(서버 이름 + 로그 경로)는 크래시·재시작 이후에도 유지되므로, 시작·확인·등록이 args로 조회하고 재시작 시 동일 슬롯을 재사용한다. **pid**는 연산자가 pid를 지정하는 dereg 경로에, **sfd**는 커널이 fd만 전달하는 소켓 이벤트 경로에 사용된다.
불변 조건 —
args는 활성 슬롯을 고유하게 식별한다.hb_register_new_process가 할당 전에args로 먼저 조회하고 찾은 엔트리를 재사용함으로써 보장된다. 두 슬롯이 같은args를 공유하면 재시작 시 잘못된 슬롯이 재등록되어num_procs가 틀어진다.
hb_is_registered_process는 연결 레이어가 사용하는 읽기 전용 탐침이다. 락을 잡고, hb_Resource->shutdown이 설정되어 있으면 false를 반환하며, 그렇지 않으면 args 기반 조회 결과 proc != NULL 여부를 반환한다.
9.2 등록 — hb_register_new_process
섹션 제목: “9.2 등록 — hb_register_new_process”HBP_PROC_REGISTER 핸들러다. 프로세스가 고정 레이아웃의 등록 패킷(pid, type, exec_path, args)을 보내면, 마스터는 새 슬롯을 만들거나 기존 슬롯을 새 연결에 재연결한다. shutdown 확인 후(설정되어 있으면 연결 해제) args로 조회하고 의도적 proc_state를 결정한다. 찾지 못하면 hb_alloc_new_proc으로 할당(OOM이면 연결 해제)하고 HB_PSTATE_REGISTERED로 설정한 뒤 frtime을 기록한다. HB_PSTATE_STARTED 상태에서 찾으면 HB_PSTATE_NOT_REGISTERED(재시작 재연결)로, 다른 상태에서 찾으면 HB_PSTATE_UNKNOWN(살아 있는 args를 주장하는 사칭자)으로 처리한다.
proc_state | 트리거 | 의미 |
|---|---|---|
HB_PSTATE_REGISTERED | 슬롯 없음 | 최초 등록; frtime 기록 |
HB_PSTATE_NOT_REGISTERED | HB_PSTATE_STARTED 상태에서 발견 | 마스터가 방금 이 슬롯을 fork(§9.4); 자식이 보고 |
HB_PSTATE_UNKNOWN | 그 외 상태에서 발견 | 활성 연결이 있는 args를 주장하는 프로세스 — 실패 처리 |
수락 게이트는 정확히 두 경우만 허용한다. 진짜 최초 등록이거나, 보고된 pid가 우리가 fork한 pid와 일치하고 아직 살아 있는 재시작인 경우다.
// hb_register_new_process (acceptance gate) -- src/executables/master_heartbeat.c if ((proc_state == HB_PSTATE_REGISTERED) || (proc_state == HB_PSTATE_NOT_REGISTERED && proc->pid == (int) ntohl (hbp_proc_register->pid) && !(kill (proc->pid, 0) && errno == ESRCH))) /* <- pid alive check */ { proc->state = proc_state; proc->sfd = conn->fd; proc->conn = conn; gettimeofday (&proc->rtime, NULL); proc->changemode_gap = 0; proc->server_hang = false; if (proc->state == HB_PSTATE_REGISTERED) /* first-register only */ { proc->pid = ntohl (...); proc->type = ntohl (...); if (proc->type == HB_PTYPE_SERVER) proc->state = HB_PSTATE_REGISTERED_AND_STANDBY; memcpy (proc->exec_path, ...); memcpy (proc->args, ...); hb_Resource->num_procs++; } /* <- bumped ONLY on first register */ ...; return; } ...; css_remove_entry_by_conn (conn, ...); /* else: log FAILURE, tear down */pid/type/exec_path/args/num_procs는 최초 등록 시에만 기록된다. 재연결은 해당 값이 이미 채워져 있으므로 sfd/conn/rtime/changemode_gap/server_hang만 갱신한다. else 분기(사칭자 또는 만료·사망한 pid)는 연결을 해제한다.
불변 조건 —
num_procs는 최초 등록만 센다, 재연결은 세지 않는다. 정확히 한 곳(이 분기)에서 증가하고, 정확히 두 곳(_proc_dereg,_confirm_dereg, §9.5)에서 감소한다. 재연결이 이 값을 증가시키면 리스트와 카운트가 어긋난다.
9.3 생명 주기 FSM 전체
섹션 제목: “9.3 생명 주기 FSM 전체”stateDiagram-v2 [*] --> STARTED: proc_start fork+execv STARTED --> NOT_REGISTERED: 자식 재등록 \n hb_register_new_process NOT_REGISTERED --> REGISTERED: confirm_start, 비서버 NOT_REGISTERED --> REGISTERED_AND_STANDBY: confirm_start, 서버 [*] --> REGISTERED: 최초 등록, 비서버 [*] --> REGISTERED_AND_STANDBY: 최초 등록, 서버 REGISTERED_AND_STANDBY --> REGISTERED_AND_TO_BE_ACTIVE: changemode 응답 TO_BE_ACTIVE REGISTERED_AND_TO_BE_ACTIVE --> REGISTERED_AND_ACTIVE: changemode 응답 ACTIVE REGISTERED_AND_ACTIVE --> REGISTERED_AND_TO_BE_STANDBY: 데모트, changemode 응답 REGISTERED_AND_TO_BE_STANDBY --> REGISTERED_AND_STANDBY: changemode 응답 STANDBY REGISTERED_AND_STANDBY --> DEAD: 연결 끊김, cleanup DEAD --> STARTED: proc_start 재시작 REGISTERED_AND_STANDBY --> DEREGISTERED: 연산자 dereg DEREGISTERED --> [*]: confirm_dereg 슬롯 제거
Figure 9-1: HB_PROC_STATE 생명 주기. 왼쪽 열(시작/재시작) §9.4; 오른쪽(모드 전환) §9.6; 하단(dereg) §9.5.
9.4 시작과 확인 — hb_resource_job_proc_start / _confirm_start
섹션 제목: “9.4 시작과 확인 — hb_resource_job_proc_start / _confirm_start”HB_RJOB_PROC_START는 감시 대상 프로세스를 (재)실행해야 할 때마다 큐에 들어간다. 가장 흔한 경우는 크래시 이후 hb_cleanup_conn_and_start_process(§9.7)가 큐에 넣는 것이다. 잡 인자는 args, pid, 재시도 카운터, ftime을 담는다. args로 슬롯을 조회한 뒤 세 가지 pre-fork 분기가 실행된다. (1) 슬롯 없거나 HB_PSTATE_DEREGISTERED → free_and_init(arg) 후 중단. (2) proc->being_shutdown → 이전 pid가 사라졌으면(pid <= 0 또는 kill(pid,0)이 ESRCH) 플래그를 지우고 계속 진행, 아직 살아 있으면 1초 후 PROC_START를 재큐. (3) HB_GET_ELAPSED_TIME(now, proc->frtime) < HB_PROC_RECOVERY_DELAY_TIME(30 000 ms) → 1초 후 재큐(부팅 직후 반복 재시작 스로틀). 이 가드들을 통과하면 fork가 실행된다.
// hb_resource_job_proc_start (fork) -- src/executables/master_heartbeat.c args = strdup (proc->args); hb_proc_make_arg (argv, args); /* tokenise args into argv[] */ pid = fork (); if (pid < 0) { hb_resource_job_queue (HB_RJOB_PROC_START, arg, ...); free (args); return; } /* retry */ else if (pid == 0) /* child */ { error = execv (proc->exec_path, argv); /* no return on success */ ...; css_master_cleanup (SIGTERM); return; } /* execv FAILED: must die, not run on */ else /* parent */ { proc->pid = pid; proc->state = HB_PSTATE_STARTED; /* awaits re-register */ gettimeofday (&proc->stime, NULL); free (args); } hb_resource_job_queue (HB_RJOB_CONFIRM_START, arg, prm_get_integer_value (PRM_ID_HA_PROCESS_START_CONFIRM_INTERVAL_IN_MSECS));네 분기: fork 실패(재시도), 자식(execv 또는 사망), 부모(STARTED), 그 다음 confirm 큐잉.
불변 조건 —
execv에 실패한 자식은 반드시 사망해야 한다. fork 이후 자식은 부모의 마스터 역할을 공유한다.execv가 반환했다는 것은 실패했다는 의미이므로, 자식은 계속 실행하지 않고css_master_cleanup(SIGTERM)을 호출한다. 두 프로세스가 동시에 자신을cub_master로 믿는 사태를 막기 위해서다.
hb_proc_make_arg는 strtok_r로 " \t\n" 구분자를 기준으로 args를 분해해 argv[]에 순서대로 채운다. 경계 검사는 없고, NULL 종결자도 자체적으로 추가하지 않는다 — execv 종결자는 호출자가 0으로 초기화한 argv[HB_MAX_NUM_PROC_ARGV]에 의존한다. args를 제자리에서 변형하기 때문에 strdup으로 복사한 값을 쓴다.
**hb_resource_job_confirm_start**는 자식이 재등록했는지 확인하며 최대 max_retries까지 재시도한다. 슬롯 소멸 중단 처리 후 다섯 분기를 탄다.
- 재시도 소진, 마스터 + 서버 + 격리 아님 — 마스터는
cub_server없이 운영될 수 없으므로 자기 데모트한다.hb_Resource->state = HB_NSTATE_SLAVE,retries초기화,CONFIRM_START재큐, 즉시HB_RJOB_DEMOTE_START_SHUTDOWN을 큐(8장), 반환. - 재시도 소진, 그 외 —
retries초기화,CONFIRM_START재큐, 계속 감시. kill(proc->pid, 0)이ESRCH반환 — pid 사라짐,PROC_START재큐하여 재실행.kill이 다른 이유로 실패 —CONFIRM_START재큐, 계속 감시.- 살아 있고
proc->state == HB_PSTATE_NOT_REGISTERED— 자식이 재등록함(§9.2); 서버는REGISTERED_AND_STANDBY, 로그라이터는REGISTERED로 승격,retry = false; 재시도 중이면 재큐, 아니면free_and_init(arg).
분기 1은 8장이 소유하는 데모트 경로의 리소스 측 트리거다.
9.5 등록 해제 — hb_resource_job_proc_dereg / _confirm_dereg
섹션 제목: “9.5 등록 해제 — hb_resource_job_proc_dereg / _confirm_dereg”연산자 주도 종료(10장이 명령을 연결). 슬롯은 이 잡들이 실행되기 전에 HB_PSTATE_DEREGISTERED로 설정된다. 두 잡 모두 다른 상태에서는 처리를 거부한다. _proc_dereg는 두 개의 락(css_Master_socket_anchor_lock 후 hb_Resource->lock)을 잡는다. 서버는 소켓 레이어까지 접근해야 하기 때문이다. pid 기반 조회 후 분기: 알 수 없는 pid → free 후 반환; 상태 != DEREGISTERED → free 후 반환; 서버 → 소켓 엔트리를 가져와서, 없으면 goto confirm 큐잉, 있으면 css_process_start_shutdown(정상 종료) 전송 후 CONFIRM_DEREG 큐잉; 비서버인데 이미 사망 (pid <= 0 또는 kill(pid, SIGTERM)이 ESRCH) → num_procs--, hb_remove_proc, free, confirm 잡 없이 반환; 비서버 살아 있음 → 방금 전송한 SIGTERM이 도달하므로, CONFIRM_DEREG 큐잉으로 낙하.
// hb_resource_job_proc_dereg (server vs logwriter) -- src/executables/master_heartbeat.c if (proc->type == HB_PTYPE_SERVER) { sock_entq = css_return_entry_by_conn (proc->conn, &css_Master_socket_anchor); if (sock_entq == NULL || sock_entq->name == NULL) goto hb_resource_job_proc_dereg_end; css_process_start_shutdown (sock_entq, 0, buffer); } /* graceful server shutdown msg */ else { if (proc->pid <= 0 || (kill (proc->pid, SIGTERM) && errno == ESRCH)) { hb_Resource->num_procs--; hb_remove_proc (proc); proc = NULL; /* already gone: remove now */ free_and_init (arg); return; } } /* NO confirm job */hb_resource_job_proc_dereg_end: hb_resource_job_queue (HB_RJOB_CONFIRM_DEREG, arg, prm_get_integer_value (PRM_ID_HA_PROCESS_DEREG_CONFIRM_INTERVAL_IN_MSECS));**hb_resource_job_confirm_dereg**는 에스컬레이션 루프다. 알 수 없는 pid / 잘못된 상태 중단 처리 후: kill(proc->pid, 0)이 ESRCH로 실패하면 프로세스가 사라진 것(retry = false); 그렇지 않고 ++retries > max_retries이면 kill(proc->pid, SIGKILL)로 에스컬레이션하고 재시도를 멈춘다; 아니면 CONFIRM_DEREG를 재큐한다. retry가 해제되면 num_procs--, hb_remove_proc으로 슬롯 제거, 인자 free. 즉, _proc_dereg의 SIGTERM을 먼저 보내고, max_retries 이후 confirm_dereg가 SIGKILL을 보내어 강제 종료 전에 제한된 정상 종료 창을 보장한다. hb_remove_proc(hb_list_remove + free_and_init)는 시작 종료 처리 밖에서 슬롯이 리스트를 떠나는 유일한 지점이다.
불변 조건 — 슬롯은 정확히 한 번 해제되며,
num_procs감소와 쌍을 이룬다. 두 제거 지점 모두num_procs--와hb_remove_proc(proc); proc = NULL을 함께 실행한다.proc = NULL이 해제 후 사용(use-after-free)을 방지한다.
9.6 모드 브리지 — 모드 전환 송수신
섹션 제목: “9.6 모드 브리지 — 모드 전환 송수신”노드 상태 결정이 실행 중인 cub_server에 어떻게 전달되는지를 설명한다. hb_resource_job_change_mode(HB_RJOB_CHANGE_MODE, 자가 연장 주기 잡)는 모든 procs 엔트리를 순회하며 HB_PTYPE_SERVER가 아닌 항목은 건너뛴다. 서버의 역할이 노드 역할에 뒤처지면 hb_resource_send_changemode를 호출하고, PRM_ID_HA_CHANGEMODE_INTERVAL_IN_MSECS 이후 자신을 다시 큐에 넣는다. 뒤처짐 판단: MASTER 노드는 아직 REGISTERED_AND_STANDBY 또는 REGISTERED_AND_TO_BE_ACTIVE 상태인 서버를 nudge하고; TO_BE_SLAVE 노드는 아직 REGISTERED_AND_ACTIVE 또는 REGISTERED_AND_TO_BE_STANDBY 상태인 서버를 nudge한다. 다른 노드 상태는 아무것도 매칭하지 않아 전송하지 않는다. 잡이 자가 재활성화되므로 서버가 확인할 때까지 HA_CHANGEMODE_INTERVAL마다 nudge가 반복된다.
**hb_resource_send_changemode**는 한 틱을 처리한다. changemode_gap을 읽어 서버가 무응답 상태인지 판단한 뒤 요청을 전송한다.
// hb_resource_send_changemode -- src/executables/master_heartbeat.c if (proc->conn == NULL) return ER_FAILED; if (proc->changemode_gap == HB_MAX_CHANGEMODE_DIFF_TO_TERM) sig = SIGTERM; /* 12 */ else if (proc->changemode_gap >= HB_MAX_CHANGEMODE_DIFF_TO_KILL) sig = SIGKILL; /* 24 */ if (sig) /* server ignored us too many times */ { if (proc->pid > 0 && (kill (proc->pid, 0) == 0 || errno != ESRCH)) kill (proc->pid, sig); return ER_FAILED; } switch (hb_Resource->state) /* pick target state */ { case HB_NSTATE_MASTER: state = HA_SERVER_STATE_ACTIVE; break; case HB_NSTATE_TO_BE_SLAVE: state = HA_SERVER_STATE_STANDBY; break; default: return ER_FAILED; } /* SLAVE etc: nothing to request */ css_send_heartbeat_request (proc->conn, SERVER_CHANGE_HA_MODE); /* + htonl(state) data; errors -> ER_FAILED */changemode_gap은 등록 시(§9.2)와 모드 전환 응답마다 0으로 초기화되며, 여기서 == 12 / >= 24 에스컬레이션을 위해 읽힌다. 이 리비전에는 이 값을 증가시키는 코드가 없으므로(Cross-check Notes) 항상 0이고 sig는 결코 발화하지 않는다. 전송 경로는 항상 실행된다. 노드 상태에서 목표 HA_SERVER_STATE를 결정하고 SERVER_CHANGE_HA_MODE와 네트워크 바이트 순서의 상태 워드를 전송한다. 연결/전송 실패나 요청할 것이 없는 노드 상태에서는 ER_FAILED를 반환한다.
**hb_resource_receive_changemode**는 SERVER_CHANGE_HA_MODE 응답 핸들러다. 와이어에서 서버의 실제 HA_SERVER_STATE를 읽고, 소켓 fd(hb_return_proc_by_fd(sfd), 요청 id 아님)로 프로세스를 조회한다. 프로세스가 없거나 DEREGISTERED이면 중단한다. 그 다음 hb_Cluster->lock과 hb_Resource->lock을 모두 잡고 매핑된 HB_PSTATE_*를 proc->state에 기록한다. 스위치는 ACTIVE → REGISTERED_AND_ACTIVE, TO_BE_ACTIVE → REGISTERED_AND_TO_BE_ACTIVE, TO_BE_STANDBY → REGISTERED_AND_TO_BE_STANDBY, STANDBY → REGISTERED_AND_STANDBY로 매핑하며, STANDBY 케이스에서는 hb_Cluster->state와 hb_Resource->state를 모두 HB_NSTATE_SLAVE로 강제한다(서버가 데모트를 확인하면 노드가 슬레이브로 전환됨). 모든 케이스는 마지막으로 proc->changemode_gap = 0을 초기화한다. TO_BE_ACTIVE에서 ACTIVE로의 확정이 여기서 이루어진다. 마스터가 계속 ACTIVE를 전송(§9.6 nudge)하면, 서버는 TO_BE_ACTIVE를 거쳐 최종적으로 ACTIVE를 보고하고, 그때 비로소 슬롯이 REGISTERED_AND_ACTIVE에 도달한다.
9.7 연결 끊김과 재시작 — hb_cleanup_conn_and_start_process
섹션 제목: “9.7 연결 끊김과 재시작 — hb_cleanup_conn_and_start_process”감시 대상 프로세스의 소켓이 닫히면 연결 레이어가 이 함수를 호출한다(non-static, 외부 공개). “프로세스 사망”을 “재시작 큐잉”으로 연결하고, 8장이 참조하는 반복 실패 시 데모트 결정을 소유한다.
// hb_cleanup_conn_and_start_process -- src/executables/master_heartbeat.c css_remove_entry_by_conn (conn, &css_Master_socket_anchor); if (hb_Resource == NULL) return; proc = hb_return_proc_by_fd (sfd); if (proc == NULL) return; /* fd not ours */ proc->conn = NULL; proc->sfd = INVALID_SOCKET; if (proc->state < HB_PSTATE_REGISTERED) return; /* never fully registered: leave for a job to reap */ gettimeofday (&proc->ktime, NULL); if (hb_Resource->state == HB_NSTATE_MASTER && proc->type == HB_PTYPE_SERVER && hb_Cluster->is_isolated == false && HB_GET_ELAPSED_TIME (proc->ktime, proc->rtime) < prm_get_integer_value (PRM_ID_HA_UNACCEPTABLE_PROC_RESTART_TIMEDIFF_IN_MSECS)) { hb_Resource->state = HB_NSTATE_SLAVE; /* died too soon after register: flapping -> demote */ hb_resource_job_queue (HB_RJOB_DEMOTE_START_SHUTDOWN, NULL, HB_JOB_TIMER_IMMEDIATELY); } job_arg = (HB_JOB_ARG *) malloc (sizeof (HB_JOB_ARG)); if (job_arg == NULL) return; /* OOM: no restart queued */ proc_arg->pid = proc->pid; memcpy (proc_arg->args, proc->args, ...); /* identity for restart lookup */ proc_arg->retries = 0; proc_arg->max_retries = prm_get_integer_value (PRM_ID_HA_MAX_PROCESS_START_CONFIRM); proc->state = HB_PSTATE_DEAD; /* mark dead; clears for restart */ proc->server_hang = false; proc->is_curr_eof_received = false; LSA_SET_NULL (&proc->prev_eof); ... hb_resource_job_queue (HB_RJOB_PROC_START, job_arg, HB_JOB_TIMER_WAIT_A_SECOND);분기: 우리 fd가 아님(반환), 아직 REGISTERED가 아님(conn 초기화, 잡에 맡김), job_arg OOM(재시작 없음), 완전 등록됨(메인 경로). flapping 가드 — §9.4의 frtime/30초 가드와 별개 — 는 ktime - rtime을 측정한다. 마스터의 서버가 마지막 등록 이후 HA_UNACCEPTABLE_PROC_RESTART_TIMEDIFF보다 일찍 사망하면 마스터가 데모트된다(8장). 그렇지 않으면 args를 키로 하는 재시작 HB_JOB_ARG를 만들고, 슬롯을 HB_PSTATE_DEAD로 표시하고, EOF·hang 부기를 초기화한 뒤 HB_RJOB_PROC_START를 큐에 넣는다.
이로써 루프가 닫힌다. §9.7이 사망을 감지하고 시작을 큐에 넣고, §9.4가 fork하고 확인하며, §9.2가 재등록 시 재연결하고, §9.6이 클러스터 FSM(5~8장)이 원하는 역할로 모드를 전환한다.
Cross-check Notes
섹션 제목: “Cross-check Notes”changemode_gap은 이 소스 리비전에서 어디서도 증가하지 않는다. 저장소 전체에서 이 필드의 쓰기 지점은 정확히 두 곳 —hb_register_new_process와hb_resource_receive_changemode, 둘 다 0으로 설정 — 이고, 읽기 지점은hb_resource_send_changemode에 두 곳이다. 증가 코드가 없으므로 값은 0에 고정되어, SIGTERM-at-12 / SIGKILL-at-24 에스컬레이션은 이 트리에서는 발화하지 않는다. 프로토콜 침묵 감시자가 죽은 코드다.changemode_rid는 선언만 되고 사용되지 않는다.HB_PROC_ENTRY(master_heartbeat.h)에 모드 전환 요청 id용으로 예약되었지만, 읽거나 쓰는 코드가 없다. 응답은 순전히 소켓 fd(hb_return_proc_by_fd)로 연결된다. 잔재 필드다.
Open Questions
섹션 제목: “Open Questions”changemode_gap은 어디서 증가하도록 의도되었는가? 임계값과 읽기 지점이 암시하는 것은hb_resource_job_change_mode나hb_resource_send_changemode안에서의 틱당 증가인데, 이 브랜치에서 제거되었거나 처음부터 존재하지 않았다. 과거·형제 릴리스에 실제 감시자가 있는지는 미해결이다.changemode_rid는 fd 기반 상관 관계를 대체하도록 의도된 것인가? 모드 전환 응답이 비동기 또는 다중화될 때만 의미가 있다. 현재는 죽은 무게다.
9.8 챕터 요약 — 핵심 정리
섹션 제목: “9.8 챕터 요약 — 핵심 정리”args가 식별자이고,pid/sfd는 핸들이다.hb_return_proc_by_args가 재시작 시 슬롯을 재사용하게 한다._by_pid와_by_fd는 각각 dereg와 소켓 이벤트 경로에 쓰여, 누수되거나 중복된HB_PROC_ENTRY슬롯을 방지한다.hb_register_new_process는 정확히 두 수락 케이스를 갖는다. 최초 등록(할당,frtime기록,num_procs증가, 서버는STANDBY진입)과 pid가 일치하고 살아 있는 재시작 재연결.num_procs는 최초 등록 시에만 증가한다. 그 외 모든 경우는 연결을 해제한다.- 시작은 fork+execv에 두 스로틀을 적용한다.
HB_PROC_RECOVERY_DELAY_TIME(30초,frtime기준)이 조기 재시작을 막는다. 자식은execv실패 시css_master_cleanup을 호출해야 하므로 반쪽짜리 마스터 복제본이 살아남지 못한다._confirm_start는max_retries까지 재시도하며, 서버를 유지하지 못하는 마스터는 자기 데모트한다(8장). - Dereg는 SIGTERM을 먼저, SIGKILL을 나중에 에스컬레이션한다.
_proc_dereg는 서버에는 정상 종료 메시지를, 로그라이터에는SIGTERM을 보낸다._confirm_dereg는max_retries이후 강제SIGKILL을 보낸다. 두 제거 지점 모두num_procs--와hb_remove_proc을 쌍으로 실행한다. - 모드 브리지는 주기적 nudge와 응답 핸들러의 조합이다.
hb_resource_job_change_mode가 자가 재활성화되며, 역할이 뒤처진 모든 서버에hb_resource_send_changemode를 호출한다.hb_resource_receive_changemode는 보고된HA_SERVER_STATE를proc->state에 기록하며,TO_BE_ACTIVE를ACTIVE로 확정하고 서버가STANDBY를 확인하면 노드를SLAVE로 전환한다. changemode_gap감시자는 이 리비전에서 잔재 코드다.hb_resource_send_changemode가 이를 읽어 SIGTERM(12) / SIGKILL(24)로 에스컬레이션하지만, 어떤 코드 경로도 이를 증가시키지 않아 항상 0이며 에스컬레이션이 발화하지 않는다.changemode_rid또한 선언만 되어 있고 사용되지 않으며, 응답은 소켓 fd로 연결된다. 두 항목 모두 Cross-check Notes에 드리프트로 기록되어 있다.hb_cleanup_conn_and_start_process가 사망-재시작 브리지다. 서버가 마지막 등록 이후HA_UNACCEPTABLE_PROC_RESTART_TIMEDIFF이내에 사망한 마스터를 데모트(flapping)하며, 그 외에는 슬롯을HB_PSTATE_DEAD로 표시하고args를 키로HB_RJOB_PROC_START를 큐에 넣어 감시 루프를 닫는다.
Chapter 10: 운영자 명령 처리 브리지와 종료 경로
섹션 제목: “Chapter 10: 운영자 명령 처리 브리지와 종료 경로”정상 상태 생애주기가 아닌 모든 것을 다룬다. 운영자가 기능을 켜고 끄는 방법, 관리 대상 프로세스가 cub_master에 연결하고 마스터 종료를 감지하는 방법, 그리고 각 계층이 해체되는 방법이 여기 해당한다. 네 가지 주체가 관여한다.
cubrid hb …(util_service.c) — CLI 프론트엔드. 마스터와 직접 통신하지 않고 하위 단계마다cub_commdb를 생성한다.cub_commdb(commdb.c) — 단명 courier: 연결 하나, opcode 하나, 응답 출력 후 종료.cub_master요청 루프 (master_request.c) — opcode를master_heartbeat.c의hb_*진입점에 매핑한다.- 관리 대상 프로세스 (
connection/heartbeat.c,CS_MODE라이브러리) — 마스터 연결이 끊기면 스스로 종료하는 reader 스레드를 갖는다.
이론과 정상 상태 상태 기계는 동반 문서 cubrid-heartbeat.md에 있다. 이 챕터는 노드 상태(HB_NSTATE_*), 프로세스 상태(HB_PSTATE_*), 잡 큐(Chapter 3)를 이미 알고 있다고 가정한다.
10.1 운영자 인터페이스: process_heartbeat
섹션 제목: “10.1 운영자 인터페이스: process_heartbeat”cubrid heartbeat <verb> (별칭 hb)는 process_heartbeat에서 처리된다. 이 함수는 guard와 switch (command_type)로 구성된다. 두 개의 조기 반환이 모든 처리를 차단한다. ha_mode_in_common == HA_MODE_OFF이면 ER_FAILED(“HA 아님”)를 반환하고, util_make_ha_conf 실패 시 ER_FAILED(파싱 실패)를 반환한 뒤 switch가 각 command_type을 해당 핸들러로 분기한다. 운영자가 쓰는 분기는 START/STOP/DEREGISTER/STATUS/LIST/RELOAD → 각각의 process_heartbeat_* 핸들러이며, 같은 switch에 내부 분기(SC_COPYLOGDB/SC_APPLYLOGDB → process_heartbeat_util, REPLICATION → process_heartbeat_replication)도 포함되나 이 챕터에서는 다루지 않는다. default는 ER_GENERIC_ERROR를 반환한다. Windows에서는 전체 본문이 #else로 컴파일되어 ER_FAILED를 반환한다(POSIX 전용). start를 제외한 각 verb는 css_does_master_exist를 확인하고, const char *args[]를 구성한 뒤 proc_execute (UTIL_COMMDB_NAME, …)를 호출한다. status는 3~4개의 commdb 생성을 순차 실행한다(node, process, ping-host, verbose admin). deregister는 argc < 1 조건에서 거부하고 → COMMDB_HA_DEREG_BY_PID; reload는 페이로드 없이(COMMDB_HA_RELOAD) 전달된다.
flowchart TD cli["cubrid hb <verb>"] --> ph["process_heartbeat<br/>guard + switch"] ph -->|"STOP"| pstop["process_heartbeat_stop"] ph -->|"STATUS/LIST"| pstat["process_heartbeat_status"] ph -->|"RELOAD"| prel["process_heartbeat_reload"] ph -->|"DEREGISTER"| pdereg["process_heartbeat_deregister"] pstop --> ushd["us_hb_deactivate<br/>4단계 오케스트레이션"] ushd -.->|"proc_execute"| commdb["cub_commdb<br/>(생성당 opcode 하나)"] pstat -.->|"x3-4"| commdb prel -.-> commdb pdereg -.-> commdb commdb -.->|"TCP 요청"| master["cub_master 요청 루프"]
Figure 10-1. 운영자 프론트엔드는 마스터와 직접 통신하지 않는다. cub_commdb가 단발성 courier 역할을 한다.
10.2 비활성화 오케스트레이션: 4단계 핸드셰이크
섹션 제목: “10.2 비활성화 오케스트레이션: 4단계 핸드셰이크”cubrid hb stop(-i <dbname> 없음)은 기능 전체를 비활성화한다. us_hb_deactivate는 argv 슬롯 하나를 재사용하며 후미 opcode를 네 번 덮어쓰면서 단계마다 cub_commdb를 생성한다.
// us_hb_deactivate -- src/executables/util_service.c if (immediate_stop) args[opt_idx++] = COMMDB_HB_DEACT_IMMEDIATELY; /* <- BEFORE opt_idx frozen */ args[opt_idx]=DEACT_STOP_ALL; if (proc_execute(...)) return; /* ph1 */ args[opt_idx]=DEACT_CONFIRM_STOP_ALL; while (proc_execute(...)) { if(==EXIT_FAILURE) return; sleep(1);} /* ph2 poll */ args[opt_idx]=DEACTIVATE; if (proc_execute(...)) return; /* ph3 */ args[opt_idx]=DEACT_CONFIRM_NO_SERVER; while (proc_execute(...)) { if(==EXIT_FAILURE) return; sleep(1);} /* ph4 poll */-i 플래그는 opt_idx가 고정되기 전에 args[opt_idx++]로 삽입되어 4단계 전체에 걸쳐 벡터에 유지된다. 마스터의 STOP_ALL 핸들러만 이 플래그를 읽으며, 2~4단계에서는 args[opt_idx]만 덮어써지므로 무해하다.
불변 조건 — 비활성화는 확인 루프(confirm-loop)이며 fire-and-forget이 아니다. 2단계와 4단계는 성공할 때까지 바쁜 폴링(busy-poll)을 수행하며 EXIT_FAILURE(중단)와 그 외 비제로(아직 아님: sleep(1), 재시도)를 구분한다. 이를 생략하면 “여전히 드레이닝 중” 응답이 전체 stop을 중단시켜 클러스터가 반쯤 해체된 상태로 남는다. 이 루프들이 cubrid hb stop이 서버가 플러시하는 동안 눈에 띄게 멈추는 이유다.
각 opcode의 마스터 측 처리:
| commdb opcode | 마스터 핸들러 (master_request.c) | 효과 |
|---|---|---|
DEACT_STOP_ALL | css_process_deact_stop_all | hb_Deactivate_immediately 설정, hb_prepare_deactivate_heartbeat (HB_RJOB_CLEANUP_ALL 큐잉) |
DEACT_CONFIRM_STOP_ALL | css_process_deact_confirm_stop_all | hb_is_deactivation_ready()(proc에 conn 없음) 시 SUCCESS |
DEACTIVATE_HEARTBEAT | css_process_deactivate_heartbeat | hb_deactivate_heartbeat |
DEACT_CONFIRM_NO_SERVER | css_process_deact_confirm_no_server | hb_get_deactivating_server_count()==0 시 SUCCESS |
ACTIVATE_HEARTBEAT | css_process_activate_heartbeat | hb_activate_heartbeat |
RECONFIG_HEARTBEAT | css_process_reconfig_heartbeat | hb_reconfig_heartbeat -> hb_reload_config |
변경하는(mutating) 비활성화 핸들러 두 개만 origin을 검사한다. css_process_deact_stop_all과 css_process_deactivate_heartbeat는 처음에 hb_check_request_eligibility(conn->fd)를 실행한다. HB_HC_FAILED(getpeername 실패) / HB_HC_UNAUTHORIZED(피어 IP가 노드 목록에 없음) → HA_REQUEST_FAILURE. HB_HC_ELIGIBLE_REMOTE(피어 IP가 노드와 일치)는 허용되지만 hb_disable_er_log로 기록된다. HB_HC_ELIGIBLE_LOCAL(AF_UNIX 소켓)만 조용히 통과한다. 이 구조 덕에 원격 cubrid hb stop -h <peer>가 승인된다. 두 confirm 핸들러(css_process_deact_confirm_stop_all, css_process_deact_confirm_no_server)는 eligibility 검사를 전혀 하지 않는다. css_process_activate_heartbeat와 css_process_reconfig_heartbeat는 origin이 아닌 HA_DISABLED()에만 의존한다.
10.2.1 cub_commdb 내부: courier 핸들러
섹션 제목: “10.2.1 cub_commdb 내부: courier 핸들러”세 개의 opcode 전용 courier는 같은 골격을 공유한다. 요청 전송, 문자열 응답 수신, 출력 또는 실패. 그러나 인수 수와 결과 판정 방식이 다르다.
| 핸들러 | 요청 | 응답 수 | NO_ERROR 조건 | ER_FAILED 조건 |
|---|---|---|---|---|
process_reconfig_heartbeat | RECONFIG_HEARTBEAT, 인수 없음 | 1 | 응답이 비어 있지 않음 (size>0 && [0]!='\0') | 응답이 비어 있음 |
process_activate_heartbeat | ACTIVATE_HEARTBEAT, 인수 없음 | 1 | strncmp(reply, HA_REQUEST_SUCCESS, size-1)==0 | 그 외 |
process_deactivate_heartbeat | DEACTIVATE_HEARTBEAT, 인수 없음 | 2 (msg, result) | result == HA_REQUEST_SUCCESS | 그 외 |
process_reconfig_heartbeat는 내용으로 판정한다(비어 있지 않은 응답은 printf되고 성공; 비어 있으면 ER_FAILED). process_activate_heartbeat는 sentinel로 판정하므로 토큰과 일치하지 않는 “이미 활성화” 메시지는 마스터가 이를 양호하게 처리하더라도 실패로 보고된다. process_deactivate_heartbeat는 하나의 rid에서 응답을 두 번 읽는다. 메시지 문자열(비어 있지 않으면 출력)과 성공 여부를 검사하는 result 문자열이다. 모든 버퍼는 모든 분기에서 free_and_init된다. -i 플래그는 이 세 함수에 도달하지 않는다. process_deact_stop_all만 send_request_one_arg로 commdb_Arg_deact_immediately를 전송하며, 나머지는 send_request_no_args를 사용한다.
10.3 css_process_deact_stop_all과 immediately 플래그
섹션 제목: “10.3 css_process_deact_stop_all과 immediately 플래그”// css_process_deact_stop_all -- src/executables/master_request.c if (hb_is_deactivation_started () == false) { /* <- idempotent: only first wins */ hb_start_deactivate_server_info (); hb_Deactivate_immediately = (deact_immediately && *((bool*)deact_immediately)) ? true : false; if (hb_prepare_deactivate_heartbeat () != NO_ERROR) goto error_return; } css_send_data (conn, request_id, HA_REQUEST_SUCCESS, HA_REQUEST_RESULT_SIZE);hb_prepare_deactivate_heartbeat는 리소스 락 하에서 hb_Resource->shutdown = true를 설정하고(이미 설정되어 있으면 NO_ERROR를 즉시 반환하므로 두 번째 STOP_ALL은 무해), HB_JOB_TIMER_IMMEDIATELY로 HB_RJOB_CLEANUP_ALL 하나를 큐에 넣는다.
불변 조건 — hb_Deactivate_immediately는 정중함과 강제 중 하나를 선택한다. false일 때: cleanup은 연결된 각 cub_server PID를 hb_Deactivate_info.server_pid_list에 기록하고 graceful shutdown을 전송하여 서버가 플러시하도록 한다. true일 때: 해당 bookkeeping을 건너뛰고 confirm 잡이 첫 번째 패스에서 생존 프로세스를 SIGKILL한다. 전역 변수로, 사이클당 한 번 설정되며 hb_master_init에서 false로 재설정된다.
HB_DEACTIVATE_INFO(정적 hb_Deactivate_info)가 단계들을 연결한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
server_pid_list | cleanup 시작 시 캡처된 cub_server PID의 int* 배열 | 4단계에서 kill(pid,0)으로 각 DB 서버 종료 여부를 폴링 |
server_count | 유효한 항목 수 | 폴링 루프의 경계. 0 = 기다릴 서버 없음 |
info_started | bool latch: 비활성화 진행 중 | STOP_ALL을 멱등성으로 만들고 경쟁하는 hb_activate_heartbeat를 차단 |
10.4 hb_activate_heartbeat, hb_deactivate_heartbeat, hb_reload_config
섹션 제목: “10.4 hb_activate_heartbeat, hb_deactivate_heartbeat, hb_reload_config”hb_activate_heartbeat — 네 가지 guard 분기:
| guard | 동작 | 근거 |
|---|---|---|
hb_Cluster == NULL | return ER_FAILED | 초기화 미완료 |
hb_Deactivate_info.info_started == true | return ER_FAILED | 해체 중 재초기화는 리스트를 손상시킴 |
hb_Is_activated == true | return NO_ERROR | 양호: 운영자는 실패가 아닌 안내를 받음 |
| 그 외 | hb_master_init(); 에러 시 ER_FAILED, 아니면 hb_Is_activated=true | 싱글턴 재구성; hb_Deactivate_immediately=false 재설정 |
hb_deactivate_heartbeat — 대칭적인 off 스위치(3단계):
// hb_deactivate_heartbeat -- src/executables/master_heartbeat.c if (hb_Cluster == NULL) return ER_FAILED; if (hb_Is_activated == false) return NO_ERROR; /* already off, benign */ if (hb_Resource && resource_Jobs) hb_resource_shutdown_and_cleanup (); /* resource FIRST */ if (hb_Cluster && cluster_Jobs) hb_cluster_shutdown_and_cleanup (); /* cluster SECOND */ hb_Is_activated = false;리소스가 클러스터보다 먼저 해체된다. 리소스 측이 살아 있는 프로세스 연결을 소유하고, 클러스터 측이 피어 상태를 소유하기 때문이다. 프로세스를 먼저 드레이닝하면 로컬 DB 서버가 이미 종료 중인 상태에서 클러스터의 작별 heartbeat가 전송된다. 두 호출 모두 잡 큐가 아직 존재하는 경우에만 실행되므로, 부분적으로 초기화된 모듈도 깔끔하게 비활성화된다.
hb_reload_config — stop/start 없이 라이브 토폴로지를 변경할 수 있는 유일한 verb. hb_Cluster->lock 하에서 backup-mutate-restore를 수행한다.
// hb_reload_config -- src/executables/master_heartbeat.c (condensed) sysprm_reload_and_init (NULL, NULL); /* re-read cubrid.conf */ if (prm_get_string_value (PRM_ID_HA_NODE_LIST) == NULL) return ER_FAILED; lock; hb_list_move (&old_ping_hosts,...); hb_list_move (&old_nodes,...); /* backup + null out */ old_myself = ...->myself; old_master = ...->master; if (hb_cluster_check_valid_ping_server () == false) goto reconfig_error; /* fail 1 */ if (...->num_ping_hosts == 0 && hb_cluster_load_tcp_ping_host_list (...), /* ICMP empty: TCP ping */ hb_cluster_check_valid_ping_server () == false) { unlock; return ER_FAILED; } /* fail 2: early */ ...->num_nodes = hb_cluster_load_group_and_node_list (...); if (...->num_nodes < 1 || (master && hb_return_node_by_name(master->host_name)==NULL)) goto reconfig_error; /* fail 3 */ for (new_node...) for (old_node...) if (are_hostnames_equal(...)) /* carry state forward by host */ { ...->master = new_node if matches old_master; copy state/score/heartbeat_gap/last_recv_hbtime; old_node->host_name[0]='\0'; } /* mark "kept", no goodbye */ free old_ping_hosts + old_nodes; unlock; return NO_ERROR;reconfig_error: /* swap old_* back into hb_Cluster, free half-built new lists, unlock, return error */불변 조건 — reload는 피어에게 원자적이다. 새 토폴로지가 완전히 적용되거나, 아니면 이전 것이 완전히 복원된다. goto reconfig_error(fail 1 = 잘못된 ICMP ping 서버, fail 3 = 비어 있거나 유효하지 않은 노드 리스트)는 old_* 리스트를 복원하고 반쯤 구성된 새 리스트를 해제한다. TCP-ping 실패(fail 2)는 복원 없이 조기 반환한다. 해당 시점에는 ping_hosts(이미 백업)만 건드렸기 때문이다. 런타임 score/state는 호스트명으로 전달되므로 ping 호스트를 추가해도 선출 진행 상황이 재설정되지 않는다. 이 원자성이 없다면 오타가 포함된 ha_node_list가 마스터를 해제된 메모리를 가리키게 만들 수 있다.
10.5 hb_deregister_by_pid / _by_args와 cleanup 잡
섹션 제목: “10.5 hb_deregister_by_pid / _by_args와 cleanup 잡”관리 대상 프로세스는 PID(cubrid hb deregister <pid>)나 args(내부)로 추방된다. hb_deregister_by_pid/_by_args는 얇은 래퍼다. hb_Resource == NULL이면 bail out. 락 하에서 프로세스를 조회(hb_return_proc_by_pid / hb_return_proc_by_args)하고 없으면 unlock 후 반환(“프로세스를 찾을 수 없음”). 있으면 hb_deregister_process(잡 인수 구성, proc->state 전환)를 호출하고 unlock 후 IMMEDIATELY로 HB_RJOB_PROC_DEREG를 큐에 넣는다(큐 실패 시 assert(false) 후 인수 해제).
공유 함수 hb_deregister_process는 상태가 [HB_PSTATE_DEAD, HB_PSTATE_MAX) 범위 밖이거나 pid < 0인 프로세스를 거부하고, proc->dtime을 기록하며, HB_JOB_ARG(PID, args, retries=0, max_retries = PRM_ID_HA_MAX_PROCESS_DEREG_CONFIRM)를 할당한 뒤 반환 전에 proc->state = HB_PSTATE_DEREGISTERED로 전환한다. 비동기 HB_RJOB_PROC_DEREG 잡이 종료를 확인한다(Chapter 9).
hb_resource_job_cleanup_all(HB_RJOB_CLEANUP_ALL 워커)은 css_Master_socket_anchor_lock을 잠근 뒤 hb_Resource->lock을 잠근다(이 고정 순서). hb_Deactivate_immediately == false이면 server_pid_list를 calloc하여 proc->conn && proc->type==HB_PTYPE_SERVER인 각 PID를 기록한다(server_count). 이어서 hb_resource_shutdown_all_ha_procs(10.7)를 호출하고, confirm HB_JOB_ARG를 malloc한다(OOM 시 두 락 모두 해제 후 반환). retries=0, max_retries = PRM_ID_HA_MAX_PROCESS_DEREG_CONFIRM으로 설정한 뒤 두 락을 unlock하고 WAIT_500_MS로 HB_RJOB_CONFIRM_CLEANUP_ALL을 큐에 넣는다.
그 다음 hb_resource_job_confirm_cleanup_all로 연결되어 모든 프로세스가 사라질 때까지 재큐잉된다. 각 분기:
| 분기 | 조건 | 동작 |
|---|---|---|
| null guard | `arg == NULL | |
| deadline / force | ++retries > max_retries OR hb_Deactivate_immediately == true | 모든 proc 순회: 살아 있으면 SIGKILL, hb_remove_proc; assert(num_procs==0); goto end |
| 정상: non-server | proc->type != HB_PTYPE_SERVER | 즉시 SIGKILL + hb_remove_proc(stateless 레플리카) |
| 정상: server 살아 있음 | HB_PTYPE_SERVER, kill(pid,0)==0 | 그대로 유지; conn != NULL이면 num_connected_rsc 카운트 |
| 정상: server 종료됨 | HB_PTYPE_SERVER, kill -> ESRCH | hb_remove_proc, continue |
| 완료 | num_procs == 0 OR num_connected_rsc == 0 | goto end, arg 해제, ER_SET “비활성화 준비 완료” |
| 루프 | 그 외 | unlock, …DEREG_CONFIRM_INTERVAL…로 재큐 |
불변 조건 — cub_server 프로세스는 마지막에, 그리고 부드럽게 종료된다. 정상 패스에서 copylogdb/applylogdb는 즉시 SIGKILL되어 제거되지만, HB_PTYPE_SERVER는 이미 종료된(kill(pid,0) -> ESRCH) 경우에만 제거된다. 서버는 플러시 후 스스로 종료한다. retry 예산이 소진되거나 -i가 지정된 경우에만 강제 분기가 생존 프로세스에 SIGKILL을 보낸다. 단, 모든 프로세스는 먼저 hb_resource_shutdown_all_ha_procs를 통과한다.
stateDiagram-v2 [*] --> CleanupAll: STOP_ALL이 HB_RJOB_CLEANUP_ALL 큐잉 CleanupAll --> Confirm: server pid 기록 \n shutdown_all_ha_procs \n CONFIRM 큐잉 Confirm --> Confirm: retries <= max \n non-server kill \n server 대기 \n 재큐 Confirm --> Done: num_procs == 0 \n or num_connected == 0 Confirm --> ForceKill: retries > max \n or 즉시 비활성화 ForceKill --> Done: 생존 프로세스 SIGKILL \n 모든 proc 제거 Done --> [*]: ER_SET 비활성화 준비 완료
Figure 10-2. cleanup-all / confirm-cleanup-all 루프. graceful 드레인 경로와 강제 kill 경로를 모두 보여준다.
10.6 모듈 해체: cluster와 resource의 shutdown-and-cleanup
섹션 제목: “10.6 모듈 해체: cluster와 resource의 shutdown-and-cleanup”두 개의 공개 진입점은 각각 두 줄이다. hb_resource_shutdown_and_cleanup = hb_resource_job_shutdown() 후 hb_resource_cleanup(). hb_cluster_shutdown_and_cleanup = hb_cluster_job_shutdown() 후 hb_cluster_cleanup().
순서 불변 조건 — 잡 큐가 건드리는 데이터를 해제하기 전에 잡 큐를 먼저 중단해야 한다. hb_*_job_shutdown은 워커 스레드를 드레이닝하고 중단하여(Chapter 3) hb_*_cleanup이 procs/nodes를 해제한 이후에는 어떤 잡도 실행되지 않도록 한다. 순서가 반대이면 실행 중인 HB_CJOB_* 잡이 해제된 리스트를 역참조한다.
hb_resource_cleanup은 락을 걸고 hb_resource_shutdown_all_ha_procs를 재호출한다(멱등성 — 모든 conn이 이미 NULL). 이어서 conn && pid > 0인 프로세스에 SIGKILL을 보내고, hb_remove_all_procs로 리스트를 해제하며, procs=NULL, num_procs=0, state=HB_NSTATE_UNKNOWN, shutdown=true로 재설정한다.
hb_cluster_cleanup은 작별 인사를 담당한다. 락 하에서 state = HB_NSTATE_UNKNOWN을 설정하고, 자신을 제외한 모든 피어에게 마지막 hb_cluster_send_heartbeat_req를 전송하면서 heartbeat_gap을 증가시킨다. 피어는 gap이 늘어나는 것을 보고 즉시 failover를 시작한다. 그런 다음 노드를 해제하고, master/myself를 초기화하며, sfd를 닫고, ping 호스트를 해제하고, 미확인 노드 진단 리스트를 해제한다.
// hb_cluster_cleanup -- src/executables/master_heartbeat.c hb_cluster_remove_all_ui_nodes (hb_Cluster->ui_nodes); /* unauthorized-host diagnostics */ hb_Cluster->ui_nodes = NULL; hb_Cluster->num_ui_nodes = 0;ui_nodes 리스트(HB_UI_NODE_ENTRY)는 heartbeat를 전송했지만 노드 목록/group-id 인가 검사를 통과하지 못한 호스트를 보관한다. 순수하게 진단 목적(cubrid hb status -v)으로 쓰이며, 노드별 처리 없이 일괄 삭제된다.
10.7 hb_resource_shutdown_all_ha_procs: 연결 해제 vs. 종료 요청
섹션 제목: “10.7 hb_resource_shutdown_all_ha_procs: 연결 해제 vs. 종료 요청”cleanup-all과 최종 cleanup이 공유하는 프로세스별 연결 해제 단계다. 타입에 따라 분기한다.
// hb_resource_shutdown_all_ha_procs -- src/executables/master_heartbeat.c for (proc...) { if (proc->conn) { if (proc->type != HB_PTYPE_SERVER) { /* copy/apply: yank the socket */ css_remove_entry_by_conn (proc->conn, &css_Master_socket_anchor); proc->conn=NULL; proc->sfd=INVALID_SOCKET; } else { /* server: ask it to shut down */ sock_ent = css_return_entry_by_conn (proc->conn, &css_Master_socket_anchor); if (sock_ent && sock_ent->name) css_process_start_shutdown (sock_ent, 0, buffer); else { proc->conn=NULL; proc->sfd=INVALID_SOCKET; } } } else MASTER_ER_LOG_DEBUG (... "invalid socket-queue entry" ...); proc->state = HB_PSTATE_DEREGISTERED; } /* <- unconditional, EVERY proc */copylogdb/applylogdb는 소켓 큐 항목이 제거되고 conn이 null 처리된다. stateless 레플리카이므로 바로 버려도 안전하다. cub_server는 항목을 유지하되 graceful css_process_start_shutdown을 수신하여 스스로 플러시하고 종료한다. 항목이 없는 경우에만 마스터가 conn을 직접 null 처리한다. 마지막 줄은 모든 proc에 무조건 실행되어, confirm 잡(10.5)이 의존하는 HB_PSTATE_DEREGISTERED 선행 조건을 충족한다.
10.8 프로세스 측 브리지: hb_process_init과 자살 reader
섹션 제목: “10.8 프로세스 측 브리지: hb_process_init과 자살 reader”관리 대상 프로세스(copylogdb/applylogdb, CS_MODE)는 hb_process_init을 한 번 호출한다. thread-safe 일회성 latch다.
// hb_process_init -- src/connection/heartbeat.c static std::atomic<bool> is_first{true}; static std::mutex init_mtx; if (is_first.load(acquire)==false) return NO_ERROR; /* fast path */ std::lock_guard<std::mutex> lock(init_mtx); if (is_first.load(relaxed)==false) return NO_ERROR; /* double-check */ if (hb_Exec_path[0]=='\0' || *(hb_Argv)==0) return ER_FAILED; /* not set up */ hb_Conn = hb_connect_to_master (server_name, log_path, type); sleep(1); if ((error = hb_register_to_master (hb_Conn, type))) return error; /* send registration */ if ((error = hb_create_master_reader ())) return error; /* spawn reader */ is_first.store (false, release);hb_connect_to_master는 이름을 패킹하고($=copylogdb, %=applylogdb, hb_pack_server_name 통해) prm_get_master_port_id()에 연결한다. hb_register_to_master는 SERVER_REGISTER_HA_PROCESS와 HBP_PROC_REGISTER 페이로드(PID, type, exec path, argv, hb_make_set_hbp_register 생성)를 전송한다. 전송 에러 발생 시 페이로드를 해제하고 ER_FAILED를 반환한다. hb_create_master_reader는 hb_thread_master_reader를 실행하는 detached pthread를 생성한다.
// hb_thread_master_reader -- src/connection/heartbeat.c if (hb_process_master_request () != NO_ERROR) { hb_process_term (); sleep(1); kill (getpid(), SIGTERM); }불변 조건 — 마스터를 잃은 관리 대상 프로세스는 반드시 종료되어야 한다. hb_process_master_request는 항상 ER_FAILED를 반환하므로(성공 경로 없음) reader는 항상 자신을 kill한다. 고아가 된 copylogdb/applylogdb는 failover된 마스터로 계속 복제해서는 안 된다. 종료 후 새 마스터에 의해 재생성된다.
// hb_process_master_request -- src/connection/heartbeat.c while (false == hb_Proc_shutdown) { po[0].fd = hb_Conn->fd; po[0].events = POLLIN; r = css_platform_independent_poll (po, 1, PRM_ID_TCP_CONNECTION_TIMEOUT*1000); switch (r) { case 0: break; /* timeout: loop, master alive */ case -1: if (!IS_INVALID_SOCKET(hb_Conn->fd) && fcntl(hb_Conn->fd,F_GETFL,status)<0) hb_Proc_shutdown=true; break; /* socket truly broken (guards EINTR) */ default: if (hb_process_master_request_info(hb_Conn)!=NO_ERROR) hb_Proc_shutdown=true; break; } } /* closed */ return (ER_FAILED);hb_process_master_request_info는 css_receive_heartbeat_request를 수행하며 내용을 무시한다. 요청 자체가 liveness probe이기 때문이다. 정상적인 읽기는 루프를 유지하고, 읽기 에러는 hb_Proc_shutdown을 설정하고 종료를 유발하여 자살을 트리거한다. r==0은 양호하며, r==-1은 fcntl이 fd 불량을 확인한 경우에만 종료한다.
flowchart TD init["hb_process_init<br/>(한 번, latched)"] --> conn["hb_connect_to_master"] conn --> reg["hb_register_to_master<br/>SERVER_REGISTER_HA_PROCESS"] reg --> spawn["hb_create_master_reader<br/>detached pthread"] spawn --> loop["hb_process_master_request<br/>poll 루프"] loop -->|"poll timeout r=0"| loop loop -->|"readable, 읽기 성공"| loop loop -->|"읽기 에러 / 소켓 불량"| term["hb_process_term<br/>conn 닫기"] term --> suicide["kill(getpid, SIGTERM)<br/>프로세스 종료"]
Figure 10-3. 한 번 등록하고, 마스터 연결 해제를 자기 종료로 바꾸는 reader 스레드.
hb_deregister_from_master는 클린 종료 시의 graceful 상대방이다. hb_Conn으로 SERVER_DEREGISTER_HA_PROCESS와 자신의 PID를 전송한다. hb_Conn이 없거나 전송 중 하나라도 실패하면 ER_FAILED를 반환한다. 네 가지 wire 기본 연산 — css_send_heartbeat_request/_data, css_receive_heartbeat_request/_data — 은 프레임된 send/css_readn 래퍼로, NULL/무효 소켓이면 CONNECTION_CLOSED, 전송량이 부족하면 ERROR_ON_WRITE/ERROR_ON_READ, 정확한 크기일 때만 NO_ERRORS를 반환한다. 요청은 network-order int 하나이며, 데이터는 불투명한 blob이다.
10.9 레플리카 특수 케이스
섹션 제목: “10.9 레플리카 특수 케이스”레플리카는 ha_replica_list로 설정하며 HB_REPLICA_PRIORITY(0x7FFF — 최저 우선순위, master_heartbeat.h에 정의)로 파싱된다. 두 가지 규칙이 따라온다.
- 마스터로 선출되지 않는다.
hb_cluster_job_calc_score(Chapter 5) — 주기적으로 score 합산 헬퍼hb_cluster_calc_score를 호출하는 잡 — 에서 합산 직후 guard가if (hb_Cluster->state == HB_NSTATE_REPLICA || hb_Cluster->hide_to_demote == true) goto calc_end;를 실행하여 master-isolation, split-brain, failover 전환을 건너뛴다. 최저 우선순위는 만약 score가 계산되더라도 이중 안전장치 역할을 한다. - isolation 판정에 포함되지 않는다.
hb_cluster_is_isolated는HB_NSTATE_REPLICA노드를continue로 건너뛰어, 마스터 하나와 레플리카 하나만 있어도 “isolated”로 판정한다.
종료 시 레플리카는 일반 노드와 동일하다. 동일한 hb_cluster_cleanup 경로를 거치며 다른 피어처럼 작별 heartbeat를 전송한다.
10.10 챕터 요약 — 핵심 정리
섹션 제목: “10.10 챕터 요약 — 핵심 정리”- 운영자는
cub_master와 직접 통신하지 않는다.process_heartbeat는 오직cub_commdb를 생성할 뿐이다. courier(process_activate_heartbeat/process_deactivate_heartbeat/process_reconfig_heartbeat)는 opcode 하나를 전송하고 응답을 출력하며,master_request.c가 이를hb_*함수에 매핑한다. - 비활성화는 4단계 confirm 핸드셰이크다 (
us_hb_deactivate):STOP_ALL→CONFIRM_STOP_ALL폴링 →DEACTIVATE→CONFIRM_NO_SERVER폴링. busy-poll 루프가EXIT_FAILURE(중단)와 “아직 아님”(재시도)을 구분하기 때문에 클린 stop이 눈에 띄게 멈출 수 있다. hb_Deactivate_immediately가 강제와 정중함을 선택한다. 정중 모드는 서버가 플러시할 때까지HA_MAX_PROCESS_DEREG_CONFIRM간격만큼 대기한다.-i(STOP_ALL핸들러만 읽음)는 첫 번째 confirm 패스에서 생존 프로세스에SIGKILL을 보낸다.hb_reload_config는 원자적인 backup-mutate-restore 트랜잭션이다. 호스트명으로 노드별 score/state를 전달하며, 세 가지 검증 실패 중 하나라도 발생하면 이전 리스트를 복원하여 피어가 반만 적용된 토폴로지를 볼 수 없게 한다.- 해체 순서는 고정되어 있다: 잡 큐를 먼저 중단하고 데이터를 해제한다. 리소스 해체가 클러스터보다 먼저다.
hb_resource_shutdown_all_ha_procs는 copy/apply 소켓을 즉시 버리지만cub_server종료는 요청만 하며, 마지막에, 그리고 deadline/force 하에서만 kill한다. - 관리 대상 프로세스는 마스터 연결 손실 시 스스로 종료한다.
hb_process_init이 한 번 등록하고 detached reader를 생성한다.hb_process_master_request는 항상ER_FAILED를 반환하므로 reader는 필연적으로hb_process_term+kill(getpid(), SIGTERM)을 실행한다. - 레플리카는 영구적인 비후보자다. 최저 우선순위
HB_REPLICA_PRIORITY(0x7FFF),hb_cluster_job_calc_score에서HB_NSTATE_REPLICAguard로 상태 전환 전에 단락 처리되며,hb_cluster_is_isolated에서 건너뛰어진다. 단, 해체 시에는 다른 피어와 동일하게 처리된다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
PRM_ID_HA_HEARTBEAT_INTERVAL_IN_MSECS | src/base/system_parameter.h | 226 |
PRM_ID_HA_CALC_SCORE_INTERVAL_IN_MSECS | src/base/system_parameter.h | 227 |
PRM_ID_HA_MAX_HEARTBEAT_GAP | src/base/system_parameter.h | 235 |
SERVER_CHANGE_HA_MODE | src/connection/connection_defs.h | 155 |
css_send_heartbeat_request | src/connection/heartbeat.c | 160 |
css_send_heartbeat_data | src/connection/heartbeat.c | 187 |
css_receive_heartbeat_request | src/connection/heartbeat.c | 211 |
css_receive_heartbeat_data | src/connection/heartbeat.c | 239 |
hb_make_set_hbp_register | src/connection/heartbeat.c | 262 |
hb_register_to_master | src/connection/heartbeat.c | 298 |
hb_thread_master_reader | src/connection/heartbeat.c | 347 |
hb_deregister_from_master | src/connection/heartbeat.c | 378 |
hb_process_master_request_info | src/connection/heartbeat.c | 411 |
hb_process_master_request | src/connection/heartbeat.c | 456 |
hb_connect_to_master | src/connection/heartbeat.c | 588 |
hb_create_master_reader | src/connection/heartbeat.c | 618 |
hb_process_init | src/connection/heartbeat.c | 691 |
hb_process_term | src/connection/heartbeat.c | 755 |
HB_DEFAULT_HEARTBEAT_INTERVAL_IN_MSECS | src/connection/heartbeat.h | 36 |
HB_DEFAULT_CHECK_VALID_PING_SERVER_INTERVAL_IN_MSECS | src/connection/heartbeat.h | 38 |
HB_TEMP_CHECK_VALID_PING_SERVER_INTERVAL_IN_MSECS | src/connection/heartbeat.h | 39 |
HB_DEFAULT_MAX_HEARTBEAT_GAP | src/connection/heartbeat.h | 47 |
HB_JOB_TIMER_IMMEDIATELY | src/connection/heartbeat.h | 50 |
HB_JOB_TIMER_WAIT_A_SECOND | src/connection/heartbeat.h | 51 |
HB_JOB_TIMER_WAIT_500_MILLISECOND | src/connection/heartbeat.h | 52 |
HB_DISK_FAILURE_CHECK_TIMER_IN_MSECS | src/connection/heartbeat.h | 54 |
hb_proc_type | src/connection/heartbeat.h | 60 |
HB_PTYPE_SERVER | src/connection/heartbeat.h | 62 |
HBP_CLUSTER_MESSAGE | src/connection/heartbeat.h | 73 |
HBP_CLUSTER_HEARTBEAT | src/connection/heartbeat.h | 75 |
HB_MAX_GROUP_ID_LEN | src/connection/heartbeat.h | 79 |
HB_MAX_NUM_PROC_ARGV | src/connection/heartbeat.h | 81 |
HB_NODE_STATE | src/connection/heartbeat.h | 86 |
HB_NSTATE_UNKNOWN | src/connection/heartbeat.h | 88 |
HB_NSTATE_SLAVE | src/connection/heartbeat.h | 89 |
HB_NSTATE_TO_BE_MASTER | src/connection/heartbeat.h | 90 |
HB_NSTATE_TO_BE_SLAVE | src/connection/heartbeat.h | 91 |
HB_NSTATE_MASTER | src/connection/heartbeat.h | 92 |
HB_NSTATE_REPLICA | src/connection/heartbeat.h | 93 |
hbp_header | src/connection/heartbeat.h | 114 |
HBP_PROC_REGISTER | src/connection/heartbeat.h | 137 |
hbp_proc_register | src/connection/heartbeat.h | 138 |
process_reconfig_heartbeat | src/executables/commdb.c | 814 |
process_deactivate_heartbeat | src/executables/commdb.c | 852 |
process_deact_confirm_no_server | src/executables/commdb.c | 895 |
process_deact_confirm_stop_all | src/executables/commdb.c | 927 |
process_deact_stop_all | src/executables/commdb.c | 959 |
process_activate_heartbeat | src/executables/commdb.c | 997 |
cluster_Jobs | src/executables/master_heartbeat.c | 246 |
resource_Jobs | src/executables/master_heartbeat.c | 247 |
hb_Deactivate_immediately | src/executables/master_heartbeat.c | 248 |
hb_Deactivate_info | src/executables/master_heartbeat.c | 252 |
hb_cluster_jobs | src/executables/master_heartbeat.c | 259 |
hb_resource_jobs | src/executables/master_heartbeat.c | 272 |
hb_list_add | src/executables/master_heartbeat.c | 351 |
hb_list_remove | src/executables/master_heartbeat.c | 368 |
hb_list_move | src/executables/master_heartbeat.c | 389 |
hb_add_timeval | src/executables/master_heartbeat.c | 412 |
hb_compare_timeval | src/executables/master_heartbeat.c | 431 |
hb_job_queue | src/executables/master_heartbeat.c | 517 |
hb_job_dequeue | src/executables/master_heartbeat.c | 568 |
hb_job_set_expire_and_reorder | src/executables/master_heartbeat.c | 614 |
hb_job_shutdown | src/executables/master_heartbeat.c | 679 |
hb_cluster_job_init | src/executables/master_heartbeat.c | 708 |
hb_cluster_job_heartbeat | src/executables/master_heartbeat.c | 734 |
hb_cluster_is_isolated | src/executables/master_heartbeat.c | 762 |
hb_cluster_is_received_heartbeat_from_all | src/executables/master_heartbeat.c | 785 |
hb_cluster_job_calc_score | src/executables/master_heartbeat.c | 811 |
hb_cluster_job_calc_score | src/executables/master_heartbeat.c | 812 |
hb_cluster_job_check_ping | src/executables/master_heartbeat.c | 992 |
hb_cluster_job_failover | src/executables/master_heartbeat.c | 1163 |
hb_cluster_job_demote | src/executables/master_heartbeat.c | 1236 |
hb_cluster_job_failback | src/executables/master_heartbeat.c | 1351 |
hb_cluster_check_valid_ping_server | src/executables/master_heartbeat.c | 1463 |
hb_cluster_job_check_valid_ping_server | src/executables/master_heartbeat.c | 1500 |
hb_cluster_calc_score | src/executables/master_heartbeat.c | 1556 |
hb_cluster_request_heartbeat_to_all | src/executables/master_heartbeat.c | 1646 |
hb_cluster_send_heartbeat_req | src/executables/master_heartbeat.c | 1677 |
hb_cluster_send_heartbeat_resp | src/executables/master_heartbeat.c | 1696 |
hb_cluster_send_heartbeat_internal | src/executables/master_heartbeat.c | 1702 |
hb_cluster_receive_heartbeat | src/executables/master_heartbeat.c | 1750 |
hb_set_net_header | src/executables/master_heartbeat.c | 1914 |
hb_cluster_job_dequeue | src/executables/master_heartbeat.c | 2061 |
hb_cluster_job_queue | src/executables/master_heartbeat.c | 2075 |
hb_cluster_job_set_expire_and_reorder | src/executables/master_heartbeat.c | 2094 |
hb_add_node_to_cluster | src/executables/master_heartbeat.c | 2130 |
hb_add_ping_host | src/executables/master_heartbeat.c | 2210 |
hb_add_tcp_ping_host | src/executables/master_heartbeat.c | 2246 |
hb_cluster_load_ping_host_list | src/executables/master_heartbeat.c | 2318 |
hb_port_str_to_num | src/executables/master_heartbeat.c | 2362 |
hb_cluster_load_tcp_ping_host_list | src/executables/master_heartbeat.c | 2422 |
hb_return_node_by_name | src/executables/master_heartbeat.c | 2493 |
hb_return_node_by_name_except_me | src/executables/master_heartbeat.c | 2517 |
hb_is_heartbeat_valid | src/executables/master_heartbeat.c | 2535 |
hb_valid_result_string | src/executables/master_heartbeat.c | 2572 |
hb_return_ui_node | src/executables/master_heartbeat.c | 2597 |
hb_add_ui_node | src/executables/master_heartbeat.c | 2629 |
hb_remove_ui_node | src/executables/master_heartbeat.c | 2666 |
hb_cleanup_ui_nodes | src/executables/master_heartbeat.c | 2686 |
hb_cluster_load_group_and_node_list | src/executables/master_heartbeat.c | 2730 |
hb_resource_job_confirm_cleanup_all | src/executables/master_heartbeat.c | 2870 |
hb_resource_job_cleanup_all | src/executables/master_heartbeat.c | 3003 |
hb_resource_job_proc_start | src/executables/master_heartbeat.c | 3071 |
hb_resource_job_proc_dereg | src/executables/master_heartbeat.c | 3203 |
hb_resource_demote_start_shutdown_server_proc | src/executables/master_heartbeat.c | 3307 |
hb_resource_demote_confirm_shutdown_server_proc | src/executables/master_heartbeat.c | 3356 |
hb_resource_demote_kill_server_proc | src/executables/master_heartbeat.c | 3384 |
hb_resource_job_demote_confirm_shutdown | src/executables/master_heartbeat.c | 3416 |
hb_resource_job_demote_start_shutdown | src/executables/master_heartbeat.c | 3494 |
hb_resource_job_confirm_start | src/executables/master_heartbeat.c | 3552 |
hb_resource_job_confirm_dereg | src/executables/master_heartbeat.c | 3702 |
hb_resource_job_change_mode | src/executables/master_heartbeat.c | 3791 |
hb_resource_job_dequeue | src/executables/master_heartbeat.c | 3858 |
hb_resource_job_queue | src/executables/master_heartbeat.c | 3872 |
hb_resource_job_set_expire_and_reorder | src/executables/master_heartbeat.c | 3891 |
hb_alloc_new_proc | src/executables/master_heartbeat.c | 3925 |
hb_remove_proc | src/executables/master_heartbeat.c | 3957 |
hb_return_proc_by_args | src/executables/master_heartbeat.c | 3992 |
hb_return_proc_by_pid | src/executables/master_heartbeat.c | 4014 |
hb_return_proc_by_fd | src/executables/master_heartbeat.c | 4037 |
hb_proc_make_arg | src/executables/master_heartbeat.c | 4061 |
hb_cleanup_conn_and_start_process | src/executables/master_heartbeat.c | 4089 |
hb_is_registered_process | src/executables/master_heartbeat.c | 4203 |
hb_register_new_process | src/executables/master_heartbeat.c | 4238 |
hb_resource_send_changemode | src/executables/master_heartbeat.c | 4356 |
hb_resource_receive_changemode | src/executables/master_heartbeat.c | 4444 |
hb_resource_check_server_log_grow | src/executables/master_heartbeat.c | 4518 |
hb_resource_send_get_eof | src/executables/master_heartbeat.c | 4577 |
hb_resource_receive_get_eof | src/executables/master_heartbeat.c | 4605 |
hb_thread_cluster_worker | src/executables/master_heartbeat.c | 4659 |
hb_thread_cluster_reader | src/executables/master_heartbeat.c | 4704 |
hb_thread_resource_worker | src/executables/master_heartbeat.c | 4769 |
hb_thread_check_disk_failure | src/executables/master_heartbeat.c | 4811 |
hb_cluster_job_initialize | src/executables/master_heartbeat.c | 4912 |
hb_cluster_initialize | src/executables/master_heartbeat.c | 4952 |
hb_resource_initialize | src/executables/master_heartbeat.c | 5072 |
hb_resource_job_initialize | src/executables/master_heartbeat.c | 5104 |
hb_thread_initialize | src/executables/master_heartbeat.c | 5146 |
hb_master_init | src/executables/master_heartbeat.c | 5250 |
hb_resource_shutdown_all_ha_procs | src/executables/master_heartbeat.c | 5343 |
hb_resource_cleanup | src/executables/master_heartbeat.c | 5401 |
hb_resource_shutdown_and_cleanup | src/executables/master_heartbeat.c | 5433 |
hb_cluster_cleanup | src/executables/master_heartbeat.c | 5446 |
hb_cluster_shutdown_and_cleanup | src/executables/master_heartbeat.c | 5496 |
hb_reload_config | src/executables/master_heartbeat.c | 5576 |
hb_kill_process | src/executables/master_heartbeat.c | 6242 |
hb_deregister_by_pid | src/executables/master_heartbeat.c | 6346 |
hb_deregister_by_args | src/executables/master_heartbeat.c | 6396 |
hb_deregister_process | src/executables/master_heartbeat.c | 6441 |
hb_prepare_deactivate_heartbeat | src/executables/master_heartbeat.c | 6515 |
hb_deactivate_heartbeat | src/executables/master_heartbeat.c | 6557 |
hb_activate_heartbeat | src/executables/master_heartbeat.c | 6599 |
hb_enable_er_log | src/executables/master_heartbeat.c | 6723 |
hb_start_deactivate_server_info | src/executables/master_heartbeat.c | 7167 |
hb_is_deactivation_ready | src/executables/master_heartbeat.c | 7187 |
hb_get_deactivating_server_count | src/executables/master_heartbeat.c | 7213 |
HB_PING_RESULT | src/executables/master_heartbeat.h | 45 |
HB_PING_SUCCESS | src/executables/master_heartbeat.h | 48 |
HB_PING_FAILURE | src/executables/master_heartbeat.h | 51 |
HB_CLUSTER_JOB | src/executables/master_heartbeat.h | 62 |
HB_CJOB_INIT | src/executables/master_heartbeat.h | 64 |
HB_CLUSTER_JOB | src/executables/master_heartbeat.h | 64 |
HB_RESOURCE_JOB | src/executables/master_heartbeat.h | 76 |
HB_RESOURCE_JOB | src/executables/master_heartbeat.h | 77 |
HB_RJOB_CHANGE_MODE | src/executables/master_heartbeat.h | 82 |
HB_PROC_STATE | src/executables/master_heartbeat.h | 93 |
HB_REPLICA_PRIORITY | src/executables/master_heartbeat.h | 119 |
HB_NODE_SCORE_MASTER | src/executables/master_heartbeat.h | 122 |
HB_NODE_SCORE_TO_BE_MASTER | src/executables/master_heartbeat.h | 123 |
HB_NODE_SCORE_SLAVE | src/executables/master_heartbeat.h | 124 |
HB_NODE_SCORE_UNKNOWN | src/executables/master_heartbeat.h | 125 |
HB_BUFFER_SZ | src/executables/master_heartbeat.h | 127 |
HB_MAX_NUM_NODES | src/executables/master_heartbeat.h | 128 |
HB_MAX_PING_CHECK | src/executables/master_heartbeat.h | 130 |
HB_MAX_WAIT_FOR_NEW_MASTER | src/executables/master_heartbeat.h | 131 |
HB_MAX_CHANGEMODE_DIFF_TO_TERM | src/executables/master_heartbeat.h | 132 |
HB_MAX_CHANGEMODE_DIFF_TO_KILL | src/executables/master_heartbeat.h | 133 |
HB_NOLOG_DEMOTE_ON_DISK_FAIL | src/executables/master_heartbeat.h | 155 |
HB_VALID_RESULT | src/executables/master_heartbeat.h | 161 |
HB_VALID_NO_ERROR | src/executables/master_heartbeat.h | 163 |
HB_GET_ELAPSED_TIME | src/executables/master_heartbeat.h | 176 |
HB_IS_INITIALIZED_TIME | src/executables/master_heartbeat.h | 180 |
HB_PROC_RECOVERY_DELAY_TIME | src/executables/master_heartbeat.h | 183 |
HB_UI_NODE_CLEANUP_TIME_IN_MSECS | src/executables/master_heartbeat.h | 185 |
hb_list | src/executables/master_heartbeat.h | 191 |
hb_node_entry | src/executables/master_heartbeat.h | 200 |
hb_ping_host_entry | src/executables/master_heartbeat.h | 216 |
hb_ui_node_entry | src/executables/master_heartbeat.h | 228 |
hb_cluster | src/executables/master_heartbeat.h | 242 |
is_ping_check_enabled | src/executables/master_heartbeat.h | 261 |
num_ping_hosts | src/executables/master_heartbeat.h | 264 |
HB_PROC_ENTRY | src/executables/master_heartbeat.h | 272 |
changemode_rid | src/executables/master_heartbeat.h | 292 |
changemode_gap | src/executables/master_heartbeat.h | 293 |
hb_resource | src/executables/master_heartbeat.h | 307 |
hb_cluster_job_arg | src/executables/master_heartbeat.h | 321 |
hb_resource_job_arg | src/executables/master_heartbeat.h | 329 |
hb_job_arg | src/executables/master_heartbeat.h | 345 |
struct hb_job_entry | src/executables/master_heartbeat.h | 354 |
hb_job_entry | src/executables/master_heartbeat.h | 355 |
hb_job | src/executables/master_heartbeat.h | 370 |
struct hb_job | src/executables/master_heartbeat.h | 370 |
hb_Cluster | src/executables/master_heartbeat.h | 382 |
css_process_deactivate_heartbeat | src/executables/master_request.c | 1527 |
css_process_deact_confirm_no_server | src/executables/master_request.c | 1625 |
css_process_deact_stop_all | src/executables/master_request.c | 1691 |
css_process_activate_heartbeat | src/executables/master_request.c | 1782 |
us_hb_deactivate | src/executables/util_service.c | 3948 |
process_heartbeat_stop | src/executables/util_service.c | 4636 |
process_heartbeat_deregister | src/executables/util_service.c | 4718 |
process_heartbeat_status | src/executables/util_service.c | 4773 |
process_heartbeat_reload | src/executables/util_service.c | 4881 |
process_heartbeat | src/executables/util_service.c | 5069 |
log_lsa | src/transaction/log_lsa.hpp | 35 |
Sources
섹션 제목: “Sources”cubrid-heartbeat.md— 상위 개요 문서. 함께 참고할 문서:cubrid-ha-replication.md(failover가 보호하는 복제 구조),cubrid-master-process.md.- 원시 분석 자료:
raw/code-analysis/cubrid/distributed/heartbeat/. - 소스 코드:
src/executables/master_heartbeat.{c,h},src/connection/heartbeat.{c,h},util_service.c,commdb.c. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md.