MySQL vs PostgreSQL: пул підключень — кому потрібно раніше на VPS

Було корисно?

Ви запускаєте маленький додаток на маленькому VPS. Спочатку він поводиться чемно. А потім одного дня відправляють маркетинговий лист, трафік утричі зростає, і ваша база даних перетворюється на швейцара в нічному клубі: «Не сьогодні». При цьому CPU навіть не пітніє. Пітніє RAM. Логи помилок починають говорити чужою мовою: too many connections, таймаути, дивні сплески латентності, час від часу скидання підключень.

Це той момент, коли люди відкривають для себе пул підключень. Зазвичай під тиском. Зазвичай після того, як вони звинуватили мережу, ORM, DNS, хмарного провайдера і — якщо дуже прискіпливі — місяць.

Головна теза: кому потрібен пул раніше?

Якщо ви запускаєте один VPS з обмеженою пам’яттю, PostgreSQL зазвичай потребує пулінгу раніше, ніж MySQL з однієї простої причини: підключення клієнта до Postgres зазвичай відображається у виді виділеного бекенд-процесу, і цей процес споживає помітну кількість пам’яті навіть коли він «іdle». Достатньо багатьох іdle-підключень — і ваш VPS починає свапити, що вбиває латентність і врешті-решт вашу гідність.

Дефолтна модель MySQL (особливо з InnoDB) часто більш витривала до помірної кількості клієнтів, бо вона не є відкриттям «одного важкого процесу ОС на підключення» в тому ж сенсі, і має регулювання типу кешування нитей, яке може пом’якшити вартість постійних підключень/відключень. Але «більш витривала» — не означає «непробивна». На VPS з малою пам’яттю і вибухоподібним веб-трафіком обидві можуть впасти від бурі підключень; Postgres просто досягає стіни раніше, і стіна зазвичай — пам’ять.

Є друга відверта істина: більшість додатків не потребують пулінгу при 20 підключеннях; їм потрібні розумні ліміти, таймаути та менше повторних підключень. Але як тільки у вас багато робочих процесів і короткоживучих запитів, пулінг стає не «оптимізацією», а «паском безпеки».

Цікаві факти і трохи історії (щоб ви перестали це повторювати)

  • Походження PostgreSQL «один бекенд на підключення» походить від ранніх Unix-моделей процесів і сильної переваги ізоляції. Ця архітектура досі присутня, навіть якщо внутрішні механізми значно еволюціонували.
  • PgBouncer став популярним не тому, що Postgres повільний, а тому, що багато веб-стеків створювали занадто багато короткоживучих сесій, і «просто підвищити max_connections» перетворилося на дороге захоплення.
  • Модель MySQL «нітя на підключення» існує багато років, але практична поведінка сильно залежить від кешування нитей і конфігурації. Сучасні MySQL/MariaDB можуть краще обробляти високий churn, ніж їхня найгірша репутація підказує — якщо їх налаштувати.
  • Дефолт max_connections у PostgreSQL консервативний, бо кожне підключення може споживати пам’ять у різних контекстах (work_mem, buffers, структури на бекенді). Система підштовхує вас до пулінгу.
  • Вартість рукопотискань у MySQL колись була більшою, коли TLS та аутентифікація були повільнішими і додатки постійно перепідключались. Це досі має значення, але апаратне забезпечення та бібліотеки покращилися.
  • «Пулінг в додатку» став звичним, коли фреймворки почали запускати багато процесів-воркерів (думайте про pre-fork сервери) і кожен воркер тримав власний пул, множачи підключення непомітно.
  • Пулінг транзакцій — відносно грубий інструмент, який приносить у жертву можливості на рівні сесії (як-от prepared statements та змінні сесії) в ім’я виживання під навантаженням. Це компроміс, а не магія.
  • Postgres з часом додав багатшу спостережуваність (наприклад, pg_stat_activity, wait events та ін.), що іронічно полегшує виявлення штормів підключень — і, отже, паніку теж.

Що насправді вирішує пул підключень (а що не вирішує)

З’єднання до бази даних — це не «рядок». Це погоджена сесія з автентифікацією, алокаціями пам’яті, сокет-буферами і — в залежності від БД — виділеними сервісними ресурсами на сервері. Створення і знищення таких сесій у великій кількості — дороге й непередбачуване.

Пул підключень вирішує дві проблеми:

  1. Churn: він амортизує витрати на встановлення/закриття підключень між багатьма запитами.
  2. Fan-out: він обмежує кількість серверних сесій, навіть коли у вашому додатку багато воркерів або нитей.

