Контейнери проти віртуальних машин: який профіль CPU підходить для чого

Було корисно?

Ви не обираєте «контейнери» або «ВМ» абстрактно. Ви обираєте їх у той момент, коли вам дзвонять унаслідок того, що p99-латентність зросла вдвічі,
пакетна задача стала в 3× повільнішою або «дрібне» оновлення вузла перетворилося на тиждень історії звинувачень.

CPU — це те, де це рішення стає політичним. Усі думають, що розуміють CPU. Потім зустрічаються з throttling, steal time, NUMA
і неприємною правдою: планувальник не переймається вашими спринт-зобов’язаннями.

Профілі CPU, які справді мають значення

«CPU-bound» — це недостатній опис. Вибір між контейнерами й ВМ залежить від форми попиту на CPU і від штрафів, які ви готові платити: джиттер, хвостова латентність, накладні витрати на контекстні переключення, руйнування кешу, поведінка при превенції та наскільки ви довіряєте навколишнім навантаженням.

Профіль 1: Чутливі до хвостової латентності (p99/p999 — це продукт)

Приклад: трейдинг, торги рекламними запитами, OLTP, request/response-сервіси зі строгими SLO або будь-що, де один повільний ядро псує вам день.
Такі навантаження ненавидять:

  • CPU throttling (cgroup квоти) та сплески в плануванні.
  • Непередбачувана превенція від інших навантажень (шумні сусіди).
  • NUMA-доступ до пам’яті між сокетами та промахи кешу.
  • Штормові переривання, що падають на «ваші» ядра.

Зазвичай вони хочуть гарантованого CPU часу, ізоляції ядер і стабільного розміщення. Контейнери можуть це забезпечити, але лише якщо ви
дійсно це налаштуєте. ВМ теж можуть, але лише якщо перестанете вірити, що overcommit — це безкоштовний бонус.

Профіль 2: Пакетні задачі, орієнтовані на пропускну здатність (закінчити до ранку, джиттер не важливий)

Приклад: ETL, кодування відео, індексування, генерація ознак для ML. Цим навантаженням важливі сумарні CPU-секунди і масштабування.
Вони терплять джиттер. Вони не терплять, коли їм рахують просто простої ядер.

Тут часто перемагають контейнери: їх можна щільніше пакувати, швидко масштабувати і дозволити планувальнику робити свою роботу. ВМ конкурують,
якщо потрібні явні межі орендарів або інші ядра/ядро, але ви заплатите за щільність і оперативну фрикцію.

Профіль 3: Спайкові інтерактивні (різкі навантаження, люди в циклі)

Приклад: CI-runner’и, середовища попереднього огляду для розробників, внутрішні дашборди з піками під час деплоїв. Ці системи хочуть швидкого старту й еластичності.
Операційно контейнери зазвичай виграють. По CPU — вони виграють, якщо ви не «оптимізуєте» встановленням агресивних CPU-лімітів, що перетворюють піки на throttled misery.

Профіль 4: Змішаний режим «все нормально, поки не стало не нормально» (спільні хости, багато сервісів)

Це корпоративний дефолт: багато дрібних сервісів, кілька середніх і один таємничий процес, за який ніхто не відповідає.
Конкуренція за CPU тут — це проблема управління, замаскована під технічну.

Контейнери дають тонший контроль і кращу щільність, але й більше способів випадково створити contention. ВМ дають товстіший мур, що приємно, поки ви не зрозумієте, що фізичний CPU все одно спільний і мур не звукоізольований.

Профіль 5: Спеціальна поведінка CPU (реального часу, DPDK, HPC-подібне)

Якщо вам потрібне real-time планування, передбачувана маршрутизація переривань або user-space мережа, що вимагає виділених ядер — це категорія «перестаньте гадати».
І контейнери, і ВМ можуть працювати, але, ймовірно, доведеться робити CPU pinning, isolcpus, hugepages і явне NUMA-розміщення. Тоді проблема вже не «контейнери чи ВМ».
Питання в тому, чи може ваша платформа підтримувати детерміноване тонке налаштування без зламу всього іншого.

Ментальна модель CPU: що насправді робить ядро і гіпервізор

Контейнери — це не маленькі ВМ. Це процеси з додатковими правилами: namespaces, щоб приховати, що вони бачать, і cgroups, щоб обмежити, що вони можуть використовувати. Планувальник CPU залишається планувальником хоста в ядрі.

ВМ запускають гість-ядро на віртуальних CPU (vCPU). Ці vCPU потім плануються на фізичні CPU гіпервізором/ядром хоста.
Отже, у вас є два планувальники: гостьовий планує потоки на vCPU, а хост планує vCPU на pCPU.
Ця подвійність — одночасно особливість (ізоляція) і джерело дивностей (steal time, взаємне впливання планування).

