MySQL vs PostgreSQL: «CPU 100%» — як довести, що це запити, а не обладнання

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

CPU 100% — це у виробничому середовищі еквівалент пожежної тривоги, до якої неможливо дістатися: гучно, нав’язливо, і всі хочуть, щоб вона зупинилася негайно. Перший інстинкт — звинуватити «залізо» (або інстанс у хмарі, або гіпервізор, або сусідів). Часто цей інстинкт хибний.

Це польовий посібник, як довести, що насправді відбувається, коли MySQL або PostgreSQL «заїдає» CPU: чи ви обмежені CPU через виконання запитів, через фонову роботу, через блокування, або ж просто неправильно вимірюєте. Текст написано для тих, хто має пояснити свої висновки скептичним інженерам і ще більш скептичному фінансовому відділу.

Що насправді означає «CPU 100%» для баз даних

Коли хтось каже «CPU 100%», запитайте: чий CPU? Загальна система? Один ядро? Користувацький час? Час ядра? Steal time? Один гарячий потік? Дюжина робітників кожен по 80%? Від цього залежить, чи ви оптимізуєте запити, змінюєте конфігурацію або з упевненістю телефонуєте в хмарний сервіс.

CPU-bound виглядає інакше, ніж I/O-bound

Бази даних витрачають CPU з кількох передбачуваних причин:

  • Виконання запитів: сканування рядків, оцінка предикатів, обчислення агрегатів, сортування, хешування, з’єднання.
  • Планування: генерація й оцінка вартості планів запитів (зазвичай мало, але може сплескувати на складних запитах або при некоректній статистиці).
  • Конкурентність: блокування, лічильники, MVCC‑облік, цикл спінування, витрати на переключення контексту.
  • Фонова робота: vacuum, очищення purge/undo, контрольні точки, застосування реплікації, збір статистики.
  • Криптографія/стиснення: TLS, шифрування на диску, стиснення, хешування для контрольних сум.

CPU-bound означає, що база активно виконує інструкції. I/O-bound означає, що вона здебільшого чекає на читання/запис. Обидва можуть показувати «високе навантаження», але лише один вирішується «більшим CPU».

Load average — це не використання CPU. Load рахує runnable завдання плюс завдання в uninterruptible sleep (часто через диск I/O). Тож високий load при помірному використанні CPU — зазвичай історія про I/O. Високий CPU при скромному load може означати один гарячий воркер, що займає ядро.

Ще одна пастка: steal time (віртуалізація). Ваша VM думає, що зайнята, але гіпервізор відбирає цикли. Це не «запити», але виглядає як «тиск на CPU», поки ви не виміряєте.

Жарт №1: Якщо база на 100% CPU, а перше рішення — «додати індекс», вітаю — ви приєдналися до давньої релігії карго-культу продуктивності.

Мій стандарт доказу

Не потрібно мати PhD з мікроархітектури. Вам потрібні відтворювані докази, що прив’язують споживання CPU до і врешті до конкретних шаблонів запитів. Якщо ви можете надати:

  1. доказ на рівні ОС, що CPU споживає процес бази (не irq, не ядро, не steal),
  2. доказ на рівні БД, які сесії й шаблони SQL відповідальні,
  3. доказ на рівні плану, чому ці запити зараз дорогі,

…тоді ви можете сказати «це запити», не виглядаючи як здогадник.

Один операційний доказ вартий десяти переписок у Slack. Як сказав Вернер Вогельс (CTO Amazon): «Everything fails, all the time.» Ваше завдання — фейлитися з квитанціями.

MySQL vs PostgreSQL: чим відрізняється насичення CPU

MySQL і PostgreSQL обидва можуть «приплавити» CPU. Просто вони роблять це по-різному, і діагностичні важелі відрізняються.

PostgreSQL: процеси на сесію, зрозуміла інспекція

PostgreSQL зазвичай запускає один бекенд‑процес на з’єднання (плюс фон‑процеси). Це означає, що коли CPU приписано, ви часто можете швидко зіставити: «PID 12345 гарячий» → «цей PID — бекенд» → «виконує запит X». Це подарунок. Використовуйте його.

Postgres також зазвичай експонує багату телеметрію, орієнтовану на запити: pg_stat_activity, pg_stat_statements та auto_explain можуть сказати, що виконується, що дороге в часі та як виглядають плани. Якщо ви серйозні — увімкніть їх у продакшені (обережно).

MySQL: потоки в межах процесів, performance_schema — сироватка правди

