Debian 13: журнал повільних запитів MySQL — знайдіть запит, що тихо вбиває сервер

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

Продакшен‑MySQL рідко «лампадає» драматично з аварією. Він просто сповільнюється. Зростає затримка, поглиблюється черга, команда розробників каже «нічого не змінювалося», а ваша ніч на виклику розчиняється в тумані напів‑правд і панелей, що не погоджуються одна з одною.

Журнал повільних запитів — це інструмент, який прорізає цей шум. Не тому, що він витончений, а тому що він записує те, чим насправді займалася база даних. Якщо ви працюєте на Debian 13 і підозрюєте, що один запит тихо перетворює ваш сервер на обігрівач, ось як його впіймати, довести і виправити, не зробивши гірше.

Чому журнал повільних запитів досі переміг у 2025 році

MySQL значно додав інструментів: Performance Schema, перегляди sys schema, трасування оптимізатора, дайджести запитів і стільки перемикачів, що можна зібрати кабіну пілота. Але журнал повільних запитів залишається найшвидшим способом зв’язати «користувачі сердяться» з «ось яке SQL це робить». Він не ідеальний. Він не повний. Він просто стабільно корисний.

Ось у чому він кращий за більшість панелей:

  • Фіксує реальність під навантаженням. Не запит, який ви хотіли б запускати, а той, що реально запустився, коли все горіло.
  • Дешево почати. Увімкніть, візьміть вибірку, продовжуйте. Performance Schema може важити більше, якщо ви не знаєте, що саме увімкнули.
  • Портативний доказ. Файл журналу — це те, що можна передати колезі, прикріпити до тікета інциденту та порівнювати між релізами.
  • Перетворює загальні скарги на ранжований список підозрюваних. Загальний час, кількість, Rows_examined, Lock_time — достатньо, щоб знати, куди копати.

І так, він також може перетворитися на купу «повільних» запитів, які насправді нормальні. Ось чому ви налаштовуєте пороги і інтерпретуєте його як оператор: вам важливі пропускна здатність, хвостова латентність і конкуренція за ресурси — не лише одне число з назвою «Query_time».

Жарт 1: Журнал повільних запитів схожий на трекер робочого часу: це незручно, поки ви не побачите, куди насправді пішов ваш день.

Цитата, яку варто пам’ятати (парафразована ідея): «Надія — не стратегія», часто приписують колам операційного лідерства; ставтеся до роботи з продуктивністю так само — спершу виміряйте, потім дійте.

Цікаві факти та трохи історії (щоб не повторювати старі помилки)

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

  1. Журнал повільних запитів передував сучасній спостережливості. Це один із перших вбудованих способів знайти проблемні запити без зовнішніх профайлерів.
  2. «Повільно» — це політичний вибір, а не універсальна істина. Запит на 200 мс повільний для ендпоінта логіну, але швидкий для нічного звіту.
  3. MySQL може логувати запити, що не використовують індекси. Прапорець log_queries_not_using_indexes існує, бо повні сканування таблиць часто приводять до несподіваних аварій.
  4. Журнал може включати адміністративні оператори. З log_slow_admin_statements ви впіймаєте «невинні» DDL або технічні роботи, що зупиняють продакшен.
  5. InnoDB змінив характер «повільного». Коли InnoDB став за замовчуванням, багато уповільнень перемістилися з CPU‑зв’язаних на очікування I/O і блокування.
  6. Кеш запитів було видалено в MySQL 8.0. Ціла ера «вирішили через кешування» закінчилася — і на щастя, бо інвалідизація кешу була джерелом хаосу.
  7. Реплікація може показати повільні запити двічі. Оператор, повільний на первинному, часто повільний і на репліках; плюс репліки можуть відставати і посилювати проблему.
  8. Реплікація на рівні рядків зробила «той самий оператор» менш релевантним. Проблеми з продуктивністю іноді ховаються в логіці тригерів або побічних ефектах, а не в тексті SELECT.
  9. Сучасний MySQL дає дайджести запитів. Performance Schema може агрегувати за нормалізованими шаблонами; slow log показує конкретні приклади, які можна відтворити.

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

Це порядок дій, який швидко знаходить винуватця, не погіршуючи інцидент. Мета — не «оптимізувати MySQL», а «зупинити кровотечу».

