Published on

EKS Pod가 Pending(Insufficient memory)일 때 점검법

Authors

서론

EKS에서 배포는 성공했는데 Pod가 계속 Pending에 머물고, kubectl describe pod0/… nodes are available: … Insufficient memory가 찍히는 상황은 흔하지만 원인 스펙트럼이 넓습니다. 단순히 “노드 메모리가 부족하다”로 끝나지 않고, 리소스 요청(request) 설정, 스케줄링 제약(affinity/taint), 오토스케일러(Karpenter/Cluster Autoscaler) 미동작, 노드의 allocatable 감소(daemonset/예약/시스템 오버헤드) 같은 요인이 겹치기 때문입니다.

이 글은 “지금 당장 뜨는 Pending을 풀기”와 “다시 안 터지게 구조를 바꾸기”를 목표로, EKS에서 Insufficient memory를 만났을 때의 체크리스트를 순서대로 제공합니다.

1) 증상 확정: 정말 메모리 때문에 스케줄링이 막혔나?

가장 먼저 이벤트를 정확히 읽어야 합니다. Pending은 이미지 풀 실패, 볼륨 attach, 노드 셀렉터 미스매치 등으로도 발생합니다.

Pod 이벤트 확인

kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>

Events에서 아래와 같은 메시지가 핵심입니다.

  • 0/6 nodes are available: 6 Insufficient memory.
  • 또는 0/6 nodes are available: 2 Insufficient memory, 4 node(s) had taint ...

두 번째 케이스처럼 Insufficient memory 외 다른 제약이 같이 있을 수 있습니다. 이 경우 메모리를 늘려도 계속 Pending일 수 있으니, 이벤트에 나열된 모든 조건을 함께 해결해야 합니다.

스케줄러 관점에서 더 자세히 보기(선택)

클러스터에 따라 스케줄러 로그를 보긴 어렵지만, kubectl get events로도 단서를 얻습니다.

kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -n 30

2) 가장 흔한 원인: requests가 너무 크거나 limit만 믿고 있다

스케줄링은 기본적으로 requests 기준으로 일어납니다. 즉, 컨테이너가 실제로 메모리를 적게 쓰더라도 requests.memory가 크면 그만큼 노드에 “자리”가 있어야 스케줄됩니다.

리소스 설정 확인

kubectl get pod -n <ns> <pod> -o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}'

배포 YAML도 같이 확인합니다.

kubectl get deploy -n <ns> <deploy> -o yaml | sed -n '1,200p'

Anti-pattern: limit만 있고 request가 없는 경우

request가 없으면 기본적으로 0에 가까운 값으로 취급되어 스케줄은 잘 되지만, 런타임에서 메모리 경쟁이 심해져 OOMKill이 빈번해질 수 있습니다. 반대로 “안정성”을 위해 request를 크게 잡아놨더니 스케줄이 막히는 경우도 많습니다.

권장 접근은:

  • request는 평상시 사용량(P50~P90) + 여유분
  • limit은 스파이크 상한(P99) 또는 OOM 방지선

예시: request를 합리적으로 낮추고 HPA로 보완

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/api:2026-02-23
          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "1000m"
              memory: "1024Mi"

여기서 핵심은 “스케줄링 가능한 수준의 request”로 맞추고, 트래픽 증가는 HPA/오토스케일로 대응하는 것입니다.

3) 노드에 메모리가 있어 보이는데도 Pending? allocatable을 봐야 한다

EC2 인스턴스 메모리 = 쿠버네티스가 Pod에 줄 수 있는 메모리가 아닙니다.

  • kubelet/system reserved
  • DaemonSet(예: CNI, kube-proxy, fluent-bit)
  • eviction threshold

등으로 인해 Allocatable이 줄어듭니다.

노드 allocatable 확인

kubectl describe node <node-name> | sed -n '/Allocatable:/,/System Info:/p'

또는 전체 노드를 요약합니다.

