# 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 메트릭을 별도로 관찰해야 합니다.
핵심 정리:
- CPU 사용률 ≠ Throttling 없음: 30% 사용률에서도 심각한 throttling 가능
- Limit 제거를 두려워하지 말 것: 많은 조직에서 성공적으로 운영 중
- Burst 워크로드는 넉넉한 limit 필요: Request의 3~5배 권장
- 모니터링 필수:
container_cpu_cfs_throttled_*메트릭 대시보드화
“CPU가 여유로운데 왜 느리지?”라는 질문의 답은 대부분 여기에 있습니다.