Перший: вирішіть, чи ви CPU‑зв’язані, I/O‑зв’язані чи маєте проблеми з блокуваннями

  • Якщо CPU завантажено: очікуйте погані плани, відсутні індекси, великі сорти або занадто багато одночасних запитів.
  • Якщо диск насичений: очікуйте сканування, великі читання, пропуски в буфер‑пулі або тиск під час чекпоїнтів/флашів.
  • Якщо потоки чекають: очікуйте блокувань, проблем з metadata lock, довгих транзакцій або «гарячих» рядків.

Другий: увімкніть або підтвердіть slow log, потім захопіть чистий часовий проміжок

Не дивіться тижневий журнал і не припускайте. Прокрутіть, потім захопіть 5–15 хвилин під час болю. Вам потрібне вузьке вікно, що відповідає симптомам.

Третій: ранжуйте за сумарним часом, а не за «найгіршим одиночним запитом»

Запит, що виконувався 20 секунд один раз, драматичний. Запит, що виконується по 120 мс 4 000 разів на хвилину, тихо їсть ваш обід. Ранжуйте за sum(Query_time) і за count, потім розбирайтеся з прикладами.

Четвертий: перевірте за допомогою EXPLAIN і фактичних статистик обробника

Slow logs кажуть, що сталося. EXPLAIN каже, що MySQL планував робити. Обидва можуть помилятися поодинці. Поєднуйте їх.

Увімкнення журналу повільних запитів на Debian 13 (і як не нашкодити)

Debian 13 зазвичай запускає MySQL близько 8.0 або MariaDB залежно від вибору. Механіка подібна, але розташування файлів конфігурації та пакункові дефолти відрізняються. Я припущу Oracle MySQL (пакет mysql-server) з systemd; ті ж кроки здебільшого застосовні до MariaDB з трохи іншими іменами сервісів та змінними.

Де зазвичай лежить конфіг

  • /etc/mysql/my.cnf включає інші файли.
  • /etc/mysql/mysql.conf.d/mysqld.cnf — звичне місце для налаштувань сервера.
  • /var/log/mysql/ — поширена директорія для логів, але перевірте права та профілі AppArmor.

Мінімальна адекватна конфігурація slow‑log

Не перевантажуйте себе. Почніть з логування у файл, помірного порогу і уникайте логування кожного запиту на завантаженій системі.

cr0x@server:~$ sudo editor /etc/mysql/mysql.conf.d/mysqld.cnf
...add or adjust...
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 0.2
log_queries_not_using_indexes = OFF
log_slow_admin_statements = ON
log_slow_replica_statements = OFF

Чому такі вибори:

  • long_query_time=0.2 — достатньо агресивно, щоб зафіксувати «смерть від тисячі порізів», без логування всього (зазвичай). Налаштовуйте пізніше.
  • log_queries_not_using_indexes=OFF спочатку, бо це може засипати журнал «шкідливими» дрібними скануваннями таблиць.
  • log_slow_admin_statements=ON бо онлайн‑DDL і адмін‑команди можуть зупинити продакшен.

Безпечний перезавантаження

Зміна налаштувань slow log часто динамічна, але не робіть на це сліпих припущень. Використовуйте SET PERSIST там, де доречно, і перевіряйте через SHOW VARIABLES.

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

Це операторські кроки, які працюють о 2:00 ночі. Кожен включає, що означає вивід і яке рішення ви приймаєте далі.

Завдання 1: Підтвердіть, який сервер ви запускаєте (MySQL чи MariaDB) і версію

cr0x@server:~$ mysql --version
mysql  Ver 8.0.36-0+deb13u1 for Linux on x86_64 (MySQL Community Server - GPL)

Що це означає: Ви знаєте, які імена змінних і функції застосовуються. MySQL 8 має SET PERSIST; MariaDB відрізняється.

Рішення: Використовуйте синтаксис MySQL 8 і очікуйте, що Performance Schema доступна за замовчуванням.

Завдання 2: Перевірте, чи увімкнено повільне логування

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log';"
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | OFF   |
+----------------+-------+

Що це означає: У вас наразі немає форензичного сліду.

Рішення: Увімкніть його негайно (динамічно, якщо можливо), потім захопіть коротке вікно під навантаженням.

Завдання 3: Увімкнути slow log динамічно (відразу, не після перезавантаження)

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = ON;"
...Query OK, 0 rows affected...

Що це означає: Нові повільні запити будуть логуватися негайно.

Рішення: Встановіть шлях до файлу логів далі, перевірте, чи його можна записувати, потім встановіть поріг.