MySQL більш орієнтований на потоки всередині невеликої кількості процесів, тож «PID гарячий» менш специфічний. Ви покладаєтесь на performance_schema, slow query log і дигести запитів. Якщо ви все ще користуєтеся «SHOW PROCESSLIST і відчуттями», ви літаєте з вимкненими приладами.

InnoDB додає власний смак CPU: contention mutex, churn у buffer pool, purge/undo очищення і скидання сторінок можуть створювати тиск на CPU, який не є безпосередньо «мій SELECT повільний», але все одно викликаний навантаженням і схемою.

Велика практична різниця: як швидко можна ізолювати винуватця

У Postgres ізоляція головних порушників часто починається з pg_stat_statements і закінчується EXPLAIN (ANALYZE, BUFFERS). У MySQL ви зазвичай починаєте зі statement digests і закінчуєте EXPLAIN ANALYZE (8.0+) плюс роботу з індексами і схемою. Обидві системи вимагають прийняти неприємну істину: більшість «проблем з залізом» — це фактично посилення роботи — ваші запити виконують більше роботи, ніж ви думаєте.

Швидкий план діагностики (перший/другий/третій)

Перший крок: підтвердити, що це дійсно CPU, і чий саме

  1. Перевірте розподіл CPU: user vs system vs iowait vs steal.
  2. Визначте гарячі процеси/потоки: це mysqld/postgres або щось інше?
  3. Перевірте чергу виконання: потоки runnable (обмежені CPU) чи заблоковані (I/O/блокування)?

Другий крок: зіставте гарячість ОС з активністю БД

  1. Postgres: гарячий PID → pg_stat_activity, щоб отримати запит і стан.
  2. MySQL: гарячий потік → performance_schema, щоб отримати statement digest, користувача, хост.
  3. Перевірте конкурентність: ви CPU‑насичені через багато середніх запитів або через один монструозний?

Третій крок: вирішіть, чи це регресія плану, статистики, контенція або фонова робота

  1. Регресія плану: той самий запит отримав гірший план після росту даних, зносу статистики або зміни параметра.
  2. Відсутні/невикористані індекси: «він сканує, бо може», або «використовує неправильний індекс, бо статистика бреше».
  3. Блокування/контенція: CPU може зрости через спін‑цикли, плюс пропускна здатність падає.
  4. Фонові задачі: autovacuum (Postgres), purge/флашинг (InnoDB), застосування реплікації.

Якщо ви не можете відповісти на ці три кроки за 10 хвилин, у вас не «проблема продуктивності». У вас — проблема спостережуваності.

Побудова ланцюга доказів: від ОС до тексту запиту

Коли CPU доходить до межі, люди пропонують випадкові виправлення: перезапустити БД, переключити фейловер, додати репліки, масштабувати вгору/вниз, «настроїти ядро», принести в жертву козла. Не робіть випадкових дій. Побудуйте ланцюжок доказів, який потім можна показати.

Що рахується як доказ

  • ОС: CPU зайнятий у user space (або kernel) і це процес бази даних.
  • БД: топ‑споживачі CPU корелюють з конкретними шаблонами SQL (digests) і клієнтами.
  • План: дорогий запит дорогий з певної причини (прочитані рядки, цикли, сортування, хеш‑спілли, поганий порядок з’єднань).
  • Кореляція змін: перед піком була розгортка, видалення індексу, знос статистики, ріст даних або зміна конфігурації.

Що не рахується як доказ

  • «CPU високий, тому інстанс замалий.»
  • «Мій друг сказав, що Postgres повільніший за MySQL для читань.»
  • «Ми нічого не змінювали.» (Так, ви змінили. Дані змінилися. Трафік змінився. Світ змінився.)

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

Все нижче має бути виконуване на типовому Linux‑хості з MySQL або PostgreSQL. Кожне завдання містить: команду, що шукати у виводі, і яке рішення вона підштовхне. Не виконуйте їх усі одночасно на «вмираючій» машині. Виберіть найменший інструмент, що відповідає на наступне питання.

Завдання 1: Підтвердити розподіл CPU (user/system/iowait/steal)

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:10 PM  CPU   %usr %nice  %sys %iowait %irq %soft %steal %idle
12:01:11 PM  all   92.40  0.00  6.80   0.20 0.00  0.10   0.00  0.50
12:01:11 PM    7   99.50  0.00  0.40   0.00 0.00  0.00   0.00  0.10

Значення: Високий %usr вказує на виконання/обчислення запитів. Високий %sys свідчить про роботу ядра (мережа, файлові системи, контенція). Високий %steal означає шум від сусідів/перенаселеність.

Рішення: Якщо %steal нетривіальний — припиніть сперечатися про SQL і перевірте проблеми з розміщенням хоста. Якщо домінує %iowait, то, ймовірно, ви I/O‑bound, а не CPU‑bound.