CPU-час: три великі метрики, що вирішують вашу долю

  • Usage: скільки CPU фактично споживає ваше навантаження.
  • Wait: час, коли процес runnable, але не працює (черги, contention).
  • Jitter: дисперсія латентності через планування, throttling, переривання і ефекти кешу.

Контейнери зазвичай мінімізують накладні витрати й максимізують щільність, але підвищують ймовірність «wait» і «jitter», якщо ви не дисциплінуєте керування ресурсами.

ВМ додають шар накладних витрат і складність планування, але можуть зменшити зв’язування між орендарями при правильній конфігурації. Ключова фраза — «при правильній конфігурації». Більшість систем такої не мають.

Квоти vs shares: як контейнери отримують «менше CPU», а ви цього не помічаєте

У cgroup v2 контроль CPU зазвичай складається з:

  • weights (відносний пріоритет під час contention), та
  • max (жорстка межа, яка може викликати throttling навіть коли є вільний CPU в інших місцях, залежно від конфігурації та поведінки burst).

Класичний режим відмови: ви ставите CPU-ліміти «заради справедливості», потім ваш latency-чутливий сервіс досягає ліміту під час сплеску,
його тротлять і p99 різко падає, тоді як середній CPU виглядає «нормальним».

Steal time: як ВМ каже вам «хтось інший вкрав мій час»

У віртуалізованому оточенні steal — це CPU-час, який гість хотів використати, але не отримав, бо хост запустив щось інше.
Високе steal — це еквівалент стояння у довгій черзі за кавою перед початком вашої зустрічі.

NUMA: податок, який ви платите, коли пам’ять «там»

На мульти-сокетних системах CPU і пам’ять організовані у NUMA-вузли. Доступ до локальної пам’яті швидший, ніж до віддалої.
Коли ви плануєте потоки на одному сокеті, а їхня пам’ять живе на іншому, ви отримаєте продуктивність, що відчувається як втрата пакетів: спорадично,
важко відтворювана і завжди спочатку звинувачують «мережу».

Поведение кешу і контекстні переключення: прихований рахунок

Якщо ви пакуєте багато контейнерів на хості, ви підвищуєте контекстні переключення й тиск на кеш. Це не завжди погано.
Погано, коли ви очікуєте хвилинної хвостової латентності і ставитеся до CPU як до безкінечно подільного ресурсу.

Перефразована ідея від Werner Vogels (CTO Amazon): Build it, run it; володіння покращує надійність.
Це також застосовується тут: команда, що встановлює політику CPU, має відчувати пейджер.

Хто перемагає коли: контейнери проти ВМ за профілем CPU

Сервіси, чутливі до латентності: зазвичай ВМ для жорсткої ізоляції, контейнери — для дисциплінованих платформ

Якщо ви керуєте мульти-орендною платформою, де команди не координуються, ВМ — безпечніший дефолт для latency-чутливих сервісів.
Не тому, що ВМ — це магія, а тому, що межі важче випадково обійти. Контейнер без CPU-лімітів і без ізоляції може зіпсувати весь вузол; ВМ теж можуть, але це вимагає іншого типу неправильних налаштувань.

Якщо у вас дисциплінована Kubernetes-платформа з:

  • Guaranteed QoS pods (requests == limits for CPU),
  • static CPU manager policy для pinned ядер там, де потрібно,
  • NUMA-aware scheduling,
  • налаштування IRQ affinity на гарячих шляхах,

…тоді контейнери можуть забезпечити відмінну хвостову латентність із більшою щільністю, ніж ВМ. Але це не «дефолтний Kubernetes». Це
«Kubernetes після того, як ви прочитали дрібний шрифт і заплатили податок».

Високопродуктивні stateless-обчислення: контейнери перемагають за щільністю й швидкістю операцій

Для stateless, горизонтально масштабованих обчислень, де джиттер прийнятний, контейнери — кращий інструмент. Накладні витрати нижчі,
планування простіше і кращий bin packing. Також: rolling updates, autoscaling і швидкі відкатування легше виконувати, не тримаючи гіпервізорний фліт як сакральний об’єкт.

Легасі-додатки та інші ядра: ВМ перемагають, і це навіть не близько

Якщо вам потрібне інше ядро, kernel modules або ви запускаєте софт, який вважає, що володіє світом (привіт, старі ліцензійні системи), використовуйте ВМ. Контейнери діляться ядром хоста; якщо ядро — це шар сумісності, який ви маєте контролювати, контейнери — неправильна абстракція.

Ризик шумного сусіда: ВМ зменшують радіус вибуху; контейнери потребують політики та примусу

І контейнери, і ВМ ділять один фізичний CPU. Різниця в тому, скільки шарів треба зіпсувати, перш ніж одне навантаження зашкодить іншому.

  • У контейнерах планувальник ядра безпосередньо спільний. Помилки cgroup показують себе негайно.
  • У ВМ неправильний розмір vCPU, overcommit і contention на хості проявляються як steal time і джиттер.

