Skip to content

Latest commit

 

History

History
96 lines (71 loc) · 12.1 KB

File metadata and controls

96 lines (71 loc) · 12.1 KB

v1.14 — Karpenter Balanced Consolidation: 저장 대비 손해를 점수로 재는 노드 통합 정책

메타데이터

항목 내용
프로젝트 Karpenter (kubernetes-sigs/karpenter)
관련 릴리스 v1.14.0 (2026-07-11)
관련 PR #2962 — feat: implement Balanced consolidation policy (merged 2026-07-01), #2942 — docs: Balanced consolidation policy RFC (merged 2026-04-22)
설계 문서 designs/balanced-consolidation.md
발견 출처 v1.14.0 릴리스 노트 Features 항목("implement Balanced consolidation policy (#2962)")
검증 근거 로컬 클론(sources/karpenter)의 실제 커밋 43964dc5(diff 34개 파일, +2762/-253) 및 설계 문서 원문

개요

Karpenter의 consolidation(노드 통합)은 지금까지 "저장액이 조금이라도 있으면 실행" 아니면 "노드가 완전히 비어야만 실행" 둘 중 하나였다. v1.14에서 세 번째 옵션 consolidationPolicy: Balanced가 추가됐다 — 각 통합 동작(move)을 "얼마나 아끼는지"와 "얼마나 disruption을 일으키는지"의 비율로 점수화해서, 그 비율이 임계값(k=2, 즉 저장액 비중이 disruption 비중의 절반 이상)을 넘을 때만 실행한다. 목적은 "$0.02 아끼려고 30분짜리 워밍업 캐시를 가진 파드를 쫓아내는" 식의 손해 보는 통합을 걸러내는 것이다.

사전 지식 — Consolidation 정책과 "얼마나 아끼는가"의 기존 계산법

Karpenter는 사용률이 떨어진 노드를 찾아 더 싼 인스턴스로 바꾸거나(REPLACE) 아예 지워버리는(DELETE) 방식으로 비용을 줄인다. 이런 통합 동작 하나하나를 이 리포트에서는(설계 문서 용어 그대로) move라고 부른다. v1.14 이전 consolidationPolicy는 두 값만 가능했다.

  • WhenEmpty: 파드가 하나도 없는 노드만 지운다.
  • WhenEmptyOrUnderutilized(기본값): 파드가 남아있어도, 다시 스케줄링해서 더 싸게 packing할 수 있으면 통합한다. 저장액이 얼마든(단돈 몇 센트라도) 양수이기만 하면 승인된다.

각 파드에는 이미 disruption cost라는 개념이 있었다(pkg/utils/disruptionEvictionCost/ReschedulingCost) — pod-deletion-cost 어노테이션과 파드 priority를 반영해 "이 파드를 쫓아내는 게 얼마나 부담스러운가"를 수치화한다. 하지만 이 값은 같은 노드 안에서 어떤 candidate를 먼저 시도할지 정렬하는 데만 쓰였다: consolidation.sortCandidates가 후보를 disruption cost 오름차순(가장 disruption이 적은 노드부터)으로 정렬하고, multi-node consolidation의 이진 탐색은 그 정렬 순서 그대로 앞에서부터 얼마나 많은 노드를 한 번에 묶을 수 있는지를 찾았다. 즉 "이 통합이 손해인지 아닌지"를 판단하는 별도의 관문은 없었고, 오직 "저장액 > 0"과 disruption budget(동시에 몇 대까지)만이 실행 여부를 결정했다.

변경 분석 — "저장액 > 0"에서 "저장액/disruption 비율 ≥ 임계값"으로

