Kubernetes: Pod resources.requests, resources.limits та Linux cgroups

Автор |  02/07/2025

Як саме resources.requests та resources.limits в Kubernetes manifest працюють “під капотом”, і як саме Linux буде виділяти та обмежувати ресурси для контейнерів?

Отже, Kubernetes для Pod ми можемо задати два основні параметри для CPU та Memory – spec.containers.resources.requests та spec.containers.resources.limits:

  • resources.requests: впливає на те, як і де Pod буде створено, і скільки ресурсів гарантовано він отримає
  • resources.limits: впливає на те, скільки ресурсів максимум він може споживати
    • якщо resources.limits.memory більше ліміту – под може бути вбито з OOMKiller, якщо на WorkerNode недостатньо вільної пам’яті (Node memory pressure)
    • якщо resources.limits.cpu більше ліміту – то включиться режим CPU throttling

Якщо з Memory все доволі зрозуміло – задаємо кількість байт, то з CPU все трохи цікавіше.

Тож спершу давайте глянемо на те, як ядро Linux взагалі планує те, скільки CPU time буде приділятись кожному процесу завдяки механізму Control Groups.

Linux cgroups

Linux Control Groups (cgroups) – один з двох основних механізмів ядра, які забезпечують ізоляцію і контроль над процесами:

  • Linux namespaces: створюють ізольований простір імен з власним деревом процесів (PID Namespace), мережевими інтерфейсами (net namespace), User IDs (User namespace) і так далі – див. What is: Linux namespaces, примеры PID и Network namespaces
  • Linux cgroups: механізм контролю ресурсів процесами – скільки пам’яті, CPU, мережевих ресурсів та дискових I/O операцій буде доступно процесу

Groups в назві – бо всі процеси об’єднані в групи у вигляді дерева – parent-child tree.

Тому якщо для parent-процесу заданий ліміт в 512 мегабайт – то сума доступної пам’яті його і його потомків не може бути вища за 512 МБ.

Всі групи задані в каталозі /sys/fs/cgroup/, який підключається окремим типом файлової системи – cgroup2:

$ mount | grep cgro
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

cgroups є більш старої версії 1, та нової – 2, див. man cgroups.

По факту, cgroups v2 вже є новим стандартом, тому говорити будемо про неї – але cgroups v1 все ще присутній, коли ми говоримо про Kubernetes.

Перевірити версію можна за допомогою stat і каталога /sys/fs/cgroup/:

$ stat -fc %T /sys/fs/cgroup/
cgroup2fs

Якщо тут буде tmpfs – то це cgroups v1.

Каталог /sys/fs/cgroup/

Типовий вигляд директорії на Linux-хості – тут приклад з мого домашнього ноутбука з Arch Linux:

$ tree /sys/fs/cgroup/ -d -L 2
/sys/fs/cgroup/
├── dev-hugepages.mount
├── dev-mqueue.mount
├── init.scope
...
├── system.slice
│   ├── NetworkManager.service
│   ├── bluetooth.service
│   ├── bolt.service
    ...
└── user.slice
    └── user-1000.slice

Ту ж ієрархію можна побачити з systemctl status або systemd-cgls.

В systemctl status дерево виглядає так:

$ systemctl status
● setevoy-work
    State: running
    ...
    Since: Mon 2025-06-09 12:21:11 EEST; 3 weeks 1 day ago
  systemd: 257.6-1-arch
   CGroup: /
           ├─init.scope
           │ └─1 /sbin/init
           ├─system.slice
           │ ├─NetworkManager.service
           │ │ └─858 /usr/bin/NetworkManager --no-daemon
           ...
           │ └─wpa_supplicant.service
           │   └─1989 /usr/bin/wpa_supplicant -u -s -O /run/wpa_supplicant
           └─user.slice

Тут всі процеси згруповані по типам:

  • system.slice: всі systemd-сервіси (nginx.service, docker.service, і т.д.)
  • user.slice: user-процеси
  • machine.slice: віртуальні машини, контейнери

