Як саме
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 відповідних контейнерів
- в Kubernetes manifest значення в
memory.max
: скільки пам’яті можна використати без ризику потрапити під Oout of Memory Killer- в Kubernetes manifest значення
resources.requests.memory
впливає тільки на Kubernetes Scheduler для вибору Kubernetes WorkerNode
- в Kubernetes manifest значення
cpu.weight
: визначає пріорітет групи процесів при навантаженому CPU- в Kubernetes manifest значення в
resources.requests.cpu
впливає на налаштуванняcpu.weight
для cgroup відповідних контейнерів
- в Kubernetes manifest значення в
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 на обох ядрах – або “одне ціле ядро”.
Корисні посилання
- How CPU Weight Is Calculated в блогах VictoriaMetrics
- Making Sense of Kubernetes CPU Requests And Limits
- Cgroups – Deep Dive into Resource Management in Kubernetes
- Resource Management for Pods and Containers
- cgroups (Arch Wiki)
- CPU and Memory Management on Kubernetes with Cgroupsv2