kubectl get nodes -o custom-columns=NAME:.metadata.name,MEM:.status.allocatable.memory,CPU:.status.allocatable.cpu

DaemonSet이 메모리를 많이 먹는지 확인

특히 로깅/모니터링 DaemonSet이 request를 크게 잡으면, 작은 인스턴스 타입에서는 스케줄링이 급격히 어려워집니다.

kubectl get ds -A
kubectl describe ds -n kube-system <daemonset>

fluent-bit 계열 이슈로 노드 리소스가 잠식되는 케이스도 많습니다. 로그 파이프라인이 의심된다면 EKS에서 fluent-bit 로그 누락·지연 원인 9가지도 함께 점검해보세요(리소스/버퍼/백프레셔가 메모리 사용량에 영향을 줍니다).

4) “Insufficient memory”가 떠도 실제 원인은 스케줄링 제약일 수 있다

이벤트에 Insufficient memory만 보이는 듯해도, 실제론 아래 설정이 노드 후보를 줄여 “남은 후보가 메모리 부족”이 되는 경우가 있습니다.

  • nodeSelector / nodeAffinity
  • podAffinity / podAntiAffinity
  • topologySpreadConstraints
  • tolerations 부족(taint 미허용)

스펙에서 제약 확인

kubectl get pod -n <ns> <pod> -o yaml | sed -n '1,220p'

특히 nodeSelector로 특정 노드그룹만 타겟팅 중인데 그 노드그룹이 작으면, 다른 노드가 놀고 있어도 Pending이 납니다.

taint/toleration 빠짐 확인

kubectl describe node <node> | grep -i taints -A2
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.tolerations}'

5) 오토스케일러가 노드를 늘려야 하는데 안 늘어난다

Pending이 “정상”인 경우도 있습니다. 즉, 오토스케일러가 새 노드를 띄우는 동안 잠깐 Pending일 수 있습니다. 문제는 아예 노드가 늘지 않는 상황입니다.

Karpenter/Cluster Autoscaler 동작 여부

  • Karpenter 사용 시: Provisioner/NodePool 제약, 인스턴스 타입 필터, 서브넷/SG 태그, 스팟 용량 등
  • Cluster Autoscaler 사용 시: ASG 태그, max size, expander, scale-down 설정 등

Karpenter 도입 환경에서 “왜 Pending이 계속인데 노드가 안 늘지?”가 자주 발생합니다. 해당 패턴은 Karpenter 도입 후 EKS 노드가 안 늘 때 해결법에 체크리스트 형태로 잘 정리돼 있으니 같이 보는 것을 권합니다.

빠른 확인 명령

kubectl get pods -A | grep -E 'karpenter|cluster-autoscaler'
kubectl logs -n karpenter deploy/karpenter -f --tail=200
# 또는
kubectl logs -n kube-system deploy/cluster-autoscaler -f --tail=200

로그에서 다음을 찾습니다.

  • “no instance type satisfied resources”
  • “all available instance types exceed limits”
  • “insufficient capacity” (특히 spot)
  • “subnet/sg discovery failed”

6) 즉시 해결(응급처치) 옵션 6가지

운영 중 긴급 복구가 필요할 때 선택할 수 있는 옵션입니다. 단, 응급처치만 하고 끝내면 재발합니다.

1) request를 낮춰 재배포

가장 빠르고 비용 증가가 적습니다. 단, 너무 낮추면 OOMKill로 바뀔 수 있습니다.

kubectl -n <ns> patch deploy <deploy> --type='json' \
  -p='[
    {"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"512Mi"}
  ]'

2) replicas를 줄여 빈자리 확보

kubectl -n <ns> scale deploy <deploy> --replicas=1

3) PDB(DisruptionBudget) 때문에 축출/재스케줄이 막히는지 확인

노드 교체/스케일 과정에서 PDB가 강하면 전체가 교착되는 경우가 있습니다.

kubectl get pdb -A
kubectl describe pdb -n <ns> <pdb>