Коли «майже bare metal» має значення: контейнери мають менше накладних витрат, але ВМ можна налаштувати

Контейнери зазвичай мають менше накладних витрат по CPU, бо немає другого ядра і емульованих пристроїв (за умови, що ви не робите щось креативне з мережею). ВМ можуть бути дуже близькі до нативу з апаратною віртуалізацією і paravirtualized драйверами, але ви все одно платите за exits, interrupts і індирекцію планування.

Практична порада: якщо ви женетеся за останніми 5–10% у CPU-важких навантаженнях — вимірюйте. Не сперечайтеся.

Факти та історія, що пояснюють сучасні дивності

  1. 1970-ті: віртуалізація — не новина. IBM mainframe запускали віртуальні машини десятиліттями до того, як хмара стала трендом; ідея була в ізоляції й кращому використанні ресурсів.
  2. 2006: AWS популяризувала «орендуй ВМ». EC2 зробив VM-центровані операції дефолтною ментальною моделлю для покоління інженерів.
  3. 2007–2008: з’явилися Linux cgroups. Control groups дали ядру спосіб рахувати й обмежувати CPU/пам’ять по групах процесів — контейнери пізніше підхопили цю хвилю.
  4. 2008: LXC зробив «контейнери» відчутними. До Docker, LXC уже поєднував namespaces+cgroups; просто не мав того UX і історії розповсюдження.
  5. 2013: Docker зробив пакування заразним. Вбивчою фішкою було не ізолювання; а доставка додатку з його залежностями повторювано.
  6. 2014: Kubernetes перетворив планування на продукт. Він нормалізував ідею, що платформа виділяє CPU, а не команда додатку просить ВМ.
  7. 2015+: епоха Spectre/Meltdown ускладнила розмови про накладні витрати. Деякі міри впливали на syscall-heavy і virtualization-heavy шляхи; обговорення продуктивності стало більш нюансованим.
  8. cgroup v2 уніфікував семантику контролю. Це зменшило частину спадкових дивностей, але ввело нові регулятори, які люди неправильно читають, особливо навколо CPU.max і burst-поведінки.
  9. Сучасні CPU — це не «просто ядра». Turbo, frequency scaling, SMT/Hyper-Threading і спільні кеші роблять ізоляцію CPU ймовірнісною задачею, якщо ви не робите pinning і ізоляцію.

Швидкий playbook діагностики

Коли CPU-продуктивність йде не туди, ваша перша задача — з’ясувати, чи ви маєте справу з незадостатнім CPU,
невиходом на планування або занадто великою роботою на запит. Все інше — декорація.

По-перше: підтвердіть симптом і масштаб

  • Чи зросла латентність, знизилась пропускна здатність або й те, й інше?
  • Чи це один pod/VM, один вузол, одна зона доступності або всюди?
  • Почалося після деплойменту, масштабування, перезапуску вузла, оновлення ядра чи зміни типу хоста?

По-друге: вирішіть, це throttling/steal чи справжня насиченість

  • Контейнери: перевірте лічильники throttling у cgroup та конфігурацію CPU.max/CPU quota.
  • ВМ: перевірте steal time і contention на рівні хоста.
  • Обидва: перевірте довжину run queue і частоту контекстних переключень.

По-третє: перевірте патології розміщення (NUMA, pinning, переривання)

  • NUMA-неврівноваженість або доступ до пам’яті між вузлами.
  • CPU-піновані навантаження, що ділять sibling hyperthreads.
  • IRQs, що падають на ті ж ядра, що й ваші latency-чутливі потоки.

По-четверте: валідовуйте частоту та енергоменеджмент

  • Неочікувано низька частота CPU через power governor або thermal throttling.
  • Turbo-поведінка, що змінилася після BIOS/firmware оновлень.

По-п’яте: лише тоді дивіться на «неефективність застосунку»

Якщо ви починаєте з flamegraphs, коли ядро буквально тротлить ваш процес, ви робите археологію у темряві.

Практичні завдання: команди, виходи та рішення (12+)

Ось перевірки, які ви можете виконати сьогодні. У кожному: команда, що означає типовий вихід, і яке рішення приймати далі.
Запускайте їх на вузлі/хості, а потім всередині контейнера або ВМ за потреби. Не покладайтеся лише на дашборди; там нюанси вмирають.

Завдання 1: Переконайтеся, чи хост CPU-сатурований (run queue і load vs кількість CPU)

cr0x@server:~$ nproc
32
cr0x@server:~$ uptime
 15:42:10 up 12 days,  3:17,  2 users,  load average: 48.12, 44.90, 39.77

Значення: Load average ~48 на 32-ядерному хості свідчить про сильне наростання runnable-черги (або багато непреривного сну; перевірте наступні завдання).
Якщо p99 поганий — це тривожний знак.

