8코어에서 limits.cpu 1은 12.5ms 만에 바닥난다 — Kubernetes CFS throttling

seonwoo_jung·7일 전

limits.cpu: 1을 준 Pod의 평균 CPU 사용률이 40%인데도 p99 응답 지연이 주기적으로 튄다 — 이게 어떻게 가능한가? "limit이 1코어니까 1코어를 다 쓰기 전엔 안 막히겠지"라는 직관은 틀렸다. 컨테이너는 코어를 다 쓰기 한참 전에 throttle된다. limits.cpu 한 줄이 리눅스 cgroup의 quota/period로 번역되어 스케줄러가 스레드를 강제로 재우는 과정을 끝까지 따라가 봤다.

CPU limit은 점유율 상한이 아니라 시간 예산이다

가장 먼저 바로잡아야 할 오해가 이것이다.

Kubernetes CPU limit은 코어 점유율 상한이 아니라, 일정 주기(기본 100ms)마다 리필되는 CPU 시간 예산(quota) 이다. 한 주기 안에서 예산을 다 쓰면, 멀티코어로 아무리 여유가 있어도 다음 주기 경계까지 그 cgroup의 모든 스레드가 강제로 throttle(재워짐)된다.

리눅스의 CFS(Completely Fair Scheduler)는 CONFIG_FAIR_GROUP_SCHED 위에서 CFS Bandwidth Control이라는 기능을 제공한다. 한 cgroup의 CPU 대역폭은 두 값으로 정의된다 (Linux Kernel Docs, "CFS Bandwidth Control").

  • cpu.cfs_period_us: 한 주기의 길이. 기본 100000µs = 100ms.
  • cpu.cfs_quota_us: 한 주기 안에서 쓸 수 있는 누적 CPU 시간. 기본 -1(무제한).

limits.cpu가 quota로 번역되는 규칙

Kubernetes의 limits.cpu는 이 quota로 직접 매핑된다. period는 기본 100ms로 고정된다.

limits.cpuquota (period 100ms 기준)의미
"1"100000µs (100ms)주기의 100% = 1코어어치
"500m"50000µs (50ms)50%
"2"200000µs (200ms)주기당 200ms = 2코어어치

cgroup v2에서는 두 값이 cpu.max 한 파일에 "quota period" 형태로 합쳐진다.

# limits.cpu: "1.5" (cgroup v2)
$ cat /sys/fs/cgroup/.../cpu.max
150000 100000        # quota=150ms / period=100ms

여기서 짚고 넘어갈 비대칭이 하나 있다. requests.cpu는 quota가 아니다. requests는 cpu.shares(v1) / cpu.weight(v2)로 매핑되는 상대적 가중치일 뿐이다. 경합이 일어났을 때 분배 비율만 정하고 상한을 강제하지 않는다. throttling을 만드는 건 오직 limits.cpu다.

quota가 소진되는 흐름

quota는 전역 풀에 주기 경계마다 리필된다. 스레드가 runnable해지면 전역 풀에서 slice 단위로 per-CPU 런큐("silo")로 quota를 떼어 온다(kernel docs: "transferred to cpu-local silos on a demand basis"). 이 batch 전송이 코어 수가 많은 머신에서 전역 락 경합을 줄여 준다고 알려져 있다.

period 시작 (t=0ms)                          period 끝 (t=100ms)
|---------------------------------------------|
[quota 100ms 리필] → 스레드 실행하며 silo에서 소진
                          ↑
                  quota 0 도달 (t=40ms)
                  → 이후 그 cgroup의 모든 스레드 THROTTLED
                  → 다음 period 경계까지 강제 sleep (60ms 놀고 있음)

핵심은 "quota 소진 = throttle"이지 "코어 다 씀 = throttle"이 아니라는 점이다. quota는 벽시계 시간이 아니라 누적 CPU 시간(cpu-seconds) 으로 센다. 여러 코어에서 동시에 도는 멀티스레드 앱은 그만큼 quota를 빨리 고갈시킨다.