Завдання 2: Знайти найгарячіший процес

cr0x@server:~$ top -b -n 1 | head -20
top - 12:01:22 up 12 days,  3:12,  2 users,  load average: 18.32, 17.90, 16.01
Tasks: 412 total,   2 running, 410 sleeping,   0 stopped,   0 zombie
%Cpu(s): 93.1 us,  6.5 sy,  0.0 ni,  0.3 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  64218.3 total,   2311.7 free,  21342.5 used,  40564.1 buff/cache

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 5123 mysql     20   0 6432108  12.8g  49288 S 1342.0  20.5  93:21.11 mysqld

Значення: Насправді процес бази даних «їсть» CPU. Зверніть увагу: %CPU може перевищувати 100%, бо це відношення до ядра.

Рішення: Якщо це не процес БД — зупиніться. Переслідуйте справжнього винуватця (інструмент бекапу, лог‑шиппер, агент, потокове ядро).

Завдання 3: CPU на рівні потоків, щоб побачити — «один поганий актор» чи «смерть від тисячі порізів»

cr0x@server:~$ pidstat -t -p 5123 1 5
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:31 PM   UID      TGID       TID    %usr %system  %CPU   CPU  Command
12:01:32 PM   112      5123      6120   98.00    1.00 99.00     7  mysqld
12:01:32 PM   112      5123      6121   76.00    2.00 78.00     3  mysqld
12:01:32 PM   112      5123      6177   10.00    0.00 10.00     9  mysqld

Значення: Декілька потоків працюють на межі. Це часто відповідає конкретним з’єднанням або внутрішнім воркерам.

Рішення: Якщо домінує один потік — шукайте одиничну сесію/запит. Якщо багато потоків гарячі — дивіться на зсув навантаження або глобальну контенцію.

Завдання 4: Перевірити тиск черги виконання і переключення контекстів

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
18  0      0 236812  91872 41231840  0    0     3    44 4211 9802 92  6  1  0  0
21  0      0 232104  91872 41241432  0    0     0    32 4877 12120 93  6  1  0  0

Значення: r — runnable потоки. Якщо r постійно вищий за кількість ядер, ви чергуєтеся на CPU. Високий cs може вказувати на надмірну конкурентність або контенцію блокування.

Рішення: Якщо runnable потоків багато — розгляньте обмеження з’єднань, розмір пулу та конкурентність запитів. Якщо з’явилося свапування — це окрема аварія.

Завдання 5: Перевірити steal time і тротлінг CPU (реальність хмари)

cr0x@server:~$ sar -u 1 3
Linux 6.1.0 (db01)  12/31/2025  _x86_64_  (16 CPU)

12:01:55 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
12:01:56 PM     all     89.12      0.00      7.01      0.20      3.10      0.57
12:01:57 PM     all     88.90      0.00      7.30      0.10      3.20      0.50

Значення: %steal — це час CPU, який ви хотіли б отримати, але не отримали. Це може імітувати «база стала повільнішою», без змін у запитах.

Рішення: Якщо steal підвищений — перевірте розміщення хоста, клас інстансу й чи витрачені кредити на burst. Не оптимізуйте запити, щоб компенсувати погане розміщення.

Завдання 6: Postgres — зіставити гарячий PID з запитом і станом очікування

cr0x@server:~$ sudo -u postgres psql -x -c "select pid, usename, application_name, client_addr, state, wait_event_type, wait_event, now()-query_start as age, left(query,200) as query from pg_stat_activity where state<>'idle' order by age desc limit 5;"
-[ RECORD 1 ]-----+--------------------------------------------
pid               | 28741
usename           | app_user
application_name  | api
client_addr       | 10.20.3.41
state             | active
wait_event_type   |
wait_event        |
age               | 00:02:14.12031
query             | SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= now()-interval '30 days' GROUP BY o.customer_id;

Значення: Порожнє wait_event + state=active означає, що він виконується на CPU (або принаймні не чекає на відстежувану подію). Текст запиту — ваш підозрюваний.

Рішення: Якщо багато active виконують однаковий шаблон — у вас гаряча кінцева точка. Якщо wait events показують блокування — трактуйте як контенцію, а не «потрібен ще CPU».

Завдання 7: Postgres — отримати топ‑порушників за загальним часом

cr0x@server:~$ sudo -u postgres psql -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) as query from pg_stat_statements order by total_exec_time desc limit 10;"
 calls | total_ms | mean_ms | rows  | query
