본문으로 건너뛰기
operator 패턴 — 복제·다운타임·정족수·업그레이드

operator 패턴 — 복제·다운타임·정족수·업그레이드

operator 패턴 — 복제·다운타임·정족수·업그레이드

한눈에

  • 복제: RMT는 멀티마스터라 승격(promotion) 절차가 없다 — replica 하나가 죽어도 살아있는 replica가 read+write를 그대로 계속한다 .
  • 다운타임: EBS-first라 노드 급사는 볼륨 reattach + 델타 catch-up(수 분)이지, 로컬 NVMe의 전량 재수화(수 시간, RF2→실질 RF1)가 아니다 . 단 ungraceful death는 자동 복구되지 않고 개입이 필요하다 .
  • Keeper 정족수: 이 아키텍처의 진짜 SPOF는 데이터 노드가 아니라 Keeper 과반이다 — 잃으면 데이터 노드가 멀쩡해도 CH가 read-only로 전락한다 .
  • 버전/업그레이드: CH·Keeper·operator·HyperDX·OTel Collector·MongoDB 6개가 독립 케이던스로 돈다. 다운그레이드는 “없다고 가정"하고, 직전 EBS 스냅샷 + clickhouse-backup 이중 안전으로 replica 단위 롤백한다.
  • 네 축에 공통인 안전장치: **RF2 + anti-affinity(hostname) + topologySpread(AZ) + PDB(maxUnavailable 1)**가 자발적 중단을 직렬화한다. 단 EBS는 AZ-bound라 AZ 장애만은 reattach로 못 풀고 cross-AZ RF로만 방어된다.

Altinity CHI/CHK 위에 클러스터를 얹으면 그 위에서 네 가지 운영 난이도가 실제로 발생한다 — 복제(누가 죽으면 무슨 일이 나나) · 다운타임(복구가 얼마나 걸리나) · Keeper 정족수(진짜 SPOF가 어디인가) · 버전/업그레이드(6개 구성요소를 어떻게 올리고 되돌리나). 컴포넌트별 가용성이 blast radius를 컴포넌트 단위로 종합했다면, 이 페이지는 그중 ClickHouse/Keeper 두 컴포넌트 내부에서 operator가 실제로 만들어내는 운영 난이도를 한 층 더 파고든다. 각 축의 기준 문서는 operator 토폴로지·다운타임 · Keeper · 복제·멀티마스터·failover · 버전 호환성·업그레이드이고, CHI/CHK 필드 전수·스케일 함정·롤링 런북은 Altinity operator 운영이 기준 문서다. 여기서는 반복 없이 판단에 필요한 수치·표·설정 조각만 추린다.

전제: 1 shard × RF2(2 AZ) + CHK 3노드(3 AZ), HyperDX Only(clickhouse.enabled:false) + Altinity CHI/CHK, 쓰기는 기본 async.

① 복제 — 멀티마스터, 승격 없는 failover

RMT는 shared-nothing이고 각 replica가 완전한 사본을 자기 EBS 볼륨에 갖는다. 복제는 row/statement 스트림이 아니라 part 단위로 동작한다 — INSERT가 로컬에서 즉시 하나의 part로 굳고, 그 존재만 Keeper /log에 등록되면 다른 replica가 그 로그를 보고 part 바이트를 sibling에서 직접 pull한다 . 잠깐 offline이던 replica는 복귀 후 자기 log_pointer 이후 밀린 엔트리만 이어 소비(catch-up)한다.

가장 흔한 오해는 “replica 중 하나가 leader이고 죽으면 승격한다"이다. 틀렸다. is_leader/can_become_leader는 primary 표시가 아니라 머지/뮤테이션 할당 참여 여부이고, 20.6부터 여러 replica가 동시에 leader일 수 있다 . INSERT·ALTER는 애초에 아무 살아있는 replica로나 보낼 수 있으므로(멀티마스터), 죽은 replica가 있어도 “승격"할 대상 자체가 없다.

전통 primary-replicaClickHouse RMT
쓰기 수용 노드primary 1개만모든 replica(멀티마스터)
장애 시 조치standby → primary 승격승격 없음 — 살아있는 replica가 계속
failover 오케스트레이터Patroni·repmgr 등 외부없음
split-brain 방지fencing/STONITHKeeper Raft 정족수 — 소수파는 쓰기 불가