기존 WhenEmptyOrUnderutilized가 실무에서 일으킨 문제들은 설계 문서가 실제 이슈 번호와 함께 구체적으로 나열한다: 93~99% 사용률인 노드가 오히려 disruption 대상으로 선택되는 경우(kubernetes-sigs#2319), 같은 인스턴스 타입으로 순저장액 없이 몇 시간씩 교체를 반복하는 루프(aws#8536 등), consolidateAfter가 이미 잘 packing된 노드의 disruption을 막지 못하는 경우(kubernetes-sigs#2705) 등이다. 공통 원인은 "저장액이 있다"는 조건만으로는 disruption 규모를 전혀 걸러내지 못한다는 것.

[변경 전] WhenEmptyOrUnderutilized: 저장액 > 0 이면 무조건 승인
┌───────────────┐   재스케줄 시뮬레이션    ┌──────────────────────┐
│ 후보 노드 목록  │ ──────────────────────▶ │ savings = 기존가 - 신규가 │
│ (disruption    │  (오름차순 정렬 순서로)   │  savings > 0 ?         │
│  cost 오름차순) │                         │   yes → 무조건 실행     │
└───────────────┘                         └──────────────────────┘
   ※ disruption 규모(쫓겨나는 파드 수·비용)는 실행 여부에 전혀 반영 안 됨
   ※ "얼마나 아끼는지"만 보고, "얼마나 손해인지"는 안 봄

Balanced 정책은 두 값을 NodePool 단위로 정규화해서 비율로 비교한다(pkg/controllers/disruption/balanced.go, types.go):

savings_fraction     = savings / nodepool_total_cost
disruption_fraction  = disruption_cost / nodepool_total_disruption_cost
score                = savings_fraction / disruption_fraction
move 승인 조건: score >= 1/k   (Balanced는 k=2 고정 → score >= 0.5)
  • nodepool_total_cost는 풀에 속한 노드 가격의 합, nodepool_total_disruption_cost후보 여부와 무관하게 풀에 속한 모든 노드의 disruption cost 합(비후보 노드도 분모에 포함 — 그래야 통합 직후 분모가 인위적으로 작아져 다음 라운드가 과도하게 공격적으로 되는 걸 막는다).
  • 노드 하나의 disruption cost는 PerNodeBaseDisruptionCost(1.0) + 파드별 max(0, EvictionCost(pod))의 합이다(computeRescheduleDisruptionCost). 이 1.0이라는 "노드 자체를 건드리는 데 드는 기본 비용"(cordon·drain·API 호출·교체 지연)이 새로 추가된 개념이다 — 예전에는 파드가 없으면 disruption cost도 정확히 0이었다.
  • "빈 노드"의 정의도 바뀌었다: 예전 Emptinesslen(reschedulablePods) == 0(파드가 문자 그대로 하나도 없음)을 봤지만, 새 Candidate.IsEmpty()RescheduleDisruptionCost <= PerNodeBaseDisruptionCost를 본다 — 파드는 있지만 전부 pod-deletion-cost가 음수라서 disruption cost가 0으로 clamp되는 노드도 이제 "empty" 취급된다. consolidation.goShouldDisrupt도 이제 empty 노드는 아예 걸러내고 Emptiness 컨트롤러에게 넘긴다(budget 계산을 올바른 카테고리로 귀속시키기 위해) — 예전엔 이 구분이 ConsolidationPolicy != WhenEmptyOrUnderutilized라는 정책 이름 체크 하나로 뭉뚱그려져 있었다.
  • multi-node consolidation의 후보 정렬도 disruption cost 오름차순에서 SavingsRatio = Price / RescheduleDisruptionCost 내림차순으로 바뀌었다. 예전 정렬은 "묶기 쉬운 노드(disruption 적은 노드)부터" 찾았다면, 새 정렬은 "묶을 가치가 있는 노드(단위 disruption당 저장액이 큰 노드)부터" 찾는다 — 이진 탐색이 budget 안에서 가장 임팩트 큰 move들을 먼저 실행하게 하려는 의도다.

설계 결정 및 트레이드오프

왜 k=2인가: 설계 문서는 c7i/m7i/r7i 세 인스턴스 패밀리(medium4xlarge, 15개 가격대), 16대 노드, 노드당 0~4개 파드, 파드당 disruption cost {1,2,5,10} 조합을 전수 열거해서 검증했다. k를 정하는 기준은 "저장액 단조성", "disruption 단조성", "빈 노드는 항상 삭제 가능", "동일가 교체는 항상 거절" 같은 대수적 성질(모든 k에서 성립)과, k값에 따라 갈리는 두 실증적 성질 — "같은 패밀리 안에서의 REPLACE가 실제로 통과하는가"와 "churn chain(m7i.xlarge → m7i.large → c7i.medium 같은 연쇄 교체)이 수렴하는가"다. k=2는 이 두 실증적 성질을 만족하는 가장 작은 k였다(같은 패밀리 REPLACE가 통과하면서 churn chain은 최대 4단계 안에 수렴).

기각된 대안들: 문서는 다섯 가지를 검토 후 기각했다 — ① 가격 개선 비율(old/new >= 2배 같은 factor, disruption을 아예 안 봄), ② 절대 달러 임계값(풀 크기에 따라 상대적 의미가 달라짐), ③ CA류의 사용률 임계값(같은 40% 사용률이라도 파드 성격에 따라 손해 여부가 다름을 구분 못함), ④ single-node consolidation(REPLACE) 전체 비활성화(저장액/disruption 비율과 무관하게 전부 막아버림), ⑤ 별도의 disruption-cost 어노테이션 신설(기존 pod-deletion-cost/priority로 이미 충분하다고 판단). 공통적으로 이 대안들은 "저장액과 disruption을 함께, 비례적으로" 보지 못한다는 게 기각 이유다.

받아들인 트레이드오프: NodePool 합계(nodepool_total_cost/nodepool_total_disruption_cost)는 사이클당 한 번만 스냅샷하고, 같은 사이클 안의 뒤쪽 move들은 그 스냅샷이 낡아진 채로 평가된다 — 경계값 근처의 move는 사이클이 지나면 판정이 뒤집힐 수 있지만 다음 사이클에서 자연히 교정된다는 전제로 정확성보다 단순성을 택했다. PerNodeBaseDisruptionCost = 1.0도 "아직 정밀하게 모델링하지 않은 자리 표시자"라고 문서가 명시한다(예: GPU 노드가 CPU 노드보다 disruption cost가 커야 하는가는 미해결). 그리고 k=2가 실제로 다양한 워크로드에서 적절한 기본값인지는 문서도 "모른다"고 인정한다 — 그래서 Balanced는 기본값이 아니라 NodePool별 opt-in(consolidationPolicy: Balanced를 명시해야 적용)으로 도입해, 실제 사용 데이터로 이후 기본값 논의를 이어가는 전략을 택했다.

[변경 후] Balanced: NodePool 단위로 저장액/disruption 비율을 점수화
┌────────────────────────┐
│ 풀 전체 스냅샷           │
│  total_cost             │
│  total_disruption_cost  │  (비후보 노드도 분모에 포함)
└───────────┬────────────┘
            │
            ▼
┌────────────────────────┐   score = savings_fraction   ┌───────────────────┐
│ move 후보 (Delete/Replace)│ ───────────────/disruption_fraction──▶│ score >= 1/k(=0.5)? │
│  savings, disruption_cost │                             │  yes → 승인, 이벤트 기록 │
└────────────────────────┘                             │  no  → 거절, 이벤트 기록 │
                                                          └───────────────────┘
   ※ 승인된 move들은 SavingsRatio 내림차순으로 정렬 후 budget 안에서 실행

더 살펴볼 점

  • k=2가 실제 다양한 워크로드(특히 pod-deletion-cost를 안 쓰는 대다수 클러스터)에서도 적절한 기본값으로 검증될까, 아니면 이후 릴리스에서 조정될까?
  • 단일 노드 풀(pool size=1)의 DELETE가 항상 score 1.0으로 통과하는 엣지 케이스는 disruption budget(nodes: 0) 설정 없이도 실사용에서 문제가 될까?
  • PerNodeBaseDisruptionCost가 GPU/특수 인스턴스에 대해 실제로 다르게 매겨지는 후속 변경이 나올까?

참고 자료

참고 외 별도 확인 링크

본문 참고 자료로 충분했다.