Ваш под MySQL «виглядає добре», поки вузол не перезавантажується, Kubernetes не починає робити те, для чого його сконстрували, і раптом база даних опиняється в петлі перезапусків з томом, який відмовляється монтуватися. Тим часом команда застосунку каже, що це «проблема Kubernetes», а платформа називає це «проблемою бази даних». Вітаю: тепер ви — дорослий у кімнаті.
Це практичний, трохи суб’єктивний посібник з експлуатації MySQL і MariaDB у Kubernetes так, щоб перевірки не ставали відмовою в обслуговуванні, перезапуски — корупцією, а «безстанний підхід» — втратою даних. Ми поговоримо, що ламається, чому це відбувається, і які команди ви реально виконуватимете о 2:00 ночі.
Що Kubernetes фактично робить з вашою базою даних (і чому це боляче)
Kubernetes послідовний в одному: йому байдуже. Він перезапустить ваш процес, якщо перевірка не пройшла. Він перемістить под, якщо вузол дренується. Він вб’є контейнери при дефіциті ресурсів. І він робитиме це, поки ваша база даних напівзаписує, напіввідновлюється або під час транзакції, бо у нього немає поняття «це святе — з журналом передзапису».
Бази даних — це машини станів із дорогими інваріантами. Інваріанти InnoDB пов’язані з redo/undo журналами, doublewrite (залежно від налаштувань) і надійним fsync. MariaDB і MySQL мають багато спільного тут, але тиск Kubernetes виводить на поверхню різні крайні випадки — особливо під час запуску, поведінки перевірок і стеків реплікації, які ви можете використовувати (async реплікація, semi-sync або Galera).
Три поведінки Kubernetes, що найбільше важливі для сервісів зі станом:
- Перевірки — це шлюзи трафіку та вимикачі смерті. Readiness вирішує, чи отримаєте ви трафік. Liveness вирішує, чи будете жити. Startup існує, бо інакше liveness буває булі.
- Перезапуски — «нормальні». Їх не можна вважати винятковими. Ваша база має вміти безпечно та швидко перезапускатися багаторазово.
- Сховище прикріплюється і від’єднується людьми (і контролерами). Затримки при монтуванні томів, застарілі приєднання і повільний fsync можуть домінувати над усім іншим.
Якщо ви запам’ятаєте лише одну операційну істину: для станних систем Kubernetes не «самовиліковний», він «самоповторює спроби». Ваше завдання — зробити ці спроби безпечними.
MySQL проти MariaDB: що важливо у Kubernetes
MySQL та MariaDB настільки схожі, що команди заспокоюються думкою про операційну еквівалентність. Ця передумова — насінина кількох дуже дорогих інцидентів.
Поведение ядра: переважно схоже, але зверніть увагу на дефолти й крайні випадки
У типовому розгортанні в Kubernetes ви запускатимете InnoDB в обох. Відновлення після аварії є в обох. Обидві можуть відновитись після раптового завершення. Але диявол у деталях:
- Варіативність тривалості старту. Після неналежного завершення InnoDB recovery може займати від секунд до «ваша liveness-перевірка тепер — зброя». Відновлення залежить від розміру redo-журналу, віку контрольної точки, пропускної здатності IO і поведінки fsync у вашому StorageClass.
- Різниця в інструментах клієнта. Деякі образи містять
mysqladmin, деякі — обгорткиmariadb-admin, а деякі — мінімальні клієнти. Перевірки та init-скрипти, які жорстко вбудовують один інструмент, зазвичай ламаються в найгірший момент. - Деталі GTID і реплікації. GTID MariaDB не є GTID MySQL; вони не сумісні «вставляти напряму». Якщо ви мігруєте або змішуєте інструменти, історія «це просто GTID» перетворюється на довгу ніч.
Стек реплікації: async vs Galera змінюють поведінку перевірок
«MySQL на Kubernetes» часто означає один primary плюс репліки (async реплікація) або кластер, яким керує оператор. «MariaDB на Kubernetes» часто означає те саме або Galera (wsrep), бо легко продати «multi-primary» менеджменту.
Перевірки для async реплікації можуть бути простими: чи живий mysqld і приймає з’єднання, і (для readiness) чи реплікація досить догнала? Перевірки для Galera мають враховувати стан кластера: вузол може приймати TCP-з’єднання й одночасно бути небезпечним для записів (або навіть читань) залежно від стану донору/десинкації.
Оператори та образи: готовність у Kubernetes — це продуктове рішення
Ваш досвід експлуатації часто визначається вибором оператора/образу більше, ніж брендом бази даних. Деякі оператори реалізують адекватні startupProbe, гачки для коректного завершення та ворота відновлення. Інші поставляють оптимістичні дефолти й дозволяють вам навчитися вогнем.
Моя думка: якщо ви запускаєте станні сервіси без опініонованого оператора (або ретельно підтримуваного внутрішнього чарта), ви фактично обрали «пет-базу даних на оркестрації для тіл». Це не стратегія; це настрій.
Цікаві факти та історичний контекст (те, що люди забувають)
- MariaDB створена у 2009 році як спільнотний форк після придбання Sun Microsystems компанією Oracle (а отже — MySQL).
- За замовчуванням у MySQL змінювалася кодувальна сторінка з часом (зокрема на
utf8mb4в сучасних версіях), і це впливає на сумісність схем та довжину індексів при апгрейдах. - GTID у MariaDB реалізований інакше ніж у MySQL; інструменти, що припускають MySQL GTID, можуть неправильно діагностувати стан реплікації в MariaDB.
- Galera стала «фішкою» MariaDB для багатьох підприємств, але це інша модель відмов: членство та кворум — операційні вимоги, а не приємні дрібниці.
- Kubernetes додав startupProbe відносно пізно (у порівнянні з liveness/readiness), частково тому, що занадто багато реальних навантажень мали повільний, але коректний старт, який гинув від liveness.
- Вартість відновлення InnoDB не лінійна з розміром даних; вона пов’язана з обсягом redo і поведінкою контрольних точок, тому «мала база» може все одно повільно стартувати після IO-застою.
- Поведінка fsync змінилася між поколіннями хмарного сховища; те, що було «нормально» на локальних SSD, може спричинити бурю перезапусків на мережевих томах з варіативністю затримок.
- MySQL 8 ввів транзакційний словник даних, що поліпшило послідовність, але також змінило деякі поведінки апгрейдів/відкатів і відновлення порівняно зі старими MySQL та лінійкою MariaDB.
Перевірки readiness, liveness та startup, які вам не шкодитимуть
Перевірки — це місце, куди Kubernetes торкається вашої бази кожні кілька секунд. Якщо їх налаштувати правильно, вони захистять под від трафіку під час хвороби. Неправильно — вони самі створять хворобу.
Правила на замітку (суб’єктивні, бо вони потрібні)
- Ніколи не використовуйте liveness для перевірки «логічного здоров’я» бази. Liveness має відповідати: «Чи процес завис без шансу відновитися?» Якщо ви робите liveness залежним від відставання реплікації або складного SQL-запиту, ви вбиватимете повністю відновлювані поди.
- Readiness може бути суворим. Readiness — місце, де пропускати трафік на основі «читає/пише зараз» і (за потреби) «достатньо догнав репліку». Це правильно.
- Використовуйте startupProbe, щоб купити час для відновлення. Якщо не зробите цього, liveness вб’є ваш InnoDB recovery. Він перезапуститься, знову зайде в recovery і буде вбитий знову. Ця петля — класична.
- Віддавайте перевагу exec-перевіркам через локальний сокет, коли можливо. TCP-перевірки можуть пройти, коли SQL заблокований. SQL-перевірки можуть падати, коли DNS повільний. Локальні перевірки через сокет мінімізують рухомі частини.
Як виглядає хороша перевірка readiness
Readiness має бути дешевою й детермінованою. Поширений шаблон: підключитися локально і виконати SELECT 1. Якщо ви репліка, опційно перевірте відставання реплікації. Якщо Galera — перевірте wsrep-стан.
Уникайте «дорогої істини» — запитів, які сканують великі таблиці або запитують метадані, що блокують. Вам потрібно знати: «чи може прийняти тривіальний запит і швидко відповісти». Це добре корелює з досвідом користувача і не зруйнує под під навантаженням перевірок.
Як виглядає хороша перевірка liveness
Liveness має бути консервативною. Для MySQL/MariaDB прийнятна liveness-перевірка — локальний ping через адмін-утиліту з коротким таймаутом. Якщо вона не відповідає N послідовних перевірок, щось зашкоджено.
Startup probes: купіть час для відновлення
Після неналежного завершення InnoDB може по-праву займати хвилини на відновлення. Ваші перевірки мають надати цей час. Це не «поступливість» — це коректність.
Жарт №1: Liveness-перевірка, що вбиває mysqld під час відновлення, — як перевіряти пульс пацієнта, вимикаючи вентилятор.
Таймаути перевірок: мовчазний вбивця
Багато невдач перевірок — це не «база впала», а «таймаут перевірки занадто короткий для випадкової затримки сховища». Якщо ваш PVC на мережевому сховищі, таймаут у 1 секунду — генератор інцидентів.
Перезапуски, семантика завершення та відновлення InnoDB після аварії
Завершення в Kubernetes — це сигнал плюс дедлайн. Kubernetes надсилає SIGTERM, чекає terminationGracePeriodSeconds, потім SIGKILL. Ваша база має достатньо часу, щоб скинути буфери, оновити стан і коректно завершитись.
Коректне завершення дає швидший старт
Чисте завершення: менше redo-журналів для застосування, швидший старт, менше лякаючих повідомлень у логах, менше часу в «не готово». Неналежне завершення: час відновлення залежить від IO, яким ви не керуєте під час події вузла.
Петлі перезапусків — зазвичай баги політики перевірок
Якщо под перезапускається повторно і логи показують, що recovery InnoDB починається кожного разу, ваші перевірки надто агресивні або ви не використовуєте startupProbe. Kubernetes не «лікує» вашу БД; він перериває лікування.
Ідея надійності (перефразовано)
«Надія — не стратегія.» — перефразована ідея, що часто приписується інженерам у колах надійності. Сприйміть як керівництво: проєктуйте з урахуванням відмов, не бажайте їх.
Сховище та безпека даних: PVC, файлові системи та вартість fsync
Якщо ви хочете, щоб база була надійною, треба платити за надійні записи. Kubernetes цього не змінює. Він лише заплутує білінг, бо повільна частина тепер у StorageClass.
Семантика PVC, що має значення
- Режим доступу (RWO vs RWX): Більшість томів для баз даних повинні бути RWO. RWX мережеві файлові системи можуть працювати, але продуктивність і семантика блокувань різняться; не кидайте MySQL на NFS і очікуйте щастя.
- Політика видалення: «Delete» має місце, але ви не повинні виявляти це у проді після видалення StatefulSet.
- Затримки при приєднанні/монтуванні: Под може бути запланований швидко, але довго чекати на приєднання тому. Перевірки і таймаути мають це враховувати.
fsync та інші моменти
Налаштування надійності (як innodb_flush_log_at_trx_commit і sync_binlog) взаємодіють із нижнім шаром сховища. На низьколатентних локальних SSD fsync за коміт може бути прийнятним. На зайнятому мережевому томі з піками затримки це може стати крахом продуктивності.
Зменшувати надійність заради «виправлення продуктивності» — це бізнес-рішення, навіть якщо ви прикидаєтесь, що це технічне. Якщо ви послаблюєте fsync, ви обираєте, скільки даних готові втратити при падінні вузла.
Файлова система і опції монтування
ext4 і XFS — зазвичай вибір. Більше значення має послідовність і спостережуваність: вам потрібно знати, на чому саме ви працюєте. Якщо ви використовуєте overlay-файлові системи дивним чином — припиніть. Бази даних хочуть нудне блочне сховище.
Жарт №2: Є два типи сховища: те, яке ви бенчмаркуєте, і те, що бенчмаркує вас у проді.
Практичні завдання: команди, очікуваний результат, і рішення, яке ви приймаєте
Це реальні завдання, які можна виконати з адміністраторської машини з доступом kubectl, плюс кілька перевірок всередині контейнера. Кожне містить, що означає вивід і яке рішення слід прийняти далі.
Завдання 1: Дізнатися, чи це проблема перевірок чи крах процесу
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-0 0/1 CrashLoopBackOff 7 18m 10.42.3.17 node-7
Значення: CrashLoopBackOff з кількома перезапусками натякає, що процес завершується або liveness його вбиває. Інформації ще недостатньо.
Рішення: Перегляньте події та попередні логи далі; не чіпайте випадкові налаштування MySQL прямо зараз.
Завдання 2: Прочитати події пода, щоб зрозуміти, чи вас вбивають liveness/readiness
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | sed -n '/Events:/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 2m (x12 over 16m) kubelet Liveness probe failed: command timed out: context deadline exceeded
Normal Killing 2m (x12 over 16m) kubelet Container mysql failed liveness probe, will be restarted
Значення: Контейнер вбивається через тайм-аут liveness, а не обов’язково падає сам по собі.
Рішення: Додайте/налаштуйте startupProbe і збільшіть timeoutSeconds перевірок. Також перевірте латентність сховища перед тим, як звинувачувати MySQL.
Завдання 3: Порівняти поточні й попередні логи контейнера
cr0x@server:~$ kubectl -n prod logs mysql-0 -c mysql --previous --tail=60
2025-12-31T01:12:17.812345Z 0 [Note] InnoDB: Starting crash recovery from checkpoint LSN=187654321
2025-12-31T01:12:46.991201Z 0 [Note] InnoDB: 128 out of 1024 pages recovered
2025-12-31T01:12:48.000901Z 0 [Note] mysqld: ready for connections.
2025-12-31T01:12:49.103422Z 0 [Warning] Aborted connection 12 to db: 'unconnected' user: 'healthcheck' host: 'localhost' (Got timeout reading communication packets)
Значення: Він фактично досягнув «ready for connections», а потім його вбили/виникли помилки через таймаути.
Рішення: Ймовірно, запит перевірки занадто повільний або таймаут занадто короткий під IO-навантаженням. Виправте перевірки в першу чергу; потім виміряйте IO.
Завдання 4: Підтвердити визначення перевірок (ви будете шоковані, як часто вони неправильні)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.containers[0].livenessProbe.exec.command}{"\n"}{.spec.containers[0].readinessProbe.exec.command}{"\n"}{.spec.containers[0].startupProbe.exec.command}{"\n"}'
[mysqladmin ping -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD)]
[mysql -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD) -e SELECT 1]
[]
Значення: StartupProbe не налаштовано. Liveness використовує розгортання пароля так, що це може не працювати залежно від обробки shell.
Рішення: Додайте startupProbe і використовуйте обгортковий скрипт, який безпечно обробляє секрети. Також уникайте розміщення паролів прямо в аргументах команд.
Завдання 5: Перевірити період грації завершення (чисте завершення важливе)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.terminationGracePeriodSeconds}{"\n"}'
30
Значення: 30 секунд часто замало для зайнятого інстансу InnoDB, щоб коректно завершитись.
Рішення: Збільште його (зазвичай 120–300с залежно від навантаження) і переконайтеся, що ваш контейнер правильно обробляє SIGTERM.
Завдання 6: Перевірити PVC і чи том застряг підключенням
cr0x@server:~$ kubectl -n prod get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
data-mysql-0 Bound pvc-3d5b2c9a-6c62-4a9c-a0c2-1f2a2e6b9b8a 200Gi RWO fast-ssd
Значення: PVC прив’язано; приєднання може бути повільним, але принаймні provisioning не падає.
Рішення: Якщо под у статусі Pending — перевірте події приєднання; якщо запущений але повільний — перейдіть до діагностики IO.
Завдання 7: Перевірити тиск на вузлі, що може викликати евакуації або троттлінг
cr0x@server:~$ kubectl describe node node-7 | sed -n '/Conditions:/,/Addresses:/p'
Conditions:
Type Status LastHeartbeatTime Reason Message
---- ------ ----------------- ------ -------
MemoryPressure False 2025-12-31T01:15:10Z KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True 2025-12-31T01:15:10Z KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False 2025-12-31T01:15:10Z KubeletHasSufficientPID kubelet has sufficient PID available
Значення: DiskPressure True може викликати евакуації і корелювати з IO-застоями.
Рішення: Виправте тиск диска на вузлі (очистка образів, логів, збільшення кореневого диска). Не налаштовуйте MySQL, щоб «вирішити» голодування вузла.
Завдання 8: Подивитися коди виходу при перезапусках контейнера (крах процесу vs вбивство)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{" "}{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
137 Error
Значення: Код виходу 137 зазвичай означає SIGKILL (часто liveness kill або OOM kill).
Рішення: Перевірте події на OOMKilled; інакше розглядайте це як проблему перевірок/termination-grace.
Завдання 9: Перевірити, чи не OOM-вбивається mysqld під час відновлення
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | grep -E 'OOMKilled|Reason:|Last State' -n
118: Last State: Terminated
119: Reason: OOMKilled
Значення: Ліміт пам’яті занадто малий для навантаження або фази відновлення; InnoDB може робити піки алокацій (buffer pool, кеші, сортувальні буфери залежно від конфігурації).
Рішення: Збільшіть обмеження пам’яті, зменшіть InnoDB buffer pool і переконайтеся, що не використовуєте агресивні буфери на з’єднання при великій max_connections.
Завдання 10: Швидко перевірити статус MySQL/MariaDB (всередині пода)
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysqladmin -uroot -p"$MYSQL_ROOT_PASSWORD" ping && mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE '\''Uptime'\'';"'
mysqld is alive
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Uptime | 43 |
+---------------+-------+
Значення: Сервер запущений і відповідає. Якщо readiness все ще падає, перевірка readiness неправильна або занадто сувора.
Рішення: Узгодьте логіку readiness з тим, що ви щойно довели: базова доступність плюс мінімальні критерії коректності.
Завдання 11: Перевірити індикатори відновлення InnoDB і налаштування надійності
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_flush_log_at_trx_commit\",\"sync_binlog\",\"innodb_doublewrite\");"'
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_doublewrite | ON |
| innodb_flush_log_at_trx_commit | 1 |
| sync_binlog | 1 |
+------------------------------+-------+
Значення: Це режим максимальної надійності. Добре для безпеки даних, потенційно жорстко на повільному сховищі.
Рішення: Якщо затримки неприйнятні — спочатку виправте сховище. Послаблюйте ці налаштування лише якщо бізнес явно погоджується на ризик втрати даних.
Завдання 12: Виміряти відставання репліки (для випадку async)
cr0x@server:~$ kubectl -n prod exec -it mysql-1 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"'
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187
Значення: Репліка відстає. Вона може бути живою, але не готовою обслуговувати читання, якщо ваш застосунок очікує свіжі дані.
Рішення: Використовуйте readiness, щоб обмежувати трафік за порогом відставання, придатним для вашого застосунку. Не використовуйте liveness для цього.
Завдання 13: Виміряти wsrep-стан (для MariaDB Galera)
cr0x@server:~$ kubectl -n prod exec -it mariadb-0 -c mariadb -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW STATUS LIKE '\''wsrep_%'\'';" | egrep "wsrep_local_state_comment|wsrep_cluster_status|wsrep_ready"'
wsrep_cluster_status Primary
wsrep_local_state_comment Synced
wsrep_ready ON
Значення: Вузол у Primary компоненті, синхронізований і готовий. Це стан, який ви хочете бачити перед оголошенням readiness.
Рішення: Якщо wsrep_ready OFF або стан Donor/Joining — тримайте под NotReady, щоб уникнути неконсистентних читань/записів.
Завдання 14: Переконатися, що файлову систему відповідає вашим очікуванням
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'df -T /var/lib/mysql | tail -n +2'
/dev/nvme1n1 ext4 205113212 83412228 111202456 43% /var/lib/mysql
Значення: ext4 на блочному пристрої — типовий варіант. Якщо ви бачите nfs або щось несподіване, змініть очікування і перевірки.
Рішення: Якщо це мережеве файлове сховище — будьте значно більш консервативні з таймаутами і розгляньте зміну StorageClass для продового OLTP.
Завдання 15: Слідкувати за симптомами повільного fsync у статусі MySQL
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE \"Innodb_os_log_fsyncs\"; SHOW GLOBAL STATUS LIKE \"Innodb_log_waits\";"'
+---------------------+--------+
| Variable_name | Value |
+---------------------+--------+
| Innodb_os_log_fsyncs| 983211 |
+---------------------+--------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 1249 |
+------------------+-------+
Значення: Innodb_log_waits вказує на контеншн при скиданні журналу; часто корелює з латентністю IO або занадто малими лог-буферами/файлами журналу.
Рішення: Досліджуйте латентність сховища і розгляньте зміни конфігурації журналу. Якщо ви працюєте на «шумних» мережевих томах — спочатку виправте це.
Швидкий план діагностики: знайдіть вузьке місце, перш ніж звинувачувати не в ту сторону
Коли под бази даних фліпає або повільний у Kubernetes, ви можете витратити години на суперечки «DB чи платформа». Не треба. Дотримуйтесь послідовності, що швидко скаже вам, де реальність.
По-перше: визначте, чи Kubernetes вбиває його, чи він крашиться
- Події: невдачі liveness, OOMKilled, евакуації, проблеми з монтуванням томів.
- Код виходу: 137 (kill) проти стека аварії mysqld.
- Попередні логи: чи досягав він «ready for connections»?
По-друге: визначте, чи сховище — вузьке місце
- Умови вузла: DiskPressure, підказки про насичення IO.
- Час старту/відновлення: якщо провал відбувається тільки під час вікон відновлення, підозрюйте повільне IO і надто агресивні перевірки.
- Індикатори бази: log waits, stalled checkpoints, відставання репліки при кореляції з навантаженням записів.
По-третє: визначте, чи ваша логіка перевірок перенавчена
- Чи readiness залежить від відставання репліки? Добре. Але чи поріг сенсовний для вашого застосунку?
- Чи liveness залежить від відставання репліки? Погано. Змініть це.
- Чи перевірка викликає правильного клієнта? Образи MariaDB і MySQL не завжди симетричні.
По-четверте: перевірте завершення та політику перезапуску
- terminationGracePeriodSeconds достатньо довгий?
- preStop hook щоб припинити приймати трафік до SIGTERM?
- startupProbe існує і налаштований під найгірший випадок відновлення?
Три міні-історії з корпоративного світу (усі надто реальні)
Міні-історія 1: Інцидент, спричинений хибним припущенням
Середня SaaS-компанія мігрувала з віртуалок у Kubernetes поетапно. Перші сервіси були безстанні і все пройшло гладко. Довіра зросла. Потім перенесли «просту репліку MySQL» для звітності. План був скопіювати конфігурацію старої VM, вставити її в StatefulSet і додати readiness-перевірку на відставання реплікації, щоб захищати дашборди від застарілих даних.
Хибне припущення: «Якщо репліка відстає — под нездоровий.» Перевірка readiness випадково була реалізована як liveness — той же скрипт, скопійований у неправильний блок. У стейджингу це пройшло, бо датасет був малий і відставання рідко перевищувало кілька секунд.
У проді був щоденний сплеск записів і аналітична задача, що створювала періодичні IO-піки. Під час сплеску відставання росло. Liveness впав. Kubernetes перезапустив репліку. MySQL пішов у recovery, який під тим самим IO-навантаженням був повільнішим. Liveness знову впав. Петля перезапусків. Дашборди впали, потім застосунок почав фейлитися, бо тихо використовував цю репліку для деяких читань.
Виправлення було банальним: перенести перевірки відставання в readiness, додати startupProbe з щедрим бюджетом і припинити маршрутизувати критичні читання на репліку без явної терпимості до застарілості. У розборі помилок незручний висновок: «репліка для звітів» ніколи не була справді ізольована. Вона була лише соціально ізольована.
Міні-історія 2: Оптимізація, що відгукнулась бумерангом
Велике підприємство хотіло швидше писати в MariaDB у Kubernetes. Вони працювали з налаштуваннями надійності і скаржилися на затримки комітів у пікові години. Хтось запропонував послабити надійність: поставити innodb_flush_log_at_trx_commit=2 і sync_binlog=0, бо «сховище все одно надлишкове».
Продуктивність покращилася. Графіки затримок виглядали краще. Люди святкували і пішли далі. Через кілька тижнів при падінні вузла під час активного запису Kubernetes переселив под, MariaDB стартувала, відновлення пройшло швидко — бо було менше складного стану для узгодження.
Потім настала тиха частина: низка останніх транзакцій зникла. Не пошкоджена. Зникла. Застосунок поводився непослідовно, бо деякі бізнес-події ніколи не відбулися. Логи не кричали. Вони шепотіли. Компанія витратила дні на звіряння даних через upstream-потоки подій і служби підтримки. Це найгірший вид відмови: база «здоровa», але бізнес — ні.
Зрештою політика була така: якщо ви послаблюєте надійність, документуйте точне вікно втрати даних, яке ви приймаєте, і створіть компенсуючі контролі (ідемпотентні записи, event sourcing, завдання з узгодження). Інакше зберігайте 1/1 і платіть за справжнє сховище. Оптимізація «працювала», поки не стала проблемою під час фінансового аудиту.
Міні-історія 3: Нудна, але правильна практика, що врятувала
Фінтех-компанія запускала MySQL у Kubernetes зі строгою дисципліною: кожен StatefulSet мав startupProbe, налаштований під найгірший випадок відновлення, достатній termination grace і preStop hook, що ставив под у NotReady перед SIGTERM. Також вони періодично відпрацьовували відновлення з бекапів у scratch-простір. Ніхто не любив це робити. Це було не гламурно.
Одного дня планове оновлення кластера дренувало вузли швидше, ніж очікувалось, через неправильно налаштований disruption budget в іншому місці. Кілька MySQL-под були завершені під навантаженням. Декілька довелося відновлювати після рестарту. Це не було гарно, але було контрольовано: поди перед завершенням ставали NotReady, трафік відводився, а період грації дозволив більшості інстансів закритися чисто.
Справжнє порятунок: одна репліка піднялася з пошкодженим datadir через якийсь збій бекенда сховища. Замість імпровізації на чергуванні виконали відпрацьований рукопис: cordon вузол, від’єднати том, створити новий PVC і відновити з останнього відомо хорошого бекапу. Сервіс деградував, але залишився доступним. Нікому не довелося «просто видалити под і подивитися».
Їхня перевага була не в кращих інженерах. Вона полягала в меншій кількості несподіванок. Вони відпрацювали нудні дії доки ті не стали автоматичними.
Поширені помилки: симптом → корінна причина → виправлення
1) CrashLoopBackOff під час відновлення після аварії
Симптоми: Под перезапускається кожні 30–90 секунд; логи показують, що InnoDB recovery починається щоразу.
Корінна причина: Liveness падає під час легітимного відновлення; немає startupProbe; таймаут занадто короткий; сплески латентності сховища.
Виправлення: Додайте startupProbe з достатнім failureThreshold×periodSeconds для покриття найгіршого випадку. Збільшіть таймаут liveness. Тримайте liveness дешевим (admin ping), а не важким SQL.
2) Под запущений, але ніколи не Ready
Симптоми: STATUS Running, але READY 0/1; застосунок не бачить endpoint-ів.
Корінна причина: Readiness перевіряє відставання репліки з нереалістично суворим порогом; або перевірка підключається через DNS/service, який ще не готовий; або неправильні облікові дані/утиліта в образі.
Виправлення: Використовуйте локальний сокет; переконайтеся, що бінарник для перевірки існує; розслабте критерії readiness до прийнятних для застосунку; розділіть «обслуговує читання» і «обслуговує записи», якщо потрібно.
3) Випадкове «server has gone away» під час дренування вузлів
Симптоми: Короткі сплески помилок клієнта під час деплойментів/оновлень; логи показують раптові розриви з’єднань.
Корінна причина: Немає preStop hook, який видаляє под з endpoints перед завершенням; termination grace занадто короткий; не реалізовано зливання з’єднань.
Виправлення: Додайте preStop, щоб поміняти readiness (або паузу після позначення NotReady). Збільшіть termination grace. Розгляньте проксі-слой для дренування з’єднань.
4) Репліки назавжди відстають після рестарту
Симптоми: Seconds_Behind_Master росте і не відновлюється; потоки IO/SQL працюють, але повільно.
Корінна причина: Пропускна здатність сховища недостатня для темпу застосування; обмеження CPU тротлиться; однониткове застосування; або довгі транзакції на primary.
Виправлення: Спочатку виправте StorageClass/IOPS; підвищте ліміти CPU; налаштуйте паралелізм apply реплікації де застосовано; зменшіть довгі транзакції.
5) Пошкодження каталогу даних після «простого redeploy»
Симптоми: mysqld не стартує; помилки про відсутні tablespaces або невідповідність redo-журналів.
Корінна причина: Два поди змонтували той самий RWO-том через ручне втручання або зламаний контролер; або образ контейнера змінив права на datadir; або init-контейнери випадково перезаписали.
Виправлення: Забезпечте ідентичність StatefulSet і PVC; уникайте ручних «приєднань кудись ще»; зафіксуйте init-логику, щоб вона ніколи не витирала існуючий datadir.
6) «Повільно», але тільки в Kubernetes
Симптоми: Та сама конфігурація на VM швидка, на k8s повільна; коміти бувають стрибкоподібними.
Корінна причина: Варіативність латентності мережевого сховища; тротлінг CPU через низькі ліміти; ефект шумного сусіда; неправильно підібраний buffer pool для ліміту пам’яті.
Виправлення: Бенчмаркуйте StorageClass; резервуйте CPU/пам’ять відповідно; забезпечте, щоб buffer pool поміщався в ліміт пам’яті; тримайте налаштування надійності стабільними, якщо не готові до втрат.
Контрольні списки / покроковий план
Чекліст A: Дизайн перевірок для MySQL/MariaDB (зробіть це до проду)
- Використовуйте startupProbe, щоб захистити відновлення після аварії (бюджет для найгіршого випадку, не середнього).
- Liveness: простий локальний ping без SQL; консервативні таймаути.
- Readiness: локальне підключення +
SELECT 1; опційно перевірка відставання репліки або wsrep готовності. - Уникайте секретів в argv, де можливо; віддавайте перевагу env-перемінним або файлу конфігурації, доступному лише mysql-користувачу.
- Налаштовуйте таймаути перевірок під ваше сховище, а не під оптимізм.
Чекліст B: Безпека перезапусків і коректність завершення
- terminationGracePeriodSeconds підібраний для скидання/завершення під навантаженням.
- preStop hook, який зупиняє маршрутизацію трафіку перед SIGTERM (readiness gate або drain проксі).
- PDB, що не дозволяє одночасно порушувати кілька критичних подів.
- Pod anti-affinity, щоб не поміщати primary і репліку в ту ж домену відмови.
Чекліст C: Позиція щодо безпеки даних (вирішіть явно)
- Налаштування надійності: оберіть
innodb_flush_log_at_trx_commitіsync_binlogзгідно з прийнятним вікном втрати. - StorageClass: оберіть клас з передбачуваною латентністю fsync; протестуйте його з тим самим патерном доступу.
- Бекапи: автоматизовані, зашифровані, з відпрацьованими відновленнями у scratch-просторі.
- Міграції схем: поетапні й оборотні, коли можливо; не виконувати довгі блокуючі міграції в піку.
Поширені запитання
1) Чи варто використовувати MySQL чи MariaDB у Kubernetes?
Якщо вам потрібна максимальна сумісність з екосистемою MySQL (і фічі MySQL 8), обирайте MySQL. Якщо ви прихильник фіч MariaDB, наприклад Galera-патернів, обирайте MariaDB. Операційно обидва можна безпечно запускати — але лише якщо ви спроєктували перевірки та сховище правильно.
2) Чи потрібен мені оператор?
Якщо ви виконуєте продові записи — так, або зрілий оператор, або дуже дисциплінований внутрішній чарт із runbook-ами. Станна надійність приходить від автоматизації «потворних» крайніх випадків: bootstrap, failover, бекапи і безпечні апгрейди.
3) Чи може liveness-перевірка перевіряти відставання реплікації?
Ні. Це питання readiness. Liveness, що вбиває відстаючу репліку, перетворює «тимчасово позаду» на «назавжди мертву».
4) Чому іноді рестарт бази займає так багато часу?
Неналежне завершення викликає відновлення після аварії. Час відновлення залежить від обсягу redo і пропускної здатності/латентності вашого тому. На повільному або змінному сховищі він може коливатися значно.
5) Чи безпечно запускати MySQL/MariaDB на мережевому сховищі?
Це може бути безпечно, якщо сховище забезпечує передбачувану латентність і правильні семантики, але продуктивність і хвостова латентність часто страждають. Для важкого OLTP локальний SSD або якісне блочне сховище з хорошими fsync-характеристиками зазвичай безпечніші.
6) Чи варто ставити innodb_flush_log_at_trx_commit=2 для продуктивності?
Лише якщо бізнес готовий втратити до ~1 секунди транзакцій при падінні (і ви розумієте також поведінку binlog). Якщо ви цього не можете терпіти — не «оптимізуйте». Виправте сховище та ізоляцію ресурсів замість цього.
7) Як повинна поводитися readiness для Galera (MariaDB)?
Пропускайте readiness, коли wsrep має: cluster status Primary, local state Synced і wsrep_ready=ON. Інакше ви направите трафік на вузли, що приєднуються або віддають дані, і отримаєте дивну поведінку клієнтів.
8) Яка №1 причина втрати даних у налаштуваннях бази в Kubernetes?
Людські рішення у вигляді дефолтів: послаблена надійність без явного схвалення, видалення StatefulSet із політикою reclaim=Delete або зламана дисципліна бекапів/відновлення. Kubernetes рідко випадково видаляє ваші дані; люди це роблять.
9) Чи створюють перевірки навантаження на базу даних?
Так. Якщо перевіряти надто часто, виконувати важкі запити або використовувати багато одночасних з’єднань для перевірок, ви можете спричинити ту саму латентність, що й призведе до падіння перевірок. Тримайте перевірки дешевими і обмеженими за частотою.
10) Який найпростіший безпечний підхід до перевірок?
StartupProbe: щедрий mysqladmin ping. Liveness: той самий ping з консервативним таймаутом і періодом. Readiness: локальне підключення + SELECT 1, плюс опційно gating за реплікою/wsrep.
Наступні кроки, які можна виконати цього тижня
Якщо ви зараз запускаєте MySQL або MariaDB у Kubernetes, ось практична послідовність, що підвищить безпеку без потреби мігрувати або впроваджувати новий оператор завтра:
- Додайте startupProbe до кожного пода бази даних, налаштований під найгірший випадок відновлення для вашого StorageClass.
- Аудит liveness-перевірок і приберіть усе, що перевіряє логічне здоров’я (відставання реплікації, wsrep, довгі SQL).
- Збільште termination grace і додайте preStop hook, щоб припинити маршрутизацію трафіку перед завершенням.
- Виміряйте хвостову латентність сховища у піку і корелюйте з провалами перевірок і затримками комітів; якщо вона стрибкоподібна — виправте сховище перед налаштуванням MySQL.
- Проведіть тест відновлення в scratch-просторі. Якщо це боляче — це не бекап; це бажання.
- Запишіть свою позицію щодо надійності (1/1 vs послаблений), з явною згодою бізнес-власника, якщо ви обираєте ризик втрати даних.
Мета не в «ніколи не перезапускати». Мета — «перезапуски пережиті, передбачувані і нудні». Kubernetes буде продовжувати повторювати спроби. Переконайтеся, що він повторює щось безпечне.