본문으로 건너뛰기
operator 배포 플레이북 — 로컬 NVMe 실전 구성

operator 배포 플레이북 — 로컬 NVMe 실전 구성

Altinity operator에 CHI를 적용해도 EC2의 NVMe가 자동으로 ClickHouse 데이터 볼륨이 되지는 않습니다. 노드에서 디스크를 포맷·마운트하고 local PV로 노출한 뒤, CHI의 PVC가 그 PV에 바인딩돼야 합니다. Keeper는 별도의 gp3 볼륨을 사용합니다.

아래 예시는 EKS에서 i7i/i8g 로컬 NVMe, ReplicatedMergeTree, S3 cold tier를 조합하는 배포 순서입니다. operator 선택과 스토리지 설계를 매니페스트로 옮깁니다. 조사 버전은 Altinity operator 0.27.1, CRD는 clickhouse.altinity.com/v1 / clickhouse-keeper.altinity.com/v1입니다.

YAML은 구성 필드를 설명하는 예시입니다. # [미확인] 주석이 있는 값과 생략 기호는 배포 환경에서 확정해야 하며, 전체를 실행 검증한 매니페스트는 아닙니다. ✓는 CRD·공식 예제·릴리스 노트에서 확인한 내용, ≈는 설계 판단, ?는 추가 확인이 필요한 항목입니다. 근거는 출처에 정리했습니다. 배포 후 변경 절차는 운영과 복구에서 다룹니다.

배포 청사진 — 5계층과 순서

디스크 준비는 노드와 provisioner가 맡고, operator는 준비된 StorageClass를 참조해 CHK/CHI를 구성합니다. 따라서 PVC가 Pending이면 CHI만 수정하기보다 아래 순서에서 어느 단계가 끝나지 않았는지 확인합니다 ✓.

[1] 노드 부트스트랩(userData)   mkfs.xfs → /mnt/fast-disks/<uuid> 마운트     책임: Karpenter EC2NodeClass
        ▼                       (i7i/i8g.4xlarge = 단일 디스크 → RAID0 불필요)
[2] local PV provisioner        DaemonSet이 /mnt/fast-disks 감시 → Local PV 자동 생성   책임: local-static-provisioner
        ▼                       (PV에 nodeAffinity + storageClassName: fast-disks)
[3] StorageClass                fast-disks(no-provisioner, WaitForFirstConsumer) / gp3(ebs.csi)   책임: 클러스터 관리자
        ▼
[4] CHK + CHI 매니페스트         CHK(Keeper 3노드, gp3) → CHI(shard×replica, fast-disks)   책임: Altinity operator
        ▼                       volumeClaimTemplates[].storageClassName 로 [3] 참조
[5] Pod 스케줄 시점             파드가 간 노드의 로컬 PV에 PVC late-bind → /var/lib/clickhouse 마운트

배포 순서:  operator 설치(CRD 포함) → StorageClass 2종 → 스토리지 NodePool → CHK apply → CHK Ready 확인
           → CHI apply(zookeeper.keeper 이름 참조) → 스키마·anti-affinity·PDB 확인 → 백업 사이드카·모니터링

참조 아키텍처는 스토리지 페이지의 참조 배치를 operator 관점으로 구체화합니다 — clickhouse-data NodePool(i8g/i7i.4xlarge, On-Demand, taint dedicated=clickhouse) + clickhouse-keeper NodePool(소형, gp3, 멀티 AZ) + clickhouse-backup 사이드카 → S3.

1. 사전 준비 — 노드·프로비저너·StorageClass

노드 마운트와 local PV 발견을 확인한 뒤 CHK/CHI를 적용합니다. PV가 없거나 StorageClass 이름이 다르면 데이터 파드는 시작되지 않습니다.

스토리지 노드풀 — taint / label / userData

예시는 i8g/i7i.4xlarge의 단일 3,750GB NVMe를 사용합니다. dedicated=clickhouse:NoSchedule taint와 workload=clickhouse label을 붙이고, userData에서 mkfs.xfs 후 /mnt/fast-disks/<uuid>에 마운트합니다. 이 구성은 instanceStorePolicy: RAID0를 사용하지 않습니다. 해당 옵션이 준비하는 kubelet ephemeral-storage와 ClickHouse PV는 별도로 관리하기 때문입니다(로컬 PV 설명).

노드 교체 때는 데이터 재수화가 발생합니다. On-Demand/Reserved를 사용하고 do-not-disrupt와 consolidationPolicy: WhenEmpty로 자발적 중단을 제한합니다. 대형 노드로 합치기 전에 노드당 복구 시간을 측정합니다.

local PV provisioner 선택

provisioner언제 쓰나비고
local-static-provisioner (기본 권장)노드 NVMe 전부 사용, 안정 최우선AWS 공식 DB 레시피 ✓
TopoLVM한 노드 NVMe를 여러 PVC로 분할·용량 격리·온라인 확장 필요LVM 기반, capacity-aware ✓
OpenEBS LocalPV-LVM/Device이미 OpenEBS 생태계이거나 thin·변종 필요TopoLVM과 기능 동급 ✓