Завдання 4: Встановіть шлях файла slow log і перевірте, що MySQL може його записувати

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';"
...Query OK, 0 rows affected...
cr0x@server:~$ sudo install -o mysql -g adm -m 0640 /dev/null /var/log/mysql/mysql-slow.log
cr0x@server:~$ sudo ls -l /var/log/mysql/mysql-slow.log
-rw-r----- 1 mysql adm 0 Dec 30 01:12 /var/log/mysql/mysql-slow.log

Що це означає: Файл існує з адекватною власністю. MySQL не зламається мовчки через неможливість відкрити лог.

Рішення: Якщо власність невірна — виправте її перед тим, як шукати фантомні «відсутні повільні запити».

Завдання 5: Встановіть long_query_time для триажу

cr0x@server:~$ mysql -e "SET GLOBAL long_query_time = 0.2; SHOW VARIABLES LIKE 'long_query_time';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| long_query_time | 0.200 |
+-----------------+-------+

Що це означає: Запити повільніше 200 мс фіксуються.

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

Завдання 6: Прокрутіть slow log, щоб ізолювати часове вікно

cr0x@server:~$ mysql -e "FLUSH SLOW LOGS;"
...Query OK, 0 rows affected...
cr0x@server:~$ sudo ls -lh /var/log/mysql/mysql-slow.log*
-rw-r----- 1 mysql adm  12K Dec 30 01:15 /var/log/mysql/mysql-slow.log
-rw-r----- 1 mysql adm 1.8M Dec 30 01:12 /var/log/mysql/mysql-slow.log.1

Що це означає: Ви створили чистий «поточний» файл журналу. Старий архівований з суфіксом.

Рішення: Захопіть наступні 5–15 хвилин болю і аналізуйте лише їх.

Завдання 7: Підтвердіть, що MySQL справді пише записи

cr0x@server:~$ sudo tail -n 5 /var/log/mysql/mysql-slow.log
# Time: 2025-12-30T01:16:19.123456Z
# User@Host: app[app] @ 10.0.2.41 []  Id:  9123
# Query_time: 0.412  Lock_time: 0.000  Rows_sent: 20  Rows_examined: 250000
SET timestamp=1735521379;
SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20;

Що це означає: Логування працює. Цей приклад вже пахне «відсутній індекс для ORDER BY + filter».

Рішення: Не оптимізуйте за одним рядком. Спочатку агрегуйте.

Завдання 8: Швидка агрегація з mysqldumpslow

cr0x@server:~$ sudo mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
Reading mysql slow query log from /var/log/mysql/mysql-slow.log
Count: 184  Time=0.29s (52s)  Lock=0.00s (0s)  Rows=20.0 (3680), app@app[10.0.2.41]
  SELECT * FROM orders WHERE user_id=N ORDER BY created_at DESC LIMIT N

Count: 21  Time=1.10s (23s)  Lock=0.02s (0s)  Rows=1.0 (21), app@app[10.0.2.55]
  UPDATE inventory SET qty=qty-N WHERE sku='S'

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

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

Завдання 9: Встановіть і запустіть pt-query-digest для реального ранжування

cr0x@server:~$ sudo apt-get update
...output...
cr0x@server:~$ sudo apt-get install -y percona-toolkit
...output...
cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log | sed -n '1,80p'
# 2025-12-30T01:20:44
# Profile
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ====
#    1 0xA1B2C3D4E5F6A7B8  52.1230 65.0%   184 0.2833  0.12 SELECT orders
#    2 0x1122334455667788  23.0041 28.7%    21 1.0954  0.03 UPDATE inventory
#    3 0x9988776655443322   3.9002  4.9%    97 0.0402  0.01 SELECT sessions

Що це означає: Тепер у вас є ранжований список за сумарним часом. Фінгерпринти запитів зменшують шум «той самий запит, але з різними ID».

Рішення: Витягніть повний розділ звіту для рангу 1 і використайте його для відтворення та EXPLAIN.

Завдання 10: Отримайте контекст схеми та індексів для топового запиту

cr0x@server:~$ mysql -e "SHOW CREATE TABLE orders\G" | sed -n '1,60p'
*************************** 1. row ***************************
Table: orders
Create Table: CREATE TABLE `orders` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `created_at` datetime(6) NOT NULL,
  `status` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB

Що це означає: Немає композитного індексу на (user_id, created_at). Запит фільтрує по user_id і сортує по created_at — класичний випадок.