Рішення: Якщо load > cores протягом тривалого часу, припиніть сперечатися про мікрооптимізації. Знизьте contention: масштабуйтесь, зменшуйте колокацію або підвищте виділення CPU.

Завдання 2: Перевірте розклад CPU та steal time (підказка для ВМ)

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (node-7) 	01/12/2026 	_x86_64_	(32 CPU)

15:42:21     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %idle
15:42:22     all   62.10    0.00   10.43    0.12    0.00    0.55   18.34    8.46
15:42:23     all   63.02    0.00   10.21    0.09    0.00    0.61   17.80    8.27
15:42:24     all   61.77    0.00   10.66    0.14    0.00    0.59   18.02    8.82

Значення: 18% steal — це величезно. У ВМ це означає, що гіпервізор не планує ваші vCPU.

Рішення: Не налаштовуйте застосунок поки що. Перемістіть ВМ, зменшіть overcommit хоста або узгодьте зарезервовану ємність. Якщо не можете — прийміть гіршу хвостову латентність: це фізика з рахунками.

Завдання 3: Перевірте по-процесний CPU і тиск контекстних переключень

cr0x@server:~$ pidstat -w -u 1 3
Linux 6.5.0 (node-7) 	01/12/2026 	_x86_64_	(32 CPU)

15:43:10   UID       PID    %usr %system  %CPU   CPU  Command
15:43:11  1001     24811   310.0    42.0 352.0    7  java
15:43:11  1001     24811  12000.0  800.0          -  cswch/s nvcswch/s
15:43:11  1001     24811  12000.0   650.0         -  java

Значення: Надзвичайно висока частота контекстних переключень зазвичай означає сильну contention потоків, churn локів або переповнення.

Рішення: Якщо це контейнер на спільному вузлі — подумайте про pinning і зменшення співмешкання. Якщо це ВМ — перевірте кількість vCPU стосовно потоків і перевірте планування на хості.

Завдання 4: Для контейнерів — перевірте CPU-ліміти cgroup v2 (CPU.max)

cr0x@server:~$ cat /sys/fs/cgroup/cpu.max
200000 100000

Значення: Це означає квоту 200ms CPU на 100ms період (ефективно 2 ядра). Якщо навантаження перевищить це, його буде тротлено.

Рішення: Для latency-чутливих сервісів уникайте жорстких CPU-капів, якщо ви не навмисно не вводите передбачувану поведінку. Віддавайте перевагу requests/Guaranteed QoS і виділеним ядрам.

Завдання 5: Для контейнерів — перевірте лічильники throttling

cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 12888904512
user_usec  12110000000
system_usec 778904512
nr_periods  934112
nr_throttled 212334
throttled_usec 9901123456

Значення: Якщо nr_throttled велике і throttled_usec нетривіальне, ваш контейнер примусово паузиться ядром через квоти.

Рішення: Якщо вам важливий p99, або підніміть/видаліть ліміт, або переробіть патерн сплеску (наприклад, контроль конкурентності). Throttling — машина для латентності.

Завдання 6: У Kubernetes — підтвердіть QoS клас (Guaranteed vs Burstable)

cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{.status.qosClass}{"\n"}'
Burstable

Значення: Burstable-поди можуть бути депріоритизовані під час contention і частіше зазнають варіацій CPU-часу.

Рішення: Для latency-критичних сервісів прагніть до Guaranteed (set CPU requests equal to limits) або використовуйте виділені вузли без CPU-лімітів, але зі строгою admission control.

Завдання 7: Перевірте CPU requests/limits у Kubernetes і знайдіть «limit встановлено, request мізерний»

cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{range .spec.containers[*]}{.name}{" req="}{.resources.requests.cpu}{" lim="}{.resources.limits.cpu}{"\n"}{end}'
api req=100m lim=2000m

Значення: Планувальник думає, що вам потрібно 0.1 ядра, але ви можете сплескнути до 2 ядер. Під час contention ви втратите пріоритет планування і отримаєте джиттер.

Рішення: Для стабільної латентності встановлюйте реалістичні requests. Для економії — не обманюйте планувальник, а потім не скаржтеся на результат.

Завдання 8: Перевірте політику cpu manager на вузлі Kubernetes (можливість pinning)

cr0x@server:~$ ps -ef | grep -E 'kubelet.*cpu-manager-policy' | head -n 1
root     1123     1  1 Jan10 ?        00:12:44 /usr/bin/kubelet --cpu-manager-policy=static --kube-reserved=cpu=500m --system-reserved=cpu=500m

Значення: Політика static дозволяє виділити виключні CPU для Guaranteed pod-ів (з цілими запитами CPU).

Рішення: Якщо вам потрібна стабільна хвостова латентність, розгляньте вузли зі static cpu manager і topology manager. Без цього ви граєте в азарт.

Завдання 9: Визначте NUMA-топологію і чи ваше навантаження перетинає вузли