라우팅은 여전히 필요하다 — 죽은 replica로 요청이 안 가야 한다. 우리 기본 경로는 HyperDX → operator가 만든 cluster Service(ClusterIP, 8123/9000)이고, readiness probe(/ping)가 죽은 replica를 엔드포인트에서 뺀다 . 1 shard라 remote_serversload_balancing·Distributed 프록시는 지금은 불필요하고, chproxy는 사용자별 쿼터나 shard 2+가 실제 요구가 될 때 얹는다.

RF2 consolidation의 안전성: replica A를 drain/재부팅하는 동안 replica B가 승격 절차 없이 read+write를 그대로 서빙한다 . 이 창 동안 그 shard는 실질 RF1이라 2차 독립 장애에는 취약하지만, EBS라 창이 수 분으로 짧아 RF2가 방어 가능한 기본값이 된다(②). 동시 하락은 PDB maxUnavailable: 1이 직렬화한다(⑤).

② 다운타임 — EBS reattach가 재수화를 대체한다

전제 뒤집기 한 줄: 로컬 NVMe는 노드 급사=데이터 소실=전량 재수화(수 시간, RF2→실질 RF1)이지만, EBS 볼륨은 인스턴스와 독립된 네트워크 블록 스토리지라 노드가 죽어도 데이터는 볼륨에 남는다. 그래서 EBS-first의 복구 동작은 “재수화"가 아니라 **“detach → 같은 AZ 새 노드에 reattach → Keeper 로그가 가리키는 델타만 catch-up”**이 된다 . 정확한 reattach+part-load 실소요는 hot 데이터량·파트 수에 좌우되며 아직 실측 전이다 ?(staging 리허설로 메운다).

시나리오데이터복구 동작대략 소요개입
계획된 노드 교체(drain, Karpenter voluntary)생존detach → 같은 AZ reattach → catch-up수 분 자동
노드 급사(ungraceful: HW/OS hang)생존파드 Terminating에 무한정 잔류 → 개입 필요무개입 시 무한/6분+, 개입 시 수 분 out-of-service taint 필수
AZ 1개 장애그 AZ만 접근 불가(볼륨 AZ-bound)reattach 자체가 불가AZ 복구까지cross-AZ RF만 방어
롤링 업그레이드/설정 reconcile무영향in-place 재시작, detach 없음replica수 × (재시작+catch-up)자동

ungraceful death가 최대 함정이다. Kubernetes는 죽은 노드의 파드가 정말 멈췄는지 확인할 수 없고, RWO 볼륨 더블 마운트(=데이터 손상)를 막기 위해 컨트롤 플레인은 파드를 Terminating으로 남긴 채 새 파드를 만들지 않는다. force-detach는 6분 뒤 시도되지만 CSI 정합성 때문에 지연될 수 있다. 정석 복구는 out-of-service taint(K8s 1.28 GA)다 :

# 노드가 정말 죽었음을 확인(재부팅 중이 아님 — 오판 시 더블 마운트 위험)한 뒤
kubectl taint nodes <dead-node> node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
# → 파드 강제 삭제 + EBS 즉시 detach → 같은 AZ 새 노드에 reattach → CH startup → 델타 catch-up

읽기·쓰기 자체는 RF2의 다른 replica가 계속 서빙하므로 클러스터 다운은 아니다(저하 상태) — 하지만 개입 없이는 그 replica가 무한정 미가용이라는 점이 EBS 특유의 운영 부담이다. AZ 장애는 EBS·로컬 NVMe 모두 cross-AZ replica가 유일 방어라는 점에서 EBS의 이점이 미치지 않는 유일한 축이다. 상세 S1~S9 시나리오·PDB/probe/reconcile 노브는 operator 토폴로지·다운타임이 기준 문서다.

③ Keeper 정족수 — 데이터 노드가 아니라 조정 계층이 진짜 SPOF

Keeper는 NuRaft로 합의를 돌리고 홀수 노드로 배치한다(floor(N/2)+1이 과반).

Keeper 노드 수과반견디는 손실
321대
431대(3노드와 동일 — 배치 의미 없음)
532대

과반을 잃으면(우리 3노드 기준 2대 소실) SELECT은 계속되지만 INSERT/DDL/머지/뮤테이션은 정지한다 — TABLE_IS_READ_ONLY(에러 코드 242), system.replicas.is_readonly=1로 드러난다 . part 등록·블록번호 배정·복제 로그 기록이 전부 Keeper 쓰기를 요구하므로, 데이터 replica가 셋 다 멀쩡해도 쓰기 경로가 통째로 멈춘다 — 이것이 데이터 노드가 아니라 조정 계층이 진짜 SPOF인 이유다. 정족수 없이 쓰기를 허용하면 일관성을 보장할 수 없으므로 일부러 막는 보호 장치다.

