Karpenter가 새벽마다 노드를 뽑아버린 이유
Karpenter가 새벽마다 노드를 뽑아버린 이유
지난주에 삽질한 이야기를 하나 풀어보려고 한다. 새벽 3시쯤 CI 파이프라인이 자꾸 실패한다는 얼럿이 들어왔다. 파이프라인 로그를 보면 특정 스텝에서 노드가 사라지고 파드가 재스케줄되면서 타임아웃이 나고 있었다. 어떤 날은 30분 안에 두세 번, 어떤 날은 조용했다. 재현이 잘 안 됐다.
처음엔 스팟 노드 회수를 의심했다
우리 CI 클러스터는 대부분 스팟이라 자연스럽게 스팟 종료 이벤트부터 봤다. CloudTrail에서 SpotInterruptionWarning을 시간대별로 찾아봤는데, 파이프라인 실패 시각이랑 매칭이 잘 안 됐다. 어떤 실패는 스팟 이벤트 없이도 발생했고, 어떤 스팟 이벤트는 실패 없이 넘어갔다.
스팟이 아니라면 뭘까? Karpenter 로그를 뒤졌더니 disruption 관련 이벤트가 유난히 많이 찍혀 있었다. consolidation 태그가 붙은 노드 제거가 새벽 시간대에 몰려 있었다.
disrupting via consolidation replace, terminating 1 nodes ip-10-0-3-45.ec2.internal
launching machine with 1 pods
CI 잡이 절반쯤 진행됐는데 노드를 replace한다고 뽑아버리고 있었다.
consolidateAfter 기본값이 1분
문제를 좁혀보니 우리는 Karpenter v1.2로 최근에 올렸는데, NodePool의 consolidateAfter를 명시하지 않고 있었다. 이 값의 기본이 1분이다. 1분 동안 노드 상태에 변화가 없으면 consolidation 대상이 될 수 있다는 뜻이다.
CI 잡의 패턴이 문제였다. 우리 파이프라인은 초반에 도커 빌드로 CPU를 확 쓰고, 중간에 이미지 푸시 대기하면서 조용해지고, 뒤에 통합 테스트로 다시 튄다. 이 "조용해지는 구간"이 1분을 넘기는 경우가 꽤 있었다. Karpenter는 그 순간을 노드가 안정됐다고 판단하고 더 작은 노드로 옮길 계획을 짜기 시작한다.
거기다 v1.2부터 기본 정책이 WhenEmptyOrUnderutilized다. 예전처럼 완전히 빈 노드만 지우는 게 아니라, "이 파드들을 더 작은 노드에 밀어넣을 수 있으면" 언제든 옮긴다. 우리 CI 잡은 대부분 requests가 낮게 잡혀 있어서, Karpenter 입장에서는 계속 "합칠 수 있어 보이는" 상태였다.
시도한 것들
처음엔 karpenter.sh/do-not-disrupt: "true" 어노테이션을 CI 잡 파드에 붙였다. 이건 작동은 하는데 CI 러너 컨트롤러가 파드를 만들 때 어노테이션을 넣도록 매니페스트를 다 수정해야 했다. Argo Workflows 템플릿, 자체 스크립트 러너, 몇 개 GitOps 레포에 다 손이 갔다. 절반쯤 하다가 이건 답이 아니라는 생각이 들었다.
다음으로 consolidateAfter를 10분으로 늘렸다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ci-spot
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
이걸로 새벽 실패는 거의 사라졌다. 근데 문제는 저녁에 CI 트래픽이 확 줄어드는 시간대에 노드가 오래 붙어있게 됐다. 비용 리포트를 보니 야간 스팟 비용이 한 15% 정도 올라갔다.
disruption budget으로 속도 제한
10분이라는 값이 최적인지는 아직 확신이 없다. 그래서 추가로 disruption budget을 넣어서, consolidation이 한꺼번에 여러 노드를 뽑는 건 막았다.
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
budgets:
- nodes: "10%"
- nodes: "0"
schedule: "0 22 * * *"
duration: 8h
reasons: ["Underutilized"]
야간에는 underutilized 이유로 뽑는 건 8시간 동안 아예 막았다. drift(노드 스펙 변경 반영)나 empty(진짜 빈 노드)는 그대로 두고 싶어서 reasons를 지정했다. 이 필드가 v1에 들어오면서 세밀한 제어가 가능해진 게 컸다.
그래서 배운 것
버전 올릴 때 기본값 변경을 매뉴얼로 훑는 게 얼마나 중요한지 다시 느꼈다. v1beta1 시절엔 consolidationPolicy: WhenUnderutilized가 별도 옵트인이었는데, v1 오면서 WhenEmptyOrUnderutilized가 기본이 됐고 consolidateAfter도 짧아졌다. 릴리스 노트는 봤지만 우리 CI 워크로드처럼 CPU 사용률이 톱니바퀴 모양으로 뛰는 경우가 얼마나 취약해지는지는 실제로 겪어봐야 감이 왔다.
한 가지 아직 정리 안 된 건 consolidateAfter의 적정값을 워크로드별로 어떻게 뽑을 것인가다. CI, 배치, 상시 서비스가 같은 NodePool에 있으면 값 하나로 안 맞는다. 지금은 NodePool을 워크로드 특성별로 세 개로 쪼개는 걸 검토 중이다. 다 정리되면 후속 글로 다시 쓰겠다.
혹시 비슷한 이슈 겪으신 분 있으면 어떻게 푸셨는지 궁금하다. 특히 CI/배치 워크로드에서 consolidation을 어떻게 다루시는지.