Пулінг не вирішує:

  • Повільні запити (він може лише приховати їх, поки не виникне черга)
  • Погані індекси
  • Конфлікти блокувань
  • Насичення дискового вводу-виводу
  • Запуск аналітики на тому ж самому сервері, що й OLTP, бо «це всього лише один звіт»

Жарт №1: Пул підключень — як спільний офісний принтер: всі його люблять, поки хтось не скине 300-сторінковий PDF і не заблокує чергу.

MySQL на VPS: поведінка підключень, яка б’є першою

MySQL (та MariaDB) зазвичай працює як один серверний процес, що керує багатьма нитями. Кожне клієнтське підключення зазвичай відображається як серверна нить. Це може бути багато нитей, що має свої накладні витрати, але зазвичай це не вибухає RSS так, як бекенд-процеси Postgres на машинах з малою пам’яттю — принаймні не на тих самих значеннях підключень.

Що перш за все падає у MySQL у світі VPS, зазвичай виглядає так:

  • Створення нитей при churn, коли додатки постійно підключаються/відключаються без налаштованого кешування нитей.
  • Вичерпання max_connections через «сплячі» сесії від клієнтів з витоком або величезних пулів додатків.
  • Черги та таймаути, якщо навантаження зв’язане з CPU або I/O і підключення накопичуються у черзі.

MySQL може витримувати «багато підключень» краще, ніж Postgres в окремих випадках, але це також підштовхує людей до дурної практики: піднімати max_connections і називати це плануванням ємності. На VPS так ви міняєте «занадто багато підключень» на «ядро викликало OOM killer та вбило мою базу».

MySQL: що дає пулінг вам рано

Якщо ваш додаток використовує багато коротких запитів (PHP-FPM, serverless-подібні воркери, cron-шторми), пулінг переважно дає менші витрати на рукопотискання та менше одночасних нитей. Але багато клієнтських бібліотек і фреймворків для MySQL вже реалізують базовий пулінг, і MySQL часто розгортають за одним рівнем додатків, де ви можете налаштувати розумні розміри пулів на процес.

На практиці: у MySQL ви часто можете протриматися довше без зовнішнього пулера, якщо додаток сам правильно пулує і ви налаштуєте серверні параметри (thread cache, таймаути, backlogs). Але «протриматися довше» не означає «добре»; це означає, що у вас є час виправити проблему до того, як запрацює аварійний виклик.

PostgreSQL на VPS: чому «один backend на підключення» змінює все

Архітектура PostgreSQL славнозвісна простотою: postmaster приймає підключення і форкає (або реюзує) бекенд-процес для його обробки. Одне підключення — один бекенд. Це чиста ізоляція, чіткі межі відмов і дуже помітна модель витрат.

На VPS режим відмови настільки ж послідовний, наскільки й жорсткий:

  • Ваш додаток піднімає більше воркерів (або більше процесів).
  • Кожен воркер тримає власний пул (часто 5–20 підключень на воркер за замовчуванням).
  • Кількість підключень стрибає вгору.
  • Використання пам’яті зростає разом із цим.
  • Вузол починає свапити або OOM-иться.
  • Латентність стає функцією «скільки часу ми витрачаємо на паніку».

Postgres може обробляти важкі навантаження на скромному залізі, але він не любить, коли його використовують як мультиплексор сокетів для недисциплінованого шару додатків. Коли люди кажуть «Postgres потребує пулінгу», вони насправді мають на увазі «ваш додаток потребує дорослого нагляду».

Postgres: чому пулінг іноді не опціональний

Навіть «іdle» бекенди коштують пам’яті. А зайняті бекенди коштують значно більше, особливо при сортуванні, хешуванні та налаштуваннях на сесію. На малих VPS кілька сотень підключень можуть бути катастрофічними навіть при помірному QPS, бо вузьке місце — пам’ять і перемикання контекстів, а не сирий пропуск запитів.

Ось чому PgBouncer (або подібні) настільки поширені: він обмежує серверні сесії, дозволяючи додатку вірити, що у нього багато «підключень». Також це дає централізоване місце для примусового встановлення лімітів і таймаутів. Це не модно. Це рятує життя.

Отже, кому потрібен пул раніше на VPS?

PostgreSQL потребує пулінгу раніше у типовому сценарії VPS, бо серверна вартість підключення вища і більше пов’язана з процесами ОС. Якщо ви запускаєте веб-додаток з багатьма воркерами, ви можете вдарити стіну на дивно низькому трафіку: десятки воркерів × пул по 10 кожний — це вже сотні підключень.