우리 CHK는 gp3 영속 볼륨을 쓰므로, Keeper 노드가 급사해도 Raft 로그/스냅샷이 볼륨에 살아남아 데이터 경로와 동일하게 reattach로 정족수를 되살린다 . 로컬 NVMe였다면 Keeper 노드 급사가 곧 메타데이터 소실이라 앙상블 재구성이 훨씬 번거로웠을 것 — EBS-first는 데이터 경로와 조정 경로의 복구 모델을 통일한다.

한 가지 더 못박을 것: Keeper는 이벤트 데이터를 큐잉하지 않는다. 정족수 상실이 쓰기를 막는 이유는 조정 메타데이터(파트 참조·복제 로그·dedup 체크섬)를 못 쓰기 때문이지, Keeper가 in-flight INSERT를 들고 있다가 잃어서가 아니다 — 이 구분과 ingest 유실 방지 설계(OTel persistent queue·insert_quorum)는 Keeper가 기준 문서다.

④ 버전·업그레이드 — 6구성요소 독립 케이던스, 다운그레이드는 없다고 가정

이 스택은 ClickHouse · Keeper · Altinity operator · HyperDX · OTel Collector · MongoDB 6개가 독립적으로 버전이 돈다. “한 번에 다 올리기"는 원인 추적을 불가능하게 하므로 각자 별도 케이던스로 올린다 .

구성요소핀 정책근거/함정
ClickHouse24.8 LTS(또는 검증된 안정판)ClickStack 최소요구는 24.8+, 차트 기본 태그(25.7)와는 별개 숫자 — self-host HyperDX Only라 우리가 분리 통제
KeeperCH와 동일 태그 정렬(24.8)별도 이미지라 명시 정렬이 필요
Altinity operator0.27.1, minor 단계별로만(0.26→0.27)CH 21.11+·K8s 1.25+ 요구. CRD 삭제는 절대 금지(연쇄 삭제)
HyperDX app차트 appVersion 추종MergeTree 표준 기능만 사용 → CH 하한을 새로 밀어올리는 경우는 드묾
OTel Collector배포판 태그(2.29.0)ClickStack 배포판. persistent queue 확장 포함 여부는 버전마다 재확인 ?
MongoDB5.0.32(차트 기본)메타데이터 전용, 버전 민감도 낮음
매트릭스 함정: operator **0.27.0+**는 async_replication/use_xid_64기본 활성화하는데 이 기능은 **Keeper 25.3+**를 요구한다 . 우리가 CH/Keeper를 24.8 LTS로 핀하면 이 기본값이 충돌할 수 있다 — operator가 Keeper 버전을 감지해 자동 무효화하는지, 아니면 그대로 켜서 오류를 내는지는 배포 스테이징에서 반드시 실동작 검증이 필요하다 ?.

CH는 함부로 못 내린다. 온디스크 파트 포맷이 바뀐 뒤로는 이전 버전이 새 파트를 못 읽어 startup에서 죽는다.

다운그레이드 차단 트리거효과
marks 포맷 변경(25.8)25.8→25.3 롤백 불가(startup fatal) ✓⁽#86837⁾
String with_size_stream 직렬화(25.11)25.10 미만으로 불가 ✓⁽v25.11 BIC⁾
JSON advanced shared data(25.12)25.8 미만으로 불가 ✓⁽v25.12 BIC⁾
OPTIMIZE TABLE ... FINAL 실행파트를 새 포맷으로 재작성 — 롤백 창을 스스로 닫음
compatibility 서버 설정은 명시적으로 안 바꾼 설정의 기본값만 옛 버전 것으로 되돌린다 — 조용한 동작 회귀는 막지만, 온디스크 포맷·바이너리 버전은 되돌리지 못한다. “롤백 노브"가 아니다 .

실질 롤백은 스냅샷/백업뿐이다. EBS-first에서는 업그레이드 직전 데이터 볼륨 EBS 스냅샷이 가장 확실한 롤백 지점이고, clickhouse-backup을 이중 안전으로 건다 ✓⁽AWS⁾. RF2/RF3라 replica 단위로 좁힐 수 있다 — 한 replica씩 스냅샷 → 업그레이드 → 실패 시 그 replica만 스냅샷 복원 → 나머지 healthy replica에서 델타 catch-up .