Рішення: Заплануйте індекс, що відповідає патерну доступу: (user_id, created_at DESC) в MySQL 8 (DESC підтримується), або (user_id, created_at) зазвичай теж допоможе.

Завдання 11: Перевірте план виконання (EXPLAIN) і те, що сканується

cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: orders
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 248901
     filtered: 10.00
        Extra: Using where; Using filesort

Що це означає: Повне сканування таблиці (type: ALL) плюс filesort. Оце ваші I/O та CPU.

Рішення: Додайте правильний індекс, потім перезапустіть EXPLAIN, щоб підтвердити його використання, і виміряйте до/після.

Завдання 12: Додайте індекс обережно (онлайн, якщо можливо) і спостерігайте вплив

cr0x@server:~$ mysql -e "ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at DESC);"
...Query OK, 0 rows affected...

Що це означає: Індекс створено. Залежно від розміру таблиці і налаштувань MySQL це може зайняти час і споживати I/O. На великих таблицях робіть це у вікно технічного обслуговування або використовуйте інструменти для онлайн зміни схеми.

Рішення: Негайно перевірте новий план і чи зменшився обсяг slow log. Якщо є реплікація — перевірте відставання реплік.

Завдання 13: Повторіть EXPLAIN, щоб підтвердити покращення плану

cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: orders
         type: ref
possible_keys: idx_user_created
          key: idx_user_created
      key_len: 8
          ref: const
         rows: 20
        Extra: Using index condition

Що це означає: MySQL тепер використовує індекс і очікує ~20 рядків, а не ~250k. Filesort зник.

Рішення: Моніторте латентність і I/O. Якщо покращилося — ймовірно, ви знайшли «той самий запит». Продовжуйте з наступним підозрюваним.

Завдання 14: Перевірте, чи «повільний» час насправді був часом блокування

cr0x@server:~$ grep -E "Query_time|Lock_time" -n /var/log/mysql/mysql-slow.log | head
2:# Query_time: 0.412  Lock_time: 0.000  Rows_sent: 20  Rows_examined: 250000
8:# Query_time: 1.905  Lock_time: 1.723  Rows_sent: 1  Rows_examined: 1

Що це означає: Другий приклад провів більшість часу, чекаючи блокувань, а не виконуючись.

Рішення: Припиніть «оптимізувати» текст SQL і знайдіть, хто тримає блокування: довгі транзакції, «гарячі» рядки або metadata locks.

Завдання 15: Ідентифікуйте поточних блокувальників і очікування блокувань

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | sed -n '1,20p'
Id	User	Host	db	Command	Time	State	Info
9123	app	10.0.2.41:51022	prod	Query	2	Sending data	SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20
9150	app	10.0.2.55:53488	prod	Query	35	Waiting for row lock	UPDATE inventory SET qty=qty-1 WHERE sku='ABC-123'
9201	admin	localhost	prod	Query	120	Starting alter table	ALTER TABLE inventory ADD COLUMN last_checked datetime

Що це означає: У вас є очікування на блокування рядків і виконується DDL. Цей DDL може впливати на конкурентність залежно від алгоритму і metadata locks.

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

Завдання 16: Перевірте тиск на буфер‑пул InnoDB (I/O проти пам’яті)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name                         | Value      |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests      | 987654321  |
| Innodb_buffer_pool_reads              | 12345678   |
+---------------------------------------+------------+

Що це означає: Читання з диска немалі. Високий показник Innodb_buffer_pool_reads означає пропуски кешу і плату в I/O.

Рішення: Якщо буфер‑пул замалий — збільшіть його (обережно). Але спочатку усуньте сканування‑важкі запити; інакше ви просто будете кешувати свої помилки.

Читати slow logs як SRE, а не як ворожка

Запис у slow log — це маленька історія: хто його виконав, скільки часу це зайняло, що повернулося і скільки було переглянуто. Хитрість — зрозуміти, яка частина цієї історії придатна до дій.

Поля, що мають значення (і що вони намагаються вам сказати)

  • Query_time: час за стінкою. Включає очікування CPU, I/O, блокувань і іноді затримки планування. Це симптом, а не діагноз.
  • Lock_time: час, витрачений в очікуванні таблицевих/рядкових блокувань (залежить від рушія). Якщо він великий — ваш «повільний» запит може бути швидким SQL, що стоїть за повільною транзакцією.
  • Rows_examined: скільки рядків переглянуто. Високий Rows_examined при низькому Rows_sent — типовий підпис відсутнього індексу.
  • Rows_sent: скільки даних повернуто. Якщо це величезно — запит «працює як задумано», але дизайн неправильний (балакучі ендпоінти, гігантські експорти).
  • User@Host: ідентифікує джерело навантаження. Часто вказує, яка служба мусить виправити проблему.
  • Timestamp: дозволяє корелювати з деплоєм, cron‑завданнями або стрибками трафіку.