세부: local-static-provisioner는 no-provisioner 방식으로 1 PV = 1 디스크/배열, 사전 마운트가 필수이며 DB 정석 레시피입니다 ✓. TopoLVM은 topolvm.io CRD로 LVM VG에서 LV를 동적 절단하고 capacity-aware 스케줄링·allowVolumeExpansion을 지원하며 cert-manager에 의존합니다 ✓. OpenEBS는 LVM(local.csi.openebs.io) 또는 Device(openebs.io/local, cas-type: local) 방식입니다 ✓.

ClickHouse 전용 노드에서 디스크 하나를 통째로 쓴다면 local-static-provisioner로 구성합니다. 디스크를 여러 PVC로 나누거나 볼륨을 확장해야 한다면 LVM 계열을 검토합니다. 이때도 실제 VG에 여유 공간이 있어야 하며, 물리 용량이 고정된 local PV를 CHI 설정만으로 늘릴 수는 없습니다(§2.3).

StorageClass 2종 — 로컬 CH용 + gp3 Keeper/로그용

두 개를 만듭니다. 로컬 SC는 반드시 WaitForFirstConsumer — 로컬 PV는 nodeAffinity로 특정 노드에 묶여 있어 파드 스케줄 전에 바인딩하면 스케줄러가 노드 제약을 반영 못 해 엉뚱한 노드로 가거나 Pending에 빠집니다(k8s 공식은 이 모드를 “recommended"로 서술하나 로컬에선 사실상 필수) ✓.

# (1) ClickHouse 데이터용 — 로컬 NVMe
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: fast-disks }
provisioner: kubernetes.io/no-provisioner   # 로컬 볼륨은 동적 프로비저닝 없음
volumeBindingMode: WaitForFirstConsumer      # 로컬 PV 필수
reclaimPolicy: Delete                        # SC 레벨은 Delete, CHI VCT에서 Retain으로 이중 보호(§2.3)
---
# (2) Keeper 데이터 + CH 로그용 — 영속 EBS
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: gp3 }
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true                   # gp3는 온라인 확장 가능
reclaimPolicy: Delete
parameters: { type: gp3 }
곁가지 — provisioner 없이 직접 마운트 (hostPath / emptyDir)

provisioner 없이 직접 마운트 — hostPath / emptyDir(로컬 강조) ✓. Altinity 공식 예제는 provisioner를 거치지 않는 로컬 경로도 시연합니다: 11-local-storage-01/02-*-host-path(hostPath 데이터 디렉토리 직접 마운트), 03-persistent-volume-09-with-template-emptydir(노드 로컬 임시 볼륨). hostPath는 PV/provisioner 없이 노드 디렉토리를 그대로 붙여 PoC·단일 노드엔 최단 경로지만 node affinity·용량 회계·권한을 손으로 져야 합니다. emptyDir은 파드 수명과 함께 사라져 stateful CH 데이터엔 부적합(rolling update 예제 전용). 프로덕션 로컬 NVMe는 provisioner 경로(local-static-provisioner)가 정석이고 hostPath/emptyDir은 “provisioner 세우기 전 빠른 검증"이나 재생성 가능 데이터에 한정합니다 ≈.

2. 핵심 배포 — CHK + CHI

배포 순서상 Keeper(CHK)를 먼저 Ready로 만든 뒤 CHI가 zookeeper.keeper 이름으로 붙습니다.

CHK — Keeper 3노드 (gp3 영속)

layout.replicasCount: 3(1 장애 허용, 프로덕션 최소). 데이터는 gp3 — 로컬 NVMe에 두면 노드 소실 시 Raft 로그/스냅샷이 날아가 quorum 복구가 번거롭습니다. 소량(20Gi급)이면 충분하고 저지연 fdatasync가 관건입니다 ✓.

apiVersion: "clickhouse-keeper.altinity.com/v1"
kind: "ClickHouseKeeperInstallation"
metadata:
  name: analytics-keeper
  annotations: { prometheus.io/port: "7000", prometheus.io/scrape: "true" }
spec:
  defaults:
    templates: { podTemplate: keeper-pod, dataVolumeClaimTemplate: keeper-data }
  configuration:
    clusters:
      - name: keeper
        layout: { replicasCount: 3 }        # 홀수 3노드 정족수. 더 높은 가용성은 5노드
    settings:                               # Keeper config.xml
      keeper_server/tcp_port: "2181"
      listen_host: "0.0.0.0"
      keeper_server/four_letter_word_white_list: "*"   # ruok/imok 라이브니스(0.27.0+)
      prometheus/endpoint: "/metrics"
      prometheus/port: "7000"
      prometheus/metrics: "true"
  templates:
    podTemplates:
      - name: keeper-pod
        spec:                               # 표준 PodSpec — Keeper끼리 서로 다른 노드/AZ
          affinity:
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchExpressions:
                      - { key: "app", operator: In, values: ["clickhouse-keeper"] }
                  topologyKey: "kubernetes.io/hostname"
          containers:
            - name: clickhouse-keeper
              image: "clickhouse/clickhouse-keeper:24.8"   # 태그 고정(프로덕션)
              resources:
                requests: { memory: "256M", cpu: "1" }
                limits:   { memory: "4Gi",  cpu: "2" }      # Keeper는 4GB면 충분
    volumeClaimTemplates:
      - name: keeper-data                    # → /var/lib/clickhouse-keeper
        spec:
          accessModes: ["ReadWriteOnce"]
          storageClassName: gp3              # ← CH는 fast-disks(로컬), Keeper는 gp3(영속)
          resources: { requests: { storage: 20Gi } }
