본문으로 건너뛰기
07 ClickHouse 복제 전환 — 레플리카 둘과 Keeper 세 대, 데이터는 제자리에서

ClickHouse 복제 전환 — 레플리카는 워커 둘에, 과반은 세 노드에, 데이터는 제자리에서

hub 클러스터에서 HyperDX가 쓰는 ClickHouse는 node1에 레플리카 하나로 돌았고 테이블은 전부 비복제였습니다. 앞 글에서 워커가 둘이 되고 존 라벨이 붙은 뒤, 이 ClickHouse를 레플리카 2개(node1, node2)와 ClickHouse Keeper 3대(master1, node1, node2)로 바꿨습니다. 기존 데이터(변환 시점에 약 2.7GiB)는 버리지 않고 제자리에서 복제 엔진으로 변환했습니다. 2026-10-05에 조사와 설계, 리허설을 했고 10-06에 운영에 적용했습니다. 10-07 00:10에는 레플리카 1의 콜드 티어를 S3로 바꾸는 작업까지 끝났습니다.

운영에서 확인한 것은 단계마다의 전후 대조입니다. 엔진, 행 수, 파트, 재시작 횟수, 컬렉터의 전송 실패 수를 봤습니다. Keeper를 멈추거나 파드와 볼륨을 지우는 장애 시험은 운영에서 하지 않았고, 같은 네임스페이스에 따로 띄운 시험 구성에서만 했습니다. 노드를 통째로 내리는 시험은 시험 구성에서도 하지 않았습니다. 시험 구성도 운영과 같은 세 노드 위에 떠 있었습니다. 본문에서 ‘리허설’이라고 적은 값은 그 시험 구성의 값이고, ‘로컬 실험’은 노트북에서 같은 버전의 바이너리로 돌린 값입니다.

S3 엔드포인트와 버킷 이름은 자리표시로 바꿨고 비밀값은 싣지 않았습니다. 이 ClickHouse가 관측 경로에서 맡는 자리는 관측 스택 일원화에 있습니다.

출발점

전환 전 구성은 아래와 같았습니다.

  • Altinity clickhouse-operator 0.27.4가 관리하는 ClickHouse 25.3, 1샤드 1레플리카, node1의 로컬 볼륨
  • 테이블 12개(MergeTree 8개, SummingMergeTree 4개)와 materialized view 4개, 전부 비복제
  • 데이터 약 2.4GiB(10-05 조사 시점). 7일이 지난 파트는 SeaweedFS의 S3로 내려가는 계층형 정책 tiered(S3 콜드 티어링의 방식)
  • 쓰는 쪽은 HyperDX 컬렉터, 읽는 쪽은 HyperDX 앱과 알림 CronJob, Grafana. 전부 Service 하나로 붙음

03 글은 로그와 트레이스를 이 ClickHouse 하나로 모으면서 node1 장애의 영향이 커졌다고 적었습니다. 워커가 둘이 됐으니 “멀티마스터로 다시 구성하고 node2의 로컬 디스크를 쓰자"가 이번 작업의 요청이었습니다.

ClickHouse에 멀티마스터라는 이름의 기능은 따로 없습니다. 그 말에 해당하는 것은 ReplicatedMergeTree 계열 엔진과 Keeper입니다. 복제 테이블에서는 모든 레플리카가 쓰기를 받고, 한쪽이 죽었을 때 다른 쪽을 승격하는 절차가 없습니다. 어느 레플리카에 쓰든 파트를 받았다는 기록이 Keeper의 복제 로그에 오르고 다른 레플리카가 그 파트를 가져갑니다. 동작은 복제와 failover와 Keeper의 역할에, operator가 레플리카와 재시작 순서를 다루는 방식은 clickhouse-operator 선택에 정리돼 있습니다.

이 복제 방식은 두 가지를 뜻했습니다. 하나는 레플리카 수만 늘려서는 복제되지 않는다는 것입니다. 기존 MergeTree 테이블은 새 레플리카에 같은 이름의 빈 테이블로 생길 뿐이고, Service가 두 레플리카에 연결을 나누면 레플리카마다 일부 INSERT만 갖게 됩니다. 따라서 테이블 엔진을 바꿔야 합니다. 다른 하나는 지금까지 없던 Keeper 의존이 생긴다는 것입니다. Keeper 과반이 없으면 복제 테이블은 읽기 전용이 됩니다.

복제로 얻는 것은 node1의 디스크를 잃어도 데이터가 남는다는 것과, ClickHouse를 한 대씩 재시작할 수 있다는 것입니다. DROP이나 잘못된 INSERT, TTL 삭제는 그대로 복제되므로 백업을 대신하지는 않습니다.

정한 것과 이유

정할 것고른 것견준 안
기존 데이터제자리 변환. 플래그 파일과 재시작 한 번새 테이블로 옮기기, 새 ClickHouse를 나란히 띄우기, 버리고 새로 시작
Keeper3대. master1, node1, node2에 한 대씩node1에 1대
볼륨노드 로컬 디스크. 노드를 한정한 StorageClass없음(06 글의 판단을 따름)
node2의 경로루트 SSD의 프로비저너 기본 경로NVMe
콜드 티어처음에는 레플리카 1만 로컬 디스크. 전환 뒤 둘 다 S3로 바꿈처음부터 레플리카별 prefix로 S3 두 벌
재시작재시작이 필요한 변경을 한 번에 묶음변경마다 재시작
리허설운영과 같은 네임스페이스에 시험 구성을 띄워서 함바로 운영에 적용

메모리는 ClickHouse 두 레플리카 모두 request 1Gi에 limit 8Gi, Keeper는 request 100Mi에 limit 1Gi로 뒀습니다. 이미지는 25.3에서 25.3.14.14로 고정했습니다. 움직이는 태그를 두면 다음 패치부터 노드마다 다른 버전이 뜰 수 있습니다.

Keeper는 세 대, 세 번째는 master1

Keeper는 Raft로 과반을 정합니다. 대수별로 견딜 수 있는 손실은 다음과 같습니다.

Keeper 수과반견디는 손실이 클러스터에서
110node1에 두면 node1이 죽을 때 레플리카 0과 Keeper가 함께 사라지고 남은 레플리카는 읽기 전용
220한 대만 죽어도 과반 상실. 장애점이 둘이라 1대보다 나쁨
321노드 한 대 손실을 견딤

