- Published on
EKS에서 nodefs ImageGC로 Pod가 Evicted될 때
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데 어느 날부터 Pod가 간헐적으로 Evicted되고, 이벤트에는 nodefs/imagefs/ImageGC 같은 단어가 찍히는 경우가 있습니다. 특히 EKS에서는 노드가 오토스케일링으로 자주 교체되고, 이미지 풀(pull)·로그 적재가 반복되면서 디스크 압박이 누적되기 쉬워 이 문제가 자주 발생합니다.
이 글에서는 nodefs 디스크 압박 → kubelet의 이미지 GC(Image Garbage Collection) → 그래도 부족하면 Eviction이라는 흐름을 정확히 이해하고, EKS에서 실제로 어떻게 진단하고 어떤 옵션으로 해결하는지까지 한 번에 정리합니다.
1) 증상: Pod Evicted + nodefs/ImageGC 이벤트
대표적으로 아래와 같은 형태로 나타납니다.
- Pod 상태가
Evicted로 종료 kubectl describe pod이벤트에The node was low on resource: ephemeral-storage또는nodefs관련 메시지- 노드 이벤트에
ImageGC가 수행되었다는 로그
먼저 Pod 이벤트를 확인합니다.
kubectl describe pod -n <ns> <pod>
예시(환경에 따라 문구는 조금씩 다릅니다):
Warning Evicted kubelet The node was low on resource: ephemeral-storage.
Warning FreeDiskSpaceFailed kubelet (nodefs) failed to free disk space
Normal ImageGC kubelet Image garbage collection succeeded
여기서 핵심은 **Eviction의 직접 원인이 CPU/메모리가 아니라 “ephemeral-storage(노드 로컬 디스크)”**라는 점입니다.
2) nodefs vs imagefs: 무엇이 부족한가?
쿠버네티스 노드의 로컬 스토리지는 크게 두 영역으로 이해하면 쉽습니다.
- nodefs: 컨테이너 런타임/쿠버릿이 사용하는 노드 파일시스템(기본 루트 볼륨).
/var/lib/kubelet,/var/log, 컨테이너 writable layer 등이 들어갑니다. - imagefs: 이미지 레이어 저장소(컨테이너 런타임이 이미지 저장에 쓰는 파일시스템). 환경에 따라 nodefs와 분리되어 있을 수도, 같은 볼륨일 수도 있습니다.
EKS의 기본 설정(특히 관리형 노드그룹 기본 AMI)에서는 종종 이미지 저장도 루트 EBS에 붙는 구조가 많아 nodefs 압박이 곧 이미지/로그/컨테이너 레이어 압박으로 이어집니다.
3) kubelet의 ImageGC와 Eviction 흐름
노드 디스크가 부족해지면 kubelet은 대략 다음 순서로 대응합니다.
- ImageGC 실행: 사용하지 않는(참조되지 않는) 이미지부터 삭제
- 그래도 부족하면 Eviction: 특정 Pod를 축출(Evicted)하여 디스크를 확보
즉, ImageGC가 보인다는 것은 “kubelet이 디스크를 확보하려고 노력 중”이라는 뜻이고, 그 뒤에 Evicted가 나오면 “GC로도 충분히 확보하지 못했다”는 의미일 가능성이 큽니다.
Eviction이 누구를 먼저 죽이나?
Eviction은 우선순위/요청량/품질(QoS)에 따라 영향을 받습니다.
BestEffortPod가 먼저 희생되는 경향requests/limits가 없는 워크로드는 디스크/메모리 압박 시 더 취약PriorityClass가 낮은 Pod가 먼저 밀릴 수 있음
하지만 디스크(ephemeral-storage) 압박은 로그 폭증, 이미지 과다, 큰 emptyDir, 큰 writable layer 등 다양한 요인이 섞여 있어 “특정 Pod만” 원인인 경우도 있지만 “노드 전체의 운영 패턴”이 원인인 경우가 더 많습니다.
4) 빠른 진단 체크리스트(현장에서 바로 쓰는 순서)
4.1 노드 상태/이벤트에서 디스크 압박 확인
kubectl describe node <node-name>
아래 항목을 봅니다.
Conditions에DiskPressure=True가 있었는지Events에ImageGC/FreeDiskSpaceFailed/eviction manager관련 메시지
4.2 노드에서 실제 디스크 사용량 확인
노드에 접속할 수 있다면(SSM/SSH) 가장 빠릅니다.
df -h
sudo du -xh /var/lib/containerd | sort -h | tail -n 20
sudo du -xh /var/log | sort -h | tail -n 20
sudo du -xh /var/lib/kubelet | sort -h | tail -n 20
/var/lib/containerd(containerd 기준) 또는/var/lib/docker(dockerd 사용 시)/var/log/containers,/var/log/pods/var/lib/kubelet/pods(emptyDir, 볼륨 마운트 등)
4.3 어떤 Pod가 ephemeral-storage를 많이 쓰는지 확인
쿠버네티스는 CPU/메모리처럼 ephemeral-storage도 request/limit 및 사용량을 볼 수 있습니다(메트릭 서버/프로메테우스 구성에 따라 다름).
kubectl top pod -A --containers
다만 kubectl top은 기본적으로 CPU/메모리 중심이라, ephemeral-storage는 Prometheus/CloudWatch Container Insights에서 확인하는 편이 더 확실합니다.
로그가 원인이라면 CloudWatch 비용도 같이 튀는 경우가 많습니다. 로그 폭증 패턴이 보이면 아래 글의 절감/원인 파트가 진단에 도움이 됩니다.
5) 원인별 대표 시나리오 6가지
5.1 이미지가 너무 크거나 태그가 계속 바뀌어 캐시가 쌓임
:latest또는 커밋 SHA 태그로 매 배포마다 새 이미지- 노드가 여러 워크로드를 돌며 이미지가 계속 쌓임
- 사용하지 않는 이미지가 GC 대상이 되지만, GC 타이밍/임계치 때문에 디스크 압박이 먼저 올 수 있음
대응:
- 이미지 사이즈 줄이기(멀티스테이지 빌드, 불필요 패키지 제거)
- 태그 전략 정리, 불필요한 이미지 pull 빈도 줄이기
5.2 애플리케이션 로그 폭증 → /var/log 가득 참
- stdout/stderr로 과도한 로그
- JSON 로그가 너무 큼
- 에러 루프(재시도)로 동일 로그가 폭발
대응:
- 로그 레벨/샘플링/레이트 리밋
- Fluent Bit 설정에서 버퍼/파일 스풀링 확인
- 노드 로컬에 로그가 쌓이지 않게 로테이션/수집 파이프라인 점검
5.3 emptyDir 사용량 증가(캐시/임시파일)
emptyDir는 기본적으로 노드 디스크를 사용- 대용량 캐시, 모델 파일 다운로드, 압축 해제 등이 누적
대응:
emptyDir.sizeLimit설정- 가능하면 EBS/EFS 같은 영속 볼륨로 이동
5.4 writable layer에 큰 파일이 계속 생성됨
- 컨테이너 내부 경로에 파일을 계속 쌓는 패턴
- 예:
/tmp,/app/cache등에 대용량 파일
대응:
- 해당 경로를
emptyDir로 분리하거나 PV로 이동 - 주기적 정리(하지만 컨테이너 내 cron은 운영상 비추)
5.5 디스크가 작은 인스턴스/루트 볼륨
- 노드 루트 EBS가 20GiB 등으로 작게 잡힘
- 이미지/로그 조금만 늘어도 임계치 도달
대응:
- 노드그룹 Launch Template로 루트 볼륨 크기 상향
5.6 노드 교체/스케일링 패턴 때문에 이미지 pull이 급증
- HPA/Cluster Autoscaler로 노드가 자주 생겼다 사라짐
- 새 노드는 캐시가 없어 이미지 pull이 집중
대응:
- 이미지 프리풀(daemonset), 워밍 전략
- 노드 수/스케일 정책 완화
6) 해결 전략: “임계치 조정”보다 “원인 제거/용량 설계”가 우선
6.1 (권장) 루트 볼륨 크기 늘리기: 가장 즉효
EKS Managed Node Group를 Launch Template로 구성했다면 루트 EBS 크기를 늘릴 수 있습니다.
- 예: 20GiB → 80GiB/100GiB
- 워크로드가 이미지가 크거나 로그가 많은 편이면 사실상 필수
Terraform을 쓴다면 노드가 NotReady나 초기화 이슈가 같이 보일 때도 있는데, 노드 구성 점검은 아래 글도 참고할 만합니다.
6.2 ephemeral-storage requests/limits를 명시해 “누가 디스크를 쓰는지” 통제
CPU/메모리처럼 ephemeral-storage도 request/limit을 걸 수 있습니다. 최소한 문제를 자주 일으키는 워크로드부터 적용하면 “노드 전체가 같이 죽는” 상황을 줄일 수 있습니다.
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:1.2.3
resources:
requests:
cpu: "250m"
memory: "512Mi"
ephemeral-storage: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
ephemeral-storage: "2Gi"
이렇게 하면 특정 컨테이너가 로컬 디스크를 무한정 쓰는 것을 방지하고, 스케줄링 단계에서부터 노드 용량과 매칭되도록 유도할 수 있습니다.
6.3 emptyDir에 sizeLimit 적용(캐시/임시파일 폭주 방지)
volumes:
- name: tmp
emptyDir:
sizeLimit: "1Gi"
containers:
- name: worker
volumeMounts:
- name: tmp
mountPath: /tmp
6.4 이미지 GC/eviction 임계치 조정(신중히)
kubelet에는 이미지 GC 임계치(high/low)와 eviction 임계치가 있습니다. 다만 EKS에서는 노드 부트스트랩/AMI에 의해 설정이 관리되며, 무턱대고 임계치만 바꾸면 Eviction이 늦게 발생해 노드가 더 불안정해지거나 kubelet이 정상 동작을 못 하는 상황이 생길 수 있습니다.
그럼에도 불구하고 “정책적으로” 조정이 필요하다면, 보통은 다음 원칙을 권합니다.
- 임계치 조정보다 루트 볼륨 증설 + 로그/이미지 최적화를 먼저
- 조정은 Launch Template user-data 또는 EKS 권장 방식으로 kubelet config를 관리
- 변경 후에는 반드시 노드 롤링으로 일관성 확보
아래는 kubelet config(JSON)에서 자주 보는 키 예시입니다(환경에 따라 적용 방식이 다릅니다).
{
"imageGCHighThresholdPercent": 85,
"imageGCLowThresholdPercent": 80,
"evictionHard": {
"nodefs.available": "10%",
"imagefs.available": "10%",
"nodefs.inodesFree": "5%",
"imagefs.inodesFree": "5%"
}
}
핵심은 “Eviction을 늦추는 것”이 아니라, Eviction이 발생하지 않도록 디스크를 설계하고 소비를 통제하는 것입니다.
6.5 로그 파이프라인/수집 에이전트(Fluent Bit 등) 디스크 스풀 설정 점검
Fluent Bit/Fluentd가 파일 스풀을 쓰거나, 재전송 실패로 로컬 버퍼가 커지면 nodefs를 빠르게 잡아먹습니다.
- 출력(destination) 장애 시 로컬 버퍼가 폭증하는지
- 재시도 정책이 과도한지
- 멀티라인/파서 오류로 이벤트가 증폭되는지
CloudWatch로 보내는 구조라면 비용과 장애가 함께 나타나는 경우가 많으니, 위에서 소개한 CloudWatch 비용 글의 “폭증 패턴”을 같이 보는 것을 추천합니다.
7) 재현/검증: 안전한 방식으로 Eviction을 테스트하기
운영에서 바로 터뜨리기보다는, 별도 노드그룹/테스트 네임스페이스에서 “ephemeral-storage를 많이 쓰는 Pod”를 띄워 이벤트가 어떻게 찍히는지 확인하면 원인 파악이 빨라집니다.
아래는 의도적으로 파일을 계속 생성해 ephemeral-storage를 소비하는 간단한 예시입니다.
apiVersion: v1
kind: Pod
metadata:
name: fill-ephemeral
spec:
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c"]
args:
- |
i=0
while true; do
dd if=/dev/zero of=/tmp/file-$i bs=10M count=10 2>/dev/null
i=$((i+1))
sleep 1
done
resources:
requests:
ephemeral-storage: "200Mi"
limits:
ephemeral-storage: "500Mi"
이 Pod가 제한에 걸려 종료되는지, 혹은 노드 전체 DiskPressure로 번지는지 관찰하면 현재 클러스터의 정책/임계치가 어떤 방식으로 작동하는지 감이 잡힙니다.
8) 운영 관점의 권장 조합(현실적인 정답)
EKS에서 nodefs ImageGC로 Pod가 Evicted되는 문제는 보통 한 가지 조치로 끝나지 않습니다. 운영에서 안정적으로 가려면 아래 조합이 가장 재현성이 좋습니다.
- 루트 EBS 증설(가장 즉효)
- ephemeral-storage requests/limits 적용(문제 Pod 격리)
- 로그 폭증 차단 + 수집기 버퍼 점검(nodefs 급증 방지)
- 이미지 최적화 + pull 패턴 완화(ImageGC 압박 감소)
- 필요 시에만 kubelet 임계치 조정(마지막 수단)
9) 마무리: Eviction은 “증상”, nodefs 설계가 “원인”
Evicted는 쿠버네티스가 스스로를 살리기 위해 내리는 결정입니다. 따라서 Eviction 메시지를 없애려면 임계치만 만지기보다, nodefs를 누가/왜/얼마나 쓰는지를 먼저 분해해야 합니다.
- 이미지가 큰가?
- 로그가 터졌는가?
- emptyDir/writable layer가 비정상적으로 커지는가?
- 노드 루트 볼륨이 워크로드에 비해 작지 않은가?
이 네 가지를 정리하면 대부분의 nodefs ImageGC → Evicted 케이스는 재발 없이 안정화됩니다.