cr0x@server:~$ lscpu | egrep 'NUMA node|Socket|CPU\(s\)|Thread|Core'
CPU(s):                               32
Thread(s) per core:                   2
Core(s) per socket:                   8
Socket(s):                            2
NUMA node(s):                         2
NUMA node0 CPU(s):                    0-15
NUMA node1 CPU(s):                    16-31

Значення: Два NUMA-вузли. Якщо процес стрибає по CPU 0–31 і алокує пам’ять вільно, ви можете платити штрафи за віддалену пам’ять.

Рішення: Для CPU-латентних сервісів тримайте CPU і пам’ять локальними: пінуйте CPU, використовуйте NUMA-aware scheduling або запускайте менші інстанси, що вміщуються в один вузол.

Завдання 10: Перевірте, на яких CPU процесу дозволено працювати (cpuset / affinity)

cr0x@server:~$ taskset -pc 24811
pid 24811's current affinity list: 0-31

Значення: Процес може працювати будь-де. Це максимізує гнучкість, але може підвищити churn кешу й NUMA-ефекти.

Рішення: Якщо ви бачите джиттер і крос-NUMA проблеми — розгляньте звуження affinity (або використовуйте можливості оркестратора для безпечного виконання). Якщо мета — throughput, лишайте гнучкість.

Завдання 11: Перевірте розподіл переривань (гарячі точки IRQ affinity)

cr0x@server:~$ cat /proc/interrupts | head -n 8
            CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
  24:   98122321          0          0          0    1022334          0          0          0   PCI-MSI 524288-edge      eth0-TxRx-0
  25:          0   93455321          0          0          0     993442          0          0   PCI-MSI 524289-edge      eth0-TxRx-1
  26:          0          0   90211233          0          0          0     889120          0   PCI-MSI 524290-edge      eth0-TxRx-2
  27:          0          0          0   88777210          0          0          0     901223   PCI-MSI 524291-edge      eth0-TxRx-3
 NMI:    1223311    1219988    1213444    1209987    1198877    1190021    1189911    1180221   Non-maskable interrupts

Значення: Мережеві переривання сконцентровані на конкретних CPU. Якщо ваш latency-чутливий процес ділить ці CPU, ви побачите джиттер.

Рішення: Налаштуйте IRQ affinity або ізолюйте CPU для навантаження. Не «вирішуйте» хвостову латентність додаванням retry; саме так виникають персоналізовані аварії.

Завдання 12: Перевірте governor частоти і поточний MHz (мовчазний вбивця продуктивності)

cr0x@server:~$ cpupower frequency-info | egrep 'governor|current CPU frequency' | head -n 6
  The governor "powersave" may decide which speed to use
  current CPU frequency: 1200 MHz (asserted by call to hardware)

Значення: powersave governor на 1.2GHz на сервері, який має працювати гарячо — це… неідеально.

Рішення: Переключіть на performance governor для latency-вузлів або хоча б валідовуйте налаштування firmware/power. Заміряйте економію енергії перед тим, як продавати ідею.

Завдання 13: У ВМ — зіставте steal з contention на хості (вид із гостя)

cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (vm-12) 	01/12/2026 	_x86_64_	(8 CPU)

15:45:01     CPU     %user     %nice   %system   %iowait    %steal     %idle
15:45:02     all     52.11      0.00      9.88      0.10     22.34     15.57
15:45:03     all     50.90      0.00     10.12      0.08     21.77     17.13
15:45:04     all     51.33      0.00      9.94      0.09     22.01     16.63

Значення: Steal >20% означає, що ваша ВМ регулярно не планується. Гість бачить «idle» також, але це не справжня проста.

Рішення: Не додавайте vCPU рефлекторно; це часто погіршує планування. Натомість зменшіть overcommit, перемістіть на інший хост або використайте reserved instances/CPU pinning на гіпервізорі.

Завдання 14: Знайдіть contention між sibling-ами гіперпотоків (SMT/HT awareness)

cr0x@server:~$ for c in 0 1 2 3; do echo -n "cpu$c siblings: "; cat /sys/devices/system/cpu/cpu$c/topology/thread_siblings_list; done
cpu0 siblings: 0,16
cpu1 siblings: 1,17
cpu2 siblings: 2,18
cpu3 siblings: 3,19

Значення: CPU0 ділить фізичне ядро з CPU16 і т.д. Якщо ви пінуєте два галасливі навантаження на sibling-потоки, чекайте несподіванок у продуктивності.

Рішення: Для latency-чутливої роботи надавайте перевагу одному потоку на ядро (уникайте спільного sibling) або вимикайте SMT на виділених вузлах, якщо можете собі дозволити втрату ємності.

Жарт #1: CPU-ліміти — як офісні бюджети: усі відчувають «дисципліну», поки не станеться перший інцидент, тоді раптом ліміти стають «лише рекомендацією».

