Деякі бази даних падають голосно. MySQL і MariaDB у Docker часто «падають ввічливо»: вони продовжують обслуговувати запити, поки тихо перетворюють ваш SSD на перкусійний інструмент.
Крутий обвал продуктивності зазвичай не через вашу схему або «якийсь дивний запит». Це — значення за замовчуванням: драйвери сховища Docker, семантика файлової системи, обмеження пам’яті, поведінка fsync і кілька налаштувань бази, які колись були розумними в 2008 році, але згубні в контейнері в 2026 році.
Справжній ворог: невідповідність очікувань бази даних і реальності контейнера
MySQL і MariaDB зростали, припускаючи доволі простий контракт з операційною системою:
- «Коли я викликаю fsync — мої дані на стійкому сховищі.»
- «Коли я виділяю пам’ять — вона залишається моєю.»
- «Коли я записую на диск — латентність відносно передбачувана.»
- «Коли я використовую тимчасові таблиці — у мене достатньо локального диску.»
Docker не порушує цей контракт навмисно. Він просто додає шарів: union-файлові системи, copy-on-write, обмеження cgroup, віртуалізовані файлові системи на macOS/Windows та драйвери томів з дуже різними характеристиками надійності й латентності. Ваша база все ще намагається бути коректною. Платформа контейнерів намагається бути гнучкою. Коректність і гнучкість можуть співіснувати — якщо ви перестанете довіряти значенням за замовчуванням.
Теза: ставтеся до контейнеризованої бази даних як до пристрою зберігання. Сховище — це першочергова залежність. CPU зазвичай у порядку. Мережа рідко перша проблема. Майже завжди це — латентність вводу/виводу, поведінка fsync або тиск пам’яті, замасковані під «повільні запити».
Цікаві факти та трохи історії (бо значення за замовчуванням мають бекграунд)
Це невеликі факти, але кожен пояснює момент «чому воно так?» при налаштуванні MySQL/MariaDB у Docker:
- InnoDB став движком за замовчуванням у MySQL 5.5, переважно тому, що краще обробляє збої та конкурентність порівняно з MyISAM. Це означає, що ви успадкували сильні міркування InnoDB щодо fsync і redo-логів.
- MariaDB відгалузилася від MySQL у 2009 році після придбання Sun компанією Oracle. Форк зберіг сумісність, але експлуатаційні значення за замовчуванням і характеристики продуктивності з часом дивергувалися.
- Overlay-файлові системи Docker проектувалися під образи, а не під бази даних. Copy-on-write чудово підходить для шарування файлів застосунку; це податок для робочих навантажень, що інтенсивно записують дрібні випадкові блоки.
- Подвійний запис InnoDB існує через часткові записи сторінок (відключення живлення, дивна поведінка контролера, баги ядра). Він обмінює збільшення записів на безпеку — неприємний, але зазвичай правильний компроміс.
- Кеш запитів MySQL було видалено в MySQL 8.0, бо він спричиняв контенцію й непередбачувану продуктивність. Якщо ви все ще «налаштовуєте кеш запитів» у контейнері, ви, мабуть, використовуєте застарілу збірку і маєте більші проблеми.
- Семантика O_DIRECT і fsync у Linux відрізняються за файловими системами та опціями монтування. Дурність у надійності бази даних — не універсальна істина; це узгодження між шаром програмного забезпечення та шарами сховища.
- cgroups роками робили «доступну пам’ять» в контейнерах оманливою. Сучасний MySQL краще читає обмеження cgroup, але багато образів і старих версій іще розмірковують буфери, ґрунтуючись на RAM хоста.
- Круті злети латентності SSD реальні: коли ви насичуєте чергу пристрою або запускаєте GC, латентність стрибає раніше за пропускну здатність. Ваша база часом відріже довше, ніж моніторинг покаже «диск на 100% зайнятий».
І одна надійна цитата — бо це досі єдиний урок, який має значення:
«Сподівання — це не стратегія.» — генерал Гордон Р. Салліван
Швидкий план діагностики: знайдіть вузьке місце за 15 хвилин
Якщо екземпляр MySQL/MariaDB в контейнері повільний, не починайте з SQL. Почніть з фізики. База чекає на щось.
Перше: підтвердьте, що означає «повільно»
- Чи це підвищена латентність на запит, чи зменшена пропускна здатність, чи те й інше?
- Чи це лише записи, лише читання, чи все одно?
- Чи це періодично (стрибки), чи постійно?
Друге: перевірте латентність диска та тиск fsync
- Шукайте високу тривалість fsync для redo-логу та затримки через «dirty page».
- Підтвердьте, що ви не записуєте в overlay2 або тонкий мережевий том з жахливою синхронною латентністю.
Третє: перевірте пам’ять і обмеження cgroup
- Чи відбувається OOM-убивство mysqld, чи свопінг, чи агресивне звільнення page cache?
- Чи розмір InnoDB buffer pool налаштовано під пам’ять хоста, а не під ліміт контейнера?
Четверте: перевірте CPU steal і тротлінг
- Голод CPU маскується під «випадкову» повільність БД, особливо при сплесках навантаження.
П’яте: тільки тепер читайте slow query log
- Якщо повільні запити чекають на «waiting for handler commit» або I/O, ви знову до диска і fsync.
- Якщо вони CPU-зв’язані з поганими планами, тоді налаштовуйте SQL і індекси.
Це послідовність. Коли її пропускають, довго налаштовують неправильну річ.
Налаштування за замовчуванням, що псують продуктивність (і що робити замість них)
1) Запис datadir на overlay2 (або будь-яку union-файлову систему)
Класичний антипатерн Docker: ви запускаєте MySQL у контейнері, забуваєте змонтувати реальний том, і datadir живе в записному шарі контейнера. Воно працює. На бенчмарку — жахливо. Крім того, це ускладнює оновлення й бекапи, бо стан приклеєний до ефемерного шару.
Чому це шкодить: overlay2 — copy-on-write. Бази багато пишуть дрібними оновленнями, і InnoDB пише у шаблонах, що провокують метадані. Навіть якщо у вас «SSD», шар файлової системи може додавати латентність і навантаження на CPU.
Робіть так замість цього: розміщуйте /var/lib/mysql на іменованому томі або bind-монту на реальній файловій системі. В продакшені віддавайте перевагу локальним томам на XFS/ext4 з розумними опціями монтування або ретельно підібраним CSI-томом з відомою латентністю синхронізації.
2) Припускати, що «Docker volume» означає «швидкий» (не обов’язково)
Docker «том» — це абстракція. Фактичне сховище може бути локальною директорією на ext4, або NFS, або блочним диском у хмарі, або розподіленою файловою системою. Профіль латентності може бути «нормальний» або «чому commit триває 200ms?»
Робіть так замість цього: вимірюйте латентність fsync на точному драйвері тому, який ви використовуєте. Ставтеся до сховища як до залежності з SLO.
3) Стандартні налаштування InnoDB для надійності + повільне сховище = сум
Дві змінні найважливіші для навантажень з великою кількістю записів:
innodb_flush_log_at_trx_commit(зазвичай 1 за замовчуванням)sync_binlog(часто 1 у більш обережних налаштуваннях, але варіюється)
innodb_flush_log_at_trx_commit=1 означає, що InnoDB скидає redo на диск при кожному коміті. На сховищі з високою латентністю fsync — затримка коміту стає латентністю сховища. Якщо також увімкнено біналог і sync_binlog=1, ви платите двічі.
Що робити: вирішіть вимоги до надійності свідомо. Для багатьох внутрішніх систем innodb_flush_log_at_trx_commit=2 — прийнятний ризик. Для фінансових систем — можливо ні. Але приймайте рішення усвідомлено, не успадковуйте їх автоматично.
Жарт №1: Якщо встановите innodb_flush_log_at_trx_commit=0 у продакшені, ваша база тепер працює на вайбі та оптимізмі.
4) Біналоги на повільному сховищі (і відсутність плану для них)
Біналоги не опціональні, якщо ви хочете реплікацію або відновлення до точки в часі. Це також постійний потік записів, який може стати домінантним I/O. У Docker люди часто забувають:
- Де зберігаються binlog (за замовчуванням у тому ж datadir, якщо не налаштовано)
- Як швидко вони ростуть під навантаженням записів
- Що очищення вимагає політики
Виправлення: розрахуйте сховище під збереження binlog, увімкніть автоматичне видалення за віком і моніторьте зростання.
5) Обмеження пам’яті контейнера + стандартний буфер InnoDB = треш кеша
У контейнері пам’ять — політична тема. MySQL може бачити пам’ять хоста (залежно від версії та конфігурації) і щасливо виділити великий buffer pool. Потім спрацьовує ліміт cgroup, і ядро починає вбивати процеси або агресивно звільняти пам’ять.
Симптоми: періодичні затримки, OOM-убивства, «випадкові» сплески латентності запитів і ОС, яка виглядає спокійною, поки контейнер горить.
Виправлення: встановіть innodb_buffer_pool_size відповідно до ліміту контейнера, а не RAM хоста. Залиште місце для:
- буферів підключень (на підключення виділяється пам’ять)
- sort/join буферів під навантаженням
- внутрішнього оверхеду InnoDB
- файлового кешу ОС (так, все ще важливий)
6) Занадто багато підключень (бо значення за замовчуванням щедрі)
max_connections часто встановлено високо «про всяк випадок». У контейнерах це «про всяк випадок» перетворюється на «про всяк випадок у свопі». Кожне підключення може виділяти пам’ять на потік. При сплесках пам’ять роздувається, і ядро робить те, що воно робить: карає за брехню про ємність.
Виправлення: обмежте max_connections, використовуйте пулінг і розумно розміруйте буфери на потік. Ваша база не концертний зал; їй не потрібна нескінченна стояча площа.
7) tmpdir і тимчасові таблиці потрапляють не на той диск
Великі сорти, ALTER TABLE та складні запити можуть виливатися на диск. У контейнерах tmpdir може опинитися на маленькій root-файловій системі, а не на вашому великому томі даних.
Виправлення: встановіть tmpdir у шлях на тому ж швидкому томі (або на виділеному швидкому томі) і моніторьте вільне місце. Якщо ви на Kubernetes, тут епемерні ліміти сховища часто підкрадаються неприємністю.
8) Відсутність або неправильне налаштування розміру redo-логу
Redo-логи, що занадто малі, спричиняють часті чекпоїнти, що збільшує фонове скидання і підсилює тиск записів. Занадто великі можуть подовжити час відновлення після збою. У контейнерному світі варіант «занадто малий» частіший, бо значення за замовчуванням консервативні, а сховище часто повільніше, ніж очікується.
Виправлення: налаштуйте ємність redo-логу під вашу швидкість записів і цілі відновлення. У сучасному MySQL думайте про innodb_redo_log_capacity; у MariaDB і старих MySQL працюйте з розміром і кількістю файлів журналу.
9) «Ми просто використаємо мережеве сховище» (а потім fsync = 20ms)
Віддалене сховище може працювати. Але воно також тихо перетворює шлях коміту в кругову поїздку мережею і трьома шарами кешування. Багато розподілених систем оптимізовані під пропускну здатність, а не під латентність fsync.
Виправлення: перевірте латентність синхронного запису перед тим, як покладатися на неї для бази даних. Якщо мусите використовувати мережеві томи — віддавайте перевагу тим, що гарантують передбачувану надійність і низьку хвильову латентність. Вимірюйте p99, а не середні значення.
10) Не прив’язуєте CPU або не спостерігаєте троттлінг
Бази даних люблять стабільність. Тротлінг і конкуренція за CPU можуть виглядати як очікування I/O, бо потокам бази не дають ресурсу, щоб завершити роботу. У контейнерах ви можете випадково запустити MySQL на «best effort» CPU, поки пакетні задачі тиснуть вузол.
Виправлення: задайте адекватні CPU requests/limits (або Docker CPU quotas) і спостерігайте метрики тротлінгу. Якщо бачите тротлінг під нормальним навантаженням — ви недопровізовано або неправильно налаштовано.
11) Запуск на Docker Desktop для macOS/Windows і сподівання на Linux-native I/O
Розробницькі середовища — де народжуються міфи про продуктивність. Docker Desktop використовує VM. Bind-монти проходять через шар трансляції. Файлові операції можуть бути драматично повільнішими, особливо синхронні патерни як InnoDB redo.
Виправлення: для реалістичних тестів продуктивності запускайте на Linux з реальним томом. Для локальної розробки прийміть повільніший I/O або перейдіть на іменовані томи замість bind-монту.
12) Трактування конфігу MySQL як «всередині контейнера», а не «частина сервісу»
Якщо конфігурація вшита в образ без можливості зовнішньої зміни, ви рано чи пізно зробите зміну, яка потребуватиме перекомпоновки образів під тиском. Так прості інциденти стають довгими.
Виправлення: монтуйте конфігурацію, версіонуйте її і зробіть її спостережуваною (SHOW VARIABLES має відповідати вашому наміру).
Практичні завдання: команди, виводи та рішення (12+)
Це перевірки рівня продакшен. Кожна містить: команду, що означає її вивід, і рішення, яке ви з цього робите.
Завдання 1: Підтвердити, куди MySQL реально пише дані (overlay чи том)
cr0x@server:~$ docker inspect -f '{{ .Mounts }}' mysql-prod
[{volume mysql-data /var/lib/docker/volumes/mysql-data/_data /var/lib/mysql local true }]
Значення: /var/lib/mysql знаходиться на іменованому томі, а не в шарі контейнера.
Рішення: Якщо ви не бачите монтування для /var/lib/mysql, зупиніться і виправте це перед тим, як налаштовувати інші речі.
Завдання 2: Визначити драйвер зберігання Docker (overlay2, devicemapper тощо)
cr0x@server:~$ docker info --format '{{ .Driver }}'
overlay2
Значення: використовується overlay2. Добре для контейнерів. Погано для стану бази, якщо ви не змонтували томи правильно.
Рішення: Якщо datadir не на томі, чекайте проблем. Якщо на томі — ви в основному уникнули покарань overlay2.
Завдання 3: Перевірити тип файлової системи та опції монтування, що підтримують том
cr0x@server:~$ docker run --rm -v mysql-data:/var/lib/mysql alpine sh -c "df -T /var/lib/mysql && mount | grep ' /var/lib/docker'"
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 xfs 488245288 124332456 363912832 26% /var/lib/docker
/dev/nvme0n1p2 on /var/lib/docker type xfs (rw,relatime,attr2,inode64,logbufs=8,logbsize=32k)
Значення: цей том на XFS з типовими опціями.
Рішення: Якщо ви бачите NFS або віддалений монтування тут, вважайте латентність fsync основним підозрюваним у продуктивності записів.
Завдання 4: Перевірити ліміт пам’яті контейнера порівняно з тим, що MySQL думає, що має
cr0x@server:~$ docker inspect -f '{{ .HostConfig.Memory }}' mysql-prod
2147483648
cr0x@server:~$ docker exec -it mysql-prod bash -lc "cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes"
2147483648
Значення: контейнер обмежено до 2 GiB.
Рішення: Встановіть innodb_buffer_pool_size на значення близько 1.0–1.4 GiB залежно від навантаження і кількості підключень, а не «80% від хоста».
Завдання 5: Перевірити реальний розмір InnoDB buffer pool
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -N -e \"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';\""
innodb_buffer_pool_size 1073741824
Значення: Buffer pool — 1 GiB.
Рішення: Якщо він крихітний (наприклад 128 MiB) на завантаженому інстансі — очікуйте постійних читань з диска і поганої продуктивності.
Завдання 6: Перевірити OOM-убивства та сигнали тиску пам’яті
cr0x@server:~$ docker exec -it mysql-prod bash -lc "dmesg -T | tail -n 5"
[Fri Jan 3 09:28:41 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-3b1a...,mems_allowed=0,oom_memcg=/docker/3b1a...
[Fri Jan 3 09:28:41 2026] Killed process 1973 (mysqld) total-vm:3121044kB, anon-rss:1632100kB, file-rss:0kB, shmem-rss:0kB
Значення: Ядро вбило mysqld через обмеження пам’яті cgroup.
Рішення: Зменшіть використання пам’яті MySQL (буфери, підключення) або підвищте ліміт пам’яті контейнера. Також виправте цикли рестарту і проаналізуйте ризики корупції.
Завдання 7: Інспектувати тиск fsync на redo-лог і затримки комітів
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW ENGINE INNODB STATUS\\G\" | sed -n '1,120p'"
...
Log sequence number 912345667890
Log flushed up to 912345660000
Last checkpoint at 912345100000
0 pending log flushes, 0 pending chkp writes
...
Значення: Якщо ви бачите багато очікувань скидання журналу або великий розрив між «Log sequence number» і «Log flushed up to», ваше сховище не встигає за redo fsync.
Рішення: Покращіть латентність сховища, налаштуйте ємність redo, розгляньте компроміси щодо надійності (innodb_flush_log_at_trx_commit) і зменшіть підсилення записів.
Завдання 8: Підтвердити змінні, пов’язані з надійністю (ті, які люди «припускають»)
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -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 |
+------------------------------+-------+
Значення: Ви платите повну ціну надійності (redo fsync на кожен коміт + binlog fsync на кожен коміт).
Рішення: Залишайте це, якщо потрібно. Якщо ні — змініть свідомо і задокументуйте профіль ризику.
Завдання 9: Виміряти реальну латентність диска зсередини контейнера (швидко і грубо)
cr0x@server:~$ docker exec -it mysql-prod bash -lc "dd if=/dev/zero of=/var/lib/mysql/dd-test.bin bs=4k count=25000 oflag=dsync 2>&1 | tail -n 2"
25000+0 records in
25000+0 records out
Значення: З oflag=dsync кожен блок синхронізується. Якщо ця команда триває на кілька секунд довше, ніж очікується, ваша синхронна латентність запису погана. (Рядок з часом dd тут опущено, бо він змінюється, але ви побачите його у себе.)
Рішення: Якщо синхронні записи повільні — припиніть звинувачувати індекси. Виправляйте сховище або налаштування надійності.
Завдання 10: Перевірити, чи випадково ви не використовуєте bind-монтаж з поганою семантикою (Desktop, CIFS тощо)
cr0x@server:~$ docker inspect -f '{{ range .Mounts }}{{ .Type }} {{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' mysql-prod
volume /var/lib/docker/volumes/mysql-data/_data -> /var/lib/mysql
Значення: Це том, керований Docker; хороший початок.
Рішення: Якщо ви бачите bind на шляху, що живе на повільній/віддаленій файловій системі, ви знайшли ймовірне вузьке місце.
Завдання 11: Перевірити поведінку тимчасових таблиць і tmpdir (злиття на диск)
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'tmpdir'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';\""
+---------------+----------+
| Variable_name | Value |
+---------------+----------+
| tmpdir | /tmp |
+---------------+----------+
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 48291 |
+-------------------------+-------+
Значення: tmpdir = /tmp, і у вас багато тимчасових таблиць на диску.
Рішення: Поставте tmpdir на швидкий том з достатнім місцем. Також проаналізуйте запити, що викликають spill до диска.
Завдання 12: Виявити тротлінг CPU в контейнерах
cr0x@server:~$ docker stats --no-stream mysql-prod
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
3b1a1c2d3e4f mysql-prod 398.23% 1.62GiB / 2GiB 81.0% 1.2GB / 900MB 40GB / 12GB 58
Значення: Велике використання CPU може бути нормальним. Головне — чи ви досягаєте квот CPU (не показано тут) і чи є кореляція між затримками та тротлінгом.
Рішення: Якщо CPU високий і латентність висока — перевірте плани запитів. Якщо CPU низький, а латентність висока — спочатку перевірте I/O та блокування.
Завдання 13: Підтвердити, що slow query log увімкнено і куди він пише
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'slow_query_log_file';\""
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------------+-------------------------------+
| Variable_name | Value |
+----------------------+-------------------------------+
| slow_query_log_file | /var/lib/mysql/mysql-slow.log |
+----------------------+-------------------------------+
Значення: Логуваня увімкнено і пише на том даних.
Рішення: Якщо він пише у файлову систему контейнера і ви погано обертаєте логи, ви можете заповнити root та впасти MySQL. Покладіть логи на персистентне сховище і налаштуйте ротацію.
Завдання 14: Перевірити, скільки потоків і підключень ви реально маєте
cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';\""
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 412 |
+-------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 800 |
+-----------------+-------+
Значення: У вас багато підключень. Навіть якщо запити швидкі, пам’ять на потоки може зруйнувати контейнер.
Рішення: Додайте пулінг, обмежте max connections і вимірюйте зростання пам’яті під час сплесків.
Три корпоративні міні-історії (анонімізовані, болісно знайомі)
Міні-історія 1: Інцидент через хибне припущення
Вони мігрували легасі-застосунок з VM у контейнер, бо «це лише пакування». База теж пішла в контейнер, бо це звучало стильно і сучасно. Команда використала популярний образ MySQL, запустила в Docker Swarm і зробила bind-монтаж до шляху на хості, що виглядав достатньо персистентним.
На третій день p95 латентності запису подвоївся, потім подвоївся знову. Застосунок не впав; він просто сповільнився, і UI виглядав так, ніби його рендерить дуже терплячий стажер. Інженери копалися в планах запитів, додавали індекси й сперечалися про ORM. Slow query log показував невинні INSERT-и, що тривали сотні мілісекунд.
Зрештою хтось перевірив хост: шлях bind-монту жив на мережевому шарі, змонтованому «для зручності», бо ops-команді було зручно робити бекапи. Ніхто явно не назвав це NFS у Docker compose; це просто була директорія. Латентність fsync була жахливою, а хвильова латентність — ще гіршою.
Виправлення було банальним: перемістити datadir на локальне блочне сховище з передбачуваною латентністю синхронізації, а потім налаштувати бекапи через логічні дампи і binlog (та пізніше фізичні бекапи). Латентність впала миттєво. Індекси, які додали — не були шкідливими, але вони не були проблемою.
Міні-історія 2: Оптимізація, що дала зворотний ефект
Інша компанія мала сервіс з великою кількістю записів. Хтось прочитав, що innodb_flush_log_at_trx_commit=2 може бути швидшим, і це було вірно. Вони змінили його, побачили відмінні бенчмарки і широко розгорнули зміни.
Місяць усе виглядало добре. Пропускна здатність зросла, графіки сховища заспокоїлися. Потім один вузол сильно впав — kernel panic, некоректне завершення, повний спектакль. MySQL перезапустився, відновився, сервіс повернувся. Але деякі недавно підтверджені записи зникли.
Ніхто не був радий, але ніхто не був здивований. Сервіс не мав чіткого контракту надійності: він підтверджував транзакції до того, як вони стали повністю стійкими, а шар застосунку припускав «підтверджено = назавжди». Оптимізація була технічно правильною, але операційно без власника.
Вони врешті повернули сувору надійність для критичних таблиць і реалізували ідемпотентність у шляхах запису, де можна було терпіти повтори. Справжнє виправлення не було лише у конфігу; це було вирівнювання семантики застосунку з реальністю зберігання.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Команда, що запускала MariaDB у контейнерах, мала одну звичку: кожна скарга на продуктивність починалася з одних і тих же трьох перевірок — тип тому, латентність fsync і запас пам’яті у контейнері. Це не було гламурно, але скорочувало інциденти.
Під час піку трафіку вони побачили, що реплікаційне відставання зростає. Інженери додатку одразу звинуватили «поганий реліз запиту». SRE на чергуванні перевірив латентність диска на первинному і репліках. Репліки були в нормі; первин мав періодичні стрибки латентності синхронного запису.
Корінною причиною не був MySQL взагалі. Заплановане сканування антивірусом (так, на Linux) проходило по точці монтування тому і гамселило метадані. Оскільки вони мали звичку перевіряти сховище перш за все, знайшли проблему за хвилини, не години.
Вони виключили каталоги бази з перевірок, задокументували це і рухалися далі. Найкраще — нікому не довелося вивчати новий параметр бази даних о 2 ранку.
Типові помилки: симптом → корінна причина → виправлення
Цей розділ призначений для використання під час інциденту, коли хтось питає, чому «БД повільна», і в кімнаті стає голосно.
1) Симптом: сплески латентності INSERT/UPDATE, читання здебільшого в порядку
- Корінна причина: латентність fsync (скидання redo-логу, синхронізація binlog) на повільному сховищі або мережевому томі.
- Виправлення: перемістіть datadir/binlogs на низьколатентне сховище; виміряйте синхронні записи; налаштовуйте надійність лише за явного погодження ризику.
2) Симптом: MySQL «рандомно» перезапускається, контейнер виходить з кодом 137
- Корінна причина: OOM-убивство через обмеження пам’яті cgroup; buffer pool + пам’ять на підключення перевищили ліміт.
- Виправлення: зменшити buffer pool, обмежити підключення, використовувати пулінг, збільшити ліміт пам’яті і перевірити використання під навантаженням.
3) Симптом: повільні запити показують «Copying to tmp table» або збільшуються дискові тимчасові таблиці
- Корінна причина: tmpdir на повільній або маленькій файловій системі; spill до диска через сорти/group by.
- Виправлення: перемістіть tmpdir на швидкий том; обережно підвищуйте пороги тимчасових таблиць; оптимізуйте запити/індекси, що спричиняють spill.
4) Симптом: «Диск заповнений», але том datadir має місце
- Корінна причина: binlog або slow-логи зростають без ротації; tmpdir на корені контейнера; root-файлова система Docker заповнюється.
- Виправлення: налаштуйте ротацію логів; встановіть термін зберігання binlog; перемістіть tmpdir і логи на правильний том; моніторьте і Docker root, і томи БД.
5) Симптом: пропускна здатність падає під час бекапів
- Корінна причина: метод бекапу насичує I/O або блокує таблиці; снапшотування на сховищі, що карає copy-on-write; читання з того ж тому, куди пишуть redo.
- Виправлення: плануйте бекапи з обмеженнями I/O; використовуйте репліку для бекапів; застосовуйте фізичні бекапи, якщо доречно; вимірюйте вплив і встановіть SLO.
6) Симптом: реплікаційне відставання зростає після переходу на контейнери
- Корінна причина: репліки на іншому класі сховища; тротлінг CPU; вартість fsync binlog; занадто мало місця для relay log або повільний диск.
- Виправлення: уніфікуйте типи сховища; перевірте тротлінг; налаштуйте реплікацію; переконайтеся, що relay logs і datadir на швидких персистентних томах.
7) Симптом: продуктивність нормальна до сплеску навантаження, потім усе таймаутиться
- Корінна причина: шторм підключень; max_connections занадто високий; створення потоків і вибух у пам’яті.
- Виправлення: використовуйте пулінг; обмежуйте одночасність на боці застосунку; встановіть max_connections відповідно до ємності; розгляньте можливості thread pool, якщо доступні.
8) Симптом: CPU виглядає низьким, але запити повільні
- Корінна причина: очікування I/O, контенція блокувань або тротлінг, не видимі в простих метриках.
- Виправлення: перевірте InnoDB status на очікування; перевірте латентність диска; перевірте метрики тротлінгу контейнера; дослідіть блокування.
Жарт №2: Контейнери не зроблять вашу базу швидшою; вони лише допомагають їй подорожувати з її поганими звичками.
Чеклісти / покроковий план
Покроково: продакшен-ready MySQL/MariaDB у Docker
- Обирайте клас сховища першим. Локальне блочне сховище з SSD переважає мережеве для низьколатентних комітів, якщо ви не довели протилежне.
- Монтуйте datadir як том. Без винятків. Якщо не можете змонтувати том — у вас не база даних, у вас демо.
- Вирішіть питання надійності явно. Документуйте
innodb_flush_log_at_trx_commitіsync_binlogз чітким описом ризиків. - Розмірюйте пам’ять проти лімітів контейнера. Buffer pool + оверхед + пікові підключення мають вміщатися з запасом.
- Обмежте підключення і застосовуйте пулінг. Використовуйте розумний
max_connections. Не допускайте, щоб застосунок «випробував» ліміт через відмови. - Розмістіть tmpdir у безпечному місці. Швидкий, великий, під наглядом. Те саме стосується логів.
- Увімкніть спостережуваність. Slow query log, performance schema (коли доречно), ключові метрики InnoDB.
- Плануйте бекапи і відновлення як систему. Тестуйте час відновлення і коректність. Якщо не можете відновити — у вас немає бекапів.
- Навантажуйте тестово на тій самій платформі. Бенчмарки Docker Desktop — для відчуттів, не для планування ємності.
- Репетируйте відмови. Виконуйте kill -9 контейнера в staging і перевіряйте відновлення та цілісність даних.
Мінімальні «day 1» рішення з конфігурації (запишіть їх)
- Де зберігається
/var/lib/mysqlі що це підтримує? - Який очікуваний p95 і p99 fsync латентності?
- Який ліміт пам’яті контейнера і як розмір buffer pool?
- Яка максимальна дозволена кількість одночасних підключень?
- Де зберігаються binlogs і slow логи, і як їх ротувати?
- Яка ваша процедура відновлення і коли ви востаннє її запускали?
Питання та відповіді
1) Чи варто запускати MySQL/MariaDB у Docker у продакшені?
Так, якщо ставитеся до цього як до stateful-сервісу з реальним інженерним підходом до сховища. Ні, якщо ваш план — «у мене працювало на ноуті». Контейнери не скасовують потребу в дисципліні ops; вони роблять її більш нагальною.
2) Іменований том чи bind-монтаж для /var/lib/mysql?
На Linux-серверах обидва варіанти можуть працювати. Іменовані томи часто простіші в експлуатації. Bind-монти підходять, якщо ви контролюєте файлову систему, опції монтування і бекапи. На Docker Desktop іменовані томи зазвичай показують кращу продуктивність, ніж bind-монти.
3) overlay2 — мій Docker драйвер — чи я приречений?
Ні. Ви приречені лише якщо файли бази живуть у записному шарі overlay2. Використайте реальний том — і ви переважно обійдете найгірше.
4) Чи безпечно innodb_flush_log_at_trx_commit=2?
Це бізнес-рішення, замасковане під конфігураційний ключ. Воно може призвести до втрати до ~1 секунди транзакцій під час збою (залежно від таймінгу). Якщо застосунок може ретраїти або терпіти невеликі втрати — може бути прийнятним. Якщо ні — тримайте 1.
5) Чому продуктивність нормальна вранці і жахлива вдень?
Часто через контенцію на сховище (інші робочі навантаження на вузлі), фонова робота, бекапи, ротацію логів або збірку сміття SSD під тривалими записами. Вимірюйте латентність диска у часі і корелюйте з подіями навантаження.
6) Slow query log показує прості запити, що тягнуться. Чому?
Бо «простий» SQL може все одно чекати на fsync при коміті, чекати блокувань або flush-ів буферного пулу. Перед переписуванням запитів дивіться на очікування коміту, I/O та блокування.
7) Чи варто відключити подвійний запис InnoDB, щоб пришвидшити?
Лише якщо ви повністю розумієте ризик і ваш стек сховища гарантує атомарні записи сторінок (багато хто цього не гарантує). Відключення може покращити запис, але підвищує ризик корупції при часткових записах. Більшість команд повинні лишати його увімкненим і вирішувати проблему латентності сховища.
8) Як зрозуміти, чи тимчасові таблиці мені шкодять?
Перевірте Created_tmp_disk_tables і спостерігайте за дисковим простором там, куди вказує tmpdir. Якщо дискові тимчасові таблиці стрибають під час уповільнення — ймовірно, у вас spill до диска або tmpdir повільний.
9) Чи реплікація повільніша у контейнерах?
Не обов’язково. Але в контейнерах легше випадково запустити первинний і репліки на різних класах сховища, з різними CPU-квотами або різними «шумними сусідами». Однорідність інфраструктури важливіша за «контейнер vs VM».
10) Який найкращий один режим для підвищення продуктивності?
Розмістіть datadir та redo/binlogs на низьколатентному сховищі і перевірте поведінку fsync. Більшість скарг «MySQL повільний у Docker» — це проблеми семантики зберігання, прикриті SQL.
Висновок: кроки, які можна зробити сьогодні
Якщо ви запускаєте MySQL/MariaDB у Docker і продуктивність — загадка, припиніть гадати. Зробіть це в порядку:
- Підтвердьте, що
/var/lib/mysqlна реальному томі, а не на overlay. - Виміряйте латентність синхронного запису на цьому томі (не «пропускну здатність диска»).
- Перевірте ліміти пам’яті контейнера і розмір buffer pool, щоб вони відповідали реальності.
- Перевірте налаштування надійності і підтвердіть, що вони відповідають бізнес-контракту.
- Перемістіть tmpdir/логи/binlogs на правильне сховище і налаштуйте ротацію.
Потім налаштовуйте запити. Потім індекси. Не навпаки.