쿠버네티스 학습 노트 목차

스케줄링과 리소스 — 파드는 어느 노드로 가나

스케줄러는 딱 한 가지 결정만 한다 — 이 파드를 어느 노드에 둘 것인가. 그런데 그 한 결정에 Pending 의 거의 모든 원인이 들어 있다.


1. 두 단계로 정한다

  • 필터링(Filtering) 둘 수 있는 노드를 추린다

    • 자원이 남는가 (requests 기준)
    • 노드 셀렉터·어피니티 조건에 맞는가
    • 테인트를 견딜 수 있는가
    • 포트가 비어 있는가 · 볼륨을 붙일 수 있는가
  • ② 스코어링(Scoring) 남은 것 중 가장 좋은 곳을 고른다

    • 자원이 고르게 퍼지도록
    • 이미지가 이미 있는 노드를 선호
    • 어피니티 가중치 반영

하나도 안 남으면 파드Pending 으로 멈춘다 그리고 그 이유가 이벤트에 그대로 적힌다

kubectl describe pod web-xxx | tail -20
# Events:
#   Warning  FailedScheduling  0/5 nodes are available:
#     3 Insufficient cpu, 2 node(s) had untolerated taint {node-role...}

Pending 을 만나면 반드시 describe 를 읽는다. 몇 개 노드가 왜 탈락했는지가 문장으로 나온다. 추측할 필요가 없다.

2. requests 와 limits — 역할이 다르다

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "512Mi"
  • requests — '스케줄링 기준' 이다

    • 이만큼 확보 가능한 노드를 찾는다
    • 실제 사용량과 무관하다 — 예약이지 소비가 아니다
  • limits — '실행 중 상한' 이다

가장 흔한 오해 — "노드에 메모리가 남는데 왜 Pending 인가"

  • 스케줄러는 '실제 사용량' 이 아니라 'requests 의 합' 을 본다
  • requests 를 크게 잡은 파드들이 실제로는 놀고 있어도
  • 장부상으로는 노드가 꽉 찬 것이다

kubectl describe node <name> 의 Allocated resources 가 그 장부다

CPU 단위

  • 1 = 1 코어(vCPU)
  • 500m — = 0.5 코어 (m = milli)
  • 250m — = 0.25 코어
메모리
  Mi/Gi   2진 (1Mi = 1,048,576 바이트)   ← 보통 이것을 쓴다
  M/G     10진 (1M = 1,000,000 바이트)

512M 과 512Mi 는 다르다. 섞어 쓰면 계산이 어긋난다

3. QoS 클래스 — 자원이 모자랄 때 누가 먼저 죽나

  • Guaranteed — 모든 컨테이너requests == limits (cpu·memory 둘 다)

    • 가장 나중에 퇴출된다
  • Burstable — requests 는 있는데 limits 와 다르거나 일부만 있다

    • 중간
  • BestEffort — requests·limits 가 아예 없다

    • 가장 먼저 퇴출된다
kubectl get pod web -o jsonpath='{.status.qosClass}'

노드 메모리가 부족해지면 kubelet파드를 퇴출(evict)한다 순서 — BestEffort → Burstable(requests 초과분이 큰 순) → Guaranteed

중요한 워크로드일수록 requests 를 명시해야 하는 이유다 아무것도 안 적으면 BestEffort 가 되어 제일 먼저 쫓겨난다

4. 어디에 둘지 지정하기

# ① 가장 단순 — 라벨이 맞는 노드에만
nodeSelector:
  disktype: ssd
# ② 어피니티 — 조건을 더 표현할 수 있다
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:      # 필수
      nodeSelectorTerms:
        - matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values: ["ap-northeast-2a", "ap-northeast-2c"]
    preferredDuringSchedulingIgnoredDuringExecution:     # 선호
      - weight: 100
        preference:
          matchExpressions:
            - key: node-type
              operator: In
              values: ["compute"]
  • required — 만족하는 노드가 없으면 Pending

  • preferred — 없으면 그냥 다른 곳에 둔다 (점수만 준다)

  • IgnoredDuringExecution 이 이름에 붙은 이유

    • 이미 떠 있는 파드는 조건이 깨져도 쫓겨나지 않는다
    • 배치 시점에만 본다

