이중화 (HA) 설정
플랫폼 두 벌(A · B) 을 나란히 두고, 한쪽이 통째로 죽어도 운영 데이터를 한 건도 잃지 않게 하는 구성입니다. 발전소 · 대형 플랜트 · 24/365 무중단 설비를 위한 것입니다.

HA 는 데이터의 가용성 · 일관성 · 무유실을 담당합니다. 설비의 안전 상태는 어떤 소프트웨어 실패에서도 PLC / 안전 PLC(SIS) 가 독립적으로 보장합니다 (IEC 61508 / 61511 SIL). 플랫폼은 안전을 보장하는 주체가 아닙니다.
두 가지 약속
1 · 데이터는 잃지 않는다
한쪽이 죽어 있던 동안 상대에만 쌓인 구간을, 복구된 뒤 놓친 만큼만 되돌려 받습니다. 시간을 계산하거나 추측하지 않습니다 — Kafka 커밋 오프셋 커서가 어디까지 받았는지를 정확히 알고 있기 때문입니다.
2 · 제어는 한 곳으로만 나간다
제어 명령은 데이터와 정반대입니다. 양쪽에 보내지 않고 건강한 단일 리더에만 갑니다. 권한자가 모호하면 보내지 않습니다(fail-closed).
연결이 거부된 것은 전송 전이므로 안전하게 다른 쪽으로 넘깁니다. 그러나 타임아웃은 「갔는지 안 갔는지 모르는」 상태라 재시도하지 않습니다 — 재시도가 곧 이중 작동입니다.
구성 — 엣지-플랫폼 쌍
PLC 하나를 엣지 두 대가 각자 읽어 서로 다른 플랫폼에 보냅니다. 그래서 양쪽 플랫폼이 같은 데이터를 라이브로 받습니다.
한 PLC 를 두 엣지가 독립 수집해 엣지#1은 A 에만, 엣지#2는 B 에만 보냅니다. 플랫폼 하나에 엣지 두 대를 붙이는 것과는 다릅니다 — 그건 중복 저장입니다.
같은 스위치에 물리면 그 스위치가 단일 장애점이라 이중화가 뜻을 잃습니다 → 엣지 이중화
평소에는 아무것도 하지 않습니다
plantpulse-ha 데몬은 각 플랫폼에 상시 기동해 두지만, 정상 상태에서는 트래픽을
대신 흘리지 않습니다. 평상시 양쪽 공급은 위의 엣지 쌍이 합니다. 데몬이 일하는 것은
한쪽이 다운됐다가 복구된 뒤뿐이고, 그때 두 가지를 합니다.
| 하는 일 | 무엇 |
|---|---|
| 시계열 백필 | 피어의 pp-batch 토픽을 구독하다가, 로컬이 복구되면 다운 동안 놓친 구간만 로컬 pp-batch 로 재발행합니다. 기존 batch-web 이 그대로 적재하므로 목적지 코드는 손대지 않습니다 |
| PG 메타 동기화 | 태그 메타 · 자산 구조 · 알람 정의처럼 Kafka 에 실리지 않는 설정을, 주 클러스터의 PG 에서 대기 클러스터가 60초마다 pull 합니다 |
PG 메타 동기화는 현장 태블릿 앱(frontline)의 것도 옮깁니다 — 작업자 계정과 맡은 범위(PIN 해시 포함), 키오스크 설정 · 설비별 정지 이유 목록 · 등록한 태블릿. AI 키는 metasync.include.tokens=true 일 때만 옮기고, 세션 · 감사 로그 · 로그인 캐시는 노드마다 따로 둡니다(frontline 이중화).
event 가 아니라 pp-batch 인 이유pp-batch 는 CEP 를 이미 통과한 뒤의 형태입니다. 그래서 재생해도 목적지에서
CEP 가 다시 발화하지 않습니다. 옛 모델(event 토픽 상시 팬아웃)은 CEP 재발화와
상시 이중 전달 부하 때문에 폐기됐습니다.
백필이 어떻게 도나
INIT → GUARD → WAIT_LOCAL_READY → CATCH_UP → TAIL
↑ ↓
PAUSED ←─────┘ (헬스 상실 / 회복)
| 상태 | 무엇 |
|---|---|
GUARD | 피어와 로컬의 clusterId 가 같으면 기동을 거부합니다 — 자기 자신을 피어로 잡는 사고 방지 |
WAIT_LOCAL_READY | 로컬 server-web 의 헬스가 안정될 때까지 기다립니다 |
CATCH_UP | 경계 시각 T_b 이전 구간을 로컬로 재발행합니다 |
TAIL | T_b 이후는 이미 엣지가 로컬에도 넣었을 구간이라 폐기합니다 |
PAUSED | 헬스를 잃으면 멈추고 커밋하지 않습니다 |
INCIDENT | retention 을 넘겨 못 읽은 구간이 있으면 기록하고 earliest 부터 이어 갑니다 |
전환 기준은 컨슈머 lag 이 아니라 «레코드 타임스탬프 대 고정 경계 T_b» 입니다.
트래픽이 적은 파티션에서 lag 으로 판단하면 오판과 플래핑이 납니다.
TAIL 에서도 60초 지연 커밋을 합니다(commit.lag.seconds) — 헬스 상실이 감지되기
직전의 짧은 창을 다음 CATCH_UP 이 자연히 다시 덮게 하려는 것입니다.
루프는 구조적으로 막혀 있습니다
재발행하는 레코드에 pp-ha-origin 헤더를 답니다. 그 헤더가 붙은 레코드는 다시
재발행하지 않습니다 — 원본(h=0) → 재발행(h=1)까지만이고 h=1 은 멈춥니다.
중복은 흡수됩니다
경계 부근에서 같은 레코드가 두 번 갈 수 있지만, Aster TSDB 재적재가
tag_id + timestamp 기준 멱등이라 태그 포인트 유실도 중복도 남지 않습니다.
보장과 한계
피어의 pp-batch 보존 기간보다 오래 죽어 있으면 그만큼은 자동으로 못 받습니다.
그 구간은 INCIDENT 로 기록되고, 되살리려면 backfill.force.from 으로 수동 재재생
합니다. 보존 기간을 예상 최대 다운시간보다 길게 잡으십시오.
| 한계 | 내용 |
|---|---|
| 통계 카운터는 비멱등 | 태그 포인트 재적재는 완전 멱등이지만 count = count + ? 형태의 통계 카운터는 중복 재생 시 과대 집계될 수 있습니다 |
| 비대칭 장애는 못 고칩니다 | 엣지 한 대만 죽어 한쪽 공급만 끊긴 경우, 로컬 플랫폼 자체는 건강하므로 헬스 게이트가 감지하지 못합니다 → 수동 재재생 |
| PG 동기화는 단방향 | 주 → 대기입니다. 콘솔 설정은 항상 주 클러스터에서만 바꾸십시오 — 대기에서 바꾸면 다음 사이클에 덮어써집니다 |
엣지가 한 대뿐인 사이트 — 이중 전송 클라이언트
엣지를 두 대 놓을 수 없는 현장에서는 수집기가 양쪽 클러스터에 직접 이중 전송합니다.
plantpulse-api-ha 가 기존 APIClient 와 같은 표면을 제공하므로 생성부만 교체하면
됩니다.
- 쓰기는 모든 목적지로 팬아웃하고 즉시 반환합니다(목적지마다 바운드 큐 + 워커 + 백오프).
- 읽기는 리더 우선 + 페일오버 + sticky 로 갑니다.
- 토폴로지는 ha 데몬의
/api/cluster를 10초마다 폴링해 캐시합니다(last-known-good).
운영
# 기동 · 정지 (각 플랫폼에서)
sh/start.sh [설정파일] # 기본 config/plantpulse-ha.properties, 이미 떠 있으면 WARN + exit 0
sh/stop.sh # SIGTERM → 15초 대기 → SIGKILL
sh/log-viewer.sh
배포 레이아웃은 <서비스 루트>/plantpulse-ha/{sh,lib,logs,config,etc} 입니다.
장애가 났을 때
| 상황 | 무엇이 일어나나 |
|---|---|
| 클러스터 B 다운 | A 는 그대로 적재합니다. B 가 복구되면 B 의 데몬이 CATCH_UP 으로 A 에만 쌓인 구간을 읽어 B 로 재발행 → 수렴 |
| 데몬만 다운 | 커밋 오프셋은 데몬이 아니라 피어 브로커에 있습니다. 재시작하면 마지막 커밋부터 이어 갑니다(헬스 게이트는 다시 통과해야 합니다) |
| 주 클러스터에서 설정 변경 | 대기 클러스터가 다음 사이클(60초)에 diff 를 반영합니다 |
함께 보기
엣지 이중화(HA) · 백업 · 복원 · 클러스터 설치