-------+----------+---------+-------+------------------------------------------------------------
  8231 | 912345.4 | 110.81  | 15321 | SELECT o.customer_id, count(*) FROM orders o JOIN order_items...
 12001 | 610112.7 | 50.84   | 12001 | SELECT * FROM sessions WHERE token = $1

Значення: Тепер у вас є шаблони запитів, ранжовані за загальним часом виконання (проксі для споживання CPU, але не тотожне).

Рішення: High total_exec_time провокує інциденти. High mean_exec_time впливає на хвіст латентності. Вирішіть, що виправляти в першу чергу.

Завдання 8: Postgres — доведіть, куди іде час за допомогою EXPLAIN (ANALYZE, BUFFERS)

cr0x@server:~$ sudo -u postgres psql -c "explain (analyze, buffers, verbose) SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= now()-interval '30 days' GROUP BY o.customer_id;"
HashAggregate  (cost=... rows=... width=16) (actual time=2150.113..2150.901 rows=4821 loops=1)
  Buffers: shared hit=120344 read=8123
  ->  Hash Join  (cost=... ) (actual time=310.123..2011.221 rows=241233 loops=1)
        Hash Cond: (i.order_id = o.id)
        Buffers: shared hit=120344 read=8123
        ->  Seq Scan on public.order_items i  (actual time=0.021..1190.332 rows=5200000 loops=1)
              Buffers: shared hit=99844 read=6500
        ->  Hash  (actual time=305.110..305.111 rows=402113 loops=1)
              ->  Seq Scan on public.orders o  (actual time=0.030..221.900 rows=402113 loops=1)
                    Filter: (created_at >= (now() - '30 days'::interval))

Значення: Послідовні сканування і великі хеш‑агрегати вимогливі до CPU. Buffers показують попадання в кеш проти читань; цей запит робить і те, і інше, але все одно витрачає багато часу на обчислення.

Рішення: Якщо він сканує мільйони рядків, щоб відповісти на невелике питання — додавайте/налаштовуйте індекси, перепишіть запит або попередньо агрегуйте. Якщо більшість в кеші, але все одно повільно — це CPU‑важка логіка (хешування, сортування, функції).

Завдання 9: MySQL — ідентифікувати топ‑дигести за часом CPU

cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(SUM_LOCK_TIME/1e12,2) AS lock_s, ROUND(SUM_ROWS_EXAMINED/COUNT_STAR,0) AS avg_rows_examined FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT o.customer_id, COUNT ( * ) FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.created_at >= ? GROUP BY o.customer_id
COUNT_STAR: 8120
total_s: 945.21
lock_s: 2.11
avg_rows_examined: 510220

Значення: Цей digest споживає найбільше часу запитів. avg_rows_examined — величезний сигнал «привіт, я сканую занадто багато».

Рішення: Націлюйтеся на найгірші дигести першими. Якщо lock_s високий відносно total — можлива контенція блокувань, а не CPU. Якщо rows examined величезні — працюйте з індексами та шляхами доступу.

Завдання 10: MySQL — побачити активні потоки і що вони роблять

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;"
Id	User	Host	db	Command	Time	State	Info
9312	app	10.20.3.41:51844	prod	Query	132	Sending data	SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id
9441	app	10.20.3.52:52011	prod	Query	98	Copying to tmp table	SELECT ... ORDER BY ...

Значення: Довго виконувані активні запити видно. «Copying to tmp table» і «Sending data» часто означають великі проміжні результати, сортування або погане індексування.

Рішення: Якщо один запит домінує — можна його обережно вбити (узгоджено). Якщо багато подібних довгих запитів — виправлення системне.

Завдання 11: MySQL — пояснити план і підтвердити, чи ви скануєте

cr0x@server:~$ mysql -e "EXPLAIN SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: o
type: range
possible_keys: idx_orders_created_at
key: idx_orders_created_at
rows: 401233
Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: i
type: ref
possible_keys: idx_order_items_order_id
key: idx_order_items_order_id
rows: 12
Extra:

Значення: «Using temporary; Using filesort» — класична CPU+пам’ятьова плата за групування/сортування без гарного шляху.

Рішення: Розгляньте складені індекси, щоб підтримати групування, зменшити кількість рядків на ранніх етапах або змінити форму запиту. Якщо range scan все ще зачіпає сотні тисяч рядків за виклик — ви платите за кожен запит.

Завдання 12: MySQL 8 — використати EXPLAIN ANALYZE для реальної поведінки виконання