파드끼리의 관계

# 같은 노드에 모으기 (affinity) — 캐시와 앱을 붙인다
podAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels: { app: cache }
      topologyKey: kubernetes.io/hostname

# 흩뜨리기 (anti-affinity) — 같은 노드에 몰리지 않게
podAntiAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels: { app: web }
      topologyKey: kubernetes.io/hostname
topologyKey 가 '무엇을 같은 곳으로 볼 것인가' 를 정한다
  kubernetes.io/hostname          같은 노드
  topology.kubernetes.io/zone     같은 가용 영역

anti-affinity 를 required 로 걸고 replicas 를 노드 수보다 크게 잡으면 남는 파드가 영원히 Pending 이 된다 — 매우 흔한 실수다

더 나은 도구 — topologySpreadConstraints

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels: { app: web }

anti-affinity 는 '같이 두지 마라' 만 말할 수 있다 spread 는 '얼마나 고르게' 를 수치로 말한다

maxSkew: 1        영역 간 파드 수 차이가 1을 넘지 않게
whenUnsatisfiable
  DoNotSchedule    못 지키면 Pending
  ScheduleAnyway   못 지켜도 배치 (점수만 반영)  ← 대개 이쪽이 안전하다

5. 테인트와 톨러레이션 — 노드가 거부한다

kubectl taint nodes node1 gpu=true:NoSchedule
tolerations:
  - key: "gpu"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"
방향이 반대다
  어피니티      파드가 "나는 이런 노드로 가고 싶다"
  테인트        노드가 "나는 아무나 안 받는다"
  톨러레이션    파드가 "나는 그 거부를 견딜 수 있다"
effect
  NoSchedule         새로 배치되지 않는다 (기존 파드는 유지)
  PreferNoSchedule   가급적 피한다
  NoExecute          기존 파드도 쫓아낸다

쓰임

  • GPU 노드를 전용으로 — GPU 파드만 톨러레이션을 갖는다
  • 컨트롤 플레인 노드에 일반 워크로드가 안 뜨게 (기본 테인트)
  • 문제 있는 노드를 격리 (cordon/drain 과 함께)

주의 — 톨러레이션이 있다고 '그 노드로 간다' 는 뜻이 아니다

  • '갈 수 있다' 는 뜻일 뿐이다. 유도하려면 nodeAffinity 를 함께 쓴다

6. 노드를 비우기 — cordon 과 drain

kubectl cordon node1       # 새 파드를 안 받는다 (기존은 그대로)
kubectl drain node1 --ignore-daemonsets --delete-emptydir-data
kubectl uncordon node1     # 다시 받는다
drain 은 노드의 파드를 다른 곳으로 옮긴다 (퇴출 API 사용)
  --ignore-daemonsets      DaemonSet 파드는 어차피 노드마다 떠야 하므로 제외
  --delete-emptydir-data   emptyDir 데이터가 사라짐을 인정한다

노드 업그레이드·교체의 표준 절차다

  • cordon → drain → 작업 → uncordon

PodDisruptionBudget — 한꺼번에 빠지지 않게

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 2          # 또는 maxUnavailable: 1
  selector:
    matchLabels: { app: web }

drain 이나 클러스터 오토스케일러가 파드를 뺄 때 이 조건을 지킨다

  • 세 개 중 두 개는 항상 살아 있어야 한다면 minAvailable: 2

주의

  • PDB 는 '자발적 중단' 만 막는다 (drain · 오토스케일러)
    • 노드가 갑자기 죽는 것(비자발적)은 못 막는다
  • minAvailable 을 replicas 와 같게 두면 drain 이 영원히 멈춘다
    • 노드 업그레이드가 진행되지 않는다. 흔한 사고다