MySQL потребує пулінгу раніше якщо поведінка клієнтів патологічна: багато підключень/відключень на запит, відсутність keepalive, низький thread cache або якщо ваш рівень додатків сильно фанує (багато незалежних задач чи сервісів, що луплять ту саму БД). MySQL може впасти від churn підключень і штормів нитей, а також страждати від конкуренції ресурсів при величезній кількості підключень, навіть якщо пам’ять не стрибає так сильно.

Ось практичне правило прийняття рішення для VPS:

  • Якщо ви на Postgres і не можете впевнено сказати «ми обмежили загальну кількість підключень нижче 100 і знаємо чому», треба вважати, що вам потрібен зовнішній пулер або агресивний пулінг на боці додатка зараз.
  • Якщо ви на MySQL і бачите високий churn підключень, часті Aborted_connects або стрибки в кількості виконуваних нитей, вам потрібен пулінг або принаймні дисципліноване повторне використання підключень зараз.

Жарт №2: Якщо ваш план — «ми просто піднімемо max_connections», вітаю — ви винайшли denial as a service.

Варіанти дизайну пулера: пул в додатку vs проксі vs серверний пул

1) Пул на боці додатка (вбудований у фреймворки)

Це дефолт у багатьох стеках: кожен процес підтримує пул відкритих підключень. Це просто, швидко і зберігає семантику сесії (prepared statements, тимчасові таблиці, змінні сесії). Але це також множить підключення на кількість робочих процесів. На VPS саме це множення може тихо вбити вас о 3 ранку.

Використовуйте пул на боці додатка коли:

  • У вас невелика кількість процесів додатка
  • Ви можете примусити суворі ліміти на розмір пулу на процес
  • Вам потрібні можливості на рівні сесії

2) Зовнішній пулер/проксі (PgBouncer, ProxySQL)

Це «дорослий у кімнаті». Ви ставите легкий сервіс перед базою, і додаток підключається до нього. Пулер утримує контрольовану кількість серверних підключень, обслуговуючи багато клієнтських підключень.

Торгові місця:

  • Чудово: захищає БД від штормів підключень; централізує ліміти; зменшує churn.
  • Не дуже: семантика сесії може порушитися в режимах transaction/statement pooling.
  • Операційно: це ще одна рухома частина на маленькому VPS; тримайте її простою і під моніторингом.

3) «Просто підняти max_connections» (не робіть так)

Підняття лімітів може бути правильним, якщо ви виміряли пам’ять на підключення і у вас є запас RAM. Але на VPS це часто пластир на відрізану артерію. Якщо ви піднімаєте ліміт, не змінюючи поведінку клієнтів, ви не збільшуєте пропускну здатність; ви просто збільшуєте радіус вибуху.

Практичні завдання: команди, виводи та рішення, які ви приймаєте

Ось перевірки, які я виконую, коли хтось каже «база повільна», а насправді має на увазі «підключення плавлять вузол». Запускайте їх на VPS. Не гадати. Обладнання VPS скінченне і дуже чесне щодо цього.

Завдання 1: Перевірте тиск пам’яті й свап

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       2.9Gi       180Mi        52Mi       720Mi       420Mi
Swap:          1.0Gi       820Mi       204Mi

Що це означає: Свап використовується інтенсивно, доступна пам’ять низька. Ваша латентність буде виглядати як генератор випадкових чисел.

Рішення: Негайно підозрюйте кількість підключень; скоротіть одночасні сесії і розгляньте зовнішній пулер перед «налагодженням запитів».

Завдання 2: Визначте, чи DB була вбита OOM

cr0x@server:~$ journalctl -k --since "2 hours ago" | tail -n 20
Dec 29 10:41:12 vps kernel: Out of memory: Killed process 2143 (postgres) total-vm:5216444kB, anon-rss:3102420kB, file-rss:0kB, shmem-rss:0kB
Dec 29 10:41:12 vps kernel: oom_reaper: reaped process 2143 (postgres), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Що це означає: Ядро вбило Postgres. Це не «баг Postgres». Це арифметика ресурсів.

Рішення: Миттєва міра: обмежте підключення, вбивайте іdle-підключення, увімкніть пулер і припиніть додавати воркерів, поки пам’ять не стабілізується.

Завдання 3: Перевірте стани TCP підключень до порту БД

cr0x@server:~$ ss -tanp | awk '$4 ~ /:5432$/ || $4 ~ /:3306$/ {print $1, $2, $3, $4, $5}' | head
ESTAB 0 0 10.0.0.10:5432 10.0.0.21:51122
ESTAB 0 0 10.0.0.10:5432 10.0.0.21:51130
SYN-RECV 0 0 10.0.0.10:5432 10.0.0.22:60718
TIME-WAIT 0 0 10.0.0.10:5432 10.0.0.23:49810