Де slice – це абстракція systemd, за якою він групує різні процеси – див. man systemd.slice

До якої саме cgroup належить процес можна побачити в його /proc/<PID>/cgroup, наприклад NetworkManager з PID “858”:

$ cat /proc/858/cgroup 
0::/system.slice/NetworkManager.service

Також cgroup slice може бути задана в systemd-файлі сервісу:

$ cat /usr/lib/systemd/system/[email protected] | grep Slice
Slice=system.slice

CPU та Memory в cgroups, та cgroups v1 vs cgroups v2

Отже, в cgroup для всього слайсу задаються параметри того, скільки CPU та Memory процеси цієї групи можуть використовувати (тут і далі будемо говорити тільки про CPU та Memory).

Наприклад, для мого юзера setevoy (з ID 1000) маємо файли cpu.max та memory.max:

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/cpu.max
max 100000

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/memory.max 
max

cpu.max в cgroups v2 замінив cpu.cfs_quota_us і cpu.cfs_period_us з cgroup v1

Тут в cpu.max маємо налаштування того, скільки часу CPU буде приділятись процесам мого юзера.

Формат файлу – <quota> <period>, де <quota> – це доступний процесу (чи групі) час, а <period> – тривалість одного періоду в мікросекундах (100.000 мкс = 100 мс).

В cgroups v1 ці значення задавались в cpu.cfs_quota – для <quota> в v2, та cpu.cfs_period_us – для <period> у v2.

Тобто в файлі вище бачимо:

  • max: доступний весь час
  • 100000 мкс = 100 мс, один CPU period

CPU period тут – це інтервал часу, протягом якого Linux ядро перевіряє, скільки процеси у cgroup використали CPU: якщо групі заданий ліміт (quota), і процеси його вичерпали – то вони будуть призупинені до завершення поточного period (CPU throttling).

Тобто якщо для процесу заданий ліміт в 50.000 (50 мс) при period в 100.000 мікросекунд (100 мс) – то процеси можуть використати тільки 50 мс у кожному 100 мс “вікні”.

Використання пам’яті можна побачити у файлі memory.current:

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/memory.current 
47336714240

Що дає нам:

$ echo "47336714240 / 1024 / 1024" | bc
45143

45 гігабайт зайнятої пам’яті процесами юзера 1000.

Також перевірити поточне використання ресурсів кожною групою можемо з systemd-cgtop:

Або передавши ім’я слайсу:

Для CPU є загальна статистика по групі від початку створення процесів в цій групі – cpu.stat:

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/cpu.stat
usage_usec 2863938974603
...

В Kubernetes показники cpu.max та memory.max будуть визначатись, коли ми задаємо resources.limits.cpu та resources.limits.memory.

Чому “Kubernetes CPU Limits – погана ідея”?

Дуже часто можна зустріти згадку про те, що задавати ліміти на CPU в Kubernetes – погана ідея.

Чому так?

Бо якщо ми задаємо ліміт (тобто значення != max в cpu.max) – то коли група процесів використає свій час в поточному вікні CPU Time – то ці процеси будуть обмежені навіть попри те, що загалом CPU має можливість виконати запити.

Тобто, навіть якщо в системі є вільні ядра, але cgroup уже вичерпала свій cpu.max у поточному періоді – процеси цієї групи будуть призупинені до завершення періоду (CPU throttling) незалежно від загального навантаження системи.

Див. For the Love of God, Stop Using CPU Limits on Kubernetes та Making Sense of Kubernetes CPU Requests And Limits.

Linux CFS та cpu.weight

Вище бачили cpu.max, де для мого юзера дозволяється використовувати весь доступний CPU час за кожен CPU-період.

Але якщо обмеження не встановлено (тобто max), і кілька груп процесів хочуть доступ до CPU одночасно – то ядро має вирішити кому виділити більше CPU time.

Для цього в cgroups задається ще один параметр – cpu.weight (у cgroups v2) або cpu.shares (у cgroups v1): це відносний пріоритет групи процесів при визначенні черги доступу до CPU.

