본문으로 건너뛰기
무엇을 봐야 하나 — 메트릭·로그·이벤트

09 · 무엇을 봐야 하나 — 메트릭·로그·이벤트

한눈에

  • 코어가 export하는 메트릭은 60개입니다. promauto·MustRegister 직접 호출이 0건이라 이 목록이 전량입니다.
  • karpenter_nodeclaims_disrupted_total{reason} 하나가 “노드가 왜 갈렸나"를 전부 가릅니다. reason 값은 10종이고 §3에 전부 있습니다.
  • karpenter_nodes_*의 라벨 수는 프로바이더에 따라 달라집니다. EKS에서는 instance_family·instance_size 등이 더 붙습니다 — 대시보드를 코어 기준으로 짜면 어긋납니다.
  • 판정 로그는 --log-level debug에서만 나옵니다. consolidation score, marking drifted, abandoning ... due to timeout 전부 V(1)입니다.
  • ICE(Insufficient Capacity Error)와 NodeClass 미준비가 같은 로그 문자열을 씁니다(failed launching nodeclaim). 로그만으로는 구분이 안 되고 메트릭 reason을 봐야 합니다.
  • 이벤트에는 dedupe 창이 있습니다. Unconsolidatable은 15분, FailedScheduling은 5분 — 카운트를 빈도로 읽으면 안 됩니다.

왜 이 문서인가. 앞의 여덟 문서는 “왜 그렇게 판단하나"를 코드로 따라갔습니다. 이 문서는 그 판단을 밖에서 어떻게 확인하나 — 대시보드·로그·이벤트 — 를 다룹니다.

예산 판정 절차는 08 §6, 폴백 여부는 07이 소유합니다. 여기서는 신호의 목록과 성질만 정리하고 무엇을 실제로 저장할 것인가10 메트릭 수집 비용이 받는다 — 60개 전부를 긁는 건 비용에서만 문제가 됩니다.

1. 어디서 나오나 — 세 층이 섞여 있다

/metrics 엔드포인트 하나에 출처가 다른 셋이 합쳐져 나옵니다. 이걸 모르면 “문서에 있는데 내 클러스터엔 없다"가 반복됩니다.

무엇이 문서의 범위
코어 (kubernetes-sigs/karpenter)karpenter_* 60개전량 열거
프로바이더 (provider-aws)오퍼링 가격 등 + karpenter_nodes_*추가 라벨범위 밖
operatorpkg · controller-runtimeoperator_status_condition_*, controller_runtime_*, workqueue_*범위 밖

karpenter_nodes_*의 라벨 집합은 컴파일 타임이 아니라 런타임에 결정됩니다 — 클라우드 프로바이더가 v1.WellKnownLabels에 자기 라벨을 Insert하기 때문입니다(metrics/node/controller.go:62-64). 두 번째 층이 함정입니다. 코어 라벨은 nodepool·zone·region·instance_type·arch·os·capacity_type·windows_build·node_name·phase·managed뿐이지만 EKS에서는 instance_family·instance_size 등이 더 붙습니다.

이름 조립 규칙은 karpenter + Subsystem + Name입니다(pkg/metrics/constants.go:27).

2. 먼저 볼 것 — 질문 여섯 개

질문메트릭
노드가 왜 갈렸나karpenter_nodeclaims_disrupted_total{reason}
예산이 막고 있나karpenter_nodepools_allowed_disruptions{reason}
파드가 왜 안 뜨나karpenter_scheduler_unschedulable_pods_count
AZ가 한쪽으로 쏠리나karpenter_scheduler_pending_pods_by_effective_zone_count
통합이 왜 거부됐나karpenter_consolidation_score{decision,policy}
클러스터에 여유가 있나karpenter_cluster_utilization_percent{resource_type}

첫 두 개가 실무의 8할입니다. 아래 조합이 “노드가 안 줄어든다"를 한 화면에서 가릅니다.

# 이유별 예산 허용량 — 0인 구간이 의도한 창과 일치하는가
karpenter_nodepools_allowed_disruptions

# 이유별 실제 disruption 속도 — 위가 0이 아닌데 여기가 0이면 예산이 아닌 다른 원인
rate(karpenter_nodeclaims_disrupted_total[30m])