4) 노드그룹 인스턴스 타입을 메모리 큰 것으로 변경

예: m5.large(8GiB)m5.xlarge(16GiB) 또는 메모리 최적화 r* 계열.

5) 새 노드 추가(수동 스케일)

관리형 노드그룹이면 Desired size를 올려 즉시 완화할 수 있습니다.

6) 우선순위(PriorityClass)로 중요한 Pod부터 스케줄

덜 중요한 워크로드가 자리를 차지하고 있다면, Priority/Preemption을 설계합니다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: critical-services
value: 100000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "Critical services should be scheduled first"

Deployment에 적용:

spec:
  template:
    spec:
      priorityClassName: critical-services

7) 재발 방지: “메모리 요청”을 데이터 기반으로 운영하기

Insufficient memory는 대개 “설정이 실제 사용량을 반영하지 못함”에서 시작합니다.

1) VPA(Vertical Pod Autoscaler)로 request 추천 받기

VPA를 자동 적용까지 열어두기 부담스럽다면, recommendation 모드로 시작해도 효과가 큽니다.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  updatePolicy:
    updateMode: "Off"  # 추천만

2) HPA + 적절한 메모리/CPU 지표 설계

메모리 기반 HPA는 신중해야 합니다(가비지 컬렉션, 캐시 특성). CPU/요청률(RPS), 큐 길이 같은 지표가 더 안정적인 경우가 많습니다.

3) 노드 사이징과 DaemonSet 예산을 함께 설계

예를 들어 노드당 DaemonSet이 700Mi를 고정으로 가져가면, 2GiB 노드는 사실상 쓸 수 있는 공간이 매우 제한적입니다. 노드 타입을 정할 때:

  • 시스템/DaemonSet 오버헤드
  • 최대 Pod 수(ENI/IP 제한 포함)
  • 워크로드의 평균 request 합

을 같이 계산해야 합니다.

8) 자주 헷갈리는 포인트(Q&A)

Q1. 노드 메모리 사용률은 낮은데 왜 Insufficient memory?

쿠버네티스 스케줄러는 “현재 사용률”이 아니라 requests 합계가 allocatable을 넘는지로 판단합니다. 실제 사용률이 낮아도 request가 크면 스케줄 불가가 정상입니다.

Q2. limit을 올리면 해결되나?

아닙니다. 스케줄링은 request 기준입니다. limit은 런타임 상한이고, Pending(Insufficient memory)에는 보통 직접 도움이 되지 않습니다.

Q3. 메모리 부족이면 노드에서 OOM이 나야 하는데 왜 Pending에서 멈추나?

Pending은 “아예 배치가 안 됨”입니다. OOMKill은 “배치된 뒤 실행 중 메모리 초과”입니다. 둘은 단계가 다릅니다.

9) 마무리 체크리스트(10분 트러블슈팅 루틴)

  1. kubectl describe pod 이벤트에서 Insufficient memory 외 조건 동반 여부 확인
  2. requests.memory가 과도한지(특히 sidecar 포함) 확인
  3. kubectl describe node로 Allocatable 확인(인스턴스 메모리와 혼동 금지)
  4. DaemonSet request/limit이 작은 노드에서 과도한지 확인
  5. affinity/taint/toleration로 후보 노드가 줄어드는지 확인
  6. 오토스케일러 로그에서 “왜 노드가 안 늘지”를 확인(Karpenter/CA)
  7. 응급처치(요청 낮춤/replica 감소/노드 추가) 후, VPA 추천 기반으로 재조정

같은 EKS 장애라도 원인이 네트워크/노드 설정 쪽인 경우도 많습니다. 특히 노드 레벨 디버깅이 필요하다면 EKS Bottlerocket 노드 SSH 없이 SSM으로 접속·디버깅처럼 “노드에 어떻게 들어가서 무엇을 확인할지”까지 준비해두면, Pending 이후 연쇄 장애(이미지 GC, kubelet 압박 등) 대응 속도가 크게 올라갑니다.