곁가지 — CHK 자동 관리 · 포트 기본값 · suspend

CHK가 pod ordinal별 server_id(Raft peer), quorum/startup, 4LW 라이브니스를 자동 관리합니다(수동 STS 대비 Raft 실수 제거). hostTemplates로 포트를 바꾸지 않으면 operator CHK 관례 기본은 zkPort 2181 / raftPort 9444입니다(9181/9234는 독립형 Keeper의 네이티브 기본값) ✓. CHK 전용 수명주기 필드로 spec.suspend(리컨사일 일시중지, CHI의 stop에 대응)가 있습니다 ✓.

정족수 산술 — 왜 3, 언제 5 ✓. Keeper는 데이터를 나르지 않는 소규모 조정 계층이지만 정족수를 잃으면 replication 조정·DDL·INSERT가 전부 멈춰 클러스터 전체의 쓰기 가용성이 정지하는 숨은 SPOF입니다(정족수를 실제로 잃었을 때의 증상·복구 런북은 변경관리·복구 §Keeper 정족수 상실). 3/5노드·gp3·멀티 AZ 결정은 모두 이 SPOF 방어에서 나옵니다. Raft 과반은 floor(N/2)+1, 견디는 동시 유실은 floor((N-1)/2)입니다.

노드 수 N과반(쓰기 가능 최소)견디는 동시 유실비고
110단일 장애 = 전면 정지. 금지
321프로덕션 최소, 3 AZ 각 1대
4313노드 대비 견딤 이득 없이 비용·복제 지연만↑ → 무의미
532높은 가용성. 재수화 위험 창 중 2차 장애 방어

3노드와 4노드는 모두 한 대 장애를 허용합니다. 5노드가 되면 두 대 장애를 허용하므로 가용성 요구가 높을 때 선택합니다. AZ 장애를 고려한다면 각 AZ에 놓인 노드 수까지 세어 과반이 남는지 확인합니다. Keeper의 gp3 복구와 ClickHouse 데이터 재수화는 서로 다른 절차입니다.

CHI — 범용 분석 클러스터 (로컬 NVMe, 데이터/로그 분리)

2 shard × 2 replica, 데이터=로컬 NVMe(fast-disks), 로그=gp3, CHK 이름 참조, anti-affinity(hostname+zone 이중), storageManagement Retain, 볼륨 securityContext ✓.

apiVersion: "clickhouse.altinity.com/v1"
kind: "ClickHouseInstallation"
metadata:
  name: analytics
