Published on

EKS 노드 디스크 부족 Evicted 폭주 해결 가이드

Authors

서버가 멀쩡해 보이는데 갑자기 Pod가 우수수 Evicted로 떨어지고, 재스케줄링이 반복되며 클러스터가 불안정해지는 경우가 있습니다. EKS에서는 특히 노드 디스크 부족(정확히는 kubelet이 감지하는 DiskPressure 또는 inode 부족) 상황에서 이런 “Evicted 폭주”가 자주 발생합니다. 문제는 단순히 노드 볼륨을 키우는 것만으로는 재발을 막기 어렵다는 점입니다. 컨테이너 이미지, 로그, emptyDir, 오버레이 파일시스템, kubelet eviction 설정, 그리고 앱의 ephemeral 사용량이 얽혀 있기 때문입니다.

이 글에서는 (1) 지금 당장 멈추는 응급처치, (2) 원인 정확히 찾는 진단 루틴, (3) 재발 방지 아키텍처/설정 개선을 EKS 기준으로 정리합니다.

Evicted 폭주의 메커니즘: 왜 한 번 터지면 계속 터지나

Kubernetes에서 Evicted는 스케줄러가 아니라 kubelet이 노드 리소스 압박을 감지했을 때 Pod를 강제 종료하는 이벤트입니다. 디스크 관련 압박은 크게 3가지로 나뉩니다.

  • nodefs 부족: 노드의 루트 파일시스템(/) 공간 부족
  • imagefs 부족: 컨테이너 이미지 저장소(보통 /var/lib/containerd 또는 /var/lib/docker) 공간 부족
  • inode 부족: 파일 개수 한도(inode) 소진

한 번 DiskPressure=True가 되면 kubelet은 우선순위가 낮거나 요청(request)이 낮은 Pod부터 내보내고, 그 Pod는 다시 스케줄되며 또 디스크를 사용합니다. 특히 아래 조건이 겹치면 “폭주”가 됩니다.

  • 이미지 pull/untar로 디스크가 더 증가
  • 로그가 계속 쌓임(특히 JSON 로그 폭증)
  • emptyDir/캐시/임시파일이 정리되지 않음
  • ephemeral-storage request/limit 미설정으로 BestEffort/불명확한 QoS

1단계: 10분 내 응급처치(폭주 멈추기)

1) 어떤 노드가 터졌는지 즉시 확인

kubectl get nodes
kubectl describe node <node-name> | sed -n '/Conditions:/,/Addresses:/p'

# 이벤트에서 DiskPressure/eviction 확인
kubectl describe node <node-name> | sed -n '/Events:/,$p'

ConditionsDiskPressure True가 보이면 거의 확정입니다.

2) Evicted Pod를 빠르게 집계

kubectl get pod -A --field-selector=status.phase=Failed \
  -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,REASON:.status.reason,NODE:.spec.nodeName' \
  | grep Evicted

# 최근 이벤트에서 eviction만 보기
kubectl get events -A --sort-by=.lastTimestamp | grep -i evict | tail -n 50

3) “새로운 디스크 소비”를 잠깐 멈추기

  • 대량 배포/롤링업데이트 중이면 배포를 잠깐 중지하거나 replica를 줄입니다.
  • 로그 폭증이 의심되면 로그 레벨을 임시로 낮추거나, 특정 컴포넌트(예: noisy sidecar)를 잠깐 내립니다.
# 예: 특정 디플로이먼트 임시 축소
kubectl -n <ns> scale deploy/<name> --replicas=0

4) 노드 디스크를 즉시 확보(가장 효과적인 2가지)

EKS(Managed Node Group)에서는 노드에 직접 접속해 정리하는 방법이 가장 빠를 때가 많습니다.

  • 컨테이너 이미지/레이어 정리: containerd 환경에서 이미지가 과도하게 쌓이는 경우
  • 로그 파일 정리: /var/log/containers, /var/log/pods, journald

노드에 SSM 또는 SSH로 접속한 뒤(조직 정책에 맞게), containerd 기준 예시는 다음과 같습니다.

# 이미지/컨테이너 상태 확인
sudo crictl images | head
sudo crictl ps -a | head

# 사용하지 않는 이미지 정리(주의: 운영 중인 이미지 지우지 않도록 필터링 필요)
# 안전한 방법: 오래된/태그 없는 이미지 위주로 선별 삭제
sudo crictl rmi --prune

