Ви бачили це: сервіс отримав «більше CPU», а якось став повільнішим. Або постачальник обіцяв «вдвічі більше ядер»,
а ваш p99-затримка лише знизала плечима, немов вона на зарплаті. Тим часом панелі показують купу запасу —
але користувачі оновлюють сторінки, канал інцидентів активний, а on-call має той самий втомлений погляд у тисячу ярдів.
Переломна точка була не в той день, коли CPU отримали додаткові ядра. Вона настала тоді, коли ми колективно дізналися — часто важким шляхом —
що тактові частоти перестали нас рятувати. Відтоді продуктивність стала інженерною дисципліною, а не шопінгом.
Переломна точка: від тактів до ядер
Довгий час робота над продуктивністю фактично була процесом закупівель. Ви купували наступне покоління CPU, воно працювало на вищому
такті, і ваш застосунок магічно покращувався — навіть якщо код був як музейний експонат. Та епоха скінчилась, коли щільність тепла та
енергоспоживання стали обмеженням, а не кількість транзисторів.
Коли зростання частот сповільнилось, «швидше» перетворилось на «більше паралельності». Але паралельність — це не безкоштовний обід; це
рахунок, що приходить щомісяця, деталізований у вигляді блокувань, промахів кешу, пропускної здатності пам’яті, накладних витрат планувальника,
хвостової затримки та «чому цей потік завис на 100%?».
Ось неприємна правда: ядра не перемагають такти автоматично. Вони перемагають такти лише тоді, коли ваше ПЗ і операційна модель
вміють використовувати паралелізм без потоплення в витратах на координацію.
Сухий дотеп №1: Додавати ядра в додаток з великою кількістю блокувань — це як додати більше кас у супермаркеті, залишивши одного касира,
який наполягає перевіряти кожну купонну знижку особисто.
«Справжня переломна точка» — це коли ви перестаєте ставитися до CPU як до скалярної величини й починаєте розглядати машину як систему:
CPU і пам’ять і сховище і мережа і планування ядра. У продакшені ці підсистеми не чергують роботу ввічливо.
Факти та контекст, що варто пам’ятати
Це короткі, конкретні пункти, які вберігають вас від повторення історії. Не тривіальні знання. Якоря.
-
Зростання частот досягло стелі в середині 2000-х, коли щільність потужності та відведення тепла зробили «просто підвищити такт»
проблемою надійності та упаковки, а не просто інженерним викликом. -
«Dennard scaling» перестав працювати на вашу користь: коли транзистори зменшувалися, напруга не падала так само швидко,
тож потужність на одиницю площі зросла і змусила обирати консервативні частоти. -
Мульти‑ядро було не розкішшю, а обходом проблеми. Якщо безпечно збільшити частоту не вдається, додають одиниці паралельного виконання.
Потім складність переходить у програмне забезпечення. -
Закон Амдала став операційною реальністю: серійна частка вашого робочого навантаження визначає межу масштабування,
і продакшн‑трафік любить знаходити серійну стежку. -
Ієрархія кешів стала фактором першого порядку. Поведінка L1/L2/L3, трафік когерентності кешу та NUMA‑ефекти
регулярно домінують у графіках «використання CPU». -
Спекулятивне виконання й out‑of‑order трюки дали продуктивність без вищих тактів, але також підвищили складність і поверхню вразливостей;
пізніші міграції вимог змінювали профілі продуктивності вимірювано. -
Віртуалізація та контейнери змінили значення «ядра». Планування vCPU, steal time, троттлінг CPU та галасливі сусіди
можуть зробити вузол з 32 ядрами схожим на втомлений ноутбук. -
Сховище стало швидшим, але затримка залишилась впертою. NVMe значно покращив ситуацію, але різниця між «швидко» й «повільно»
часто — це черги, поведінка файлової системи та семантика синхронізації, а не лише специфікації пристрою.
Що насправді змінилося в продакшені
1) «CPU» перестав бути вузьким місцем і став кур’єром симптомів
В епоху масштабування частот завантаження CPU було розумним проксі для «ми зайняті». У епоху ядер CPU часто лише те місце, де ви
помічаєте симптом: потік крутиться в очікуванні блокування, пауза GC, шлях ядра робить надто багато роботи на пакет, або штурм
системних викликів, викликаний дрібним I/O.
Якщо ви вважаєте високий CPU проблемою, ви налаштуєте неправильну річ. Якщо ви сприймаєте CPU як кур’єра, ви запитаєте:
яка робота виконується, на чиїй вона користь і чому саме зараз?
2) Хвостова затримка стала метрикою, що має значення
Паралельні системи чудово виробляють середні значення, що виглядають добре, поки користувачі страждають. Коли ви одночасно запускаєте
багато запитів, найповільніші кілька — утримувачі блокувань, відстаючі, холодні кеші, NUMA‑промахи, спайки черг диска — визначають досвід користувача і таймаути.
Ядра підсилюють конкуренцію; конкуренція підсилює черги; черги підсилюють p99.
Ви можете випустити «швидшу» систему, яка насправді гірша, якщо ваші оптимізації збільшують дисперсію. Іншими словами: throughput виграє демонстрації; стабільність рятує дзвінки на пейджер.
3) Планувальник ядра став частиною архітектури вашого застосунку
З додатковими ядрами планувальнику доступні більші вибори — і більше можливостей зробити вам гірше. Міграція потоків може зруйнувати кеші.
Неправильне розміщення IRQ може вкрасти цикли у ваших гарячих потоків. Cgroups і квоти CPU можуть ввести троттлінг, який схожий на «таємничу затримку».
4) Пам’ять і NUMA перестали бути «просунутими темами»
Коли у вас кілька сокетів, пам’ять — це не просто RAM; це локальна RAM проти віддаленої RAM.
Потік на сокеті 0, що читає пам’ять, виділену на сокеті 1, може отримати помітний штраф, і цей штраф наростає при насиченні пропускної здатності пам’яті.
Ваш код може бути «CPU‑bound», поки не стане «memory‑bound», і ви не помітите цього, дивлячись лише на завантаження CPU.
5) Продуктивність сховища стала питанням координації більше, ніж пристроїв
Системи зберігання також паралельні: черги, злиття, readahead, writeback, журнали, copy‑on‑write, контрольні суми.
Пристрій може бути швидким, а система — повільною, тому що ви створили ідеальний шторм з дрібних синхронних записів,
контенції метаданих або write amplification.
Як інженер зі сховища, я скажу тиху істину вголос: для багатьох робочих навантажень файлова система — це перша залежність продуктивності вашої бази даних.
Ставтеся до неї з тією ж повагою, що й до планувальника запитів.
Один цитат, який витримує перевірку в операціях:
«Сподівання — не стратегія.»
— генерал Гордон Р. Салліван
Вузькі місця: де «більше ядер» зникає
Серійнá робота: Амдал збирає свою ренту
У кожної системи є серійна частка: глобальні блокування, один лідер, один поток для компактації, один WAL‑писар, один координатор шарду,
один mutex ядра в гарячому шляху. Ваш сяючий рахунок ядер здебільшого збільшує кількість потоків, що чекають своєї черги.
Правило прийняття рішень: якщо додавання конкуруючих потоків підвищує пропускну здатність, але погіршує затримку, ви, ймовірно, влучили у серійне горлечко плюс черги.
Вам не потрібно більше ядер. Потрібно зменшити контенцію або розшардити серійний ресурс.
Контенція блокувань та спільний стан
Спільний стан — це класичний податок паралелізму: mutex‑и, rwlock‑и, атоміки, глобальні аллокатори, підрахунок посилань, пул з’єднань
та «тільки одна маленька блокування метрик». Іноді блокування не в вашому коді; воно в рантаймі, libc аллокаторі, ядрі або файловій системі.
Пропускна здатність пам’яті та когерентність кешу
Сучасні CPU швидкі в арифметиці. Вони повільніші в очікуванні пам’яті. Додаючи ядра, ви збільшуєте кількість голодних ртів,
які конкурують за пропускну здатність пам’яті. Потім з’являється трафік когерентності кешу: ядра витрачають час на погодження значення кеш‑лінії
замість виконання корисної роботи.
NUMA: коли «RAM» має географію
Проблеми NUMA часто виглядають як випадковість: один і той самий запит дає різну затримку, залежно від того, яке ядро його виконувало і де знаходиться пам’ять.
Якщо ви не закріплюєте, не виділяєте локально або не обираєте топологіє‑чутливу конфігурацію, ви отримаєте «дрейф продуктивності», який приходить і йде.
Час у ядрі: системні виклики, переключення контексту та переривання
Високі темпи переключень контексту можуть стерти вигоди від паралелізму. Багато системних викликів від дрібного I/O або надмірного логування
можуть зробити сервіс CPU‑важким, не виконуючи бізнес‑роботу. Неправильно розміщені переривання можуть прив’язати навантаження черги NIC до тих же ядер,
що запускають ваші latency‑чутливі потоки.
Введення/виведення сховища: черги та write amplification
Ядра не допоможуть, якщо ви застрягли на синхронних fsync‑шаблонах, дрібних випадкових записах або шарі зі сховищем, який підсилює записи
через copy‑on‑write та оновлення метаданих. Гірше: більше ядер може створити більше паралельних I/O, що підвищує глибину черги та затримку.
Мережа: обробка пакетів та час softirq
Якщо у вас висока PPS, вузьким місцем може бути обробка softirq, conntrack, iptables або TLS. CPU не «зайнятий» вашим кодом;
він зайнятий як помічник мережевої карти.
Сухий дотеп №2: Єдина річ, яка масштабується лінійно в моїй кар’єрі — це кількість дашбордів, що стверджують, що все в порядку.
Швидкий план діагностики
Це порядок дій, який допоможе вам швидко дістатися до вузького місця без тижня інтерпретативного танцю в Grafana. Мета — не бути хитрим; мета — бути швидким і правильним.
Спочатку: встановіть, що саме насичується (CPU vs пам’ять vs I/O vs мережа)
- Перевірте середнє навантаження (load average) щодо runnable потоків і I/O wait.
- Перевірте розбиття CPU (user/system/iowait/steal) і троттлінг.
- Перевірте затримку диска і глибину черги; підтвердіть, чи збігаються ці очікування з p99.
- Перевірте падіння/ретрансляції NIC і час softirq CPU, якщо сервіс мережево‑важкий.
По‑друге: визначте, чи ліміт серійний, спільний чи зовнішній
- Один потік на 100% в той час як інші простіють: серійна робота або гаряче блокування.
- Усі ядра помірно завантажені, але p99 поганий: черги, контенція або затримки пам’яті.
- CPU низький, але затримка висока: I/O або зовнішня залежність.
- CPU високий у режимі kernel: мережа, системні виклики, файлові системи або переривання.
По‑третє: валідуйте цілеспрямованим профілюванням (не «за відчуттями»)
- Використовуйте
perf top/perf recordдля гарячих шляхів CPU. - Використовуйте flame graphs, якщо можете, але навіть знімки стека в потрібний момент допомагають.
- Використовуйте
pidstatдля CPU по потоках і кількості переключень контексту. - Використовуйте
iostatта статистику файлової системи для розподілу затримок I/O та перевірки насичення.
По‑четверте: змініть одну змінну, виміряйте, швидко відкати
- Зменшіть конкуренцію і перевірте, чи покращиться хвіст (діагностика черг).
- Закріпіть потоки / налаштуйте IRQ affinity, якщо домінують кеш і ефекти планування.
- Обачно змінюйте стратегію синхронізації (пакетний fsync, груповий commit), якщо це безпечно.
- Масштабуйте горизонтально, коли доведено, що це не узгоджувальна межа одного вузла.
Практичні завдання з командами: виміряй, інтерпретуй, вирішуй
Ці завдання свідомо «запускаються о 03:00». Кожне включає: команду, приклад виводу, що це означає і якесь рішення.
Використовуйте їх у порядку, коли ви загубилися.
Завдання 1: Швидко перевірити насичення CPU та iowait
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
01:12:01 PM CPU %usr %nice %sys %iowait %irq %soft %steal %idle
01:12:02 PM all 42.10 0.00 12.40 8.60 0.00 2.10 0.20 34.60
01:12:02 PM 7 98.00 0.00 1.00 0.00 0.00 0.00 0.00 1.00
01:12:02 PM 12 10.00 0.00 40.00 30.00 0.00 5.00 0.00 15.00
Значення: CPU 7 фактично насичений у користувацькому просторі (ймовірно, гарячий потік). CPU 12 багато проводить в sys + iowait (ядро + очікування сховища).
Рішення: Якщо одне CPU зашкалює, шукайте серійне вузьке місце або гаряче блокування. Якщо iowait високий, переходьте до перевірки затримок диска перед налаштуванням CPU.
Завдання 2: Відокремити runnable навантаження від I/O навантаження
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
3 1 0 421312 91240 812340 0 0 1024 2048 5400 9800 41 12 35 9 3
9 0 0 418900 91300 811900 0 0 120 250 6200 21000 55 15 25 2 3
Значення: r показує runnable потоки; b — заблоковані (зазвичай I/O). Перший рядок вказує на деяке блокування, другий — на тиск CPU (r=9).
Високий cs (context switches) натякає на контенцію або надмірно балакучу конкуренцію.
Рішення: Якщо r постійно > кількість CPU, ви CPU‑bound або трешите. Якщо b стрибає, пріоритет — розслідування I/O.
Завдання 3: Виявити троттлінг CPU в контейнерах (cgroups)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 9034211123
user_usec 7011200456
system_usec 2023010667
nr_periods 120934
nr_throttled 23910
throttled_usec 882134223
Значення: Навантаження було троттлетовано в 23,910 періодах; накопичилось майже 882 секунди тротл‑часу.
Це може виглядати як «випадкова затримка» і одночасно «idle CPU».
Рішення: Якщо троттлінг суттєвий під час інцидентів, підвищте ліміти/запити CPU, вирішіть проблему шумних сусідів або перестаньте вважати «idle» доступним.
Завдання 4: Перевірити steal time на віртуальних машинах
cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (app03) 01/09/2026 _x86_64_ (8 CPU)
01:18:01 PM CPU %user %system %iowait %steal %idle
01:18:02 PM all 22.10 7.30 1.20 18.40 51.00
01:18:03 PM all 24.00 8.10 1.10 19.20 47.60
Значення: ~19% steal означає, що гіпервізор забирає час; із середини гостя ви не зможете цього відрегулювати.
Рішення: Якщо steal високий, мігруйте хости, змініть тип інстансу або зменште конкуренцію на рівні віртуалізації.
Завдання 5: Виявити процеси‑жертовників CPU та шторм переключень контексту
cr0x@server:~$ pidstat -t -p 2147 1 3
Linux 6.5.0 (api01) 01/09/2026 _x86_64_ (32 CPU)
01:21:10 PM UID TGID TID %usr %system %CPU cswch/s nvcswch/s Command
01:21:11 PM 1001 2147 2159 98.00 1.00 99.00 0.00 12.00 java
01:21:11 PM 1001 2147 2164 5.00 18.00 23.00 12000.00 8000.00 java
Значення: Один потік CPU‑bound (ймовірно гарячий цикл або серійне горлечко). Інший має багато системного часу і переключень — часто контенція блокувань, системні виклики або хаос планувальника.
Рішення: Потік CPU‑bound: профілюйте з perf. Високі переключення: перевірте блокування, поведінку аллокатора, логування і гарячі точки ядра.
Завдання 6: Знайти CPU‑гарячі точки за допомогою perf (швидкий триаж)
cr0x@server:~$ sudo perf top -p 2147
Samples: 2K of event 'cycles', Event count (approx.): 2289012345
38.12% libpthread-2.35.so [.] pthread_mutex_lock
14.55% libc-2.35.so [.] __memmove_avx_unaligned_erms
10.09% [kernel] [k] tcp_recvmsg
7.44% [kernel] [k] ext4_da_write_end
Значення: Велика частка у pthread_mutex_lock — сигнатура контенції. Гарячі точки ядра вказують, що шляхи прийому мережі та запису файлової системи також релевантні.
Рішення: Якщо домінує mutex lock — зменшіть спільний стан, збільште шардінг або змініть модель конкурентності. Якщо домінує мережа в ядрі — перевірте softirq/IRQ і швидкість пакетів.
Завдання 7: Підтвердити затримку диска та глибину черги
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme0n1 120.0 950.0 4800 81200 154.0 9.80 11.2 3.1 12.3 0.7 82.0
Значення: Високий avgqu-sz і підвищений await вказують на черги. %util на 82% свідчить, що пристрій сильно завантажений; під час стрибків затримка зростатиме.
Рішення: Якщо await росте з навантаженням, зменште тиск синхронних записів, пакетизуйте записи, налаштуйте файлову систему/ZFS або перемістіть гарячі дані/логи на швидший або виділений пристрій.
Завдання 8: Побачити процеси, що генерують I/O
cr0x@server:~$ sudo iotop -o -b -n 3
Total DISK READ: 0.00 B/s | Total DISK WRITE: 58.23 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2147 be/4 app 0.00 B/s 22.10 M/s 0.00 % 12.50 % java -jar api.jar
1762 be/4 postgres 0.00 B/s 31.90 M/s 0.00 % 18.00 % postgres: wal writer
Значення: WAL‑писар створює помітні записи; ваш застосунок також інтенсивно пише. Якщо проблеми з затримкою корелюють, ви можете бути синхронно‑обмежені або обмежені журналюванням.
Рішення: Перевірте шаблони fsync і конфігурацію сховища. Розгляньте переміщення WAL/логів на окремий пристрій або налаштування commit відповідно до вимог на надійність.
Завдання 9: Перевірити файлову систему та опції монтування (нудна правда)
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/lib/postgresql
/dev/nvme0n1p2 /var/lib/postgresql ext4 rw,noatime,data=ordered
Значення: noatime уникає зайвих записів метаданих. Ext4 у режимі ordered загалом розумний вибір для баз даних на Linux.
Рішення: Якщо бачите дивні опції (наприклад sync або вимкнені бар’єри без причини), виправте їх. Не використовуйтe флаги продуктивності як культ.
Завдання 10: Перевірити здоров’я пулу ZFS і тиск на латентність (якщо ви його використовуєте)
cr0x@server:~$ sudo zpool iostat -v tank 1 3
capacity operations bandwidth
pool alloc free read write read write
---------- ----- ----- ----- ----- ----- -----
tank 1.20T 2.30T 210 1800 8.20M 95.1M
mirror 1.20T 2.30T 210 1800 8.20M 95.1M
nvme1n1 - - 110 920 4.10M 47.6M
nvme2n1 - - 100 880 4.10M 47.5M
Значення: Висока кількість операцій запису відносно пропускної здатності натякає на дрібні записи. Міри можуть впоратися, але затримка залежить від поведінки синхронізації та наявності SLOG.
Рішення: Якщо домінують дрібні синхронні записи, оцініть окремий SLOG, recordsize і пакетизацію fsync у застосунку — не жертвуючи надійністю.
Завдання 11: Виявити тиск пам’яті та треш при звільненні
cr0x@server:~$ sar -B 1 3
Linux 6.5.0 (cache01) 01/09/2026 _x86_64_ (16 CPU)
01:33:20 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
01:33:21 PM 0.0 81234.0 120000.0 12.0 90000.0 0.0 54000.0 39000.0 72.2
Значення: Інтенсивне сканування сторінок і високий pgpgout свідчать про тиск на reclaim; великі faults вказують на реальне сторінкування на диск.
Рішення: Якщо reclaim активний під час спайків затримки, зменшіть пам’ятний слід, виправте розмір кеша або перейдіть на вузли з більшою RAM. Більше ядер не допоможе.
Завдання 12: Перевірити проблеми локальності NUMA
cr0x@server:~$ numastat -p 2147
Per-node process memory usage (in MBs) for PID 2147 (java)
Node 0 18240.3
Node 1 2240.8
Total 20481.1
Значення: Пам’ять сильно сконцентрована на Node 0; якщо потоки запускаються на обох сокетах, потоки на Node 1 часто звертатимуться до віддаленої пам’яті.
Рішення: Якщо дисбаланс NUMA корелює з затримками, розгляньте закріплення процесу до одного сокета, увімкнення NUMA‑чутливого аллокатора або налаштування розміщення потоків.
Завдання 13: Перевірити розподіл переривань і навантаження softirq
cr0x@server:~$ cat /proc/interrupts | head -n 8
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 9123401 102332 99321 88210 90111 93321 92011 88712 PCI-MSI 524288-edge eth0-TxRx-0
25: 10231 8231201 99221 88120 90211 93411 92101 88602 PCI-MSI 524289-edge eth0-TxRx-1
NMI: 2012 1998 2001 2003 1999 2002 1997 2004 Non-maskable interrupts
Значення: IRQ сконцентровані на CPU0 та CPU1 для окремих черг. Це не завжди погано, але якщо ваші гарячі потоки діляться цими CPU, ви отримаєте джиттер.
Рішення: Якщо latency‑чутливі потоки та IRQ‑важкі CPU перекриваються, встановіть IRQ affinity, щоб ізолювати їх, або перемістіть додаток з цих ядер.
Завдання 14: Підтвердити TCP‑ретрансляції та дропи (мережево‑індукована затримка)
cr0x@server:~$ netstat -s | egrep -i 'retrans|segments retransmited|listen drops|RTO' | head
124567 segments retransmited
98 timeouts after RTO
1428 SYNs to LISTEN sockets dropped
Значення: Ретрансляції і RTO створюють хвостову затримку, що виглядає як «застряглий додаток». Dропи SYN можуть виглядати як випадкові відмови з’єднань під навантаженням.
Рішення: Якщо ретрансляції стрибають під час інцидентів, перевірте насичення NIC, налаштування черг, здоров’я балансувальника, conntrack і втрати пакетів upstream.
Завдання 15: Виміряти тиск дескрипторів файлів (прихована серіалізація)
cr0x@server:~$ cat /proc/sys/fs/file-nr
24576 0 9223372036854775807
Значення: Перше число — виділені файлові дескриптори; при наближенні до лімітів ви побачите помилки і повторні спроби, що створюють дивні патерни контенції.
Рішення: Якщо ви підходите до лімітів, підвищте їх і виправте витоки. Не дозволяйте нестачі дескрипторів маскуватися під проблему масштабування CPU.
Завдання 16: Виявити одноядерні вузькі місця в метриках застосунку за допомогою top
cr0x@server:~$ top -H -p 2147 -b -n 1 | head -n 12
top - 13:41:01 up 12 days, 3:22, 1 user, load average: 6.20, 5.90, 5.10
Threads: 98 total, 2 running, 96 sleeping, 0 stopped, 0 zombie
%Cpu(s): 45.0 us, 12.0 sy, 0.0 ni, 35.0 id, 8.0 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2159 app 20 0 9856.2m 3.1g 41224 R 100.0 9.8 32:11.20 java
2164 app 20 0 9856.2m 3.1g 41224 S 23.0 9.8 10:05.88 java
Значення: Один потік, що завалює ядро, — типовий приклад «ядра не допоможуть». Цей потік — ваша межа пропускної здатності та обрив затримки.
Рішення: Профілюйте його, потім переробіть: розділіть роботу, зменшіть область блокування, шардуйте стан або винесіть роботу з обробки запиту.
Три корпоративні міні-історії (аномізовано, правдоподібно та технічно точно)
Міні‑історія №1: Інцидент через неправильне припущення
Середня SaaS‑компанія перевела свій API‑рівень з старіших 8‑ядерних інстансів на нові 32‑ядерні інстанси. Та сама пам’ять. Такий самий клас сховища.
План міграції був простим: замінити вузли, залишити ті ж пороги автоскейлінгу, насолоджуватись дешевшою вартістю за ядро.
Першого робочого дня після перемикання рівень помилок повільно зростав. Не катастрофічно — але достатньо, щоб викликати повторні спроби клієнтів.
Затримка піднялася, потім вирівнялася, потім знову піднялася. Панелі казали, що CPU «в порядку»: 40–55% по кластеру.
Керівник інциденту задав класичне питання: «Чому ми повільні, якщо CPU використовується лише наполовину?»
Неправильне припущення було в тому, що відсоток CPU лінійно відображає пропускну здатність. Насправді трапилось тонше: у застосунку був один
глобальний lock, що захищав структуру кешу, якою користувалися при кожному запиті. На 8‑ядерних машинах lock був дратівливим. На 32‑ядерних
— він перетворився на фестиваль контенції. Більше ядер означало більше одночасних претендентів, вищі темпи переключень контексту і довший час очікування на запит.
Пропускна здатність не збільшилась; хвостова затримка зросла.
Виправлення не було «додати більше CPU». Виправленням стало розшарування кешу за keyspace і використання lock striping. Також тимчасово понизили
ліміт конкуруючих запитів на вхідній межі — менше throughput на папері, краще p99 на практиці — поки не випустили зміни в коді.
Урок, що залишився: ядра підсилюють і паралельність, і витрати на координацію. Якщо ви не вимірюєте контенцію, ви вгадуєте.
А вгадувати — означає отримувати сповіщення вночі.
Міні‑історія №2: Оптимізація, що обернулась проти команди
Команда платформи даних хотіла зменшити затримку записів для сервісу інгесту. Вони помітили, що база даних витрачає час на змивання та синхронізацію.
Хтось запропонував «швидке рішення»: перемістити логи та WAL на той самий швидкий NVMe, що використовується для даних, і збільшити кількість воркерів, щоб «використати всі ядра».
Зміни впровадили поступово, невеликий тест виглядав добре: більший throughput, менша середня затримка.
Через два тижні, під час передбачуваного спайку трафіку, система почала таймаутитися. Не рівномірно — лише стільки, щоб зашкодити. Графіки вузла показали,
що %util диска зростає. iostat показав збільшення глибини черги. CPU залишався доступним. Інженери підняли thread pool, щоб «пробитися».
Це лише погіршило ситуацію.
Наслідком став крах через чергування: більше потоків породжували більше дрібних синхронних записів, що підвищувало глибину черги пристрою, що підвищувало
затримку операції, що збільшувало кількість одночасних операцій у польоті, що знову підвищувало глибину черги. Петля зворотного зв’язку.
Середній throughput виглядав прийнятним, але p99 вирвався, бо деякі запити опинялись за довгими I/O чергами.
Виправлення було навмисно нудним: обмежити конкуренцію, розділити пристрої для WAL/логів та впровадити пакетизацію fsync. Також додали алерти на await і глибину черги, а не лише на throughput.
Після цього система витримувала піки без драм.
Урок: «використати всі ядра» — це не ціль. Це ризик. Конкуренція — це ручка, і правильна настройка залежить від найбільш повільного спільного ресурсу.
Міні‑історія №3: Нудна, але правильна практика, що врятувала день
Фінтех компанія мала сервіс розрахунків, що робив великі читання, помірні записи й вимагав суворої надійності для частини операцій.
У них була звичка, що не виграє призів на хакатоні: щоквартально вони проводили репетицію ємності та режиму відмов під продакшн‑подібним навантаженням,
з чіткими рукбуками й правилом «без героїзму».
Під час однієї репетиції вони помітили нешикарну річ: p99 повільно зростав, наближаючись до пікової пропускної здатності, хоча CPU виглядав в порядку.
Вони зібрали pidstat, iostat і профілі perf і виявили легку контенцію блокувань плюс підйом глибини черги сховища під час періодичних чекпоінтних сплесків. Нічого «зламаного» — просто близько до краю.
Вони зробили дві зміни: (1) закріпили певні пулі робітників на CPU, що не перетиналися з ядрами IRQ для NIC, і (2) перенесли журнал стійкості на окремий пристрій з передбачуваною латентністю.
Також встановили явні SLO на p99 і додали алерти на троттлінг та steal time.
Через місяці реальний спайк трафіку потрапив під шумного сусіда на рівні віртуалізації. Їхні системи деградували, але залишились в межах SLO достатньо довго, щоб поступово скидати навантаження. Інші команди мали інциденти; у них — Slack‑нитка і постмортем без адреналіну.
Урок: нудні практики — це те, що робить ядра корисними. Репетируйте, міряйте правильні речі, і ви побачите уріз перед тим, як з’їхати з обриву.
Типові помилки: симптом → первинна причина → виправлення
1) Симптом: CPU «всього 50%», але затримка жахлива
Первинна причина: одноядерне горлечко, контенція блокувань або троттлінг cgroup, що маскує реальне насичення.
Виправлення: Використовуйте top -H/pidstat -t, щоб знайти гарячий потік; використовуйте perf top, щоб знайти блокування або гарячий цикл.
Перевірте /sys/fs/cgroup/cpu.stat на троттлінг. Перепроєктуйте серійний шлях; не додавайте просто інстанси.
2) Симптом: Throughput зростає з додаванням потоків, а потім раптово падає
Первинна причина: крах чергування на I/O або downstream‑залежності; конкуренція перевищила стабільний робочий регіон сервісу.
Виправлення: Обмежте конкуренцію, додайте зворотний тиск і вимірюйте глибину черги/await. Налаштуйте пул потоків вниз, поки p99 не стабілізується.
3) Симптом: Випадкові p99‑спайки після переходу на більші багатосокетні машини
Первинна причина: NUMA‑ефекти і проблеми локальності кешу; потоки мігрують і звертаються до віддаленої пам’яті.
Виправлення: Перевірте numastat. Закріпіть процеси або використайте NUMA‑чутливі аллокатори. Залишайте latency‑критичні сервіси в межах одного сокета, коли можливо.
4) Симптом: Системний час CPU зростає з трафіком, але код додатка не змінювався
Первинна причина: системні виклики і накладні витрати ядра від мережі, дрібного I/O, логування або метаданих файлової системи.
Виправлення: Використовуйте perf top для виявлення символів ядра, перевірте розподіл переривань, зменшіть частоту системних викликів (пакетизуйте, буферизуйте, робіть асинхронно),
і переоцініть обсяг логування та політику flush.
5) Симптом: Високий iowait при «швидких дисках»
Первинна причина: чергування пристрою, синхронні шаблони запису, write amplification (copy‑on‑write, дрібні блоки) або спільне використання пристрою з іншим навантаженням.
Виправлення: Підтвердіть за допомогою iostat -x і iotop. Розділіть WAL/логи, пакетизуйте fsync, налаштуйте record size файлової системи,
і переконайтеся, що підґрунтний пристрій не перевантажений.
6) Симптом: Масштабування горизонтально додає вузли, але не дає ємності
Первинна причина: централізована залежність (один писар DB, лідер‑вибори, точка координатора кешу, downstream з обмеженням).
Виправлення: Ідентифікуйте спільну залежність, розшардуйте її або правильно реплікуйте, і переконайтеся, що клієнти рівномірно розподіляють навантаження.
«Більше стейтлес‑подів» не вирішить stateful‑горлечко.
7) Симптом: Продуктивність гірша після «зроблення більш конкурентним»
Первинна причина: збільшена контенція і трафік когерентності кешу; більше потоків спричиняє більше спільних записів і false sharing.
Виправлення: Зменшіть спільний стан, уникайте гарячих атомних лічильників на шляху обробки запиту, використовуйте пакетизацію по ядру/потоку і профілюйте контенцію.
Контрольні списки / покроковий план
Покроково: доведіть, чи допоможуть ядра
-
Виміряйте хвостову затримку під навантаженням (p95/p99) і скорелюйте з розбиттям CPU (usr/sys/iowait/steal).
Якщо p99 погіршується, а CPU «доступний», підозрюйте контенцію або зовнішні очікування. -
Знайдіть обмежувальний потік або блокування:
запустітьtop -Hіpidstat -t, щоб локалізувати гарячі потоки й шторми переключень. -
Профілюйте до налаштування:
використайтеperf top, щоб ідентифікувати топ‑функції (блокування, memcpy, syscalls, шляхи ядра). -
Перевірте затримку I/O і глибину черги:
iostat -xіiotop, щоб підтвердити, чи сховище — це те, що обмежує. -
Перевірте штучні обмеження CPU:
троттлінг cgroup, steal time і обмеження планувальника можуть імітувати «поганий код». -
Підтвердіть NUMA і розміщення IRQ:
перевірте локальність за допомогоюnumastat, перевірте розподіл переривань через/proc/interrupts. -
Тільки потім приймайте рішення:
- Якщо CPU справді насичений у користувацькому часі по ядрах: більше ядер (або швидші ядра) можуть допомогти.
- Якщо домінує контенція: переробіть модель конкурентності; додаткові ядра можуть погіршити ситуацію.
- Якщо домінує I/O: виправте шлях сховища; додаткові ядра лише створять більше очікування.
- Якщо домінує мережа/ядро: налаштуйте IRQ, offloads і шлях пакетів; розгляньте швидші ядра.
Операційний чекліст: зробити поведінку мульти‑ядер предиктивною
- Встановіть явні ліміти конкуренції (на інстанс) і сприймайте їх як контроль ємності, а не як «тимчасові хаки».
- Налаштуйте алерти на троттлінг CPU і steal time; це тихі вбивці ємності.
- Відстежуйте диск
awaitі глибину черги; сам по собі throughput — брехун. - Вимірюйте переключення контексту і довжину черги runnable; високі значення часто передують болю p99.
- Тримайте гарячий стан зашардованим; не централізуйте лічильники й мапи на шляху запиту.
- Розділяйте логи, що чутливі до надійності, від масових даних, коли це можливо.
- Перевіряйте локальність NUMA на багатосокетних машинах; закріплюйте, якщо потрібна детермінованість.
- Репетируйте пікове навантаження з даними, подібними до продакшн; серійний шлях покаже себе.
FAQ
1) Чого віддати перевагу: вищій частоті чи більшій кількості ядер для latency‑чутливих сервісів?
Віддавайте перевагу вищій продуктивності на ядро, коли у вас є відомий серійний компонент, інтенсивна обробка в ядрі/мережі або код, чутливий до блокувань.
Більше ядер допомагають, коли навантаження «соромно» паралельне і спільного стану мінімум.
2) Чому CPU‑використання виглядає низьким, коли сервіс таймаутиться?
Бо сервіс може чекати: блокувань, I/O, викликів до залежностей або бути троттлетованим cgroup. Також «низьке середнє CPU»
може приховувати одне насичене ядро. Завжди дивіться по‑ядерно та по‑поточно.
3) Який найшвидший спосіб виявити контенцію блокувань?
На Linux perf top, що показує pthread_mutex_lock (або futex‑шляхи в ядрі), — сильний сигнал.
Поєднайте це з pidstat -t для переключень контексту і пер‑потоковим CPU, щоб знайти винуватця.
4) Як я зрозумію, чи я I/O‑bound чи CPU‑bound?
Якщо iostat -x показує зростаюче await і глибину черги під час спайків затримки — ймовірно I/O‑bound.
Якщо vmstat показує багато runnable потоків і низький iowait — ймовірно CPU‑bound або контенція‑bound.
5) Чи може додавання потоків зменшити затримку?
Іноді — для I/O‑важких навантажень, де конкуренція ховає час очікування. Але як тільки ви влучаєте в спільне горлечко, більше потоків підвищує черги й дисперсію.
Правильний підхід зазвичай — обмежена конкуренція плюс зворотний тиск.
6) Яка найпоширеніша пастка «ядра проти тактів» у Kubernetes?
Обмеження CPU, що викликає троттлінг. Под може показувати «використання CPU нижче ліміту», але при цьому тротлитись у піках, створюючи спайки затримки.
Перевіряйте /sys/fs/cgroup/cpu.stat всередині контейнера і корелюйте з затримкою запитів.
7) Чому більші машини іноді працюють гірше за менші?
NUMA‑ефекти, міграція планувальника і локальність кешу. Також великі машини часто привертають більше ко‑розміщених навантажень, що підвищує конкуренцію
за спільні ресурси — пропускну здатність пам’яті й I/O.
8) Чи актуальне сховище, якщо я на NVMe?
Дуже навіть. NVMe покращує базову затримку й пропускну здатність, але черги все ще існують, семантика синхронізації залишається, а файлові системи все ще виконують роботу.
Якщо ви генеруєте багато дрібних синхронних записів, NVMe просто дозволить вам швидше вдаритись об стіну черг.
9) Які метрики варто поставити на дашборд, щоб відобразити реальність «ери ядер»?
Per‑core CPU, CPU steal, троттлінг CPU, переключення контексту, довжина run queue, disk await і глибина черги, мережеві ретрансляції, і p95/p99 затримка.
Середні значення — окей, але тільки як другорядні актори.
Наступні кроки, які ви можете зробити цього тижня
Якщо ви хочете, щоб системи отримували вигоду від додаткових ядер замість того, щоб принижуватися ними, зробіть наступне — практично, а не як сподівання.
-
Додайте один панель дашборду, що показує per‑core CPU і топ‑потоки (або експортуйте per‑thread CPU для головного процесу).
Вловлюйте одноядерну стелю рано. - Налаштуйте алерти на троттлінг CPU і steal time. Якщо ви в контейнерах або VM і не алертите на це, ви обираєте сюрприз.
-
Відстежуйте диск
awaitі глибину черги разом із p99 затримкою. Якщо ви відстежуєте лише throughput, ви оптимізуєте не той тип успіху. -
Запустіть годинний «дриль по вузьким місцям»: під контрольованим навантаженням зафіксуйте
mpstat,vmstat,iostat,pidstatі короткий зразокperf.
Запишіть три основні обмежувальні фактори. Повторюйте щокварталу. - Встановіть явні ліміти конкуренції для ваших найбільш завантажених сервісів. Сприймайте ліміт як контроль стабільності; налаштовуйте його, як запобіжник.
Справжня переломна точка була не в мульти‑ядерності CPU. Вона була в момент, коли ми перестали довіряти тактам, щоб прикривати наші гріхи.
Якщо ви вимірюєте контенцію, черги і локальність — і готові знижувати конкуренцію, коли це допомагає — ядра переможуть такти.
Інакше вони просто переможуть вас.