spec:
  defaults:
    storageManagement:
      provisioner: StatefulSet          # 로컬 NVMe: 확장 불가 → 기본값으로 충분(§2.3)
      reclaimPolicy: Retain             # 실수 삭제 방어(STS/CHI 삭제해도 PVC 잔존)
    templates:
      podTemplate: ch-nvme
      dataVolumeClaimTemplate: data-nvme      # → /var/lib/clickhouse
      logVolumeClaimTemplate:  log-gp3        # → /var/log/clickhouse-server
      serviceTemplate: ch-svc
  configuration:
    zookeeper:
      keeper: { name: analytics-keeper }  # 위 CHK를 이름으로 참조(0.27.0+ 권장)
      session_timeout_ms: 30000
    clusters:
      - name: main
        pdbManaged: "yes"               # PDB operator 자동 생성(§4 pdbManaged)
        pdbMaxUnavailable: 1            # 한 번에 pod 1개만 down → shard 정족수 보호
        layout:
          shardsCount: 2
          replicasCount: 2              # 로컬 NVMe 하한: shard당 replica ≥ 2(내구성)
    settings:                           # config.xml (config.d)
      max_concurrent_queries: 200
      logger/level: information
    users:                              # users.xml (users.d) — 시크릿 참조(평문 금지)
      app/k8s_secret_password: default/ch-secret/password_sha256
      app/networks/ip: ["10.0.0.0/8"]
      app/profile: default
  templates:
    podTemplates:
      - name: ch-nvme
        zone:                           # AZ 핀(선택) → nodeAffinity로 렌더
          key: "topology.kubernetes.io/zone"
          values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
        podDistribution:
          - { type: ShardAntiAffinity, topologyKey: "kubernetes.io/hostname" }        # 같은 shard replica를 다른 노드로
          - { type: ShardAntiAffinity, topologyKey: "topology.kubernetes.io/zone" }   # 같은 shard replica를 다른 AZ로
        spec:                           # 표준 PodSpec
          securityContext:              # 로컬/hostPath 데이터 디렉토리 권한(03-persistent-volume-07-security-context)
            fsGroup: 101                # [미확인] 이미지의 clickhouse uid/gid에 맞춰 조정
            runAsUser: 101
            runAsGroup: 101
          nodeSelector: { workload: clickhouse }   # 스토리지 노드풀 label(§1)
          tolerations:
            - { key: dedicated, operator: Equal, value: clickhouse, effect: NoSchedule }
          containers:
            - name: clickhouse
              image: clickhouse/clickhouse-server:24.8   # ClickStack 병용 시 24.8 LTS+
              resources:
                requests: { cpu: "8", memory: "60Gi" }   # R-type 8GB:1core
                limits:   { cpu: "8", memory: "60Gi" }
              volumeMounts:              # VCT 이름과 정확히 일치해야 바인딩
                - { name: data-nvme, mountPath: /var/lib/clickhouse }
                - { name: log-gp3,   mountPath: /var/log/clickhouse-server }
    volumeClaimTemplates:
      - name: data-nvme
        reclaimPolicy: Retain            # 로컬 데이터 보호(VCT 개별 override)
        spec:
          accessModes: ["ReadWriteOnce"] # 로컬 PV는 RWO
          storageClassName: fast-disks   # WaitForFirstConsumer 로컬 SC(§1)
          resources: { requests: { storage: 3400Gi } }   # 3.75TB 중 여유 제외 [미확인: 실측 조정]
      - name: log-gp3                     # 로그는 작은 gp3로 분리(로컬 NVMe를 데이터 전용으로)
        spec:
          accessModes: ["ReadWriteOnce"]
          storageClassName: gp3
          resources: { requests: { storage: 50Gi } }
    serviceTemplates:
      - name: ch-svc
        spec:
          type: ClusterIP
          ports:
            - { name: http, port: 8123 }
            - { name: tcp,  port: 9000 }

layout을 선언하면 operator가 remote_servers와 per-host macros({shard}/{replica}/{cluster})를 자동 렌더하므로 config.d에 손으로 remote_servers를 쓸 필요가 없습니다 ✓. 데이터 mountPath /var/lib/clickhouse는 확정, 로그 자동 mountPath는 슬롯명 기반 기본값이라 위처럼 volumeMounts에 명시하면 확실합니다 ≈.

RF 선택 — RF2 vs RF3 (임의 2대 유실을 견디나)

위 예제의 replicasCount: 2(RF2)는 shard당 replica 2벌을 뜻하고 이 값이 곧 “몇 대까지 죽어도 데이터가 사는가"를 정하는 가용성 결정입니다. RMT는 shard 단위로 데이터를 나눠 갖고 replica끼리만 사본을 공유하므로 fault tolerance는 shard 안에서 셉니다 ✓.

RF2RF3
shard당 사본23
복제 완료된 파트의 원본이 남는 손실 수(shard당)1대2대
재수화 창 중 잔여 사본1(실질 RF1)2
비용 배수(노드·NVMe·cross-AZ 복제)×2×3

RF2는 “아무 2대나 죽어도 안전"을 보장하지 않습니다. RF2 × shard 3 = 6대 구성에서 임의로 2대가 동시에 죽는 경우의 수는 6C2 = 15, 그중 하필 같은 shard의 두 replica인 조합은 shard마다 1쌍씩 3쌍 → 3/15 ≈ 20%. 이 20%를 뽑으면 그 shard는 사본이 0이 되어 데이터를 잃고, 나머지 80%는 서로 다른 shard라 무사합니다. 모든 파트가 세 replica에 복제된 상태라면 RF3에서 같은 shard 두 대를 잃어도 한 벌이 남습니다 ✓⁽조합 산술⁾. quorum 2로 ACK한 직후 그 두 replica를 모두 잃는 경우까지 무손실을 보장하는 계산은 아닙니다.

위 20%는 여섯 노드 중 두 노드를 동일 확률로 고른다는 조건의 조합 계산입니다. 실제 장애 발생 확률을 뜻하지 않으며 AZ별 배치와 상관 장애도 반영하지 않습니다. RF2에서 RF3로 늘리면 노드·NVMe 사본 비용은 1.5배가 되고 복제 트래픽도 늘어납니다.

노드 두 대 유실 후에도 사본을 남겨야 하거나, 재수화 시간이 길어 추가 장애에 노출되는 시간이 크다면 RF3를 검토합니다. AZ 장애 내성은 RF만으로 결정되지 않으므로 아래 배치 조건을 함께 적용합니다. 다른 데이터스토어의 RF 선택도 참고할 수 있습니다.

쓰기 내구성 노브 — insert_quorum