Значення cpu.weight враховується Linux CFS (Completely Fair Scheduler), щоб розподілити CPU пропорційно між кількома cgroup. – див. CFS Scheduler та Process Scheduling in Linux.

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/cpu.weight
100

Діапазон значень тут – від 1 до 10.000, де 1 – мінімальний пріорітет, а 10.000 – максимальний. Значення 100 – дефолтне.

Чи пріорітет вищий – тим більше часу CFS буде виділяти процесам цієї групи.

Але це враховується тільки коли є гонка за часом CPU: коли процесор вільний, то всі процеси отримують стільки CPU time, скільки їм потрібно.

В Kubernetes показник cpu.weight буде визначатись із resources.requests.cpu.

А от значення resources.requests.memory впливає тільки на Kubernetes Scheduler для вибору Kubernetes WorkerNode для пошуку ноди, на якій є достатньо вільної пам’яті.

cpu.weight vs process nice

Окрім cpu.weight/cpu.shares маємо ще process nice, який задає пріорітет задачі.

Різниця між ними в тому, що cpu.weight задаються на рівні cgroup – а nice на рівні конкретного процесу всередині однієї групи.

І якщо більше значення в cpu.weight вказує на більший пріорітет, то з nice навпаки – чим нижче значення nice (від -19 до 20 максимум) – тим більше часу буде виділено процесу.

Якщо обидва процеси в одній cgroup, але з різним nice – то буде враховуватись nice.

А якщо це різні cgroup – то буде враховуватись саме cpu.weight.

Тобто, cpu.weight визначає яка група процесів важливіша для ядра, а nice – який саме процес в групі має пріорітет.

Linux cgroups Summary

Отже, кожна Control Group визначає те, скільки CPU та пам’яті буде виділятись процесу.

  • cpu.max: визначає скільки часу від кожного CPU period група процесів може витратити
    • в Kubernetes manifest значення в resources.limits.cpu та resources.limits.memory впливають на налаштування cpu.max та memory.max для cgroup відповідних контейнерів
  • memory.max: скільки пам’яті можна використати без ризику потрапити під Oout of Memory Killer
    • в Kubernetes manifest значення resources.requests.memory впливає тільки на Kubernetes Scheduler для вибору Kubernetes WorkerNode
  • cpu.weight: визначає пріорітет групи процесів при навантаженому CPU
    • в Kubernetes manifest значення в resources.requests.cpu впливає на налаштування cpu.weight для cgroup відповідних контейнерів

Kubernetes Pod resources та Linux cgroups

ОК, тепер, як розібрались з cgroups в Linux – давайте детальніше глянемо на те, як значення в Kubernetes resources.requests та resources.limits впливають на контейнери.

Коли ми задаємо spec.container.resources в Deployment чи Pod, і Pod створюються на WorkerNode – то kubelet на цій ноді отримує значення зі PodSpec, і передає їх до Container Runtime Interface (CRI) (ContainerD чи CRI-O).

CRI перетворює їх в специфікацію контейнера в JSON, в якому вказує відповідні значення для cgroup цього контейнеру.

Kubernetes CPU Unit vs cgroup CPU share

В маніфестах Kubernetes ресурси CPU ми задаємо в CPU units: 1 юніт == 1 повне CPU ядро – фізичне чи віртуальне, див. CPU resource units.

1 millicpu або millicores – це 1/1000 від одного ядра CPU.

Один Kubernetes CPU Unit – це 1024 CPU shares у відповідній Linux cgroup.

Тобто: 1 Kubernetes CPU Unit == 1000 millicpu == 1024 CPU shares у cgroup.

Крім того, є нюанс з тим, як саме Kubernetes рахує cpu.weight для Pods – бо Kubernetes використовує CPU shares, які потім переводить в cpu.weight для cgroup v2 – далі побачимо, як це виглядає.

Перевірка Kubernetes Pod resources в cgroup