Keeper 1대 구성은 전환 전보다 쓰기 가용성이 낮아집니다. 비복제 테이블은 Keeper 없이 쓰기를 받지만 복제 테이블은 Keeper가 없으면 받지 못합니다. 리허설에서 Keeper 과반을 일부러 없앴을 때 INSERT 한 건이 143.2초 매달렸다가 실패했습니다. Keeper 3대에서 한 대만 멈추는 주입은 세 번 했습니다. 리더를 멈춘 두 번은 새 리더가 2~3초 만에 뽑혔고, 가장 오래 매달린 INSERT가 각각 13.2초와 10.9초 뒤에 성공했습니다. 팔로워 한 대의 볼륨과 파드를 지운 한 번은 1초 넘는 지연이 없었습니다. 세 번 모두 실패는 0건입니다.

노드가 셋뿐이므로 세 번째 Keeper는 master1에 갑니다. 06 글이 적은 대로 워커가 둘이면 과반을 만들 세 번째 자리는 control plane 노드밖에 없습니다. control plane 노드에 상태를 두지 않는다는 원칙의 예외입니다. Keeper 파드에 control-plane toleration을 주고 kubernetes.io/hostname 기준 anti-affinity로 노드당 한 대만 뜨게 했습니다.

# ClickHouseKeeperInstallation의 podTemplate
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: kubernetes.io/hostname
        labelSelector:
          matchLabels: {hdx-keeper: "true"}
tolerations:
  - {key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule}

대가는 master1의 디스크를 etcd와 나눠 쓴다는 것입니다. Keeper는 쓰기마다 fsync를 하고 etcd도 디스크 지연에 민감합니다. Keeper를 올린 직후 master1 디스크의 쓰기 지연은 1.37ms에서 1.39ms, 사용률은 0.060에서 0.061이었습니다. Keeper 세 대의 메모리는 32~87Mi, CPU 스로틀 비율은 0.004 이하였습니다. 쓰기 지연이 5ms를 넘거나 사용률이 0.30을 넘은 채 한 시간 유지되면 Keeper 단계를 통째로 되돌리기로 문턱을 정해 뒀습니다. Keeper는 3대로 고정이라 master1에서만 빼는 길이 없기 때문입니다.

대수를 1대로 시작해 나중에 3대로 늘리는 안은 고르지 않았습니다. 이 operator에서 Keeper 대수를 바꾸는 조작이 안전한지 확인하지 못해서 처음부터 최종 대수로 만들었습니다.

ClickHouse가 Keeper를 찾을 주소로는 Keeper 전체를 가리키는 Service 하나가 아니라 호스트별 -client Service 세 개를 나열했습니다. 이 Service는 Ready인 Keeper만 DNS에 올립니다. 리허설에서 죽은 Keeper는 이름이 풀리지 않아 4ms 만에 후보에서 빠졌습니다. operator에는 Keeper를 이름으로 참조하는 선언도 있습니다. 그 방식은 reconcile 맨 앞에서 Keeper 파드 전부가 Running일 때까지 120초를 기다리고, 실패하면 다른 변경까지 막습니다. 노드 하나가 내려가 있을 때 ClickHouse 설정을 고칠 수 없게 되므로 쓰지 않았습니다.

로컬 볼륨과 노드 한정 StorageClass

ClickHouse와 Keeper는 스스로 복제하므로 노드마다 로컬 디스크를 씁니다. 판단의 배경은 로컬 디스크와 복제에 있습니다.

local-path 프로비저너의 기존 클래스 둘은 allowedTopologies로 node1에 한정돼 있었습니다. 워커가 늘 때 기존 상태형 워크로드가 빈 볼륨을 받아 새 노드에 뜨는 일을 막으려고 건 제한입니다. 그 제한을 풀지 않고 클래스를 둘 더했습니다. node2 전용 클래스와, 세 노드를 모두 허용하는 Keeper 전용 클래스입니다. 둘 다 reclaim 정책은 Retain입니다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path-node2
provisioner: rancher.io/local-path
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
  - matchLabelExpressions:
      - {key: kubernetes.io/hostname, values: [node2]}

WaitForFirstConsumer와 allowedTopologies를 함께 쓰면 그 클래스의 PVC를 가진 파드는 허용된 노드로만 갑니다. 레플리카 1에는 nodeSelector도 affinity도 주지 않았습니다. 임시 클래스로 먼저 확인했고, 운영에서도 레플리카 1의 PV가 node2에 생겼습니다.

레플리카마다 클래스가 다르므로 볼륨 템플릿도 레플리카별로 나눴습니다. 레플리카 0의 템플릿은 이름과 클래스, 크기를 그대로 두고 레플리카 1의 템플릿을 뒤에 추가했습니다. StatefulSet의 volumeClaimTemplates는 고칠 수 없는 필드라, 레플리카 0의 템플릿을 건드리면 operator가 StatefulSet을 지우고 다시 만듭니다.

조사 초기에 잘못 짚은 것이 하나 있습니다. “프로비저너의 helper pod가 노드 taint를 넘지 못해 PV가 생기지 않는다"고 봤습니다. helper pod는 spec.nodeName을 직접 받아 스케줄러를 거치지 않고, kubelet은 NoExecute taint만 봅니다. 실제로 막고 있던 것은 클래스의 노드 한정이었습니다. 리허설에서 master1에도 PV가 7초 만에 생겼습니다.

기존 ClickHouse PV의 reclaim 정책은 Delete였습니다. PVC가 지워지면 노드의 데이터 디렉터리까지 지워집니다. 이번 작업은 PVC 주변을 만지므로 시작하기 전에 Retain으로 바꿨습니다. Retain의 대가는 볼륨을 다시 만들 때마다 Released로 남은 PV와 노드의 디렉터리를 손으로 지워야 한다는 것입니다. local-path는 용량 제한을 강제하지 않으므로 PVC의 30Gi와 60Gi는 이름표일 뿐입니다.

node2의 경로는 루트 SSD 아래 프로비저너 기본 경로로 뒀습니다. 여유가 더 큰 NVMe는 다른 용도의 홈 디렉터리로 쓰이고 있고 정리될 예정이라, 거기에 두면 정리 전에 옮겨야 할 일이 하나 생깁니다.

데이터는 제자리에서 변환

ClickHouse 25.3에는 비복제 테이블을 제자리에서 복제 테이블로 바꾸는 수단이 셋 있고 로컬 실험에서 모두 돌려 봤습니다. 테이블 디렉터리에 convert_to_replicated라는 빈 파일을 놓고 재시작하는 방법, ATTACH TABLE … AS REPLICATED, 새 복제 테이블을 만들어 파티션을 옮겨 붙이고 이름을 맞바꾸는 방법입니다. 플래그 파일 방식을 골랐습니다. Service 이름과 클라이언트가 그대로이고, 재시작 한 번에 12개 테이블이 함께 바뀝니다. 데이터를 버리고 새로 시작하는 안이 가장 단순하지만 되돌릴 수 없습니다.

