본문으로 건너뛰기
Kubernetes CPU Throttling and CFS Quota production guide cover

# Kubernetes CPU Throttling 완벽 가이드: CFS Quota가 당신의 애플리케이션을 죽이고 있다

Table of Contents

“CPU 사용률이 30%밖에 안 되는데 왜 응답이 느리죠?”

모니터링 대시보드를 보면 CPU 사용률은 여유롭고 메모리도 충분한데, 애플리케이션의 응답 시간이 갑자기 튀는 현상. Kubernetes를 운영하다 보면 누구나 한 번쯤 마주치는 미스터리입니다.

범인은 바로 CPU Throttling입니다. 그리고 이것은 Linux CFS(Completely Fair Scheduler)가 CPU 시간을 할당하는 방식과 Kubernetes의 리소스 제한이 만나면서 발생하는 구조적인 문제입니다.

CPU Throttling이란 무엇인가?

CPU Throttling은 컨테이너가 설정된 CPU 제한을 초과하려 할 때 커널이 강제로 실행을 일시 중지시키는 현상입니다. 문제는 이 “제한 초과”의 기준이 우리의 직관과 다르다는 점입니다.

CFS Quota의 동작 원리

Linux CFS는 CPU 시간을 100ms(기본값) 주기로 나누어 관리합니다. Kubernetes에서 cpu: 500m을 설정하면 이는 “100ms마다 50ms의 CPU 시간을 사용할 수 있다”는 의미입니다.

resources:
  limits:
    cpu: "500m" # 100ms 중 50ms 사용 가능
    memory: "512Mi"
  requests:
    cpu: "200m"
    memory: "256Mi"

여기서 핵심 문제가 발생합니다. 만약 애플리케이션이 10ms 동안 집중적으로 CPU를 사용하면 어떻게 될까요?

시간 축 (100ms 주기)
|----|----|----|----|----|----|----|----|----|----|
0    10   20   30   40   50   60   70   80   90   100ms

할당된 quota: 50ms
실제 사용: [=====] (10ms에 50ms 전부 소진!)

             남은 90ms 동안 THROTTLED

CPU 사용률로 보면 전체의 50%만 사용했지만, 처음 10ms에 할당량을 모두 소진해 버려 남은 90ms 동안은 완전히 멈춥니다.

Burstable 워크로드의 함정

이 문제는 특히 다음과 같은 워크로드에서 치명적입니다.

  • 웹 서버: 요청 처리 시 순간적으로 높은 CPU 필요
  • 배치 처리: 특정 시점에 집중적인 연산 수행
  • 가비지 컬렉션: GC 발생 시 일시적으로 CPU 급증
  • JIT 컴파일: 초기 실행 시 높은 CPU 사용

Throttling 진단하기

1. 컨테이너 레벨 확인

가장 직접적인 방법은 cgroup 통계를 확인하는 것입니다.

# Pod 내부에서 실행
cat /sys/fs/cgroup/cpu/cpu.stat

출력 예시:

nr_periods 45678
nr_throttled 12345
throttled_time 987654321000
  • nr_periods: 전체 스케줄링 주기 수
  • nr_throttled: throttling이 발생한 주기 수
  • throttled_time: 총 throttling 시간 (나노초)

Throttling 비율 계산:

throttle_ratio = nr_throttled / nr_periods
               = 12345 / 45678
               = 27%

27%는 심각한 수준입니다. 일반적으로 5% 이하를 목표로 해야 합니다.

2. Prometheus 메트릭 활용

cAdvisor가 수집하는 메트릭으로 클러스터 전체를 모니터링할 수 있습니다.

# Throttling 비율 (%)
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m]) * 100

# 초당 Throttling 시간 (초)
rate(container_cpu_cfs_throttled_seconds_total[5m])

Grafana 대시보드용 완전한 쿼리:

# Namespace별 Top 10 Throttled Pods
topk(10,
  sum by (namespace, pod) (
    rate(container_cpu_cfs_throttled_periods_total{
      container!="",
      container!="POD"
    }[5m])
  )
  / sum by (namespace, pod) (
    rate(container_cpu_cfs_periods_total{
      container!="",
      container!="POD"
    }[5m])
  ) * 100
)