RF는 “몇 벌 두나"를 정하고 insert_quorum은 “쓰기를 몇 벌 확정된 뒤 ack하나"를 정합니다 — 다른 축입니다. 기본 RMT 쓰기는 비동기로, Keeper 로그에 파트 등록 ack만 나면 클라이언트에 성공을 돌려주고 나머지 replica는 뒤따라 fetch합니다(operator 페이지의 복제 설명) ✓. 다른 replica의 응답을 기다리지 않지만 ack 직후 그 replica 노드가 소실되면 아직 복제되지 않은 파트는 유실될 수 있고 뒤처진 replica를 읽으면 stale 결과가 나옵니다.

설정효과트레이드오프
(기본, async, ack=1)다른 replica의 쓰기 확인을 기다리지 않음ack 후 즉시 노드 소실 시 미복제 파트 손실 가능
insert_quorum: 2최소 2 replica 확정 후 ack → 내구성↑확정 가능 replica가 정족수 미달이면 쓰기 차단(가용성↓)
insert_quorum_timeoutquorum 대기 상한(ms)짧으면 쓰기 실패↑, 길면 쓰기 지연↑
select_sequential_consistency: 1quorum 반영 전 데이터를 읽지 않음읽기 지연·가용성 일부 희생
insert_quorum 계열은 profiles(users.xml)에 ✓. 이 셋은 서버 레벨(config.xml) setting이 아니라 세션/유저 레벨이라 settings(config.d) 대신 configuration.profiles(users.xml)에 넣어야 실제 적용됩니다 — settings에 두면 서버가 프로파일 기본값으로 인식하지 못해 무효가 됩니다(대비: §3 티어링의 storage_configuration은 서버 레벨이므로 settings가 맞습니다).
spec:
  configuration:
    profiles:                                        # users.xml 프로파일 — 세션 레벨 노브는 여기에
      default/insert_quorum: "2"                     # [확인됨] 최소 2 replica 확정 후 ack
      default/insert_quorum_timeout: "60000"         # ms
      default/select_sequential_consistency: "1"     # quorum 읽기 일관성(선택)

이 노브는 내구성 vs 가용성을 맞바꿉니다 ≈. 재수화 위험 창과 겹칠 때가 결정적입니다 — RF2에서 한 replica가 재수화 중이면 그 shard의 확정 가능 replica는 1벌뿐이라 insert_quorum: 2를 걸어 뒀다면 창이 닫힐 때까지 그 shard로 들어가는 쓰기가 막힙니다(내구성을 지키려고 가용성을 포기). 반대로 기본 async면 쓰기는 계속되지만 창 동안 들어온 파트는 단일 사본이라 창 중 2차 장애 시 함께 사라집니다. RF3는 재수화 중에도 2벌이 남아 insert_quorum: 2를 유지한 채 쓰기와 내구성을 모두 지킬 여지가 있습니다 — insert_quorum을 실제로 켜려면 RF3가 짝이 되기 쉬운 이유입니다.

배치·분산 강제 — 노드/AZ 1개 소실이 shard 전멸이 되지 않게

replica를 2~3벌 두는 것만으로는 부족합니다 — 그 사본들이 서로 다른 고장 도메인에 놓여야 합니다. 기본 스케줄러·팩킹은 같은 shard의 replica들을 한 노드나 한 AZ에 몰 수 있습니다. 그러면 노드 1대(또는 AZ 1개) 사망이 그 shard 전멸로 번집니다. 이 배치를 우연에 맡기지 않고 설계로 강제하는 것이 위 CHI의 세 필드입니다 ✓:

기제필드막는 것
hostname anti-affinitypodDistribution: ShardAntiAffinity(hostname)같은 shard replica의 노드 co-location
AZ topology spreadShardAntiAffinity(zone) + topologySpreadConstraints같은 shard replica의 AZ 몰림
PDBpdbMaxUnavailable: 1선택된 파드에 대한 Eviction API 기반 동시 중단 제한
  • hostname anti-affinity의 인과: 같은 shard의 replica가 한 노드에 co-locate되면 그 노드 장애가 shard 전멸로 번집니다 — 상세 인과는 Altinity operator 참고.
  • AZ spread의 인과: 각 shard의 replica를 서로 다른 AZ에 배치하면 AZ 하나의 장애 뒤에도 사본을 남길 수 있습니다. 실제 쓰기·조회 지속 여부는 파트 복제 상태, insert_quorum, Keeper 정족수와 서비스 연결 상태까지 확인해야 합니다. spread가 없으면 스케줄러가 한 shard의 replica들을 같은 AZ에 몰 수 있어 AZ 1개 소실 = 그 shard 전멸입니다. RF2를 여러 AZ에 분산해도 장애 AZ에 replica가 있던 shard는 RF1로 내려갑니다. 해당 shard들이 재수화 위험 창에 진입합니다. AZ 장애 중에도 쓰기를 이어가야 한다면 RF3와 쓰기 확인 조건을 함께 봅니다(위 ‘RF 선택’ 절).
  • PDB는 Eviction API를 사용하는 drain 등의 자발적 중단을 제한합니다. StatefulSet이나 operator가 직접 수행하는 롤링 순서를 모두 직렬화하는 장치는 아닙니다. 배포 후 kubectl get pdb -o yaml로 셀렉터와 적용 범위를 확인하고 ?, operator의 reconcile 순서와 복제 지연 확인을 함께 적용합니다. 독립적인 하드웨어 장애는 PDB로 막을 수 없으므로 사본 수와 복구 시간을 별도로 설계합니다.