Три корпоративні міні-історії з CPU-траншей

Міні-історія 1: Інцидент, спричинений хибним припущенням (контейнери «не мають накладних витрат»)

Середня SaaS-компанія перемістила latency-чутливе API з ВМ у Kubernetes. Меседж був простий: «контейнери легші, отже отримаємо кращу продуктивність і більшу щільність». Міграція пройшла швидко — і це мало бути першим знаком.

API було деплойнуто з CPU-лімітами, щоб «запобігти шумним сусідам». Запити були маленькі — 100m — бо сервіс мало використовував CPU в середньому. Ліміти були 2 ядра, що звучало щедро. При нормальному трафіку все виглядало добре. Потім вони запустили маркетингову кампанію і трафік підскочив. Сервіс масштабувався, але кожен pod досягав своєї CPU-квоти під час сплесків.

Графіки показували використання CPU нижче ємності вузла, тому перша реакція була оптимізувати код і звинуватити базу даних. Тим часом ядро пасткично тротлило найгарячіші pod-и. p99 подвоївся, потім потроївся. Пішли таймаути. Autoscaler злякався і додав більше pod-ів, що підсилювало contention і створило петлю погіршення.

Реальне виправлення було нудним: прибрати CPU-ліміти для цього сервісу на виділених вузлах, встановити реалістичні CPU-requests і використати static CPU manager для pinned ядер. Також вони ввели guardrails в admission control, щоб «100m requests» не просочувались у продакшн для критичних сервісів.

Хибне припущення було не в тому, що «контейнери швидші». Контейнери можуть бути швидкими. Хибне припущення було в тому, що середній CPU — релевантна метрика для спайкового сервісу зі строгими хвостовими SLO.

Міні-історія 2: Оптимізація, що відкотилася назад (overprovisioning vCPU)

Команда підприємства тримала VM-платформу для внутрішніх сервісів. На них тиснули скоротити кількість VM. Хтось запропонував «right-sizing», додавши більше vCPU у VM, щоб одна VM вміщувала більше воркер-потоків і зменшити кількість інстансів.

На папері виглядало ефективно: менше VM, менше менеджменту, краща утилізація. На практиці кластер гіпервізора вже був помірно overcommitted. Збільшення vCPU зробило кожну VM важчою для планування на зайнятих хостах. Steal time зріс, але лише у пікові години.

Найгірше: команда додала vCPU знову, вважаючи, що застосунок голодує. Це збільшило runnable-навантаження і зробило планування ще хаотичнішим. Хвостова латентність погіршилася в кількох сервісах, не лише в «оптимізованому».

Вони відкотили зміни vCPU, розподілили навантаження по більшій кількості VM і ввели суворішу політику overcommit для того кластера.
Урок непомітний: більші VM не завжди швидші. Планування — реальне обмеження, а не деталь реалізації.

Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію (ізоляція CPU + дисципліна змін)

Платіжній системі поруч з основним потоком мали невеликий набір сервісів із жорсткими SLO і важкою криптографією. Команда платформи тримала їх на виділеному пулі вузлів зі static CPU manager, pinned ядрами і явним налаштуванням IRQ. Це було непопулярно. Виділені пули виглядали як «витрачена ємність» для фінансів і «втрата фану» для інженерів, які хотіли одну платформу для всього.

Вони також мали політику змін: оновлення ядра і BIOS проводилися поетапно, з канаркою, яка запускала синтетичні навантаження і відстежувала p99 і джиттер, а не лише throughput. Коли хтось просив пропустити канарку, відповідь була ввічливою і стійкою: ні.

Одного дня рутинне оновлення firmware в загальному пулі змінило поведінку живлення CPU. Декілька сервісів помітили зміни латентності і деякі команди пішли в інцидент. Пул для платежів навіть не моргнув. Їхні вузли були закріплені на валідованому профілі governor і оновлювалися лише після того, як канарка показала стабільні результати.

Практика не була хитрою. Вона була просто послідовною. У продакшині «нудне» — це фіча.

Жарт #2: Єдина річ більш віртуальна, ніж ВМ — це впевненість в постмортемі, написаному до приходу метрик.

Типові помилки: симптом → корінь проблеми → виправлення

1) Симптом: p99-латентність стрибає кожні кілька хвилин; середній CPU виглядає нормально

Корінь: Троттлінг CPU контейнера (cgroup CPU.max / CFS quota) під час сплесків.

Виправлення: Приберіть або підніміть CPU-ліміти для сервісу; встановіть реалістичні CPU-requests; використовуйте Guaranteed QoS і розгляньте CPU pinning для строгих SLO.

2) Симптом: CPU-використання ВМ високе, але throughput низький; графіки показують «idle» також

Корінь: Високий steal time через overcommit або contention на хості.

Виправлення: Зменшіть overcommit; перемістіть ВМ на менш завантажений хост; використайте резервації або політику виділених хостів; не додавайте vCPU як першу реакцію.