Що це означає: SYN-RECV вказує на тиск на accept backlog або те, що DB повільно приймає з’єднання; багато TIME-WAIT натякає на churn.

Рішення: Якщо домінує TIME-WAIT, виправте повторне використання клієнтами/пулінг. Якщо домінує SYN-RECV, перевірте backlog прослуховування DB і завантаження CPU.

Завдання 4: Порахуйте живі підключення по процесам (PostgreSQL)

cr0x@server:~$ sudo -u postgres psql -c "select state, count(*) from pg_stat_activity group by 1 order by 2 desc;"
  state  | count
---------+-------
 idle    |   142
 active  |     9
         |     1
(3 rows)

Що це означає: 142 іdle-сесії сидять на бекенд-процесах. На VPS це часто марна пам’ять і зайві переключення контекстів.

Рішення: Додайте PgBouncer або зменшіть розмір пулу додатка; також встановіть idle_in_transaction_session_timeout і перегляньте поведінку keepalive.

Завдання 5: Перевірте Postgres max_connections і зарезервовані слоти

cr0x@server:~$ sudo -u postgres psql -c "show max_connections; show superuser_reserved_connections;"
 max_connections
-----------------
 200
(1 row)

 superuser_reserved_connections
-------------------------------
 3
(1 row)

Що це означає: Звичайним користувачам доступно лише 197 підключень. Цей кап встановлений не просто так.

Рішення: Не піднімайте це, поки не виміряєте пам’ять на бекенд і не переконаєтеся, що свап не задіяний.

Завдання 6: Оцініть приблизний розмір пам’яті бекенду Postgres

cr0x@server:~$ ps -o pid,rss,cmd -C postgres --sort=-rss | head
  PID   RSS CMD
 2149 178432 postgres: appdb appuser 10.0.0.21(51122) idle
 2191 165120 postgres: appdb appuser 10.0.0.21(51130) idle
 2203  98204 postgres: appdb appuser 10.0.0.22(60718) active
 2101  41288 postgres: checkpointer
 2099  18864 postgres: writer

Що це означає: Кожен бекенд тут ≈100–180MB RSS. На 4GB VPS 50 таких сесій можуть зіпсувати вам тиждень.

Рішення: Потрібен пулінг і/або зниження налаштувань пам’яті; також дослідіть, чому RSS на бекенд такий високий (розширення, prepared statements, поведінка work_mem).

Завдання 7: Перевірте кількість підключень MySQL і виконувані ниті

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_%';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_cached    | 32    |
| Threads_connected | 180   |
| Threads_created   | 9124  |
| Threads_running   | 14    |
+-------------------+-------+

Що це означає: Багато підключень, помірна кількість виконуваних нитей, а високе Threads_created вказує на churn (залежно від аптайму).

Рішення: Збільште thread_cache_size, зменшіть повторні підключення додатка і обмежте пули. Якщо churn інтенсивний, розгляньте ProxySQL або виправлення пулінгу в додатку.

Завдання 8: Перевірте MySQL max_connections і aborted connects

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 200   |
+-----------------+-------+
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_connects | 381   |
+------------------+-------+

Що це означає: Aborted connects можуть бути викликані проблемами аутентифікації, скиданнями мережі або зіткненням штормів підключень з лімітами/таймаутами.

Рішення: Якщо це зростає під час піків трафіку, ваш додаток занадто часто перепідключається або його обмежують. Виправте пулінг; потім налаштуйте таймаути і backlog.

Завдання 9: Знайдіть, хто відкриває підключення (з боку сервера)

cr0x@server:~$ sudo lsof -nP -iTCP:5432 -sTCP:ESTABLISHED | awk '{print $1,$2,$9}' | head
postgres 2149 TCP 10.0.0.10:5432->10.0.0.21:51122
postgres 2191 TCP 10.0.0.10:5432->10.0.0.21:51130
postgres 2203 TCP 10.0.0.10:5432->10.0.0.22:60718

Що це означає: Ви можете зіставити джерела підключень за IP/портом; поєднайте це з логами додатків або сервіс-дискавері, щоб ідентифікувати «галасливого сусіда».

Рішення: Якщо один хост домінує, обмежте його насамперед. Не карайте весь кластер через один проблемний job runner.

Завдання 10: Перевірте Postgres на idle-in-transaction сесії