데이터/로그 볼륨 분리와 storageManagement

볼륨 분리 ✓: VCT를 두 개 만들고 dataVolumeClaimTemplate(→ /var/lib/clickhouse)·logVolumeClaimTemplate(→ /var/log/clickhouse-server)에 각각 지정하면 operator가 자동 매핑합니다. 로컬 볼륨은 1 디스크=1 PV라 같은 로컬 디스크를 data/log로 쪼갤 수 없으므로 로그는 gp3로 빼서 로컬 NVMe를 데이터 전용으로 지키는 편이 자연스럽습니다(같은 로컬 디스크 분할이 필요하면 TopoLVM 계열).

storageManagement ✓:

  • provisioner(값: StatefulSet(기본) | Operator) — 로컬 NVMe 권고는 StatefulSet. Operator는 CSI allowVolumeExpansion 환경에서 파드 재시작 없이 온라인 확장할 때만 쓰며, 로컬 NVMe는 물리적으로 확장 불가라 이점이 없습니다.
  • reclaimPolicy(값: Retain | Delete(기본)) — 로컬 NVMe 권고는 Retain. STS/CHI 삭제·helm uninstall에도 PVC가 잔존해 실수 삭제를 방어합니다. stop: 1은 Replicas=0으로 만들되 PVC는 intact.