7. 오토스케일링 세 가지

  • HPA (Horizontal Pod Autoscaler) 파드 '개수' 를 늘린다
  • VPA (Vertical Pod Autoscaler) — 파드의 requests/limits 를 조정한다
  • Cluster Autoscaler — '노드' 를 늘린다
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

중요 — HPA 의 CPU 사용률은 'requests 대비' 비율이다

  • requests 가 없으면 계산이 불가능해 HPA 가 동작하지 않는다
  • HPA 를 쓰려면 requests 는 필수다

그리고 metrics-server 가 설치돼 있어야 한다 없으면 kubectl top 도 안 되고 HPA 도 unknown 으로 멈춘다

세 오토스케일러의 상호작용 HPA 와 VPA 를 같은 지표(CPU)로 함께 쓰면 서로 싸운다

  • VPA 는 requests 를 올리고, HPA 는 그 requests 기준 비율을 본다

  • 권장되지 않는 조합이다 HPA + Cluster Autoscaler 는 잘 맞는다

  • 파드가 Pending 이 되면 노드를 늘린다

In-Place 리사이즈 (1.35 GA)

예전에는 requests/limits 를 바꾸면 파드가 재생성됐다 1.35 부터 재시작 없이 조정할 수 있다 (In-Place Pod Resize) 1.36 에서는 파드 수준 리소스도 조정 가능해졌다 (베타)

상태 있는 워크로드에서 특히 의미가 크다 — 스케일 조정이 곧 중단이 아니게 된다

8. LimitRange 와 ResourceQuota

# 네임스페이스 안 파드의 기본값·상한을 정한다
apiVersion: v1
kind: LimitRange
spec:
  limits:
    - type: Container
      default:        { cpu: "500m", memory: "512Mi" }   # limits 기본값
      defaultRequest: { cpu: "100m", memory: "128Mi" }   # requests 기본값
      max:            { cpu: "2",    memory: "2Gi" }
# 네임스페이스 전체 총량을 제한한다
apiVersion: v1
kind: ResourceQuota
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    pods: "50"

LimitRange 가 있으면 requests 를 안 쓴 파드에도 기본값이 붙는다

  • BestEffort 파드가 생기는 것을 구조적으로 막는다

ResourceQuota 가 걸린 네임스페이스에서는

  • requests/limits 를 명시하지 않은 파드가 '생성 거부' 된다
  • "파드가 아예 안 만들어진다" 의 원인. 이벤트에 이유가 나온다

한눈에 정리

  • 스케줄링 — 필터링 → 스코어링. 실패 이유는 describe 의 Events 에 문장으로
  • requests — 스케줄링 기준(예약). 실제 사용량이 아니라 장부다
  • limits — 실행 중 상한. CPU 는 스로틀, 메모리는 OOMKilled
  • QoS — Guaranteed > Burstable > BestEffort 순으로 늦게 퇴출된다
  • nodeSelector/어피니티 — required 는 없으면 Pending, preferred 는 점수만
  • anti-affinity required + replicas > 노드 수 = 영구 Pending
  • spread — maxSkew 로 고르게. ScheduleAnyway 가 대개 안전하다
  • 테인트 — 노드가 거부 · 톨러레이션은 '견딜 수 있다' 일 뿐 유도가 아니다
  • cordon/drain — 노드 교체 표준 절차. PDB 로 한꺼번에 빠지는 것을 막는다
    • minAvailable == replicas 면 drain 이 영원히 멈춘다
  • HPA — requests 대비 비율이다 → requests 없으면 동작하지 않는다
    • metrics-server 필수

출처 — Kubernetes Docs: "Kubernetes Scheduler" · "Assigning Pods to Nodes" · "Taints and Tolerations" · "Pod Topology Spread Constraints" · "Resource Management for Pods and Containers" · "Pod Quality of Service Classes" · "Horizontal Pod Autoscaling" · "Disruptions" · CKA Curriculum v1.35

워크로드 — 파드부터 Deployment까지네트워킹 — 서비스부터 Gateway API까지