cr0x@server:~$ mysql -e "EXPLAIN ANALYZE SELECT o.customer_id, count(*) FROM orders o JOIN order_items i ON i.order_id=o.id WHERE o.created_at >= NOW()-INTERVAL 30 DAY GROUP BY o.customer_id;"
-> Group aggregate: count(0)  (actual time=0.333..2150.221 rows=4821 loops=1)
    -> Nested loop inner join  (actual time=0.112..2010.011 rows=241233 loops=1)
        -> Index range scan on orders using idx_orders_created_at  (actual time=0.041..220.333 rows=402113 loops=1)
        -> Index lookup on order_items using idx_order_items_order_id (order_id=o.id)  (actual time=0.003..0.004 rows=12 loops=402113)

Значення: Вкладений цикл, помножений на 402k рядків — курильний доказ. Ось як «тонкі» індекси все одно створюють величезні рахунки CPU.

Рішення: Розгляньте переписування для попередньої агрегації order_items або зменшення множини замовлень. Іноді інша стратегія join або покриваючий індекс різко знижує кількість циклів.

Завдання 13: Перевірити, чи контенція блокувань маскує проблеми CPU (Postgres)

cr0x@server:~$ sudo -u postgres psql -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where state='active' group by 1,2 order by 3 desc;"
 wait_event_type |     wait_event      | count
-----------------+---------------------+-------
 Lock            | relation            |    18
                 |                     |     6

Значення: Багато active, що чекають на relation locks, означає, що ви можете бачити падіння пропускної здатності і дивну поведінку CPU (спін, переключення контексту, повторні спроби).

Рішення: Перестаньте налаштовувати SELECT. Ідентифікуйте блокувальників і довгі транзакції, уникайте довгих DDL у пікові години, розгляньте таймаути блокувань і безпечніші патерни міграції.

Завдання 14: Перевірити внутрішній тиск InnoDB (MySQL)

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,140p'
------------
TRANSACTIONS
------------
Trx id counter 93211245
History list length 81234
...
--------
SEMAPHORES
--------
OS WAIT ARRAY INFO: reservation count 221234, signal count 221100
Mutex spin waits 912334, rounds 1823344, OS waits 22344

Значення: Велика history list length може означати відставання purge (часто через довгі транзакції). Високі mutex spin waits можуть вказувати на контенцію, яка спалює CPU, навіть якщо «повільні запити» неочевидні.

Рішення: Якщо purge відстає — знайти довгі транзакції і виправити поведінку додатка. Якщо контенція висока — зменшіть конкурентність, перегляньте «гарячі» індекси/таблиці і розгляньте зміну конфігурації — після ідентифікації тригера навантаження.

Завдання 15: Довести, чи у вас свап або memory‑thrashing (обидва)

cr0x@server:~$ free -m
               total        used        free      shared  buff/cache   available
Mem:           64218       21342        2311        1024       40564       40112
Swap:              0           0           0

Значення: Відсутність використання swap — добре. Низький «free» не обов’язково погано; важливіше «available».

Рішення: Якщо swap використовується — CPU може підійматися через накладні витрати на сторінковий обмін, і запити поводяться непередбачувано повільно. Вирішіть проблему пам’яті перед переписуванням SQL.

Завдання 16: Зробити знімок профілю CPU (досить безпечно) за допомогою perf

cr0x@server:~$ sudo perf top -p 5123
Samples: 42K of event 'cycles', 4000 Hz, Event count (approx.): 12000000000
Overhead  Shared Object        Symbol
  18.21%  mysqld               Item_func_like::val_int
  11.03%  mysqld               JOIN::exec
   9.88%  libc.so.6            __memcmp_avx2_movbe

Значення: Ви бачите, куди йдуть цикли CPU. Тут це порівняння рядків і виконання join, а не «диск». Профілі швидко підтверджують шаблони запитів (LIKE ‘%…%’) і погані join‑и.

Рішення: Якщо CPU‑гарячі точки співпадають з відомими дорогими операторами (sort, hash, LIKE, JSON‑витяг) — пріоритезуйте перепис запитів і індекси. Якщо бачите ядрові гарячі точки (мережевий стек) — дивіться на клієнтські хвилі або накладні TLS.