플래그가 있는 테이블은 서버가 기동하면서 다음 순서로 바뀝니다.

  1. 테이블 메타데이터의 엔진을 Replicated…로 고쳐 씁니다. SummingMergeTree는 ReplicatedSummingMergeTree가 됩니다.
  2. 그 테이블의 파트를 전부 detached/로 옮깁니다. 머지로 이미 대체된 옛 파트(inactive)도 함께 갑니다.
  3. Keeper에 테이블과 레플리카를 등록합니다.
  4. 살아 있던(active) 파트만 하나씩 다시 붙입니다.

그래서 변환이 끝나면 옛 파트가 detached/에 남고, 이 옛 파트는 나중에 따로 지워야 합니다. S3에 있는 콜드 파트는 다시 올리지 않습니다. 다만 파트마다 1바이트짜리 버전 파일 하나를 지우고 새로 쓰기 때문에 변환에는 S3 쓰기도 필요합니다. materialized view는 대상 테이블이 제자리에서 엔진만 바뀌므로 다시 만들지 않습니다.

이 방식의 함정은 둘입니다. 하나는 플래그가 있는데 Keeper 설정이 없는 서버는 기동하지 못한다는 것입니다. 플래그를 놓은 뒤 새 설정이 적용되기 전에 파드가 다른 이유로 재시작하면 CrashLoop입니다. 다른 하나는 4번에서 같은 파티션에 내용이 바이트 단위로 같은 파트가 둘 이상이면 하나만 붙고 나머지는 오류 없이 detached/에 남는다는 것입니다. 로컬 실험에서는 롤업 테이블의 합계가 10% 줄었습니다. 두 함정을 어떻게 막았는지는 뒤의 두 절에 있습니다.

순서는 Keeper 추가, 레플리카가 하나인 채로 변환, 검증, 레플리카 추가입니다. 변환을 먼저 해 두면 새 레플리카는 테이블이 만들어지는 순간부터 스스로 파트를 가져옵니다.

컬렉터가 기동할 때 실행하는 스키마 생성 SQL에는 비복제 엔진 이름이 고정으로 적혀 있습니다. 이미 있는 테이블은 건너뛰므로 이 SQL은 고치지 않았습니다. 이 SQL을 고치지 않은 대가는 마지막 절에 적었습니다.

콜드 티어 — 처음에는 레플리카 1만 로컬 디스크

처음 설계에서는 레플리카 1의 콜드 볼륨을 S3가 아니라 같은 볼륨 안의 로컬 디스크로 두고, node2에는 S3 키를 싣지 않았습니다.

처음 설계에서 레플리카 1은 7일 지난 파트를 같은 볼륨의 로컬 디스크로 옮겨, 콜드가 node1의 SeaweedFS에 의존하지 않습니다.
도식 텍스트
  • node1
  • node2
  • 레플리카 0 — 핫 7일 · 로컬 볼륨
  • SeaweedFS S3 — prefix data/
  • 레플리카 1 — 핫 7일 · 로컬 볼륨
  • 로컬 콜드 디스크 — 같은 볼륨 안
  • 콜드 이동
  • 콜드 이동

이유는 넷이었습니다.

  • SeaweedFS는 파드가 전부 node1에 있고 데이터는 NAS 한 대에 있습니다. 콜드를 S3에 두 벌 올려도 같은 장비 위라서 독립된 사본이 아닙니다.
  • node1이 죽어도 레플리카 1이 90일 전체를 혼자 읽습니다. 로컬 실험에서 S3가 죽은 채 서버를 재기동하면 콜드 파트를 가진 테이블은 S3가 돌아올 때까지 로드되지 않았습니다(227초 동안 대기).
  • node2는 호스트에 직접 들어가는 사용자가 여럿인 장비입니다. 다른 버킷에도 통하는 S3 키를 거기에 싣지 않습니다.
  • 레플리카 0의 S3 엔드포인트와 prefix를 건드리지 않습니다. 콜드 파트의 메타데이터는 prefix에 상대적인 키를 저장합니다. 로컬 실험에서 prefix를 바꿔 재기동하니 콜드 파트가 전부 깨진 파트로 떨어졌습니다.

두 레플리카는 같은 설정 파일을 쓰고, 콜드 볼륨의 디스크 이름만 파드의 환경변수에서 받습니다.

<cold>
  <disk from_env="CH_COLD_DISK"/>
  <perform_ttl_move_on_insert>false</perform_ttl_move_on_insert>
</cold>

같은 키를 레플리카별 설정 파일로 덮어쓰는 방법은 쓸 수 없었습니다. operator가 호스트 단위로 내려 주는 파일은 공통 파일보다 우선순위가 낮습니다. 리허설에서는 환경변수가 빠지면 서버가 Unknown disk로 기동하지 못했습니다. 조용히 다른 디스크를 쓰는 일은 없다는 뜻이라 두 레플리카 모두에 값을 적었습니다.

이 설계의 대가는 설정이 레플리카마다 다르다는 것과, 90일치 콜드가 node2 로컬에 쌓인다는 것입니다(하루 약 220MiB 기준으로 약 19.5GiB, 추정). 전환을 마친 뒤 이 결정은 바꿨습니다. 그 내용은 “전환 뒤” 절에 있습니다.

조사, 검토, 리허설에서 바뀐 것

설계 전에 주제별 조사 여섯 건과 그것들을 서로 대조하는 점검을 했습니다. 설계는 세 관점(operator와 차트, 데이터 안전, GitOps와 운영)으로 나눠 검토받았고 지적이 34건(큰 것 10건) 나왔습니다. 리허설은 10-05 20:32부터 23:16까지, 운영과 같은 네임스페이스에 다른 이름의 ClickHouse와 Keeper를 띄워 확인 항목 30개를 돌렸습니다. operator가 그 네임스페이스만 보기 때문에 같은 네임스페이스여야 했습니다. operator와 이미지는 운영과 같고, 파트 수는 운영과 비슷하게 만들었지만(active 442개, inactive 1,134개) 데이터 크기는 훨씬 작습니다.

아래는 처음 적은 내용이 틀렸거나 모자랐던 곳입니다.