3) Симптом: продуктивність погіршилася після масштабування на більший тип інстансу

Корінь: NUMA-ефекти або планування між сокетами; частина пам’яті віддалена для частини навантаження.

Виправлення: Забезпечте NUMA-aware розміщення; пінуйте CPU і пам’ять; віддавайте перевагу інстансам, що вміщуються в один NUMA-вузол для latency-чутливих сервісів.

4) Симптом: «випадкові» стрибки латентності під час мережевого навантаження

Корінь: Переривання (IRQs) падають на ядра застосунку; перевантаження softirq.

Виправлення: Налаштуйте IRQ affinity; відокремте мережеві переривання від CPU застосунків; перевірте через /proc/interrupts і softirq-метрики.

5) Симптом: Java/Go сервіс в контейнері стає нестабільним під навантаженням, багато контекстних переключень

Корінь: Переповнення потоків відносно виділення CPU; thrash планувальника, підсилений жорсткими CPU-лімітами.

Виправлення: Зменшіть кількість потоків; підвищіть CPU-requests; уникайте низьких requests з високими limits; розгляньте CPU pinning для стабільної поведінки.

6) Симптом: все сповільнилося після впровадження «енергозбереження»

Корінь: Governor частоти CPU встановлено в powersave; thermal/power capping.

Виправлення: Використовуйте performance governor для критичних вузлів; перевірте BIOS-настройки; моніторьте частоту і throttling під тривалим навантаженням.

7) Симптом: Kubernetes-вузол виглядає недовикористаним, але pod-и повільні

Корінь: CPU-ліміти тротлять по-pod’ах; вузол може бути idle, поки окремі pod-и capped.

Виправлення: Перегляньте ліміти; використовуйте requests для розміщення, а не limits як важкий інструмент; розділяйте шумні навантаження на окремі вузли.

8) Симптом: сервіс на ВМ регресував після ввімкнення «більшого рівня безпеки»

Корінь: Зміни мікрокоду/ядра, що впливають на syscall/virtualization-heavy шляхи (залежно від навантаження).

Виправлення: Бенчмаркуйте до/після; ізолюйте зміну; якщо потрібно, відкоригуйте розмір інстансу або перемістіть навантаження у профіль, що терпить накладні витрати.

Чеклисти / покроковий план

Покроково: як обирати контейнери чи ВМ для CPU-профілю

  1. Випишіть метрику успіху для CPU. p99-латентність? requests/sec? час завершення задачі? Якщо ви не можете її назвати — будете оптимізувати під вайби.
  2. Класифікуйте навантаження. Latency-sensitive, throughput batch, bursty interactive, mixed-mode, special CPU behavior.
  3. Вирішіть потреби в ізоляції. Мульти-орендність зі слабким управлінням? Віддавайте перевагу ВМ або виділеним вузлам. Дисциплінована платформа? Контейнери також підходять.
  4. Вирішіть, як запобігати шумним сусідам.

    • Контейнери: requests, QoS, CPU pinning для критичних pod-ів, node pools, admission control.
    • ВМ: обмеження overcommit, резервації, CPU pinning де потрібно, моніторинг contention на хості.
  5. Встановіть дефолти, що відповідають реальності. Жодних «100m request» для сервісів зі сплесками. Жодних «8 vCPU» для сервісу, що половину часу чекає.
  6. Валідуйте на репрезентативному обладнанні. NUMA-топологія, SMT, поведінка частоти. «Та сама кількість vCPU» не означає «така ж продуктивність».
  7. Тестуйте навантаження на джиттер, не лише середнє. Відстежуйте p95/p99/p999 і дисперсію під contention і під колокацією.
  8. Операціоналізуйте діагностику. Виводьте throttling/steal/run-queue метрики в алерти. Якщо це не вимірюється — це перетвориться на дебат.

Чеклист: керування CPU для контейнерної платформи

  • Визначте дозволені діапазони CPU requests/limits для рівнів сервісів.
  • Застосовуйте через admission policies: жодних мізерних requests для критичних навантажень.
  • Використовуйте Guaranteed QoS для строгих SLO; розгляньте static CPU manager.
  • Розділіть node pools: «throughput pack» vs «latency clean room».
  • Моніторьте лічильники throttling у cgroup і алертуйте на тривале throttling.
  • Документуйте, коли CPU-ліміти потрібні (зазвичай для fairness у спільних batch-пулах).

Чеклист: керування CPU для VM-платформ

  • Встановіть і опублікуйте політику overcommit (і дотримуйтесь її).
  • Моніторьте steal time і run queue хоста; алертуйте, коли contention триває.
  • Уникайте рефлекторного масштабування vCPU; валідовуйте вплив на планування.
  • Для критичних навантажень: розгляньте CPU pinning і зарезервовану ємність.
  • Відстежуйте NUMA-розміщення і узгодженість топології хоста для більших ВМ.