Цікаві факти та історичний контекст (чому сучасна поведінка така)

  • PostgreSQL розпочинався як POSTGRES в UC Berkeley у 1980‑х, з ідеями про розширюваність і складні типи даних — чому він досі любить багаті індекси і кастомні оператори.
  • MySQL створювався з простоти й швидкості для типових веб‑навантежень у середині 1990‑х — чому він традиційно віддавав перевагу прагматичним дефолтам і простоті розгортання.
  • InnoDB став рушієм за замовчуванням у MySQL, бо транзакційність і відновлення після аварій перемогли у реальному світі, навіть якщо це означало більше внутрішньої машини (і більше шляхів спалювання CPU).
  • MVCC у Postgres робить читання незаблокованими у багатьох випадках, але це переносить витрати на vacuum і перевірки видимості — CPU може підійматися, коли autovacuum відстає або розростається bloat.
  • performance_schema у MySQL еволюціонував від «приємної додаткової фічі» до необхідної, коли системи росли і «SHOW STATUS» перестав вистачати для атрибуції.
  • Планувальники запитів стали розумнішими і складнішими в обох системах; розумніший планувальник потребує статистики, і погана статистика — борг продуктивності, який тихо накопичується.
  • Реплікація змінила форму інцидентів: репліки зменшили CPU читань на примарних вузлах, але додали нові CPU‑гарячі точки на репліках (відтворення/applay, vacuum або підтримка вторинних індексів).
  • Залізо стало швидшим, а запити лінивими: команди перестали відчувати вартість сканування, поки ріст даних не зробив «тимчасові» плани постійними.

Три короткі корпоративні історії з польових умов

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

Компанія мала стабільний кластер MySQL та квартальний ритуал: якщо латентність росте — піднімаємо розмір інстансу. Це працювало доти, поки не перестало. Одної п’ятниці CPU зафіксувався, і кількість помилок зросла. Перша реакція була класичним корпоративним рефлексом: масштабувати вгору.

Масштабування допомогло приблизно на 20 хвилин. Потім CPU знову піднявся, ще швидше. Інженери почали звинувачувати хмарного провайдера. Хтось запропонував відключити бінлог «просто щоб спробувати». Холодніші голови майже перемогли.

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

Крапка: набір даних подвоївся, і план запиту змінився на «Using temporary; Using filesort». Інстанс не був замалий; запит став мультиплікатором CPU. Ми обмежили швидкість endpoint, додали складений індекс і переписали запит для попередньої агрегації. CPU впав, і розмір інстансу знизили наступного дня.

Неправильне припущення було не технічним. Воно було психологічним: використовувати залізо як виправдання відсутності вимірювань.

Інцидент №2: оптимізація, що спрацювала проти нас («корисний індекс», що погіршив CPU)

Інша команда працювала на PostgreSQL і стикалася з періодичними сплесками CPU у пікові години. Вони помітили повільний запит, додали індекс і побачили, що він став швидшим у staging. Зміна затверджена, розгорнута і привітана. Коротко.

У продакшені CPU погіршився, і p99 латентності піднялася. Індекс використовувався, так — але він викликав план, що робив багато випадкових доступів і потім сортував значно більший проміжний результат. Старий план був послідовним скануванням з жорстким фільтром, що вигравав від теплого кешу.

Ми використали EXPLAIN (ANALYZE, BUFFERS) на даних, схожих на продакшн (не іграшкових staging). Новий індекс змусив планувальник недооцінити селективність. Він обрав nested loops, де hash join був дешевшим. CPU пішов у join‑обробку та порівняння, а не в I/O.

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

Інцидент №3: нудна практика, що врятувала день (телеметрія повільних запитів під рукою)

Платіжна платформа використовувала і MySQL, і PostgreSQL з історичних причин. У них була одна звичка, що здавалась нудною на планерках: вони завжди тримали ввімкнену легку телеметрію запитів. Не повне логування всіх запитів — лише достатньо, щоб атрибутувати час шаблонам.

Коли стався сплеск CPU, не було метушні з «увімкнути slow log і чекати». У MySQL вже були statement digests у performance_schema. У Postgres — pg_stat_statements. Вони могли відповісти за кілька хвилин, які шаблони запитів змінилися в загальному часі та частоті викликів.

Винуватець був ніби банальний: планувальник задач збійнув і запустив забагато паралельних воркерів. Кожен воркер виконував «розумний» запит. Разом вони створили бурю CPU і трясли кеші. Команда не сперечалась про залізо. Вони довели зміну конкурентності.

Вони виправили планувальник, обмежили конкурентність на рівні пулу і додали стримуючий алерт: «той самий digest, раптове зростання викликів/сек». Інцидент виявився антикліматичним. Саме це і важливо. Нудні практики не роблять гарних бойових історій, але зберігають роботу.

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

1) Симптом: CPU 100%, але латентність іноді тільки підскакує

Корінна причина: Похилі запити або пакетні задачі конкурують з OLTP‑трафіком; CPU насичується у часових інтервалах.

Виправлення: Розділити навантаження (черги для пакетів), встановити обмеження конкурентності, і ідентифікувати топ‑дигести за загальним часом і швидкістю викликів.

2) Симптом: високий CPU і високий load average, але mpstat показує iowait

