본문으로 건너뛰기
CPU Burst (커널 5.14)

CPU Burst (커널 5.14)

03 · CPU Burst — CPU limit을 지키면서 불필요한 throttling만 걷어낸다

한눈에

  • CFS bandwidth control은 period를 독립 정산하고 안 쓴 quota를 버린다. 그래서 평균 사용률이 limit의 절반이어도 순간 수요만으로 throttle이 걸린다. 자원이 부족한 게 아니라 나눠주는 방식의 문제다.
  • CPU Burst는 이전 period의 미사용분을 buffer에 적립해 뒀다가 빌려 쓰게 한다. 순간 상한만 Quota + Buffer로 늘고, 누적 상한 Σ CPUTime ≤ Quota × N은 그대로다. limit을 올려주는 것과 근본적으로 다르다.
  • 평균이 아니라 꼬리가 개선된다. 발표 실측에서 RT Avg는 30+ms→9.6ms(약 1/3)인데 P99는 500+ms→27.32ms(약 1/20). throttling이 망가뜨리는 건 tail이기 때문이다.
  • 공짜가 아니다. 이웃 컨테이너가 deadline을 놓칠 수 있고 그게 누적되면 unbounded fail이다. 다만 정량화돼 있다 — 평균 CPU 사용률 70% 미만이면 안전하고, 직관과 반대로 컨테이너 수가 적은 노드가 더 위험하다.
  • cpu.cfs_burst_us 기본값 0 = 기존 동작과 완전히 동일. 커널만 올려도 아무것도 안 바뀐다. 도입 리스크가 낮은 편.
  • ⚠️ k8s에서 Pod spec으로 켜는 건 아직 안 된다(#104516). 현재는 노드에서 cgroup을 직접 만지거나 벤더 annotation을 쓴다.

출처: KubeCon + CloudNativeCon China 2021 — CPU Burst: Getting Rid of Unnecessary Throttling… (常怀鑫·丁天琛, Alibaba Cloud 커널팀). 이 글은 발표 내용에 커널 동작과 운영 판단을 덧붙여 재구성했다.

자매 문서: 챕터 개요 · 이 문서가 푸는 문제를 먼저 진단하는 쪽은 02 CPU Throttling — 증상 관측과 다른 대응책(limit 제거·CPU Manager)이 거기 있다 · 같은 CPU limit 문제의 다른 얼굴은 istio 09 §8 · 리소스를 재시작 없이 바꾸는 쪽은 01 In-Place Pod Resize

1. 먼저 requests와 limits를 분리해야 한다

이름이 비슷해서 묶이지만 커널 구현이 다르다. 이 차이가 문제의 출발점이다.

requestslimits
커널 구현cpu.shares (CFS Share)CFS Bandwidth Control
계산CPUTime = NumCPUs × shares / Σsharesperiod마다 quota 리필
경쟁이 없을 때천장이 열린다 — 노드 전체까지 쓸 수 있다경쟁과 무관하게 막는다
스케줄링 영향있다없다

requests는 분모(Σshares)가 상황에 따라 변한다. 32 CPU 노드에 requests: 8 컨테이너가 혼자 있으면 실제로 32를 다 쓴다. 하나 더 붙으면 16, requests: 16짜리가 더 붙으면 8이 된다. 바닥은 보장하되 여유가 있으면 그냥 쓰게 해주는 구조다.

limits는 정반대다. 노드가 텅 비어 있어도 막는다. 이 비대칭이 아래의 문제를 만든다.

2. 문제 — period 간에 이월이 없다

cpu.cfs_period_us = 100000   # 정산 주기 100ms
cpu.cfs_quota_us  =  20000   # period당 20ms → 0.2 CPU

동작은 단순하다. period가 시작하면 quota가 리필되고, 소진하면 period 끝까지 강제로 재운다(throttle). 다음 period에 다시 리필.

여기서 핵심은 각 period가 완전히 독립적이라는 것이다. 지난 period에 quota를 하나도 안 썼어도 그건 이월되지 않고 사라진다. 이 한 줄이 이 문서의 전부다.

그래서 벌어지는 일

요청 하나에 CPU 30ms가 연속으로 필요한 애플리케이션을 보자.

설정처리 시간
limit 없음30ms몰아 쓰고 끝
limit 0.2 CPU110ms20ms 쓰고 80ms 대기 → 다음 period에 10ms

버그에 가깝다고 부르는 이유가 여기 있다. 사용자 입장에선 “준 것의 절반밖에 안 쓰는데 왜 느려지지?“가 된다. 커뮤니티에서 오래 굴러온 문제이기도 하다 — #67577, #51135.

지금 확인해볼 것

cat /sys/fs/cgroup/cpu/<컨테이너경로>/cpu.stat
nr_periods     157
nr_throttled    12          # ← throttle 걸린 period 수
throttled_time  1668131531  # ← 누적 throttle 시간(ns)

nr_throttled / nr_periods 비율을 먼저 보라. 이 값이 낮으면 아래 내용은 당신 클러스터와 무관하다. 높은데 CPU 사용률이 낮다면 정확히 이 문제다.

3. 기존 해법이 왜 부족한가

limit을 올린다requests = limits(Guaranteed)면 requests도 같이 올라 노드당 Pod 수가 줄고 클러스터 사용률이 떨어진다. requests를 고정하고 limits만 키우면 over-commitment가 되어 과금 정합성과 성능 일관성이 깨진다.

quota와 period를 함께 키운다20ms/100ms40ms/200ms. throttling 횟수는 줄지만 한 번 걸렸을 때 더 오래 재워진다. 최대 대기가 100ms에서 200ms로 늘어난다. 횟수는 줄고 한 방이 커지는 교환이고, 커널의 period 상한 1초 때문에 무한정 키울 수도 없다.

둘 다 사용률·과금·응답시간 중 뭔가를 포기하고, 워크로드마다 사람이 튜닝해야 한다.

4. 설계 — 과거의 underrun을 빌린다

안 쓴 건 권리를 포기한 게 아니라 그때 일이 없었을 뿐이다. 그러면 적립해 뒀다가 쓰게 하면 된다.

quota 20ms / period 100ms / buffer 20ms일 때:

periodCPU 수요동작buffer
1없음미사용분 적립0 → 20ms
230msquota 20 + buffer 10 차용 → throttle 없음20 → 10ms
330msquota 20 + buffer 10 → 소진 후 throttle10 → 0ms

같은 장부를 wall time으로 펴면 이렇게 보인다. 위쪽은 3 period 동안 40ms밖에 못 쓰고 나머지는 멈춰 있다 — 일이 있는데도 그렇다. 아래쪽은 60ms를 쓰는데, 그게 바로 다음 절에서 볼 300ms × 0.2 = 60ms, 즉 limit이 원래 허락한 총량이다.

핵심 불변식

3 period 실제 사용:  0 + 30 + 30 = 60ms
같은 구간의 limit:  300ms × 0.2   = 60ms

순간적으로 30ms를 썼지만 전체로 보면 limit을 1ms도 넘지 않았다.

Burst 없음CPUTime ≤ Quota
Burst 있음CPUTime ≤ Quota + Buffer
항상Σ CPUTime ≤ Quota × Number(Periods)

buffer는 반드시 이전 underrun에서만 나온다. 없는 걸 만들지 않는다. 그래서 순간 상한만 느슨해지고 누적 상한은 불변이다. limit을 2배 주면 장기 평균도 2배가 되지만, CPU Burst는 장기 평균이 원래 limit 그대로다. 과금 모델과 용량 계획이 안 깨지는 이유가 이것이다.

throttling이 사라지는 게 아니다. 빌려올 시간이 없으면 여전히 throttle된다(위 표 period 3). CPU Burst는 throttling 제거가 아니라 불필요한 throttling 제거다.

5. 인터페이스 — 파일 하나

# 설정: 적립 가능한 최대치. 기본값 0 = 비활성
echo 20000 > /sys/fs/cgroup/cpu/<경로>/cpu.cfs_burst_us

cpu.stat에 관측 필드 둘이 추가된다.

nr_throttled     0          # ← 0이 됨
throttled_time   0
nr_bursts       99          # (신규) burst를 사용한 period 수
burst_time  1899843279      # (신규) 누적 burst 사용 시간(ns)

튜닝은 이 두 값만 보면 된다.

관측해석조치
nr_throttled 여전히 높음buffer 부족키운다
nr_bursts ≈ 0burst가 필요 없는 워크로드켤 이유 없음
nr_throttled 0, nr_bursts > 0정상 동작유지

기본값이 0이라 커널만 올려도 동작이 안 바뀐다. 도입 리스크가 낮다는 점에서 in-place resize와 성격이 다르다.

6. 실측 — 평균이 아니라 꼬리를 본다

발표에서 제시된 두 사례다.

사례지표
GC 스레드 많은 앱RT Avg30+ms9.6ms
RT P99500+ms27.32ms
다른 앱RT Avg1310ms952ms (-27%)

평균은 1/3인데 P99는 1/20이다. 이 비대칭이 CPU Burst의 성격을 그대로 보여준다 — throttling은 평균을 조금씩 갉는 게 아니라 일부 요청을 크게 지연시킨다. 그래서 평균만 보는 대시보드에서는 문제가 잘 안 보이고, P99를 봐야 드러난다.

첫 사례에서 효과가 컸던 이유는 그 Pod이 GC 스레드 수를 많이 잡아 둬서 CPU 사용이 순간적으로 튀는 패턴이었기 때문이다. GC는 평소 놀다가 한 번에 몰아 쓰는 대표적 bursty 패턴이고, JVM 계열이 CPU limit과 유독 상성이 나쁜 이유이기도 하다. 반대로 CPU를 꾸준히 꽉 채우는 배치·계산 작업은 적립될 여유가 없어 효과가 거의 없다.

7. 부작용 — 이웃이 대가를 치른다

발표의 약 30%가 이 부분이다. 이유가 있다 — 업스트림 메인테이너가 병합 조건으로 요구한 숙제였다.

CPU Burst 이전에는 이 전제가 성립했다.

모든 태스크의 quota 총합 ≤ 100%로 설정하면 → 스케줄링 안정성과 실시간성이 보장된다

task1(50%) + task2(50%)에서 task1에만 buffer 10%를 주면:

task1task2
Burst 없음50%50%둘 다 deadline 충족
Burst 있음60%40%task2 missed deadline

더 나쁜 건 누적이다. 밀린 작업이 다음 주기로 넘어가고, 그 사이 task1이 또 burst를 쓴다. 메인테이너의 표현으로 “unbounded fail” — 따라잡을 시간이 영영 안 올 수 있다.

정량화 — WCET

이걸 “위험할 수도 있다"로 두면 아무도 못 쓴다. 그래서 **WCET(Worst-Case Execution Time)**로 정량화했다.

  • Burst 없음 → 모든 태스크가 한 period 안에 완료 → WCET < 1
  • Burst 있음 → WCET > 1 가능 → 그 시점의 스케줄링 시스템은 불안정

방법은 대기행렬 이론 모델링 + 몬테카를로 시뮬레이션이다(CPU를 서비스 창구로, 태스크 수요가 period 간격으로 도착). 입력은 수요 분포 · 노드의 컨테이너 총수 · buffer 크기, 출력은 WCET 기댓값과 P(WCET > 1). 시뮬레이터가 공개돼 있어 자기 환경 수치로 돌려볼 수 있다.

결론 — 70% 선, 그리고 직관과 반대인 것

평균 CPU 사용률 70% 미만이면 CPU Burst가 이웃에 큰 영향을 주지 않는다. (지수분포 수요, 컨테이너 20개, buffer = quota × 1배 기준)

대부분의 프로덕션 노드가 이 조건을 만족한다. 위험도를 좌우하는 축은 둘이다.

요인방향이유
평균 부하 ↑위험 ↑자명하다
컨테이너 수 ↓위험 ↑독립 동일 분포에서 수가 많을수록 수요가 평균에 수렴해 초과분과 여유분이 상쇄된다 (중심극한정리)

두 번째가 직관과 어긋난다. 컨테이너가 빽빽한 노드가 아니라 몇 개 안 되는 노드가 더 위험하다. 파레토 분포(꼬리가 두꺼운 현실적 모델)로 바꿔도 경향은 같다.

부하가 계속 높은 환경이라면? 발표자들은 솔직하게 선을 긋는다 — 그때는 CPU Burst 자체가 도움이 안 되니 사양을 올리거나 부하를 줄이라고 한다.

8. buffer 크기 — 처리량 ↔ 예측가능성 다이얼

출발점은 quota와 같은 크기(1배). 안전성 분석도 이 기준이다.

Buffer를 키운다Buffer를 줄인다
전체 처리량을 높이고 싶을 때스케줄링 안정성·실시간성이 중요할 때
평균 부하가 높지 않을 때전체 부하가 높을 때
→ burst 효과 ↑, 개별 최적화 이득 ↑→ WCET ↓, 이웃 간섭 ↓
WCET ↑, 이웃 간섭 ↑→ 최적화 이득 ↓

9. 지금 쓸 수 있나

상태
커널5.14+ 메인라인 (commit f4183717b370)
cgroup v1cpu.cfs_burst_us
cgroup v2cpu.max.burst — ⚠️ 발표에서 다루지 않음, 별도 확인 필요
배포판Anolis OS 8.2+ (CentOS 8 생태계 호환)
Kubernetes⚠️ Pod spec 지원 없음#104516 WIP
Alibaba ACKPod annotation alibabacloud.com/cpuBurst: '{"policy":"auto"}'

여기가 실무의 발목이다. 커널에는 5.14부터 들어가 있지만 k8s가 Pod spec으로 노출하지 않는다. 그래서 지금 쓰려면 노드에서 cgroup을 직접 만지거나(파드 재생성 시 날아간다), 벤더가 붙인 annotation에 의존해야 한다. 매니지드 클러스터라면 사실상 벤더 지원 여부가 전부다.

01 In-Place Pod Resize와 대조하면 성격이 뚜렷하다. 그쪽은 k8s API가 먼저 갖춰지고 커널 반영의 정합성이 숙제였고, 이쪽은 커널이 5년 전에 끝났는데 k8s 표면이 없다.

10. 도입 체크리스트

  • 커널 5.14+ 인가 (또는 Anolis OS 8.2+)
  • 대상 워크로드의 nr_throttled / nr_periods 비율이 실제로 높은가
  • 그런데 평균 CPU 사용률은 limit보다 낮은가 — 이 둘이 동시에 참이어야 대상이다
  • 워크로드가 bursty한가 (웹/API, GC 많은 JVM ✅ / 배치·CPU-bound ❌)
  • 노드 평균 CPU 사용률 70% 미만인가
  • 노드의 컨테이너 수가 충분한가 (적으면 더 위험)
  • cpu.cfs_burst_usquota와 같은 값으로 시작
  • 적용 후 nr_throttled(↓) / nr_bursts(>0) 재확인
  • 이웃 컨테이너의 P99에 악화가 없는지 확인 — 여기가 진짜 검증 지점이다

11. 정리

세 문장으로 줄이면 이렇다.

  1. 불필요한 throttling은 자원 부족이 아니라 period 독립 정산의 부작용이다. 평균 사용률이 limit보다 낮은데 throttle이 걸리면 그게 증거다.
  2. CPU Burst는 순간 상한만 늘리고 누적 상한은 건드리지 않는다. limit을 올리는 것과 혼동하면 안 된다.
  3. 대가는 이웃이 치르지만 정량화돼 있다. 70% 미만·컨테이너 다수면 안전하고, 그 바깥이면 시뮬레이터로 먼저 확인하라.

실무에서 지금 당장의 병목은 기술이 아니라 k8s 표면의 부재다. 커널은 준비돼 있으니, #104516의 진행을 지켜보는 게 현재로선 가장 현실적인 대응이다.