멀티코어 burst가 만드는 조기 throttling — 가장 흔한 함정

limits.cpu: "1"(quota=100ms / period 100ms)인 컨테이너가 8코어 노드에서 8개 스레드로 동시에 일을 시작한다고 하자.

8 스레드 × 12.5ms 동시 실행 = 누적 100ms CPU 시간 → 12.5ms 만에 quota 소진
→ 남은 87.5ms 동안 8개 스레드 전원 throttle
→ 벽시계 기준 CPU 사용률은 100%의 1/8밖에 안 보이는데 지연은 발생

평균 사용률만 보면 "limit 여유 있는데 왜 느리지?"가 된다. 모니터링 대시보드의 평균 CPU%가 limit보다 낮은데도 throttling이 일어나는 전형적 원인이 이것이다. 그래서 latency에 민감한 워크로드에서는 CPU limit을 일부러 제거하거나(요청/가중치만 사용) period를 줄이는 식의 완화책이 종종 논의된다.

throttle을 눈으로 본다 — cpu.stat

추측 대신 측정해야 한다. throttling은 cpu.stat으로 직접 노출된다.

$ cat /sys/fs/cgroup/.../cpu.stat
nr_periods 50000        # 지금까지 지난 period 수
nr_throttled 1200       # 그중 throttle이 발생한 period 수
throttled_usec 95000000 # 누적 throttle된 시간(µs)

진단의 핵심 신호는 두 가지다. nr_throttled / nr_periods(throttle 비율)와 throttled_usec의 증가분. 이 값이 꾸준히 오르면, 평균 CPU%가 limit 미만이어도 burst 패턴 때문에 조기 throttle 중이라는 뜻이다. (cgroup v1은 필드명이 throttled_time(ns), v2는 throttled_usec(µs)로 다르다.)

실제 노드에서 매핑을 확인하는 흐름은 이렇다.

# Pod: limits.cpu="500m" → 기대: cpu.max = "50000 100000"
$ POD_CG=/sys/fs/cgroup/kubepods.slice/.../<container>
$ cat $POD_CG/cpu.max
50000 100000          # quota 50ms / period 100ms = 0.5코어 ✔

# throttling 누적 관측 (period마다 갱신)
$ grep -E 'nr_(periods|throttled)|throttled_usec' $POD_CG/cpu.stat

burst — 미사용 quota 이월

워크로드는 보통 매 주기 quota를 다 쓰지는 않는다. cpu.cfs_burst_us(기본 0)는 미사용 quota를 일정 한도까지 누적 했다가 가끔 튀는 peak에서 빌려 쓰게 해 준다. kernel docs는 이를 "미래의 underrun을 담보로 지금 시간을 빌린다(bounded)"고 설명한다. 평균은 WCET 아래로 짜되 가끔의 spike를 허용해 조기 throttle을 줄이는 절충인데, 시스템 전체로는 매 overrun이 underrun과 짝지어져 안정성을 유지한다고 알려져 있다.

정리

  • CPU limit은 코어 점유율 상한이 아니라 100ms 주기당 누적 CPU 시간 예산이다. 멀티스레드 앱은 여러 코어에서 동시에 돌며 그 예산을 순식간에 태운다.
  • 그래서 평균 CPU% < limit인데도 throttle이 일어난다. 평균 사용률은 벽시계 기준이라 짧은 burst 안의 throttle을 숨긴다.
  • 진짜 신호는 cpu.statnr_throttledthrottled_usec다. 대시보드의 평균선이 아니라 이 값을 봐야 한다.

다음에 더 파고들 만한 것: cgroup v1 cpu.shares(1024 기준) vs v2 cpu.weight(1~10000) 매핑과 경합 시 실제 분배 계산, 그리고 period 단축(예: 10ms)이 throttling/지연에 미치는 효과와 그 부작용(전역 quota 회계 압력).

참고 자료

0개의 댓글