Створимо тестовий Pod:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-test
  namespace: default
spec:
  containers:
    - name: nginx
      image: nginx
      resources:
        requests:
          cpu: "1"
          memory: "1Gi"
        limits:
          cpu: "1"
          memory: "1Gi"

Запускаємо його, і знаходимо відповідну WorkerNode:

$ kk describe pod nginx-test
Name:             nginx-test
Namespace:        default
Priority:         0
Service Account:  default
Node:             ip-10-0-32-142.ec2.internal/10.0.32.142
...

Підключаємось по SSH, і глянемо на параметри cgroups.

Kubernetes kubepods.slice cgroup

Всі параметри для Kubernetes Pods задані в каталозі /sys/fs/cgroup/kubepods.slice/:

[root@ip-10-0-32-142 ec2-user]# ls -l /sys/fs/cgroup/kubepods.slice/
...
drwxr-xr-x. 5 root root 0 Jul  2 12:30 kubepods-besteffort.slice
drwxr-xr-x. 6 root root 0 Jul  2 12:30 kubepods-burstable.slice
drwxr-xr-x. 4 root root 0 Jul  2 12:31 kubepods-pod32075da9_3540_4960_8677_e3837e04d69f.slice
...

Аби знайти який сам cgroup slice відповідає за наш контейнер – перевіримо запущені поди в namespace k8s.io:

[root@ip-10-0-32-142 ec2-user]# ctr -n k8s.io containers ls
CONTAINER                                                           IMAGE                                                                                             RUNTIME                  
00d432ee10181ce579af7f0d02a3a04167ced45f8438167f3922e385ed9ab58f    602401143452.dkr.ecr.us-east-1.amazonaws.com/eks/eks-pod-identity-agent:v0.1.29                   io.containerd.runc.v2    
...
987bb39fa50532a89842fe1b7a21d1a5829cdf10949a11ac2d4f30ce4afcca2f    docker.io/library/nginx:latest                                                                    io.containerd.runc.v2
...

Note: namespace в ctr – це неймспейси самого containerd, а не Linux, див. containerd namespaces for Docker, Kubernetes, and beyond

Наш контейнер – “987bb39fa50532a89842fe1b7a21d1a5829cdf10949a11ac2d4f30ce4afcca2f“.

Перевіряємо всю інформацію по ньому:

[root@ip-10-0-32-142 ec2-user]# ctr -n k8s.io containers info 987bb39fa50532a89842fe1b7a21d1a5829cdf10949a11ac2d4f30ce4afcca2f
...
        "linux": {
            "resources": {
                "devices": [
                    {
                        "allow": false,
                        "access": "rwm"
                    }
                ],
                "memory": {
                    "limit": 1073741824,
                    "swap": 1073741824
                },
                "cpu": {
                    "shares": 1024,
                    "quota": 100000,
                    "period": 100000
                },
                "unified": {
                    "memory.oom.group": "1",
                    "memory.swap.max": "0"
                }
            },
            "cgroupsPath": "kubepods-pod32075da9_3540_4960_8677_e3837e04d69f.slice:cri-containerd:987bb39fa50532a89842fe1b7a21d1a5829cdf10949a11ac2d4f30ce4afcca2f",
...

Тут ми бачимо resources.memory та resources.cpu.

З memory все ясно, а в resources.cpu маємо три поля:

  • shares: це наші requests із Pod manifest (PodSpec)
  • quota: це наші limits
  • period: CPU період, про який говорили вище – “вікно обліку” для CFS

В cgroupsPath бачимо який саме cgroup slice містить інформацію по цьому контейнеру:

[root@ip-10-0-32-142 ec2-user]# ls -l /sys/fs/cgroup/kubepods.slice/kubepods-pod32075da9_3540_4960_8677_e3837e04d69f.slice/cri-containerd-987bb39fa50532a89842fe1b7a21d1a5829cdf10949a11ac2d4f30ce4afcca2f.scope/
...
-rw-r--r--. 1 root root 0 Jul  2 12:31 cpu.idle
-rw-r--r--. 1 root root 0 Jul  2 12:31 cpu.max
...
-rw-r--r--. 1 root root 0 Jul  2 12:31 cpu.weight
...
-rw-r--r--. 1 root root 0 Jul  2 12:31 memory.max
...

І відповідні значення в них:

[root@ip-10-0-32-142 ec2-user]# cat /sys/fs/cgroup/kubepods.slice/kubepods-pod[...]cca2f.scope/cpu.max
100000 100000

Тобто максимум 100.000 мікросекунд від кожного вікна у 100.000 мікросекунд – бо ми задали resources.limits.cpu == “1”, тобто “повне ядро”.

Kubernetes, cpu.weight та cgroups v2

А от якщо ми поглянемо на файл cpu.weight – то тут картина буде така:

[root@ip-10-0-32-142 ec2-user]# cat /sys/fs/cgroup/kubepods.slice/kubepods-pod[...]cca2f.scope/cpu.weight
39

Звідки взялось значення “39”?

В опису контейнера ми бачили shares == 1024:

...
        "linux": {
            "resources": {
                ...
                "cpu": {
                    "shares": 1024,
...

cpu.shares 1024 – це значення, яке ми задали в Kubernetes, коли вказали resources.requests.cpu == “1”, бо, як говорилось вище – “Один Kubernetes CPU Unit – це 1024 CPU shares”.

Тобто, для cgroups v1 – у файлі cpu.shares ми б мали значення 1024.

Але з cgroup v 2 трохи цікавіше.

“Під капотом” в Kubernetes все одно рахується CPU Shares у форматі 1 ядро == 1024 shares, які потім транслюються в формат cgroups v2.

Якщо ми глянемо загальний cpu.weights для всього слайсу kubepods.slice – то там буде значення 76:

[root@ip-10-0-32-142 ec2-user]# cat /sys/fs/cgroup/kubepods.slice/cpu.weight
76

А звідки взялося “76”?

Перевіряємо скільки ядер на цьому інстансі:

[root@ip-10-0-32-142 ec2-user]# lscpu | grep -E '^CPU\('
CPU(s):                                  2

Формула, за якою рахується cpu.weight описана у файлі group_manager_linux.go#L566:

...
func CpuSharesToCpuWeight(cpuShares uint64) uint64 {
  return uint64((((cpuShares - 2) * 9999) / 262142) + 1)
}
...

Маючи 2 ядра == 2048 CPU shares для v1 – рахуємо:

((((2048 - 2) * 9999) / 262142) + 1)
79

Тобто на весь kubepods.slice визначається “вага” в 79 cpu.weight.

Але це ми рахували взагалі всі CPU shares – а на ділі частина CPU резервується під систему і контролери типу kubelet.

Kubernetes Quality of Service Classes

Див. Kubernetes: Evicted поды и Quality of Service для подов та Pod Quality of Service Classes.

В каталозі /sys/fs/cgroup/kubepods.slice/ ми маємо:

  • kubepods-besteffort.slice: BestEffort QoS: якщо requests та limits задані, але limits менші за requests
  • kubepods-burstable.slice: для Burstable QoS – коли задані тільки requests
  • kubepods-pod32075da9_3540_4960_8677_e3837e04d69f.slice: Quarateed QoS – коли requests та limits задані, і дорівнюють один одному

Наш Pod – саме Quarateed QoS:

$ kk describe pod nginx-test | grep QoS
QoS Class:                   Guaranteed

Бо ми задали requests == limits.

А так як ми задали 1 повне ядро в requests – то Kubernetes через cgroups виділяє йому половину всіє cpu.weight доступної для kubepods.slice:

38*2
76

Саме те значення, яке ми бачили в kubepods.slice/cpu.weight.

І тому Linux CFS зажди буде видавати нашому контейнеру половину всього доступного CPU time на обох ядрах – або “одне ціле ядро”.

Корисні посилання