Це завжди трапляється в найгірший момент: крихітна VM, раптовий сплеск трафіку, кілька повільних запитів — і база даних зникає, наче кудись пішла. Ви заходите й бачите: Linux OOM killer вбив mysqld. Знову.
Коли хтось каже «Просто підніміть max_connections, щоб користувачі перестали бачити помилки», зазвичай мають на увазі обмін видимого ґавна (занадто багато підключень) на невидиму катастрофу (вибух пам’яті), і невидима катастрофа виграє, бо вона краде процес. Ця стаття про те, як зробити цей обмін явним, правильно його розрахувати і не здивуватися.
Що насправді означає max_connections (і чому це обіцянка пам’яті)
max_connections — це не регулятор продуктивності. Це регулятор ризику.
На вигляд це максимально допустима кількість одночасних клієнтських з’єднань, які сервер дозволяє. У спокійний день можна думати, що це просто про конкурентність: більше підключень — більше користувачів. У робочий день це про те, скільки «ненажерливих по пам’яті» потоків база готова витримати одночасно, перш ніж закінчиться RAM і процес буде вбитий.
Кожне підключення може вимагати пер-сесійні й пер-потокові виділення пам’яті. Деякі маленькі. Деякі дивовижно великі. Деякі виділяються лише за потреби (сортування, джоїни, тимчасові таблиці). Проблема в тому, що «за потреби» трапляється саме тоді, коли система і так напружена: повільні запити накопичуються, підключення ростуть, і ці дорогі виділення відбуваються саме тоді, коли їх найменше хочеться.
Отже, правильна ментальна модель така:
- Глобальна пам’ять (спільна для всіх): innodb buffer pool, лог-буфери, кеші, внутрішні структури.
- Базовий витрат на підключення: стек потоку, мережеві буфери, сесійні структури.
- Пер-запитні сплески: sort buffers, join buffers, пам’ять тимчасових таблиць, read buffers.
Якщо встановити max_connections занадто високо на маленькому сервері, ви фактично пообіцяли ядру: «Я можу створити до N потоків, і кожен з них може одночасно виконувати пам’ятенасичену роботу». Ядро вірить вам. OOM killer має іншу думку.
Короткий жарт, бо він потрібен: Підвищувати max_connections, щоб виправити «Too many connections», це як купувати більший відерко для сміття, щоб загасити кухонну пожежу. Ви усуваєте видимий симптом і ігноруєте жар.
Цікавинки та контекст (бо у цієї дебати є історія)
- Модель «потік на підключення» в MySQL стара і надійна. Вона дивовижно добре масштабується при правильних шаблонах навантаження, але робить «занадто багато простаючих підключень» проблемою пам’яті та планувальника.
- MariaDB відгалузилася від MySQL у 2009 році після епохи Sun/Oracle; сумісність залишалась високою, але значення за замовчуванням і функції з часом розійшлися.
- MySQL 5.6 популяризував еру великого InnoDB buffer pool; до того багато розгортань покладалися на кеш ОС і MyISAM. Це змінило культуру планування пам’яті.
- InnoDB став двигуном за замовчуванням у MySQL 5.5. Ця зміна підвищила значення глобального планування пам’яті і знизила терпимість до «випадкового тюнінгу».
- Performance Schema (MySQL) та подібні інструменти змінили спосіб спостереження за пам’яттю й очікуваннями — якщо ви ввімкнете їх і дійсно їх використовуєте.
- Поводження Linux OOM killer не «випадкове», але неінтуїтивне: він обирає жертву за шкалою badness, використанням пам’яті та обмеженнями. Бази даних великі, тому вони привабливі як цілі.
- „Swap робить бази повільними” стало міфом; але насправді «відсутність swap на маленькій VM» часто перетворює відновлюваний сплеск на вбивство процесу.
- Пулювання підключень стало мейнстрімом у 2010-х для веб-стеків, бо бази з моделлю потік-на-підключення не люблять тисячі здебільшого простаючих TCP-сесій.
MySQL vs MariaDB: відмінності, що впливають на ризик OOM
Якщо ви шукаєте просту відповідь на кшталт «MySQL використовує менше пам’яті» або «MariaDB безпечніша», її не існує. Обидві можуть бути стабільними або катастрофічними. Різниця — в тому, де лежать «пістолети для ніг» і які значення за замовчуванням ви успадкували.
За замовчуванням і пакування: ваша реальна база — «MySQL + вибір дистрибутива»
На малих серверах найбільший ризик — не форк; а паковані конфігурації й те, що ваша команда скопіювала з блогу 2016 року.
Типові патерни:
- Пакети MariaDB часто постачаються з обережними значеннями за замовчуванням для малих інстансів, але ваш образ у хмарі може перезаписати їх «корисними» пресетами.
- MySQL на керованих платформах іноді вмикає інструментарій або додаткові кеші, що підвищують базову пам’ять.
- Обидва можна конфігурувати «в нуль», встановивши пер-потокові буфери занадто великими й піднявши
max_connections.
Обробка потоків і ліміти підключень
Зазвичай обидва використовують по потоку на клієнтське підключення. Накладні витрати планувальника й пам’яті зростають з кількістю підключень.
Де люди обпікаються:
- Високе
max_connectionsразом з довгимwait_timeoutстворює велику популяцію простаючих сесій. Простоювання — не безкоштовне. - Висока частота відкриття/закриття з’єднань спричиняє витрату CPU і інколи фрагментацію пам’яті, що виглядає як «витік».
- Thread cache може допомогти, але також може утримувати велику кількість ресурсів потоків, якщо його не контролювати.
Різниця в спостережуваності пам’яті
Performance Schema у MySQL покращився у сучасних версіях у розподілі пам’яті, але він не «безкоштовний» автоматично. MariaDB має власний інструментарій і статус-перемінні; практична різниця — це те, яким інструментом ваша команда дійсно користується.
Для запобігання OOM переможе та система, що:
- дозволяє оцінити пер-підключену та пер-запитну пам’ять,
- показує поточний розподіл станів підключень (Sleep vs active),
- допомагає знайти повільні запити, що спричиняють накопичення,
- робить простим встановлення розумних обмежень.
Одна реальна відмінність форку, що має значення: не можна автоматично переносити налаштування 1:1
Більшість ключів конфігурації схожі, але не ідентичні між версіями й форками. Не можна безпечно застосовувати гайди з налаштування «для MySQL», не перевіривши, який сервер ви використовуєте. Деякі змінні є в обох, але з різними значеннями за замовчуванням або поведінкою. Пам’ять не пробачає; невелика невідповідність достатня.
План швидкої діагностики
Ось порядок, який я використовую, коли малий сервер лихоманить і люди кричать.
1) Підтвердьте, що це OOM, і визначте патерн вбивства
- Перевірте журнали ядра на записи OOM killer, що вказують
mysqld. - Вирішіть, чи це одиничний сплеск (поганий запит), чи хронічний overcommit (занадто багато підключень + занадто великі буфери).
2) Виміряйте поточний стан підключень і причину накопичення
- Скільки підключень активні, а скільки сплять?
- Чи застрягли потоки в «Sending data», «Copying to tmp table», «Sorting result» або чекають блокувань?
- Чи є «thundering herd» через некоректні налаштування пулу?
3) Швидко порахуйте «найгірший правдоподібний сценарій пам’яті»
- Глобально: innodb buffer pool + лог-буфери + інші глобальні кеші.
- Базове на підключення: стек потоку + мережеві буфери + сесія.
- Сплески: sort/join буфери і тимчасові таблиці для активних запитів.
4) Застосуйте стабілізуюче обмеження
- Зменшіть
max_connectionsдо значення, яке ви можете собі дозволити сьогодні. - Зменшіть пер-потокові буфери до розумних значень, якщо хтось їх роздув.
- Скоротіть тайм-аути, щоб простаючі сесії не займали місце вічно.
- Впровадьте або виправте пулювання підключень на рівні додатку.
5) Усуньте корінь проблеми: повільні запити й суперечки за блокування
- Знайдіть найгірших порушників і зупиніть їх утримувати ресурси.
- Додайте індекси, перепишіть запити, зменшіть розміри результатів або пакетну обробку.
Якщо ви зробите кроки 1–4, кровотеча зупиниться. Крок 5 не дозволить їй повернутися.
Модель пам’яті: глобальні буфери, пер-потокові буфери і пастка
Бази даних OOM-ляться на малих серверах тому, що люди мислять про пам’ять як про одну ручку. Це не так. Це купа ручок, і деякі множаться на кількість підключень.
Глобальна пам’ять: те, що завжди є
На InnoDB-важких системах (а це більшість) велике споживання — це:
innodb_buffer_pool_size: зазвичай найбільший споживач. Чудово для продуктивності. Фатально, коли його занадто багато.
Інші глобальні або майже глобальні споживачі включають:
innodb_log_buffer_sizeта інші внутрішні алокації InnoDB,- різні кеші (table cache тощо),
- накладні витрати інструментації, якщо вона ввімкнена на масштабі.
Глобальна пам’ять принаймні передбачувана. Ви її задаєте, вона лишається більш-менш стабільною.
Базовий витрат на підключення: мовчазний множник
Кожне підключення має накладні витрати. Точний слід залежить від версії, збірки й навантаження, але поведінка множника не змінюється: більше сесій — більше пам’яті і більше витрат планувальника.
Ключові компоненти:
thread_stack: виділяється на потік.- Мережеві буфери й пер-сесійні структури.
- Ефекти thread cache: він може тримати ресурси потоків живими після розриву з’єднання.
Пер-запитні сплески: де OOM трапляється під час «шторму повільних запитів»
Схильні до сплесків алокації — справжні злочинці. Класичні:
sort_buffer_size: використовується для сортувань, які не задовольняються індексами.join_buffer_size: використовується для джоїнів, коли індекси не застосовуються ефективно.read_buffer_sizeіread_rnd_buffer_sizeдля певних патернів доступу.- Тимчасові таблиці: можуть використовувати пам’ять до ліміту, а потім виштовхуватися на диск.
Ось пастка: ці буфери часто виділяються на потік на операцію. Якщо 200 активних підключень одночасно виконують сортування, навіть помірні розміри буферів можуть стати катастрофічними.
Чому «вчора у мене була вільна пам’ять» не означає, що ви в безпеці
OOM-збитки зазвичай корелюють зі сплесками конкурентності й хвостовою латентністю. Повільний запит збільшує свій час виконання, що збільшує кількість одночасних запитів, що збільшує використання пам’яті, що ще більше гальмує. Ви отримуєте петлю зворотного зв’язку, яка закінчується або зниженням навантаження… або вбивством вашої бази ядром.
Другий короткий жарт, бо нам дозволено рівно два: OOM killer — ваш найрішучіший SRE-колега: він ніколи не сперечається, він просто видаляє найбільший процес і йде їсти.
Одна оперативна цитата
Перефразована ідея від John Allspaw: «В складних системах інциденти виникають через взаємодію нормальної роботи, а не через одну зламану частину.»
Це важливо, бо OOM на малих серверах рідко буває «витоком». Це нормальні буфери + нормальні підключення + одна повільна річ, одночасно.
Правильне визначення max_connections на малих серверах
Ось упереджена порада: на малих серверах краще віддавати перевагу меншій кількості підключень, які тримаються «теплими» і мультиплексуються пулом, ніж дозволяти «усім підключатися напряму».
Крок 1: оберіть бюджет пам’яті для MySQL/MariaDB
На маленькій VM ви не володієте цілою машиною. ОС потребує page cache, менеджер сервісів — запасу ресурсу, і якщо у вас є співрозміщені додатки, їм теж потрібне місце.
Практичний підхід:
- Визначте, скільки оперативної пам’яті ви можете віддати процесу бази під навантаженням. Не «доступно», а «безпечно».
- Зарезервуйте запас для сплесків, файлового кешу та непередбачуваного.
Якщо ви працюєте без swap, будьте особливо консервативні. Різниця між «повільно» і «мертвим» іноді складає 512MB.
Крок 2: порахуйте глобальну базу
Почніть з innodb_buffer_pool_size. Додайте інші глобальні алокації. Не перенавантажуйтеся дрібницями; просто залиште запас.
Крок 3: оцініть витрати на підключення (базові + правдоподібний сплеск)
На малих серверах вам не потрібне ідеальне число. Вам потрібен консервативний верхній межа.
Розбийте на дві категорії:
- Базове на підключення: стек потоку + сесія + мережеві буфери. Припускайте кілька мегабайт на підключення, якщо немає доказів інакшого.
- Навантаження активного запиту: для частини підключень, що можуть одночасно виконувати пам’ятенасичені операції, припускайте використання sort/join буферів.
Остання частина ключова: не всі сесії активні. Якщо у вас 300 підключень, але лише 20 дійсно активні одночасно, ви рахуєте 300 базових + 20 сплесків. Якщо у вас 300 активних, бо додаток синхронний і все повільно, ви вже в біді і max_connections не має бути 300.
Крок 4: встановіть max_connections на те значення, яке ви реально витримаєте
На малому сервері безпечний діапазон часто в межах 50–200, в залежності від навантаження і буферів. Якщо вас тягне поставити 1000 «на всяк випадок», ви поводитеся з базою як з чергою повідомлень. Вона не черга повідомлень.
Також вирішіть, чи потрібен вам зарезервований адміністративний слот:
max_connectionsобмежує загальну кількість.super_read_onlyі практики адміністративного доступу варіюються, але загальна практика — тримати місце для екстреного доступу та автоматики.
Крок 5: виправте патерн підключень в додатку
Якщо у вас багато короткоживучих підключень, ви відчуєте навантаження від рукопотискання та частого створення потоків. Використовуйте пул. Тримайте його помірним. Встановіть жорсткий ліміт на інстанцію додатку. Потім масштабуйтесь горизонтально за потреби.
Бонус: пулювання підключень зазвичай покращує хвостову латентність, бо усуває «шторм підключень», що трапляється під час часткових відмов.
Практичні завдання: 14 команд, що скажуть вам, що робити далі
Це реальні команди, які можна виконати на Linux-сервері. Кожна має: команду, типовий вивід, що це означає, і рішення, яке ви приймаєте.
Завдання 1: підтвердьте, що OOM killer вбив mysqld
cr0x@server:~$ sudo journalctl -k -b | egrep -i 'oom-killer|out of memory|killed process|mysqld' | tail -n 20
Dec 29 09:12:01 server kernel: Out of memory: Killed process 1423 (mysqld) total-vm:3187420kB, anon-rss:1456820kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Dec 29 09:12:01 server kernel: oom_reaper: reaped process 1423 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Що це означає: Це вбивство на рівні ядра, а не збій бази. Ваша БД попросила пам’ять; ядро сказало ні.
Рішення: Розглядайте це насамперед як проблему місткості пам’яті/лімітів, а не «помилку MySQL». Ви не можете налаштувати мертвий процес.
Завдання 2: перевірте стан системної пам’яті та swap
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 3.4Gi 120Mi 52Mi 310Mi 140Mi
Swap: 0B 0B 0B
Що це означає: Ви працюєте практично без запасу і без swap. Будь-який сплеск — вбивство.
Рішення: Додайте swap (навіть невеликий) або негайно зменшіть пам’яття БД. Краще обидва підходи.
Завдання 3: подивіться RSS процесу mysqld та кількість потоків (коли він живий)
cr0x@server:~$ ps -o pid,cmd,rss,vsz,nlwp --sort=-rss -C mysqld
PID CMD RSS VSZ NLWP
2189 /usr/sbin/mysqld 1684200 3278800 198
Що це означає: RSS — реальна пам’ять у використанні. NLWP дає уявлення про масштаб потоків/підключень.
Рішення: Якщо NLWP зростає разом з кількістю підключень, вам потрібен пул/тайм-аути і зниження max_connections.
Завдання 4: перевірте поточний max_connections та використання підключень
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL STATUS LIKE 'Threads_connected';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 600 |
+-----------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 412 |
+----------------------+-------+
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 389 |
+-------------------+-------+
Що це означає: Ви дозволили 600, і активно сидите близько 400. На маленькій VM це зазвичай поганий знак.
Рішення: Обмежте max_connections ближче до того, що ви можете собі дозволити (і зменшіть реальну конкурентність через пул).
Завдання 5: розбивка «Sleep» vs активні підключення
cr0x@server:~$ mysql -N -e "SELECT COMMAND, COUNT(*) FROM information_schema.PROCESSLIST GROUP BY COMMAND ORDER BY COUNT(*) DESC;"
Sleep 340
Query 28
Connect 3
Binlog Dump 1
Що це означає: Більшість підключень простає. Це часто проблема розмірів пулу додатку або занадто довгих тайм-аутів.
Рішення: Зменшіть wait_timeout і виправте максимальний розмір пулу на одиницю сервісу. Не «вирішуйте» це підвищенням max_connections.
Завдання 6: визначте, що роблять активні запити (розподіл станів)
cr0x@server:~$ mysql -N -e "SELECT STATE, COUNT(*) FROM information_schema.PROCESSLIST WHERE COMMAND='Query' GROUP BY STATE ORDER BY COUNT(*) DESC;"
Sending data 11
Copying to tmp table 6
Sorting result 4
Waiting for table metadata lock 3
executing 2
Що це означає: Тимчасові таблиці і сортування відбуваються зараз. Саме там зазвичай і трапляються сплески пам’яті.
Рішення: Знайдіть проблемні запити і зменшіть потребу в сортуваннях/тимчасових таблицях (індекси, перепис запитів), і затисніть пер-потокові буфери.
Завдання 7: знайдіть найдовше виконувані запити, що зараз працюють
cr0x@server:~$ mysql -e "SELECT ID,USER,HOST,DB,TIME,STATE,LEFT(INFO,120) AS INFO FROM information_schema.PROCESSLIST WHERE COMMAND='Query' ORDER BY TIME DESC LIMIT 10;"
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
| ID | USER | HOST | DB | TIME | STATE | INFO |
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
| 7712 | app | 10.0.2.15:53342 | prod | 188 | Copying to tmp table | SELECT ... ORDER BY created_at DESC LIMIT 1000 |
| 7761 | app | 10.0.2.15:53388 | prod | 163 | Sorting result | SELECT ... JOIN ... WHERE ... |
| 7799 | app | 10.0.2.18:40210 | prod | 142 | Waiting for table metadata lock | ALTER TABLE ... |
+------+-------+-----------------+------+------|-------------------------------+----------------------------------------------------------+
Що це означає: «Шторм» видно. Є довгі виконання і навіть DDL, що чекає на блокування.
Рішення: Розгляньте можливість акуратно вбити найгірших виконавців, і винесіть онлайн-зміни схеми з пікових годин. Виправте шаблони запитів після стабілізації.
Завдання 8: перевірте змінні, чутливі до пам’яті на потік
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('sort_buffer_size','join_buffer_size','read_buffer_size','read_rnd_buffer_size','thread_stack','tmp_table_size','max_heap_table_size');"
+--------------------+----------+
| Variable_name | Value |
+--------------------+----------+
| join_buffer_size | 4194304 |
| max_heap_table_size| 67108864 |
| read_buffer_size | 2097152 |
| read_rnd_buffer_size| 4194304 |
| sort_buffer_size | 8388608 |
| thread_stack | 262144 |
| tmp_table_size | 67108864 |
+--------------------+----------+
Що це означає: Хтось надто роздув буфери. При високій конкурентності це рецепт OOM.
Рішення: Зменшіть їх до консервативних значень, якщо немає доказів користі. Великі буфери допомагають окремим запитам; вони шкодять загальній стабільності.
Завдання 9: перевірте розмір InnoDB buffer pool щодо RAM
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 3221225472 |
+-------------------------+------------+
Що це означає: Це 3GiB. На хості з ~4GiB це залишає майже нічого для підключень, тимчасових таблиць і ОС.
Рішення: Зменшіть buffer pool на малих машинах (часто 50–70% RAM залежно від схем співрозміщення та swap). Потім переоцініть cache hit rate і продуктивність.
Завдання 10: перевірте, чи тимчасові таблиці виливаються на диск (підказка про продуктивність і пам’ять)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+---------+
| Variable_name | Value |
+-------------------------+---------+
| Created_tmp_disk_tables | 183492 |
| Created_tmp_files | 2281 |
| Created_tmp_tables | 512044 |
+-------------------------+---------+
Що це означає: Значна кількість тимчасових таблиць спливає на диск. Це може уповільнювати запити, викликаючи накопичення підключень; пам’ять також може пікувати перед спливом на диск.
Рішення: Виправляйте запити/індекси перш за все. Не підвищуйте tmp_table_size/max_heap_table_size на малому сервері без розуміння — це підвищує ризик пікової пам’яті.
Завдання 11: перевірте thread cache і створення потоків
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'thread_cache_size'; SHOW GLOBAL STATUS LIKE 'Threads_created'; SHOW GLOBAL STATUS LIKE 'Connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 50 |
+-------------------+-------+
+-----------------+--------+
| Variable_name | Value |
+-----------------+--------+
| Threads_created | 924812 |
+-----------------+--------+
+--------------+---------+
| Variable_name| Value |
+--------------+---------+
| Connections | 985004 |
+--------------+---------+
Що це означає: Якщо Threads_created зростає швидко відносно Connections, у вас churn підключень і недостатнє кешування.
Рішення: Виправте додаток, щоб повторно використовувати підключення. Налаштуйте thread_cache_size помірно, але не дозволяйте йому маскувати поганий патерн підключень.
Завдання 12: перевірте тайм-аути, що контролюють накопичення простаючих підключень
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout';"
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| wait_timeout | 28800 |
+---------------+--------+
+-------------------+--------+
| Variable_name | Value |
+-------------------+--------+
| interactive_timeout| 28800 |
+-------------------+--------+
Що це означає: Вісім годин. На малих серверах із завантаженими додатками це часта причина накопичення Sleep-підключень.
Рішення: Зменшіть тайм-аути (для неінтерактивних користувачів) і примусьте розміри пулів. Якщо вам справді потрібні довгоживучі підключення, потрібна більша машина або менше таких підключень.
Завдання 13: перевірте відкриті файли та тиск на кеш таблиць (вторинний симптом)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'table_open_cache'; SHOW GLOBAL STATUS LIKE 'Open_tables'; SHOW GLOBAL STATUS LIKE 'Opened_tables';"
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| table_open_cache | 4000 |
+------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables | 3980 |
+---------------+-------+
+----------------+---------+
| Variable_name | Value |
+----------------+---------+
| Opened_tables | 784920 |
+----------------+---------+
Що це означає: Якщо Open_tables тримається біля table_open_cache і Opened_tables швидко зростає, ви часто відкриваєте таблиці. Це додає накладні витрати і латентність, опосередковано збільшуючи конкурентність підключень.
Рішення: Налаштуйте кеш таблиць і OS-ліміти на файли, але не плутайте це з основним драйвером OOM. Спочатку виправляйте множники пам’яті.
Завдання 14: перевірте ліміти systemd і обмеження cgroup пам’яті
cr0x@server:~$ systemctl show mysql -p MemoryMax -p TasksMax -p LimitNOFILE
MemoryMax=infinity
TasksMax=4915
LimitNOFILE=1048576
Що це означає: Тут вас не обмежує systemd memory limit. Якщо б MemoryMax було встановлено, БД могла б вбиватися через обмеження cgroup, а не системний OOM.
Рішення: Якщо ви працюєте в контейнерах або строгих cgroup, розраховуйте для цього ліміту, а не для RAM хоста, і розгляньте налаштування oom_score_adj і резервацій пам’яті уважно.
Три корпоративні міні-історії з OOM-трясок
Міні-історія №1: інцидент через неправильне припущення
У них була мала продакшн-середа для «некритичного» внутрішнього інструменту. Інструмент став критичним, коли на нього повісили звітність по зарплатам. БД жила на 4GB VM і «була нормальною місяцями». Це фраза, яка завжди передує падінню.
Розробник помітив іноді помилки Too many connections під час навантаження і змінив налаштування: max_connections підняли з кількох сотень до тисячі. Припущення було просте: більше підключень — менше помилок. Ніхто не перевірив математичні розрахунки по пам’яті. Ніхто не спитав, що роблять простаючі підключення.
Наступного понеділка невинний звітний запит став повільнішим через відсутній індекс. Запити накопичилися, додаток відкрив більше сесій, і БД підпорядкувалася, бо їй дозволили. Пам’ять росла, і ядро вбило mysqld. Інструмент впав, звітність провалилася, і багато людей дізналися, що таке «зона ураження».
Постмортем був не драматичним. Він був гіршим: нудний. «Ми припустили, що помилки підключення — причина, а не симптом». Вони виправили запит, впровадили пул із жорстким лімітом, зменшили max_connections і скоротили тайм-аути. Та сама VM пережила майбутні сплески з тим самим RAM. Різниця була в тому, що база більше не добровільно ставала стрес-мішком для додатку.
Міні-історія №2: оптимізація, що відбилася
Команда гналася за латентністю на клієнтському API. Вони читали, що збільшення sort_buffer_size і join_buffer_size може пришвидшити складні запити. Вони підняли обидва значно, протестували одиночним бенчмарком і результат був чудовий. Зміна пройшла.
Під реальною продуктивною конкурентністю вийшла інша історія. Кілька робочих процесів додатку запускали схожі звітні запити одночасно, кожен виділяв великі буфери. Використання пам’яті не просто зростало — воно піднімалося як сходи при збільшенні конкурентності. Вони не вдарили по межі продуктивності; вони вдарили по OOM killer.
Погане те, що падіння було випадковим. Іноді патерн навантаження складався так, що буфери виділялися одночасно; іноді ні. Це виглядало як «випадковий збій» або «витік». Вони витратили дні на вивчення графіків і звинувачували шар зберігання.
Виправлення було принизливим: відкотити розміри буферів ближче до дефолту, додати індекс, що прибрав сортування, і обмежити конкурентність на рівні пулу, щоб важкі запити не влаштовували «штампування». Загальна продуктивність покращилася, бо база перестала трястися, а додаток перестав повторювати запити. Мораль не в «ніколи не тюнити буфери». Вона в «ніколи не тюнити пер-потокові буфери тестом з одним потоком і не вважати роботу зробленою».
Міні-історія №3: нудна, але правильна практика, що врятувала день
Інша організація тримала багато маленьких інстансів MySQL/MariaDB, кожен під сервіс. Нічого складного. Трюк був у процесі: у кожної інстанції був легкий «лист місткості», занесений у репозиторій поруч з конфігом. Він містив RAM, розмір buffer pool, припущення про пер-потокові буфери і безпечний ліміт підключень. Це не було ідеально. Це було послідовно.
Однієї ночі сплеск трафіку влучив у сервіс низької пріоритетності, що ділив ноду з іншими речами. Пул сервісу зламався і намагався відкрити значно більше підключень, ніж зазвичай. БД відмовилася після досягнення max_connections, і додаток почав помилятися. Неприємно, помітно і відновлювано.
Через те, що база не OOM-нулася, інші сервіси на хості залишилися живими. On-call встиг як людина: зменшити розмір пулу, перезапустити кілька воркерів і випустити фікс конфігу. Ніякого відновлення файлової системи. Ніяких циклів краху. Ніякого пошкодження стану.
Ось суть: чиста відмова на межі БД часто краща, ніж дозволяти ОС вистрілити в базу. Нудна практика — документовані ліміти та обережні капи — перетворила потенційний інцидент кількох сервісів у єдине тьху-випадкове гальмування одного сервісу.
Поширені помилки: симптом → корінь проблеми → виправлення
1) Симптом: «Too many connections» помилки, потім OOM після підвищення max_connections
Корінь: Накопичення підключень через повільні запити або некоректний пул; підвищення max_connections збільшує експозицію пам’яті.
Виправлення: Тримайте max_connections в межах бюджету пам’яті. Впровадьте пулювання з жорстким лімітом. Знайдіть і виправте повільний запит або конкуренцію за блокування, що спричинила накопичення.
2) Симптом: Сотні простаючих підключень; пам’ять все одно висока
Корінь: Довгий wait_timeout плюс надмірно великі пул(и), плюс накладні витрати сесій/потоків.
Виправлення: Зменшіть wait_timeout (для користувачів додатка), встановіть ліміт пулу на інстанцію і зменшіть простаючі підключення. Якщо вам потрібні багато довгоживучих сесій — збільшуйте сервер.
3) Симптом: OOM відбувається тільки під час звітів або місячних батчів
Корінь: Конкурентність пам’ятенасичених запитів (сортування, джоїни, тимчасові таблиці) запускає пер-потокові сплески.
Виправлення: Налаштуйте запити й індекси, щоб зменшити сортування/тимчасові таблиці, обмежте конкурентність цих завдань і тримайте пер-потокові буфери консервативними. Розгляньте виконання батчів на окремому репліці.
4) Симптом: OOM відбувається після «оптимізації продуктивності» з великими буферами
Корінь: Розміри пер-потокових буферів, помножені на активні сесії, перевищують RAM.
Виправлення: Поверніть розміри буферів до розумних значень; вимірюйте з продукційною конкурентністю. Краще покращувати запити/індекси, ніж надмірно роздувати буфери.
5) Симптом: БД перезапускається в циклі; в логах нічого очевидного
Корінь: Вбита OOM до того, як встигає записати корисні логи, або вбита обмеженням cgroup/container memory.
Виправлення: Перевірте журнали ядра і cgroup-ліміти. Зменшіть пам’яттєвий слід і/або підніміть ліміт контейнера. Додайте swap там, де доцільно.
6) Симптом: Використання пам’яті повільно зростає тижнями; іноді крахи
Корінь: Часто це не витік, а зміна робочого навантаження: більше одночасних сесій, більші результати, зростання використання тимчасових таблиць або тиск фрагментації.
Виправлення: Слідкуйте за розподілом підключень, slow query logs і метриками тимчасових таблиць. Встановіть жорсткі ліміти конкурентності і виправляйте драйвери навантаження.
Чеклісти / покроковий план
Стабілізувати зараз (той самий день)
- Підтвердіть OOM killer через журнали ядра. Якщо це OOM — припиніть дебати.
- Зменшіть max_connections до безпечного числа на основі поточної RAM і розмірів буферів. Так, користувачі побачать помилки. Краще контрольовані помилки, ніж відновлення даних.
- Зменшіть пер-потокові буфери, якщо їх роздмухували (
sort_buffer_size,join_buffer_size, read buffers). - Зменшіть wait_timeout для користувачів додатка, щоб уникнути накопичення простаючих підключень.
- Увімкніть або перевірте пулювання підключень і встановіть жорсткі ліміти на інстанції.
- Вбивайте або переплановуйте найгірші запити лише якщо розумієте зону ураження (звітні запити зазвичай безпечні для зупинки; DDL/міграції складніші).
Зробити міцним (цього тижня)
- Захопіть базові метрики: max used connections, активні vs sleep, тимчасові таблиці, кількість повільних запитів.
- Увімкніть лог повільних запитів з адекватним порогом для вашого навантаження і перегляньте основних порушників.
- Додайте відсутні індекси для запитів, що викликають сортування і тимчасові таблиці.
- Задайте «контракт місткості» у конфігу: документований розмір buffer pool, пер-потокові буфери і безпечний ліміт підключень.
- Вирішіть питання swap: невеликий swap на малих серверах часто підвищує виживаність. Моніторьте swap-in; не дозволяйте йому стати постійним.
Запобігти повторенню (цього кварталу)
- Тестуйте навантаження з продакшн-подібною конкурентністю, а не одиночним бенчмарком.
- Розділяйте OLTP і звіти: репліки, виділені інстанції або принаймні обмеження конкурентності на звіти.
- Автоматизуйте охоронні механізми: алерти на Threads_connected, Max_used_connections, що наближаються до максимуму, і на тиск пам’яті.
- Регулярно переглядайте налаштування пулу з командами додатків так само ретельно, як ви переглядаєте правила фаєрволу: явно і регулярно.
ЧаПи
1) Чи слід встановлювати max_connections у відповідності з кількістю потоків додатку?
Ні. Вам слід налаштовувати максимуми пулів додатка так, щоб сума всіх пулів була нижче безпечного ліміту БД, з резервом для адмінів і пакетних задач.
2) Чи безпечніша MariaDB ніж MySQL щодо пам’яті?
Не по суті. Обидві можуть OOM-нути однаково: занадто багато потоків і занадто велика пер-потокова пам’ять. Безпека походить від правильного розміру та обмежень, а не від логотипу.
3) Чому БД OOM-иться, коли більшість підключень сплячі?
Сплячі підключення все одно споживають пер-сесійну пам’ять і потоки. Також популяція «сплячих» може приховувати меншу кількість активних підключень, що виконують пам’ятенасичені операції й викликають сплеск.
4) Якщо я зменшу max_connections, хіба я не отримаю більше помилок?
Так, але це контрольовані помилки. БД з капом відмовляє швидко й передбачувано. БД, вбита OOM, ламає все, включно з завданнями відновлення, і може викликати каскадні повтори.
5) Чи добре додавати swap для баз даних?
На малих серверах часто так — якщо це зроблено свідомо. Невеликий swap може поглинути короткі сплески і запобігти OOM. Якщо ви бачите тривале використання swap у нормальному навантаженні — ви недопроцідуєте або неправильно налаштовані.
6) Які змінні найбільше множать пам’ять на підключення?
Типові винуватці: sort_buffer_size, join_buffer_size і ліміти пам’яті тимчасових таблиць (tmp_table_size/max_heap_table_size). Вони не завжди виділяються повністю, але визначають найгірший випадок при конкурентності.
7) Що краще для виправлення «Too many connections»: пулування чи підняття max_connections?
Пулювання, майже завжди. Підняття max_connections іноді доречне після того, як ви зробили розрахунки пам’яті і довели, що маєте запас, але це не перший крок на маленькому обладнанні.
8) Як зрозуміти, чи повільні запити викликають накопичення підключень?
Подивіться на TIME/STATE у processlist, логи повільних запитів і чи активні запити зосереджені навколо тимчасових таблиць, сортувань або очікувань блокувань. Поступове зростання кількості підключень разом із затримкою — явна ознака.
9) Чи слід зменшити innodb_buffer_pool_size, щоб зупинити OOM?
Якщо buffer pool відтісняє все інше на маленькому сервері — так. Продуктивність може впасти, але стабільна база швидша за мертву. Потім покращуйте запити і навантаження, щоб повернути продуктивність.
10) Чи «просто запхнути БД у контейнер» вирішить це?
Контейнери не розв’язують математичну проблему пам’яті. Вони її контролюють. Якщо ви встановите низький ліміт пам’яті, вас чекають швидші вбивства, якщо не налаштувати буфери і ліміти підключень відповідно.
Висновок: наступні кроки, які ви можете зробити цього тижня
Якщо ваш малий сервер падає — припиніть ставитися до max_connections як до ручки «задоволення клієнта». Це зобов’язання щодо пам’яті. Зробіть його консервативним.
Виконайте ці кроки в порядку:
- Доведіть, що це OOM через журнали ядра і виміряйте RSS/потоки, поки БД жива.
- Правильно розподіліть глобальну пам’ять (особливо
innodb_buffer_pool_size), щоб ОС і підключення мали запас повітря. - Обмежте max_connections до того, що сервер реально може витримати, а не до того, що додаток може спамити.
- Тримайте пер-потокові буфери консервативними, якщо немає вагомих доказів і тестів з продакшн-конкурентністю.
- Виправте патерн підключень через пулювання і адекватні тайм-аути. Більшість проблем «місткості БД» — це насправді проблеми поведінки додатка в костюмі бази даних.
- Усуньте повільні запити, що спричиняють накопичення і тимчасові таблиці. Менше одночасних запитів — найдешевше апгрейд пам’яті.
Коли наступний сплеск трапиться, ви хочете, щоб база «сказала» «ні» рано і чітко, а не щоб ядро її вбило. Це не песимізм. Це експлуатація.