Ви купили VPS з 2 ГБ, бо «достатньо для невеликого додатка». Потім база даних почала поводитися як крихкий кімнатний вазон: трохи більше з’єднань, важкий запит — і вона падає, іноді забираючи з собою весь вузол. Вам не потрібні героїчні заходи. Потрібні здорові бюджети пам’яті, передбачуваний I/O і налаштування, що відмовляють поступово, а не вибухово.
Це польовий посібник для запуску або MySQL, або MariaDB на маленькій машині без перетворення OOM-killer ядра на вашого головного DBA. Я висловлюю думку прямо, бо продакшн сам по собі прямолінійний: йому байдуже на ваш бенчмарк, якщо сервер перезавантажується о 3 ранку.
Вибір MySQL чи MariaDB на 2 ГБ: справжні критерії рішення
На VPS з 2 ГБ питання «MySQL чи MariaDB» менше про ідеологію й більше про оперативну зручність: дефолти, пакети та які підводні міни ви готові обслуговувати.
Моя рекомендація (з думкою й застереженнями)
- Якщо вам потрібна передбачувана поведінка в різних хмарах і керованих сервісах: обирайте MySQL 8.0. Він відповідає тому, що запускають більшість хостингів, і операційна експертиза широка.
- Якщо ви самостійно хостите і хочете органи керування, корисні для маленьких машин: MariaDB може бути дружнішим, особливо з thread pool (в деяких збірках) і більш раціональним пакуванням у багатьох дистрибутивах.
- Якщо ви покладаєтесь на фічі MySQL 8 (окремі JSON- та реплікаційні поведінки, новіший оптимізатор, офіційні інструменти): обирайте MySQL, налаштуйте консервативно й живіть далі.
- Якщо ви хочете мінімізувати сюрпризи під час оновлень: беріть те, що найкраще підтримує ваш дистрибутив, і тримайте це зафіксованим. На 2 ГБ сюрприз дорого обходиться.
За продуктивністю будь-який двигун заповнить вашу маленьку CPU і повільний диск перш ніж здобуде призи. Справжній ризик — пам’ять: база даних професійно витрачає все, що ви їй даєте, плюс те, що ви забули, що вона може взяти.
Жарт №1: VPS з 2 ГБ і нетюнованою базою даних — як ручний багаж: здається, що все влізе, аж поки блискавка на блискавці не скаже, що ви її образили.
Цікаві факти й контекст, що мають значення
Це не факти для вікторини. Це ті, що пояснюють, чому налаштування різняться, чому існують дефолти і чому «просто скопіювати цей my.cnf» — пастка.
- MariaDB відгалузилася від MySQL після придбання Sun компанією Oracle (2010): це відгалуження було не тільки політичним — воно привело до відмінних дефолтів, фіч і іноді різного трактування «сумісності».
- MySQL 5.6 зробив InnoDB головним вибором для серйозних навантажень: перехід від налаштувань ери MyISAM до InnoDB-центричної оптики змінив уявлення про те, що «безпечне».
- MySQL 8.0 повністю прибрав query cache: старі «гайди по продуктивності» все ще радять його крутити; у MySQL 8 цих ручок немає, а в MariaDB вони можуть ще й нашкодити.
- Інструмент продуктивності — буферний пул InnoDB: правило «дайте 80% RAM» поширилось на виділені бази — на 2 ГБ VPS з іншими процесами це необачно.
- Thread pooling пішов різними шляхами: реалізації MariaDB широко використовуються; історія MySQL залежить від видання та версії. На малих ядрах обробка потоків впливає на хвостову латентність більше, ніж ви думаєте.
- Логи InnoDB / redo еволюціонували: налаштування ємності redo та поведінки flush важливі, бо маленькі VPS-диски часто мережеві та стрибкоподібні.
- Дефолти пакування Ubuntu/Debian змінювались з часом: у різних версіях ви побачите різні базові конфіги, отже «стоковий MySQL» — це не одна і та сама річ.
- OOM killer Linux не дбає про ваше SLA: він просто вбиває щось. Часто mysqld. Іноді ваш додаток. У будь-якому випадку ваш інцидент-канал загориться.
Ментальна модель для 2 ГБ VPS: куди насправді йде пам’ять
На маленьких хостах питання не «яке налаштування найшвидше?» — а «яку максимальну шкоду це налаштування може зробити при піковій паралельності?» Це зрушення у мисленні запобігає крахам.
Плануйте RAM так само, як плануєте сон під час чергувань
З 2 ГБ RAM у вас немає повних 2 ГБ для MySQL/MariaDB. У вас є:
- Ядро + сторінкова кеш-пам’ять + slab: зазвичай кілька сотень МБ, більше під навантаженням I/O.
- Системні сервіси (sshd, journald, cron, агент моніторингу): 50–200 МБ.
- Ваше виконання додатка (PHP-FPM, Node, Java, Python workers): від «керовано» до «чому він займає 900 МБ».
- База даних: все, що залишилось, і вона намагатиметься взяти все.
На виділеному DB-only 2 ГБ VPS (без додатка) можна сильніше завантажувати БД. На спільному app+DB вузлі (часто у дешевих стеків) треба резервувати простір, щоб ядро не панікувало й не починало вбивати процеси.
Два види пам’яті бази: глобальна та на з’єднання
Більшість гайдів говорять про глобальні буфери (buffer pool, log buffers). Справжній тихий вбивця — пам’ять на одне з’єднання:
- sort buffers
- join buffers
- read buffers
- tmp tables (в пам’яті поки не викинуться на диск)
- thread stacks
Кожен виглядає дрібним. Помножте на 200 з’єднань — і ви маєте RAM-санту. Саме тому «просто збільшити max_connections» — одне з найдорожчих рішень, яке можна зробити.
Диск — інша вісь: маленьке сховище часто — «театральний I/O»
Ваш диск VPS може бути мережевим, зберігати бурсти, тротлитись або бути на overcommitted сховищі. Бази даних безжально виявляють ці обмеження. Якщо ви налаштуєте пам’ять, але ігноруєте I/O (частота fsync, очищення брудних сторінок), отримаєте класичні симптоми:
- швидко — поки не стало повільно
- випадкові стрибки латентності
- зависання під час контрольних точок
- реплікаційні лаги, що з’являються «без причини»
Коротка цитата про надійність (перефразована думка)
«Надія — не стратегія.» — генерал Г. Норман Шварцкопф (перефразована думка, часто використовувана в інженерії й операціях)
Це стосується й тут: не можна сподіватися, що VPS не зіштовхнеться з піковою паралельністю; треба задати межі, щоб пік не вбив систему.
Безпечні профілі налаштування (копіюйте/вставляйте) для 2 ГБ RAM
Ці профілі орієнтовані на «не падає» перш за все, «достатньо швидко» по-друге. Вони припускають InnoDB (ви повинні бути на InnoDB, якщо у вас немає дуже конкретної причини інакше).
Перед копіюванням/вставкою: визначте тип розгортання
- DB-only VPS: база може використовувати більшу частину пам’яті, але все одно залиште запас для ОС.
- App + DB на одному VPS: база має бути скромною, а ліміти з’єднань — суворими.
Також визначте характеристики сховища:
- SSD-подібне зі стабільним fsync: можна дозволити більш агресивні налаштування flushing.
- Мережеве/дешеве блочне сховище: очікуйте проблем з fsync; налаштовуйте, щоб уникнути стрибків.
Профіль A: «Додаток + БД разом» (найпоширеніший дешевий VPS стек)
Мета: MySQL 8.0 або MariaDB 10.6+ на 2 ГБ VPS, де також працює сервер додатка. Це профіль для випадків, коли ви краще приглушите продуктивність, ніж викличете відсіювання всього інженерного складу.
cr0x@server:~$ sudo tee /etc/mysql/conf.d/99-vps-2gb-safe.cnf >/dev/null <<'EOF'
[mysqld]
# Core safety limits
max_connections = 60
skip_name_resolve = ON
wait_timeout = 60
interactive_timeout = 300
# InnoDB: keep it small and stable
innodb_buffer_pool_size = 512M
innodb_buffer_pool_instances = 1
innodb_log_buffer_size = 16M
innodb_redo_log_capacity = 256M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_io_capacity = 200
innodb_io_capacity_max = 400
# Reduce per-connection memory blowups
tmp_table_size = 32M
max_heap_table_size = 32M
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
thread_stack = 256K
# Keep table cache reasonable
table_open_cache = 800
open_files_limit = 65535
# Observability that pays rent
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = OFF
EOF
Чому це працює: buffer pool 512M не виграє бенчмарки, але не задушить ваш додаток. Суворі таймаути зменшують утримання простих з’єднань. Помірні tmp/sort/join буфери обмежують пам’ять на з’єднання. Ви обираєте контрольований збиток.
Профіль B: «Тільки БД, 2 ГБ VPS» (більше простору, але все ще не іграшка)
Якщо на машині працює тільки база (плюс моніторинг), можна дати InnoDB трохи більше ресурсів.
cr0x@server:~$ sudo tee /etc/mysql/conf.d/99-vps-2gb-dbonly.cnf >/dev/null <<'EOF'
[mysqld]
max_connections = 120
skip_name_resolve = ON
wait_timeout = 120
innodb_buffer_pool_size = 1G
innodb_buffer_pool_instances = 1
innodb_log_buffer_size = 32M
innodb_redo_log_capacity = 512M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_io_capacity = 300
innodb_io_capacity_max = 600
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
thread_stack = 256K
table_open_cache = 1200
open_files_limit = 65535
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
EOF
Жорстке правило: не ставте buffer pool 1.4G на 2 ГБ VPS і не дивуйтесь, коли воно почне свопитись. Ви будете на краю, а край — це місце інцидентів.
Специфічні для MariaDB ручки, що варто розглянути на малих машинах
MariaDB часто дає додаткові операційні важелі. Використовуйте їх обережно; «більше ручок» не означає «краще».
- Thread pool може зменшити треш потоків при високій кількості з’єднань, покращуючи хвостову латентність. Якщо його вмикаєте, тримайте max_connections реалістичним.
- Aria та старі движки існують; не використовуйте їх випадково для таблиць з великими записами без розуміння режимів відмов.
Реалії MySQL на малих машинах
- Дефолти MySQL 8 загалом розумні, але «розумні» передбачають, що ви не запускаєте 200 з’єднань на 2 ГБ.
- Не гнатесь за видаленими фічами типу query cache. Якщо гайд каже налаштувати його для MySQL 8 — закрийте вкладку.
Про налаштування надійності (аргумент fsync)
innodb_flush_log_at_trx_commit=1 — налаштування за замовчуванням для надійності. На поганому сховищі воно може бути болючим. Встановлення в 2 зменшує частоту fsync і підвищує пропускну здатність, але ви погоджуєтесь втратити до ~1 секунди транзакцій при краші. Якщо ви на одному VPS без реплікації і цінуєте дані — не «оптимізуйте» надійність лише тому, що блог порадив. Якщо навантаження може це терпіти (сесії, кеші, похідні дані) — це бізнес-рішення. Запишіть його.
Жарт №2: Вимикати durability, щоб «покращити продуктивність», — це як зняти пожежний датчик, бо він дратує.
Практичні завдання: команди, виводи та рішення
Ці завдання призначені для моменту, коли ви дивитесь на 2 ГБ VPS і задаєтеся питанням, чому він повільний, зависає або перезавантажується. Кожне завдання містить: команду, реалістичний вивід, що він означає, і рішення, яке ви приймаєте.
Завдання 1: Підтвердіть, що ви справді встановили (і не налаштовуєте неправильний демон)
cr0x@server:~$ mysql --version
mysql Ver 8.0.36-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
Значення: Версія показує, які фічі доступні і які міфи по налаштуванню ігнорувати. У MySQL 8 нема query cache; у MariaDB інші дефолти.
Рішення: Прив’язуйте поради з налаштування до вашої версії. Якщо ви на MariaDB, також перевірте точну мінорну версію, бо дефолти змінюються.
Завдання 2: Підтвердіть flavor сервера зсередини SQL
cr0x@server:~$ mysql -e "SELECT VERSION(), @@version_comment;"
+-----------+------------------------------+
| VERSION() | @@version_comment |
+-----------+------------------------------+
| 8.0.36 | (Ubuntu) |
+-----------+------------------------------+
Значення: Це підтверджує, що ви під’єднані до того сервера, який вважаєте за справжній. Звучить очевидно. Перестає бути очевидним під час міграцій, failover і «тимчасових» контейнерів налагодження.
Рішення: Якщо version_comment не відповідає очікуванню — зупиніться й знайдіть реальний endpoint. Налаштовувати неправильний інстанс — це особливий вид марної витрати часу.
Завдання 3: Перевірте RAM, swap та чи ви вже живете ризиковано
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 1.9Gi 1.2Gi 140Mi 42Mi 620Mi 420Mi
Swap: 1.0Gi 180Mi 820Mi
Значення: Низьке «available» разом із використанням swap означає реальний тиск. Свопінг на DB-хості часто корелює зі стрибками латентності та зависаннями.
Рішення: Якщо swap ненульовий і латентність БД погана — зменшіть споживання пам’яті БД (buffer pool, кількість з’єднань, буфери на з’єднання) перед тим, як «оптимізувати запити».
Завдання 4: Доведіть участь OOM killer (або виключіть її)
cr0x@server:~$ sudo dmesg -T | tail -n 20
[Mon Dec 30 09:11:02 2025] Out of memory: Killed process 1421 (mysqld) total-vm:3074824kB, anon-rss:1320440kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:3140kB oom_score_adj:0
[Mon Dec 30 09:11:02 2025] oom_reaper: reaped process 1421 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Значення: Це не «MySQL впав». Це «Linux його вбив». Виправлення — це додаткові ресурси або обмеження пам’яті, а не реіндексація.
Рішення: Встановіть жорсткі межі: зменшіть max_connections, зменшіть буфери на з’єднання і підберіть innodb_buffer_pool_size. Розгляньте додавання swap лише як подушку останньої надії, а не як план.
Завдання 5: Знайдіть, хто прямо зараз поглинає пам’ять
cr0x@server:~$ ps -eo pid,comm,rss,pmem --sort=-rss | head
PID COMMAND RSS %MEM
1421 mysqld 1184500 60.2
911 php-fpm8.1 210300 10.7
947 php-fpm8.1 185120 9.4
612 node 121800 6.1
Значення: RSS показує поточну resident-пам’ять. Це знімок, але він каже, чи БД справді джерело тиску.
Рішення: Якщо mysqld значно найбільший — налаштуйте пам’ять БД. Якщо додаток більше — виправте модель процесів додатка (кількість воркерів, витік пам’яті, кешування).
Завдання 6: Підтвердіть innodb_buffer_pool_size і ключові змінні пам’яті
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_buffer_pool_size','max_connections','tmp_table_size','max_heap_table_size','sort_buffer_size','join_buffer_size');"
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 536870912 |
| join_buffer_size | 2097152 |
| max_connections | 60 |
| max_heap_table_size | 33554432 |
| sort_buffer_size | 2097152 |
| tmp_table_size | 33554432 |
+-------------------------+-----------+
Значення: Багато змінних мають значення в байтах. Buffer pool на 512MB відповідає Профілю A.
Рішення: Якщо ці значення не відповідають бажаному профілю — ви виправляєте вигадку. Перевірте порядок завантаження конфиґів, видаліть конфліктні фрагменти, перезапустіть чисто.
Завдання 7: Оціни реальну паралельність з’єднань і чи max_connections — фантазія
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 18 |
+-------------------+-------+
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| Max_used_connections| 54 |
+---------------------+-------+
Значення: Історично ви досягали 54 паралельних з’єднань. З max_connections=60 ви близькі до стелі.
Рішення: Якщо max_used близький до ліміту, або (a) підніміть його трохи й зменшіть буфери на з’єднання, або (b) виправте пулінг у додатку, щоб не потрібно було 60 реальних з’єднань.
Завдання 8: Перевірте переливи тимчасових таблиць (класичний тихий вбивця продуктивності)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 1842 |
| Created_tmp_files | 211 |
| Created_tmp_tables | 9321 |
+-------------------------+-------+
Значення: Тимчасові таблиці на диск відбуваються. Частково це нормально; багато — свідчить про сорти/групування, що не вміщаються в пам’ять або відсутність індексів.
Рішення: Якщо кількість Created_tmp_disk_tables велика відносно Created_tmp_tables — не просто підвищуйте tmp_table_size на 2 ГБ. Виправте запит, додайте індекси або прийміть disk spills і налаштуйте I/O замість цього.
Завдання 9: Перевірте, чи buffer pool надто малий (або якраз той)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+---------+
| Variable_name | Value |
+---------------------------------------+---------+
| Innodb_buffer_pool_read_requests | 9284432 |
| Innodb_buffer_pool_reads | 188322 |
+---------------------------------------+---------+
Значення: Reads vs read_requests дає уявлення про cache misses. Деякі промахи очікувані. Дуже високе співвідношення промахів свідчить про замалий buffer pool або про навантаження, що погано кешується.
Рішення: Якщо промахів багато й у вас є пам’яті в запасі (і ви не свопите) — обережно збільшуйте buffer pool. Якщо ви обмежені пам’яттю — виправляйте запити й індекси.
Завдання 10: Швидко знайдіть головні класи очікувань (знімок статусу InnoDB)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-30 09:22:11 0x7f2d2c1ff700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 6 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 2199 srv_active, 0 srv_shutdown, 12195 srv_idle
srv_master_thread log flush and writes: 14394
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 31212
OS WAIT ARRAY INFO: signal count 30009
RW-shared spins 0, rounds 0, OS waits 12
RW-excl spins 0, rounds 0, OS waits 48
------------
TRANSACTIONS
------------
Trx id counter 1149091
History list length 188
Значення: Це швидка перевірка реальності. Високі OS waits в семафорах можуть вказувати на контеншн; великий history list length — на відставання purge (часто через довгі транзакції).
Рішення: Якщо history list length росте й тримається високим — шукайте довгі транзакції. Якщо semaphore waits стрибають під навантаженням — зменшіть паралельність, налаштуйте запити і подумайте про thread pooling (особливо в MariaDB).
Завдання 11: Знайдіть довгі транзакції, що тримають undo/purge зайнятими
cr0x@server:~$ mysql -e "SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started;"
+--------+---------------------+---------------------+----------------------------------+
| trx_id | trx_started | trx_mysql_thread_id | trx_query |
+--------+---------------------+---------------------+----------------------------------+
| 1148802| 2025-12-30 09:04:01 | 312 | SELECT * FROM orders WHERE ... |
+--------+---------------------+---------------------+----------------------------------+
Значення: Одна транзакція, що працює з 09:04, може утримувати undo сегменти і заважати purge, впливаючи на записи і призводячи до роздування.
Рішення: Виправте поведінку додатка (уникати інтерактивних транзакцій), додайте індекси і встановіть адекватні таймаути. Іноді треба вбити транзакцію, але профілактика дешевша.
Завдання 12: Переконайтеся, що slow query logging увімкнено і корисне
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+
Значення: Slow logs — ваш бюджетний профайлер. На 2 ГБ VPS ви не маєте права гадати.
Рішення: Якщо slow log вимкнений — увімкніть. Якщо він занадто галасливий — тимчасово підвищіть long_query_time, але не вимикайте лог.
Завдання 13: Читайте slow log як оператор, а не турист
cr0x@server:~$ sudo tail -n 25 /var/log/mysql/slow.log
# Time: 2025-12-30T09:27:14.123456Z
# User@Host: app[app] @ 10.0.0.15 []
# Query_time: 3.214 Lock_time: 0.002 Rows_sent: 50 Rows_examined: 982341
SET timestamp=1735550834;
SELECT * FROM events WHERE user_id=123 ORDER BY created_at DESC LIMIT 50;
Значення: Rows_examined ~ 1M для LIMIT 50 — класичний випадок відсутнього/поганого індексу. Lock_time малий; це не «блокування», а «робота».
Рішення: Додайте/підправте індекс (ймовірно (user_id, created_at)) або перепишіть запит, щоб уникнути сканування. Не чіпайте розміри буферів, щоб «виправити» це.
Завдання 14: Перевірте, що ви випадково не на подвійній кеші (погано з O_DIRECT mismatches)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_method';"
+---------------------+----------+
| Variable_name | Value |
+---------------------+----------+
| innodb_flush_method | O_DIRECT |
+---------------------+----------+
Значення: O_DIRECT зменшує подвійне буферування між InnoDB buffer pool і сторінковою кеш-пам’яттю ОС. На малій RAM це допомагає робити використання пам’яті передбачуваним.
Рішення: Якщо це не O_DIRECT і у вас є тиск пам’яті — подумайте про зміну (спочатку тестуйте; деякі сторижі поводяться дивно).
Завдання 15: Перевірте відкриті таблиці і тиск на дескриптори файлів
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Open_tables'; SHOW VARIABLES LIKE 'table_open_cache';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables | 790 |
+---------------+-------+
+------------------+------+
| Variable_name | Value|
+------------------+------+
| table_open_cache | 800 |
+------------------+------+
Значення: Якщо Open_tables постійно досягає ліміту кеша, MySQL часто відкриває/закриває таблиці, що викликає оверхед і метаданну конкуренцію.
Рішення: Якщо у вас ще є дескрипторів файлів і пам’яті в запасі — помірно підніміть table_open_cache. На 2 ГБ не ставте його 10 000 як шведський стіл.
Завдання 16: Перевірте симптоми I/O з боку ОС
cr0x@server:~$ iostat -xz 1 3
Linux 5.15.0-91-generic (server) 12/30/2025 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.21 0.00 6.44 22.12 0.00 53.23
Device r/s w/s rkB/s wkB/s await %util
vda 42.0 58.0 912.0 2310.0 48.30 92.10
Значення: await ~48ms і %util ~92% вказують, що диск — вузьке місце. Великий iowait підтверджує це.
Рішення: Якщо диск завантажений — припиніть крутити CPU-ручки. Зменшуйте write amplification (індекси, розмір транзакцій), налаштуйте redo capacity розумно і подумайте про апгрейд сховища або перенесення БД з вузла.
Швидкий план діагностики
Це «у вас 15 хвилин, щоб виглядати компетентно» робочий процес. Він упорядкований так, щоб швидко знайти вузьке місце на 2 ГБ VPS.
Перше: чи воно вмирає (OOM, рестарти або свопи)?
- Перевірте
dmesgна OOM killing. - Перевірте
free -hна активність swap і низьке available. - Перевірте, чи mysqld перезапускається (systemd журнали).
cr0x@server:~$ sudo journalctl -u mysql --since "1 hour ago" | tail -n 30
Dec 30 09:11:04 server systemd[1]: mysql.service: Main process exited, code=killed, status=9/KILL
Dec 30 09:11:04 server systemd[1]: mysql.service: Failed with result 'signal'.
Dec 30 09:11:06 server systemd[1]: mysql.service: Scheduled restart job, restart counter is at 3.
Рішення: Якщо є OOM/рестарти — на першому місці поставте обмеження пам’яті та ліміти з’єднань перед будь-якою мікрооптимізацією.
Друге: чи це диск (iowait, fsync-зависи, переливи тимчасових таблиць)?
iostat -xzдля await/%util.- Перевірте переливи тимчасових таблиць на диск.
- Перевірте тиск на redo/log flush.
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';"
+----------------------+--------+
| Variable_name | Value |
+----------------------+--------+
| Innodb_os_log_fsyncs | 192113 |
+----------------------+--------+
+-----------------------+-----------+
| Variable_name | Value |
+-----------------------+-----------+
| Innodb_os_log_written | 987654321 |
+-----------------------+-----------+
Рішення: Якщо диск — вузьке місце, зосередьтеся на виправленні запитів/індексів і скороченні записів. Не загортайте buffer pool так, щоб примусити своп — ви обміняєте один простій на інший.
Третє: чи це CPU/блокування (занадто багато потоків, contention)?
top/pidstatдля перегляду завантаженості CPU.- InnoDB статус на предмет semaphore waits і блокувань рядків.
- Processlist на предмет багатьох одночасних важких запитів.
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | head -n 15
Id User Host db Command Time State Info
311 app 10.0.0.15:41012 prod Query 12 Sending data SELECT * FROM events WHERE user_id=...
312 app 10.0.0.15:41014 prod Query 520 Updating UPDATE inventory SET ...
313 app 10.0.0.15:41015 prod Sleep 58 NULL
Рішення: Якщо бачите довгі оновлення і багато читачів у стані «Sending data» — скоріш за все проблема в індексації або дизайні транзакцій, а не в якійсь магічній конфігурації.
Три корпоративні міні-історії (бо невдачі вчать)
1) Інцидент через хибне припущення: «max_connections — просто ліміт, не налаштування пам’яті»
Вони тримали невеликий портал клієнтів на 2 ГБ VPS: веб, база, все в одному місці. Під сезонним піком портал не тільки сповільнився. Він почав перезавантажуватися. Команда думала, що то поганий кернел або «шумний сусід» на хості VPS.
Першим їхнім фіксом було класичне: підняти max_connections з 100 до 400, бо клієнти отримували «too many connections». Аутейдж став гіршим. Тепер сайт не тільки падав; він увійшов у цикл перезапусків бази й напівзавершених деплоїв.
Після інциденту ми пройшлися по обліку пам’яті. Кожному з’єднанню дозволяли виділяти кілька буферів. Під спайком з’єднання розгортались, деякі виконували сорти й join-и, кожне потихеньку захоплювало мегабайти. Помножте на сотні — отримаєте пам’ятну скелю. Ядро зробило своє: вбило mysqld.
Нудне, але правильне виправлення: жорстко обмежити з’єднання, примусити пулінг у додатку, зменшити буфери на з’єднання і тримати скромний innodb_buffer_pool_size. Потім, бо люди забувають, додайте алерти на Max_used_connections і використання swap.
Результат не був «швидшим» в сенсі бенчмарків. Він був стабільним. Портал деградував плавно під навантаженням замість того, щоб детонувати. У продакшні плавне деградування — це функція.
2) Оптимізація, що вилазить боком: «Зробимо tmp_table_size величезним, щоб позбутися диск-спилів»
Сервіс, що багато генерує звітів, страждав від повільних групувань. Хтось помітив багато Created_tmp_disk_tables і вирішив підняти tmp_table_size і max_heap_table_size драматично. На папері — менше disk temp tables означає менше I/O і швидші запити. Це спокуса.
Зміна працювала в тестах. Потім потрапила в прод. Використання пам’яті під час паралельних звітів пішло вгору. Не лінійно. Раптово. Сервіс не просто уповільнився; він впав. OOM killer став регулярним учасником стендапів.
Тонкий момент: тимчасові таблиці в пам’яті споживають RAM на сесію, а складні запити можуть створювати великі тимчасові структури. При паралельності ця «допомога» перетворюється на непідконтрольну відповідальність. На 2 ГБ VPS «необмежений» — інше слово для «скоро».
Правильний фікс був багаточастинним і трохи дратівливим: додати індекси, переписати найгірші звіти і прийняти, що деякі disk temp tables — нормальне явище. Тримайте tmp_table_size скромним, аби режим відмови був «повільний звіт», а не «мертва база».
Також вони перенесли звітні навантаження в непіковий час і пізніше додали репліку. Великий виграш — не ручка. Це рішення, що навантаження не має працювати на первинному в робочий час.
3) Нудно, але правильно, що врятувало день: «Slow query log + один індекс за інцидент»
Інша команда працювала на MariaDB з жорстким бюджетом 2 ГБ. Нічого особливого. У них була одна практика, що виглядала надто базовою: slow query logging завжди увімкнений, ротований і переглядається після будь-якого інциденту.
Коли вийшов новий фіч-реліз, p95 латентності подвоївся. Вони не почали з зміни buffer pools або flush. Вони витягли slow log і знайшли запит, що переглядав занадто багато рядків для простого дашборда користувача. Це не було навмисно — просто бракувало композитного індексу.
Вони додали індекс, запустили, і латентність повернулась до нормального рівня. Без страхітливих змін конфігурації. Без «тимчасового» зменшення durability. Без тижневої оптимізаційної роботи. Просто дисциплінований фідбек-цикл.
Пізніше, коли вони налаштовували конфіг, це було вже з урахуванням спостережуваних вузьких місць — спайки з’єднань, співвідношення тимчасових таблиць, диск await. Секрет команди не був у геніальності. Він полягав у відмові працювати в сліпу.
Якщо ви працюєте на малому залізі, нудна спостережливість — це не наклад. Це платня за житло.
Поширені помилки: симптом → корінь → виправлення
1) Симптом: mysqld вбивають, він рестартує або зникає
Корінь: OOM killer через необмежену пам’ять на з’єднання, надмірний buffer pool або конкуренція додатка і БД за RAM.
Виправлення: Зменшити max_connections, зменшити буфери на з’єднання (sort/join/read), підібрати innodb_buffer_pool_size і переконатися, що додаток використовує пулінг. Підтвердіть через dmesg.
2) Симптом: помилки «Too many connections» під час спайків
Корінь: churn з’єднань і відсутність пулінгу; або max_connections занадто малий для пікової паралельності.
Виправлення: Впровадьте пулінг (на рівні додатка або проксі), скоротіть wait_timeout для очищення неактивних з’єднань і підніміть max_connections тільки якщо одночасно обмежите пам’ять на з’єднання.
3) Симптом: випадкові стрибки латентності, особливо на записах
Корінь: диск стає вузьким (fsync, checkpointing, дешеве сховище), або фонове очищення конфліктує з foreground-запитами.
Виправлення: Перевірте iostat. Налаштуйте redo capacity помірно, зберігайте durability, якщо не готові втратити дані, і зменшуйте write amplification (індекси, батчі).
4) Симптом: CPU високий, load average росте, але запити «не такі вже й великі»
Корінь: занадто багато runnable-потоків через надмірну паралельність; неефективні запити; відсутні індекси, що призводять до сканів.
Виправлення: Зменшіть паралельність (з’єднання), використовуйте slow logs, додавайте індекси і розгляньте thread pool (особливо в MariaDB), щоб зменшити треш планувальника.
5) Симптом: реплікаційний лаг (якщо у вас репліка)
Корінь: I/O репліки не витримує (диск), або SQL-потік заблокований довгими транзакціями або важкими запитами.
Виправлення: Перевірте стан реплікації, зменшіть write load і оптимізуйте найповільніші запити. На 2 ГБ запускати реплікацію плюс додаток плюс БД — амбіційно; будьте чесні щодо ресурсів.
6) Симптом: тимчасові таблиці «таємничо» заповнюють диск
Корінь: великі сорти/групування, що виходять на диск; tmpdir на маленькому файловому розділі; недостатні індекси.
Виправлення: Помістіть tmpdir на файлову систему з місцем, але в першу чергу виправляйте запити/індекси. Не просто підвищуйте tmp_table_size на 2 ГБ.
7) Симптом: стан processlist «Sending data» домінує
Корінь: таблиці/індекси скануються і важке рядкове опрацювання; іноді поганий порядок join-ів.
Виправлення: Використовуйте slow log + EXPLAIN, додайте потрібні індекси, зменште кількість вибраних стовпців і впровадьте пагінацію. Конфіг не врятує вас від сканування мільйонів рядків заради забави.
Чеклісти / покроковий план
Покроково: стабілізуйте спочатку (уникніть крашів), потім оптимізуйте
- Підтвердіть версію і движок: MySQL vs MariaDB, точна версія і що таблиці InnoDB.
- Встановіть межі з’єднань: оберіть реалістичний
max_connections(60–120 на 2 ГБ залежно від додатка та вартості запиту). - Обмежте пам’ять на з’єднання: тримайте sort/join буфери скромними; не «допомагайте» кожному запиту величезними буферами.
- Підібрати buffer pool: 512M для app+DB, ≈1G для DB-only як старт.
- Увімкніть slow query logging: завжди, з ротацією логів.
- Переконайтесь, що не свопите: своп — не кеш бази даних, це податок на продуктивність.
- Виміряйте латентність диска: використовуйте
iostat; якщо await поганий — прийміть, що ви обмежені сховищем. - Виправте топ-3 запити: додайте індекси, перепишіть, зменшіть сканування рядків. Не починайте з 40 ручок конфігу.
- Налаштуйте алерти: використання swap, Max_used_connections близько до ліміту, %util диска і зміни обсягів slow queries.
- Перетестуйте під навантаженням: стейджинг або контрольований прогін у проді. Слідкуйте за пам’яттю і диском.
Санітні ліміти для 2 ГБ VPS (правила великої руки)
- max_connections: 40–80, якщо app+DB; 80–150, якщо DB-only і навантаження на одне з’єднання легке.
- innodb_buffer_pool_size: 512M (спільний) до 1G (DB-only). Рідко більше.
- tmp_table_size / max_heap_table_size: 32M–64M. Більше — пастка при паралельності.
- sort/join buffers: 1M–4M максимум у більшості випадків на малій RAM. Великі значення для специфічних навантажень з низькою паралельністю.
- Durability: тримайте
innodb_flush_log_at_trx_commit=1, якщо не маєте явної толерантності до втрати даних.
Операційна гігієна, що запобігає «таємній повільності»
- Ротуйте slow logs і error logs, щоб диск не заповнився.
- Тримайте таблиці і індекси в порядку; уникайте невикористовуваних індексів, що підсилюють записи.
- Запускайте
ANALYZE TABLE, коли плани запитів дивні після великих змін даних (акуратно, у непіковий час). - Плануйте оновлення; раптові великі апгрейди на 2 ГБ VPS — це шлях зустрічі з найгіршим з оптимізатора.
Питання й відповіді
1) Чи MariaDB швидша за MySQL на 2 ГБ VPS?
Іноді, для специфічних навантажень, але домінуючий фактор на 2 ГБ зазвичай — тиск пам’яті і латентність диска. Обирайте двигун, який вам простіше експлуатувати, а потім виправляйте запити.
2) Яка найважливіша ручка, щоб уникнути крашів?
max_connections, бо він опосередковано обмежує експлозію пам’яті на з’єднання. За ним — стримані буфери на з’єднання.
3) Який розмір innodb_buffer_pool_size на 2 ГБ?
Почніть з 512M, якщо на машині також працює додаток. Якщо це лише БД — стартуйте з ≈1G. Збільшуйте тільки якщо не свопите і кеш-промахи реалістично шкодять.
4) Чи варто вмикати swap на VPS для бази даних?
Своп може врятувати від негайної смерті OOM, але також може створити довгі латентні паузи. Якщо використовуєте своп — вважайте його аварійною подушкою та тримайте пам’ять БД консервативною. Якщо своп стає звичним — ви недопроvisioned або неправильно налаштовані.
5) Чи прийнятно innodb_flush_log_at_trx_commit=2?
Прийнятно тільки якщо бізнес погоджується втратити до ~1 секунди підтверджених транзакцій при краші. На одиночному VPS з первинними даними зазвичай краще залишити налаштування за замовчуванням.
6) Чому не робити tmp_table_size величезним, щоб уникнути диск тимчасових таблиць?
Бо на маленькому сервері це граната при паралельності. Кілька одночасних запитів можуть витратити сотні мегабайт кожен у гіршому випадку. Спочатку виправляйте запити та індекси.
7) Чи варто запускати додаток і базу даних разом на 2 ГБ?
Можна, але треба суворо дотримуватись правил: обмежити пам’ять БД, капати з’єднання і зменшити кількість воркерів у додатку. Якщо додаток росте — розділяйте раніше; вертикальне масштабування на одній маленькій машині має різкі краї.
8) Як швидко зрозуміти, CPU-bound я чи disk-bound?
Використовуйте iostat -xz. Високе %iowait, велике await і високе %util вказують на диск. Якщо iowait низький, а CPU завантажений — ви CPU/запит/паралельність bound.
9) Чи потрібно налаштовувати table_open_cache на 2 ГБ?
Лише якщо бачите churn (Open_tables близько до ліміту кеша і проблеми з метаданою). Тримайте його помірним; великі кеші споживають пам’ять і дескриптори файлів.
10) Якщо можу виправити лише одну річ в додатку — що це?
Пулінг з’єднань і адекватні таймаути. Це зменшить потребу в високих max_connections і значно знизить волатильність пам’яті.
Висновок: практичні наступні кроки
Якщо ви хочете, щоб база на 2 ГБ VPS не падала, перестаньте думати «про найкращу продуктивність» і почніть думати «про обмежену відмову». Обмежте з’єднання. Тримайте буфери на з’єднання маленькими. Дайте InnoDB buffer pool, що відповідає реальності, а не его. Потім використовуйте slow query log, щоб заробити реальні покращення продуктивності чесним шляхом: роблячи менше роботи.
Зробіть це далі, по порядку
- Оберіть Профіль A (спільний) або Профіль B (тільки БД) і застосуйте його чисто; перезапустіть сервіс.
- Запустіть швидкі діагностики:
free,dmesg,iostat, Threads_connected/Max_used_connections. - Перегляньте slow log на найгірші запити; виправте найгірший індексом або переписом.
- Встановіть алерти на використання swap і Max_used_connections, що наближається до вашого ліміту.
- Якщо все ще досягаєте лімітів: розділіть додаток і БД або апгрейдніть RAM/сховище. На 2 ГБ «масштабування вгору» часто найдешевше оптимізаційне рішення.
MySQL і MariaDB можуть добре поводитися на 2 ГБ VPS. Фокус не в пошуку ідеальних налаштувань. Він у відмові дозволяти одному налаштуванню зробити відмову необмеженою.