무엇처음 적은 것확인한 것고친 것
Keeper 배선과 재시작Keeper 주소를 넣는 변경은 재시작 없이 반영된다operator 0.27.4 소스에서, 없던 Keeper 설정을 처음 켜는 reconcile은 정책과 무관하게 재시작그 적용을 재시작 단계로 삼고 콜드 디스크 설정, 이미지 고정, 메모리 limit을 같은 PR에 묶음. 리허설에서 재시작 1회 확인
중복 파트 점검머지를 멈춘 뒤 내용이 같은 파트가 0인지 확인하고 변환롤업 테이블은 평소에도 내용이 같은 파트를 분당 7~9개 받는다. 몇 초 안에 머지돼 안 보일 뿐이라, 머지를 멈추면 쌓여서 이 점검은 통과할 수 없음머지를 멈춘 직후 파트 목록을 떠 두고, 변환 뒤 detached/를 그 목록과 대조해 옛 파트와 걸러진 파트를 가름
걸러지는 파트변환 뒤에 찾아서 다시 붙인다복원의 중복 제거는 서버 기본 프로파일의 insert_deduplicate를 따름프로파일에 insert_deduplicate: 0. 리허설에서 켠 회차는 586개가 걸러졌고 끈 회차는 0개
변환의 성공 판정파드 Ready와 operator의 Completed복원이 끊겨도 파드는 Ready, 상태는 Completed. 정상일 때도 Ready가 복원 완료보다 2~10초 앞섬변환 로그의 Attached all partitions 12줄과 system.replicas 12행으로 판정. 파드를 지우기 전에 로그를 파일로 저장
재시작 중의 요청실패가 아니라 대기파드에 직접 붙은 쿼리만 대기. Service를 거치면 26초 동안 연결 거부(2회 모두)알림 문구와 판정 기준을 고침. 그 구간은 컬렉터의 재시도 큐에 맡김
레플리카 유실 재구축PVC를 지우고 reconcilePVC가 사라지자마자 StatefulSet 컨트롤러가 같은 이름으로 다시 만들어 operator가 유실을 보지 못함. 테이블 없는 레플리카가 Service에 들어가 요청의 절반이 실패StatefulSet을 먼저 지움. 레플리카 1은 76초, 레플리카 0은 97초에 자동 복구
레플리카 0 유실자동 복구가 안 되므로 다른 레플리카에서 먼저 등록을 지운다자동 복구됨. operator가 테이블 생성 실패 뒤 ATTACH TABLE로 바꿔 Keeper의 옛 등록을 이어받음절차에서 그 단계를 뺌. 대신 S3의 옛 객체가 전부 고아로 남는다는 것을 적음
레플리카 이름이름으로 식별된다operator는 레플리카 목록을 위치로 식별. "1"은 목록의 1번 자리에 있을 때만 기존 호스트이름이 자리 번호와 다르면 차트 렌더가 실패하게 함. 늘릴 때는 끝에 붙이고 줄일 때는 끝에서만 뺌

몇 가지는 설명이 더 필요합니다.

복원이 끊기는 일은 계획에 없이 리허설에서 실제로 일어났습니다. 중복으로 걸러진 파트는 이름에서 뮤테이션 번호가 떨어진 채 detached/로 돌아갑니다. 그 이름의 뮤테이션 전 파트가 아직 지워지지 않고 남아 있으면 이름이 겹쳐 DIRECTORY_ALREADY_EXISTS(Code 84)로 그 테이블의 복원이 멈춥니다. 뮤테이션 51초 뒤에 머지를 멈춰서 생긴 조건이었습니다. 그 상태에서 파드는 Ready였고 system.tables 조회까지 오류를 냈습니다. 파드를 한 번 재시작하고 떠 둔 파트 목록으로 910개를 다시 붙였습니다. 그래도 한 파티션이 1,000행 모자랐습니다. 이름이 같은 파트가 서로 다른 디스크의 detached/에 하나씩 있어서 분류가 걸러진 파트 하나를 옛 파트로 센 것입니다. 행 수 대조로 그 파트를 찾아 손으로 하나 더 붙였고, 그제야 적용 직전에 떠 둔 파티션별 행 수와 맞았습니다. 이때 ClickHouse가 붙인 것은 걸러진 파트가 아니라 같은 이름의 뮤테이션 전 파트였습니다. 그 뮤테이션이 TTL 재계산뿐이어서 내용이 같았을 뿐입니다. 그 뒤로 변환 전 조건에 “최근 10분 안에 끝난 뮤테이션이 없을 것"과, 이름이 겹칠 수 있는 파트를 세는 점검을 더했습니다. insert_deduplicate: 0이면 걸러지는 파트가 없어 같은 조건에서도 끊기지 않았습니다.

insert_deduplicate: 0은 변환 뒤의 쓰기 동작도 바꿉니다. 비복제 테이블에는 원래 삽입 중복 제거가 없었습니다. 복제 테이블이 되면 기본으로 켜지는데, 이 값을 0으로 두면 변환 전과 같아집니다. 컬렉터가 타임아웃 뒤 같은 배치를 다시 보내면 중복 행이 들어갈 수 있다는 한계도 전과 같습니다.

레플리카 이름은 조사 단계에서 한 번, 검토에서 한 번 더 고쳤습니다. r0, r1 같은 이름을 주면 StatefulSet 이름이 달라져 기존 호스트가 새 호스트로 취급되고 기존 PVC는 고아가 됩니다. 그래서 "0", "1"로 정했는데, 검토에서 이 이름도 자리 번호와 같을 때만 통한다는 것이 드러났습니다. 목록에서 0번을 빼면 남은 "1"은 0번 자리의 새 호스트가 됩니다.

리허설에서 잰 시간은 다음과 같습니다.

무엇리허설 값
변환을 적용하고 Completed까지45초, 46초(2회)
그동안 Service 경유 연결 거부26초(2회 모두)
레플리카 추가를 적용하고 Completed까지72초
Keeper 리더 정지(2회)새 리더 2~3초. 가장 오래 매달린 INSERT는 13.2초, 10.9초 뒤 성공
Keeper 팔로워의 볼륨과 파드 삭제(1회)1초 넘는 지연 없음
Keeper 과반 상실 중 INSERT143.2초 뒤 실패

리허설은 kubectl apply로 했습니다. Argo CD를 거치는 경로는 보지 못했고, 그 경로의 첫 시험은 운영의 1단계와 2단계였습니다.

자동 sync 환경에서 단계 나누기

이 클러스터는 main에 머지된 변경을 Argo CD가 몇 분 안에 적용합니다. 변경을 PR 다섯 개로 나누고, 각 PR이 ClickHouse를 재시작시키는지를 기준으로 삼았습니다.

단계PR이 바꾸는 것레플리카 0 재시작
1차트 템플릿에 복제용 값과 렌더 가드 추가. 렌더 결과는 그대로없음
2Keeper 3대없음
3Keeper 배선, 콜드 디스크 설정, 이미지 고정, insert_deduplicate: 0, 레플리카 목록 명시, 메모리 limit한 번. 이 재시작에서 엔진을 변환
4레플리카 1 추가없음
5문서없음

단계마다 스크립트 하나를 한 번 실행하도록 준비했습니다. 스크립트는 사전 점검, PR 머지, 대조, 적용, 검증을 차례로 하고 통과 조건에서 벗어나면 멈춥니다. 스크립트에 코드로 걸어 둔 이 통과 조건을 아래에서는 게이트라고 적습니다. 클러스터를 바꾸기 전에 멈추면 걸어 둔 것을 스스로 풉니다.