Три архетипи «тихо вбивають вас»

Ви бачитимете ці повторно:

  1. Висока частота, помірний час: запит, що не жахливий, але виконується постійно. Його виправлення покращує CPU і хвостову латентність. Це найкращий ROI.
  2. Низька частота, величезний час: звіти, батч‑роботи та «один великий запит». Часто прийнятно, якщо заплановано; неприпустимо, якщо викликається користувачами.
  3. Блокування‑доміновані: запити виглядають повільними, бо їх блокували. Тут виправляєте область транзакцій, поведінку ізоляції або «гарячі» ділянки даних.

Де операторів вводять в оману

Slow log буквальний. Він записує, що запит тривав 1.2 секунди. Він не записує чому. Причина може бути:

  • спад latency файлової системи,
  • запуск чекпоїнту,
  • контенція м’ютексів,
  • I/O‑навантаження на репліці,
  • або одна сесія тримає блокування, бо почала транзакцію і пішла за кавою.

Останнє трапляється частіше, ніж хочеться визнати.

Жарт 2: Довга транзакція — як залишити мікрохвильовку в офісній кухні увімкненою: всі помічають, ніхто не знає, хто це зробив.

Інструменти: mysqldumpslow, pt-query-digest та друзі

Ви багато можете зробити лише з grep і терпінням, але у вас є краще заняття, ніж вручну нормалізувати SQL. Використовуйте інструменти. Знайте, у чому вони хороші.

mysqldumpslow: швидко і грубо

Поставляється з MySQL і підходить для першого зрізу. Групує запити грубою нормалізацією (замінює числа і рядки). Добре підходить для «який зараз головний підозрюваний?». Не дуже добрий для глибокого аналізу між багатьма джерелами або складних варіантів операторів.

pt-query-digest: триаж для продакшену

Дайджест Percona Toolkit — робоча конячка. Він ф fingerprintує запити, агрегує за сумарним часом і варіаціями та друкує репрезентативні зразки. Він корисно нав’язливий: змушує фокусуватися на важливому.

Якщо у вас стабільне продакшен‑середовище, розгляньте планову задачу, яка буде періодично обробляти slow log і зберігати зведення. Не тому, що ви любите звіти, а тому, що інциденти коротші, коли ви вже знаєте звичних підозрюваних.

Коли Performance Schema допомагає

Slow log реактивний: він каже про запити, що перетнули поріг. Performance Schema може бути проактивним: показувати топ‑запити за сумарним часом навіть якщо вони індивідуально «не повільні».

Але якщо ви вже в біді і не знаєте поточних витрат, не починайте вмикати десяток інструментів моніторингу посеред інциденту. Ваша задача — відновити сервіс, а не стати одноособовою платформою телеметрії.

Режими відмов: коли журнали повільних запитів брешуть (або вводять в оману)

Журнал повільних запитів не помиляється. Він просто неповний. Ось поширені способи, якими він шле вас за хибним слідом.

1) Поріг занадто високий, і справжній вбивця не з’являється

Якщо long_query_time = 2 секунди, ви пропустите запит 150 мс, що виконується 1 000 разів на секунду. Цей запит може завантажити CPU, ніби він ніколи не був «повільним».

Виправлення: тимчасово знизьте long_query_time під час розслідування або використайте Performance Schema, щоб ранжувати за сумарним часом.

2) Поріг занадто низький, і ви тонуєте в шумі

Якщо ви встановите long_query_time в 0.01 на зайнятому сервері, ви можете генерувати логи швидше, ніж встигаєте їх записувати. Вітаю, ви щойно винайшли журнал‑DOS.

Виправлення: робіть вибірки в коротких вікнах. Ротація журналів. Аналізуйте невеликі зрізи. Підніміть поріг пізніше.

3) Очікування блокувань роблять невинні запити підозрілими

Простий UPDATE може показати Query_time: 2.0 з Lock_time: 1.9. SQL не повільний; модель конкурентності зламана.

