EKS 노드 Ready/NotReady Flapping - Disk I/O 병목
EKS 클러스터에서 노드가 Ready/NotReady를 반복하며 6회 상태가 바뀌었다. 노드 CPU 92%를 보고 CPU 과부하를 의심했지만, 실제로는 새벽 배치 CronJob이 유발한 Disk I/O 병목이었다.
환경: EKS, Terraform, gp3 EBS 날짜: 2026-01-26
상황
새벽 2시에 테스트 EKS 클러스터에서 Node Ready Flapping Alert이 발생했다. 특정 노드가 Ready/NotReady를 반복하며 약 23분간 6회 상태가 바뀌었고, 메트릭 수집까지 끊겼다. Alert 확인 직후 눈에 들어온 건 노드 CPU 사용량 92%였다. CPU 과부하로 판단하고 조사를 시작했다.
가설과 검증 과정
가설 1: CPU 병목 — “노드 CPU 92%니까 CPU 과부하 아닌가?”
CPU 92%라는 숫자를 보면 자연스럽게 CPU 병목을 의심하게 된다. 실제로 이 노드에서 동작하는 worker-app의 CPU 사용량을 확인했다.
worker-app 컨테이너 CPU 사용량은 최대 0.38 cores, 노드(2 cores)의 19%에 불과했다. 노드 전체는 92%인데 주요 워크로드가 19%만 쓴다면, 나머지 73%가 어디에 쓰이는지가 문제다.
CPU 사용량 구성을 분해해보니 답이 나왔다. **iowait이 52%**였다. CPU 사용률 92% 중 절반 이상이 I/O 대기(iowait)였다. CPU가 바빠서 92%가 아니라, I/O를 기다리느라 block된 것이다.
결과: 기각. CPU 병목이 아니라 I/O 병목이었다.
전환점: iowait 52% — 병목은 Disk I/O
iowait은 CPU가 I/O 작업(디스크 읽기/쓰기)의 완료를 기다리며 아무 일도 하지 못하는 시간의 비율이다. 정상 시 6%이던 iowait이 52%까지 치솟았고, 같은 시간대에 Disk Read가 160KB/s에서 7.4MB/s로 46배 증가했다. 무언가가 디스크를 극심하게 읽고 있었다.
Disk I/O가 노드 장애로 이어지는 메커니즘은 이렇다. kubelet은 매 초 PLEG(Pod Lifecycle Event Generator)를 통해 모든 컨테이너의 상태를 점검(relist)한다. 이 relist 결과를 기반으로 API 서버에 heartbeat를 보내 “이 노드는 살아있다”고 알린다. I/O 병목이 생기면 kubelet 프로세스 자체가 느려지고, PLEG relist가 정상 0.04초에서 10초로 250배 지연된다. Kubernetes API 서버는 node-monitor-grace-period(기본 50초) 동안 heartbeat가 오지 않으면 노드를 NotReady로 전환하는데, relist가 10초씩 걸리면 이 50초 안에 heartbeat를 충분히 보내지 못한다.
sequenceDiagram participant APP as worker-app participant IO as iowait participant PLEG as PLEG participant NODE as Node Note over APP: 02:08 APP->>IO: Disk Read 46배 증가 Note over IO: 02:09 IO->>PLEG: iowait 52% Note over PLEG: 02:10 PLEG->>PLEG: relist 0.04초 → 10초 Note over NODE: 02:14 PLEG->>NODE: heartbeat 실패 → NotReady
이 시점에서 다음 질문은 “어떤 Pod가 이 I/O를 만들고 있는가?”였다. 이 환경에서는 Pod별 Disk I/O 메트릭을 직접 수집하고 있지 않아서, 네트워크 수신량을 proxy 지표로 활용했다. 이 배치 작업은 API 요청을 받아 대량으로 디스크에 쓰는 패턴이므로, 네트워크 수신량이 많은 Pod가 I/O를 유발하는 Pod일 가능성이 높다. 확인 결과 문제 노드의 worker-app Pod로 트래픽이 집중되어 있었다. 02:08 시점에 문제 노드의 Pod는 25MB를 수신했지만 정상 노드의 Pod는 4MB뿐이었다. 02:11에는 그 차이가 4000:1까지 벌어졌다.
가설 2: CronJob 동시 실행이 원인 — “2개가 겹쳐서 문제 아닌가?”
02:00에 두 CronJob이 동시 실행됐다는 사실이 확인됐다. update-accumulated-rank(26분 소요)와 update-total-step(6분 소요)이 겹쳤다. 처음엔 두 작업의 동시 실행이 원인이라고 생각했다. 그렇다면 스케줄만 분산하면 해결되니까 비교적 간단하다.
그런데 update-total-step이 02:11에 완료된 후에도 문제가 02:26까지 지속됐다. 02:11 이후 update-accumulated-rank만 단독 실행되는 구간에서도 iowait이 다시 4658%까지 치솟았고, PLEG도 910초를 기록했다. update-accumulated-rank 단독으로도 Node 장애를 유발할 수 있다는 뜻이었다.
결과: 부분 채택. update-total-step 동시 실행은 문제를 악화시킨 요인이지만, 근본 원인은 update-accumulated-rank 단일 작업의 과도한 I/O 자체다. 스케줄 분산만으로는 충분하지 않고, 작업 자체의 I/O를 줄이거나 디스크 성능을 올려야 한다.
왜 특정 노드만 문제가 발생했는가?
worker-app Pod는 2개가 각각 다른 노드에 배치되어 있었다. update-accumulated-rank의 요청이 한쪽 Pod로 라우팅되면서 해당 노드에서만 I/O 폭증이 발생한 것이다. 다른 노드는 정상이었으므로, 이건 노드 자체의 문제가 아니라 워크로드 편중 문제였다.
근본 원인
update-accumulated-rank CronJob의 과도한 Disk I/O가 노드를 포화시켰다. 26분간 지속된 대량 읽기 작업이 iowait을 58%까지 올렸고, kubelet의 PLEG relist가 10초로 지연되면서 heartbeat 실패 → Node Flapping이 발생했다.
flowchart TD A[update-accumulated-rank<br/>26분 소요] --> B[Disk I/O 46배 증가] B --> C[iowait 58%] C --> D[kubelet 응답 지연] D --> E[PLEG relist 10초] D --> F[메트릭 수집 실패] E --> G[Node Flapping 6회]
컨테이너 CPU/메모리 limit이 정상이어도 Disk I/O는 노드 전체를 포화시킬 수 있다는 점이 핵심이다. Kubernetes는 CPU와 메모리에는 cgroup으로 격리를 제공하지만, Disk I/O에 대한 기본 격리는 없다.
해결 방법
병목이 Disk I/O이므로 디스크 성능을 올리는 것이 직접적인 해결책이다. Terraform으로 worker 노드의 EBS 설정을 변경했다.
worker = {
instance_types = ["t4g.medium"]
block_device_mappings = {
xvda = {
device_name = "/dev/xvda"
ebs = {
volume_size = 20
volume_type = "gp3"
iops = 6000
throughput = 250
}
}
}
}gp3 기본값(IOPS 3,000 / Throughput 125MB/s)에서 IOPS 6,000 / Throughput 250MB/s로 2배 증가시켰다. 추가 비용은 월 $20 수준이다.
장기적으로는 update-accumulated-rank 작업 자체의 I/O를 최적화해야 한다. IOPS를 올리는 건 병목의 임계점을 높이는 것이지 제거하는 것이 아니다. 데이터가 늘어나면 같은 문제가 재발할 수 있다.
교훈
- 노드 CPU 90% 이상을 보고 CPU 병목이라고 단정하지 마라. iowait 비중을 함께 확인해라. iowait이 높으면 CPU가 바쁜 게 아니라 I/O를 기다리고 있는 것이다.
- Node NotReady가 발생하면 PLEG Duration부터 확인해라. PLEG 지연이 있으면 kubelet 자체가 느린 것이고, CPU가 아닌 I/O 병목일 가능성이 높다.
- 두 작업의 동시 실행을 의심했다면, 한 작업이 끝난 후에도 문제가 지속되는지 확인해라. 단독 실행 시에도 문제가 있다면 스케줄 분산이 아닌 작업 자체의 최적화가 필요하다.