cr0x@server:~$ sudo -u postgres psql -c "select pid, usename, state, now()-xact_start as xact_age, left(query,80) as query from pg_stat_activity where state like 'idle in transaction%' order by xact_start asc limit 5;"
 pid  | usename  |        state         |  xact_age  |                                     query
------+----------+----------------------+------------+------------------------------------------------------------------------------
 3012 | appuser  | idle in transaction  | 00:12:41   | UPDATE orders SET status='paid' WHERE id=$1
(1 row)

Що це означає: Одне підключення тримає транзакцію відкритою і може блокувати vacuum, роздувати таблиці і створювати конфлікти блокувань, що виглядає як «підключення повільні».

Рішення: Виправте управління транзакціями в додатку; встановіть idle_in_transaction_session_timeout і обережно розгляньте transaction pooling (воно може маскувати баги).

Завдання 11: Перевірте backlog і listen налаштування в Linux

cr0x@server:~$ sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096

Що це означає: Ліміти backlog пристойні. Якщо ви все ще бачите багато SYN-RECV, додаток/БД може бути занадто повільним для accept або CPU голодний.

Рішення: Якщо значення дуже малі (наприклад, 128), підвищте їх; але не використовуйте налаштування ядра, щоб уникнути виправлення штормів підключень.

Завдання 12: Перевірте Postgres wait events та топ-запити (високий рівень)

cr0x@server:~$ sudo -u postgres psql -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where state='active' group by 1,2 order by 3 desc;"
 wait_event_type |  wait_event   | count
-----------------+---------------+-------
 Lock            | transactionid  |     6
 IO              | DataFileRead   |     3
(2 rows)

Що це означає: Ваша «проблема з підключеннями» може насправді бути контенцією блокувань або I/O-затримками. Пулінг цього не виправить; він просто поставить біль у чергу.

Рішення: Якщо домінують блокування — шукайте довгі транзакції і «гарячі» рядки. Якщо домінує I/O — перевірте латентність диска і коефіцієнт попадань у кеш перед тим, як чіпати пулінг.

Завдання 13: Перевірте список процесів MySQL на флуд сплячих

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | head
Id	User	Host	db	Command	Time	State	Info
412	appuser	10.0.0.21:51912	appdb	Sleep	287		NULL
413	appuser	10.0.0.21:51920	appdb	Sleep	290		NULL
444	appuser	10.0.0.22:38110	appdb	Query	2	Sending data	SELECT ...

Що це означає: Багато Sleep означає, що клієнти тримають підключення відкритими. Це нормально при пулінгу, але повинно бути обмеженим.

Рішення: Якщо кількість Sleep величезна і ви досягаєте max_connections, зменште розмір пулів, додайте дисципліну пулінгу або використайте проксі-пулер, щоб обмежити серверні сесії.

Завдання 14: Перевірте фановут додатку з боку ОС