pending_pods_by_effective_zone_countzone 라벨은 실제 zone 이름 외에 flexible(zone 제약 없음)과 none(교집합이 빔)을 가집니다. none이 0이 아니면 그건 용량 문제가 아니라 오설정입니다.

3. 노드가 왜 갈렸나 — reason 10종

karpenter_nodeclaims_disrupted_totalreason 값은 소스에 흩어져 있습니다. 모아 놓으면 노드 교체 원인의 전체 분류가 됩니다.

reason무슨 일이 있었나예산
underutilized통합이 더 싼 배치를 찾음소비
empty재스케줄 대상 파드가 0소비
drifted해시·requirement 불일치소비
expiredexpireAfter 만료
unhealthyNode Repair가 제거
garbage_collected클라우드에 인스턴스가 없음
insufficient_capacityICE — 런치 실패
nodeclass_not_readyEC2NodeClass 미준비
launch_timeout5분 안에 런치 실패
registration_timeout15분 안에 등록 실패

앞의 셋만 08의 예산을 탑니다. expired·unhealthy가 올라가는데 예산을 조이고 있다면 헛수고 중입니다.

뒤의 넷은 disruption이라기보다 런치 실패입니다 — insufficient_capacity가 계단식으로 오르면 07의 ICE 경로입니다. registration_timeout이 오르면 노드는 떴는데 kubelet이 붙지 못한 것이라 폴백이 구제하지 못합니다.

karpenter_pods_drained_totalreason은 성격이 다릅니다 — NodeClaim의 DisruptionReason 컨디션을 그대로 쓰되 없으면 리터럴 "Forceful Termination"이 들어갑니다(eviction.go:223-238).

4. 전체 목록

4.1 프로비저닝 · 스케줄링 (14)

메트릭 14개 펼치기
메트릭타입라벨비고
karpenter_scheduler_scheduling_duration_secondsHistogramcontroller
karpenter_scheduler_queue_depthGaugecontroller,scheduling_id
karpenter_scheduler_unfinished_work_secondsGaugecontroller,scheduling_id히스토그램 미집계분
karpenter_scheduler_ignored_pods_countGauge
karpenter_scheduler_unschedulable_pods_countGaugecontroller
karpenter_scheduler_pending_pods_by_effective_zone_countGaugecontroller,zone
karpenter_pods_scheduling_decision_duration_secondsHistogram최초 인지 → 첫 스케줄 시도
karpenter_nodeclaims_created_totalCounterreason,nodepool,min_values_relaxed
karpenter_nodes_created_totalCounternodepool,zone
karpenter_pods_stateGaugenode라벨+phase,ready,scheduled,owner,managed값 1
karpenter_pods_startup_duration_secondsSummary생성 → Running
karpenter_pods_unstarted_time_secondsGaugename,namespace
karpenter_pods_bound_duration_secondsHistogram생성 → bound
karpenter_pods_unbound_time_secondsGaugename,namespace

4.2 Disruption · consolidation (17)

  • karpenter_nodeclaims_disrupted_total · Counter · {reason,nodepool,capacity_type} — §3
  • karpenter_pods_disruption_initiated_total · Counter · {reason,nodepool,capacity_type} — 재스케줄 대상 파드 누적(DaemonSet·mirror 제외)
  • karpenter_nodepools_allowed_disruptions · Gauge · {nodepool,reason}
  • karpenter_nodepools_nodes_consuming_budgets · Gauge · {nodepool,reason}
  • karpenter_voluntary_disruption_eligible_nodes · Gauge · {reason}
  • karpenter_voluntary_disruption_decisions_total · Counter · {decision,reason,consolidation_type}
  • karpenter_voluntary_disruption_decisions_by_nodepool_total · Counter · 위 + {nodepool}
  • karpenter_voluntary_disruption_decision_evaluation_duration_seconds · Histogram · {reason,consolidation_type}
  • karpenter_voluntary_disruption_queue_failures_total · Counter · {decision,reason,consolidation_type}
  • karpenter_voluntary_disruption_consolidation_timeouts_total · Counter · {consolidation_type}
  • karpenter_voluntary_disruption_failed_validations_total · Counter · {consolidation_type} — 선정 후 validation 실패
  • karpenter_consolidation_score · Histogram · {decision,nodepool,policy}Balanced 스코어
  • karpenter_consolidation_moves_total · Counter · {decision,nodepool,policy}
  • karpenter_nodeclaims_unhealthy_disrupted_total · Counter · {condition,nodepool,capacity_type,image_id} — Node Repair
  • karpenter_pods_eviction_requests_total · Counter · {code}
  • karpenter_pods_drained_total · Counter · {reason}
  • karpenter_nodes_drained_total · Counter · {nodepool}

