IT/AWS

Karpenter consolidation 너무 믿었다가 새벽 3시에 호출받은 이야기

gfrog 2026. 6. 23. 09:47
SMALL

지난주 화요일 새벽 3시 12분. 폰이 진동했다. PagerDuty다.

알림 내용은 단순했다. "checkout-api 50x error rate spike". 처음에는 트래픽 이슈인가 싶었는데, 그래프를 열어보니 패턴이 이상했다. 트래픽은 평소 새벽 수준 그대로인데 5xx만 튀고 있었다. P99 레이턴시도 2초를 넘기고 있었다.

결론부터 말하면 Karpenter consolidation 정책을 잘못 건드린 게 원인이었다. WhenEmptyOrUnderutilized를 그대로 두고 consolidateAfter를 30초로 줄였더니, 새벽 트래픽이 잠깐 빠질 때마다 노드가 통째로 갈리면서 stateful한 워크로드들이 같이 흔들렸다.

우리가 뭘 바꿨길래

며칠 전에 비용 최적화 한답시고 NodePool 설정을 손봤다. 원래는 이랬다.

disruption:
  consolidationPolicy: WhenEmpty
  consolidateAfter: 60s

그런데 사내 클라우드 비용 리포트에서 "야간 시간대 노드 활용률이 30%대"라는 지적이 나왔고, 나는 별 생각 없이 이렇게 바꿨다.

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 30s

Karpenter v1.5부터 들어간 emptiness-first consolidation 동작도 이미 켜져 있었고, 빈 노드를 더 빠르게 정리하라는 의도였다. 머릿속으로는 "유휴 노드만 빨리 치우는 거니까 안전하겠지" 였다. 그게 첫 번째 착각이었다.

실제로 무슨 일이 벌어졌나

WhenEmptyOrUnderutilized는 이름과 다르게 "빈 노드만" 정리하는 게 아니다. underutilized 노드의 파드를 다른 노드로 옮기고, 원본 노드를 죽인다. 더 싼 인스턴스 타입으로 갈아끼우는 동작까지 포함된다.

새벽 2시쯤 트래픽이 평소의 40% 수준으로 내려갔다. checkout-api 파드 8개가 4대 노드에 분산되어 있었는데, Karpenter 입장에서는 "어, 이거 한 대로 충분히 packing 되겠는데?" 였던 것이다.

그래서 3대를 죽이기 시작했다. 30초 간격으로 하나씩.

문제는 우리 서비스가 워밍업이 좀 길었다. JVM 기반이고, 컨슈머 캐시 채우는 데 60~90초 걸린다. Karpenter는 PDB(maxUnavailable: 1)는 지켜줬지만, 새로 스케줄된 파드가 readiness probe를 통과하기 전에 다음 노드를 갈아엎기 시작했다. 결과적으로 정상 동작하는 레플리카가 2개까지 떨어진 순간이 있었고, 그 사이 들어온 요청 일부가 5xx를 받았다.

근데 더 짜증났던 건, 이게 트래픽 패턴 따라 반복적으로 발생했다는 점이다. 새벽 2시, 2시 40분, 3시 10분... 트래픽이 살짝 빠질 때마다 같은 패턴.

응급 처치

새벽에 졸린 눈으로 한 건 두 가지다.

먼저 NodePool의 disruption을 일시 정지시켰다.

kubectl patch nodepool default --type=merge -p '{
  "spec": {
    "disruption": {
      "budgets": [{"nodes": "0"}]
    }
  }
}'

budgets: nodes: 0 으로 두면 모든 voluntary disruption이 멈춘다. emergency stop 같은 거다. 이걸 알아두니까 새벽에 진짜 유용했다.

그리고 PDB를 좀 더 빡빡하게 묶었다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout-api-pdb
spec:
  minAvailable: 75%
  selector:
    matchLabels:
      app: checkout-api

기존엔 maxUnavailable: 1 이었는데 레플리카 수 대비 비율로 묶었다. 8개 중 6개는 항상 살아있도록.

이 두 가지 적용하고 나니까 5xx가 잦아들었다. 잠은 그때쯤 다 깨 있었지만.

다음날 회고에서 정리한 것

다음날 팀 회의에서 한 시간 정도 떠들었다. 정리하면 이렇다.

consolidateAfter를 짧게 두는 건 위험하다. 30초는 진짜 너무 짧았다. 30초 안에 트래픽이 잠깐 빠지는 건 일상이다. 우리는 5분(5m)으로 다시 늘렸다. 비용은 좀 더 들지만 안정성을 사는 거다.

WhenEmptyOrUnderutilized를 쓰려면 워크로드 워밍업 시간을 진지하게 봐야 한다. JVM, ML inference, DB connection pool 채우는 서비스들은 readiness probe만으로 부족할 수 있다. Karpenter는 readiness가 OK면 다음 노드로 넘어간다.

PDB는 비율(minAvailable: %)로 거는 게 정신건강에 좋다. 절대값(maxUnavailable: 1)은 레플리카가 적을 때 너무 헐겁고, 많을 때는 너무 빡빡하다. 비율로 걸면 자동 조정된다.

비용 최적화는 단계적으로. 우리는 1단계로 WhenEmpty + 5m, 2단계로 시간대별 NodePool 분리(야간엔 spot 우대) 같은 식으로 했어야 했다. 한 번에 WhenEmptyOrUnderutilized + 30s 같은 공격적 설정은 정말 trade-off를 다 검토한 다음에 가야 한다.

그래서

지금은 비용 살짝 더 내고 잠을 자기로 했다. 한 달에 EC2 비용 대략 8% 정도 더 나오지만, 새벽 호출받는 것보단 훨씬 낫다.

혹시 Karpenter v1.x 쓰면서 WhenEmptyOrUnderutilized 잘 쓰고 계신 분 있으면 어떻게 튜닝했는지 궁금하다. 우리는 일단 보수적으로 가는 중이지만, 더 좋은 패턴 있을 것 같긴 하다.

BIG