Karpenter NodePool Disruption Budget 실전 적용 가이드
Karpenter NodePool Disruption Budget 실전 적용 가이드
Karpenter 쓰다 보면 어느 순간 이런 상황 마주친다. 새벽에 갑자기 consolidation이 돌면서 노드 절반이 한꺼번에 교체됨. 파드 재스케줄링 폭풍. 알림 폭탄. 아침에 출근해서 슬랙 열어보고 한숨.
Disruption Budget이 그래서 있다. 근데 문서가 좀 애매하다. nodes: "10%"가 뭘 의미하는지, expireAfter랑은 왜 따로 노는지, schedule이랑 결합하면 어떻게 되는지 — 실제로 프로덕션에 넣기 전에 몇 번 시행착오를 겪게 된다. 이 글은 그 시행착오를 압축한 가이드다.
왜 기본값이 문제인가
Karpenter는 NodePool에 disruption.budgets를 지정하지 않으면 기본으로 nodes: "10%" 하나만 적용한다. 클러스터가 작을 때는 큰 문제가 없다. 노드 20개면 동시에 2개까지만 disrupt 되니까.
문제는 노드가 많거나 스팟 비중이 큰 클러스터다. 우리 팀의 스테이징 클러스터가 노드 80개 정도 굴러가는데, drift 이벤트가 한 번 터지면 이론상 8개까지 동시에 죽을 수 있다. 8개 노드에 얹혀 있던 파드가 한꺼번에 재스케줄링 요청을 보내면 API 서버가 살짝 헐떡거리기 시작하고, HPA가 흔들리고, PDB가 빡빡한 서비스는 아예 스케줄링이 밀린다.
기본값의 또 다른 함정은 시간 개념이 없다는 것이다. 10%가 한 번에 나가고, 다시 준비되고, 또 10%가 나가고 — 이게 몇 분에 걸쳐 반복될지 예측이 안 된다. 그래서 "동시성 제한"보다 "시간당 유량 제한"이 더 필요할 때가 많다.
프로덕션에서 쓰는 3단 구성
여러 조합을 시도해봤는데 결국 이런 형태로 수렴했다. 세 개의 budget을 겹쳐서 쓴다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
# 1. 평시 - 시간당 1대만
- nodes: "1"
reasons:
- Underutilized
- Empty
# 2. Drift - 이미지/설정 변경은 조금 더 열어줌
- nodes: "3"
reasons:
- Drifted
# 3. 업무시간 완전 차단
- nodes: "0"
schedule: "0 9 * * mon-fri"
duration: 9h
reasons:
- Underutilized
- Drifted
첫 번째 budget이 평시 기본값이다. Consolidation 목적의 disrupt는 시간당 1대로 묶는다. 이렇게 하면 새벽에 갑자기 노드 열 개가 정리되는 일이 없다. 대신 정리가 느려서 비용 최적화 효과가 조금 미뤄지지만, 안정성이 그만큼 올라간다.
두 번째는 drift 전용이다. AMI 업데이트나 인스턴스 타입 변경 같은 drift는 어차피 언젠가는 다 갈아야 하니까 조금 더 여유를 준다. 3대씩. 다만 3이 절대 안전 상한이라는 뜻은 아니고, 우리 팀의 PDB 설정과 재스케줄링 지연을 감안한 실측치다.
세 번째가 핵심인데, 업무시간(월~금 09시부터 9시간)에는 아예 disrupt 자체를 막는다. 팀원들이 배포하고 있을 때, 알림이 사방에서 울리는 상황에서 노드 교체까지 겹치면 원인 분석이 지옥이 된다. 그래서 그냥 이 시간대는 조용히 둔다.
expireAfter의 함정
여기서 사람들이 자주 걸려 넘어지는 부분이 있다. Disruption budget은 Expired reason에는 적용되지 않는다. 정확히는 문서에 있긴 한데 읽고 지나치기 쉽다.
즉, NodeClass에 expireAfter: 24h를 걸어두면, 24시간 지난 노드는 disruption budget이 뭐라고 하든 무조건 죽는다. 업무시간이든 아니든 상관 없이.
이걸 모르고 있다가 낮에 노드가 우수수 갈리는 걸 겪었다. 원인 찾느라 두 시간 날렸다. 그 이후로는 두 가지 중 하나를 선택한다.
첫째, expireAfter를 아주 길게 잡는다. 최소 720h(30일) 정도. 그리고 노드 교체는 drift에 맡긴다. AMI를 정기적으로 업데이트하면 drift로 자연스럽게 갈아진다.
둘째, expireAfter를 유지하되 만료 시점을 새벽으로 밀어놓는다. 새 NodePool 생성 시각을 조정해서 24시간 뒤가 업무시간에 걸리지 않게 한다. 이건 관리가 좀 귀찮다.
우리는 첫 번째로 갔다. 관리 포인트가 하나 줄어드는 게 크다.
실측: 어떻게 확인할까
Budget 설정이 실제로 먹히는지 확인하려면 두 가지를 본다.
Karpenter가 노출하는 메트릭 중에 karpenter_nodepools_allowed_disruptions가 있다. 현재 시점에 disruption이 허용되는 노드 수다. 업무시간에 이 값이 0인지 그라파나로 확인한다.
karpenter_nodepools_allowed_disruptions{nodepool="default"}
이 값이 예상과 다르면 budget이 겹쳐서 어느 하나가 이기고 있는 거다. Karpenter는 여러 budget이 매치되면 가장 제한적인 값을 채택한다. 그래서 위의 3단 구성에서 업무시간에는 세 번째 budget(0)이 이겨서 결과적으로 0이 된다.
또 하나, 노드 이벤트를 직접 본다.
kubectl get events -A --field-selector reason=DisruptionBlocked
Blocked 이벤트가 예상 시간대에 잘 뜨는지 확인하면 된다. 반대로 예상 안 한 시간대에 대량으로 뜬다면 스케줄이 잘못 설정된 거다.
안 하는 게 나을 때
Disruption budget이 만능은 아니다. 오히려 너무 빡빡하게 걸면 이런 상황이 온다.
배치 워크로드가 몰리는 시간대에 스팟 노드가 회수되고 있는데, budget이 0이라 새 노드로 교체가 안 됨. 그런데 잠깐, 스팟 회수는 disruption budget이 막지 못한다. 이건 involuntary disruption이니까. 헷갈리지 말자. Budget이 관여하는 건 Karpenter가 자발적으로 하는 consolidation/drift/expiration 뿐이다.
또, 클러스터가 아주 작을 때(노드 5대 이하)는 굳이 복잡한 budget을 걸 필요가 없다. 어차피 동시에 여러 대가 죽어도 총량이 작아서 영향이 제한적이다. 그냥 기본값으로 두거나, 아예 nodes: "1" 한 줄로 끝내자.
마무리
Disruption budget은 "안전장치" 관점에서 접근해야 한다. 최적화 도구가 아니다. 비용을 아끼려고 넣는 게 아니라, 자동화된 노드 교체가 사고로 이어지지 않게 막는 도구다.
우리 팀은 위의 3단 구성으로 4개월째 돌리고 있는데, 이후로는 새벽에 깨는 일이 없어졌다. 물론 상황이 다르면 맞는 값도 다르다. karpenter_nodes_disrupted_total 메트릭을 며칠 관찰해보고 자기 클러스터에 맞는 상한을 찾는 게 최선이다.
혹시 다른 형태의 budget 구성으로 재미 본 분 있으면 어떻게 쓰시는지 궁금하다.