라벨 값: consolidation_typemulti·single. decision(스코어) ∈ approved·rejected. policyconsolidationPolicy 문자열 그대로. voluntary 계열의 reason은 snake_case(underutilized·empty·drifted)다.

4.3 NodePool · NodeClaim · Node 상태 (22)

메트릭 22개 펼치기
메트릭타입라벨비고
karpenter_nodepools_limit/_usageGaugeresource_type,nodepool
karpenter_nodepools_cost_totalGaugenodepoolKarpenter 관점 비용, 과금 근거 아님
karpenter_nodepools_cost_tracker_errors_totalCounternodepool
karpenter_nodes_allocatableGaugenode라벨+resource_type
karpenter_nodes_total_pod_requests/_pod_limitsGauge바인딩 파드(DaemonSet 포함)
karpenter_nodes_total_daemon_requests/_daemon_limitsGaugeDaemonSet 몫만
karpenter_nodes_system_overheadGaugecapacity − allocatable
karpenter_nodes_current_lifetime_secondsGauge노드 나이
karpenter_cluster_utilization_percentGaugeresource_type
karpenter_nodes_termination_duration_secondsSummarynodepool삭제요청 → finalizer 제거
karpenter_nodes_lifetime_duration_secondsHistogram(15분~30일)nodepool
karpenter_nodes_terminated_totalCounternodepool,zone
karpenter_nodeclaims_terminated_totalCounternodepool,capacity_type,zone
karpenter_nodeclaims_termination_duration_secondsHistogram(1~2048초)nodepool
karpenter_nodeclaims_instance_termination_duration_secondsHistogram(1~1024초)nodepool
karpenter_cluster_state_node_countGauge
karpenter_cluster_state_syncedGauge(0/1)
karpenter_cluster_state_unsynced_time_secondsGauge
karpenter_build_infoGauge(상수 1)version,goversion,goarch,commit

_total_pod_requests에서 _total_daemon_requests를 빼면 워크로드 몫만 남습니다. 노드 크기를 줄일 수 있는지 판단할 때 이 차이가 실제 근거입니다 — DaemonSet은 노드를 줄여도 줄지 않습니다.

cluster_state_synced가 0인 구간은 Karpenter가 낡은 상태로 시뮬레이션하고 있었다는 뜻입니다. 그 구간의 판단은 전부 의심해야 합니다.

4.4 클라우드 프로바이더 인터페이스 (2)

  • karpenter_cloudprovider_duration_seconds · Histogram · {controller,method,provider}
  • karpenter_cloudprovider_errors_total · Counter · {controller,method,provider,error}

error 라벨의 well-known 값은 NodeClaimNotFoundError·NodeClassNotReadyError·InsufficientCapacityError와 빈 문자열(미분류)입니다. ICE를 이 메트릭으로도 볼 수 있습니다disrupted_total{reason="insufficient_capacity"}와 교차 확인하면 “정말 용량 문제인지"가 갈립니다.

4.5 ALPHA — 이름이 바뀔 수 있다 (5)

  • karpenter_pods_provisioning_bound_duration_seconds · Histogram
  • karpenter_pods_provisioning_unbound_time_seconds · Gauge · {name,namespace}
  • karpenter_pods_provisioning_startup_duration_seconds · Histogram
  • karpenter_pods_provisioning_unstarted_time_seconds · Gauge · {name,namespace}
  • karpenter_pods_provisioning_scheduling_undecided_time_seconds · Gauge · {name,namespace}

다섯 모두 Help에 같은 단서가 붙습니다 — “this calculated from a point in memory, not by the pod creation timestamp.” 파드 생성 시각이 아니라 Karpenter가 그 파드를 처음 본 시점 기준이라 컨트롤러 재시작 시 기준점이 리셋됩니다. SLO에 쓰기 어려운 이유입니다.

karpenter_nodepools_cost_* 둘도 alpha 컴포넌트에서 나옵니다(state/cost/cost.go:60).

5. 로그