Корінна причина: Система I/O‑bound з багатьма потоками, заблокованими в uninterruptible sleep. Load — не те саме, що CPU.

Виправлення: Виміряйте латентність дисків, показники попадання в буфер‑кеш, тиск контрольних точок і плани запитів, що викликають великі читання.

3) Симптом: CPU високий у kernel time (%sys), а не user time

Корінна причина: Мережеві витрати (шторм з’єднань), вартість TLS, файлові конфлікти або надмірні системні виклики і переключення контексту.

Виправлення: Використовуйте пулювання з’єднань, зменшуйте кількість з’єднань на запит, перевірте налаштування TLS і шукайте патерни з великою кількістю syscall.

4) Симптом: Postgres CPU високий, багато «active» сесій, але wait_event показує Lock

Корінна причина: Контенція блокувань плюс повторні спроби або накопичення блокованих бекендів.

Виправлення: Ідентифікуйте блокувальників, скоротіть транзакції, уникайте довгого DDL у пікові години і розгляньте таймаути блокувань та безпечні шаблони міграції.

5) Симптом: MySQL CPU високий, зростає history list length в InnoDB

Корінна причина: Довгі транзакції перешкоджають purge; внутрішнє очищення відстає і витрачає CPU.

Виправлення: Знайдіть і виправте довгі транзакції, забезпечте коміти, і налаштуйте навантаження. Іноді рішення — «припинити аналітику на первинному вузлі».

6) Симптом: Запит, який раніше був нормальним, тепер дорогий

Корінна причина: Ріст даних + знос статистики + регресія плану.

Виправлення: Оновіть статистику, перевірте зміни плану, додайте складені індекси узгоджені з предикатами/ключами з’єднання і розгляньте перепис запиту.

7) Симптом: CPU високий після додавання індексу

Корінна причина: Планувальник обирає гірший план, або індекс збільшує накладні витрати на запис і підтримку.

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

8) Симптом: CPU високий переважно на репліках

Корінна причина: Застосування реплікації, vacuum, підтримка індексів або читальне навантаження перемістилось без планування ємності.

Виправлення: Виміряйте відставання apply і активність фон‑воркерів; правильно підійміть розміри реплік; не вважайте репліки «безкоштовними».

Жарт №2: Графік CPU на 100% — як корпоративний загальний збір: всі зайняті, і наче нічого не рухається швидше.

Контрольні списки / покроковий план

Покроково: доведіть, що це запити (не обладнання) під час інциденту

  1. Захопіть знімок ОС: mpstat, top, pidstat, vmstat.
  2. Підтвердьте, що БД — гарячий процес: визначте PID/TGID і топ‑потоки.
  3. Перевірте шум віртуалізації: steal time; підтвердьте відсутність тротлінгу CPU/виснаження burst.
  4. Шлях Postgres: зіставте гарячий PID → pg_stat_activity запит; перевірте wait events; використайте pg_stat_statements для топ‑порушників.
  5. Шлях MySQL: використайте підсумки digest у performance_schema; корелюйте з processlist; підтвердьте rows examined.
  6. Вирішіть контенцію vs виконання: блокування/waits проти чистого обчислення.
  7. Запустіть доказ плану: EXPLAIN (ANALYZE, BUFFERS) або EXPLAIN ANALYZE; зафіксуйте counts і loops.
  8. Застосуйте найменш ризиковану міру: обмежте швидкість, вбийте найгірші запити, зменшіть конкурентність або тимчасово вимкніть endpoint.
  9. Реалізуйте довготривале виправлення: індекс/перепис запиту/статистика, потім валідуйте метриками до/після.
  10. Напишіть постмортем з ланцюжком доказів: «ОС → БД → digest запиту → план → зміна.»

Операційні засоби, які я рекомендую (і чому)

  • Пулювання з’єднань: бо шторм потоків/процесів — це податок на CPU і планування.
  • Телеметрія запитів завжди увімкнена (легка): щоб наступний інцидент не починався з «увімкніть slow log і чекайте 30 хвилин».
  • Обмеження конкурентності для робочих навантажень: бо пропускна здатність зростає, поки раптом не падає.
  • Дисципліна змін для індексів і статистики: бо зміни продуктивності — це продакшн‑зміни, а не «просто схема».
  • Стенд з даними, схожими на продакшн, для тестів планів: бо планувальник не дбає про ваш синтетичний датасет.

Як обрати між «виправити запити» і «масштабувати залізо»