cr0x@server:~$ ps -eo pid,cmd | egrep 'gunicorn|puma|php-fpm|sidekiq|celery' | head
1021 /usr/bin/puma 5.6.7 (tcp://0.0.0.0:3000) [app]
1033 sidekiq 6.5.9 app [0 of 10 busy]
1102 php-fpm: pool www
1103 php-fpm: pool www

Що це означає: Налаштування з великою кількістю воркерів множать пули. Якщо кожен з цих процесів має пул по 10, ви щойно створили фабрику підключень.

Рішення: Сума вашого найгіршого випадку: workers × pool_size. Якщо вона перевищує безпечний ліміт БД — вам потрібен пулер або менший слід відбитку.

Швидкий план діагностики

Коли ви на VPS, мета — не ідеальний аналіз. Мета — припинити кровотечу і правильно ідентифікувати домінуючий вузький горлечко, перш ніж ви «оптимізуєте» не те.

По-перше: підтвердіть, чи це тиск підключень або тиск на запити/IO

  • Пам’ять + свап: free -h. Якщо свап значний, підключення — підозрюваний перш ніж довести протилежне.
  • Кількість підключень: Postgres pg_stat_activity, MySQL Threads_connected.
  • Логи ядра: OOM kills або TCP помилки.

По-друге: визначте, чи підключення іdle, заблоковані або дійсно зайняті

  • Postgres: порахуйте idle vs active vs idle in transaction.
  • MySQL: SHOW PROCESSLIST, зверніть увагу на багато Sleep vs багато «Sending data» або «Locked».

По-третє: якщо зайняті, визначте домінуюче чекання

  • Блокування: довгі транзакції, гарячі рядки, відсутні індекси.
  • I/O: повільний диск, недостатній кеш, занадто багато випадкових зчитувань.
  • CPU: занадто багато одночасних запитів; пулінг може допомогти, обмежуючи конкурентність, але це — обмежувач, а не рішення.

По-четверте: застосуйте найменш ризиковану міру

  • Обмежте кількість воркерів і пулів у додатку.
  • Включіть зовнішній пулер (PgBouncer/ProxySQL), якщо БД тоне в сесіях.
  • Зменшіть churn: keepalive, розумні таймаути, повторне використання підключень.

Три міні-історії з корпоративного життя (анонімізовані, але болісно реальні)

Інцидент: хибне припущення, що спричинило потік підключень

Середня SaaS-команда запускала Postgres на відносно потужному, але не надміцному VPS. Рівень додатків був змішаний: веб-воркери і фонова робота. Під час запуску продукту час відгуку пішов від «добре» до «заморожено». Їхні дашборди показували CPU на 40%. Інженер на виклику, логічно, звинуватив балансувальник навантаження і почав шукати проблеми в мережі.

Справжня проблема була в тихому припущенні: «наш ORM пулить підключення». Правильно, але неповно. Кожен процес воркера мав свій пул, і деплой збільшив кількість воркерів, множачи загальні підключення. Postgres форкав бекенд для кожного, пам’ять зростала, почався свап, і ядро почало вбивати процеси. Балансувальник не робив нічого поганого; він просто спостерігав пожежею.

Виправлення не було екзотичним. Вони обмежили розмір пулу на процес, тимчасово зменшили кількість воркерів і встановили PgBouncer у режимі session pooling. Раптом БД перестала форкати себе до смерті. Також вони додали жорстке сповіщення по загальних підключеннях і використанню свапу. Урок не був «PgBouncer — чудо». Урок був: рахуйте підключення так само, як рахуєте CPU — глобально.

Оптимізація, що призвела до зворотного ефекту: агресивний transaction pooling без гігієни додатка

Команда електронної торгівлі хотіла «змусити Postgres масштабуватися» на VPS без апгрейду. Вони розгорнули PgBouncer у режимі transaction pooling, бо це виглядало найефективніше. Кількість підключень стабілізувалася. Усі святкували. Потім почалися найдивніші баги: періодичні «prepared statement does not exist», інколи «current transaction is aborted», і кілька платежів застрягли в підвішеному стані.

У них було дві проблеми. По-перше, додаток залежав від стану сесії: prepared statements і налаштування на сесію. У transaction pooling клієнт може не отримати той самий серверний коннекшн у наступній транзакції, тож стан сесії стає ненадійним, якщо не адаптувати додаток. По-друге, в коді були неакуратні межі транзакцій: деякі шляхи залишали транзакції відкритими довше, ніж очікувалося, і пулер підсилював симптоми, перетасовуючи клієнтів між меншим числом серверних бекендів.

Вони врешті переключилися на session pooling для основного додатка, залишили transaction pooling для статусно-незалежного job runner, і виправили межі транзакцій у коді. Пропускна здатність стала трохи нижчою, ніж «максимально теоретична», але коректність повернулася. VPS залишився стабільним. Це було дороге нагадування, що «режим пулера» — це контракт з додатком, а не просто серверна зміна.

Нудна, але правильна практика, що врятувала день: суворі бюджети і таймаути

Компанія, що працює поруч з фінансами, тримала MySQL на маленькому VPS для внутрішнього інструменту. Інструмент не був «життєво необхідним», поки раптом не став. У них була одна звичка, яка здавалася параноїдальною: таблиця бюджету підключень. Нічого феєричного. Просто таблиця: кількість процесів додатка, розмір пулу на процес, очікувана пікова кількість підключень і запас щодо max_connections.

Коли був доданий новий пакетний job, йому треба було задекларувати використання підключень і реалізувати експоненційний backoff при спробах підключення. Також були розумні таймаути: connect timeout, query timeout на клієнті і серверні idle-таймаути, щоб уникнути зомбі-сесій.

Одного квартального кінця пакетний job запустився пізно, веб-інтерфейс отримав більшу навантаженість, і мережевий флап спричинив деякі штормові перепідключення. Система не впала. Вона деградувала: з’явилися черги, деякі запити швидко таймаутилися, а база залишилася живою. Нудна практика — явні бюджети і примусово таймаути — запобігла каскадному відмові. Ніхто не отримав медалі. Саме так ви знаєте, що це спрацювало.

Поширені помилки: симптом → корінь → виправлення

1) «Занадто багато підключень» з’являється після масштабування воркерів додатка