레벨은 --log-leveldebug·info·error입니다. zapr가 logr V(1)을 zap DebugLevel로 매핑하므로 V(1) 로그는 debug에서만 보입니다. .V(2)는 0건 — 더 깊은 티어는 없습니다.

예외 하나. IgnoreDebugEvents sink가 events 서브로거의 V(1)을 무조건 드롭합니다(logging.go:96-119) — 스케일아웃 때 event recorder가 로그를 덮는 걸 막는 장치입니다.

5.1 판정 로그 — debug에서만 나온다

메시지무엇을 알려주나
consolidation scoreBalanced의 스코어·임계값·승인 여부 전체
marking driftedreason 필드에 drift 사유
marking consolidatable그 노드가 후보가 된 시점
removing consolidatable status conditionlastPodEventTime·consolidateAfter·timeSincePodEvent
abandoning single-node consolidation due to timeoutcandidates_evaluated
stopping multi-node consolidation after timeout마지막 유효 커맨드
abandoning empty node consolidation attempt due to pod churn파드가 들어와 무효가 됨

consolidation score의 필드가 특히 촘촘합니다 — score·savings_fraction·disruption_fraction·threshold·k·approved·candidates가 전부 붙습니다. 06 §1의 부등식을 실제 값으로 확인하려면 이 한 줄이면 됩니다.

실제로 무언가를 지울 때는 info로 나옵니다 — disrupting node(s)command 필드와 함께 찍힙니다.

5.2 스케줄 실패

메시지레벨
could not schedule podError
skipping, nodepool requirements filtered out all instance typesInfo
pod(s) have a preferred Anti-Affinity which can prevent consolidationInfo
pod(s) have a preferred TopologySpreadConstraint which can prevent consolidationInfo
instance types were excluded because they would breach limitsdebug
relaxing soft constraints for pod since it previously failed to scheduledebug
scheduling simulation timed outdebug

가운데 둘은 경고 성격이지만 Info로 나옵니다. “통합이 안 됩니다"의 원인인데 info 볼륨에 묻혀 놓치기 쉽습니다. 각각 최대 10개 파드를 샘플로 찍습니다.

node limits have been exhausted for nodepoolall available instance types exceed limits for nodepool로그가 아니라 에러 문자열입니다. could not schedule pod의 err 필드와 FailedScheduling 이벤트 메시지로만 보입니다.

5.3 런치 실패 — 같은 문자열, 다른 원인

ICE와 NodeClass 미준비가 똑같이 failed launching nodeclaim으로 찍힙니다(launch.go:87:99). 로그 문자열만으로는 구분되지 않습니다. 가르려면 메트릭을 봐야 합니다.

karpenter_nodeclaims_disrupted_total{reason="insufficient_capacity"}   # ICE
karpenter_nodeclaims_disrupted_total{reason="nodeclass_not_ready"}     # EC2NodeClass

CreateFleet이나 InsufficientInstanceCapacity 같은 AWS 원문 문자열은 코어에 없습니다. 코어는 에러 타입만 정의하고(cloudprovider/types.go:619-707) 실제 매핑은 provider-aws가 합니다.

6. 이벤트

kubectl get events로 보는 것들입니다. Reason 기준으로 필터링합니다.

kubectl get events -A --field-selector reason=DisruptionBlocked
kubectl get events -A --field-selector reason=Unconsolidatable
Reason대상언제
DisruptionBlockedNodePool · Node예산에 막힘 (08)
UnconsolidatableNode · NodeClaim후보 단계에서 탈락
ConsolidationCandidateNode · NodeClaim절감액 포함
ConsolidationApprovedNode · NodePool스코어가 임계 통과
ConsolidationRejectedNode · NodeClaim스코어 미달
DisruptionLaunchingNodeClaim대체 노드 기동 시작
DisruptionWaitingReadinessNodeClaim대체 노드 준비 대기
DisruptionTerminatingNode · NodeClaim실제 종료
FailedSchedulingPod(Warning)
NoCompatibleInstanceTypesNodePoolrequirements 전부 걸러냄 (Warning)
NominatedPod이 노드에 뜰 예정
InsufficientCapacityErrorNodeClaimICE (Warning)
FailedDrainingNode(Warning)
TerminationGracePeriodExpiringNode · NodeClaim(Warning)
AwaitingVolumeDetachmentNode
NodeRepairBlockedNodeunhealthy 비율 상한 초과 (Warning)