3. kubectl top과의 차이점

kubectl top pod는 평균 CPU 사용률만 보여주기 때문에 throttling을 감지할 수 없습니다.

$ kubectl top pod my-app-pod
NAME         CPU(cores)   MEMORY(bytes)
my-app-pod   150m         256Mi

CPU limit이 500m인데 150m만 사용 중이라면 여유로워 보입니다. 하지만 실제로는 순간적인 burst로 인해 심각한 throttling이 발생하고 있을 수 있습니다.

해결 방안

방안 1: CPU Limit 제거

가장 급진적이지만 효과적인 방법입니다. 많은 조직에서 CPU limit을 아예 설정하지 않는 방향으로 전환하고 있습니다.

resources:
  requests:
    cpu: "500m" # 스케줄링용
    memory: "512Mi"
  limits:
    # cpu: 제거!
    memory: "512Mi" # 메모리는 반드시 유지

장점:

  • Throttling 완전 제거
  • 유휴 CPU를 최대한 활용

단점:

  • Noisy neighbor 문제 가능
  • 리소스 계획이 어려움

적용 조건:

  • Node에 여유 CPU가 충분할 때
  • 신뢰할 수 있는 워크로드만 실행할 때
  • 강력한 모니터링이 갖춰져 있을 때

방안 2: Limit을 Request의 N배로 설정

Burst를 허용하면서도 무제한은 피하는 절충안입니다.

resources:
  requests:
    cpu: "200m"
  limits:
    cpu: "1000m" # request의 5배

추천 비율:

  • 웹 서버: 3~5배
  • 배치 작업: 5~10배
  • GC가 무거운 Java 앱: 5~8배

방안 3: CFS Quota 주기 조정

Linux 5.4+에서는 CFS quota 주기를 조정할 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  annotations:
    # 주기를 100ms에서 5ms로 줄임
    io.kubernetes.cri.cpu-period: "5000"
spec:
  containers:
  - name: app
    resources:
      limits:
        cpu: "500m"

주기가 짧아지면:

  • 5ms 주기: 5ms마다 2.5ms 사용 가능
  • Burst가 분산되어 throttling 감소

주의: 이 기능은 CRI-O나 containerd 버전에 따라 지원 여부가 다릅니다.

방안 4: 애플리케이션 최적화

궁극적으로는 burst 자체를 줄이는 것이 좋습니다.

Java/JVM 앱:

# G1GC 사용 시 GC 스레드 제한
-XX:ParallelGCThreads=2
-XX:ConcGCThreads=1

# GC 로깅으로 원인 파악
-Xlog:gc*:file=/var/log/gc.log

Node.js 앱:

// 무거운 동기 작업 분할
async function processLargeData(data) {
  const CHUNK_SIZE = 1000;
  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);
    processChunk(chunk);

    // 이벤트 루프에 제어권 양보
    await new Promise(resolve => setImmediate(resolve));
  }
}

Go 앱:

// GOMAXPROCS를 limit에 맞게 조정
import _ "go.uber.org/automaxprocs"

// 또는 수동 설정
runtime.GOMAXPROCS(2)

실전 사례: Java 애플리케이션 Throttling 해결

상황

Spring Boot 애플리케이션의 p99 응답 시간이 불규칙하게 튀는 현상:

  • 평균 응답: 50ms
  • p99 응답: 2000ms (간헐적)
  • CPU 사용률: 40%

진단

# Throttling 확인
rate(container_cpu_cfs_throttled_seconds_total{
  pod=~"my-spring-app.*"
}[5m])

결과: 초당 0.3초 throttling (30%!)

GC 로그 분석:

[GC pause (G1 Evacuation Pause) 234M->180M(512M), 0.0823456 secs]
      [Parallel Time: 78.2 ms, GC Workers: 8]

GC Worker가 8개로 설정되어 있어 GC 발생 시 순간적으로 8코어를 요구했습니다. 하지만 limit은 2코어(2000m)…

해결

# Before
resources:
  limits:
    cpu: "2000m"

# After
resources:
  requests:
    cpu: "1000m"
  limits:
    cpu: "4000m" # Burst 허용
