IT/Kubernets

Karpenter consolidation 내부 동작 파헤치기 — 노드가 사라지기까지

gfrog 2026. 8. 14. 18:12

Karpenter 1.x가 굴러가는 클러스터를 몇 개 운영해보니, "왜 이 노드가 갑자기 사라졌지?" 하는 질문이 종종 나온다. 특히 consolidation 정책을 WhenEmptyOrUnderutilized로 두면 (1.x 기본값이다) 반쯤 차 있는 노드도 사라진다. 사실 내부적으로는 꽤 정교한 시뮬레이션을 돌리고 있는데, 이걸 모르면 로그만 봐도 무슨 소린지 모르겠다.

이 글은 Karpenter가 노드 하나를 지우기로 결정하기까지 내부에서 무슨 일이 벌어지는지, 코드 구조와 로그를 따라가면서 풀어본다. 우리 팀에서 Karpenter를 도입한 지 반년쯤 됐는데, 이 흐름을 이해하고 나니 disruption budget 튜닝이 훨씬 편해졌다.

disruption controller가 하는 일

Karpenter 안에는 disruption 컨트롤러가 있다. 이게 주기적으로(기본 10초) 클러스터 상태를 스캔하면서 "지금 없앨만한 노드가 있나?"를 본다. 여기서 "없앨만하다"는 판단은 여러 method의 우선순위 리스트를 순서대로 돌린다.

우선순위는 대충 이렇다. Expiration → Drift → Emptiness → Consolidation. 앞 단계에서 후보가 나오면 그걸 먼저 처리한다. Consolidation은 마지막에 온다. 이 순서가 중요한 이유는, drift(설정이 바뀐 노드)를 먼저 정리하지 않으면 consolidation이 계속 최적화를 시도하다가 결국 drift 처리 때문에 방금 만든 노드를 또 없애는 삽질이 생기기 때문이다.

Consolidation 자체도 두 갈래로 갈린다. Delete(그냥 지우기)와 Replace(더 싼 노드로 갈아끼우기). Delete가 항상 우선이다. 남은 노드들이 대신 파드를 받아줄 수 있으면 그냥 지우는 게 이득이니까.

실제로 시뮬레이션이 어떻게 돌아가는가

여기가 재밌는 부분이다. Karpenter는 노드 하나를 후보로 골라놓고, "이 노드가 없다고 가정하고 파드들을 남은 노드에 다시 배치해봤을 때 성공하나?"를 시뮬레이션한다. 실제로 파드를 옮기는 게 아니라, scheduler 로직을 in-memory로 돌리는 거다. 이 시뮬레이션은 대충 이런 걸 검사한다.

노드의 남은 리소스, taint/toleration, node affinity, topology spread constraints, PVC의 zone 제약, PodDisruptionBudget. 특히 PDB는 자주 걸린다. maxUnavailable: 0이면 그 파드가 올라가 있는 노드는 사실상 consolidation 대상에서 빠진다. 로그를 보면 "pdb blocking disruption" 같은 라인이 뜬다.

Replace 시나리오는 더 복잡하다. "이 노드를 지우고 새 노드를 하나 띄운다면, 어떤 인스턴스 타입이 가장 싸면서도 파드들을 다 받아줄 수 있나?"를 EC2 인스턴스 카탈로그 전체를 뒤져서 계산한다. 그래서 종종 c6i.4xlarge가 c7a.2xlarge + m6i.large 조합으로 바뀌기도 하고, 반대로 여러 개가 큰 놈 하나로 합쳐지기도 한다.

다중 노드 consolidation의 함정

Karpenter는 여러 노드를 동시에 없애는 것도 시도한다. 예를 들어 6대 클러스터에서 노드 3대를 동시에 지우고 남은 3대에 몰아넣는 시나리오를 계산한다. 근데 이걸 무제한으로 하면 위험하니까 상한이 있다. 기본적으로 한 번에 후보로 삼는 노드 수에 제한이 걸려 있고, 이게 disruption budget과 곱해져서 실제 실행 가능한 개수가 정해진다.

여기서 자주 삽질한다. nodes: "10%"로 budget을 걸어놨는데 50노드 클러스터에서 5개씩 사라지길 기대했지만, 실제로는 시뮬레이션이 5개 조합을 못 찾아서 1개씩만 사라지는 경우가 흔하다. 시뮬레이션은 조합 폭발을 피하려고 후보를 pod count 기준으로 정렬해서 가장 비어있는 노드부터 순서대로 시도한다. 나머지는 다음 iteration으로 미룬다.

로그를 어떻게 읽나

Karpenter 컨트롤러 로그를 -l app.kubernetes.io/name=karpenter 로 잡아서 보면 disrupting via consolidation delete 같은 라인이 뜬다. 그 위아래를 보면 왜 이 노드가 선택됐고, 대신 어떤 파드가 어디로 옮겨질 예정인지 나온다. "marking for deletion" 이후에 실제 taint(karpenter.sh/disruption:NoSchedule)가 붙고, 파드가 하나씩 evict되면서 노드가 사라진다.

한번은 새벽에 갑자기 파드들이 재시작되길래 뭔가 했는데, 스팟 인스턴스가 회수된 게 아니라 consolidation이 spot-to-spot으로 노드를 갈아끼운 거였다. 최근에 이 기능이 GA되면서 스팟끼리도 최적화하다 보니 파드 이동이 예전보다 잦아졌다. consolidateAfter를 좀 늘려서(우리는 5분으로 뒀다) 너무 공격적인 최적화를 억제하고 있다.

그래서 뭘 튜닝해야 하나

일단 consolidationPolicy는 워크로드 성격에 따라 다르다. stateless 웹서비스만 굴린다면 WhenEmptyOrUnderutilized 켜두고 이득 본다. 하지만 배치나 stateful이 많으면 WhenEmpty만 켜서 완전히 빈 노드만 정리하는 게 안전하다.

Disruption budget은 무조건 걸어라. 최소 하나는. maxUnavailable: 20% 정도만 걸어놔도 갑자기 절반이 사라지는 사고는 막을 수 있다. 여기에 schedule을 걸어서 업무 시간에는 disruption을 0으로 만들어놓는 팀도 봤다.

consolidateAfter는 노드가 "underutilized" 상태로 얼마나 유지돼야 후보에 오르는지 정하는 값인데, 짧으면 반응이 빠른 대신 진동한다. HPA가 스케일 아웃했다가 다시 줄어드는 리듬과 겹치면 노드가 계속 왔다갔다 한다. 우리 팀은 이걸로 한참 삽질하다가 3분 → 10분으로 늘리고 안정화됐다.

아직 다 파악한 건 아니고, 특히 여러 nodepool이 섞였을 때 우선순위 계산이 어떻게 되는지는 좀 더 봐야 할 것 같다. 이 부분은 다음에 다뤄보려고 한다.