3단계가 문제였습니다. 플래그 파일을 놓은 뒤 새 설정이 적용되기까지의 사이에 파드가 재시작하면 CrashLoop입니다. 재시작을 부를 수 있는 것은 파드 자신의 사고와, 같은 저장소에 들어오는 다른 머지입니다. 이 앱의 경로에는 열흘 동안 20개 커밋이 들어왔습니다. 처음 설계는 “플래그를 놓고 PR을 머지한다"였는데, 그 사이를 지키는 것은 머지하지 말아 달라는 알림뿐이었습니다. 며칠 전에 올려 둔 PR이 그동안 바뀐 main과 합쳐졌을 때 무엇이 렌더되는지 확인하는 단계도 없었습니다.

Argo 앱의 자동 sync를 잠시 끄는 방법은 쓸 수 없습니다. 이 앱은 상위 앱(app-of-apps)이 소유하고 상위 앱이 selfHeal로 설정을 되돌립니다. 대신 Application에 argocd.argoproj.io/skip-reconcile: "true" 어노테이션을 붙였습니다. kubectl annotate는 다른 필드 매니저로 어노테이션 키 하나만 소유하므로 상위 앱의 server-side apply가 지우지 않습니다.

Argo 앱을 멈춘 채 머지하면 머지된 main의 렌더를 라이브와 대조한 다음, 고른 시점에 적용됩니다. 3단계에서 실제로 돈 순서입니다.
도식 텍스트
  • 작업 스크립트
  • Git main
  • Argo CD
  • 클러스터
  1. 1. skip-reconcile 표시
  2. 2. PR 머지
  3. 적용하지 않음 · 60초 관찰
  4. 3. main 렌더
  5. 4. 라이브 리소스와 대조
  6. 5. 머지 정지·스냅샷·플래그
  7. 6. 표시 제거·hard refresh
  8. 7. ClickHouse 리소스 적용
  9. 8. 다시 표시
  10. 변환 검증이 끝난 뒤 표시 제거

대조는 머지된 main을 helm template으로 렌더해 라이브 리소스와 경로 단위로 견주는 일입니다. 달라지는 경로가 미리 적어 둔 목록과 같아야 하고 그 밖의 차이는 0이어야 합니다. 3단계는 18개 경로, 4단계는 21개 경로였고 둘 다 목록 밖의 차이가 없었습니다. 전체 렌더에서 달라지는 문서가 ClickHouse 리소스 하나뿐인지도 봤습니다. 컬렉터가 같은 적용에 휩쓸려 재시작하면 재시도 큐가 사라지기 때문입니다.

어노테이션을 떼어 적용이 시작되면 곧바로 다시 붙이고, 검증이 끝난 뒤에 뗍니다. 변환과 검증 사이에 다른 머지가 ClickHouse나 컬렉터를 다시 재시작시키지 못하게 하려는 것입니다. 멈춰 둔 동안에는 이 앱의 selfHeal도 멈춰 있으므로, 스크립트는 단계 끝마다 어노테이션이 남아 있지 않은지 확인합니다.

멈춤이 깨진 조건

skip-reconcile은 Argo CD의 알파 기능이고 이 클러스터에서 써 본 적이 없었습니다. 그래서 운영에 영향이 없는 1단계와 되돌릴 수 있는 2단계를 시험으로 삼았습니다.

1단계에서는 멈춘 채 머지하고 hard refresh를 걸었는데 250초 동안 Argo가 옛 커밋에 머물렀습니다. 2단계에서는 머지하고 10여 초 뒤, 관찰을 시작하며 hard refresh를 건 지 6초 안에 Argo가 새 커밋을 적용해 Keeper를 만들었습니다. 그때도 어노테이션은 true로 남아 있었습니다.

두 PR의 차이는 2단계 PR이 이 앱의 Application 정의 파일도 고쳤다는 것입니다. 컨트롤러 로그에는 상위 앱이 sync를 돌며 자식 Application들을 다시 적용했고, 1초 뒤 이 앱에 hard refresh와 자동 sync가 시작된 것이 찍혀 있었습니다. 이 hard refresh는 스크립트가 관찰을 시작하며 직접 건 요청입니다. 1단계에서는 멈춘 채 같은 요청을 걸어도 250초 동안 처리되지 않았고, 뒤의 3단계에서도 멈춤이 유지됐습니다. 그래서 요청 자체가 멈춤을 깬 것은 아니라고 봤습니다. Argo CD v3.5.3 소스의 canProcessApp을 보면 skip-reconcile은 informer 이벤트가 앱을 처리 큐에 넣는 것을 막습니다. 상위 앱의 sync가 자식 앱을 큐에 넣는 경로는 이 검사를 지나지 않은 것으로 보입니다. 정확한 호출 경로까지는 따라가지 않았습니다.

정리하면 이 멈춤은 Application 정의가 바뀌지 않는 머지에서만 믿을 수 있습니다. 3단계와 4단계 PR은 values 파일 하나만 고쳤고, 두 번 모두 멈춤이 유지됐습니다. 2단계에서 깨진 것은 Keeper만 생기는 변경이라 해가 없었습니다. 남는 위험은 멈춰 둔 몇 분 사이에 다른 PR이 Application 정의 디렉터리를 고쳐 머지되는 경우입니다. 다른 앱의 정의 파일만 바뀌어도 새는지는 확인하지 못했습니다. 새는 쪽으로 가정했습니다. 이 일 뒤에 한 콜드 변경부터는 머지 직전마다 그 디렉터리에 들어온 커밋이 0인지와 Argo의 revision이 그대로인지를 스크립트가 봅니다. 06 글이 적은 경우, 곧 어노테이션을 붙일 때 이미 돌고 있던 동기화가 한 번 더 도는 경우와는 별개의 조건입니다.

운영 전환 경과

단계시각(KST)한 일결과
110-05 23:59 ~ 10-06 00:06차트에 복제용 값 추가운영 변화 없음. 멈춤이 250초 유지됨
210-06 05:34 ~ 05:36Keeper 3대세 노드에 한 대씩 Running, 리더 1에 팔로워 2. ClickHouse는 그대로. 멈춤은 머지 뒤 10여 초 만에 깨짐
310-06 21:23 ~ 21:29Keeper 배선과 엔진 변환재시작 1회, 12개 테이블 변환, 걸러진 파트 0, 줄어든 파티션 0
410-06 21:34 ~ 21:37레플리카 1 추가레플리카 0 재시작 없음. 어제까지의 파티션별 행 수가 양쪽 같음
510-06 21:38 ~ 21:39문서리소스 변화 없음
정리10-06 21:44detached/의 옛 파트 삭제1,349개, 719MiB
콜드 변경10-06 23:58 ~ 10-07 00:10레플리카 1의 콜드를 S3로다음 절

