Ви не помічаєте CPU, поки він не починає кричати. Графік виглядає нормально, потім приходить промо-лист, QPS потроюється,
і ваші сервери бази даних перетворюються на маленькі обігрівачі. Латентність стрибає. Таймаути накопичуються. Хтось задає найгірше
питання в експлуатації: «Це база даних чи застосунок?»
Це не матеріал для змагань «хто швидше». Під піковим навантаженням і MariaDB, і PostgreSQL можуть
швидко «зжерти» ядра — просто роблять це по-різному й у передбачуваний спосіб. Мета — визначити, у якому режимі відмови ви знаходитесь,
довести це командами і зробити виправлення, яке переживе наступний пік, щоб ваш on-call не став новим хобі.
Що насправді означає «зжирати ядра» під піком
Високе завантаження CPU — це не одна проблема; це родинна зустріч проблем. Насправді «сплески CPU» в базах даних походять від:
(1) виконання реально корисної роботи (запитів), (2) виконання роботи, якої ви не планували (погані плани, надмірні сортування,
повторне парсування), або (3) нічого продуктивного, але все одно крутяться ресурси (контенція, очікування блокувань з busy loops,
надмірні контекстні переключення).
Під піковим навантаженням один і той же запит може змінити статус з «ситий» на «поглинач ядер», бо форма системи змінюється:
гарячі набори ростуть, кеші промахуються, черги блокувань формуються, або план змінюється з індексного пошуку на сканування через
застарілі статистики або інший параметр прив’язки. Ваша база даних не стала дурнішою. Вона стала чесною.
Коли питають «хто швидше зжирає ядра», мають на увазі: «Який сервер досягає насичення CPU швидше і сильніше падає при рості конкурентності?»
Неприємна відповідь: залежить від того, як ви її годуєте. MariaDB (InnoDB) часто страждає, коли додають забагато конкурентних записів
або занадто багато підключень без пулінгу; PostgreSQL часто страждає, коли накопичується debt autovacuum або коли регресія плану
перетворює вузький запит на широке сканування, що множить роботу по ядрах. Обидва можна налаштувати. Обидва можна неправильно налаштувати в пожежу.
Корисна ментальна модель: проблеми CPU під піком рідко просять про сирий пропуск. Вони про координаційні накладні витрати.
Додаток просить виконати роботу; база даних координує доступ до спільного стану. Саме ця координація «з’їдає» ядра.
Одна цитата, варта стікера, бо описує більшість сплесків CPU в продакшені:
латентність — симптом, а не корінна причина
(парафраз ідеї, часто пов’язуваної з методологією продуктивності Brendan Gregg).
Ваше завдання — знайти корінь, а не сперечатися з графіком.
Як MariaDB витрачає CPU, коли панікує
MariaDB у продакшені зазвичай означає MariaDB + InnoDB (історично XtraDB). InnoDB добре робить багато речей:
відновлення після збою, блокування на рівні рядка і підтримка типової OLTP-моделі. Але пікове навантаження завжди знаходить шви.
Поширені шаблони сплесків CPU у MariaDB
1) Накладні витрати конкурентності: потоки, м’ютекси та «занадто багато клієнтів»
MariaDB використовує модель «потік на підключення». Коли ви масштабували кількість підключень лінійно з трафіком, ви додаєте не лише
роботу запитів — ви додаєте роботу планування. При високій конкурентності CPU їсться контекстними переключеннями і координацією блокувань
всередині движка. Якщо ви бачите високий CPU при помірному QPS, підозрівайте треш: багато runnable-потоків і недостатньо реального прогресу.
Якщо ви працюєте без thread pool (або з неправильно налаштованим), MariaDB може виглядати так, ніби «використовує CPU», виконуючи все менше й менше корисної роботи.
Ви помітите це у чергах ОС і в власних лічильниках статусу бази.
2) Contention у InnoDB: гарячі рядки, гарячі індекси та внутрішні засувки
Під піковим навантаженням записів з’являються гарячі точки: автоінкрементний первинний ключ під сильними вставками, один «останній» рядок,
який постійно оновлюють, або вторинні індекси, що підтримуються при кожному записі. Це реальна робота плюс координація. Якщо додаток
створює один гарячий рядок або гарячу сторінку індексу, ви можете наситити CPU, а продуктивність перестане зростати.
InnoDB покращився з часом у бік тоншого блокування, але все ще є місця, де контенція проявляється у вигляді стрибків CPU і затримок.
Багато з них формуються робочим навантаженням, а не версією ПЗ.
3) Виконання запитів: погані плани та дорогі сортування
MariaDB може швидко виконувати хороші речі, але також робити класичні помилки під тиском:
сортування великих проміжних наборів, сканування через відсутні індекси або повторення ідентичної роботи, бо додаток
надсилає трохи різний текст запитів, що ламає кеш запитів (якому не варто надто довіряти).
4) Реплікація та накладні витрати binlog
Пікове навантаження часто співпадає з відставанням реплік. Якщо ваш primary насичений CPU, він не може скинути binlog, не може тримати
репліки в курсі і не може швидко підтвердити транзакції. Поліпшення group commit допомагають, але ви все одно платите CPU за row-based логування
і за транзакційний механізм навколо нього. «Це лише запис у лог» — відомі останні слова.
Підписний режим відмови MariaDB під піком
Коли MariaDB колапсує від CPU під піком, це часто виглядає як «всі зайняті, ніхто не щасливий»:
купа підключень, багато потоків, високий system CPU і крива пропускної здатності, що перестає масштабуватися при додаванні ядер.
Ваше вирішення зазвичай — зменшити конкурентність і прибрати точки контенції, а не додавати ядра й молитися.
Перший жарт (дозволено, і так, він релевантний): База даних без connection pooling — як відкритий бар без барменів: всім технічно
налито, але нічого доброго не відбувається швидко.
Як PostgreSQL витрачає CPU, коли панікує
Архітектура PostgreSQL відома своєю консервативністю та глибокою інженерією: процес-на-підключення, shared buffers,
MVCC, потужний планувальник і фонові воркери для обслуговування. Під піком він часто відмовляє більш «логічно», ніж MariaDB:
буде витрачати CPU, виконуючи саме те, що, на його думку, ви попросили — і це іноді є проблемою.
Поширені шаблони сплесків CPU у PostgreSQL
1) Регресія плану: той самий запит, інший план, раптово дорого
Планувальник PostgreSQL потужний і іноді занадто впевнений. Якщо статистики дрейфують, розподіл даних змінюється або
параметри прив’язки сильно відрізняються, план може переключитися з індексного nested loop на hash join або на послідовне сканування.
Під піком таке перемикання може помножити роботу у багато разів, і CPU йде слідом.
PostgreSQL дає відмінні інструменти для перегляду плану й таймінгів. Пастка — не глянути до інциденту й потім виявити, що ви ніколи не знімали базові метрики.
2) Заборгованість autovacuum: ви не платите — воно все одно нараховує відсотки
MVCC означає, що мертві кортежі накопичуються, доки vacuum їх не прибере. Якщо autovacuum не встигає — через обмеження IO,
невірні налаштування або просто через пікову хвилю записів — сканування зачіпають більше сторінок, індекси блоатяться, і CPU
росте, бо кожний запит проходить через сміття. Ви побачите вищий CPU навіть для «читань», бо читання тепер роблять більше роботи на рядок.
3) Накладні витрати конкурентності: багато підключень — багато процесів
Модель «процес-на-підключення» PostgreSQL стабільна й зручна для відладки, але кожен бекенд — це реальний процес. Забагато
підключень — більше контекстних переключень, більший пам’ятний слід і CPU, витрачений на координацію, ще до виконання SQL.
При піку трафіку ви можете врізатися в стіну, де CPU горить на плануванні процесів і управлінні блокуваннями.
4) Сортування, хешування та тиск на пам’ять (мінне поле work_mem)
PostgreSQL може «спалювати» CPU на сортуваннях і хеш-операціях. Якщо work_mem занадто низький, операції виливаються
на диск і ви отримуєте суміш IO wait та CPU-накладних витрат. Якщо work_mem занадто високий і є багато конкурентних запитів,
виникає тиск на пам’ять, і ядро починає робити власні «оптимізації», через що завантаження CPU стає хаотичною інсталяцією мистецтва.
Підписний режим відмови PostgreSQL під піком
Коли PostgreSQL колапсує від CPU під піком, це часто виглядає так: кілька форм запитів домінують, вони виконуються довше, ніж зазвичай,
autovacuum відстає, блоат росте, і все повільніше. Це петля зворотного зв’язку: довші запити тримають ресурси довше, що підвищує контенцію
і заборгованість vacuum, що знову подовжує запити. Ваше вирішення зазвичай — виправити найгірші запити і тримати vacuum у формі, а не «крутити випадкові ручки».
Плейбук швидкої діагностики (перші/другі/треті перевірки)
Перше: це «справжня робота» на CPU чи «треш»?
- Висока черга виконання ОС (load average значно вище за кількість ядер, багато runnable задач): ймовірно треш або надмірна конкурентність.
- Один або кілька «гарячих» потоків/процесів: ймовірно конкретні запити, vacuum, реплікація або локальний hotspot.
- Високий system CPU: контекстні переключення, накладні витрати ядра (мережа, файлові системи), spinlock-и.
- Високий user CPU: виконання запитів, сортування, хешування, обчислення виразів.
Друге: визначте топ-формати запитів і причини очікування
- MariaDB: дивіться processlist, InnoDB status, метрики handler/read і чи прив’язане обмеження до планування потоків.
- PostgreSQL: дивіться
pg_stat_activity, топ SQL уpg_stat_statementsі індикатори vacuum/bloat.
Третє: вирішіть, чи вузьке місце тільки в CPU або «CPU — симптом»
- Якщо IO насичено, CPU може стрибати через повтори, churn кешів і накладні витрати ядра. Виправляйте IO першочергово.
- Якщо домінують блокування, сплески CPU — лише видима частина. Виправляйте порядок блокувань, гарячі рядки і тривалість транзакцій.
- Якщо сталася регресія плану, нічого іншого не має значення, поки цей план не виправлено.
Практичні завдання: команди, виводи та рішення (12+)
Це те, що я насправді запускаю під час інциденту або відразу після. Кожне має: команду, приклад виводу, що це означає та рішення.
Вони навмисно нудні. Нудність — це шлях менше відпрацювань.
Завдання 1: Підтвердити, чи ви наситили CPU або просто шум
cr0x@server:~$ uptime
14:22:01 up 31 days, 3:07, 2 users, load average: 42.18, 39.77, 28.54
Що це означає: Load average ~42 на машині з 16 ядрами означає, що runnable або uninterruptible задач значно більше, ніж CPU.
Рішення: Перестаньте гадати. Негайно перевірте чергу виконання та топ-процеси. Якщо кількість підключень величезна — почніть обмежувати конкурентність (pooling, max connections), поки діагностуєте.
Завдання 2: Подивитися розбивку CPU (user vs system) і чергу виконання
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
32 1 0 812344 92324 6231880 0 0 120 410 9800 42000 78 18 2 2 0
29 0 0 805112 92328 6239024 0 0 40 210 9600 39000 81 16 1 2 0
Що це означає: r близько 30+ вказує на тиск у черзі runnable; високий cs означає сильні контекстні переключення; високий sy натякає на накладні витрати ядра.
Рішення: Якщо cs величезний і БД має тисячі підключень — в пріоритеті connection pooling і ліміти. Якщо wa високий — перевірте сховище/IO.
Завдання 3: Визначити топ-споживачів CPU і чи їх багато чи мало
cr0x@server:~$ ps -eo pid,comm,%cpu,%mem,stat --sort=-%cpu | head
2314 mariadbd 690.3 18.2 Sl
4481 postgres 189.4 1.7 R
4520 postgres 176.0 1.6 R
911 node 42.1 0.8 R
Що це означає: Один великий процес mariadbd, що «жере» багато ядер, натякає на внутрішню контенцію або сильне навантаження в одному процесі; багато бекендів postgres, що споживають CPU, натякає на кілька дорогих запитів, що виконуються паралельно.
Рішення: Якщо один DB-процес домінує: фокус на контенції движка й плануванні потоків (MariaDB) або на конкретному фоновому воркері (Postgres). Якщо багато бекендів домінують: знайдіть топ-форму SQL.
Завдання 4: Заміряти контекстні переключення та міграції (індикатор трешу)
cr0x@server:~$ pidstat -w -p $(pgrep -o mariadbd) 1 3
Linux 6.1.0 (server) 12/30/2025 _x86_64_ (16 CPU)
01:22:10 PID cswch/s nvcswch/s Command
01:22:11 2314 1200.0 38000.0 mariadbd
01:22:12 2314 1105.0 40120.0 mariadbd
Що це означає: Масивні вимушені контекстні переключення (nvcswch/s) зазвичай означають, що планувальник жонглює runnable-потоками/процесами. Це «зжирає» CPU без пропорційного виконання роботи.
Рішення: Зменшіть конкурентність: введіть pooling, зменшіть max connections, увімкніть/налаштуйте thread pool (MariaDB), налаштуйте pooler (Postgres через pgbouncer) і знайдіть контенцію блокувань.
Завдання 5 (MariaDB): Хто зараз що виконує?
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
Id: 81423
User: app
Host: 10.2.4.18:51214
db: prod
Command: Query
Time: 12
State: Sending data
Info: SELECT ... FROM orders WHERE customer_id=? ORDER BY created_at DESC LIMIT 50
*************************** 2. row ***************************
Id: 81451
User: app
Host: 10.2.4.19:52108
db: prod
Command: Query
Time: 12
State: Sorting result
Info: SELECT ... FROM orders WHERE status='open' ORDER BY priority DESC
Що це означає: Стані як Sorting result і тривалий Time під піком зазвичай вказують на відсутні або неефективні індекси; Sending data може означати великі набори результатів або повільні клієнтські зчитування.
Рішення: Зафіксуйте топ-2–3 запити. Запустіть EXPLAIN, перевірте індекси і розгляньте тимчасове обмеження/таймаути для зупинки кровотечі.
Завдання 6 (MariaDB): Перевірити контенцію InnoDB і тиск на purge
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-30 14:24:51 0x7f1f6c0a9700 INNODB MONITOR OUTPUT
=====================================
Mutex spin waits 1203328, rounds 3821140, OS waits 44211
RW-shared spins 922110, rounds 1102100, OS waits 21011
History list length 884321
...
Що це означає: Високі spin waits і OS waits можуть означати контенцію; дуже велика history list length свідчить, що purge відстає (довгі транзакції або недостатній прогрес purge), що може надути роботу для читань і індексів.
Рішення: Знайдіть довгі транзакції, скоротіть час транзакцій і оцініть, чи записове навантаження або гарячі рядки викликають контенцію. Якщо history list вибухає — шукайте сесію, що тримає старий снапшот.
Завдання 7 (MariaDB): Швидко перевірити тиск потоків/підключень
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+--------+
| Variable_name | Value |
+-------------------+--------+
| Threads_connected | 1850 |
| Threads_running | 280 |
| Threads_created | 992134 |
+-------------------+--------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 4000 |
+-----------------+-------+
Що це означає: Тисячі підключених потоків і високий Threads_created часто корелюють з CPU, витраченим на обробку підключень і планування.
Рішення: Обмежте підключення до того, що хост реально в змозі спланувати. Впровадьте пулінг. Якщо мусите приймати багато клієнтів, використовуйте thread pool і тримайте Threads_running ближче до кількості ядер, а не в 10 разів більше.
Завдання 8 (PostgreSQL): Подивитися активні запити та їх події очікування
cr0x@server:~$ psql -XAt -c "SELECT pid, state, wait_event_type, wait_event, now()-query_start AS age, left(query,120) FROM pg_stat_activity WHERE state<>'idle' ORDER BY age DESC LIMIT 10;"
4481|active|||00:00:17.182913|SELECT ... FROM orders WHERE customer_id=$1 ORDER BY created_at DESC LIMIT 50
4520|active|LWLock|buffer_mapping|00:00:12.504991|SELECT ... FROM events WHERE tenant_id=$1 AND ts>$2 ORDER BY ts DESC LIMIT 200
4602|active|Lock|transactionid|00:00:09.991221|UPDATE accounts SET balance=balance-$1 WHERE id=$2
Що це означає: Активний стан без wait event — чиста CPU-робота. LWLock waits можуть вказувати на внутрішню контенцію (buffer mapping, WAL тощо). Lock waits означають, що ви стоїте за конкурентністю, а не за обчисленнями.
Рішення: Якщо більшість активні на CPU — знайдіть топ SQL і плани. Якщо домінують lock waits — скоротіть час транзакцій і виправте порядок блокувань/гарячі рядки. Якщо LWLock hotspot-и — дивіться на надмірний churn shared buffers, високу конкурентність або кілька шаблонів, що б’ють по спільним структурам.
Завдання 9 (PostgreSQL): Визначити топ-CPU-запити через pg_stat_statements
cr0x@server:~$ psql -X -c "SELECT calls, round(total_exec_time::numeric,1) AS total_ms, round(mean_exec_time::numeric,2) AS mean_ms, rows, left(query,120) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 8;"
calls | total_ms | mean_ms | rows | left
-------+----------+---------+--------+------------------------------------------------------------
93210 | 982344.6 | 10.54 | 186420 | SELECT ... FROM orders WHERE customer_id=$1 ORDER BY created_at DESC LIMIT 50
11234 | 621990.2 | 55.36 | 11234 | SELECT ... FROM events WHERE tenant_id=$1 AND ts>$2 ORDER BY ts DESC LIMIT 200
2011 | 318221.0 | 158.25 | 2011 | SELECT ... FROM line_items JOIN products ON ...
Що це означає: Загальний час показує, що споживає сервер, а не те, що «найповільніше одинично». Під піком топ-умовами за total ви вбивці CPU.
Рішення: Візьміть топ-2 запити, виконайте EXPLAIN (ANALYZE, BUFFERS) в не-піковий час або на репліці й виправляйте плани/індекси першочергово.
Завдання 10 (PostgreSQL): Перевірити сигнали тиску autovacuum і bloat
cr0x@server:~$ psql -X -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
relname | n_live_tup | n_dead_tup | last_autovacuum | last_autoanalyze
-----------+------------+------------+----------------------------+----------------------------
orders | 81203421 | 22190311 | 2025-12-29 03:12:44+00 | 2025-12-29 03:22:01+00
events | 190233442 | 80311220 | | 2025-12-28 22:10:08+00
Що це означає: Таблиця з величезною кількістю мертвих кортежів і без недавнього autovacuum — пастка для CPU: скани й обхід індексів роблять більше роботи на кожний корисний рядок.
Рішення: Покращуйте пропускну здатність autovacuum (cost limits, workers, IO), скорочуйте довгі транзакції і розгляньте цілеспрямоване вакуумування в контролювані вікна. Якщо ситуація екстремальна — плануйте технічне вікно для VACUUM FULL або REINDEX з обережністю.
Завдання 11: Підтвердити, чи IO — реальне вузьке місце (CPU — симптом)
cr0x@server:~$ iostat -x 1 3
Device r/s w/s rKB/s wKB/s await svctm %util
nvme0n1 820.0 610.0 98240.0 110820.0 7.10 0.32 92.40
Що це означає: Високий %util і зростаючий await вказують на насичення IO. Сплески CPU можуть супроводжувати це через churn буферів і підвищену роботу запитів із-за промахів кеша.
Рішення: Якщо сховище насичено — припиніть «тонке» налаштування CPU. Зменшіть IO: додайте індекси, щоб уникнути сканів, збільшіть hit rate кеша, виправте bloat або масштабуте сховище/репліки.
Завдання 12: Перевірити тиск на пам’ять і свапінг (мовчазний вбивця CPU)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 125Gi 96Gi 1.2Gi 2.1Gi 27Gi 18Gi
Swap: 16Gi 3.8Gi 12Gi
Що це означає: Використання свапу під час піка може перетворити графіки CPU на брехню: CPU «зайнятий», але чекає на повернення сторінок з свапу.
Рішення: Зменшіть пам’ятний слід (менше підключень, правильно виміряні буфери), припиніть неконтрольований work_mem (Postgres) і виправте корінні шаблони запитів, що викликають великі сортування/хеші.
Завдання 13: Зхопити hotspot-и на рівні ядра з perf (коли потрібен доказ)
cr0x@server:~$ sudo perf top -p $(pgrep -o mariadbd)
Samples: 915 of event 'cycles', 4000 Hz, Event count (approx.): 145392102
18.40% mariadbd [.] btr_cur_search_to_nth_level
12.15% mariadbd [.] row_search_mvcc
7.92% libpthread-2.31.so [.] pthread_mutex_lock
6.71% mariadbd [.] lock_rec_lock
Що це означає: Бачити mutex-функції високо в стеку означає контенцію; бачити B-tree пошук домінуючим — означає індексні обходи при читанні (можливо через промахи кеша або неефективні шаблони доступу).
Рішення: Якщо mutex домінує — зменшуйте конкурентність і гарячі точки. Якщо B-tree пошук домінує — покращуйте індекси, зменшуйте випадкові пошуки і збільшуйте hit rate кеша (buffer pool/shared buffers) та робочу множину.
Завдання 14 (PostgreSQL): Довести проблему плану за допомогою EXPLAIN (ANALYZE, BUFFERS)
cr0x@server:~$ psql -X -c "EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id=123 ORDER BY created_at DESC LIMIT 50;"
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Limit (cost=0.56..42.89 rows=50 width=312) (actual time=0.212..1.903 rows=50 loops=1)
Buffers: shared hit=108 read=24
-> Index Scan using orders_customer_created_idx on orders (cost=0.56..71234.12 rows=84123 width=312)
Index Cond: (customer_id = 123)
Planning Time: 0.295 ms
Execution Time: 1.982 ms
Що це означає: Співвідношення reads vs hits показує поведінку кеша. Якщо ви бачите послідовне сканування з величезною кількістю reads там, де очікували індексний скан — це ваш CPU-сплеск у маскарадному плащі.
Рішення: Виправте індекси, статистики (ANALYZE) і форму запиту. Якщо цей запит критичний — закріпіть його: стабільні індекси, уникати чутливості до параметрів або додати обмеження.
Три корпоративні міні-історії з передової
Інцидент 1: Аутейдж через неправильне припущення
Середня SaaS-компанія мігрувала білінгову підсистему з MariaDB на PostgreSQL. Міграція пройшла гладко в staging.
У продакшені теж спочатку було добре — поки не прийшов перший кінець кварталу і не стартував раунд рахунків. CPU вдавився, API таймаути,
листи у підтримку. Постмортем мав знайомий запах: «Ми припустили, що база буде масштабуватись, як стара».
Припущення було в тому, що більше підключень означає більше пропускної здатності. У MariaDB вони болісно навчилися лімітувати підключення і
використовувати пулінг. Під час міграції з’явився новий сервіс із політикою «просто відкривай підключення на запит, це нормально», бо команда
не хотіла займатися пулінгом через дедлайн. Бекенди Postgres помножилися. Контекстні переключення пішли божевільно. CPU згорів
і латентність запитів виросла.
Найбільш дратівлива частина: запити не були повільними поодинці. Вони повільні разом. У піку накладні витрати планування і черги блокувань
підсилювали все. Autovacuum також відстав під час штурму записів рахунків, додаючи bloat і погіршуючи читання саме тоді, коли портали клієнтів були найактивніші.
Виправлення не було екзотичним. Вони ввели connection pooler, встановили жорсткий ліміт активних сесій на сервіс і зробили back-pressure
у запитах першокласною функцією, а не соромною таємницею. CPU впав настільки, що справжні вузькі місця виявилися й були вирішувані.
Інцидент 2: «Оптимізація», що обернулась проти
E-commerce платформа на MariaDB мала сервіс кошика з інтенсивними записами. Хтось «оптимізував», додавши три композитні вторинні індекси
для підтримки нових звітних запитів. Індекси зробили звітні ендпоінти швидшими в тестуванні. Це було відзначено в Slack і потім мержнуто.
Прийшов пік. Після ранкової хвилі трафіку CPU стрибнув. Першими повільнули записи, потім платежі почали таймаутитись. БД була не IO-забита.
Вона була CPU-забита у storage engine, виконуючи обслуговування індексів і б’ючись у контенцію на тих самих гарячих листах індексу, які торкалися
всі оновлення кошика. Платформа ненавмисно перетворила таблицю, оптимізовану для записів, на багатодоріжний податок на запис при піку.
Плану відкату не було. Вони не могли миттєво видалити індекси без ризику тривалих блокувань і ще більшого болю. Термінове пом’якшення — направити
звіти на репліку і тимчасово відключити звітні фічі. Потім у контрольованому вікні вони прибрали найгірший індекс і замінили його на іншу форму,
що підтримувала потрібний запит, але зменшувала write amplification.
Урок не в тому, щоб «не індексувати». Урок — «індексування — це фіча write-path». Якщо ви не можете пояснити вартість індексу для записів під піком,
ви ще не зробили роботу.
Інцидент 3: Нудна, але правильна практика, що врятувала день
Фінтех-компанія на PostgreSQL мала правило: кожна зміна схеми має включати (a) очікувану зміну плану запиту,
(b) шлях відкату і (c) канарковий валідаційний запит. Нікому не подобалося правило. Це був папірець із пейджером.
Одної п’ятниці було розгорнуто, здавалося б, нешкідливу зміну: новий предикат на великій таблиці. Канарковий запит запустився автоматично після деплою
і порівняв форму плану з базовою. Він виявив несподіване послідовне сканування. Ще не в продуктивному трафіку — лише в канарці.
Ролбек зупинився сам.
На ноуті розробника індекс використовувався. У продакшн статистики були застарілими; розподіл відрізнявся; планувальник обрав інший варіант.
Команда виконала ANALYZE на ураженій таблиці в контрольованому режимі, підправила один індекс під новий предикат
і повторно запустила канарку. План повернувся до передбачуваного.
Нічого драматичного не сталося. Ось у чому суть. Інцидент, який не відбувся — найкращий: тихий, трохи нудний і фінансово непомітний.
Цікаві факти та історичний контекст (8 пунктів)
- Історія розлому MySQL важлива: MariaDB була створена оригінальними засновниками MySQL після купівлі Sun компанією Oracle, частково щоб зберегти життєздатний community-форк.
- InnoDB не завжди був «де-факто за замовчуванням»: Ранні розгортання MySQL широко використовували MyISAM; InnoDB став домінувати з ростом транзакційних навантажень і вимог надійності.
- Походження PostgreSQL академічне: Походить від проєкту POSTGRES у UC Berkeley, з довгою традицією інженерії, орієнтованої на коректність.
- MVCC — і подарунок, і рахунок: Дизайн MVCC у PostgreSQL уникає багатьох read-блокувань, але вимагає постійного vacuum; пропустіть прибирання — і витрати CPU з’являться пізніше.
- Моделі thread vs process формують поведінку під піком: Потоки MariaDB роблять накладні витрати на підключення легшими, ніж повні процеси, але все одно вразливі до планування/контенції при високій конкурентності.
- Postgres значно покращився в паралелізмі з часом: Сучасний PostgreSQL може використовувати паралельні запити в більшій кількості випадків, що підвищує пропускну здатність, але також може швидше «зжерти» ядра, якщо поганий план пішов у паралель.
- MariaDB і MySQL розійшлися у функціоналі: Залежно від версій MariaDB може пропонувати іншу поведінку оптимізатора, опції thread pool і інструменти інструментації, що змінює прояви сплесків CPU.
- Обидві екосистеми навчилися на практиці про «занадто багато з’єднань»: Операційна практика pooling і back-pressure стала загальним фольклором, бо фізика переконлива.
Другий жарт (і останній, за правилами): Налаштовувати базу даних під час аварії — як міняти шини в літака під час польоту.
Можливо, але всі запам’ятають ваш вираз обличчя.
Типові помилки: симптом → корінь → виправлення
1) Симптом: CPU 90–100%, QPS плаский, латентність зростає
Корінь: Треш конкурентності (занадто багато підключень/сесій), накладні витрати планувальника, внутрішня контенція.
Виправлення: Обмежити підключення; впровадити пулінг; зменшити паралелізм на боці додатку; додати back-pressure. Для MariaDB — розглянути thread pool; для Postgres — використовувати pooler і встановити розумний max_connections.
2) Симптом: сплески CPU прив’язані до одного endpoint або job
Корінь: Одна форма запиту стала дорогою (регресія плану, відсутній індекс, поганий порядок джойнів).
Виправлення: Зафіксуйте запит, запустіть EXPLAIN/ANALYZE, додайте/підправте індекси, виправте sargability предикатів, оновіть статистики. Додайте запобіжник: statement timeout, обмеження запитів або feature flag.
3) Симптом: CPU високе, багато lock waits, пропускна здатність колапсує
Корінь: Довгі транзакції; гарячі рядки; інверсії порядку блокувань; шаблони «оновлювати той самий рядок».
Виправлення: Скоротіть транзакції; виправте гарячі точки (шардінг лічильників, уникнення патернів «single-row latest»); забезпечте послідовний порядок блокувань; батчьте записи розумно.
4) Симптом (Postgres): CPU погіршується поступово упродовж днів/тижнів, а не хвилин
Корінь: Заборгованість autovacuum і bloat. Запити роблять більше роботи на результат у міру накопичення мертвих кортежів.
Виправлення: Налаштуйте autovacuum для важких таблиць; уникайте довгих транзакцій; моніторьте n_dead_tup; плануйте технічне обслуговування; розгляньте fillfactor або партиціювання для таблиць з великим churn.
5) Симптом (MariaDB): CPU високе під час записів, реплікація відстає
Корінь: Write amplification від індексів/binlog; накладні витрати commit-шляху; fsync-тиск; гарячі сторінки індексу.
Виправлення: Видалити непотрібні вторинні індекси; оптимізувати батчування транзакцій; перевірити durable-настройки; забезпечити швидке сховище; налаштувати group commit; перемістити читання/звітність з primary.
6) Симптом: CPU високе, але клієнти бачать «Sending data» / повільні читання
Корінь: Великі набори результатів; неефективна пагінація; клієнтська повільність, що змушує сервер тримати ресурси довше.
Виправлення: Обмежуйте розмір результатів; використовуйте keyset-пагінацію; вибирайте лише необхідні колонки; виправте N+1 патерни; додайте таймаути та обмеження рядків.
7) Симптом: сплески CPU після розгортання «дрібної» зміни
Корінь: Зміна плану через зміну предикату або дрейф статистик; новий індекс, що змінює рішення планувальника; інша розподільчість параметрів.
Виправлення: Канарні тести планів; порівнюйте бази; аналізуйте уражені таблиці; підправляйте індекси; якщо потрібно — перепишіть запит для стабільності плану.
Контрольні списки / покроковий план для стійкого пікового навантаження
A. Під час інциденту (стабілізація першочергова)
- Зупиніть натовп: увімкніть back-pressure у додатку; відсічіть некритичний трафік; обмежте швидкість важких endpoint-ів.
- Обмежте конкурентність на межі БД: пуліть з’єднання; тимчасово зменшіть max активних сесій; пріоритезуйте критичні сервіси.
- Знайдіть топ-2 форми запитів: processlist/pg_stat_activity + топ SQL-метрики. Не ганяйтеся за 20-м.
- Перевірте накопичення блокувань: якщо домінують lock waits, вбивати випадкові запити — це не стратегія; знайдіть блокер і виправте його.
- Валідуйте IO: якщо сховище зашитий, CPU-фікси не витримають; зменшіть IO-напругу першочергово.
B. Протягом 48 годин (щоб не повторилося наступного тижня)
- Базуйте плани: захопіть EXPLAIN-плани для критичних запитів і збережіть їх разом зі сервісом.
- Відновіть здоров’я vacuum/purge: налаштування autovacuum для Postgres; контроль довгих транзакцій і видимість purge у MariaDB.
- Правильно підібрані індекси: тримайте ті, що допомагають читанню; прибирайте те, що навантажує шлях запису під піком.
- Встановіть розумні таймаути: statement timeout, lock timeout і дедлайни на боці додатку. Застряглий запит — це заразний агент.
- Тестуйте пікову конкурентність: навантажувальні тести повинні імітувати production-подібні counts підключень і реалістичний розподіл даних.
C. Архітектурні вибори, що впливають на «хто зжирає ядра швидше»
- Якщо можна агресивно пулити: PostgreSQL стає спокійнішим під піком, бо ви лімітуєте бекенди і захищаєте планувальник.
- Якщо клієнтів не контролювати: MariaDB з thread pool може бути поблажливою, але все одно потребує лімітів; неконтрольована конкурентність рано чи пізно переможе.
- Якщо навантаження зосереджене на записах: Postgres потребує дисципліни autovacuum; MariaDB — дисципліни індексів і дизайну, що враховує контенцію.
- Якщо є кілька складних аналітичних запитів на OLTP: обидві системи «зжеруть» CPU; ізолюйте робочі навантаження (репліки, окремі системи) замість сподівань.
FAQ
1) То хто швидше «зжирає» ядра: MariaDB чи PostgreSQL?
При неконтрольованій конкурентності обидві можуть інцентувати CPU. MariaDB часто сильно б’є по контенції і плануванню потоків, коли
на неї кидають тисячі активних підключень. PostgreSQL частіше «зжереться» швидше, коли регресія плану або заборгованість autovacuum
помножують роботу на запит, а паралельні плани можуть посилити це. «Переможе» та, яку ви експлуатуєте найменш обережно.
2) Чому CPU стрибає, коли QPS майже не змінився?
Бо робота на запит зросла. Типові причини: регресія плану, зростання промахів кеша, bloat/мертві кортежі або контенція блокувань,
що подовжує час виконання. Такий самий QPS, але більше CPU-секунд на запит — вище навантаження.
3) Чи завжди високе CPU — це погано?
Ні. Високий CPU з стабільною латентністю і передбачуваною пропускною здатністю — це нормально; ви використовуєте те, за що заплатили.
Поганий випадок — це високий CPU з ростом латентності і падінням пропускної здатності: тут координаційні накладні або помножена робота.
4) Чи допоможе додавання ядер вирішити пікові сплески CPU?
Іноді, але це найменш надійне рішення. Якщо ви CPU-bound на чистому виконанні запитів і масштабування лінійне — більше ядер дасть запас.
Якщо ви обмежені контенцією, блокуваннями або контекстними переключеннями — більше ядер здебільшого означатиме більший рахунок, а інцидент залишиться.
5) Яка найкраща практика, щоб запобігти колапсу CPU?
Connection pooling з жорсткими лімітами та back-pressure. Це примушує систему поводитись передбачувано під піком. Без цього ви граєте в лотерею конкурентності.
6) Для PostgreSQL: як зрозуміти, що це debt autovacuum?
Дивіться на зростання n_dead_tup, повільні скани, збільшення buffer reads і відставання vacuum (або блокування через довгі транзакції).
CPU повільно повзтиме вгору, бо запити торкаються більше сторінок і виконують більше перевірок видимості.
7) Для MariaDB: який найшвидший індикатор болю підключень/потоків?
Високі Threads_connected, великі контекстні переключення на рівні ОС і Threads_running, що значно перевищує кількість ядер.
Поєднайте це зі статусом InnoDB, що показує контенцію або великий history list length для додаткового підтвердження.
8) Чи допомагають репліки від сплесків CPU?
Репліки допомагають, коли домінують читання і ви можете чітко маршрутизувати трафік. Вони не вирішують CPU-навантаження write-path на primary.
Також, якщо primary в дефіциті CPU, реплікація може відставати, і репліки стануть застарілими саме тоді, коли вони потрібні.
9) Чи варто спочатку налаштовувати параметри ядра?
Тільки після того, як ви довели вузьке місце до бази даних. Налаштування ядра може допомогти (планувальник, мережа, IO-черги), але це не заміна
виправлення конкурентності, планів запитів і здоров’я vacuum/purge.
10) Що робити, якщо CPU високе, але perf показує переважно mutex/lock-функції?
Це контенція. Ви платите за координацію, а не за обчислення. Виправлення — зменшити конкурентність і прибрати гарячі точки:
скоротіть транзакції, розшардуйте гарячі лічильники, перебудуйте патерни «single-row», і лімітуйте активні сесії.
Висновок: практичні наступні кроки
Під піковим навантаженням MariaDB і PostgreSQL не «випадково» дають сплески CPU. Вони дотримуються шаблонів. MariaDB
має тенденцію карати за неконтрольовану конкурентність і ампліфікацію write-path; PostgreSQL карає за нехтування vacuum і сюрпризи планів.
Якщо ви хочете менше сплесків — перестаньте трактувати CPU як проблему й почніть трактувати його як чек.
Наступні кроки, що приносять негайний ефект:
- Встановіть жорсткі ліміти на конкурентність БД (pooling + caps) і змусьте додаток їх дотримуватись.
- Інструментуйте топ SQL за загальним часом і ставте алерти на раптові зміни форми плану або розподілу латентності.
- Тримайте господарські процеси здоровими: дисципліна autovacuum у Postgres; контроль довгих транзакцій і видимість purge у MariaDB.
- Зробіть індексування виробничим рішенням: кожен новий індекс має виправдати свою вартість для записів під піком.
- Відрепетируйте плейбук на staging із навантаженням. Перший раз запускати ці команди під час аварії — погана ідея.