EKS 노드 Ready/NotReady Flapping - Disk I/O 병목

CPU 92%는 미끄러운 단서다. 새벽 2시 Node Flapping 알림을 받고 그 숫자를 봤을 때 CPU 과부하라고 확신했지만, 틀렸다. 실제 범인은 CronJob 하나가 유발한 Disk I/O 병목이었고, iowait 52%가 PLEG relist를 250배 지연시켜 kubelet heartbeat를 끊었다. EBS IOPS를 3,000에서 6,000으로 올려 23분간 6회 반복됐던 Flapping을 해소했다.

환경: EKS, Terraform, gp3 EBS


CPU 92%에 속았다

새벽 2시, 테스트 EKS 클러스터에서 Node Ready Flapping 알림이 울렸다. 특정 노드가 약 23분간 Ready/NotReady를 6회 반복했고, 그 사이 메트릭 수집까지 끊겼다. 첫눈에 들어온 건 노드 CPU 92%였다. CPU 과부하로 판단하고 조사를 시작했다.

이 노드에서 동작하는 worker-app 컨테이너의 CPU 사용량을 확인하니 최대 0.38 cores, 2코어 노드의 19%에 불과했다. 노드 전체는 92%인데 주요 워크로드가 19%만 쓴다면, 나머지 73%가 어디에 있는지 물어야 한다.

CPU 사용량 구성을 분해하니 답이 나왔다. **iowait이 52%**였다. CPU가 바빠서 92%가 아니라, I/O를 기다리느라 block된 것이었다. CPU 병목 가설은 기각됐다.


iowait 52%가 kubelet을 죽인 경로

정상 시 6%이던 iowait이 52%까지 치솟았고, 같은 시간대에 Disk Read는 160KB/s에서 7.4MB/s로 46배 증가했다. 무언가가 디스크를 극심하게 읽고 있었다.

Disk I/O가 Node Flapping으로 이어지는 메커니즘은 kubelet을 통해 흐른다. kubelet은 매 초 PLEG(Pod Lifecycle Event Generator)로 모든 컨테이너 상태를 점검(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 kubelet PLEG
    participant NODE as Node 상태

    Note over APP: 02:08
    APP->>IO: Disk Read 46배 증가 (160KB/s → 7.4MB/s)
    Note over IO: 02:09
    IO->>PLEG: iowait 52%
    Note over PLEG: 02:10
    PLEG->>PLEG: relist 0.04초 → 10초 (250배 지연)
    Note over NODE: 02:14
    PLEG->>NODE: heartbeat 실패 → NotReady

다음 질문은 “어떤 Pod가 이 I/O를 만들고 있는가”였다. 이 환경에서는 Pod별 Disk I/O 메트릭을 직접 수집하지 않아 네트워크 수신량을 proxy 지표로 활용했다. 이 배치 작업은 API 요청을 받아 대량으로 디스크에 쓰는 패턴이므로, 수신량이 많은 Pod가 I/O를 유발하는 Pod일 가능성이 높다. 02:08 시점에 문제 노드의 Pod는 25MB를 수신했지만 정상 노드의 Pod는 4MB뿐이었고, 02:11에는 그 차이가 4000:1까지 벌어졌다.


CronJob 동시 실행 가설, 그리고 반박

02:00에 두 CronJob이 동시 실행됐다. update-accumulated-rank(26분 소요)와 update-total-step(6분 소요)이 겹쳤다. 처음엔 두 작업의 동시 실행이 원인이라 판단했다. 스케줄만 분산하면 해결되니까 비교적 단순한 문제처럼 보였다.

그런데 update-total-step이 02:11에 완료된 후에도 문제는 02:26까지 지속됐다. update-accumulated-rank 단독 실행 구간에서도 iowait이 4658%까지 치솟았고, PLEG도 910초를 기록했다. update-accumulated-rank 단독으로도 Node Flapping을 유발할 수 있다는 뜻이었다.

update-total-step 동시 실행은 문제를 악화시킨 요인이지만, 근본 원인은 update-accumulated-rank 단일 작업의 과도한 I/O 자체였다. 스케줄 분산만으로는 충분하지 않다.

특정 노드에서만 문제가 발생한 이유도 이 맥락에서 자연스럽다. worker-app Pod는 2개가 각각 다른 노드에 배치되어 있었다. update-accumulated-rank의 요청이 한쪽 Pod로 라우팅되면서 해당 노드에서만 I/O 폭증이 발생한 것이다.

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에 대한 기본 격리는 없다.


EBS IOPS 2배, 그리고 남은 과제

병목이 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 수준이다.


이 장애가 남긴 것

Node CPU 90% 이상이 보인다면 iowait 비중을 함께 확인해야 한다. iowait이 높으면 CPU가 바쁜 게 아니라 I/O를 기다리고 있는 것이다. 이 구분 없이 CPU 병목으로 단정하면 엉뚱한 방향으로 조사하게 된다.

Node NotReady가 발생하면 PLEG Duration을 먼저 봐야 한다. PLEG 지연이 있다면 kubelet 자체가 느린 것이고, CPU가 아닌 I/O 병목일 가능성이 높다. x-cache: Hit 헤더를 보는 것처럼, 진단의 첫 단계는 현상이 어느 레이어에서 발생하는지 확인하는 것이다.

가설을 세우면 반드시 단독 실행 시에도 같은 문제가 발생하는지 확인해야 한다. 두 CronJob 동시 실행을 의심했을 때, 한 작업이 끝난 후에도 문제가 지속되는지 확인한 덕분에 스케줄 분산이 아닌 IOPS 증설이 필요하다는 결론에 도달할 수 있었다.

IOPS를 올리는 건 병목의 임계점을 높이는 것이지 제거하는 것이 아니다. 데이터가 늘어나면 같은 문제가 재발할 수 있다.


앞으로의 방향

이번 수정은 증상 완화에 가깝다. update-accumulated-rank 작업이 26분 동안 7.4MB/s의 읽기를 유지하는 구조 자체를 개선해야 근본 해결이 된다. 인덱스 최적화나 읽기 패턴 변경으로 I/O 볼륨 자체를 줄이는 것이 장기 과제다.

또한 현재 Pod별 Disk I/O를 직접 수집하지 않아 네트워크 수신량을 proxy로 사용했다. 다음 조사에서 동일한 우회 없이 바로 원인 Pod를 특정할 수 있도록, Pod 레벨 Disk I/O 메트릭 수집을 모니터링 스택에 추가하는 것이 다음 과제다.


참고 자료


정보 공백 요약

#필요한 정보이유위치
1Node Flapping 발생 구간 CloudWatch 메트릭 (CPU, iowait, Disk Read 추이)“CPU 92% → iowait 52% 전환점” 시각적 근거오프닝 섹션
2EBS IOPS 증설 전후 iowait 추이 비교”IOPS 증설로 Flapping 해소” 정량적 근거해결 방법 섹션