실행 절차 문서에 적어 둔 대기 셋은 지키지 않았습니다. 3단계의 조건은 “Keeper를 올리고 하루 관찰한 뒤"였는데 15시간 49분 뒤에 진행했고, 스크립트는 경고를 냈습니다. 변환을 검증한 뒤에는 “하루쯤 그대로 둔다"였는데 3단계가 끝나고 5분 뒤에 4단계를 시작했습니다. 이 대기는 스크립트에 게이트로 걸려 있지 않아 경고도 없었습니다. 옛 파트 삭제의 조건은 “레플리카 추가 검증 뒤 24시간"이었는데 7분 뒤에 실행했습니다. 사전 점검이 이것을 실패로 막았고, 강제로 넘기는 변수를 주고 돌렸습니다. 셋 다 사람이 넘기기로 정했습니다. 이 대기들을 넘기면서 경과 표의 3단계 시작(21:23)부터 옛 파트 삭제(21:44)까지가 21분이었습니다.

변환 스크립트가 세 번 멈췄다

3단계 스크립트는 세 번 멈추고 네 번째에 끝까지 갔습니다. 세 번 모두 클러스터를 바꾸기 전이어서 운영에는 영향이 없었습니다.

첫 번째(20:26)는 백업 게이트였습니다. 변환 전에 콜드 디스크의 메타데이터 디렉터리를 tar로 떠 두는데, 게이트가 “tar가 1MB 이상"을 요구했습니다. 기준으로 삼은 “약 37M"은 du가 센 블록 단위 크기였습니다. 파일 9천여 개가 하나하나 작아서 블록 단위로는 크게 잡혔고, 겉보기 크기는 1.5M, gzip 뒤에는 0.6M(599,161바이트)이었습니다. 온전한 백업을 실패로 본 것입니다. 크기 대신 파일 수를 대조하게 고쳤습니다(tar 9,492개, 라이브 9,492개).

두 번째(21:15)는 그 수정에서 나왔습니다. 메시지에 "$tar_n개"라고 썼습니다. macOS 기본 bash 3.2.57은 한글의 첫 바이트까지 변수 이름으로 읽어 unbound variable로 죽습니다.

echo "tar $tar_n개"     # bash 3.2: tar_n 뒤의 한글 첫 바이트까지 변수 이름으로 읽는다
echo "tar ${tar_n}개"   # 고친 것

같이 드러난 문제가 더 컸습니다. 스크립트가 죽었는데 종료 처리기는 끝 (rc=0)을 찍었습니다. bash 3.2.57에서 set -eu와 EXIT trap을 함께 쓰면, unbound variable로 죽을 때 trap이 보는 $?도 셸의 종료 코드도 0입니다. 네 줄짜리 스크립트로 다시 확인했습니다. EXIT trap이 없거나 set -u만 쓰면 1이 나옵니다. 다른 bash 버전에서는 확인하지 않았습니다. 이 스크립트는 종료 코드가 0이 아닐 때만 자동 되돌리기를 돌립니다. 머지를 멈추고 플래그를 놓은 뒤에 같은 식으로 죽었다면 플래그가 남은 채 정상 종료로 기록됐을 것입니다. 스크립트 마지막 줄에서만 세우는 표식을 두고, 표식 없이 종료 코드 0으로 끝나면 실패로 보게 종료 처리기를 고쳤습니다.

세 번째(21:19)는 PR 브랜치를 main 위로 다시 쌓는 단계였습니다. 작업 폴더에 커밋되지 않은 변경이 있다며 멈췄습니다. 앞의 원인을 살피느라 그 폴더 안에서 명령을 돌렸고, 그때 쓰던 도구가 자기 상태 파일을 그 폴더에 써 놓았습니다. 파일을 정리하고, 다시 쌓기 전에 그 경로를 되돌리는 줄을 스크립트에 넣었습니다.

네 번째 실행

시각일
21:24:03Argo 앱 멈춤
21:24:10PR 머지. 60초 관찰하는 동안 Argo revision과 리소스 generation 그대로
21:25:25머지된 main의 렌더를 라이브와 대조. 18개 경로, 목록 밖 0
21:25:30머지, TTL 머지, 이동을 정지하고 파트 목록 저장(active 433개, inactive 1,349개)
21:25:49플래그 12개 배치
21:25:53멈춤 해제. 21:25:56 적용, 21:25:59 다시 멈춤
21:26:18새 파드가 메타데이터 12개를 고쳐 쓰고 복원 시작. 21:26:29에 마지막 테이블 복원 끝
21:26:31판정. 변환 로그 12줄, 오류 0줄, system.replicas 12행
21:27:34detached/ 분류. 옛 파트 1,349개가 저장한 목록의 inactive와 일치, 걸러진 파트 0
21:29:02멈춤 해제

기준 2026-10-06 21:23 ~ 21:29 KST·범위 hub 클러스터, 3단계·값은 스크립트 로그와 증거 파일

레플리카 0 재시작 1회 플래그 배치에서 새 파드 Ready까지 41초
변환한 테이블 12개 복원 약 11초
걸러진 파트 0개 줄어든 파티션 0
컬렉터 전송 실패 0 → 0 검증에서 기다린 지 11초에 큐 0

머지를 멈춘 때부터 멈춤을 풀 때까지는 23초였습니다(상한 15분). 콜드 파트는 변환 전후 모두 175개, 같은 바이트 수였습니다. 운영에서는 Service 경유 연결이 거부된 시간을 직접 재지 않았습니다. 컬렉터의 전송 실패 카운터가 늘지 않은 것으로 재시도 큐가 그 구간을 넘겼다고 판단했습니다.

레플리카 1 추가와 옛 파트 정리

4단계도 같은 순서로 했습니다. 적용을 확인하고 감시를 시작한 때부터 10초에 node2에 파드와 PVC가 생겼고, 29초에 테이블 12개와 materialized view 4개가 만들어졌습니다. 47초에 복제 큐가 0이 됐고 53초에 Service에 들어갔으며 75초에 Completed였습니다. 멈춤을 푼 때부터 세면 79초입니다(리허설 72초). 레플리카 0과 컬렉터는 재시작하지 않았습니다.

이 단계에서는 컬렉터가 재시작하지 않도록 했습니다. 컬렉터의 스키마 생성이 아직 테이블이 없는 레플리카 1에 먼저 닿으면 거기에 비복제 테이블이 생기고, operator의 테이블 생성은 이름이 이미 있어 아무 일도 하지 않습니다. 리허설에서 이 상황을 일부러 만들었을 때 operator와 Kubernetes 어디에도 신호가 없었습니다. 그래서 레플리카가 올라오는 동안 Argo를 멈춰 두고 몇 초 간격으로(표본은 3~6초마다 찍혔습니다) 비복제 테이블 수를 봤습니다.