Симптоми: раптові помилки після деплою; CPU БД виглядає ок; пам’ять зростає; багато іdle-сесій.

Корінь: пер-процесні пули підключень помножилися через збільшення числа воркерів. Сума перевищила спроможність БД.

Виправлення: обмежити розмір пулу на процес; зменшити кількість воркерів; додати PgBouncer/ProxySQL; встановити глобальні бюджети підключень і алерти.

2) Сплески латентності з великою кількістю TIME-WAIT сокетів

Симптоми: багато короткоживучих підключень; трафік з важким рукопотисканням; періодичні таймаути.

Корінь: підключення/відключення на запит; відсутній keepalive; churn нитей/процесів сервера.

Виправлення: увімкнути пулінг на боці додатка; тримати підключення «теплими»; налаштувати кеш нитей у MySQL; розглянути пулер; задати таймаути і повтори з backoff.

3) Postgres OOM, хоча запити «не такі важкі»

Симптоми: OOM kills ядра; свап росте; багато іdle-сесій; VPS стає не чуйним.

Корінь: забагато бекендів; пам’яттєвий відбиток на бекенд; іноді великий work_mem у поєднанні з конкурентними сортуваннями/хешами.

Виправлення: зменшити max connections з пулером; знизити work_mem або обмежити конкурентність; перестати вважати max_connections за синонім пропускної здатності.

4) Після додавання PgBouncer додаток ламається дивними способами

Симптоми: помилки про prepared statement; налаштування сесії не застосовуються; тимчасові таблиці відсутні.

Корінь: використання transaction/statement pooling, тоді як додаток покладається на семантику сесії.

Виправлення: переключитись на session pooling; або рефакторити додаток, щоб уникнути стану на сесії; переконатися, що драйвери сумісні з режимом пулінгу.

5) «База повільна», але активних підключень мало

Симптоми: низька кількість підключень; високі часи відгуку; високі IO waits або wait на блокування.

Корінь: це не проблема підключень — це конфлікт блокувань, насичення I/O, відсутні індекси, довгі транзакції.

Виправлення: досліджуйте wait events, slow query logs, блокування; оптимізуйте схему/запити; додайте кеш; виносьте важкі задачі з VPS.

6) Черги в пулі підключень навіть при низькому QPS

Симптоми: час очікування в пулі високий; БД показує мало активних запитів; потоки додатка блокуються, чекаючи підключення.

Корінь: розмір пулу занадто малий для конкурентності; або підключення витікають (не повертаються); або довгі транзакції «прикручують» підключення.

Виправлення: виявити витоки; встановити таймаути на чек-аут з пулу; підібрати розмір пулу; зменшити область транзакцій; додати circuit breakers.

Чек-листи / покроковий план

Покроково: як вирішити, чи потрібен зовнішній пулер на VPS

  1. Порахуйте воркерів додатка на VPS/хостах додатка і помножте на конфігурований розмір пулу. Якщо ви цього не знаєте — у вас уже є проблема.
  2. Виміряйте реальні підключення (pg_stat_activity або статусні змінні MySQL).
  3. Перевірте запас пам’яті і свап. Якщо свап ненульовий під навантаженням — підключення головний підозрюваний.
  4. Класифікуйте підключення: idle vs active vs idle-in-transaction.
  5. Якщо Postgres і багато idle: додайте PgBouncer, агресивно обмежте серверні підключення і наведіть лад у лімітах пулів додатка.
  6. Якщо MySQL і високий churn нитей/підключень: виправте повторне використання в додатку, налаштуйте кеш нитей і обмежте пули; розгляньте ProxySQL при великій кількості клієнтів.
  7. Встановіть таймаути, які відпадають швидко: client connect timeout, query timeout і серверні idle-таймаути.
  8. Налаштуйте алерти на: загальну кількість підключень, використання свапу, події OOM, час очікування в пулі та помилки підключень/таймаути.

Покроково: «зробити стабільно сьогодні вночі»

  1. Тимчасово зменшіть concurrency додатка (воркери/ниті).
  2. Зменшіть розміри пулів у конфігурації додатка. Перезапустіть процеси додатка, щоб зміни вступили в силу.
  3. Якщо Postgres свапиться: розгорніть PgBouncer з консервативним default_pool_size і обмежте серверні підключення.
  4. Вбивайте очевидних поганих акторів: сесії idle-in-transaction; безконтрольні пакетні задачі.
  5. Перевірте, що помилки підключення зменшилися і латентність нормалізувалася.
  6. Лише потім переходьте до оптимізації запитів і індексів.