# JVM 옵션 추가
-XX:ParallelGCThreads=2
-XX:ConcGCThreads=1

결과

  • Throttling: 30% → 2%
  • p99 응답: 2000ms → 200ms

모니터링 대시보드 구성

Grafana 대시보드에 포함할 핵심 패널들:

1. Throttling Heatmap

sum by (pod) (
  rate(container_cpu_cfs_throttled_periods_total[5m])
) / sum by (pod) (
  rate(container_cpu_cfs_periods_total[5m])
) * 100

2. Throttling vs CPU Usage 비교

# 실제 CPU 사용률
sum by (pod) (rate(container_cpu_usage_seconds_total[5m]))
/ sum by (pod) (kube_pod_container_resource_limits{resource="cpu"})

# Throttling 비율
# (위의 쿼리와 동일)

3. Alert Rule

groups:
- name: cpu-throttling
  rules:
  - alert: HighCPUThrottling
    expr: |
      sum by (namespace, pod) (
        rate(container_cpu_cfs_throttled_periods_total[5m])
      ) / sum by (namespace, pod) (
        rate(container_cpu_cfs_periods_total[5m])
      ) > 0.25
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "High CPU throttling detected"
      description: "Pod {{ $labels.pod }} is being throttled {{ $value | humanizePercentage }}"

조직별 권장 전략

스타트업 / 소규모 팀

# 단순하게: Limit 제거
resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    memory: "512Mi"

모니터링을 통해 실제 사용량 파악 후 request 조정.

중규모 조직

# 보수적 Burst 허용
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "2000m" # 4배

Throttling 5% 이하 유지를 목표로 limit 조정.

대규모 / 멀티테넌트

# ResourceQuota와 함께 사용
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "1500m" # 3배

Namespace별 ResourceQuota로 전체 사용량 제어.

흔한 실수와 주의사항

1. Request = Limit 안티패턴

# 절대 피해야 할 설정
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "500m" # Guaranteed QoS지만 throttling 심각

Guaranteed QoS를 위해 request와 limit을 동일하게 설정하는 경우가 있는데, 이는 throttling의 주범입니다.

2. 평균 사용률만 보는 실수

$ kubectl top pod
NAME     CPU
my-app   200m  # "여유롭네!" -> 착각

Throttling 메트릭을 반드시 함께 확인해야 합니다.

3. HPA와의 상호작용

# HPA가 CPU 기반일 때
behavior:
  scaleUp:
    stabilizationWindowSeconds: 60

Throttling으로 실제 CPU 사용률이 낮게 측정되면 HPA가 scale-up하지 않는 문제가 발생합니다. Custom metrics(응답 시간, 큐 길이 등)를 함께 사용하세요.

마치며

CPU Throttling은 Kubernetes의 “설계된 동작”이지만, 많은 운영자들이 그 영향을 과소평가합니다. CPU 사용률 모니터링만으로는 절대 발견할 수 없는 문제이기에, 반드시 throttling 메트릭을 별도로 관찰해야 합니다.

핵심 정리:

  1. CPU 사용률 ≠ Throttling 없음: 30% 사용률에서도 심각한 throttling 가능
  2. Limit 제거를 두려워하지 말 것: 많은 조직에서 성공적으로 운영 중
  3. Burst 워크로드는 넉넉한 limit 필요: Request의 3~5배 권장
  4. 모니터링 필수: container_cpu_cfs_throttled_* 메트릭 대시보드화

“CPU가 여유로운데 왜 느리지?”라는 질문의 답은 대부분 여기에 있습니다.

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트

# WebSocket 대규모 확장 완벽 가이드: Redis Pub/Sub로 100만 동시 연결 처리하기

게시:

실시간 채팅, 알림, 라이브 스트리밍 서비스를 위한 WebSocket 서버 확장 전략을 다룹니다. Node.js Socket.IO + Redis Pub/Sub 아키텍처, 100k+ 연결에서의 메모리 누수 해결, 로드 밸런싱 전략, 그리고 2025년 프로덕션 검증된 스케일링 패턴까지 모두 포함합니다.

읽기