detached/에 남은 옛 파트 1,349개(719MiB)는 삭제 목록이 3단계에서 저장한 inactive 목록과 한 줄도 다르지 않은 것을 확인한 뒤 DROP DETACHED PART로 지웠습니다. 이 삭제는 되돌릴 수 없습니다.

전환 뒤 — 레플리카 1의 콜드를 S3로

전환이 끝난 뒤 콜드 티어의 방향을 바꿨습니다. 06 글의 가정에 맞춰 node2도 격리된 존으로 보고, S3는 존 밖의 서비스로 보기로 했습니다. AWS에서 두 존의 레플리카가 같은 S3를 쓰는 모양입니다. 그러면 두 레플리카가 모두 콜드를 S3에 두는 같은 모양이 되고 node2의 로컬 디스크에는 핫 7일치만 남습니다. 디스크 이름과 prefix는 여전히 레플리카마다 다릅니다. 처음 설계가 얻으려던 것 가운데 둘을 포기했습니다. node1이 죽었을 때 레플리카 1이 혼자 전 구간을 읽는 능력과, node2에 S3 키를 싣지 않는다는 조건입니다.

바꾼 뒤에는 두 레플리카의 콜드가 모두 node1의 SeaweedFS에 의존하고 prefix만 갈립니다. node1을 내려 보는 시험은 하지 않았습니다.
도식 텍스트
  • node1
  • node2
  • 레플리카 0 — 핫 7일 · 로컬 볼륨
  • SeaweedFS S3 — 레플리카별 prefix
  • 레플리카 1 — 핫 7일 · 로컬 볼륨
  • 로컬 콜드 디스크 — 쓰지 않음
  • 콜드 이동
  • 콜드 이동

구현에서 정한 것은 셋입니다.

디스크 정의는 재시작하지 않는 새 파일에