# 디스크 사용량 확인
df -h
sudo du -sh /var/lib/containerd/* 2>/dev/null | sort -h | tail

로그가 원인인 경우:

sudo du -sh /var/log/* 2>/dev/null | sort -h | tail
sudo du -sh /var/log/containers/* 2>/dev/null | sort -h | tail

# 예: 너무 큰 로그 파일을 truncate(삭제보다 안전)
sudo find /var/log/containers -type f -name "*.log" -size +1G -print -exec sudo truncate -s 0 {} \;

> 주의: 노드에서 수동 정리는 “지금 당장”의 안정화에 좋지만, 재발 방지책이 없으면 다시 터집니다.

2단계: 원인 진단(무엇이 디스크를 먹었나)

1) kubelet이 어떤 임계치로 eviction 했는지 확인

Eviction 메시지는 보통 Pod 이벤트에 남습니다.

kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'

여기서 The node was low on resource: ephemeral-storage 또는 nodefs/imagefs 관련 문구가 나옵니다.

2) inode 부족 여부 확인(공간은 남았는데 Evicted?)

디스크 용량이 남아도 inode가 고갈되면 동일하게 DiskPressure가 발생합니다.

df -i

특히 작은 파일을 대량 생성하는 워크로드(캐시, 임시 chunk, 압축 해제 등)에서 inode가 먼저 터질 수 있습니다.

3) 어떤 Pod가 ephemeral을 많이 쓰는지(가시화)

Kubernetes는 CPU/메모리만큼 디스크 사용량을 쉽게 보여주지 않습니다. 하지만 다음 단서로 추적합니다.

  • 문제 노드에 스케줄된 Pod 목록
  • 해당 Pod가 사용하는 emptyDir/로그/다운로드 경로
  • 앱이 /tmp, /var/tmp, 작업 디렉터리에 대용량 파일 생성 여부
# 문제 노드에 올라간 Pod 목록
kubectl get pod -A -o wide --field-selector spec.nodeName=<node-name>

# emptyDir 사용 여부 확인(매니페스트)
kubectl -n <ns> get pod <pod> -o yaml | grep -n "emptyDir" -n

4) CrashLoop/재시작이 디스크를 키우는 경우

CrashLoopBackOff가 반복되면 로그가 폭증하거나 코어덤프/임시파일이 누적되어 디스크가 급격히 찰 수 있습니다. CrashLoop 디버깅은 아래 글의 체크리스트도 같이 보면 원인 분리가 빨라집니다.

3단계: 재발 방지(구조적으로 Evicted를 막는 방법)

이제부터가 핵심입니다. “노드 디스크를 키웠는데도 또 Evicted”가 나오는 팀은 아래 항목 중 2~3개가 동시에 빠져 있는 경우가 많습니다.

3.1 Pod에 ephemeral-storage request/limit을 반드시 설정

디스크도 CPU/메모리처럼 request/limit이 있습니다. 설정하지 않으면 스케줄링/우선순위에서 불리하고, eviction 대상이 되기 쉽습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: api
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/api:1.2.3
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
              ephemeral-storage: "1Gi"
            limits:
              cpu: "1"
              memory: "1Gi"
              ephemeral-storage: "2Gi"
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 2Gi
  • ephemeral-storage노드 로컬 디스크 사용량(로그/emptyDir/쓰기 레이어 포함)에 영향을 줍니다.
  • emptyDir.sizeLimit을 함께 걸면 “한 Pod가 노드를 다 먹는” 상황을 줄일 수 있습니다.

3.2 로그를 노드에 쌓지 말고, 회전/수집을 강제

EKS에서 Evicted 폭주의 단골 원인은 로그 폭증입니다.

(1) 애플리케이션 로그 레벨/형식 점검

  • JSON 로그가 과도한 필드를 포함하거나, 요청/응답 바디를 매번 찍는 경우
  • 에러 루프에서 동일 로그를 초당 수백 건 발생

(2) 컨테이너 런타임 로그 로테이션 확인

EKS AMI/런타임 조합에 따라 다르지만, “로테이션이 느슨”하면 /var/log/containers가 비대해집니다.

운영 표준:

  • 로그 수집(Fluent Bit/Vector/OTel Collector) + 짧은 로테이션
  • 애플리케이션은 stdout/stderr 중심

3.3 이미지 최적화: pull/untar가 디스크를 터뜨린다

노드 디스크가 빡빡한데 배포가 잦으면 이미지 레이어가 쌓이고, pull 과정에서 일시적으로 더 큰 공간이 필요합니다.

  • 멀티스테이지 빌드로 이미지 슬림화
  • 불필요한 빌드 산출물 제거
  • 태그 전략 정리(과도한 태그로 캐시가 무한 증가하지 않도록)

간단한 Dockerfile 예:

# build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# runtime stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
CMD ["node", "dist/server.js"]

3.4 emptyDir/캐시/임시파일을 PV로 분리하거나 외부 스토리지로 이동

대용량 임시 데이터(예: 파일 변환, 압축 해제, 모델 캐시, 대규모 다운로드)를 emptyDir에 두면 노드 디스크를 직접 압박합니다.

선택지:

  • EBS/EFS 같은 PV로 분리(워크로드 특성에 따라)
  • 캐시를 S3/Redis 등 외부로 이동
  • /tmp 사용량을 제한하고, 작업 완료 후 정리

3.5 노드 볼륨(EBS) 사이징과 파일시스템 설계

Managed Node Group의 루트 EBS가 너무 작으면 기본 오버헤드(이미지+로그+오버레이)만으로도 위험해집니다.

  • 루트 볼륨을 현실적인 크기(예: 50~200GiB 이상)로
  • 워크로드가 이미지가 크거나 배포가 잦으면 더 크게
  • inode 여유가 많은 파일시스템 선택/옵션 고려

3.6 kubelet eviction 설정(가능한 범위에서) 이해하기

EKS Managed Node Group에서 kubelet 설정은 Launch Template/user-data로 조정할 수 있지만, 운영 표준과 충돌할 수 있어 신중해야 합니다.

핵심은:

  • evictionHard, evictionSoft, evictionMinimumReclaim이 어떤 기준으로 Pod를 내보내는지
  • imageGCHighThresholdPercent, imageGCLowThresholdPercent로 이미지 GC가 언제 도는지

설정을 바꾸기 전에는 **원인(로그/이미지/emptyDir/ephemeral 미설정)**을 먼저 제거하는 것이 보통 더 효과적입니다.

3.7 오토스케일링과 결합: “폭주”를 “완만한 저하”로

디스크는 HPA의 기본 지표가 아니므로, 노드가 꽉 차면 갑자기 Evicted가 터집니다. 따라서 다음을 함께 고려합니다.

  • Cluster Autoscaler/Karpenter로 노드 증설 여지 확보
  • 디스크 사용량을 CloudWatch/Prometheus로 모니터링하고 알람
  • 배포 시 surge 값을 보수적으로(동시에 pull하는 수를 줄임)

HPA/스케일링 설계가 꼬이면 문제 상황에서 복구가 더 느려질 수 있습니다. 특히 종료가 지연되면 노드 정리가 안 되어 압박이 지속됩니다. 관련해서는 아래 글도 참고할 만합니다.

4단계: 운영 체크리스트(재발 방지용)

아래 항목을 “예/아니오”로 점검하면 재발 가능성을 빠르게 낮출 수 있습니다.

  • 모든 주요 Deployment/Job에 ephemeral-storage request/limit이 설정되어 있다
  • emptyDir.sizeLimit 또는 임시 데이터가 PV/외부 스토리지로 분리되어 있다
  • 로그 수집 파이프라인이 있고, 노드 로그 로테이션이 강제된다
  • 이미지 크기가 통제되고(슬림화), 태그/캐시 정책이 정리되어 있다
  • 노드 루트 EBS 용량이 워크로드 특성에 맞게 충분하다
  • inode 모니터링/알람이 있다(df -i 기반)
  • 배포 surge/동시 pull이 노드 디스크를 터뜨리지 않게 제한되어 있다

부록: 자주 쓰는 명령 모음

# 노드 상태/압박 확인
kubectl get node -o wide
kubectl describe node <node> | grep -A3 -B2 -E "DiskPressure|MemoryPressure|PIDPressure"

# Evicted 빠르게 보기
kubectl get pod -A | grep Evicted

# 특정 노드의 Pod 나열
kubectl get pod -A -o wide --field-selector spec.nodeName=<node>

# 이벤트 타임라인
kubectl get events -A --sort-by=.lastTimestamp | tail -n 100

마무리

EKS에서 “노드 디스크 부족으로 Evicted 폭주”는 단순 장애가 아니라 클러스터 운영 설계의 빈틈이 드러나는 순간입니다. 응급처치로는 이미지/로그/임시파일 정리가 가장 빠르지만, 장기적으로는 ephemeral-storage request/limit, 로그 로테이션/수집, 이미지 슬림화, emptyDir 제한/분리, 노드 볼륨 사이징을 함께 갖춰야 재발을 막을 수 있습니다.

추가로, Evicted와 함께 CrashLoopBackOff가 동반되거나(로그 폭증), 스케일링이 꼬여 복구가 느리다면 위에서 소개한 내부 링크의 디버깅 체크리스트도 같이 적용해 보세요.