Хост забитий. Load average росте так, ніби йому кудись треба вчасно прибути. SSH відчувається повільним. На ваших дашбордах написано «CPU 100%», але вони не кажуть, хто винен. Docker залучений, а це означає, що проблема або акуратно ізольована… або красиво розподілена.
Це той playbook, який я використовую, коли Linux-сервер «горить» і контейнери — головні підозрювані. Це практично, трохи суб’єктивно, і допоможе вам ідентифікувати шумний контейнер, довести, що це справжнє вузьке місце, і обмежити його так, щоб не отримати в обмін затримки, тротлінг або дивну поведінку планувальника.
Швидкий план діагностики
Коли CPU зашкалює, вам не потрібний ретрит для медитації. Потрібна швидка петля триажу, яка відрізняє:
- Один контейнер, що спалює CPU vs. багато контейнерів, кожен трохи гарячий
- Насичення CPU vs. CPU-тротлінг vs. черга виконання (run queue) під тиском
- Справжня обчислювальна робота vs. spin-loop vs. накладні витрати ядра
Перш за все: підтвердіть, що це саме CPU, а не «CPU-подібний» I/O
- Запустіть
uptimeіtop, щоб порівняти load average і idle CPU. - Якщо load високий, але CPU idle теж високий, ймовірно ви блокуєтесь на I/O або локах.
Далі: ідентифікуйте відповідний контейнер(и)
- Використайте
docker stats --no-streamдля швидкого ранжування. - Потім зіставте host PID з контейнерами (бо
docker statsможе брехати опущенням у дивних випадках).
Третє: вирішіть, обмежувати, масштабувати чи виправляти код
- Обмежити, коли одна робота цькує сусідів і ви можете терпіти більшу затримку для цієї роботи.
- Масштабувати, коли робота легітимна і черезрезультативність важлива.
- Виправити, коли ви бачите spin-loop-и, ретраї, гарячі локи або патологічний GC.
Четверте: застосуйте обмеження, що відповідає вашому планувальнику
- На Linux Docker-контроль CPU — це cgroups. Ваші ліміти працюють залежно від версії cgroup та поведінки ядра.
- Обирайте CPU quota для «цей контейнер може використовувати до X CPU часу». Обирайте cpuset для «цей контейнер може запускатися тільки на цих ядрах».
Якщо ви на виклику й вам потрібен один рядок: знайдіть топовий host PID, зіставте його з контейнером, потім перевірте, чи його вже тротлять. Обмежувати контейнер, який вже тротлиться, — це як казати комусь «заспокойся», поки ви тримаєте йому голову під водою.
Що насправді означає «CPU 100%» у Docker
«CPU 100%» — це один із тих метрик, що звучать точно і поводяться як плітки.
- На хості з 4 ядрами одне повністю завантажене ядро — це 25% від загального CPU, якщо ви вимірюєте відносно загальної потужності.
- У Docker-інструментах відсоток CPU контейнера може повідомлятись відносно одного ядра або всіх ядер залежно від обчислення і версії.
- У cgroups використання CPU вимірюється як час (наносекунди). Ліміти реалізуються як квоти за період, а не «відсотки» у людському сенсі.
Ключовий операційний висновок: хост може показувати «100% CPU», тоді як важливий сервіс із затримками сповільнюється через те, що його тротлять, позбавляють часу через чергу виконання, або він втрачає час на steal у гіпервізора.
Ось модель мислення, яка вас не підведе:
- Використання CPU показує, скільки часу витрачено на виконання.
- Черга виконання (run queue) показує, скільки потоків хочуть працювати, але не можуть.
- Тротлінг показує, що ядро активно забороняло cgroup працювати, бо вона досягла квоти.
- Steal time показує, що VM хотіла CPU, але гіпервізор відповів «зараз ні».
Факти та контекст: чому це складніше, ніж здається
Декілька контекстних пунктів, що важливі в продакшн середовищі, бо пояснюють дивності у виводах:
- Обмеження CPU в Docker — це обмеження cgroup. Docker не винайшов ізоляцію CPU; він — обгортка над Linux cgroups та namespace.
- cgroups v1 vs v2 змінюють підводні механізми. Багато сесій «чому цього файлу немає?» — це просто «ви тепер на v2».
- CFS bandwidth control (cpu quota/period) з’явився в ядрі задовго до масового розвитку контейнерів; контейнери лише зробили його популярним.
- CPU shares — це не жорсткий ліміт. Shares — це вага, що використовується лише під час конкуренції; вони не зупинять контейнер від використання вільного CPU.
- «cpuset» старіший і грубий. Прив’язка до ядер детермінована, але може марнувати CPU, якщо ви погано прів’язуєте або ігноруєте NUMA.
- Тротлінг може виглядати як «низьке використання CPU». Контейнер може бути повільним і одночасно показувати помірний CPU, бо він проводить час, заблокований енфорсментом квоти.
- Load average включає не тільки CPU. На Linux load average рахує задачі в uninterruptible sleep також, тож проблеми зі сховищем можуть маскуватися під «CPU-проблеми».
- Віртуалізація додає steal time. На перевантажених хостах VM може мати «CPU 100%», яке фактично — «я б працював, якби міг».
- Моніторинг має довгий хвіст помилок. CPU-метрики легко збирати і легко неправильно інтерпретувати; різниці в вікнах вибірки і нормалізації створюють фантомні сплески.
Цитата, яку варто тримати на столі:
Вернер Фогельс (парафраз): «Усе ламається; проектуйте так, щоб відмови були очікуваними і оброблювались, а не вважались винятками».
Практичні завдання: команди, виводи, рішення
Це реальні завдання, які я очікую від інженера на виклику. Кожне включає: команду, що означає вивід, і яке рішення ухвалювати. Не запускайте їх усі наосліп; використовуйте як розгалужувальне розслідування.
Завдання 1: Перевірте, чи хост реально насичений CPU
cr0x@server:~$ uptime
14:22:19 up 37 days, 6:11, 2 users, load average: 18.42, 17.96, 16.10
Що це означає: Load average ~18 на хості з 8 ядрами — це проблема; на 32‑ядерному хості це може бути нормально. Сам по собі load не є доказом насичення CPU.
Рішення: Далі перевірте idle CPU і чергу виконання за допомогою top або mpstat. Якщо idle CPU високий, переключайтеся на I/O або локи.
Завдання 2: Перевірте idle CPU, steal time і головних порушників
cr0x@server:~$ top -b -n1 | head -25
top - 14:22:27 up 37 days, 6:11, 2 users, load average: 18.42, 17.96, 16.10
Tasks: 512 total, 9 running, 503 sleeping, 0 stopped, 0 zombie
%Cpu(s): 94.7 us, 2.1 sy, 0.0 ni, 0.6 id, 0.0 wa, 0.0 hi, 0.3 si, 2.3 st
MiB Mem : 32114.2 total, 1221.4 free, 14880.3 used, 16012.5 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 14880.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
21483 root 20 0 1614920 82364 19624 R 380.0 0.3 76:12.39 python
12914 root 20 0 2344812 109004 40220 R 160.0 0.3 21:44.07 node
9881 root 20 0 986604 61172 17640 R 95.0 0.2 12:11.88 java
Що це означає: Idle CPU ~0.6%: ви CPU‑насичені. Steal time 2.3%: не критично, але підказує, що гіпервізор забирає трохи циклів.
Рішення: Ідентифікуйте ці PID: чи вони в контейнерах? Якщо так, зіставте їх з ID контейнерів. Якщо ні — маєте проблему на хості (або контейнер запущений з host PID namespace — таке трапляється).
Завдання 3: Швидке ранжування контейнерів через docker stats
cr0x@server:~$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a12b3c4d5e6f api-prod-1 265.42% 612.4MiB / 2GiB 29.90% 1.2GB / 980MB 11.2MB / 0B 78
b98c7d6e5f4a worker-prod-queue 410.11% 1.1GiB / 1GiB 110.02% 120MB / 98MB 2.4GB / 1.9GB 213
c11d22e33f44 redis-prod 18.33% 3.2GiB / 4GiB 80.00% 420MB / 390MB 0B / 0B 6
Що це означає: worker-prod-queue і одночасно CPU‑гарячий, і перевищує ліміт пам’яті (110% може означати ризик swap/oom залежно від налаштувань). CPU‑числа понад 100% показують використання кількох ядер.
Рішення: Не обмежуйте CPU негайно. Спочатку підтвердіть, чи цей worker виконує легітимну роботу, чи трешиться через пам’ять, чи крутиться у спіні. Тиск по пам’яті може спричиняти спалювання CPU через GC, інтенсивні алокації або шторм ретраїв.
Завдання 4: Спростуйте host PID до контейнера
cr0x@server:~$ ps -o pid,comm,cgroup -p 21483 | sed -n '1,3p'
PID COMMAND CGROUP
21483 python 0::/docker/b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
Що це означає: Цей host PID належить Docker cgroup (показано шлях cgroups v2 unified як 0::). Довгий хеш відповідає префіксу ID контейнера.
Рішення: Підтвердіть через docker ps і docker inspect, потім дослідіть всередині контейнера.
Завдання 5: Підтвердіть ідентичність контейнера та entrypoint
cr0x@server:~$ docker ps --no-trunc | grep b98c7d6e5f4a
b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a acme/worker:7f3c1d "python /app/worker.py" 3 days ago Up 3 days worker-prod-queue
Що це означає: Це Python‑воркер, ймовірно споживач черги або батч‑джоб.
Рішення: Якщо це батч‑воркер, обмеження CPU часто безпечні. Якщо це latency‑чутливий API, ліміти можуть стати самозаподіяною відмовою.
Завдання 6: Перевірте, чи контейнер вже обмежений
cr0x@server:~$ docker inspect -f 'NanoCpus={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} CpusetCpus={{.HostConfig.CpusetCpus}} CpuShares={{.HostConfig.CpuShares}}' worker-prod-queue
NanoCpus=0 CpuQuota=0 CpuPeriod=0 CpusetCpus= CpuShares=0
Що це означає: Явних CPU‑обмежень або ваг не налаштовано. Контейнер може використовувати весь доступний CPU.
Рішення: У вас є опція обмежити. Але спочатку перевірте, що він робить, і після застосування ліміту спостерігайте за тротлінг‑метриками.
Завдання 7: Перевірте статистику cgroup v2 щодо тротлінгу
cr0x@server:~$ CID=b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
cr0x@server:~$ CGP=$(docker inspect -f '{{.HostConfig.CgroupParent}}' $CID); echo "${CGP:-/sys/fs/cgroup}"
/sys/fs/cgroup
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 129884420
user_usec 127110003
system_usec 2774417
nr_periods 0
nr_throttled 0
throttled_usec 0
Що це означає: На цьому хості cgroup контейнера існує під /sys/fs/cgroup/docker/<id>. Тротлінгу поки немає, бо квота не налаштована.
Рішення: Якщо пізніше ви встановите квоту і nr_throttled швидко зросте з високим throttled_usec, ви створили обмеження пропускної здатності/затримки. Це може бути правильним, але має бути усвідомленим.
Завдання 8: Перевірте, чи проблема в kernel time або user time
cr0x@server:~$ pidstat -p 21483 1 3
Linux 6.5.0-18-generic (server) 01/02/2026 _x86_64_ (16 CPU)
14:23:31 UID PID %usr %system %guest %wait %CPU CPU Command
14:23:32 0 21483 92.00 6.00 0.00 0.00 98.00 7 python
14:23:33 0 21483 93.00 5.00 0.00 0.00 98.00 7 python
14:23:34 0 21483 90.00 7.00 0.00 0.00 97.00 7 python
Що це означає: Переважно user time. Це вказує на рівні додатку обчислення або tight loop, а не на накладні витрати ядра.
Рішення: Профілюйте всередині контейнера (або на хості для цього PID) і шукайте «гарячі» функції, busy‑wait цикли або backlog черги, що створює легітимну роботу.
Завдання 9: Зайдіть у контейнер і перевірте, чи там багатопотоковість
cr0x@server:~$ docker exec -it worker-prod-queue bash -lc 'ps -eLo pid,tid,pcpu,comm --sort=-pcpu | head'
PID TID %CPU COMMAND
1 1 96.4 python
1 42 92.1 python
1 43 91.8 python
1 44 90.9 python
1 45 90.2 python
88 88 1.1 bash
Що це означає: Кілька потоків гарячі. Для Python це може означати кілька процесів/ниток, C‑розширення, що працюють, або щось на зразок gevent/eventlet, що теж спалює CPU.
Рішення: Якщо це пул воркерів, обмежте його або зменшіть конкуренцію. Якщо він не має бути багатопотоковим, перевірте випадкову паралельність (наприклад, бібліотека, що створює нитки, або зміна конфігурації).
Завдання 10: Використайте perf для пошуку «гарячих» місць (на хості, не потрібно інструментів у контейнері)
cr0x@server:~$ sudo perf top -p 21483 -n 5
Samples: 1K of event 'cycles', 4000 Hz, Event count (approx.): 250000000
Overhead Shared Object Symbol
22.11% python [.] _PyEval_EvalFrameDefault
15.37% python [.] PyObject_RichCompare
10.02% libc.so.6 [.] __memcmp_avx2_movbe
7.44% python [.] list_contains
6.98% python [.] PyUnicode_CompareWithASCIIString
Що це означає: CPU витрачається на виконання інтерпретатора та порівнянь. Це реальні обчислення, не баг ядра. Також вказує на те, що робота може бути «важкою» по даних — фільтри, дедупація, сканування.
Рішення: Для негайного стримування: капніть CPU або обмежте конкуренцію роботи. Для довгостроково: профілюйте на рівні аплікації; можливо, у вас O(n²) операції всередині батчу.
Жарт #1: Якщо ваш воркер робить O(n²) в циклі — вітаю, він винайшов обігрівач для приміщення.
Завдання 11: Перевірте навантаження черги виконання по ядрах
cr0x@server:~$ mpstat -P ALL 1 2
Linux 6.5.0-18-generic (server) 01/02/2026 _x86_64_ (16 CPU)
14:24:31 CPU %usr %nice %sys %iowait %irq %soft %steal %idle
14:24:32 all 92.11 0.00 5.02 0.00 0.00 0.44 2.12 0.31
14:24:32 7 99.00 0.00 0.90 0.00 0.00 0.10 0.00 0.00
14:24:32 8 97.00 0.00 2.70 0.00 0.00 0.30 0.00 0.00
Що це означає: Кілька CPU фактично завантажені по максимуму. Якщо гарячі тільки кілька ядер, варто подумати про cpuset‑pinning або перевірити однопотокові вузькі місця.
Рішення: Якщо хост глобально насичений, обмеження одного контейнера — це крок заради чесності. Якщо гарячий лише один ядро, кап по квоті не виправить однопотокові обмеження; виправляйте конкуренцію або прів’язку.
Завдання 12: Інспектуйте обмеження контейнера в cgroups v2 (quota та ефективні CPU)
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
max 100000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpuset.cpus.effective
0-15
Що це означає: cpu.max — це «quota/period». max означає необмежено. Період — 100000 мікросекунд (100ms). Effective CPUs показує, що контейнер може запускатися на всіх 16 CPU.
Рішення: Якщо ви хочете «2 CPU», встановіть quota 200000 для period 100000, або використайте зручний прапорець Docker --cpus=2.
Завдання 13: Застосуйте обмеження CPU онлайн (обережно) і перевірте тротлінг
cr0x@server:~$ docker update --cpus 4 worker-prod-queue
worker-prod-queue
cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' worker-prod-queue
CpuQuota=400000 CpuPeriod=100000 NanoCpus=4000000000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
400000 100000
Що це означає: Тепер контейнер може споживати до 4 CPU‑еквівалентів часу за 100ms період. Docker перетворив --cpus у quota/period.
Рішення: Спостерігайте за CPU хоста і затримками сервісів. Якщо шумний контейнер некритичний — це часто правильна негайна міра. Якщо він критичний, можливо доведеться виділити більше CPU або масштабувати.
Завдання 14: Підтвердіть, чи кап спричиняє тротлінг (і чи це прийнятно)
cr0x@server:~$ sleep 2; cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 131992884
user_usec 129050221
system_usec 2942663
nr_periods 2201
nr_throttled 814
throttled_usec 9811123
Що це означає: Тротлінг відбувається (nr_throttled збільшився). Контейнер досяг своєї CPU‑квоти. Це очікувано при обмеженні гарячого навантаження.
Рішення: Вирішіть, чи тротлінг — це мета (захист інших сервісів), або знак, що ви занадто жорстко обмежили (крах пропускної здатності, зростання беку). Перевірте глибину черги і затримки. Якщо backlog росте — підніміть ліміт або масштабувати воркерів.
Завдання 15: Визначте топ‑треті контейнера з хоста (без exec)
cr0x@server:~$ ps -T -p 21483 -o pid,tid,pcpu,comm --sort=-pcpu | head
PID TID %CPU COMMAND
21483 21483 96.2 python
21483 21510 92.0 python
21483 21511 91.7 python
21483 21512 90.5 python
21483 21513 90.1 python
Що це означає: Гарячі нитки видно з хоста. Корисно, коли образи мінімальні і в контейнері немає дебаг‑інструментів.
Рішення: Якщо домінує одна нитка — ви в однопотоковій зоні. Якщо багато ниток гарячі — квотні обмеження поведуться передбачуваніше.
Завдання 16: Перевірте, чи ви боретесь зі steal (перевантаження VM‑хоста)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
13 0 0 125132 210204 8023412 0 0 0 5 2841 7102 91 5 2 0 2
18 0 0 124980 210204 8023520 0 0 0 0 2911 7299 90 5 2 0 3
16 0 0 124900 210204 8023604 0 0 0 0 2780 7010 89 6 2 0 3
14 0 0 124820 210204 8023710 0 0 0 10 2894 7255 90 5 2 0 3
15 0 0 124700 210204 8023794 0 0 0 0 2810 7098 91 5 1 0 3
Що це означає: st (steal) 2–3%. Не катастрофа. Якщо бачите 10–30% — ви не «CPU‑обмежені», а «обмежені сусідами по обладнанню».
Рішення: Якщо steal високий, ліміти не виправлять ситуацію. Перемістіть навантаження, змініть розмір інстансів або розміщення хостів. Інакше ви оптимізуєте неправильний рівень.
Як правильно обмежувати CPU (без самосаботажу)
«Просто обмежити» — це спосіб створити наступний інцидент. Ліміти CPU — це контракт: ви кажете ядру «Це навантаження може бути сповільнене заради захисту інших». Зробіть цей контракт явним і тестованим.
Обирайте правильний контроль: quota vs shares vs cpuset
1) CPU quota/period (за замовчуванням — розумне обмеження)
Використовуйте коли: хочете, щоб контейнер отримував максимум N CPU‑еквівалентів часу, але планувальник все ще міг розміщувати його по ядрах.
- Docker‑флаг:
--cpus 2(зручність) або--cpu-quotaі--cpu-period. - Механізм ядра: CFS bandwidth control.
Що може піти не так: агресивні ліміти спричиняють сильний тротлінг, що створює бурстову затримку. Ваш додаток стає метрономом: працює, тротлиться, знову працює.
2) CPU shares (вага чесності, а не ліміт)
Використовуйте коли: хочете відносний пріоритет між контейнерами під час конкуренції, але вас влаштовує, що будь‑який контейнер використовує вільний CPU, коли хост простий.
- Docker‑флаг:
--cpu-shares.
Що може піти не так: люди ставлять shares, очікуючи жорсткого ліміту, а потім дивуються, чому «некерований» контейнер все ще загрузив хост вночі.
3) Cpuset (прив’язка до ядер)
Використовуйте коли: у вас є ліцензійні обмеження, ви ізолюєте шумних сусідів або керуєте NUMA/локальністю кешу навмисно. Це для дорослих, які люблять графіки.
- Docker‑флаг:
--cpuset-cpus 0-3.
Що може піти не так: прив’язка до «неправильних» ядер може конфліктувати з IRQ‑affinity, іншими прив’язаними навантаженнями або залишити половину машини простою, поки одне ядро печеться.
Мій улюблений підхід у продакшн
- Починайте з quota‑кепа використовуючи
--cpus, встановлюйте досить високо, щоб уникнути постійного тротлінгу. - Вимірюйте тротлінг через
cpu.statпісля зміни. Тротлінг не завжди поганий; небажаний тротлінг — так. - Якщо потрібна сильніша ізоляція (наприклад, multi‑tenant), додайте cpuset‑pinning, але тільки після аудиту топології CPU і розподілу переривань.
- Використовуйте shares для надання переваги критичним сервісам перед best‑effort, але не видавайте їх за ремінь безпеки.
Скільки CPU давати?
Не вгадуйте. Використовуйте спостережуваний попит навантаження і бізнес‑толерантність.
- Для API‑сервісів залиште достатньо CPU, щоб захистити p99‑латентність. Якщо CPU гарячий, масштабування зазвичай безпечніше, ніж капання.
- Для воркерів/батчів — капайте заради чесності і підганяйте concurrency під кап. Інакше ви просто будете тротлити натовп.
- Для баз даних будьте обережні: CPU‑капи можуть погіршувати хвости латентності і створювати накопичення локів. Краще виділені вузли або cpuset, якщо потрібно ізолювати.
Валідація з урахуванням тротлінгу: що стежити після капа
Після застосування капа перевірте такі сигнали:
- Load і idle CPU хоста: Чи відновилися інші сервіси?
- Тротлінг контейнера: Чи постійно зростає
nr_throttled? - Глибина черги/беклог: Якщо беклог росте — ви зменшили пропускну спроможність нижче швидкості надходження.
- Затримки/помилки: Для синхронних сервісів ліміти часто проявляються як таймаути, а не просто «повільніші відповіді».
Жарт #2: CPU‑квоти як корпоративні бюджети — усім їх не люблять, але альтернатива — одна команда купує шість еспресо‑машин і називає це «інфраструктурою».
Compose, Swarm і пастка «чому мій ліміт не спрацював?»
Є два класи ticket‑ів «мій CPU‑ліміт не працює»:
- Він ніколи не застосовувався. Оркестратор проігнорував його або ви поставили в неправильний розділ.
- Він застосований, але ви неправильно виміряли. Ви очікували «50%», а отримали «все ще гаряче», бо хост має багато ядер, або робота бустить і її тротлять пізніше.
Docker Compose: підводні камені версій
У Compose історично були два місця для лімітів: старіший стиль cpu_shares/cpus і Swarm‑стиль deploy.resources. Підводний камінь: non‑Swarm Compose ігнорує deploy‑ліміти у багатьох налаштуваннях. Люди копіюють конфіг з блогу і вважають, що ядро виконає YAML.
Якщо хочете надійний локальний/Compose‑кап, перевірте його через docker inspect після docker compose up. Не довіряйте файлу наосліп.
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
stack_worker_1 acme/worker:7f3c1d "python /app/worker.py" worker 2 minutes ago Up 2 minutes
cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' stack_worker_1
CpuQuota=200000 CpuPeriod=100000 NanoCpus=2000000000
Що це означає: Ліміти дійсно застосовані. Якщо вони показують нулі — ваші налаштування Compose не застосовані.
Рішення: Виправте конфіг так, щоб ліміти застосовувалися там, де runtime їх поважає, або застосуйте через docker update і закріпіть у конфігурації пізніше.
Різниці Swarm і Kubernetes (операційні, не філософські)
- У Swarm
deploy.resources.limits.cpusсправжні і примусово застосовуються, бо Swarm планує задачі враховуючи ці обмеження. - У Kubernetes CPU limits і requests взаємодіють з QoS‑класами. Ліміти можуть тротлити; requests впливають на планування. «Я встановив ліміт» — не те саме, що «я гарантував CPU».
Якщо ви дебагуєте на Docker‑хостах, але ваша ментальна модель з Kubernetes — будьте обережні: ви можете пропустити нюанс «request vs limit», що впливає на поведінку шумного сусіда.
Три корпоративні міні‑історії з полів CPU
Міні‑історія 1: Інцидент через неправильне припущення
Компанія мала Docker‑хост з «усього кількома сервісами». Це фраза, яку люди кажуть собі, щоб почуватися в контролі. Один сервіс був API, інший — воркер черги, третій — metrics sidecar, до якого ніхто не хотів торкатися, бо «він працював».
Під час піку хост почав битися об 100% CPU. Інженер на виклику відкрив дашборд, побачив, що CPU API стрибнув, і зробив логічно‑але‑невірне припущення: «API — проблема». Вони вділили API контейнер 1 CPU через live update. Хост CPU впав. Всі заспокоїлися приблизно на вісім хвилин.
Потім рівень помилок підскочив. З’явилися таймаути. API не «виправили»; його задушили. Справжнім винуватцем був воркер, що затопив Redis ретраями, бо досяг ліміту пам’яті і почав свапитися всередині cgroup. Воркер спричинив шторм ретраїв. API просто страждав, намагаючись встигнути.
Що зробило цей інцидент повчальним у постмортемі: CPU‑сплески API були симптомом downstream‑конфлікту і ретраїв, а не кореневою причиною. Коли API обмежили, він більше не міг швидко скидати навантаження. Воркер лишався гучним, API лише став слабшим.
Фікс не був драматичним. Вони прибрали кап з API, додали квоту воркеру, і — це завжди незручно — зменшили concurency воркерів так, щоб він відповідав новому CPU‑контракту. Шторм ретраїв зупинився. Redis заспокоївся. CPU нормалізувався. Урок: обмежуйте хулігана, а не жертву.
Міні‑історія 2: Оптимізація, що вистрілила собі в ногу
Інша команда мала pipeline батч‑інгінесту в контейнерах. Вони хотіли більшу пропускну і помітили, що CPU не завантажено у неробочі години. Хтось запропонував «більше паралелізму» — збільшити потоки воркерів з 4 до 32. Звучало модно. Виглядало круто в швидкому тесті. У продакшн це перетворилося на повільну мелтдаун‑сцену.
Пайплайн парсив стиснені дані і робив валідацію схем. З 32 потоками на контейнер і кількома контейнерами на хост вони створили гуркіт по CPU і пам’яті. Контекст‑перемикання піднявся. Локальна кеш‑локалізація погіршилась. Планувальник хоста показував себе як жонглер на вітрі.
Вони спробували капнути CPU, щоб «стабілізувати». Воно стабілізувало, так — як машина стабілізується, коли врізається в стіну. Тротлінг підскочив і пропускна впала. Черги бэклогу заповнились, і команда почала масштабувати контейнери. Це погіршило конкуренцію, бо вузьким місцем були CPU та пам’ять хоста, а не кількість контейнерів.
Нарешті вирішили банально: зменшили потоки воркерів, трохи збільшили кількість контейнерів, але прив’язали батч‑роботу до cpuset діапазону подалі від latency‑чутливих сервісів. Також навчилися вимірювати nr_throttled, а не лише CPU‑%.
Оптимізація провалилась, бо припускала, що CPU масштабуються лінійно. У реальних системах паралелізм змагається сам із собою.
Міні‑історія 3: Нудна, але правильна практика, що врятувала день
Є організації, що не роблять героїчних вчинків, бо їм це не потрібно. Одна платформа мала жорстке правило: кожен контейнер у продакшні обов’язково декларує CPU і пам’ять, і це перевіряється автоматично після деплою.
Інженери скаржились. Казали, що це гальмує релізи. Казали, що це «Kubernetes‑мислення», хоча це був звичайний Docker. Команда платформи чемно ігнорувала і далі контролювала. Вони також вимагали, щоб кожен сервіс мав «режим деградації»: що робити, коли CPU обмежено — скидати роботу, ставити в чергу чи фейлити швидко?
Одного дня оновлення сторонньої бібліотеки внесло регресію: busy‑loop тригерився на рідкісному патерні вхідних даних. Певний піднабір запитів спричиняв CPU‑сплески. Зазвичай це приводило б до падіння хоста і інциденту, що зачепив би багато сервісів.
Натомість уражений контейнер досяг свого CPU‑ліміту і був тротлінгований. Він став повільнішим, так, але не відкусив решту ноди. Інші сервіси продовжували обслуговувати. Моніторинг дав чистий сигнал: тротлінг цього сервісу зріс. Інженер швидко відкотив оновлення. Ніякого каскадного фейлу і північної наради.
Нудна практика — завжди ставити ліміти, завжди перевіряти їх, і визначати поведінку під обмеженням — не лише запобігла контеншену. Вона зробила режим відмови прозорим.
Типові помилки: симптоми → корінь → виправлення
Тут ми врятуємо вас від повторення хітів найбільшої слави.
1) Симптом: Host CPU 100%, але docker stats нічого екстремального не показує
Корінь: Гарячі процеси на хості (journald, node exporter, kernel threads), або контейнери запущено з host PID namespace, або вибірка docker stats пропускає короткі сплески.
Виправлення: Спочатку використайте інструменти хоста: top, потім зіставте гарячі PID з контейнерами через ps -o cgroup. Якщо нема Docker cgroup — це не проблема контейнерів.
2) Симптом: Після встановлення --cpus сервіс повільнішає і зростають таймаути
Корінь: Ви обмежили latency‑чутливий сервіс нижче його p99‑потреб.
Виправлення: Заберіть або підвищте кап; масштабування горизонтально; додайте backpressure і обмеження concurrency. Вимірюйте cpu.stat тротлінг разом з latency.
3) Симптом: Ви поставили CPU shares, але контейнер все одно завантажує хост
Корінь: CPU shares — це ваги, а не кап. Якщо на хості є вільний CPU — контейнер його забере.
Виправлення: Використайте --cpus або --cpu-quota для жорсткого капу. Зберігайте shares для пріоритизації під час конкуренції.
4) Симптом: Load average величезний, але idle CPU теж високий
Корінь: Заблоковані задачі (I/O wait, uninterruptible sleep), локи або файлові зупинки. Load average рахує більше, ніж runnable задачі.
Виправлення: Перевірте top на wa, використайте iostat (якщо є), інспектуйте заблоковані задачі і шукайте вузькі місця в сховищі/мережі. Не «капайте CPU» для I/O‑проблеми.
5) Симптом: CPU гарячий тільки на одному ядрі, а продуктивність жахлива
Корінь: Однопотокове вузьке місце, глобальний лок або один «гарячий» шард/партиція.
Виправлення: Не додавайте CPU‑капи; виправляйте конкуренцію або шардінг. Якщо треба ізолювати, cpuset‑pinning може зупинити один гарячий тред від того, щоб заважати всім, але це не зробить його швидшим.
6) Симптом: Виглядає, що CPU використання нормальне, але сервіс повільний
Корінь: Тротлінг: контейнер обмежений і проводить час, очікуючи можливості виконатися. CPU% може виглядати помірно, бо час під тротлінгом не рахується як «CPU usage».
Виправлення: Читайте cpu.stat (nr_throttled, throttled_usec). Підніміть кап, зменшіть concurrency або масштабуйтесь.
7) Симптом: CPU‑сплески після «оптимізації» логування чи метрик
Корінь: Метрики високої кардинальності, дороге форматування логів, синхронне логування або конкуренція в телеметричних пайплайнах.
Виправлення: Зменшіть кардинальність, вибірку, батчіть або виносьте важке форматування з гарячих шляхів. Обмежуйте й telemetry sidecar‑и; вони не невинні.
8) Симптом: Після прив’язки cpuset пропускна падає і деякі CPU простіють
Корінь: Поганий вибір ядер, конфлікт з IRQ‑affinity, NUMA‑невідповідність або прив’язка занадто малої кількості ядер для бустових патернів.
Виправлення: Віддавайте перевагу quota спочатку. Якщо використовуєте cpuset — аудіть топологію CPU, розгляньте NUMA і залишайте простір для роботи ядра/переривань.
Чеклисти / покроковий план
Покроково: знайти шумний контейнер
- Виміряйте хост:
uptime,top. Підтвердіть, що idle CPU низький і поділ user/system. - Швидко ранжуйте контейнери:
docker stats --no-stream. - Ранжуйте host PID: у
topсортуйте по CPU, скопіюйте топ PID. - Мап PID → cgroup:
ps -o cgroup -p <pid>. Якщо під/docker/<id>, маєте контейнер. - Підтвердіть ім’я/образ контейнера:
docker ps --no-trunc | grep <id>іdocker inspect. - Перевірте, що він робить:
pidstat,perf topабоpsвсередині контейнера.
Покроково: безпечно обмежити
- Визначте мету: захист інших робочих навантажень або збереження пропускної для цього сервісу.
- Обирайте контроль: quota (
--cpus) для більшості випадків; shares для відносної ваги; cpuset для жорсткої ізоляції. - Застосуйте кап онлайн (якщо потрібно):
docker update --cpus N <container>. - Підтвердіть застосування:
docker inspectіcat cpu.max(v2) або відповідні v1 файли. - Вимірюйте тротлінг: перевірте
cpu.statчерез кілька секунд. - Слідкуйте за SLO: latency, помилки, глибина черги, ретраї. Якщо все погіршується — підніміть кап або масштабуватись.
- Закріпіть налаштування: оновіть Compose/Swarm конфіг або pipeline; не лишайте live
docker updateяк племінну магію.
Покроково: запобігти повторенню
- Встановіть дефолти: кожен сервіс декларує CPU і пам’ять.
- Перевіряйте автоматично: пост‑деплой перевірки порівнюють бажані ліміти з
docker inspect. - Інструментуйте тротлінг: алертуйте при стійкому зростанні
nr_throttledдля критичних сервісів. - Вирівнюйте concurrency: розміри пулів воркерів мають масштабуватись разом із CPU‑капами; уникайте «32 потоки, бо десь є ядра».
- Кнопки аварійного вимкнення: feature‑flags або rate limits для зменшення створення роботи під час сплесків.
FAQ
1) Чому docker stats показує 400% CPU для одного контейнера?
Тому що він використовує ~4 ядра за вікно вибірки. CPU% часто нормалізується до одного ядра, тому мульти‑ядерне використання перевищує 100%.
2) Чи --cpus — те саме, що --cpuset-cpus?
Ні. --cpus використовує CFS quota/period (обмеження за часом). --cpuset-cpus обмежує, на яких CPU контейнер може запускатися (ізоляція по розміщенню). Вони вирішують різні задачі.
3) Чи CPU shares зупинять runaway контейнер від завали хоста?
Лише при конкуренції. Якщо на хості є вільний CPU, shares не зупинять контейнер від його використання. Для жорсткого капа використовуйте quota.
4) Як зрозуміти, чи тротлінг шкодить?
Читайте cpu.stat. Якщо nr_throttled і throttled_usec стабільно ростуть, а latency/backlog погіршуються — ви занадто сильно обмежили.
5) Чому load average високий, але idle CPU не нульовий?
Load average включає задачі, що чекають на I/O (uninterruptible sleep), а не тільки runnable CPU‑задачі. Високий load з помірним idle може означати проблеми зі сховищем, локами або блокування.
6) Чи можна обмежити CPU у запущеного контейнера без перезапуску?
Так: docker update --cpus N <container> (та споріднені прапори) застосовуються онлайн. Використовуйте це як аварійний важіль; потім закріпіть зміни в конфігурації.
7) Я поставив ліміти в docker-compose.yml під deploy:, але нічого не змінилось. Чому?
Бо deploy‑ліміти орієнтовані на Swarm mode. Багато non‑Swarm Compose установок їх ігнорують. Завжди перевіряйте через docker inspect, чи ліміти застосовані.
8) Чи варто обмежувати контейнер бази даних при гарячому CPU?
Зазвичай ні, не як перший крок. Бази під CPU‑тиском зазвичай потребують більше CPU, оптимізації запитів або ізоляції. Кап може перетворити коротке навантаження на довгі хвости латентності і накопичення локів.
9) Чи означає «CPU 100%» всередині контейнера те саме, що на хості?
Залежить від інструменту і нормалізації. Всередині контейнера ви можете бачити вигляд, що ігнорує потужність хоста. Довіряйте обліку на рівні хоста і статистиці cgroup для лімітів і тротлінгу.
10) Чи варто використовувати cpuset для проблем шумних сусідів?
Іноді. Це потужно і детерміновано, але легко зробити погано. Почніть з quota і shares; переходьте до cpuset, коли потрібна жорстка ізоляція і ви розумієте топологію CPU.
Висновок: практичні наступні кроки
Коли Docker‑хости досягають 100% CPU, виграшний хід — не «рестартувати все, поки графік не стане милий». Виграшний хід: знайти точний контейнер (і PID), підтвердити, чи він робить реальну роботу або нісенітницю, потім обмежити з наміром і перевірити поведінку тротлінгу.
Наступного разу, коли хост «пече», зробіть це:
- Використайте
top, щоб отримати найгарячіші PID, зіставте їх з контейнерами через cgroups. - Розглядайте
docker statsяк підказку, а не вирок. - Перед капом вирішіть, чи навантаження latency‑чутливе чи орієнтоване на throughput.
- Застосуйте
docker update --cpusдля швидкого стримування, потім підтвердіть черезcpu.stat, чи тротлінг прийнятний. - Після інциденту закріпіть ліміти, автоматизуйте перевірки і вирівняйте concurrency воркерів до CPU‑контракту.
CPU‑емергенсії рідко містять в собі секрет. Зазвичай це просто недостатня інструментованість, надмірні припущення і відсутність лімітів. Виправте ці три речі — і ваші on‑call ротації стануть коротшими і трохи менш поетичними.