Є особливий тип відмови, коли база даних ніби «не впала». Вона працює, приймає з’єднання, повертає деякі запити і підморгує здоровими чекерами. Натомість ваш API таймаутиться, репліки відстають або йдуть уперед, і кожен інженер дивиться на дашборд, який каже, що все «зелено».
Ось де MySQL, який ви думаєте, що запускаєте, та MySQL, який ви насправді запускаєте — RDS MySQL — перестають бути синонімами. Відмінності тонкі, поки не стають катастрофічними. Більшість інцидентів не спричинені однією великою помилкою; вони виникають, коли хибна припущення зустрічається з прихованим обмеженням о 2:13 ранку.
Основна невідповідність: «MySQL» — це не модель розгортання
Самостійно керований MySQL — це купа виборів. Файлова система, блочний пристрій, рівень RAID (або ні), прикріплення CPU, поведінка кешу сторінок ОС, особливості планувальника ядра, налаштування TCP, інструменти резервного копіювання і той дивний cron job, який колись написав колишній колега в 2019 році і ніхто не хоче торкатися. Це гнучко. Це також ваша відповідальність, а значить — ваша вина під час інцидентів.
RDS MySQL — це продукт. Він поводиться як MySQL на рівні SQL, але живе в огорожах: кероване сховище, керовані бекапи, кероване патчування, керований failover, керована спостережуваність. Це управління має обмеження, які ви не контролюєте і іноді навіть не бачите. У спокійний тиждень це — перевага. Під час інциденту це — переговори.
Типовий режим відмови — не «RDS гірший». Це «ви планували, ніби у вас самостійний сервер», або навпаки. В продакшні система, яку ви припускаєте, — це система, яку ви дебажите. Якщо ваші припущення хибні, ви будете впевнено дебажити і нічого не виправите.
Цитата, яку варто тримати приклеєною до монітора, від Вернера Вогельса: Побудував — ти ним і керуєш.
Вона коротка і болісна, бо правдива.
Факти та історія, що пояснюють сучасні гострі кути
- Типове сховище за замовчуванням InnoDB змінило гру. InnoDB став дефолтним в MySQL 5.5, і раптово «ACID» перестав бути преміальною функцією. Це також зробило розмір I/O і журналів redo першокласною операційною проблемою.
- Amazon представив RDS у 2009 році. Обіцянка була простою: припиніть дивитися за серверами. Компроміс теж був простим: ви не отримуєте root, і погоджуєтеся з опініонованою інфраструктурою.
- Performance Schema не завжди був загальноприйнятою практикою. Він розвивався роками; багато «експертів MySQL» навчалися в епоху, коли ви переглядали slow logs і гадали. RDS ускладнює глибоко OS-рівневі інструменти, тож варто вивчити сучасну інструментацію MySQL.
- «Doublewrite buffer» InnoDB існує тому, що сховище бреше. Розірвані сторінки трапляються. Кероване сховище зменшує частину ризику, але не скасовує потребу у проєктуванні, що гарантує консистентність після краху.
- Реплікація має кілька особливостей. Класична асинхронна реплікація відрізняється від semi-sync, і обидві відрізняються від group replication. RDS підтримує деякі режими, обмежує інші й додає власну операційну поведінку навколо failover.
- «General purpose SSD» не завжди був достатнім. gp2/gp3 і io1/io2 існують тому, що IOPS — це тепер продуктове рішення, а не тільки інженерна деталь. На своїй інфраструктурі ви боролися з фізикою; в хмарі ви боретесь із останнім закупівельним рішенням.
- Online DDL змінив реагування на інциденти. DDL у MySQL покращився, але не всі ALTER однакові, і RDS додає обмеження навколо довготривалих операцій та вікон обслуговування.
- Бекапи перейшли з «файлів» на «знімки». Логічні дампи портативні, але повільні; знімки швидкі, але пов’язані зі способом зберігання платформи. RDS заохочує знімки; ваша стратегія відновлення має враховувати це зв’язання.
Приховані обмеження, що б’ють під час інцидентів (і чому)
1) Сховище — це не просто «розмір»: це латентність, burst і write amplification
У самостійно керованому MySQL ви можете приєднати швидші диски, налаштувати RAID-контролер або кинути NVMe на проблему. В RDS ви вибираєте клас сховища на базі EBS і живете в його обмеженнях IOPS та пропускної здатності. Якщо ви обрали gp2 роки тому і ніколи не поверталися до цього вибору, можливо, ви живете на «burst credits», навіть не підозрюючи.
Сценарій інциденту: латентність зростає, CPU виглядає нормально, запити сповільнюються, і всі звинувачують «поганий деплой». Насправді злодій — це черга диска. InnoDB потребує багато I/O, коли робочий набір перевищує buffer pool або коли він змушений скидати брудні сторінки під тиском.
Що приховано: підсистема сховища керується. Ви не можете виконати iostat на хості. Потрібно покладатися на метрики RDS та статусні змінні MySQL. Це не гірше — це інакше: потрібні інші рефлекси.
2) «Безкоштовне місце» — це брехня під час spill-ів тимчасових таблиць і online DDL
RDS може автоматично масштабувати сховище в деяких конфігураціях, але воно не масштабує той тип місця, який потрібен вам прямо зараз для великого spill-а тимчасової таблиці, великого ALTER, що будує копію, або довгої транзакції, яка роздуває undo. Ви можете мати багато виділеного сховища і все одно влучити в стіну «тимчасового простору», яка здається випадковою, якщо ви звикли працювати на bare metal.
У самостійних середовищ часто tmpdir розміщують на окремому томі й розмічають навмисно. В RDS використання tmp взаємодіє з локальним ephemeral-простором інстансу та конфігурацією. Потрібно знати, що робить ваш движок під тиском простору і який запас у вас є, перш ніж система почне падати креативними способами.
3) max_connections — це не просто число; це радіус ураження
RDS встановлює значення за замовчуванням і іноді прив’язує дозволені значення до класу інстансу через parameter groups. На власній машині ви можете підняти max_connections поки не закінчиться RAM, а потім дивуватися, чому kernel OOM killer пише ваш постмортем. В RDS ви все одно можете «вбити» інстанс з’єднаннями — просто більш ввічливо.
Приховане обмеження зазвичай не в конфігурованому max_connections, а в тому, що відбувається перед тим, як ви досягаєте його: пам’ять на підключення (sort buffers, join buffers), витрати на планування потоків та contention mutex-ів всередині MySQL. RDS додає ще один нюанс: шторми підключень під час failover, повторні спроби в додатку і погане налаштування пулів можуть складатися і перетворити невелику латентність на повний хаос.
4) Failover — це функція продукту, а не безкоштовний обід
Самостійний failover може бути миттєвим або жахливим, залежно від того, скільки ви інвестували в автоматизацію. RDS failover зазвичай хороший, але він не магічний. Є час виявлення, час промоції, час поширення DNS та поведінка додатка при перепідключенні. У цей проміжок ваш додаток може бити по endpoint-у з ретраями, як малюк по кнопці ліфта.
Приховане обмеження: абстракція «writer endpoint» може приховувати зміни топології, але вона не знімає потреби додатка обробляти транзитні помилки, застарілі з’єднання та ідемпотентність. Якщо ваш додаток не витримує 30–120 секундний brownout, у вас не висока доступність. Маєте надію й молитви.
5) Реплікаційне відставання часто — це історія про I/O під маскою SQL
Люди трактують відставання реплік як проблему налаштувань реплікації. Іноді так. Часто це просто те, що репліка повільніше застосовує записи, бо вона перевантажена на рівні сховища, CPU або через обмеження однопотокового apply. RDS дає метрики і деякі регулятори; він не дає вам права ssh і «просто перевірити одну річ».
Приховане обмеження: клас інстансу репліки може бути менший, клас сховища може відрізнятися, або binlog формат і навантаження роблять паралельну реплікацію неефективною. В інцидентах неправильний фікс зазвичай — «перезапустити реплікацію». Правильний — «знизити тиск записів» або «виправити повільний шлях apply».
6) Parameter groups роблять конфігурацію безпечнішою — і повільнішою для змін
На самостійному сервері ви редагуєте my.cnf, перезавантажуєте і рухаєтесь далі. В RDS зміни параметрів можуть вимагати reboot, бути лише динамічними або взагалі забороненими. Також потрібно відстежувати, який parameter group прив’язаний до якого інстансу — звучить нудно, поки не виявиться, що продакшн і staging «майже» однакові.
Приховане обмеження: операційна латентність. Під час інциденту «ми налаштуємо X» — це не план, якщо ви не знаєте, чи X змінюється без простою і чи можна це зробити швидко й безпечно.
7) Спостережуваність: у вас немає хоста, тому потрібно добре знати движок
У самостійному MySQL ви можете використовувати інструменти ОС: perf, strace, tcpdump, статистику cgroup, гістограми латентності файлової системи і впевненість, що завжди можна копнути глибше. В RDS ви користуєтеся метриками CloudWatch, Enhanced Monitoring (якщо ввімкнено), Performance Insights (якщо ввімкнено) і таблицями та статусними змінними MySQL.
Якщо ці функції не ввімкнено заздалегідь, ваше майбутнє я під час інциденту буде дивитися на розмите фото місця події. Жарт №1: Спостережуваність, яку ви не ввімкнули, — як вогнегасник, що ще в кошику Amazon — дуже доступно, але марно.
8) Бекапи та відновлення: знімки швидкі; відновлення все одно процес
Знімки RDS зручні, а відновлення до точки в часі потужне. Але пастка інциденту — думати «ми просто відновимо швидко». Відновлення займає час, і новому інстансу потрібен прогрів, перевірка параметрів, перевірки security group і переключення додатка. Якщо ви звикли відновлювати локальний бекап на запасний VM, ви можете бути шоковані оркестраційними накладними витратами.
Приховане обмеження: час до операційної готовності, а не час створення інстансу.
9) Не все можна виправити «більшим інстансом»
Збільшення інстансу допомагає для CPU-bound навантажень і дає більше RAM для buffer pool. Воно не автоматично вирішує I/O-обмеження, якщо вузьке місце — пропускна здатність сховища. Не вирішує contention у «гарячих» рядках або metadata locks. Не вирішує патологічні запити. І під час інциденту масштабування може бути повільним або руйнівним.
У самостійних середовищах часто є більше «небезпечных» опцій (скинути кеші, перезапустити сервіси, відмонтувати томи, запускати аварійні скрипти). RDS зменшує кількість гострих інструментів, якими ви можете себе поранити. Він також зменшує кількість гострих інструментів, якими ви можете себе врятувати. Плануйте відповідно.
Плейбук швидкої діагностики: що перевірити першим/другим/третім
Спочатку: вирішіть, чи у вас CPU-bound, I/O-bound чи lock-bound
- Сигнали CPU-bound: високий CPU, велика кількість «active sessions» у Performance Insights, запити повільні навіть коли робочий набір поміщається в пам’ять, багато функцій/сорти, погані індекси.
- Сигнали I/O-bound: низький/помірний CPU, зростання латентності запитів, збільшення InnoDB read/write, підвищена черга на диску (CloudWatch), пропуски в buffer pool, сплески скидання брудних сторінок.
- Сигнали Lock-bound: CPU може бути низьким, але потоки «чекають на» блокування; багато підключень застрягли; кілька транзакцій блокують усе; реплікаційне відставання зростає через блокування apply.
По-друге: підтвердіть вузьке місце двома незалежними поглядами
Не довіряйте одній метриці. Поєднайте внутрішні дані движка (status, processlist, performance_schema) з платформними метриками (CloudWatch/Enhanced Monitoring) або доказами на рівні запитів (top digests, slow log).
По-третє: оберіть найбезпечнішу втручання
- Якщо lock-bound: знайдіть блокувальника, вбийте правильну сесію, зменшіть обсяг транзакцій, виправте поведінку додатка. Уникайте «перезапустити MySQL», якщо ви не обираєте навмисний простій.
- Якщо I/O-bound: припиніть те, що пише/читає занадто багато (батч job, великий звіт), зменшіть конкуренцію, тимчасово підніміть IOPS/клас сховища, а потім виправляйте схему/запити.
- Якщо CPU-bound: знайдіть топ-запити за часом, додайте або виправте індекси, зменшіть дорогі запити, розгляньте масштабування інстансу, а потім виправіть план запиту кардинально.
По-четверте: запобігайте повторному росту навантаження
Інциденти люблять відскоки: ви вбиваєте блокувальний запит, латентність падає, autoscalers або ретраї створюють навантаження знову, і ви повертаєтесь до початку. Обмежте швидкість ретраїв, призупиніть батч-воркерів і контролюйте поведінку connection pool.
Практичні завдання: команди, виводи та рішення (12+)
Ці завдання призначені для реагування на інциденти. Кожне містить команду, що означає її вивід і рішення, яке потрібно прийняти. Виконуйте їх проти самостійно керованого MySQL або RDS MySQL (з бастіону чи хоста додатка). Замініть імена хостів/користувачів за потреби.
Завдання 1: Підтвердіть базову доступність та ідентичність сервера
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT @@hostname, @@version, @@version_comment, @@read_only\G"
Enter password:
*************************** 1. row ***************************
@@hostname: ip-10-11-12-13
@@version: 8.0.36
@@version_comment: MySQL Community Server - GPL
@@read_only: 0
Значення: Ви на writer (read_only=0) і знаєте основну версію. Версія важлива, бо поведінка й інструментація відрізняються.
Рішення: Якщо ви очікували репліку, а потрапили на writer (або навпаки), зупиніться і виправте ціль, перш ніж робити «корисні» зміни.
Завдання 2: Подивіться, чим потоки займаються прямо зараз
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW FULL PROCESSLIST;"
...output...
10231 appuser 10.0.8.21:51244 appdb Query 38 Sending data SELECT ...
10244 appuser 10.0.7.19:49812 appdb Query 38 Waiting for table metadata lock ALTER TABLE orders ...
10261 appuser 10.0.8.23:50111 appdb Sleep 120 NULL
...
Значення: Ви можете помітити очевидні затримки: «Waiting for table metadata lock» — це велика червона стрілка, що вказує на DDL або довгі транзакції.
Рішення: Якщо ви бачите переважну кількість очікувань блокувань, переключіться на діагностику блокувань замість гонитви за CPU чи I/O.
Завдання 3: Визначте блокуючу транзакцію (InnoDB lock waits)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT * FROM sys.innodb_lock_waits\G"
*************************** 1. row ***************************
wait_started: 2025-12-30 01:12:18
wait_age: 00:01:42
waiting_trx_id: 321889203
waiting_pid: 10244
waiting_query: ALTER TABLE orders ADD COLUMN ...
blocking_trx_id: 321889199
blocking_pid: 10198
blocking_query: UPDATE orders SET ...
blocking_lock_mode: X
Значення: Тут видно, хто заблокований і хто блокує, з PID, на які ви можете вплинути.
Рішення: Якщо блокувальник — транзакція додатка, що застрягла в циклі, вбийте блокувальника (не жертву) і пом’якшіть проблему на рівні додатка.
Завдання 4: Вбийте правильну сесію (хірургічно, а не емоційно)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "KILL 10198;"
Query OK, 0 rows affected (0.01 sec)
Значення: Блокуючий потік завершено. Блокування має знятися; заблоковані запити повинні продовжити виконання або швидко зафейлитися.
Рішення: Негайно слідкуйте за шторми повторних підключень і spike-ами ретраїв. Вбивство одного запиту може викликати сто замінників.
Завдання 5: Перевірте статус InnoDB на предмет deadlock-ів, flushing та довжини history
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW ENGINE INNODB STATUS\G"
...output...
History list length 51234
Log sequence number 89433222111
Log flushed up to 89433111822
Pending writes: LRU 0, flush list 128, single page 0
...
Значення: Велика history list length вказує на довго виконувані транзакції, що перешкоджають purge. Pending flush list свідчить про тиск на запис.
Рішення: Якщо history list length вибухає, знайдіть і завершіть довгі транзакції; якщо pending flush високий — зменшіть навантаження на запис і розгляньте тонування сховища/IOPS.
Завдання 6: Підтвердіть здоров’я buffer pool (пам’ять проти I/O тиску)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 9812234432 |
| Innodb_buffer_pool_reads | 22334455 |
+---------------------------------------+------------+
Значення: Innodb_buffer_pool_reads — це фізичні читання. Якщо вони ростуть відносно запитів, ви втрачаєте кеш і б’єтеся по сховищу.
Рішення: Якщо фізичні читання зростають, розгляньте збільшення buffer pool (більший клас інстансу) або зменшення робочого набору (індекси, виправлення запитів).
Завдання 7: Виявіть spill-и тимчасових таблиць (тихий вбивця диска)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 18443321 |
| Created_tmp_tables | 22300911 |
+-------------------------+----------+
Значення: Багато тимчасових таблиць на диску зазвичай означає, що сорти/group by/joins виллються. В RDS це може конфліктувати з обмеженнями тимчасового простору та пропускною здатністю I/O.
Рішення: Знайдіть проблемні запити (Performance Insights / slow log / statement digests) і виправте їх; не «піднімайте tmp_table_size» і не думайте, що це вирішить усе.
Завдання 8: Знайдіть топ-запити за загальним часом через Performance Schema digests
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,1) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,1) AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE customer_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 188233
total_s: 6221.4
avg_ms: 33.1
...
Значення: Ви отримуєте ранжований список SQL-шаблонів, що споживають час. Це зазвичай найшвидший шлях до реальності.
Рішення: Візьміть топ-1–2 digests і проаналізуйте їх плани. Не оптимізуйте 19-й запит просто тому, що він «потворний». Оптимізуйте те, що горить часом.
Завдання 9: EXPLAIN план (і знайдіть відсутні індекси)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "EXPLAIN FORMAT=TRADITIONAL SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC LIMIT 50;"
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| 1 | SIMPLE | orders | ALL | idx_customer | NULL | NULL | NULL | 932112 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
Значення: type=ALL і «Using filesort» вказують на повний скан і сортування. На великих таблицях це інцидент, який чекає, щоб статись.
Рішення: Додайте композитний індекс (наприклад, (customer_id, created_at)) і перевірте, що він відповідає шаблонам запитів. Плануйте зміну безпечно (поведінка online DDL має значення).
Завдання 10: Перевірте відставання реплікації та стан apply (на репліці)
cr0x@server:~$ mysql -h prod-mysql-replica.aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW REPLICA STATUS\G"
...output...
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 187
Retrieved_Gtid_Set: ...
Executed_Gtid_Set: ...
Значення: IO і SQL потоки працюють, але lag — 187 с. Це проблема продуктивності, а не зламана лінка.
Рішення: Перевірте насичення ресурсів репліки і вузькі місця apply; розгляньте масштабування репліки, підвищення пропускної здатності сховища або тимчасове зниження write load.
Завдання 11: Перевірте, чи не досягнуто насичення з’єднань
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections');"
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| Max_used_connections| 1980 |
| Threads_connected | 1750 |
| Threads_running | 220 |
+---------------------+-------+
Значення: Багато з’єднань існує, але тільки 220 виконуються. Це часто вказує на очікування блокувань, I/O або неправильну поведінку пулу з’єднань.
Рішення: Якщо Threads_connected близько до максимуму, захистіть інстанс: обмежте швидкість додатка, увімкніть pooling, і розгляньте зниження лімітів з’єднань на сервіс, щоб найгучніший клієнт не «виграв».
Завдання 12: Перевірте довгі транзакції, що роздувають undo і блокують purge
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT trx_id, trx_started, trx_rows_locked, trx_rows_modified, trx_query FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5\G"
*************************** 1. row ***************************
trx_id: 321889101
trx_started: 2025-12-30 00:02:11
trx_rows_locked: 0
trx_rows_modified: 812331
trx_query: UPDATE orders SET ...
Значення: Транзакція, що працює з 00:02 і модифікує 800k рядків, не є «нормальним фоновим шумом». Це податок на надійність і латентність.
Рішення: Працюйте з власниками додатків над дробленням роботи, частішими commit-ами або переміщенням важких оновлень в неробочий час. Під час інциденту розгляньте можливість вбити її, якщо вона блокує або дестабілізує систему.
Завдання 13: Підтвердіть тиск binlog-ів та політики зберігання
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.012331 | 1073741824|
| mysql-bin.012332 | 1073741824|
| mysql-bin.012333 | 1073741824|
...
Значення: Багато великих binlog-ів вказують на інтенсивні записи. В RDS налаштування утримання та зростання сховища можуть стати несподіваним рахунком і причиною інциденту.
Рішення: Якщо binlog-и розростаються, перевірте здоров’я реплік, налаштування retention і чи не заважає stuck репліка очищенню логів.
Завдання 14: Перевірте блоут таблиць/індексів та дрейф кардинальності (швидка перевірка)
cr0x@server:~$ mysql -h prod-mysql.cluster-aaaa.us-east-1.rds.amazonaws.com -u ops -p -e "SELECT table_name, engine, table_rows, data_length, index_length FROM information_schema.tables WHERE table_schema='appdb' ORDER BY (data_length+index_length) DESC LIMIT 5;"
+------------+--------+-----------+------------+-------------+
| table_name | engine | table_rows| data_length| index_length|
+------------+--------+-----------+------------+-------------+
| orders | InnoDB | 93211234 | 90194313216| 32112201728 |
...
Значення: Великі таблиці визначають вашу долю. Якщо індекси найбільшої таблиці величезні або не відповідають запитам, ви заплатите в I/O і cache misses.
Рішення: Пріоритезуйте індексацію та політики життєвого циклу даних (партиціонування, архівація) для топ-таблиць, а не для тих, про які голосно скаржаться.
Три міні-історії з інцидентної підлоги
Міні-історія 1: Інцидент через хибне припущення
Середня SaaS-компанія перейшла з самостійно керованого MySQL на налаштованому NVMe RAID-боксі до RDS MySQL. Міграція пройшла чисто. Латентність додатка покращилась. Усі оголосили перемогу і повернулись до випуску фіч.
Через три місяці маркетингова кампанія спрацювала надто добре. Записний трафік подвоївся на кілька годин. Нічого «не зламалося» одразу; натомість p95 піднявся, а потім p99 вибухнув. CPU primary-інстансу тримався на рівні 35%. Інженери дивилися на нього, ніби це брехун.
Хибне припущення було простим: «Якщо CPU в порядку, то база в порядку». На їхніх старих серверах це часто було правдою, бо сховище мало запас і його моніторили безпосередньо. В RDS команда не ввімкнула Performance Insights і ледь стежила за метриками сховища. gp2-том вигорав burst credits, I/O латентність зросла, і InnoDB почав інтенсивніше скидати брудні сторінки. Система була I/O-bound, хоча CPU виглядав спокійним.
Вони спробували масштабувати інстанс. Це трохи допомогло, але не виправило корінну проблему: конфігурація сховища і write amplification їхнього навантаження. Виправлення — перейти на опцію сховища з передбачуваними IOPS, оптимізувати write-heavy патерни (менші транзакції, менше вторинних індексів на «гарячих» таблицях) і додати дашборди, що роблять «I/O борг» видимим до того, як він перетвориться на інцидент.
Після цього вони прийняли правило: кожна міграція включає «репетицію нового вузького місця». Якщо ви не можете пояснити, як вона падає, міграція не завершена.
Міні-історія 2: Оптимізація, що відсічилась
Компанія, пов’язана з фінансами, мала нічне завдання, що перераховувало агрегати. Воно було повільним, тому інженер «оптимізував» його, збільшивши конкуренцію: більше воркерів, більші батчі і більший пул з’єднань. У стенді виглядало чудово. В продакшні це перетворилося на генератор brownout-ів.
Завдання в основному робило оновлення у «гарячій» таблиці з кількома вторинними індексами. Більше воркерів означало більше одночасного обслуговування індексів і більше конфліктів на рівні блокування рядків. Redo log і поведінка скидання стали вузьким місцем, а тимчасові таблиці вилитись на диск, бо завдання також робило group-by для проміжних кроків.
На самостійних системах раніше дивились iostat і налаштовували хост. В RDS дивились на CPU і припускали, що це ліміт. Це було не так. «Оптимізація» збільшила write amplification і contention, виштовхнувши підсистему сховища в стійку високу латентність. Реплікаційне відставання зросло, і read-трафік почав бити по writer-у, бо репліки відстали занадто далеко.
Скупа, але правильна дія — знизити конкуренцію, чітко розбити оновлення по діапазонах primary key і додати індекс, що перетворив одну дорогу операцію join у дешевий lookup. Завдання виконувалось трохи повільніше, ніж «оптимізована» версія в ізоляції, але припинило ламати решту платформи.
Жарт №2: У базах даних «більше паралельності» іноді означає «більше людей, які намагаються пройти одночасно через одну й ту ж двері».
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Платформа e-commerce запускала RDS MySQL з Multi-AZ і реплікою для аналітики. Їхні інженери не були відомі захоплюючою архітектурою. Але вони були відомі runbook-ами, рутинними game days і майже дратівливою увагою до parameter groups.
Одного дня міграція схеми ввела регресію плану запиту. Латентність writer-а підскочила. Потім відставання репліки зросло. Потім додаток почав таймаутитись і робити ретраї. Класична спіраль.
On-call не почав з «тюнінгу MySQL». Вони виконали відпрацьований плейбук: підтвердити, чи це lock/I/O/CPU, знайти топ-digests, перевірити чи ретраї не підсилюють проблему і потім зробити найменш ризикове відключення навантаження. Вони тимчасово вимкнули аналітичного споживача, знизили конкуренцію воркерів і застосували feature-flag на рівні запиту, щоб зупинити найгіршого винуватця. Реплікаційне відставання стабілізувалося.
Ось де нудна практика окупилася: вони тестували відновлення зі знімка і промоцію реплік, і мали документований процес відключення читань від writer-а. Вони не дійшли до failover, але могли б зробити це спокійно, якби довелося. Інцидент тривав менше години, і постмортем був здебільшого про дисципліну огляду запитів, а не про героїчні порятунки.
Поширені помилки: симптом → корінь проблеми → виправлення
1) Симптом: CPU низький, латентність висока
Корінь: Насичення I/O (пропускна здатність сховища/IOPS) або очікування блокувань. Часто в RDS, коли gp томи втрачають burst або коли тимчасові таблиці вилитись.
Виправлення: Перевірте пропуски buffer pool і тимчасові таблиці на диску; перевірте CloudWatch на латентність/чергу диска; зменшіть тиск записів/читань; перейдіть на передбачувані IOPS; виправте запити й індекси.
2) Симптом: «Занадто багато з’єднань» після failover
Корінь: Шторм з’єднань через поведінку ретраїв додатка і відсутність пулінгу; старі з’єднання не перерозподілені; max_connections встановлено без розуміння пам’яті на потік.
Виправлення: Впровадьте pooling (ProxySQL/RDS Proxy/пул в додатку), обмежте з’єднання за сервісами, додайте джиттер до ретраїв і встановіть розумні таймаути. Краще менше, але здоровіших з’єднань, ніж тисячі простоїв.
3) Симптом: Репліки відстають постійно і не надолужують
Корінь: Пропускна здатність apply репліки нижча за write workload, часто через сховище або однопотокове apply, або через слабший клас інстансу.
Виправлення: Масштабуйте ресурси репліки і пропускну здатність сховища, зменшіть write volume (обмеження батчів), і перевірте довгі транзакції або блокування на репліці.
4) Симптом: ALTER TABLE «зависає» і все інше сповільнюється
Корінь: Metadata locks і довгі транзакції; або online DDL створює великий тимчасовий/redo тиск; іноді DDL чекає завершення транзакції.
Виправлення: Визначте блокувальників за допомогою sys.innodb_lock_waits і processlist; плануйте DDL у непіковий час; використовуйте безпечні методи зміни схеми; скорочуйте транзакції.
5) Симптом: Тривоги по диску, але розмір даних суттєво не виріс
Корінь: Binlog-и, ріст undo від довгих транзакцій, spill-и тимчасових таблиць або наслідки знімків/політик зберігання.
Виправлення: Перевірте обсяг binlog-ів і політик retention; вбийте або рефакторіть довгі транзакції; виправте запити, що виллються; підтвердіть політики зберігання резервних копій.
6) Симптом: Після масштабування інстансу продуктивність майже не покращується
Корінь: Вузьке місце — пропускна здатність сховища/IOPS або contention, а не CPU/RAM.
Виправлення: Перейдіть на сховище з вищою пропускною здатністю, зменшіть write amplification і виправте точки contention (індекси, патерни транзакцій додатка).
7) Симптом: «Вільна пам’ять» виглядає великою, але система повільна
Корінь: Пам’ять MySQL — не вся історія; InnoDB може бути недостатньо виділений або навантаження може бути I/O-bound. Метрики пам’яті RDS можуть вводити в оману, якщо ви не перевіряєте buffer pool і робочий набір.
Виправлення: Перевірте індикатори buffer pool hit ratio, проаналізуйте топ-запити і індекси, налаштуйте клас інстансу і innodb_buffer_pool_size відповідно (де це дозволено).
8) Симптом: Репліка «здоровa», але повертає старі дані
Корінь: Відставання реплікації або затримка apply; додаток припускає read-after-write консистентність з реплік.
Виправлення: Маршрутуйте read-after-write до writer (стикість сесії), забезпечте консистентність для критичних шляхів, моніторте lag і встановіть порогові значення для використання реплік.
Чеклісти / покроковий план
Перед міграцією (або перед наступним інцидентом, якщо ви вже мігрували)
- Ввімкніть правильну телеметрію зараз: Performance Insights, slow query log (із розумною вибіркою) і Enhanced Monitoring там, де потрібно.
- Визначте SLO і дії «зупинити кровотечу»: які батчі можна призупинити, які ендпоінти можна погіршити, і хто має право вмикати ті перемикачі.
- Явно мапуйте обмеження: max connections, тип/IOPS сховища, поведінка тимчасового простору, очікування щодо failover і семантика змін parameter group (динамічні чи з перезавантаженням).
- Проведіть репетицію навантаження: симулюйте два найвищі піки трафіку, які ви бачили історично, і підтвердьте, як система деградує.
- Напишіть runbook, ніби читатимете його напівсонні: короткі кроки, точні запити, точки прийняття рішень і критерії відкату.
Під час інциденту (послідовність триажу, що працює в реальному світі)
- Зупиніть підсилення: обмежте швидкість ретраїв, призупиніть батч-воркерів і обмежте пул з’єднань. Якщо нічого іншого не зробите — зробіть це.
- Класифікуйте вузьке місце: CPU vs I/O vs locks за допомогою processlist + ключових статусних змінних + платформних метрик.
- Знайдіть топ SQL-шаблони: digests / PI top waits / slow log. Оберіть топ-винуватця, а не найгалосливішу команду.
- Застосуйте найменш ризикове пом’якшення: відключіть фічу на рівні запиту, зменшіть конкуренцію або делікатно перенаправте читання/записи.
- Тільки потім тюнінгуйте або масштабуйте: масштабування інстансу і зміни параметрів — корисні інструменти, але не перша допомога.
- Фіксуйте часові позначки: кожна дія, кожне зміщення метрик, кожна зміна. Ваш постмортем залежить від цього.
Після інциденту (запобігти повторенню, а не лише уникнути сорому)
- Перетворіть корінь проблеми в огорожу: lint для запитів, огляд міграцій, попередження ємності на реальний вузький момент (часто I/O) і автоматичне навантаження-скидання.
- Виправте життєвий цикл даних: архівація, партиціонування і гігієна індексів на топ-таблицях.
- Практикуйте відновлення і failover: вимірюйте час від початку до кінця, включно з переключенням додатку і верифікацією.
- Ускладніть шиппінг регресій продуктивності: зберігайте плани запитів для критичних запитів і порівнюйте їх між релізами.
FAQ
1) Чи RDS MySQL — це «справжній MySQL»?
На рівні SQL — так. Операційно це MySQL у керованому середовищі з обмеженнями: немає root-доступу, керована поведінка сховища і продуктово визначені механіки failover та бекапів.
2) Яка одна найбільша різниця «прихованого обмеження»?
Передбачуваність продуктивності сховища. На самостійному хості ви зазвичай можете побачити і настроїти весь стек; в RDS потрібно обрати правильний клас сховища і моніторити I/O за допомогою інструментів RDS.
3) Чому інциденти на RDS здаються тяжчими для діагностики?
Тому що ви не можете перейти на хост і запускати інструменти ОС. Потрібно покладатися на інструментацію MySQL і телеметрію RDS. Якщо ви їх не ввімкнули, ви дебагуєте з половиною світла вимкненим.
4) Чи просто підняти max_connections, щоб уникнути помилок підключення?
Ні. Так ви перетворите невеликий сплеск трафіку на катастрофу пам’яті і contention. Використовуйте пулінг, обмежуйте з’єднання за сервісом і трактуйте помилки з’єднань як сигнал зворотного тиску.
5) Чому масштабування інстансу іноді не вирішує продуктивність?
Бо вузьке місце може бути I/O-bound або lock-bound. Більше CPU не виправить дискову латентність, а більше RAM не вирішить metadata lock waits. Спочатку діагностуйте, потім масштабуйте під реальний вузький момент.
6) Чи безпечні read репліки для масштабування читань під час інцидентів?
Тільки якщо ви моніторите lag і додаток спроєктований відповідно. Репліки корисні, поки вони синхронні; як тільки вони відстають, це вже проблема коректності, а не ємності.
7) Яке найшвидше безпечне втручання при спайку латентності?
Зупиніть підсилення: призупиніть батчі, зменшіть конкуренцію воркерів і обмежте ретраї. Потім визначте, чи це lock, I/O чи CPU, перед глибшими змінами.
8) Чи зміни parameter group завжди вимагають простою?
Ні, але багато з них вимагають reboot. Під час інциденту «ми змінимо параметр» корисно лише якщо ви вже знаєте, чи він динамічний і які має побічні ефекти.
9) Як уникнути інцидентів з тимчасовими таблицями в RDS?
Виправляйте запитові шаблони, що виллються (індекси, зменшення витрат sort/group by), обмежуйте конкуренцію важких звітів і слідкуйте за Created_tmp_disk_tables як раннім попередженням, а не як статистичним курйозом.
10) Чи Multi-AZ достатньо для високої доступності?
Це необхідно, але недостатньо. Ваш додаток має обробляти транзитні помилки, правильно перепідключатися, уникати штормів ретраїв і витримувати короткі brownout-и. HA — це властивість системи, а не галочка в чеклісті.
Висновок: що змінити в понеділок
MySQL і RDS MySQL виконують один і той же SQL, але вони ламаються по-різному. У самостійних середовищах відмови часто пов’язані з хостом, якого можна «поторкати» поки він не зізнається. У RDS відмови часто пов’язані з обмеженням, на яке ви погодилися місяцями раніше і забули моніторити.
Наступні кроки, що реально скоротять час інциденту:
- Увімкніть і перевірте видимість на рівні движка (Performance Insights і використання performance_schema) і потренуйтеся користуватися ними під навантаженням.
- Побудуйте дашборди, що швидко відповідають на одне питання: чи вузьке місце — CPU, I/O чи блокування?
- Напишіть runbook з навантаженням-скиданням: які задачі призупиняються, які ендпоінти деградують і як ви зупиняєте шторм ретраїв.
- Перегляньте вибір сховища і топологію реплікації на основі реального навантаження, а не на основі дефолтів, які ви успадкували.
- Перетворіть топ-5 дорогих SQL-шаблонів на «власні» елементи з індексами, перевірками стабільності планів і безпечними шляхами розгортання.
Якщо ви зробите ці п’ять речей, наступний інцидент все одно буде стресовим. Але він не буде таємничим. А таємничі інциденти — ті, що старять вас.