디스크 정의를 새 파일로 더하고 파일 이름에 no_restart를 넣었습니다. 공통 설정 파일을 고치면 operator가 두 레플리카를 재시작시킵니다. operator 0.27.4의 기본 재시작 규칙에는 files/config.d/*no_restart*: "no"가 있고, 여러 규칙에 맞으면 마지막에 맞은 규칙을 따릅니다. 적용 뒤 두 파드의 uid, 재시작 횟수, 시작 시각이 전과 같았습니다. 다만 떠 있는 서버의 system.disks에는 새 디스크가 나타나지 않았습니다. 이 디스크를 쓸 레플리카 1은 새로 뜨면서 읽으므로 지장은 없었습니다.

prefix는 레플리카마다

새 디스크의 엔드포인트에 {replica} 매크로를 넣어 ClickHouse가 자기 레플리카 이름으로 풀게 했습니다.

<endpoint>http://<S3 서비스>:8333/<버킷>/replicas/{replica}/</endpoint>

로컬 실험으로는 두 레플리카가 같은 prefix를 써도 데이터가 깨지지 않았습니다. 객체 키가 무작위이고 각 레플리카는 자기 메타데이터가 가리키는 객체만 지웁니다. 그래도 나눈 이유는 둘입니다. 하나는 prefix를 공유해도 객체가 합쳐지지 않아 각자 한 벌씩 올린다는 것입니다. 다른 하나는 레플리카 하나를 잃으면 그 레플리카가 올린 객체가 남의 것과 섞여 고아 객체를 가려낼 수 없다는 것입니다(실험에서 108개 가운데 36개). prefix가 갈려 있으면 통째로 지우면 됩니다. 레플리카 0의 기존 디스크 이름과 prefix data/는 그대로 뒀습니다. 두 레플리카가 객체를 공유하는 zero-copy 복제는 25.3에서 기본으로 꺼져 있고 소스에 운영에 쓰지 말라고 적혀 있어 쓰지 않았습니다.

레플리카 1은 고치지 않고 다시 만들었다

기존 볼륨을 둔 채 콜드 디스크만 바꾸면 서버가 뜨지 못합니다. ClickHouse 소스를 보면 테이블을 로드할 때, 정의돼 있지만 그 테이블의 정책에는 없는 디스크에서 파트를 찾으면 UNKNOWN_DISK로 멈춥니다. 레플리카 1의 로컬 콜드 디스크에는 파트 176개가 있었습니다. 이 오류를 직접 재현하지는 않았습니다. 콜드 파트를 핫으로 되돌려 놓고 바꾸는 길도 따져 봤습니다. 7일 지난 파트는 백그라운드 이동이 곧장 다시 내려보내고, 그 이동을 멈추는 명령은 수동 이동도 막습니다. 레플리카 1은 그날 21:37에 만든 사본이고 데이터가 전부 레플리카 0에 있으므로 다시 만드는 쪽이 확실했습니다.

순서는 리허설에서 고친 그대로입니다. 레플리카 1을 Service에서 빼고, StatefulSet을 지우고, 레플리카 0이 전부 받았는지 확인하고, PVC를 지운 뒤 Argo의 멈춤을 풉니다. operator가 새 설정으로 레플리카를 만들고 전량을 다시 가져옵니다.

레플리카 0이 전부 받았는가

PVC를 지우기 전에 확인할 것이 하나 있습니다. INSERT는 파트를 자기 디스크에 쓰고, 그 파트의 등록 정보와 복제 로그 항목을 한 번에 Keeper에 올립니다. 파트 데이터는 Keeper에 가지 않습니다. 다른 레플리카는 그 로그를 보고 파트를 가져갑니다. 레플리카 1이 마지막 순간에 받은 파트를 레플리카 0이 가져가기 전에 레플리카 1의 볼륨을 지우면 그 파트는 어디에도 없습니다. 그래서 레플리카 1이 완전히 내려간 뒤 30초를 두고, 레플리카 0에서 복제 로그를 끝까지 읽었는지와 받을 항목이 큐에 묵어 있지 않은지를 5초 간격으로 세 번 봤습니다.

여기서 한 번 멈췄습니다. 세 번이 모두 통과한 직후, 증거 파일에 남기려고 같은 값을 한 번 더 읽었습니다. 그 사이에 수집이 계속 들어와 레플리카 0 자신이 방금 올린 로그 항목이 아직 처리되지 않은 순간의 값이 읽혔고, 게이트는 그것을 실패로 봤습니다. 데이터 문제는 아니었습니다. 2초 간격으로 여섯 번 다시 조회하니 전부 정상이었고 큐는 0이었습니다. 통과 값이 읽힐 때까지 다시 읽게 고치고 이어서 실행했습니다. 수집이 멈추지 않는 시스템에서 “지금 0인가"를 한 번 읽어 판정한 것이 원인입니다.

결과

StatefulSet을 지운 때부터 Completed까지 레플리카 1이 없던 시간은 337초였습니다. 그 가운데 2분 48초는 위의 게이트에서 멈춰 있던 시간입니다. 멈춤을 푼 뒤로는 67초가 걸렸습니다. 그동안 사본은 레플리카 0 하나였습니다.

다시 만든 레플리카는 테이블이 생긴 직후, 복제 큐에 274개 항목이 남은 상태로 Service에 들어갔습니다. operator는 새 호스트의 복제 지연만 기다리고 다시 만든 호스트는 기다리지 않습니다. 리허설에서 본 동작 그대로였습니다. 따라잡는 동안 레플리카 1로 간 조회는 뒤처진 결과를 봤을 것입니다.

가져온 파트는 먼저 핫 디스크에 저장되고, 7일 지난 것을 백그라운드 이동이 S3로 올립니다. 설계에서는 이 이동이 몇 분에서 십몇 분 걸린다고 봤습니다. 실제로는 멈춤을 풀고 1분 반 뒤의 검증에서 콜드 대상 176파트(1.11GiB)가 전부 S3에 있었습니다. 레플리카 1의 객체 9,298개는 모두 전용 prefix 아래였고, 임시 테이블로 그 prefix에 쓰고 읽고 지우는 확인도 통과했습니다.

레플리카 1의 옛 볼륨은 Retain이라 Released PV와 node2의 디렉터리로 남았습니다. 둘은 손으로 지웠습니다. 리허설의 시험용 클래스는 Delete여서 이 정리를 미리 해 보지 못했습니다.

최종 구성

노드파드볼륨콜드
master1KeeperKeeper 전용 로컬 클래스, 2Gi, Retain없음
node1레플리카 0, Keeper기존 로컬 클래스 30Gi(PV를 Retain으로 바꿈), Keeper 2GiS3, prefix data/
node2레플리카 1, Keepernode2 전용 로컬 클래스 60Gi, Keeper 2GiS3, prefix replicas/<레플리카 이름>/

Service 하나가 두 레플리카를 가리키고 컬렉터와 HyperDX는 그 Service로 붙습니다. 삽입 quorum은 켜지 않았습니다. 켜면 한쪽이 죽었을 때 쓰기가 막힙니다.

기준 2026-10-07 08:21 KST 조회·범위 hub 클러스터·kubectl get과 SELECT

복제 테이블 12개 ReplicatedMergeTree 8 · Summing 4
복제 상태 큐 0 · 지연 0 양쪽 12행, 읽기 전용 0
콜드 파트 180개 · 1.23GiB 양쪽 같음
Keeper 리더 1 · 팔로워 2 세 노드에 한 대씩

같은 조회에서 테이블 10개의 active 행 수와 파트 수가 두 레플리카에서 같았습니다. otel_logs는 43,294,745행, otel_traces는 18,857,108행, hyperdx_sessions는 349,275행입니다. S3 객체는 레플리카마다 9,509개였고 각자의 prefix 밖에 있는 객체는 0개였습니다. detached/에 남은 파트도 0개입니다.

남은 한계와 하지 않은 것

  • SeaweedFS는 파드가 전부 node1에 있고 데이터는 NAS에 있습니다. node1이 죽으면 레플리카 1도 7일 넘은 구간을 읽지 못합니다. 그 상태에서 레플리카 1이 재시작하면 콜드 파트를 가진 테이블은 SeaweedFS가 돌아올 때까지 로드되지 않을 것입니다. 로컬 실험에서 본 동작이고 운영에서 node1을 내려 확인하지는 않았습니다. 콜드 두 벌은 같은 NAS 위라 독립된 사본이 아닙니다.
  • HyperDX의 MongoDB는 한 대입니다. 06 글에서 데이터를 NAS로 옮겼지만, 파드가 있던 노드가 죽으면 StatefulSet 파드는 스스로 다른 노드로 옮겨 가지 않습니다. ClickHouse 데이터는 남아도 화면은 쓰지 못할 수 있습니다.
  • ClickHouse와 Keeper의 지표 수집과 알림을 만들지 않았습니다. 읽기 전용 레플리카, 복제 지연, Keeper 과반 상실은 점검 스크립트를 돌려야 알 수 있습니다. 이 스크립트는 kubectl get과 SELECT만으로 비복제 테이블, 읽기 전용 레플리카, 복제 지연과 큐, Keeper 세션을 조회하는 읽기 전용 스크립트이고, 사람이 돌려야 합니다. 정기 백업도 없습니다.
  • 운영에서 장애를 주입하지 않았습니다. Keeper 정지, 파드 삭제, 레플리카 0 유실 복구의 값은 모두 데이터가 작은 시험 구성에서 잰 것입니다. 노드를 내리는 시험은 시험 구성에서도 하지 않았습니다. 레플리카를 지우고 다시 만드는 절차를 운영에서 돌린 것은 콜드 변경 때의 레플리카 1 한 번뿐입니다.
  • 컬렉터의 스키마 생성은 여전히 비복제 엔진을 씁니다. 컬렉터를 올려 새 테이블이 생기면 비복제로 만들어지고, 점검 스크립트가 잡아야 합니다.
  • PR revert 한 번으로 되돌릴 수는 없습니다. Keeper 배선을 끄거나 3단계 이전으로 되돌리면 복제 테이블이 읽기 전용으로 뜹니다. 차트의 렌더 가드가 값 변경은 막지만 Argo의 history rollback은 막지 못합니다.
  • 레플리카 목록이 위치로 식별되므로 0번(node1)만 걷어 내는 변경은 이 차트로 할 수 없습니다.
  • insert_deduplicate: 0이라 컬렉터의 재전송이 서버에서 걸러지지 않습니다. 변환 전과 같은 동작입니다.
  • 복제는 비동기입니다. 방금 한쪽에 쓴 파트가 다른 쪽에 닿기 전의 조회는 그 파트를 보지 못합니다. PodDisruptionBudget은 Ready만 세고 복제 지연을 모르므로, 노드를 비우기 전에 점검 스크립트를 먼저 돌려야 합니다.
  • node2는 호스트 프로세스가 메모리를 약 20GiB 쓰고 kubelet의 호스트 예약이 0입니다. 레플리카 1과 Keeper는 둘 다 Burstable이고, 이 전환에서는 그에 대한 대비를 두지 않았습니다.
  • Argo의 멈춤이 다른 앱의 Application 정의만 바뀐 머지에서도 새는지 확인하지 못했습니다.
  • Retain 볼륨은 다시 만들 때마다 PV와 노드 디렉터리를 손으로 지워야 합니다.
마지막 수정 일자