10:17 ранку. Продакт‑менеджер пише: «чекаут повільний». Grafana виглядає як місце злочину. Ви відкриваєте консолю БД і все відчувається липким: запити, які раніше поверталися за 20 мс, тепер виконуються секунди, кількість з’єднань росте, а додаток починає таймаутити, ніби в нього алергія на успіх.
У такі моменти богослов’я («MySQL vs Postgres») марне. Потрібна дисциплінована, швидка послідовність дій, яка знайде вузьке місце, уникне самозавданих ран і дасть безпечний наступний крок. Це та послідовність — побудована на досвіді експлуатації обох рушіїв у продакшні, на реальних дисках і з реальними людьми, які запитують, чому стало повільно.
Швидий план діагностики (15 хвилин)
Це порядок дій, який найнадійніше знаходить вузьке місце, не роблячи гірше. Він призначений для інциденту «раптом стало повільно», коли вам потрібен сигнал швидко.
Хвилини 0–2: Підтвердіть, що проблема в базі (а не ваш додаток)
- Порівняйте затримку додатка та затримку БД. Якщо додаток має таймінги по запитах — використайте їх. Якщо ні — виконайте тривіальний DB‑запит з хоста додатка (не з вашого ноутбука), щоби виключити мережу та DNS.
- Перевірте метрики пулу з’єднань. Насичений пул виглядає як повільна БД. Часто це просто «занадто багато потоків чекають на з’єднання».
Рішення: Якщо тривіальні запити швидкі, а час очікування пулу високий, БД може бути нормальною, а вогонь — у розмірі пулу / конкурентності додатка.
Хвилини 2–5: Шукайте очевидну межу ресурсів (CPU, пам’ять, I/O)
- CPU завантажений? Думайте: поганий план запиту, відсутній індекс, раптовий звіт або «thundering herd».
- Тиск на пам’ять? Свапінг змушує бази працювати ніби під водою.
- Затримка диска? Якщо сховище повільне — все повільне. Не сперечайтесь про SQL, поки диски «плавляться».
Рішення: Якщо затримка сховища висока — здійснюйте триаж шляху збереження та зменшуйте тиск на запис. Якщо CPU завантажений — ідентифікуйте топ‑запити і зупиніть найгіршого винуватця.
Хвилини 5–9: Перевірте контенцію (блокування, очікування, голод пулу)
- Блокування: знайдіть блокуючі сесії та SQL‑вираз, що утримує блокування. Вбивайте блокер лише коли зрозумієте, що це (зміна схеми? довга транзакція? пакетна робота?).
- Події очікування: MySQL: InnoDB‑waits, metadata locks. PostgreSQL: lock waits, LWLocks, I/O waits.
Рішення: Якщо бачите один блокер, що спричиняє накопичення — усуньте його першим. Контенція множить біль.
Хвилини 9–13: Визначте топ «витратні» запити зараз
- Знайдіть підписи запитів, що споживають час, а не лише кількість.
- Перевірте регресію плану (індекс не використовується, статистика застаріла, зміна параметра, інше значення зв’язування).
Рішення: Якщо один запит домінує — пом’якшіть (обмежте швидкість, вбити, додати індекс, відключити фічу) перед «хитрими» рішеннями.
Хвилини 13–15: Оберіть безпечну міру і зафіксуйте докази
- Мітигація: зупиніть кровотечу (скасуйте/вбивайте runaway‑сесії, зменшіть конкурентність, тимчасово відключіть дорогі шляхи функціоналу, додайте таргетований індекс якщо безпечно, збільшіть таймаути пулу щоб уникнути штампування).
- Збір доказів: зніміть снапшот processlist / pg_stat_activity, waits, топ‑запитів, iostat та стан реплікації. Це знадобиться після спаду адреналіну.
Парафраз ідеї від Gene Kim: «Підвищення надійності переважно про покращення зворотного зв’язку та скорочення часу діагностики.»
Цікаві факти й історія, що важливі під час інцидентів
Це не факти для вікторини. Це ті речі, що пояснюють, чому рушій поводиться саме так, коли ви на чергуванні.
- MVCC у PostgreSQL старіший за багато експлуатаційних рунабуків. Його багатоверсійний контроль конкурентності означає, що читачі не блокують писачів, але створюються мертві кортежі, які мусить прибирати vacuum — інакше з’являється bloat і повільність.
- InnoDB не завжди був «MySQL». Historically MySQL мав кілька движків зберігання; InnoDB став дефолтним, бо транзакційна коректність і відновлення після збоїв стали неминучими в реальних системах.
- Metadata locks у MySQL можуть зупинити весь світ. Деякі DDL і навіть «невинні» оператори беруть metadata lock, що може блокувати інші запити несподіваними способами під час змін схеми.
- PostgreSQL ставиться до VACUUM як до питання першої важливості. Autovacuum — не опціональна прикраса; це спосіб, яким Postgres сплачує свій MVCC‑борг і запобігає wraparound ідентифікатора транзакції.
- Реплікація — частина продуктивності. Реплікація MySQL може викликати лаг і перетворити масштабування читань у інцидент коректності даних, а лаг в Postgres зростає, коли WAL‑генерація перевантажує доставку чи реплей.
- Обидва рушії значно покращили інструменти інспекції. Performance Schema у MySQL та статистичні view у Postgres (і розширення на кшталт pg_stat_statements) перетворили «вгадування» на «вимірювання», якщо ви їх увімкнули.
- Налаштування за замовчуванням — компроміси, а не ваше навантаження. Обидві системи постачаються з параметрами, що дозволяють стартувати на скромних машинах. Для продакшна потрібні явні рішення щодо пам’яті, контрольних точок і з’єднань.
- Планувальник запитів PostgreSQL базується на вартості і чутливий до статистики. Коли статистика застаріла або розподіл даних змінився, плани можуть перейти від «index scan» до «sequential scan» і ви вирокнете, що хтось саботував базу.
- Історія MySQL має гострі краї навколо неявної поведінки. Речі на кшталт мовчазного обрізання рядків (залежно від SQL mode) і різних дефолтних ізоляційних рівнів раніше давали «в робочому середовищі працювало», а в проді — проблеми з продуктивністю і коректністю одночасно.
Практичні завдання: команди, виходи та рішення (MySQL + PostgreSQL + Linux)
Вам потрібні команди, які можна виконати під час інциденту, плюс що означає їхній вихід і яке рішення приймати далі. Ось ті, до яких я звертаюся насамперед.
Завдання 1: Перевірити завантаження хоста, CPU steal і чергу виконання
cr0x@server:~$ uptime
10:21:33 up 187 days, 3:12, 2 users, load average: 28.14, 26.90, 19.77
Значення: Load average значно вище за кількість ядер CPU вказує на контенцію (CPU, I/O або черга runnable). Якщо ви на віртуалізації/хмарі, велике навантаження при помірному використанні CPU часто означає I/O‑очікування або троттлінг.
Рішення: Якщо load величезний — негайно перевірте iowait і затримки диска (Завдання 3). Не починайте «оптимізацію» SQL в сліпу.
Завдання 2: Перевірити тиск пам’яті і свапінг
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 62Gi 55Gi 1.2Gi 512Mi 5.8Gi 2.3Gi
Swap: 8.0Gi 6.4Gi 1.6Gi
Значення: Активне використання swap на хості бази зазвичай — смертельна спіраль продуктивності. «Available» пам’ять низька, свапінг означає, що ОС виводить робочі набори.
Рішення: Зупиніть нове навантаження (обмежте трафік), зменшіть сплески пам’яті БД (наприклад, занадто багато з’єднань) та подумайте про перезапуск тільки якщо це безпечно і ви знайшли витік. Також перевірте налаштування пам’яті MySQL/Postgres відносно загальної RAM.
Завдання 3: Виміряти затримку диска і насичення
cr0x@server:~$ iostat -xz 1 3
Linux 6.2.0 (db01) 12/29/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.41 0.00 6.23 38.77 0.00 42.59
Device r/s w/s rkB/s wkB/s await aqu-sz %util
nvme0n1 120.0 1800.0 4096 98304 24.8 12.4 98.9
Значення: %util близько 100% з високим await вказує, що пристрій насичений і запити стоять у черзі. iowait теж високий. Це затримка через сховище.
Рішення: Знайдіть, хто пише (Завдання 4), зменшіть тиск на запис (пауза пакетних робіт, обережне обмеження autovacuum, уповільнення інжесту) і перевірте шторми контрольних точок/флашів у БД.
Завдання 4: Визначити топ‑письменників на диску (просто і швидко)
cr0x@server:~$ pidstat -d 1 5
Linux 6.2.0 (db01) 12/29/2025 _x86_64_ (16 CPU)
12:22:11 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:22:12 106 2143 0.00 84210.00 0.00 mysqld
12:22:12 106 1987 0.00 5200.00 0.00 postgres
12:22:12 0 8891 0.00 1200.00 0.00 rsync
Значення: Ви бачите, який процес б’є по записах на диск. Якщо rsync/резервне копіювання пише під час піку — вітаємо: знайшли «злочинця» в одязі «обслуговування».
Рішення: Якщо ненавмисні процеси не‑БД важкі — зупиніть/перенесіть їх. Якщо БД — тоді перевірте контрольні точки, флаші та великі транзакції.
Завдання 5: Підтвердити вільне місце в файловій системі та тиск inode
cr0x@server:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 900G 842G 13G 99% /var/lib
Значення: 99% заповнення може зламати продуктивність (фрагментація, затримки алокації) і загрожує повним відмовленням. Багато баз поводяться погано біля повних дисків.
Рішення: Звільніть місце зараз (ротація логів, перемістіть резервні копії), потім сплануйте ємність. Також перевірте бінлог/WAL, що можуть рости безконтрольно.
Завдання 6: Перевірити мережеві помилки й ретрансляції (часто «БД повільна» через втрату пакетів)
cr0x@server:~$ ss -s
Total: 1892 (kernel 0)
TCP: 1583 (estab 1242, closed 251, orphaned 0, timewait 251)
Transport Total IP IPv6
RAW 0 0 0
UDP 17 13 4
TCP 1332 1210 122
INET 1349 1223 126
FRAG 0 0 0
Значення: Багато встановлених з’єднань може вказувати на некоректну роботу пулу або сплеск трафіку. Зіставте це з лічильниками з’єднань БД.
Рішення: Якщо з’єднання вибухнули — шукайте деплой, що змінив пулінг або таймаути. Якщо з’єднання стабільні, а затримка висока — зосередьтесь на очікуваннях і I/O.
Завдання 7 (MySQL): Подивіться, що роблять потоки зараз
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,80p'
*************************** 1. row ***************************
Id: 93218
User: app
Host: 10.0.4.21:51344
db: shop
Command: Query
Time: 42
State: Waiting for table metadata lock
Info: ALTER TABLE orders ADD COLUMN promo_code varchar(32)
*************************** 2. row ***************************
Id: 93244
User: app
Host: 10.0.3.19:51002
db: shop
Command: Query
Time: 38
State: Sending data
Info: SELECT * FROM orders WHERE user_id=... ORDER BY created_at DESC LIMIT 20
Значення: «Waiting for table metadata lock» — великий червоний стрілка на DDL, що блокує звичайні запити. «Sending data» довго може вказувати на повні сканування, великі результати або I/O‑очікування.
Рішення: Якщо metadata lock блокує — призупиніть/вбивайте сесію DDL (обережно) або зачекайте завершення, якщо воно близьке. Далі плануйте зміни схеми правильно (online DDL, коли це можливо).
Завдання 8 (MySQL): Виявити InnoDB lock waits і блокери
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
------------
TRANSACTIONS
------------
Trx id counter 89423311
Purge done for trx's n:o < 89423000 undo n:o < 0 state: running
History list length 19876
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 89423288, ACTIVE 61 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 93244, OS thread handle 140228, query id 5129312 10.0.3.19 app Sending data
UPDATE inventory SET qty=qty-1 WHERE sku='A-1841'
------- TRX HAS BEEN WAITING 61 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 421 page no 183 n bits 72 index PRIMARY of table `shop`.`inventory` trx id 89423288 lock_mode X locks rec but not gap waiting
Значення: InnoDB каже, що транзакція чекає на record lock. «History list length» велика — може вказувати на відставання purge, що корелює з довгими транзакціями.
Рішення: Знайдіть блокуючу транзакцію (нижче у виході або через performance_schema) і вирішіть: вбити блокера, вбити чекача або зменшити конкурентність на «гарячому» ключі (SKU, рядок користувача і т. п.). Якщо history list length довго високий — шукайте довгі транзакції.
Завдання 9 (MySQL): Перевірити лаг реплікації та стан SQL‑треду
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_SQL_Error|Relay_Master_Log_File|Exec_Master_Log_Pos"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 487
Relay_Master_Log_File: binlog.003214
Exec_Master_Log_Pos: 98122314
Значення: Репліка відстає ~8 хвилин. Читання з реплік можуть бути застарілими; підняття репліки може втратити дані, якщо промоутити відстаючу.
Рішення: Якщо додаток використовує репліки для читань, тимчасово направляйте критичні запити на primary. Дослідіть, чому SQL‑тред не встигає (важкі записи, однониткове застосування, довгі транзакції).
Завдання 10 (PostgreSQL): Перевірити активні сесії та хто чекає
cr0x@server:~$ psql -c "SELECT pid, usename, state, wait_event_type, wait_event, now()-query_start AS runtime, left(query,120) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC LIMIT 10;"
pid | usename | state | wait_event_type | wait_event | runtime | q
-------+---------+--------+-----------------+--------------+-----------+----------------------------------------------------------
18421 | app | active | Lock | relation | 00:01:12 | ALTER TABLE orders ADD COLUMN promo_code text
18502 | app | active | Lock | tuple | 00:00:55 | UPDATE inventory SET qty=qty-1 WHERE sku='A-1841'
18611 | app | active | IO | DataFileRead | 00:00:49 | SELECT * FROM orders WHERE user_id=$1 ORDER BY created_at
(3 rows)
Значення: Postgres показує, чи сесії чекають на блокування або I/O. Очікування типу «relation» часто вказують на DDL або довгі транзакції, що утримують блокування.
Рішення: Якщо домінують очікування блокувань — знайдіть блокера (Завдання 11). Якщо домінують I/O‑waits — повертайтеся до iostat і перевіряйте cache hit та контрольні точки.
Завдання 11 (PostgreSQL): Знайти блокуючі PID‑и (той, хто тримає двері закритими)
cr0x@server:~$ psql -c "SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocker.pid AS blocker_pid, blocker.query AS blocker_query FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked ON blocked.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocker_locks ON blocker_locks.locktype = blocked_locks.locktype AND blocker_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE AND blocker_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocker_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocker_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocker_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocker_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocker_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocker_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocker_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocker_locks.pid != blocked_locks.pid JOIN pg_catalog.pg_stat_activity blocker ON blocker.pid = blocker_locks.pid WHERE NOT blocked_locks.granted;"
blocked_pid | blocked_query | blocker_pid | blocker_query
------------+-----------------------------------------+------------+-----------------------------------------
18502 | UPDATE inventory SET qty=qty-1 ... | 18111 | BEGIN; SELECT * FROM inventory WHERE ...
(1 row)
Значення: У вас є конкретний PID‑блокер. Часто це транзакція, що залишилась відкритою через джоб, міграцію або «тимчасову» сесію відладчика.
Рішення: Якщо блокер явно випадковий і не виконує критичну роботу — завершіть його. Якщо це міграція — координуйте дії і виберіть найменш шкідливий варіант.
Завдання 12 (PostgreSQL): Перевірити тиск vacuum/autovacuum і мертві кортежі
cr0x@server:~$ psql -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_live_tup | n_dead_tup | last_autovacuum | last_vacuum
-------------+------------+------------+------------------------+------------------------
orders | 5821934 | 2149921 | 2025-12-29 09:41:12+00 | 2025-12-27 02:10:04+00
inventory | 402113 | 210993 | 2025-12-29 10:02:01+00 |
(2 rows)
Значення: Багато мертвих кортежів вказує на bloat або на те, що vacuum не встигає, що може збільшувати I/O і погіршувати ефективність індексів.
Рішення: Якщо мертвих кортежів багато і autovacuum відстає — налаштуйте autovacuum для гарячих таблиць (локальні налаштування) і перевірте довгі транзакції, що перешкоджають очищенню.
Завдання 13 (PostgreSQL): Перевірити лаг реплікації через LSN
cr0x@server:~$ psql -c "SELECT client_addr, state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
client_addr | state | write_lag | flush_lag | replay_lag
-------------+-----------+-----------+-----------+-----------
10.0.2.15 | streaming | 00:00:01 | 00:00:03 | 00:00:07
(1 row)
Значення: Лаг низький. Якщо replay_lag високий — репліка може бути повільною при застосуванні WAL (через CPU або I/O, або конфлікти hot standby).
Рішення: Якщо лаг високий і ви використовуєте репліки для читань — розгляньте перенаправлення читань або зменшення обсягу записів. Дослідіть спайки генерації WAL і ресурси репліки.
Завдання 14 (MySQL): Перевірити здоров’я buffer pool і тиск на читання
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| Innodb_buffer_pool_reads| 982134221 |
+-------------------------+-----------+
+---------------------------------+------------+
| Variable_name | Value |
+---------------------------------+------------+
| Innodb_buffer_pool_read_requests| 3812234910 |
+---------------------------------+------------+
Значення: Якщо buffer_pool_reads швидко зростає відносно read_requests — ви промахуєтесь по кешу і потрапляєте на диск. Абсолютні числа важливі менше за темп зростання під час інциденту.
Рішення: Якщо промахи кешу різко зросли — знайдіть запит, що змінив патерн доступу, або розгляньте збільшення buffer pool (довгостроково). Короткостроково — зменшіть навантаження або додайте індекс, що запобіжить повним скануванням.
Завдання 15 (PostgreSQL): Перевірити коефіцієнт попадань у кеш і тимчасові файли
cr0x@server:~$ psql -c "SELECT datname, blks_hit, blks_read, temp_files, temp_bytes FROM pg_stat_database WHERE datname = current_database();"
datname | blks_hit | blks_read | temp_files | temp_bytes
---------+-----------+-----------+------------+------------
shop | 991233221 | 11233211 | 12933 | 987654321
(1 row)
Значення: Зростання temp_files/temp_bytes означає, що запити виливаються на диск (сортування, хеші). Це часто питання пам’яті під конкретний запит (work_mem) або поганий план, що дає великі проміжні результати.
Рішення: Ідентифікуйте запити, що спливають на диск (pg_stat_statements допоможе) і виправте їх. Уникайте загального «підвищити work_mem» на зайнятих системах — це примножує споживання пам’яті на сесію.
Завдання 16: Зняти швидкий профіль потоків БД (з перспективи Linux)
cr0x@server:~$ top -b -n 1 | sed -n '1,25p'
top - 10:23:02 up 187 days, 3:14, 2 users, load average: 27.92, 26.80, 20.01
Tasks: 612 total, 3 running, 609 sleeping, 0 stopped, 0 zombie
%Cpu(s): 14.1 us, 5.9 sy, 0.0 ni, 41.2 id, 38.1 wa, 0.0 hi, 0.7 si, 0.0 st
MiB Mem : 63488.0 total, 1320.2 free, 56502.4 used, 5665.4 buff/cache
MiB Swap: 8192.0 total, 6553.7 used, 1638.3 free. 2401.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2143 mysql 20 0 24.1g 36.2g 154m S 182.4 58.3 884:12.11 mysqld
1987 postgres 20 0 15.2g 9.1g 88m S 42.1 14.7 311:09.22 postgres
Значення: Високий iowait (wa) каже, що сховище обмежує. Високий mysqld CPU вказує на шторм CPU‑запитів. Велика RES пам’ять із свапом означає перевантаження.
Рішення: Якщо домінує iowait — припиніть запис. Якщо CPU домінує — ізолюйте топ‑запити і вбивайте або пом’якшуйте їх. Якщо свап домінує — зменшіть конкурентність і пам’ятний слід.
Особливості MySQL: InnoDB, metadata locks, реплікація та типові пастки
Як виглядає «раптом повільно» в MySQL
Інциденти MySQL часто проявляються як один із цих патернів:
- Накопичення metadata lock: один DDL або LOCK TABLES блокує гарячу таблицю; усе шикується в чергу за ним.
- Контенція на один гарячий рядок/ключ: лічильники, рядки інвентарю, оновлення «last seen» — класичний випадок «один рядок, багато писачів».
- Промахи buffer pool: набір даних виріс або змінився патерн запитів, перетворивши попадання в пам’ять на читання з диска.
- Тиск InnoDB flushing/checkpoint: брудні сторінки досягають порогу, виникають шторми флешів, записи затримуються.
- Лаг реплікації: репліки відстають; читання застарілі; безпека фейловера падає.
Дві пріоритетні діагностики MySQL, які люди пропускають (а потім шкодують)
1) Metadata locks. Якщо ви перевіряєте тільки row locks — пропустите «одне ALTER TABLE, що править всіх». Processlist чітко показує це. Коли бачите — не сперечайтесь.
2) Шторми з’єднань. MySQL не в захваті, коли ви народжуєте масивні кількості з’єднань з великою пам’яттю на потік. «Ми просто збільшимо max_connections» — так ви перетворюєте повільний інцидент на OOM‑інцидент.
Міри пом’якшення MySQL, що зазвичай безпечні
- Вбивати явно runaway SELECT, що жують CPU і I/O, особливо ад‑hoc запити.
- Обмежити швидкість порушника на боці додатка (feature flag, shaping), замість тряски БД.
- Не робити змін схеми вдень якщо ви не використовуєте правильний online‑підхід і не відпрацювали його.
- Перенести важкі читання (якщо репліки здорові і консистентні для випадку використання).
Коли «лише повільні читання» насправді проблема записів
В InnoDB інтенсивні записи можуть уповільнювати читання через насичення I/O і кочення buffer pool. Ви побачите «читання повільні», але iostat покаже правду. Слід трактувати диск як спільний ресурс.
Особливості PostgreSQL: vacuum, bloat, WAL та типові пастки
Як виглядає «раптом повільно» в PostgreSQL
Повільність Postgres має тенденцію згуртовуватися навколо цих випадків:
- Очікування блокувань через довгі транзакції: хтось тримав відкриту транзакцію, і тепер vacuum та DDL не можуть просунутися.
- Заборгованість autovacuum і bloat: запити повільнішають дні/тижні, а потім раптом падають у «раптом», коли кеш і I/O перестають це приховувати.
- Тиск checkpoint/WAL: сплески записів породжують хвилі WAL; контрольні точки стають дорогими; підвищується I/O‑затримка.
- Спливання у тимчасові файли: сорти/хеші конкретного запиту ллються на диск і тягнуть систему в I/O‑очікування.
- Зміни планів: статистика, значення параметрів або розподіл даних приводять до послідовних сканів там, де очікували індексні сканування.
Дві пріоритетні діагностики Postgres, які люди пропускають (а потім шкодують)
1) Довгі транзакції. Одна idle‑in‑transaction сесія може заважати vacuum очищувати мертві кортежі і тримати блокування. Це еквівалент того, як хтось припаркував машину поперек вузької алеї.
2) Активність тимчасових файлів. Якщо temp_bytes швидко зростає — ви спливаєте на диск і це відчутно. Виправляйте запит перш за все; «підвищити work_mem» іноді правильно, але часто ризиковано при високій конкурентності.
Кроки пом’якшення для Postgres, що зазвичай безпечні
- Завершити випадкового блокера (idle in transaction, ad hoc сесія) після перевірки, що це не критичний пакет.
- Скасувати runaway‑запити замість вбивання всього процесу бази.
- Зменшити конкурентність на рівні пулу, щоб зупинити «натовп». Postgres віддає перевагу меншій кількості добре поводжених з’єднань.
- Цілеспрямовано налаштувати vacuum по таблицях, а не глобально «підвищувати vacuum», що може штурмувати диск.
Жарт #2: Autovacuum — як винос сміття: пропустити його не економить часу, просто змінює запах інциденту.
Три міні‑історії з корпоративного світу (анонімізовані, правдоподібні, технічно точні)
Міні‑історія 1: Інцидент через неправильне припущення
У них був відпрацьований процес міграцій: blue/green деплєї, перевірки перед запуском і мантра «міграції бази безпечні», яка повторювалася настільки часто, що стала фізикою. Команда запушила реліз додатка зі зміною схеми, що додавала nullable‑стовпець. «Все ок», — сказали вони. «Онлайн».
На MySQL це не було ок. ALTER взяв metadata lock, а таблиця була гарячою. Lock не блокує все миттєво; він блокував у найгірший можливий момент: коли інші сесії наростили чергу, вони тримали з’єднання довше, пул наситився і додаток почав таймаутити. SRE чергу помітив «DB повільна» на дашборді, потім побачив, що CPU не завантажений. Вони майже пішли в кролячу нору тонкого тюнінгу запитів.
Переломний момент — виконання SHOW FULL PROCESSLIST і бачення «Waiting for table metadata lock» повторюваного як стрибок у хорор‑фільмі. «Неправильне припущення» було не в тому, що online DDL існує. Воно в тому, що «онлайн» ≠ «без операційного впливу». Online DDL все ще конкурує за ресурси і може блокувати під навантаженням, залежно від операції і версії.
Виправлення було нудним: вбити сесію міграції, відкотити деплой і запланувати зміну за відпрацьованою процедурою з maintenance‑вікном і перевірками навантаження. Після інциденту додали механізм захисту: зміни схеми мають бути перевірені на навантаженні, схожому на продакшн, а міграції лімітуються фічей‑флагами. Не гламурно. Працює.
Міні‑історія 2: Оптимізація, що обернулась проти
Кластер Postgres мав класичну проблему продуктивності: занадто багато з’єднань. База тримала це деякий час, аж поки перестала. Хтось запропонував очевидне: «Давайте підвищимо max_connections. Залізо дешеве». Затверджили, бо звучало як план ємності.
Їхні дії лише погіршили ситуацію. На наступному піку затримки стали гіршими. Не трохи — «клієнти не можуть зайти» гірше. Машина почала свапити. Моніторинг виглядав абсурдно: багато сесій роблять дрібні дії повільно, наче БД колективно вирішила пасувати.
Postgres виділяє пам’ять на кожне з’єднання і має оверхед на бекенд. Підвищення max_connections без контролю пулінгу перетворило БД у фестиваль контекст‑перемикань. Гірше: більше одночасних запитів підвищило шанс тимчасових спливань, бо пам’ять розмилася поміж більшу кількість бекендів, і зросли I/O.
Вирішенням не став хитрий GUC. Впровадили нормальний connection pooler, знизили число активних сесій і узгодили work_mem з реальною конкурентністю. Урок простий: підвищення ліміту — не оптимізація; це дозвіл системі впасти новими і витратнішими способами.
Міні‑історія 3: Нудна, але правильна практика, що врятувала день
Компанія експлуатувала і MySQL, і PostgreSQL на різних продуктах. Єдина загальна складність: шар зберігання був спільний, і «інциденти зі сховищем» були тихим вбивцею. Одна команда довго наполягала на простій практиці: знімати періодичні снапшоти ключових метрик БД і ОС під час нормальної роботи і зберігати їх поруч із маркерами деплою. Всім було нібито цікаво, ніхто не хотів платити за «нудне».
Зрештою це зробили, бо SRE втомився дебажити в сліпу. Кожні п’ять хвилин: підсумки iostat, лаг реплікації, статистика buffer/cache, топ‑очікувань, підписи топ‑запитів. Без сирих URL, без феншуйних тулів — просто регулярні снапшоти, які можна порівнювати. Також фіксували, коли запускалися backup‑джоби і коли відбувалася компресія на масиві.
Потім настала «раптом повільно» нічия. Латентність стрибнула у кількох сервісах. Перше питання: «Яка база зламалась?» Снапшоти відповіли швидше за думки. Await на диску підскочив в ту ж хвилину на всіх хостах БД. Лаг реплікації виріс після, а не перед. CPU здебільшого нормальний. Система була не «повільний SQL», а «повільне сховище».
Оскільки були базові лінії, ескалація до команди сховища відбулася з доказами: device await, глибина черги і кореляція між хостами. Команда сховища знайшла невдалу maintenance‑роботу на масиві і зупинила її. Інцидент закрився без хаотичного вбивання запитів і без взаємних звинувачень. Нудна практика. Правильна практика. Окупилась за один день.
Чеклісти / покроковий план
Чекліст A: Перші п’ять хвилин (використовуйте щоразу)
- З хоста БД: запустіть
uptime,free -h,iostat -xz 1 3. - Вирішіть: CPU‑bound, I/O‑bound чи memory/swap‑bound. Якщо не можете класифікувати — ви дивитесь не на той шар.
- Перевірте з’єднання: лічильники з’єднань БД і час очікування пулу додатка.
- Перевірте очікування: MySQL processlist + InnoDB status; Postgres pg_stat_activity wait_event.
- Зберіть докази: вставте виходи в канал інциденту. Майбутній ви буде вдячний.
Чекліст B: Якщо це I/O‑bound
- Підтвердити насичення диска: iostat await, aqu-sz, %util.
- Ідентифікувати писачів: pidstat -d; перевірте резервні копії/ротацію логів/rsync.
- Підозри на боці БД:
- MySQL: тиск на flushing/checkpoint, великі транзакції, сплески binlog.
- Postgres: контрольні точки, сплески WAL, активність vacuum, тимчасові спливання.
- Мітигація: зупиніть пакетні записувачі, зменшіть конкурентність, відкладіть неключові джоби.
- Після інциденту: оцініть QoS сховища, розділені томи та базову лінію затримки диска.
Чекліст C: Якщо це CPU‑bound
- Знайдіть топ‑запити за часом:
- MySQL: performance schema / вибірки processlist.
- Postgres: pg_stat_statements якщо увімкнено; інакше pg_stat_activity + логи.
- Потвердіть план: сканує? сортує? занадто багато приєднань?
- Мітигація: вбити runaway‑запити, додати відсутній індекс якщо безпечно, зменшити конкурентність, увімкнути feature flags.
- Після: додайте regression‑тест для плану запиту і відслідковуйте підписи запитів під час деплоїв.
Чекліст D: Якщо це блокування/контенція
- Знайти блокери: InnoDB status / performance_schema у MySQL; pg_locks join у Postgres.
- Класифікувати блокер: DDL, довга транзакція, оновлення гарячого рядка або пакетна робота.
- Мітигація: завершити блокера якщо безпечно, або призупинити фічу, що генерує контенцію.
- Після: переробити гарячі місця (шардінг лічильників, використання черг, уникання оновлення одного рядка при високій конкурентності).
Типові помилки: симптом → корінь → виправлення
1) «БД повільна», але тривіальні запити — швидкі
Симптом: Запити додатка таймаутять; БД «виглядає нормально».
Корінь: Насичення пулу з’єднань або виснаження потоків у додатку; запити ставляться в чергу ще до попадання в БД.
Виправлення: Перевірити час очікування пулу і max_connections; зменшити конкурентність додатка; виправити витоки (неповернення з’єднань); додати backpressure.
2) Багато MySQL‑сесій застрягли на «Waiting for table metadata lock»
Симптом: Раптом усе, що стосується однієї таблиці, зависає; CPU може виглядати нормально.
Корінь: DDL або сесія утримує metadata lock і блокує інші оператори.
Виправлення: Ідентифікуйте сесію DDL і зупиніть/вбийте її; переплануйте зміни схеми; використовуйте online schema change підходи, що відповідають вашій версії MySQL і операції.
3) Запити Postgres повільнішають з часом, а потім «раптово» падають у прірву
Симптом: Зростання латентності, збільшення читань з диска, більші таблиці/індекси.
Корінь: Autovacuum не встигає; bloat росте; мертві кортежі залишаються через довгі транзакції або недостатнє налаштування vacuum.
Виправлення: Знайдіть і ліквідуйте довгі транзакції; налаштуйте autovacuum для гарячих таблиць; заплануйте ручний vacuum при необхідності; узгодьте maintenance_work_mem і I/O‑потужності.
4) Репліки відстають і додаток отримує «повільні читання»
Симптом: Ендпоінти для читань повільні або неконсистентні; фейловер ризикований.
Корінь: Реплікаційне застосування не встигає через сплески записів, I/O‑вузьке місце або однониткові обмеження (залежно від рушія/налаштувань).
Виправлення: Зменшити обсяг записів, оптимізувати транзакції, забезпечити реплікам рівні або кращі I/O, перевірити налаштування реплікації. Тимчасово направляйте критичні читання на primary.
5) «Ми додали індекс і стало гірше»
Симптом: Записи сповільнилися, реплікація відстає, диск заповнюється швидше.
Корінь: Індекс підвищив write amplification; зросли витрати на підтримку; запити не використовують його або він неправильний.
Виправлення: Підтвердіть використання через плани і статистику; видаліть неефективні індекси; надавайте перевагу вузьким, селективним індексам; розгляньте partial indexes (Postgres) або складені індекси, підлаштовані під точні предикати.
6) Тимчасові спливання або шторм сортувань у Postgres
Симптом: temp_bytes стрибає; затримка диска росте; сорти стають повільними.
Корінь: Поганий план, що дає величезні проміжні набіри; недостатній work_mem для запиту; забагато одночасних сортувань.
Виправлення: Виправте запит (додайте індекс, перепишіть join/order), використайте LIMIT раніше і акуратно налаштуйте work_mem з урахуванням конкурентності.
7) Почався свапінг і БД стала непередбачуваною
Симптом: Варіабельність латентності вибухає; CPU idle може виглядати високим; iowait росте.
Корінь: Занадто багато з’єднань, занадто велика пам’ять на сесію або тиск на кеш ОС; інколи колокація/шумний сусід.
Виправлення: Зменшити конкурентність; впровадити connection pooling; підібрати пам’ять; дослідити конкуренцію на хості.
FAQ
1) Що легше діагностувати під тиском: MySQL чи PostgreSQL?
Postgres загалом більш явний щодо очікувань і блокерів через системні view. MySQL може бути так само діагностованим, якщо Performance Schema увімкнено і ви знаєте, куди дивитись, але багато команд недооцінюють його можливості.
2) Який найшвидший сигнал, що проблема — у сховищі?
iostat -xz з високим await і високим %util, плюс високий CPU iowait. Якщо це так — сперечки про індекси передчасні.
3) Чи безпечно вбивати запити?
Безпечніше, ніж дозволяти всьому таймаутити. Але робіть це обдумано: ідентифікуйте сесію, переконайтесь, що це не критична міграція/очищення, і віддавайте перевагу скасуванню запиту (Postgres) над вбиванням всього процесу, якщо можливо.
4) Чому «idle in transaction» сесії так важливі в Postgres?
Вони можуть перешкоджати vacuum видаляти мертві кортежі і тримати блокування довше, ніж ви думаєте. Вони також розширюють горизонт транзакцій, що перетворює рутинне обслуговування на інцидент згодом.
5) Чому підвищення max_connections часто робить Postgres повільнішим?
Тому що це підвищує оверхед на бекенд і заохочує необмежену конкурентність. Більше активних сесій — більше контенції, більше кочення кешу і більше тимчасових спливань. Зазвичай правильне рішення — пулінг.
6) Чому MySQL іноді «висне» під час змін схеми?
Через metadata locks і поведінку DDL. Навіть коли операція «онлайн», вона може блокувати на фазах або під певним навантаженням. Ефект чергування може виглядати як повне зависання БД.
7) Як зрозуміти, що це регресія плану?
Симптоми: CPU‑спайк з різким збільшенням логічних читань, або запит, що раніше був швидким, став повільним після деплою або зміни статистики. Підтвердіть, переглянувши план (EXPLAIN) і чи перестав запит використовувати індекс.
8) Чи слід налаштовувати буфери/кеші під час інциденту?
Рідко. Більшість змін налаштувань вимагають перезапуску (іноді) мають відкладений ефект і легко помилитися. Під час інциденту: пом’якште навантаження, зупиніть блокери, вбийте runaway‑запити і зберіть докази. Налаштуйте після з чіткою гіпотезою.
9) Що робити, якщо обидві бази повільні одночасно?
Спочатку припустіть спільну інфраструктуру: сховище, мережа, DNS, хост віртуалізації або спільна резервна робота. Корельована повільність across рушіїв зазвичай — не збіг планів запитів.
10) Якщо можна увімкнути лише одну інструментальну фічу, що вибрати?
Для Postgres: pg_stat_statements (з розумним збереженням). Для MySQL: Performance Schema з інструментами statement і wait на рівні, який ви можете дозволити. Найкращий інцидент — це той, де вам не треба гадати.
Наступні кроки, які можна зробити цього тижня
Якщо ви експлуатуєте продакшн‑бази, «раптом стало повільно» — це не якщо. Це коли. Ось як зробити наступний інцидент коротшим і менш драматичним:
- Запишіть вашу 15‑хвилинну послідовність (скопіюйте план вище) і зробіть її видимою для команди на чергуванні.
- Увімкніть фингерпринтинг запитів і збори очікувань в рушії, який ви використовуєте, і переконайтесь, що можете відповісти: «які топ‑5 запитів за сумарним часом зараз?»
- Побудуйте базову лінію затримки диска у нормальні години. Без бази даних будь‑який графік — сучасне мистецтво.
- Забезпечте дисципліну з’єднань: pool‑те з’єднання, обмежуйте конкурентність і зробіть час очікування пулу першим класом алерту.
- Зробіть зміни схеми нудними: репетируйте їх, плануйте і ставтесь до DDL як до продакшн‑навантаження з очікуваним blast radius.
Коли наступний алерт спрацює, вам не потрібні дебати про рушії. Потрібна швидка класифікація — CPU, пам’ять, I/O, блокування чи план — і один безпечний крок за раз.