FAQ

1) Чи завжди контейнери швидші за ВМ у плані CPU?

Часто контейнери мають менші накладні витрати, бо немає гостьового ядра і менше virtualization exits. Але «швидші» перестає бути істинним, якщо ви тротлите контейнери CPU-лімітами або щільно пакуєте їх у contention. ВМ можуть бути дуже близькі до нативу при належному налаштуванні і без overcommit.

2) Який еквівалент «шумного сусіда» по CPU в Kubernetes?

Pod із високим попитом на CPU плюс або відсутні ліміти (що дозволяє йому поглинати ресурси під contention), або погано встановлені ліміти (що викликає тротлінгові каскади в інших через тиск планування). Виправлення — шарувані node pools, реалістичні requests і їхнє примусове виконання.

3) Чому CPU-ліміти шкодять латентності, навіть коли вузли idle?

Бо ліміти можуть діяти як жорсткі капи по cgroup. Ваш pod може бути тротлений, навіть якщо на вузлі є вільний CPU, залежно від того, як попит лягає на період квоти. Симптом — зростання лічильників throttling при тому, що хост не завантажений.

4) Чи слід встановлювати CPU-ліміти на кожному контейнері?

Ні. Ставте ліміти, коли свідомо обмежуєте burst-поведінку заради fairness (batch-пули) або щоб запобігти runaway-процесам.
Для latency-чутливих сервісів ліміти часто — самошкод, якщо ви не перевірили, що вони не викликають throttling.

5) Чи завжди більше vCPU у ВМ краще?

Ні, не під contention. Більше vCPU може ускладнити планування ВМ і підвищити steal time. Підбирайте vCPU під реальну паралельність, яку ви можете використовувати, і перевіряйте в пікових умовах.

6) Як зрозуміти, чи мені шкодить NUMA?

Ви побачите непостійність продуктивності, гіршу хвостову латентність на більших інстансах і іноді підвищений крос-сокетний трафік. Підтвердіть NUMA-топологією, affinity процесу і (де доступно) статистикою локальності пам’яті. Виправлення — NUMA-aware розміщення та pinning.

7) SMT/Hyper-Threading допомагає чи заважає?

Воно допомагає пропускній здатності для багатьох навантажень, особливо з пропусками. Може зашкодити передбачуваності для latency-чутливої роботи, коли sibling-потоки конкурують за ресурси. Для строгих SLO уникайте спільних sibling або вимкніть SMT на виділених вузлах, якщо є резерви ємності.

8) Якщо я на Kubernetes, чи слід використовувати виділені вузли для критичних сервісів?

Так, коли p99 сервісу має значення і решта кластера — змішані навантаження від різних команд. Виділені вузли спрощують історію продуктивності і зменшують ентропію інцидентів. Це не «втрата», а плата за менше неспання о 3 ранку.

9) Які метрики мають мене будити щодо проблем з CPU?

Контейнери: throttled time (cpu.stat), run queue, контекстні переключення і насичення вузла CPU. ВМ: steal time плюс гостева run queue і contention хоста. Для обох: p99 латентність у кореляції з індикаторами планування.

10) Чи можу я отримати ізоляцію типу ВМ з контейнерами?

Ви можете наблизитися для CPU, використовуючи виділені node pools, Guaranteed QoS, static CPU manager pinning і суворі політики. Ви все одно ділите ядро, тож ізоляція не тотожна. Чи важливо це — залежить від вашої моделі загроз і оперативної зрілості.

Висновок: наступні кроки, які можна реально зробити

Обирайте, виходячи з CPU-профілю, а не з моди. Якщо вам потрібна детермінована хвостова латентність і немає сильної платформи-координації, почніть з ВМ або виділених контейнерних вузлів. Якщо вам потрібна пропускна здатність і щільність — контейнери зазвичай правильний вибір, просто не саботуйте їх наївними CPU-лімітами.

Наступні кроки:

  1. Виберіть один критичний сервіс і проганяйте швидкі діагностичні перевірки під піковим навантаженням: throttling (контейнери) або steal (ВМ), run queue, IRQ‑гарячі точки, частота.
  2. Виправте одну політику, а не десять: або приберіть шкідливі CPU-ліміти для latency-сервісів, або зменшіть overcommit ВМ там, де steal високий.
  3. Створіть два класи вузлів/хостів: «latency clean room» (пінінг/ізоляція) та «throughput pack» (висока щільність, справедливий розподіл).
  4. Зробіть requests/limits (або vCPU-sizing) частиною code review з коротким рубрикатором. Пейджер вам потім подякує.
← Попередня
Docker + TLS: Let’s Encrypt в контейнерах і на хості — обирайте безпечний підхід
Наступна →
Помилка оновлення WordPress: правильно виправити права, місце на диску та власність

Залишити коментар