clickhouse-backup create_remote pre-upgrade-$(date +%Y%m%d)     # 업그레이드 직전 백업(이중 안전 ②)
# 실패 시: 스냅샷에서 gp3 볼륨 복원(원 볼륨과 같은 AZ — EBS는 AZ-bound)
aws ec2 create-volume --snapshot-id snap-0abc... --volume-type gp3 \
  --availability-zone ap-northeast-2a

업그레이드 3규칙: ① 이미지·설정·볼륨확장은 각각 별도 reconcile(동시변경 crash 회피, v0.24.3 함정) , ② 관찰 24~48h 동안 OPTIMIZE FINAL·신규 컬럼 타입 사용 금지로 롤백 창 유지 , ③ 다운그레이드는 “없다고 가정"하고 스냅샷/백업 복구를 유일한 롤백 경로로 취급한다. 6구성요소 매트릭스 전문·ClickStack v1→v2 파괴적 변경은 버전 호환성·업그레이드가, operator 자체 minor 단계·CRD 금지·Keeper 0.26→0.27 무마이그레이션의 일반 런북은 Altinity operator 운영이 기준 문서다.

안전장치가 네 축을 하나로 묶는다

네 축 모두에 공통인 배치 규칙은 하나다 — **RF2 + anti-affinity(hostname) + topologySpread(AZ) + PDB(maxUnavailable 1)**가 자발적 중단(drain·consolidation·롤링)을 “한 번에 한 replica"로 직렬화한다.

spec:
  configuration:
    clusters:
      - name: main
        pdbManaged: "yes"
        pdbMaxUnavailable: 1          # 동시에 2대가 못 내려가게 직렬화
        layout: { shardsCount: 1, replicasCount: 2 }
  templates:
    podTemplates:
      - name: ch-ebs
        podDistribution:
          - { type: ClickHouseAntiAffinity, topologyKey: "kubernetes.io/hostname" }
        spec:
          topologySpreadConstraints:
            - { maxSkew: 1, topologyKey: "topology.kubernetes.io/zone", whenUnsatisfiable: DoNotSchedule }

단 이 안전망에는 구조적 한계가 하나 있다 — EBS는 AZ-bound라 볼륨을 다른 AZ로 못 옮긴다. anti-affinity·topologySpread·PDB는 모두 “자발적” 중단을 직렬화할 뿐이고, AZ 장애처럼 그 AZ 자체가 통째로 죽는 비자발적 사건은 reattach로 풀 수 없다 — 이 지점에서만 cross-AZ RF(복제)가 유일한 방어가 되고 EBS·로컬 NVMe의 처방이 수렴한다. 전문 매니페스트·podDistribution enum·reconcile.* 노브는 operator 토폴로지·다운타임으로 위임한다.

우리 케이스에서는

**토폴로지 shardsCount: 1 × replicasCount: 2(2 AZ) + CHK 3노드(3 AZ)**로 시작한다. 복제는 승격 없는 멀티마스터라 별도 failover 오케스트레이터를 두지 않고, HyperDX → cluster Service(readiness 기반)로 라우팅만 잡는다. 다운타임은 EBS-first가 로컬 NVMe의 전량 재수화 위험을 없애주지만 ungraceful death는 여전히 사람 개입(out-of-service taint)이 필요하므로, node-problem-detector 기반 자동화를 staging에서 먼저 검증한 뒤 팀 룰로 못박는다. Keeper 3노드 정족수는 데이터 노드보다 먼저 지켜야 할 SPOF이므로 gp3 영속·CH와 분리 배치를 타협하지 않는다.

버전은 위 6구성요소 핀 표를 그대로 채택하되, operator 0.27+ 기본값 ↔ Keeper 25.3+ 매트릭스 함정을 배포 전 스테이징 실측 항목으로 못박는다. 업그레이드는 이미지·설정·볼륨확장을 분리한 reconcile + 직전 EBS 스냅샷 + clickhouse-backup 이중 안전 + 24~48h 관찰창으로 진행하고, 다운그레이드는 없다고 가정해 사고 대응 계획을 스냅샷/백업 복구 중심으로 짠다. RF3·5노드 Keeper·chproxy는 이 페이지의 각 트리거(AZ 무저하 요구·insert_quorum:2 상시·2대 손실 허용 요구·shard 2+)가 실제로 넘어올 때만 승급한다. 시점 기준 2026-07.