Виправлення: знайдіть блокувальника (processlist, InnoDB lock waits), скоротіть час транзакцій, зменшіть «гарячі» рядки або змініть шаблон записів.

4) Повільний запит — симптом колапсу I/O, а не причина

Коли латентність сховища підскакує, все сповільнюється. Журнал покаже парад «повільних» запитів, і ви звинуватите не той запит.

Виправлення: підтвердіть здоров’я та латентність сховища. Шукайте пропуски в буфер‑пулі і сканування; потім перевіряйте платформу (диск, RAID, віртуалізацію, «шумних сусідів»).

5) Місце для журналу недоступне для запису, і ви отримуєте тишу

MySQL не завжди може писати туди, куди ви вказали, особливо з AppArmor. Ви будете думати, що у вас немає повільних запитів. Насправді вони є, але журналів немає.

Виправлення: переконайтеся, що файл існує, власність правильна, і перевірте журнали помилок MySQL на повідомлення про неможливість відкрити файл.

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

1) «CPU завантажено, але slow log порожній»

Симптом: Високий CPU, висока QPS, користувачі скаржаться, slow log майже пустий.

Корінна причина: long_query_time занадто високий; ваш вбивця «швидкий», але частий.

Виправлення: тимчасово знизьте long_query_time (наприклад, 0.1–0.2), прокрутіть логи і/або використайте Performance Schema для ранжування за сумарним часом.

2) «Все сповільнилося після увімкнення log_queries_not_using_indexes»

Симптом: Диск зайнятий, обсяг логів вибухнув, MySQL почав гірше працювати.

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

Виправлення: вимкніть його, потім використайте дайджести, щоб знайти кілька сканувань з великим Rows_examined або високим сумарним часом.

3) «Ми додали індекс і продуктивність стала гіршою»

Симптом: Записи сповільнилися, реплікація відстала, буфер‑пул почав частіше оновлюватися.

Корінна причина: Новий індекс збільшив вартість записів і пам’ять; ви оптимізували шлях читання, не врахувавши обсяг записів.

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

4) «Повільні запити — це всі однакові SELECT, але тільки іноді»

Симптом: Той самий оператор з’являється з різним Query_time.

Корінна причина: Параметри дають різну селективність; план нестабільний; статистики застаріли; або з’являються спіки блокувань.

Виправлення: порівняйте Rows_examined між зразками, виконайте EXPLAIN з репрезентативними параметрами, оновіть статистику і перевірте блокування, якщо різко зростає Lock_time.

5) «Найповільніший запит — це звіт; сайт гине»

Симптом: Великий звіт домінує в slow log, латентність застосунку зростає.

Корінна причина: Ресурси спільні. Звіт насичує I/O або буфер‑пул, шкодячи OLTP‑робочому навантаженню.

Виправлення: перемістіть звіт на репліку, заплануйте на ніч, виділіть ресурси або попередньо агрегуйте. Не «оптимізуйте», додаючи випадкові індекси до OLTP‑таблиць у продакшені в першу чергу.

6) «Запити стали повільнішими одразу після міграції схеми»

Симптом: Зростає Lock_time і стани очікування; slow log показує адміністративні оператори.

Корінна причина: DDL викликав metadata locks або пересобирання великої таблиці; міграція зіткнулася з піковим трафіком.

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

Три корпоративні міні‑історії з фронту

Інцидент: хибне припущення «репліки не важливі»

Одна команда працювала з транзакційною базою і двома репліками. Репліки були «для читання і бекапів», і ця фраза стала приводом ігнорувати їхню продуктивність. Первинний сервер моніторили ретельно; репліки вважали холодними запасними копіями.

Потім вийшла нова функція: застосунок почав направляти частину читань профілів користувачів на репліки. Не всі читання — лише деякі. Роллаут був поступовим, тому не було явного обвалу. Люди бачили повільне зростання p95 і кілька «випадкових таймаутів», що не корелювали з метриками первинки.

Slow log на первинному виглядав нудно. На репліках — кричав. Конкретний SELECT з «невинним» ORDER BY виконувся трохи нижче порога на первинному завдяки теплому кешу, але постійно перетинав поріг на репліках, бо їхні буфер‑пули були холодні, а диски повільніші.

Хибне припущення було в тому, що запит, який «працює на первинному», має бути добрим скрізь. Ні. Репліки мали інші характеристики I/O, іншу форму навантаження і задачу бекапу, що стрибала кожну годину. Slow logs це довели: той самий fingerprint запиту, різні розподіли Query_time.

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