Масштабуйте залізо, коли у вас стабільні, ефективні запити і зростає легітимне навантаження. Виправляйте запити, коли CPU на запит зростає, регресує план, кількість прочитаних рядків на виклик вибухає або зростає контенція. Як тільки ви бачите «rows examined per call» що різко збільшується, масштабування — тимчасова анестезія. Воно закінчується.

Питання та відповіді

1) Як зрозуміти, що це не поганий CPU або відмова обладнання?

Сучасні CPU рідко «частково відмовляють» так, щоб повпливати лише на MySQL. Якщо mpstat показує високий steal time — це питання тенанті чи віртуалізації. Якщо в журналах ядра є machine check errors — це апаратна проблема. Інакше припускайте навантаження першим і доведіть протилежне.

2) Чому CPU високий, але окремі запити не здаються повільними?

Бо через пропускну здатність можна «вбити» систему. Швидкий запит, викликаний 50 000 разів за хвилину, може домінувати над CPU більше, ніж один повільний запит. Дивіться на загальний час і частоту викликів, а не лише на середню латентність.

3) Чому Postgres показує багато «active» сесій без wait event?

Часто вони справді виконуються на CPU. Іноді вони в стані, що не відображається як wait event. Використовуйте інструменти ОС (pidstat/perf), щоб підтвердити, що бекенд споживає цикли.

4) Чи може контенція блокувань викликати 100% CPU?

Так. Спінлоки, повторні спроби і тряска планувальника можуть підсилити CPU, навіть коли корисна робота знижується. У MySQL дивіться метрики InnoDB semaphore; у Postgres — wait events і блоковані ланцюги.

5) Чи варто вмикати MySQL slow query log під час інциденту?

Тільки якщо ви можете дозволити собі накладні витрати і встановите розумні пороги. Віддавайте перевагу performance_schema digests для атрибуції. Slow log корисний, але ввімкнення його під час пожежі часто створює більше диму, ніж сигналу.

6) Чи PostgreSQL по суті більш «CPU‑витратний», ніж MySQL?

Немає універсальної відповіді. Postgres може витрачати CPU на перевірки видимості MVCC і поведінку vacuum; MySQL може витрачати CPU на контенцію InnoDB і патерни виконання join. Навантаження та схема визначають усе.

7) Коли варто використовувати perf?

Коли потрібно довести, куди йдуть цикли (сорти, хешування, операції зі строками, обробка JSON) і метрики на рівні БД не дають узгодженої картини. Використовуйте його, щоб підтвердити гіпотези, а не як перший рефлекс.

8) Яке найшвидше безпечне полегшення, коли CPU зафіксовано?

Зменшити конкурентність і зупинити кровотечу: обмежити швидкість гарячого endpoint, призупинити пакетну задачу, зменшити розмір пулу з’єднань або вбити один найгірший запит. Потім робіть довготривале виправлення після стабілізації графіків.

9) Чому додавання індексу не знизило CPU?

Бо індекси не зменшують обсягу роботи, якщо запит все одно звертається до величезної кількості рядків, або якщо індекс змінює стратегію виконання на гіршу для вашого розподілу даних. Перевіряйте за допомогою EXPLAIN ANALYZE і реальних counts рядків.

10) Як довести регресію плану?

Захопіть старий план (з логів, збережених explain або з базової середовищі) і порівняйте з новим планом з фактичними rows і loops. У Postgres pg_stat_statements плюс auto_explain допомагають. У MySQL порівнюйте EXPLAIN/EXPLAIN ANALYZE і часи digest до/після вікна змін.

Висновок: практичні наступні кроки

Якщо ви хочете довести «CPU 100% — це запити, а не обладнання», робіть це як оператор, а не філософ. Виміряйте розподіл CPU. Ідентифікуйте гарячі процеси/потоки. Зв’яжіть їх із сесіями і statement digests. Отримайте одне доказ на рівні плану з реальними counts рядків. Потім оберіть найменше виправлення, яке дасть вам час.

Наступні кроки, які вже приносять користь:

  1. Переконайтеся, що ви можете опитувати pg_stat_statements (Postgres) або performance_schema statement digests (MySQL) під час живого інциденту.
  2. Напишіть односторінковий runbook з розділом Швидкий план діагностики вище і тримайте його поруч з нотатками on‑call.
  3. Додайте один алерт на раптове зростання викликів за digest; це ловить погані релізи і збійні планувальники, поки CPU ще не кричить.
  4. Зробіть «EXPLAIN з реальною формою даних» вимогою для правок продуктивності. Індекси без планів — просто дорогі думки.
← Попередня
Оновлення драйвера вбило мій FPS: як правильно діагностувати
Наступна →
Повільні операції Ceph у Proxmox: знайти вузьке місце (диск, мережа або CPU)

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