Покроково: як розумно підбирати розмір підключень на VPS

  • Почніть з машини: скільки RAM може безпечно використовувати процес БД без свапу?
  • Оціни вартість на підключення: для Postgres виміряйте RSS бекенду під репрезентативним навантаженням.
  • Встановіть жорсткий кап: серверні підключення Postgres часто повинні бути значно меншими, ніж клієнтські «підключення» додатку; для цього й існують пулери.
  • Віддавайте перевагу чергам перед крахом: черга пулу неприємна; OOM — це рестарт сервісу з додатковими кроками.

Питання й відповіді

1) Чи завжди потрібен пул підключень на VPS?

Ні. Якщо у вас один процес додатка з невеликим стабільним пулом і низьким churn, зовнішній пулер може бути не потрібен. Але вам все одно потрібні ліміти й таймаути.

2) Чому PostgreSQL «часто потребує» PgBouncer?

Бо кожне підключення відображається у бекенд-процесі з помітними накладними на пам’ять. Пулінг дозволяє обслуговувати багато клієнтських сесій з меншою кількістю серверних бекендів — саме те, що потрібно VPS з обмеженою пам’яттю.

3) Чому просто не підняти PostgreSQL max_connections?

Можна, але часто це неправильний крок на VPS. Більше підключень збільшує тиск на пам’ять і перемикання контекстів. Якщо ви вже близькі до свапу, підвищення ліміту може перетворити «помилки» на «відмову».

4) Чи є у MySQL еквівалент PgBouncer?

Екосистеми різні, ідея схожа. ProxySQL часто використовується як проксі/пулер-шар, і багато додатків покладаються на пулінг на боці клієнта. Потрібність проксі залежить від того, скільки незалежних клієнтів у вас і наскільки дисципліновані пули додатків.

5) Який режим пулінгу в PgBouncer найкращий?

Session pooling — найбезпечніший для сумісності. Transaction pooling — потужний, але ламає припущення на рівні сесії; використовуйте його лише якщо додаток статeless на рівні сесії і ви протестували всі крайні випадки.

6) Чи може пулінг приховати повільні запити?

Так. Він може зробити систему «стабільною», поки запити чекають за невеликою кількістю серверних підключень. Це краще, ніж крах, але вам все одно потрібно виправити повільні запити або контенцію блокувань.

7) БД показує багато іdle-підключень. Це погано?

Не завжди. Це погано, коли іdle-підключення споживають ресурси, яких ви не маєте (Postgres на малому VPS) або коли вони доводять вас до вичерпання max_connections. Іdle — нормально; необмежене іdle — ні.

8) На що слід налаштувати алерти, щоб виявити це раніше?

Як мінімум: загальна кількість підключень, активні підключення, використання свапу, події OOM killer, час очікування пулу (метрики додатка) і рівень помилок підключень/таймаутів.

9) Чи варто пулити і в додатку, і використовувати зовнішній пулер?

Іноді. Це може добре працювати, якщо пули додатків маленькі, а зовнішній пулер гарантує глобальний кап. Небезпека — подвійні черги і заплутана латентність. Тримайте дизайн простим і спостережуваним.

10) Коротка цитата на дорогу — який підхід до надійності тут?

Перефразована ідея, приписувана John Allspaw: надійність походить від проєктування систем, які відмовляють контрольовано, а не від сподівання, що вони ніколи не зламаються.

Висновок: практичні наступні кроки

На VPS пул підключень — це не стільки про підвищення продуктивності, скільки про запобігання самонанесеному denial-of-service. PostgreSQL зазвичай вимагає пулінгу раніше, бо серверні підключення важчі. MySQL часто дає вам більше запасу, але все одно карає за churn і необмежений fan-out.

Зробіть наступне:

  1. Запишіть свій бюджет підключень: воркери додатка × розмір пулу, плюс фонові задачі.
  2. Виміряйте реальність: поточні підключення, idle vs active, і використання пам’яті під навантаженням.
  3. Кап і черга: надавайте перевагу контрольованій черзі пулу перед неконтрольованим пам’яттєвим спіраллю.
  4. Додайте пулер, коли математика вкаже: PgBouncer для Postgres, або проксі-стратегія для MySQL, якщо кількість клієнтів хаотична.
  5. Інструментуйте це: налаштуйте алерти на підключення, свап і час очікування в пулі. Перший раз, коли ви виявите це рано, окупить витрачені зусилля.
← Попередня
Заміна дисків у ZFS: безпечний робочий процес без драм у пулі
Наступна →
ZFS для резервних копій: снапшоти + send/receive без сліз

Залишити коментар