{"id":"IL-0012","org":"Omio","date":"2020-02-17","docs":["https://www.kernel.org/doc/Documentation/scheduler/sched-design-CFS.txt","https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt","http://www.brendangregg.com/usemethod.html","https://engineering.squarespace.com/blog/2017/understanding-linux-container-scheduling","https://www.linuxjournal.com/content/everything-you-need-know-about-linux-containers-part-i-linux-control-groups-and-process","https://k8s.af/","https://github.com/kubernetes/kubernetes/issues/51135#issuecomment-373454012","https://github.com/kubernetes/kubernetes/issues/67577","https://gist.github.com/bobrik/2030ff040fad360327a5fab7a09c4ff1"],"title":{"en":"CPU limits and an unpatched kernel CFS quota bug throttled multi-threaded services into stalls, failed health checks and timeouts","ko":"CPU limit 과 커널 CFS 쿼터 버그로 파드가 과하게 스로틀링돼 헬스 체크가 실패하고 애플리케이션이 멈췄다"},"domain":["kubernetes","gcp"],"impact":{"scope":"multiple applications","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A CPU limit is a time quota per 100 ms period, not a share of cores, so a multi-threaded container can be stopped for most of each period long before it uses the cores it was given, and a kernel bug can make that worse. Throttling is invisible in standard Kubernetes metrics and has to be read from the cgroup counters. Removing limits trades away protection against a container that takes a whole node, which is only acceptable when runtimes are tuned.","ko":"CFS 쿼터 버그가 고쳐지지 않은 커널을 쓴다면 쿠버네티스에서 CPU limit 을 없애거나 kubelet 에서 CFS 쿼터를 끄는 편이 낫다. CPU request 와 limit 은 설정이 비슷해 보여도 구현이 전혀 다르다. CPU 스로틀링은 쿠버네티스 기본 지표에 드러나지 않으므로 cpu.stat 을 직접 확인해야 한다. limit 을 끄면 최적화되지 않은 애플리케이션이 노드의 CPU 를 독차지할 수 있으니, JVM, Go, Node 런타임을 미리 튜닝해 둬야 한다."},"related":[],"sources":[{"url":"https://engineering.omio.com/cpu-limits-and-aggressive-throttling-in-kubernetes-c5b20bd8a718","kind":"blog","license":"unknown","archived":"http://web.archive.org/web/20250516051720/https://medium.com/omio-engineering/cpu-limits-and-aggressive-throttling-in-kubernetes-c5b20bd8a718"}],"summary":{"en":"Kubernetes implements CPU requests and CPU limits in entirely different ways, even though they look alike in the pod spec. A request becomes cpu.shares in the cgroup hierarchy under /sys/fs/cgroup/cpu,cpuacct, where 1024 shares stand for one core and the Linux CFS scheduler splits CPU in proportion to the shares, handing unused CPU back to a global pool. A limit becomes a CFS quota: cfs_period_us, always 100000 microseconds (100 ms) unless an alpha, feature-gated option changes it, and cfs_quota_us, the CPU time the container may use in each period. A limit of 2 cores becomes 200 ms per period. Omio's most common machine type has 16 cores. In the write-up's example, a container running ten parallel threads while the other pods are idle uses those 200 ms in 20 ms of wall time, and all its threads are then throttled for the remaining 80 ms. On top of that, a known kernel bug in the CFS quota code throttled containers unnecessarily and kept them from reaching even their allowed quota.\n\nOmio disabled CPU limits until the latest kernel with the bug fix was deployed across all its clusters; disabling CFS quota in the kubelet is named as the alternative. The HTTP 5xx rate and p95 latency of a critical service dropped immediately, and the team also tracked the number of instance hours used. The cost is that a container can now take all the CPU of a node. Omio judged this safe because its automated base Dockerfiles set sane runtime defaults for its main language stacks; with poorly tuned runtimes, such as an untuned JVM from a stock image, removing limits can backfire. The write-up recommends watching utilisation, saturation and errors along with API latency and error rates after the change.","ko":"Omio 는 상태 저장 워크로드와 상태 비저장 워크로드를 모두 GKE 에서 돌린다. 가장 흔한 머신 타입은 16코어다. CPU request 는 cpu.share 로 구현돼 스케줄링과 CPU 비례 배분에 쓰인다. CPU limit 은 CFS 쿼터의 두 값인 cfs_period_us 와 cfs_quota_us 로 구현한다. 둘은 설정 모양이 비슷하지만 구현은 전혀 다르고, limit 쪽은 문서도 부족해 오해하기 쉽다.\n\nCFS 쿼터는 CPU 성능이 아니라 시간 주기를 기준으로 한다. cfs_period_us 는 늘 100000us, 곧 100ms 다. limit 2코어는 200ms 쿼터가 되고, 병렬 스레드가 많으면 주기 초반에 쿼터를 다 써서 남은 시간 동안 멈춘다. 게다가 커널 스케줄러의 버그가 불필요한 스로틀링을 일으켰다. 그 결과 여러 애플리케이션이 예상보다 최대 50% 더 스로틀링됐고, 프로브 실패와 연결 끊김과 타임아웃으로 번졌다.\n\n팀은 버그를 고친 최신 커널이 모든 클러스터에 깔릴 때까지 CPU limit 을 껐다. 그러자 서비스의 HTTP 5xx 오류율이 곧바로 크게 줄었다. limit 을 끄면 컨테이너 하나가 머신의 CPU 를 모두 차지할 위험이 있다. Omio 는 주요 언어 스택마다 적절한 기본값을 담은 베이스 Dockerfile 을 자동으로 쓰고 있어서 이 위험이 문제가 되지 않았다."},"trigger":{"en":"Multi-threaded workloads running with CPU limits on nodes whose kernel did not yet have the fix for a known CFS quota bug that causes unnecessary throttling.","ko":"쿠버네티스는 CPU limit 을 커널 CFS 의 쿼터로 구현한다. 주기는 100ms 이고, limit 2코어는 주기마다 CPU 시간 200ms 가 된다. 스레드 10개가 동시에 돌면 20ms 만에 쿼터를 다 쓰고, 남은 80ms 동안 파드의 모든 스레드가 멈춘다. 여기에 커널 CFS 스케줄러의 알려진 버그가 불필요한 스로틀링을 일으켜, 컨테이너가 허용된 쿼터를 다 쓰기도 전에 스로틀링됐다."},"severity":"unknown","symptoms":["cpu-throttling","readiness-probe-failures","high-error-rate","high-latency","intermittent-timeouts","intermittent-connection-failures"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"Months of random stalls and failed health checks led the team to read the cgroup cpu.stat throttling counters inside the pods, which showed far more throttling than the limits should allow.","ko":"팀은 6개월 동안 애플리케이션이 멈추거나 헬스 체크에 응답하지 않는 현상을 겪었지만, 쿠버네티스가 cgroup 지표를 내보내지 않아 스로틀링을 바로 알아채지 못했다. 파드 안에서 /sys/fs/cgroup/cpu/cpu.stat 의 nr_throttled 와 throttled_time 을 읽어 스로틀링 비율을 확인했다."},"components":["cpu-limit","linux-kernel","kubelet"],"root_cause":"software-bug","remediation":[{"text":{"en":"removed CPU limits until the kernel with the CFS bug fix was deployed across all clusters","ko":"버그를 고친 최신 커널이 모든 클러스터에 배포될 때까지 CPU limit 을 껐다."}}],"action_items":[{"text":{"en":"Deploy the kernel with the CFS quota bug fix across all clusters","ko":"CFS 쿼터 버그를 고친 최신 커널을 모든 클러스터에 배포한다."}},{"text":{"en":"Monitor utilisation, saturation and error metrics, API latency and error rates to confirm the change behaves as expected","ko":"사용률, 포화도, 오류를 보는 USE 지표와 함께 API 지연과 오류율을 지켜보며 결과가 기대한 대로인지 확인한다."}}],"contributing":["limit-unknown","monitoring-blind-spot","known-bug-unpatched"],"recurrence_of":null,"symptoms_text":{"en":"For about six months, applications on Omio's Google Kubernetes Engine clusters stalled at random: they got stuck, stopped answering health check requests or lost network connections, and nothing the team looked at explained why.\n\nThe errors appeared as readiness probe failures, container stalls, network disconnections and timeouts in calls between services. On critical services this showed as a raised rate of HTTP 5xx responses and a higher p95 response time.\n\nThe usual Kubernetes metrics did not point at the cause. Kubernetes exposes CPU usage but not the cgroup throttling counters, and unlike a memory problem, which leaves an OOMKilled restart status, CPU throttling is hard to identify. The team read it inside the pods by running cat /sys/fs/cgroup/cpu/cpu.stat and reading nr_periods (scheduling periods elapsed), nr_throttled (periods in which the container was throttled) and throttled_time (total throttled time in nanoseconds). Several applications showed throttle rates up to 50% higher than the team had assumed their limits allowed.","ko":"Omio 는 6개월 동안 원인을 알 수 없는 멈춤을 겪었다. 애플리케이션이 멈추거나 헬스 체크에 응답하지 않았고, 네트워크 연결이 끊겼다.\n\n여러 애플리케이션의 스로틀링 비율이 높았다. limit 을 정할 때 예상한 것보다 최대 50% 더 스로틀링됐다. 이 현상은 readiness 프로브 실패, 컨테이너 멈춤, 네트워크 연결 끊김, 서비스 호출 사이의 타임아웃으로 번졌고, 결국 응답 지연과 오류율 상승으로 이어졌다.\n\n메모리 limit 문제는 파드의 마지막 재시작 상태가 OOMKilled 인지만 보면 알 수 있다. CPU 스로틀링은 쿠버네티스가 사용량 지표만 내보내고 cgroup 지표는 내보내지 않아 알아채기 어려웠다. 스로틀링은 파드 안에서 cat /sys/fs/cgroup/cpu/cpu.stat 을 실행해 확인했다. nr_periods 는 전체 스케줄 주기 수, nr_throttled 는 그중 스로틀링된 주기 수, throttled_time 은 스로틀링된 총 시간을 나노초로 보여 준다."},"updated_at":"2026-11-10T00:00:32.955534Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0012","org_slug":"omio"}