6.1 dedupe 창 때문에 카운트를 빈도로 읽으면 안 된다

이벤트마다 중복 억제 창이 다릅니다 — 기본 2분에 예외가 셋입니다.

Reason
Unconsolidatable15분
NodeRepairBlocked15분
FailedScheduling5분
NoCompatibleInstanceTypes · NodePool DisruptionBlocked1분
그 외2분

08 §4.2x297 over 8h가 “8시간에 297번"으로 읽히는 것은 그 이벤트의 창이 1분이기 때문입니다. Unconsolidatable을 같은 방식으로 읽으면 실제 발생의 15분의 1만 보게 됩니다.

Nominated에는 dedupe 외에 rate limiter도 걸려 있습니다(5qps, burst 10). 대규모 스케일아웃에서는 일부가 아예 방출되지 않습니다.

7. 소스에서 확인한 이상 징후 넷

문서가 아니라 코드를 읽어야만 보이는 것들입니다.

① 정의만 있고 방출되지 않는 이벤트가 셋입니다. NodeClassNotReady(nodeclaim/lifecycle/events.go:38), NodePool용 DisruptionBlocked 변형(disruption/events/events.go:128), TerminationFailed 상수 — 호출처가 0건입니다. 알림 룰을 이 Reason으로 걸면 영원히 안 울립니다.

NodeRepairBlocked는 전부 Node에 달립니다. Node·NodeClaim·NodePool 각각에 남기려는 의도로 보이나 InvolvedObject가 셋 다 node입니다(node/health/events.go:28-55). --field-selector involvedObject.kind=NodePool로는 안 잡힙니다.

③ Reason 레지스트리를 우회하는 이벤트가 넷 있습니다. AwaitingVolumeDetachment·InvalidDoNotDisruptAnnotation·DoNotDisruptUntil·DoNotDisruptGracePeriodElapsedpkg/events/reason.go의 상수가 아니라 인라인 문자열입니다. 상수 목록만 보고 알림을 짜면 이 넷이 빠집니다.

consolidation_timeouts_total은 0으로 시딩됩니다. multi·single 두 라벨이 init()에서 미리 0으로 등록되므로(disruption/metrics.go:35-38) 메트릭이 없는 것과 0인 것이 구분됩니다. absent() 기반 알림을 쓸 필요가 없습니다.

8. 근거

kubernetes-sigs/karpenter v1.14.0-6-gac7a021e 로컬 체크아웃 기준입니다. 상대 경로는 레포 루트.

  • 전량임의 근거opmetrics.NewPrometheus* 선언 60건(테스트 제외), promauto·MustRegister 직접 호출 0건
  • 이름 조립pkg/metrics/constants.go:27(Namespace), pkg/metrics/metrics.go:28(Subsystem)
  • scheduler 6종controllers/provisioning/scheduling/metrics.go:27-108
  • disruption 11종controllers/disruption/metrics.go:27-151
  • pod 10종(alpha 5 포함)controllers/metrics/pod/controller.go:46-198
  • node 8종 · 동적 라벨controllers/metrics/node/controller.go:45-160, 291-306, 런타임 결정 근거는 :62-64
  • nodepool 3종controllers/metrics/nodepool/controller.go:44-81
  • cluster_state 3종controllers/state/metrics.go:27-73
  • termination·drain 5종controllers/node/termination/metrics.go:31-89, .../terminator/metrics.go:27-54
  • disrupted_total의 reason 10종expiration/controller.go:88, garbagecollection/controller.go:113, node/health/controller.go:175, lifecycle/launch.go:93,104, lifecycle/liveness.go:53-54, disruption/queue.go:243
  • 로그 레벨 매핑 · events 드롭operator/logging/logging.go:41-71, 96-119
  • 이벤트 구조·dedupepkg/events/recorder.go:30-38, 56, 72-87, Reason 상수 pkg/events/reason.go:22-52
  • 에러 타입pkg/cloudprovider/types.go:619-707

확인하지 못한 것 — provider-aws와 operatorpkg 체크아웃이 없어 karpenter_cloudprovider_instance_type_offering_* 계열과 operator_status_condition_*의 정확한 이름·라벨은 열거하지 못했습니다. 실제 EKS의 /metrics에는 이 문서의 60개보다 많이 나옵니다.

마지막 수정 일자