Оптимізація, що відплатилася: індекс, який з’їв шлях запису

Сервіс, суміжний із фінансами, мав таблицю з постійними записами. Читання були часті, але записи — безперервні. Інженер побачив повільний запис у журналі для запиту з фільтром (account_id, created_at) і зробив «очевидне»: додав великий композитний індекс, який також включав широкий varchar‑стовпець, щоб повністю покрити запит.

Читання покращилися. Виглядало чудово в staging. У продакшені шлях запису почав хилитися. Латентність вставок зросла, з’явився лаг реплікації, і буфер‑пул став менш ефективним. Slow log почав показувати інші запити: не оригінальний SELECT, а вставки і оновлення, що тепер тривали довше, бо кожен запис підтримував важчі індекси.

Команда провела день у суперечках щодо того, чи «повинен» індекс допомогти. Він допоміг читанню. Він також змінив економіку всієї таблиці. InnoDB не дає індекси даром; він бере плату у вигляді збільшення вартості запису і пам’яті кожного разу.

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

Нудно, але правильно: логування у вікнах часу і ротація, що врятувала день

SaaS‑платформа мала повторювану проблему: раз на кілька днів система «відчувала себе повільно» близько десяти хвилин. Нічого драматичного, просто достатньо для тикетів підтримки. Це був тип проблеми, що губляться тижнями через переривчастість і купу теорій.

Інженер на виклику зробив нудну, але корисну річ: додав крок у playbook — прокрутити і ротацію slow log одразу після початку інциденту, потім захопити саме 10 хвилин даних. Не «файл з учора», не «можливо, о 3», а чітке вікно.

Після двох інцидентів з’явився патерн. Це не був один запит; це була батч‑робота, що запускалася кожні кілька годин і породжувала хвилю помірних запитів. Поодинці вони були нормальні. Разом вони насичували I/O і штовхали буфер‑пул через край. Slow log, коли його розрізали у правильному вікні, показав стабільний набір fingerprint‑ів, що з’являвся лише під час події.

Виправлення теж було нудним: перемістити батч на репліку і додати обмеження швидкості для роботи. Кількість тікетів підтримки впала. Ніхто не отримав медалі. Продакшен став тихішим — найближче до романтики для SRE.

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

Коли ви в інциденті (30–60 хвилин)

  1. Класифікуйте вузьке місце: CPU, I/O чи блокування. Не гадуйте — перевіряйте.
  2. Підтвердіть, що slow log увімкнено і доступне для запису: якщо він не пише — виправте це спершу.
  3. Встановіть триажний поріг: long_query_time близько 0.1–0.5 залежно від навантаження.
  4. Прокрутіть/ротація логів: ізолюйте чисте вікно під час болю.
  5. Зробіть дайджест і ранжування: використайте pt-query-digest, якщо є; інакше — mysqldumpslow.
  6. Виберіть топ‑порушника за сумарним часом: якщо тільки Lock_time не вказує на проблему з блокуванням.
  7. EXPLAIN з реальними параметрами: підтвердіть сканування vs використання індексу, filesort, тимчасові таблиці.
  8. Виправте мінімально: додайте або відрегулюйте один індекс, або перепишіть один шлях запитів. Не рефакторіть всесвіт.
  9. Повторно виміряйте: прокрутіть лог знову, порівняйте топ‑підозрюваних до/після.
  10. Задокументуйте fingerprint: нормалізований шаблон запиту — те, що має значення для повторення.

Після інциденту (наступного дня)

  1. Встановіть стійку конфігурацію: використайте файл конфігурації або SET PERSIST, щоб налаштування не загубилися при рестарті.
  2. Визначте зберігання логів: slow logs ростуть; оберніть через logrotate, шліть в централізоване сховище при потребі.
  3. Визначте пороги відповідно до SLO: виберіть long_query_time, що ловить видиму користувачеві латентність, а не шум.
  4. Зробіть базову лінію топ‑fingerprint‑ів: ваш «звичний» топ‑20 — потужний ранній сигнал.
  5. Налагодьте практику рев’ю індексів: кожен новий індекс має вартість для записів і пам’яті; ставте це в розрахунок як планування ємності.