주의: Operator provisioner + VCT 크기 변경 시 과거 데이터 손실 회귀(#1385/#457)가 있었습니다. 확장은 스테이징 검증 후에만 ✓.

3. 필드 레벨 티어링 — hot NVMe → S3 cold

스토리지 설계의 hot/cold 볼륨을 CHI에 넣습니다. storage_configuration은 settings 또는 files로 주입하고, 이동 TTL은 테이블 DDL에 둡니다 ✓. 아래 예시의 정확한 키와 값은 공식 03-persistent-volume-08-tiered-s3 예제 및 사용할 서버 버전과 대조해야 합니다.

spec:
  configuration:
    settings:
      # disks — S3 원격 + 로컬 LRU 캐시  [미확인] 정확한 키는 03-persistent-volume-08-tiered-s3로 확정
      storage_configuration/disks/s3_disk/type: s3
      storage_configuration/disks/s3_disk/endpoint: https://ch-cold.s3.ap-northeast-2.amazonaws.com/data/
      storage_configuration/disks/s3_disk/use_environment_credentials: "true"   # IRSA
      storage_configuration/disks/s3_cache/type: cache
      storage_configuration/disks/s3_cache/disk: s3_disk
      storage_configuration/disks/s3_cache/path: /var/lib/clickhouse/s3_cache/
      storage_configuration/disks/s3_cache/max_size: "200Gi"
      # policies — hot(로컬 NVMe) → cold(S3+cache)
      storage_configuration/policies/hot_to_cold/volumes/hot/disk: default
      storage_configuration/policies/hot_to_cold/volumes/cold/disk: s3_cache
-- 테이블에 정책·TTL 적용 (관측성 예: 7일 후 S3로 이동)
CREATE TABLE otel_logs (...) ENGINE = ReplicatedMergeTree
SETTINGS storage_policy = 'hot_to_cold'
TTL toDateTime(timestamp) + INTERVAL 7 DAY TO VOLUME 'cold';

use_environment_credentials로 IRSA 자격증명을 태우고 s3_cache가 없으면 cold 쿼리가 S3 지연에 직접 노출됩니다. 주 이동은 시간 기반 TTL MOVE로 하고 move_factor는 hot 포화 안전판으로만 씁니다(원칙은 스토리지 페이지). self-host는 shared-nothing이라 cold도 replica 수(RF)만큼 S3에 중복 저장되며 zero-copy replication은 프로덕션 금지입니다.

4. 자주 조정하는 CHI 옵션

  • clusters[].layout.shardsCount/replicasCount · 토폴로지 격자 — 용량·내구성 스케일 시 조정. replica↑=자동, shard↑=수동 리샤딩(변경관리·복구 §스케일 out). 로컬 NVMe는 replica ≥ 2 하한.
  • zookeeper.keeper.name · CHK 이름 참조 — 0.27.0+ 항상 권장. 참조 CHK 엔드포인트 변경 시 의존 CHI 자동 재리컨사일.
  • settings / files · config.xml / 임의 XML — 커스텀 설정·dictionary·티어링 시 조정. 반드시 이 필드로만 주입. 외부 볼륨/ArgoCD 직접 마운트는 렌더 충돌 → CrashLoop(#1456).
  • insert_quorum / _timeout / select_sequential_consistency · 쓰기 내구성(ack 전 확정 replica 수) — 미복제 손실을 못 견디는 쓰기에 조정. profiles(users.xml)로 주입(세션 레벨 — settings에 두면 무효). 확정 replica 정족수 미달·재수화 창엔 쓰기 차단(가용성↓). RF3와 짝(§2 ‘쓰기 내구성 노브’).
  • users/profiles/quotas · users.xml — 계정·권한 조정 시. 시크릿은 k8s_secret_password로(평문 금지). 업그레이드 시 clickhouse_operator 프로파일 소실 주의(#1744).
  • podDistribution + topologySpreadConstraints · 배치 강제 — AZ/노드 분산 조정 시. shard-aware는 podDistribution, AZ 균등 하드 제약(whenUnsatisfiable: DoNotSchedule)은 topologySpread. 병용.
  • reconcile.host.wait.replicas.new · 신규 replica catch-up 대기 — scale-out·재수화 시. new: "yes"로 따라잡을 때까지 다음 단계 대기.
  • reconcile.statefulSet.update.onFailure · STS 업데이트 실패 처리 — 롤링 안전장치. rollback(이전 Generation) / abort / ignore.
  • reconcile.statefulSet.recreate.onDataLoss · 볼륨 소실 시 STS 재생성 — 로컬 NVMe 노드 소실 시. recreate로 두면 재수화 자동화의 일부.
  • reconcile.host.drop.replicas.{onLostVolume,active} · 소실 replica의 Keeper 등록 정리 — 재수화 시. onLostVolume: yes + active: no(살아있는 replica는 절대 drop 안 함).
  • pdbManaged / pdbMaxUnavailable · PDB 자동 생성·튜닝 — 항상 적용. pdbMaxUnavailable: 1의 셀렉터와 Eviction API 적용 범위를 확인.
  • stop / taskID / restart / troubleshoot · 운영 제어 노브 — 정지·강제 재조정·디버깅 시. 노드 소실 복구 시 taskID patch로 재조정 트리거(변경관리·복구 §복구 런북).

reconcile hooks — pre/post SQL 자동화

0.27.0에는 reconcile 전후에 SQL을 실행하는 실험적 훅이 추가됐습니다(reconcile.host.hooks / reconcile.cluster.hooks) ✓. 예를 들어 변경 전에 merge를 멈추고 변경 후 다시 시작하는 절차를 선언할 수 있습니다. 아래 하위 구조는 CRD 확인이 남아 있습니다. 실제 롤링 순서와 실패 처리는 변경관리·복구에서 함께 검토합니다.

spec:
  reconcile:
    host:                               # 호스트 재조정 전후
      hooks:
        pre:  [ { sql: { queries: ["SYSTEM STOP MERGES"] } } ]    # [미확인] 하위 구조는 CRD 재확인
        post: [ { sql: { queries: ["SYSTEM START MERGES"] } } ]
    cluster:                            # 클러스터 재조정 전후(target 지정 가능)
      hooks:
        pre:  [ { sql: { queries: ["SYSTEM FLUSH LOGS"] } } ]

5. operator 레벨 튜닝 · 템플릿 재사용 · 워크로드 분리

operator self-config (멀티테넌시·성능)

CHI/CHK와 별개로 operator 자체를 튜닝하는 설정이 있습니다 ✓. ClickHouseOperatorConfiguration CRD(또는 etc-clickhouse-operator-files ConfigMap)로 watchNamespaces(감시 네임스페이스 한정 → 멀티 operator 격리·멀티테넌시), reconcileThreadsNumber(기본 10, 동시 reconcile 상한 → 대규모 다중 CHI 성능)를 조정합니다. 주의: operator는 자기 설정을 self-reconcile하지 않아 변경은 시작 시에만 반영되므로 operator 재시작이 필요합니다.

useTemplates / CHIT — 공유 설정 재사용

관측성 CH와 범용 분석 CH가 공통 설정(podTemplate·VCT·리소스·티어링 등)을 공유한다면, ClickHouseInstallationTemplate(CHIT)에 한 번 정의하고 두 CHI가 useTemplates로 참조해 중복을 없앱니다 ✓(chit-examples 디렉토리, 세부 파일 목록은 ?).

# 공유 템플릿 (한 번 정의)
apiVersion: "clickhouse.altinity.com/v1"
kind: "ClickHouseInstallationTemplate"
metadata: { name: ch-common }
spec:
  templates:
    podTemplates: [ { name: ch-nvme, spec: { ... } } ]
    volumeClaimTemplates: [ { name: data-nvme, spec: { storageClassName: fast-disks, ... } } ]
---
# 각 CHI가 참조
spec:
  useTemplates:
    - { name: ch-common, useType: merge }

관측성 CH vs 범용 분석 CH — 분리

두 워크로드는 별도 CHI(가능하면 별도 노드풀)로 분리 권장 ≈ — 관측성은 고volume append-only ingest·짧은 hot+S3 cold·shard 넉넉히, 범용 분석은 배치/간헐 적재·장기 보존·쿼리 패턴 기준. 하나의 operator가 CHI 2개를 관리하고 Keeper는 공유 CHK를 두 CHI가 참조하되 ZK root path를 다르게(zookeeper.root) 격리하거나 강한 격리가 필요하면 CHK도 분리합니다. ClickStack v2는 공식 operator를 끌고 들어오므로 범용 CH를 Altinity로 통일하려면 ClickStack의 clickhouse.enabled: false로 내장 CH를 끄고 Altinity 관리 CH를 바라보게 합니다(배경은 operator 페이지의 2종 공존 문제, 관측성 프론트 상세는 HyperDX 심층).

6. 안티패턴 · 배포 전 체크리스트

하지 말 것

  • 로컬 NVMe에 Keeper 데이터 → 노드 소실 시 quorum 메타데이터 소실. Keeper는 gp3(영속).
  • shard당 단일 replica로 로컬 NVMe → 노드 소실 = 영구 데이터 손실. replica ≥ 2 필수.
  • config를 외부 볼륨/ArgoCD로 직접 마운트 → operator 렌더와 충돌 CrashLoop(#1456). 반드시 settings/files/users로.
  • instanceStorePolicy: RAID0를 CH PV로 기대 → kubelet ephemeral 전용, PV를 만들지 않음.
  • 4xlarge 단일 디스크에 RAID0 → 이득 없음 + 신형 AL2023 단일 디스크 RAID0 부팅 버그(#2386). 직접 mkfs.xfs.
  • CRD를 helm/kubectl로 삭제 → 모든 CHI/CHK CR 동반 삭제. 업그레이드는 CRD 별도 apply.
  • operator/CH minor 버전 스킵 업그레이드 → 순차로, LTS 징검다리.
  • shardsCount 늘리고 자동 rebalance 기대 → weight 또는 INSERT INTO SELECT.
  • zero-copy replication 사용, 로컬 볼륨에 provisioner: Operator로 확장 시도(물리 확장 불가).
  • 동시에 한 shard의 여러 노드 교체 → 재수화 중 redundancy 0 창에서 데이터 손실 위험.
  • CH 데이터 노드를 Spot으로 → 중단 = 재수화 이벤트. On-Demand/Reserved.
  • emptyDir을 CH 데이터로 → 파드 재시작 시 소실. hostPath는 provisioner 세우기 전 검증용에 한정.
  • kubectl delete chi가 Terminating에서 멈춤 → finalizer hang. reclaimPolicy/PVC 상태를 확인하고 필요 시 finalizer를 수동 제거(Altinity KB “DELETE finalizers”) ✓.

배포 전 점검 리스트

  • operator: Altinity 0.27.1+ 설치, CRD apply 확인. ArgoCD면 ignoreDifferences /status + RespectIgnoreDifferences.
  • 노드풀: i8g/i7i.4xlarge, taint dedicated=clickhouse, label workload=clickhouse, userData mkfs.xfs → /mnt/fast-disks, instanceStorePolicy 미설정, On-Demand, do-not-disrupt.
  • provisioner/SC: local-static-provisioner DaemonSet 기동, fast-disks(no-provisioner, WaitForFirstConsumer) + gp3(ebs.csi, 확장 가능).
  • Keeper: CHK 3노드, 데이터 gp3, podAntiAffinity(노드/AZ), Ready 확인 후 CHI apply.
  • CHI 레이아웃: shard당 replica ≥ 2, podDistribution: ShardAntiAffinity(hostname+zone 이중), zone으로 AZ 핀.
  • 스토리지: storageManagement.provisioner: StatefulSet + reclaimPolicy: Retain, 데이터 fast-disks/로그 gp3 분리, 볼륨 securityContext.fsGroup.
  • 설정 주입: 모든 config는 settings/files/users로만. 시크릿은 k8s_secret_*(평문 금지).
  • 티어링: hot=로컬 NVMe → cold=S3 storage_configuration(+cache) + TTL MOVE TO VOLUME.
  • PDB / 백업 / 모니터링: pdbMaxUnavailable: 1; clickhouse-backup 사이드카(:7171)+CronJob shard-{shard} IRSA; metrics :8888/:7000/:7171 스크레이프. 사이드카·CronJob·ArgoCD 연계의 구성 상세는 변경관리·복구 §모니터링·백업 연계.
  • 보안: 전송구간 TLS를 켤 경우 security.clickhouse.tls(rootCASecretRef 등)로 선언, HTTPS 8443·native 9440 secure 포트 노출 ≈⁽표준 CH secure 포트⁾.
  • 분리·재사용: 관측성/범용을 별도 CHI(+노드풀), 공통은 CHIT useTemplates. ClickStack은 clickhouse.enabled: false로 외부 CH 연결.
  • 검증: apply 후 파드 Running, remote_servers/macros 자동 생성, 스키마 전파, anti-affinity 배치, 노드 소실 리허설(스테이징).

이 배포도가 managed와 어떻게 다른지는 Managed vs Self-hosted, 서고 난 뒤의 스케일·롤링 업그레이드·노드 소실 재수화·Keeper 정족수 복구는 Altinity operator 운영, 실제 프로덕션 운영 사례는 프로덕션 운영 사례에서 이어집니다. 시점 기준 2026-07.

마지막 수정 일자