Операційні запобіжні заходи (щоб не зашкодити собі в майбутньому)

  • Ніколи не вмикайте «логувати все» постійно на зайнятому первинному сервері.
  • Тримайте slow logs на локальному диску з передбачуваною латентністю, а не на мережевій файловій системі.
  • Робіть схему міграцій видимою: логгуйте повільні адміністративні оператори і відстежуйте metadata lock waits.
  • Навчіть команди читати Rows_examined як рахунок: якщо він величезний — хтось платить.

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

1) Логувати у файл чи в таблицю?

Файл — для більшості продакшенів. Логування в таблицю може створити контенцію, збільшити навантаження на data dictionary і ускладнити ретеншн. Використовуйте логування в таблицю тільки якщо у вас є керований пайплайн і ви перевірили накладні витрати.

2) Яке хороше значення для long_query_time?

Почніть у діапазоні 0.2–0.5 секунди для OLTP‑сервісів, потім налаштовуйте за обсягом і SLO. Під час розслідування тимчасово знижуйте, щоб захопити більше прикладів. Не ставте 0, якщо вам не подобається заповнення дисків.

3) Чому я бачу високий Rows_examined та низький Rows_sent?

Зазвичай це відсутній або невідповідний індекс, або запит не може використати індекс через функції, неявні приведення типів чи поганий порядок предикатів. Підтвердіть через EXPLAIN; шукайте type: ALL або Using filesort.

4) Slow log показує UPDATE з великим часом, але EXPLAIN виглядає добре. Що робити?

Перевірте Lock_time. Якщо він великий, ваш UPDATE був заблокований. Знайдіть блокуючу транзакцію, скоротіть час транзакцій і уникайте «гарячих» рядків, які багато сесій оновлюють одночасно.

5) Чи нашкодить увімкнення slow query log продуктивності?

Невеликий наклад є — в основному через запис логів. З розумними порогами і логуванням у локальний файл це зазвичай прийнятно. Справжній ризик — надто агресивні налаштування і величезний обсяг логів.

6) Як впіймати «не повільні, але занадто часті» запити?

Тимчасово знизьте long_query_time і зробіть дайджест за сумарним часом, або використайте Performance Schema для підсумків за станом запитів. Slow log за своєю суттю орієнтований на пороги.

7) Чому мої slow logs порожні, хоча slow_query_log=ON?

Поширені причини: шлях до файлу недоступний для запису, AppArmor блокує записи, поріг занадто високий, або навантаження домінують швидкі запити й очікування блокувань, що не перетинають поріг. Перевірте журнали помилок на повідомлення про помилку відкриття файлу і підтвердіть активний шлях через SHOW VARIABLES.

8) Чи можна автоматично знаходити відсутні індекси зі slow logs?

Ви можете отримати сильні підказки, але не впевненість. Високий Rows_examined і Using filesort вказують на можливості для індексів. Але проєкт індексу вимагає розуміння співвідношення читань/записів, кардинальності і патернів запитів. Slow log показує, куди дивитися; думати все одно доведеться вам.

9) А репліки — чи мають вони інші налаштування slow log?

Часто так. На репліках можна вести глибше логування і глибшу інструментацію, бо вони не є вузьким місцем записів. Але якщо застосунок читає з реплік, ставте їх як першокласну продукцію і моніторьте так само.

10) Як довго зберігати slow logs?

Зберігайте стільки, щоб покрити розслідування інцидентів і виявлення регресій — зазвичай від кількох днів до пара тижнів, залежно від обсягу. Робіть агресивну ротацію і зберігайте зведення, якщо потрібні довші тренди без зайвого дискового простору.

Висновки: що робити далі, завтра вранці

Якщо ваш MySQL на Debian 13 відчуває, ніби тихо вмирає, припиніть гадати. Увімкніть журнал повільних запитів із здоровим порогом, прокрутіть, щоб захопити чисте вікно, зробіть дайджест за сумарним часом і виправте головного винуватця з доказами: EXPLAIN, зміни індексів, що відповідають патернам доступу, і валідація після зміни.

Потім зробіть це рутинно. Тримайте slow logging доступним, запускайте дайджести під час інцидентів і ставтеся до змін індексів як до продакшен‑змін — з планом відкату і розумінням витрат для записів. «Тихий вбивця‑запит» рідко винахідливий. Зазвичай він нудний, повторюваний і дорогий. Саме тому він залишається непоміченим.

← Попередня
Proxmox Ceph: PG stuck/inactive — що робити, поки ризик для даних не зросте
Наступна →
Математика відновлення ZFS RAIDZ: чому ще одна відмова може вас вбити

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