Linux: PostgreSQL на малому VPS — налаштування, що запобігають OOM

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

Ви орендували малий VPS, бо дбаєте про ресурси. Потім PostgreSQL з’їдає сервер о 03:17, ядро застосовує сокиру,
а ваш «план високої доступності» виявляється вибаченням у Slack і дуже раннім ранком.

Хороша новина: більшість подій out-of-memory (OOM) на малих Postgres-серверах спричинені самими собою. Ще краща:
виправлення нудні, вимірювані й повторювані — якщо ви перестанете ставитися до пам’яті як до однієї ручки.

Як трапляється OOM на малому VPS (і чому звинувачують Postgres)

На малому VPS «пам’ять» — це не одне сховище. Це хиткий пакт між кешем сторінок ядра, анонімною
пам’яттю (куча/стек), сегментами роздільної пам’яті, виділеннями на процес і тим, що гіпервізор вирішив сьогодні не давати вашому
сусідові. PostgreSQL додає власні категорії: shared_buffers, пам’ять на бекенд, фонові воркери,
пам’ять для обслуговування та розширення, яким байдуже до вашого бюджету.

Коли відбувається OOM, PostgreSQL часто стає заголовком, бо це великий довгоживучий процес з багатьма
дочірніми процесами. Але рішення про вбивство приймає ядро, на підставі оцінки «шкідливості», cgroup пам’яті (якщо є)
і того, кого найпростіше вистрілити, не прибивши всю ОС. Іноді воно вбиває найбільший бекенд. Іноді — postmaster і ви маєте
повний аутедж. Іноді — ваш агент моніторингу; це як стріляти в пожежний датчик, бо він занадто голосно пищить.

Найпоширеніший сценарій на крихітних машинках — не «shared_buffers занадто великий». Це «необмежена конкуренція в поєднанні з налаштуваннями пам’яті на запит, що множаться». Один вираз може виділити work_mem кілька разів (по одному на вузол сортування/хешування), і багато бекендів можуть робити це одночасно. Це не баг. Це математика.

Другий типовий сценарій — повільне згоряння: autovacuum не встигає, таблиці роздуваються, індекси роздуваються, запити стають повільнішими, а повільніші запити утримують більше підключень довше, що збільшує конкуренцію і тиск на пам’ять. Вітаємо, ви винайшли зворотний зв’язок.

Жарт №1: Якщо ви виставили max_connections в 2000 на 1 GB VPS, у вас не сервер бази даних — у вас лотерея на тему пам’яті.

Цікаві факти та історичний контекст (те, що ще кусає)

  • Факт 1: Багатопроцесна архітектура PostgreSQL (один процес ОС на підключення) сягає рішень, прийнятих у часи, коли потоки були менш портативними й передбачуваними серед Unix-варіантів.
  • Факт 2: shared_buffers раніше рекомендували виставляти доволі великим, але кеш сторінок Linux і поліпшена поведінка вводу/виводу змістили кращу практику в бік «помірних буферів, дозволь ОС кешувати».
  • Факт 3: work_mem десятиліттями неправильно розуміли, бо це пам’ять на операцію, а не на запит. Один запит може використовувати її кілька разів у межах одного бекенду.
  • Факт 4: PostgreSQL вже давно має log_temp_files, але багато команд досі не увімкнули його — тому вони ніколи не дізнаються, що їхній «містичний» OOM був спричинений сортуванням, яке мало б вийти на диск.
  • Факт 5: Autovacuum був введений, щоб полегшити ручний vacuum, але його стандартна агресивність навмисно консервативна, щоб не дивувати різні робочі навантаження — на малому VPS його часто потрібно підсилити.
  • Факт 6: OOM killer ядра існує, щоб зберегти систему в живих, а не ваш сервіс. Це не алгоритм справедливості; це триаж.
  • Факт 7: Облік пам’яті в PostgreSQL не централізований, бо він розподілений між процесами та контекстами; саме тому «використання пам’яті Postgres» завжди оцінка, а не єдиний показник.
  • Факт 8: Пулінг підключень (особливо transaction pooling) став масовим для Postgres частково тому, що «один процес на підключення» ресурсомісткий на малих машинах.

Ментальна модель: куди PostgreSQL реально витрачає пам’ять

1) Роздільна пам’ять: shared_buffers (і сусіди)

shared_buffers — це кеш сторінок таблиць та індексів, керований Postgres. Він живе в роздільній пам’яті і
видно як частина сліду головного postmaster, але ним користуються всі бекенди. На малому VPS його потрібно зробити
достатньо великим, щоб уникнути постійного викидання кешу, і достатньо малим, щоб лишити місце для всього іншого
(включно з кешем сторінок ОС).

Практичне правило: на VPS 1–4 GB shared_buffers між 128 MB і 1 GB часто є розумним. Якщо ви ставите більше,
краще мати причину і вимірювання. Маленька машинка все одно потребує пам’ять для:

  • накладні витрати на підключення (стеки, контексти пам’яті, виклики системних функцій)
  • сортування/хеші (work_mem)
  • обслуговування (maintenance_work_mem)
  • WAL-буфери, фонові процеси та пам’ять розширень
  • кеш сторінок ядра (ваш дешевий прискорювач вводу/виводу)

2) Пам’ять на бекенд: мовчазний множник

Кожне клієнтське підключення — це процес бекенда. Кожен бекенд має базове використання пам’яті (від кількох MB до десятків MB,
залежно від налаштувань, розширень, локалі та форми запитів). Далі запити додають пам’ять шматками, часто в контекстах,
прив’язаних до операторів.

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

  • work_mem (сортування, хеші)
  • temp_buffers (тимчасові таблиці на сесію)
  • max_parallel_workers_per_gather (паралельні запити множать пам’ять)

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

3) Пам’ять для обслуговування: vacuum, create index та «неочікувана переробка на вихідних»

maintenance_work_mem використовується VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY та іншими.
На малих машинах його можна тримати помірним (64–256 MB) і все буде гаразд. Підступ — запуск кількох задач обслуговування
одночасно: кожна може витратити до цього ліміту. Автовакуумні воркери можуть працювати паралельно.

4) Кеш сторінок ОС: не опція, не «марнотратство»

Linux використовуватиме вільну пам’ять як кеш. Це добре. Postgres читає дані, ядро їх кешує, майбутні читання швидші.
Якщо ви позбавите кеш сторінок, роздуваючи налаштування пам’яті Postgres, ви примусите дискові читання і підвищите латентність,
що збільшує час запитів, що збільшує конкуренцію і тиск на пам’ять. Ось повільно-горючий шлях до OOM.

5) Swap: аварійний вихід, не друга вітальня

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

Швидкий план діагностики: перші, другі, треті перевірки

Це порядок, що рятує під час інцидентів. Не завжди. Але часто.

Перше: підтвердити, що це саме OOM (і хто помер)

  • Перевірте логи ядра на повідомлення OOM killer.
  • Перевірте, чи postmaster перезапустився (логи відновлення Postgres).
  • Перевірте, чи systemd/cgroup не спричинили вбивство (часто виглядає як OOM, але іноді ні).

Друге: знайти множник (підключення, паралелізм, work_mem)

  • Скільки активних підключень у піку?
  • Були раптові зміни max_connections, пулер підключень або деплой додатка?
  • Якісь запити роблять великі сортування/хеші або викидають тимчасові файли?
  • Паралельні запити увімкнені на крихітній машинці?

Третє: перевірити повільне згоряння (bloat, відставання autovacuum, IO wait)

  • Автовакуум працює? Відстає?
  • Симптоми роздування таблиць/індексів (зростання розміру, повільні сканування)?
  • Висока затримка диска? Повільні fsync для WAL? Це перетворює спайки в тривалу напругу.

Четверте: перевірити базові налаштування ОС (swap, overcommit, vm)

  • Swap існує і має адекватний розмір.
  • vm.swappiness не виставлено в щось драматичне без причини.
  • Політика overcommit пам’яті не вводить вас в оману, що виділення «безкоштовні».

Парафразована ідея (приписується): Надія — не стратегія. — генерал H. Norman Schwarzkopf, часто цитований в операційних контекстах (парафразована ідея)

Практичні завдання: команди, приклади виводів і рішення (12+)

Це перевірки, які я фактично виконую на малому VPS, коли PostgreSQL звинувачують у вбивстві системи. Кожне завдання містить:
команда → приклад виводу → що це означає → яке рішення прийняти.

Завдання 1: Підтвердити активність OOM killer

cr0x@server:~$ journalctl -k -g -i 'out of memory|oom-killer|Killed process' -n 50
Jan 12 03:17:21 vps kernel: Out of memory: Killed process 24113 (postgres) total-vm:612844kB, anon-rss:348920kB, file-rss:1200kB, shmem-rss:0kB, UID:113 pgtables:1256kB oom_score_adj:0
Jan 12 03:17:21 vps kernel: oom_reaper: reaped process 24113 (postgres), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Значення: Ядро вбило конкретний бекенд postgres. Це справжнє OOM, а не «Postgres впав сам».

Рішення: Негайно перейти до перевірок конкуренції й пам’яті на бекенд; один бекенд виріс надто великий.

Завдання 2: Перевірити, чи postmaster перезапустився (відновлення після збою)

cr0x@server:~$ sudo journalctl -u postgresql -n 80
Jan 12 03:17:22 vps postgresql[1023]: LOG:  database system was interrupted; last known up at 2026-01-12 03:12:05 UTC
Jan 12 03:17:22 vps postgresql[1023]: LOG:  database system was not properly shut down; automatic recovery in progress
Jan 12 03:17:23 vps postgresql[1023]: LOG:  redo starts at 0/5A1C2B0
Jan 12 03:17:24 vps postgresql[1023]: LOG:  database system is ready to accept connections

Значення: Postgres сам впав (postmaster помер або отримав SIGKILL). Запустилось відновлення.

Рішення: Ставитися як до аутеджу: перевірити помилки клієнтів, час відновлення WAL і врахувати поведінку systemd щодо OOM.

Завдання 3: Подивитися поточний статус пам’яті й swap

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           1.9Gi       1.6Gi        74Mi        28Mi       264Mi       152Mi
Swap:          1.0Gi       612Mi       412Mi

Значення: ОЗП натиснута; swap використовується. «Available» — це те, що важливо для нових виділень.

Рішення: Якщо swap зростає під навантаженням з піковими затримками, ви в зоні трясіння; обмежте конкуренцію і множники пам’яті.

Завдання 4: Швидко визначити топ споживачів пам’яті (RSS)

cr0x@server:~$ ps -eo pid,ppid,user,comm,rss,etime --sort=-rss | head -n 15
  PID  PPID USER     COMMAND   RSS     ELAPSED
24113  1023 postgres postgres  348920  00:03:12
24102  1023 postgres postgres  112540  00:03:11
1023      1 postgres postgres   87420  05:42:19
24098  1023 postgres postgres   80112  00:03:10
1780      1 root     node       62310  02:11:03

Значення: Один бекенд величезний. Зазвичай це великий sort/hash, паралельний план або розширення.

Рішення: Зіставте цей PID із запитом і користувачем; налаштуйте запит або зменшіть work_mem/max_parallel_workers_per_gather.

Завдання 5: Зіставити PID бекенда з його запитом

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT pid, usename, state, wait_event_type, wait_event, now()-query_start AS age, left(query,120) AS q FROM pg_stat_activity WHERE pid=24113;"
  pid  | usename | state  | wait_event_type | wait_event |   age    | q
-------+---------+--------+-----------------+------------+----------+------------------------------------------------------------
 24113 | app     | active |                 |            | 00:03:09 | SELECT ... ORDER BY created_at DESC LIMIT 200000;
(1 row)

Значення: Виконується велике ORDER BY з величезним LIMIT. Це запрошення до великих витрат пам’яті і тимчасових файлів.

Рішення: Виправити запит (індекси, зменшити ліміти, пагінація за ключем) і встановити statement_timeout як ремінь безпеки.

Завдання 6: Подивитися кількість підключень і їх стани

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
   state   | count
-----------+-------
 idle      | 82
 active    | 14
 idle in transaction | 7
(3 rows)

Значення: Надто багато idle-підключень на малому VPS — це смерть маленькими паперовими порізами.

Рішення: Додайте пулер (pgBouncer), знизьте max_connections і завершіть сесії «idle in transaction».

Завдання 7: Перевірити max_connections і поточні налаштування

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SHOW max_connections; SHOW shared_buffers; SHOW work_mem; SHOW maintenance_work_mem; SHOW effective_cache_size;"
 max_connections
-----------------
 300
(1 row)

 shared_buffers
---------------
 512MB
(1 row)

 work_mem
---------
 64MB
(1 row)

 maintenance_work_mem
----------------------
 512MB
(1 row)

 effective_cache_size
----------------------
 1536MB
(1 row)

Значення: На 2 GB машині max_connections=300 і work_mem=64MB — це рецепт OOM при спайках конкуренції.

Рішення: Зменшити max_connections (використати пулер), зменшити work_mem (часто 4–16MB) і правильно виставити пам’ять для обслуговування.

Завдання 8: З’ясувати, чи сорти/хеші викидають на диск (тимчасові файли)

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SHOW log_temp_files;"
 log_temp_files
----------------
 -1
(1 row)

Значення: Ви сліпі щодо використання тимчасових файлів у логах.

Рішення: Тимчасово виставити log_temp_files = 0 (логувати всі) під час діагностики, або поріг як 16MB для постійного режиму.

Завдання 9: Перевірити поточне використання тимчасових файлів по базах

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC;"
  datname  | temp_files | temp_bytes
----------+------------+------------
 appdb    |      18412 | 37 GB
 postgres |          3 | 12 MB
(2 rows)

Значення: Запити активно викидають у тимчасові файли. Це може бути прийнятно, але на малих дисках це створює IO-біль і подовжує час виконання.

Рішення: Виправити найгірші запити й індекси; не збільшувати work_mem, щоб «уникнути тимчасових файлів» — це шлях до OOM.

Завдання 10: Перевірити статус autovacuum і мертві рядки

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
    relname     | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze
---------------+------------+------------+-------------------------------+------------------------------
 events        |   48210321 |   9123401  | 2026-01-10 02:11:44+00        | 2026-01-10 02:30:02+00
 sessions      |    1192031 |    412120  |                               | 2026-01-08 11:02:19+00
(2 rows)

Значення: Тиск роздування. Одна таблиця накопичує мертві рядки; інша взагалі не автогазувалася останнім часом.

Рішення: Налаштувати autovacuum порогово для гарячих таблиць; переконатися, що vacuum не заблокований; розглянути ручний VACUUM у вікна низької активності.

Завдання 11: Перевірити, чи vacuum блокується через довгі транзакції

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT pid, usename, state, now()-xact_start AS xact_age, left(query,120) AS q FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start ASC LIMIT 5;"
  pid  | usename | state |  xact_age  | q
-------+---------+-------+------------+------------------------------------------------------------
 23301 | app     | idle in transaction | 02:14:09 | UPDATE sessions SET ...;
(1 row)

Значення: «Idle in transaction» годинами перешкоджає очищенню vacuum і сприяє росту bloat.

Рішення: Завершити сесію, виправити обробку транзакцій в аплікації, додати idle_in_transaction_session_timeout.

Завдання 12: Подивитися тиск IO і активність swap під навантаженням

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0 621120  74200  18324 232140    0   18   120   340  450  780 22  8 55 15  0
 3  1 623080  61840  18324 230820    0  120   200  1100  520  910 28 11 39 22  0
 4  1 624904  51220  18324 229110    0  240   180  2500  600 1020 34 12 26 28  0

Значення: Зростання swap-out (so) разом зі зростаючим IO wait (wa) свідчить про трясіння і дискову конкуренцію.

Рішення: Зменшити конкуренцію, знизити пам’ять на запит, і переконатися, що сховище не перевантажене (повільний диск під WAL шкодить).

Завдання 13: Перевірити обмеження пам’яті по cgroup (systemd), якщо застосовні

cr0x@server:~$ systemctl show postgresql -p MemoryMax -p MemoryHigh -p OOMPolicy -p ManagedOOMMemoryPressure
MemoryMax=infinity
MemoryHigh=infinity
OOMPolicy=stop
ManagedOOMMemoryPressure=auto

Значення: Тут немає явного MemoryMax; systemd все ще може реагувати через ManagedOOM залежно від дистрибутива.

Рішення: Якщо ви встановлюєте MemoryMax, робіть це усвідомлено і лишайте запасу для роздільної пам’яті та потреб ядра; інакше отримуєте «містичні вбивства».

Завдання 14: Оглянути huge pages / обмеження роздільної пам’яті

cr0x@server:~$ grep -E 'HugePages|Shmem' /proc/meminfo | head
Shmem:             28672 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0

Значення: Ніяких huge pages не налаштовано; роздільна пам’ять зараз мала. Це типово для малих VPS.

Рішення: Не ганяйтеся за huge pages як перше рішення на крихітних машинах. Спочатку виправте конкуренцію і множники пам’яті.

Завдання 15: Визначити найгірші форми запитів (швидкий топ за часом)

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT queryid, calls, mean_exec_time, rows, left(query,100) AS q FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;"
 queryid  | calls | mean_exec_time |  rows  | q
----------+-------+----------------+--------+----------------------------------------------------
 91344122 |   112 |       8421.113 | 200000 | SELECT ... ORDER BY created_at DESC LIMIT $1
 11822001 |  9011 |        312.882 |     20 | SELECT ... FROM sessions WHERE user_id = $1
(2 rows)

Значення: Один запит повільний і обробляє багато рядків; ймовірно він робить великі сорти або широкі хеші.

Рішення: EXPLAIN, проіндексувати, обмежити. На малих VPS один поганий запит може стати всією історією пам’яті.

Завдання 16: Перевірити поточні vm.swappiness та overcommit-політику ядра

cr0x@server:~$ sysctl vm.swappiness vm.overcommit_memory vm.overcommit_ratio
vm.swappiness = 10
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Значення: Консервативний swappiness, стандартна евристика overcommit. Саме по собі не погано.

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

Налаштування, що запобігають OOM (що виставити, чого уникати)

Почніть з жорсткої істини: ваш найгірший сценарій повинен вміщатися

На малих машинах налаштування — це не «зробити швидко». Це «зробити так, щоб не померти». Швидкість приходить від уникнення патологій:
занадто багато бекендів, занадто багато пам’яті на бекенд і довгі транзакції, що перетворюють спайки у тривале навантаження.

Думайте в бюджетах:

  • Бюджет A: роздільна пам’ять (буфери та накладні витрати)
  • Бюджет B: базова пам’ять на підключення × максимум активних підключень
  • Бюджет C: робоча пам’ять на запит × одночасні важкі оператори
  • Бюджет D: воркери обслуговування × пам’ять для обслуговування
  • Бюджет E: потреби ОС + кеш сторінок + «невідомі невідомі» (розширення, libc, TLS, моніторинг)

1) Виправте справжнього лиходія: кількість підключень

Якщо у вас 1–2 GB VPS і ви дозволяєте прямі підключення з веб-додатка з різким трафіком, ви граєте в азартну гру. Кожне «idle» підключення — це все одно процес з накладними витратами пам’яті, а кожен спайк створює шок пам’яті.

Що робити:

  • Використовуйте pgBouncer (transaction pooling, якщо вам не потрібні функції сесії).
  • Виставляйте max_connections так, щоб ваша пам’ять могла це витримати, а не скільки додаток може відкрити.
  • Краще менше підключень із швидкими запитами, ніж багато підключень з повільними.

2) Встановлюйте work_mem, ніби ви за нього платите (так і є)

На малих VPS глобальний work_mem 4–16 MB — надійна відправна точка. Якщо це здається маленьким — чудово: це змушує великі операції скидатися на диск, а не вбивати ОЗП. Скидання повільніше, але «мертвий» ще повільніше.

Кращий підхід — низький дефолт, вищі значення на роль чи сесію для контрольованих завдань:
роль аналітики отримує більше, OLTP — менше.

До того ж: паралельні запити збільшують використання пам’яті. Кожен воркер може виділяти work_mem. На малому VPS агресивно обмежуйте паралелізм або вимикайте його, якщо він непередбачуваний.

3) Тримайте shared_buffers помірним

Не ставте shared_buffers на 70% ОЗП, бо якийсь блог сказав «кеш — король». Linux вже кешує. Ви хочете місце для ОС і для спайкових витрат бекендів.

Практичні відправні точки:

  • 1 GB RAM: 128–256 MB shared_buffers
  • 2 GB RAM: 256–512 MB shared_buffers
  • 4 GB RAM: 512 MB–1 GB shared_buffers

Потім вимірюйте: співвідношення попадань у кеш, IO wait і затримки під навантаженням. Якщо ви збільшили буфери і продуктивність не покращилась, ви просто вкрали пам’ять там, де вона потрібніша.

4) Autovacuum: налаштуйте так, щоб bloat не перетворився на тиск на пам’ять

Autovacuum на малому VPS має працювати на випередження, а не бути чемним. За замовчуванням він налаштований консервативно для різних машин. Ваш маленький VPS не такий вже різний — він просто обмежений.

Що допомагає:

  • Знизьте autovacuum_vacuum_scale_factor для гарячих таблиць; спирайтесь менше на відсоткові пороги і більше на фіксовані пороги.
  • Збільшіть autovacuum_max_workers лише якщо IO може це витримати; інакше ви додасте конкуренцію.
  • Підвищте autovacuum_work_mem помірно (або використовуйте maintenance_work_mem), але не дозволяйте багатьом воркерам працювати безконтрольно.
  • Моніторте «idle in transaction» і довгі транзакції, що блокують очистку.

5) Поставте обмежувачі на транзакційну і заявну поведінку

Найдешевша функція надійності в Postgres — це таймаут. Таймаути перетворюють «повільно їдять ваш сервер» в «швидкий збій з логами». Ви зможете відлагодити збій. Ви не відлагодите OOM після того, як процес зник.

  • statement_timeout: припиняє втечі запитів.
  • idle_in_transaction_session_timeout: зупиняє аплікації, які відкрили транзакцію і пішли гуляти.
  • lock_timeout: зупиняє натовпи запитів через очікування блокувань.

6) Swap: мати його, але не покладатись на нього

Невеликий swap-файл (рівний ОЗП або половині ОЗП, залежно від диска і навантаження) може зменшити «миттєву смерть» під час піку.
Це не ліцензія ігнорувати налаштування пам’яті. Слідкуйте за швидкістю swap-in/out; якщо swap активно трясеться під навантаженням, ви вже в небезпечній зоні.

7) Логи, що запобігають забобонам

Щоб уникнути майбутніх ігор у звинувачення, логируйте достатньо, щоб зв’язати симптоми з причинами:

  • Увімкніть логування повільних запитів (log_min_duration_statement).
  • Увімкніть логування тимчасових файлів (порог log_temp_files).
  • Використовуйте pg_stat_statements для виявлення повторюваних порушників.

Жарт №2: Увімкнення логів не виправить ваш інцидент, але значно покращить вашу здатність бути правим під час наступного інциденту.

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

Міні-історія 1: Інцидент через хибне припущення («work_mem — на запит, чи не так?»)

Середня SaaS-команда тримала Postgres на 2 GB VPS для «не-критичної» внутрішньої аналітики. Почалося як побічний проект і стало «всі ним користуються щоранку». Так буває.

Розробник підняв work_mem до 128 MB, щоб прискорити набір дашборд-запитів. Припущення було, що запит отримає одне виділення work_mem. Також лишили щедрий max_connections, бо «підключення дешеві». У тестуванні дашборд став швидшим. В продакшні він працював краще до того моменту, як маркетинг влаштував понеділкову зустріч.

В піку десятки одночасних дашборд-запитів виконувалися з кількома вузлами сортування/хешу в одному запиті. Деякі пішли в паралель. Пам’ять швидко росла. Swap почав використовуватись. Латентність зросла. Клієнти перепідключалися. Кількість з’єднань зросла. OOM killer ядра почав відстрілювати жирні бекенди, а потім забрав postmaster.

Виправлення не було героїчним: зменшили дефолтний work_mem, обмежили паралельних воркерів, додали pgBouncer і збільшили work_mem тільки для ролі «аналітика» з контрольованою конкуренцією. Також додали statement_timeout, що перетворило «плавлення сервера» на «термін дашборда вичерпано», що набагато кращий режим відмови.

Міні-історія 2: Оптимізація, що обернулась проти («Давайте макс shared_buffers і вимкнемо swap»)

Інша компанія перенесла невеликий інструмент підтримки клієнтів на 1 GB VPS. Інструмент мав помірний трафік, але багато повнотекстового пошуку та розширень. Інженер, намагаючись допомогти, застосував тюнінг: shared_buffers=768MB, effective_cache_size=1GB, і «swap — поганий, вимкнемо його».

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

Звіт по інциденту мав знайомий запах: графіки показували зростання пам’яті до обриву. Налаштування зменшили дискові читання, але також позбавили кеш сторінок ОС простору для дихання і прибрали swap як клапан тиску. Система стала крихкою.

Вони відкотили до помірних буферів, знову ввімкнули невеликий swap-файл і зменшили дефолтний work_mem. Продуктивність покращилася, бо система припинила трястися й перезавантажуватись. Виявилось, що «вверх» — це дуже швидкий стан.

Міні-історія 3: Нудна практика, що врятувала день («таймаути, пулер і один дашборд»)

Фінансово наближена служба працювала на 4 GB VPS з Postgres. Ніяких наворотів. Вони мали дві практики, що здавались надто консервативними: суворі таймаути і пулер з навмисно низьким розміром серверного пулу.

Менеджер продукту вимагав нову функцію «експортувати все». Інженери реалізували її як фонова задача і ендпоінт для користувача. Ендпоінт мав жорсткий statement_timeout. Фонова задача використовувала окрему роль з підвищеним work_mem і низькою конкурсною чергою.

В день релізу один клієнт спробував експортувати величезний набір даних під час піку трафіку. Ендпоінт вийшов за таймаут і повернув помилку, що було дратівливо, але чесно. Фонова задача стала в чергу і виконалась пізніше з контрольованими ресурсами. База даних залишилася стабільною.

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

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

1) Симптом: випадкові відключення Postgres, «server closed the connection unexpectedly»

Корінь: OOM killer ядра вбиває бекенди або postmaster; іноді systemd вбиває через тиск пам’яті.

Виправлення: Підтвердьте логи ядра; зменшіть конкуренцію, знизьте work_mem, додайте pgBouncer і переконайтеся, що swap існує.

2) Симптом: зростає використання swap, латентність зростає, потім все таймаутиться

Корінь: Тиск пам’яті, що спричиняє трясіння swap; зазвичай забагато активних підключень або важких запитів одночасно.

Виправлення: Обмежити активні підключення, застосувати таймаути, оптимізувати найгірші запити, розглянути зменшення shared_buffers щоб відновити дихання кешу сторінок.

3) Симптом: накопичуються «idle in transaction» підключення

Корінь: Аплікація «тече» транзакціями (забула commit/rollback), утримує блокування, блокує vacuum.

Виправлення: Встановити idle_in_transaction_session_timeout; виправити логіку транзакцій у додатку; моніторити і завершувати порушників.

4) Симптом: autovacuum постійно працює, запити повільно гіршають протягом днів

Корінь: Vacuum не встигає; bloat збільшує IO і час запитів; конкуренція росте; тиск на пам’ять триває.

Виправлення: Налаштувати autovacuum для гарячих таблиць; прибрати блокування (довгі транзакції); запланувати ручний vacuum у тихий час.

5) Симптом: тимчасовий диск заповнюється, потім Postgres повідомляє про відсутність місця

Корінь: Великі сорти/хеші викидаються в тимчасові файли; недостатньо дискового простору; іноді це один звіт.

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

6) Симптом: продуктивність погіршилася після «тюнінгу» shared_buffers вгору

Корінь: Вичавили кеш сторінок ОС і пам’ять бекендів; зросли IO wait і довші запити.

Виправлення: Зменшити shared_buffers до помірного значення; вимірювати; зосередитися на планах запитів і керуванні підключеннями.

7) Симптом: один запит іноді спричиняє OOM

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

Виправлення: Знизьте work_mem; обмежте паралельних воркерів; перепишіть запит; додайте індекси; встановіть таймаут запиту.

Чеклісти / покроковий план (безпечний базовий набір для малого VPS)

Покроково: стабілізувати спочатку, потім оптимізувати

  1. Підтвердити OOM: перевірити логи ядра на вбивства; зафіксувати часові мітки і PID.
  2. Порахувати підключення: виміряти пікові активні й загальні; визначити «idle» vs «idle in transaction».
  3. Встановити або налаштувати pgBouncer: прагнути до малого, стабільного серверного пулу.
  4. Знизити max_connections: змусити пулер виконувати свою роботу.
  5. Встановити консервативні дефолти: work_mem низький, maintenance_work_mem помірний, shared_buffers помірний.
  6. Обмежити паралелізм: знизити max_parallel_workers_per_gather (або вимкнути, якщо потрібно).
  7. Додати таймаути: statement + idle-in-transaction + lock timeout для порядку.
  8. Увімкнути спостережуваність: лог повільних запитів, лог тимчасових файлів, pg_stat_statements.
  9. Виправити топ-3 запити: індекс, переписати або обмежити. На малих машинах не потрібен топ-30.
  10. Налаштувати autovacuum для гарячих таблиць: знизити коефіцієнти; переконатися, що vacuum не блокується.
  11. Перевірити swap: переконатися, що він існує; не вимикайте його; моніторити swap-in/out.
  12. Тест навантаження з конкуренцією: перевірити, що найгірший сценарій вміщується в пам’ять.

Базовий конфіг для початку (не універсальний, але відносно безпечний)

Це навмисно консервативні відправні точки для малого VPS. Налаштовуйте на основі вимірювань, не інтуїції.

  • 1 GB RAM: shared_buffers 128–256MB, work_mem 4–8MB, maintenance_work_mem 64–128MB, max_connections 50–100 (краще через пулер).
  • 2 GB RAM: shared_buffers 256–512MB, work_mem 4–16MB, maintenance_work_mem 128–256MB, max_connections 100–150 (краще через пулер).
  • 4 GB RAM: shared_buffers 512MB–1GB, work_mem 8–16MB, maintenance_work_mem 256MB, max_connections 150–200 (краще через пулер).

Операційний чекліст: після будь-якої зміни

  • Перевірити поведінку підключень під час пікового трафіку.
  • Перевірити генерацію тимчасових файлів і повільні запити.
  • Перевірити активність swap і IO wait.
  • Перевірити прогрес autovacuum і мертві рядки.
  • Мати план відкату: зміни конфігів дешеві; час відновлення — ні.

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

1) Чи є shared_buffers головною причиною OOM на малому VPS?

Іноді, але зазвичай — ні. Класичний сценарій OOM на малому VPS — це пам’ять на бекенд, що множиться через конкуренцію: занадто багато
підключень плюс щедрий work_mem, часто у поєднанні з паралельними запитами. Помірні буфери рідко вбивають систему самі по собі; неконтрольовані спайки — так.

2) Який безпечний work_mem на 1–2 GB?

Почніть з 4–8MB для загальних OLTP-навантажень. Підвищуйте по ролях для контрольованих робіт (наприклад, один воркер для звітів), а не глобально. Пам’ятайте: один запит може виділити work_mem кілька разів.

3) Чи варто вимикати swap для баз даних?

На малих VPS зазвичай — ні. Невеликий swap може запобігти різкій загибелі під час тимчасових спайків. Головна мета — уникати постійного свопінгу; моніторьте swap-in/out і IO wait. Вимикання swap часто перетворює «повільно» на «мертвий».

4) Чому я бачу багато «idle» підключень? Хіба вони нешкідливі?

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

5) Як зрозуміти, чи OOM спричинений конкретним запитом?

Корелюйте: PID з логу ядра → pg_stat_activity (якщо ще є) → логи аплікації → лог повільних запитів. Додайте pg_stat_statements і лог тимчасових файлів. Якщо одна форма запиту домінує в temp_bytes або mean_exec_time, це сильний підозрюваний.

6) Чи слід збільшувати effective_cache_size для покращення продуктивності?

effective_cache_size не виділяє пам’ять; це підказка планувальнику. Виставляйте його як реалістичну оцінку пам’яті, доступної для кешування (OS cache + shared buffers). Не трактуйте його як важіль для виправлення OOM.

7) Чи допомагає налаштування autovacuum у боротьбі з OOM?

Опосередковано — так, часто значно. Коли autovacuum відстає, bloat зростає, запити гіршають, конкуренція збільшується і тиск на пам’ять стає тривалим. Підтримка vacuum у робочому стані запобігає шляху «повільного згоряння» до OOM.

8) Чи вирішить проблему просто апгрейд VPS?

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

9) Чи шкідливі паралельні запити на малому VPS?

Вони не «аморальні». Вони операційно непередбачувані. Паралелізм може множити використання пам’яті і спричиняти CPU-конкуренцію. На малому VPS обмежуйте його низько. Якщо потрібні великі паралельні аналітичні задачі, скоріше за все вам потрібен інший сервер або архітектура.

10) Який найшвидший виграш, якщо я вже щотижня сторіную?

Додайте пулер і знизьте max_connections. Потім встановіть глобальний work_mem на консервативне значення. Ці два кроки часто усувають OOM, бо контролюють множник.

Наступні кроки, які можна зробити сьогодні

  1. Пройти швидкий план діагностики: підтвердити OOM, знайти множник і перевірити на повільне згоряння через bloat/IO.
  2. Поставити жорстку межу конкуренції: pgBouncer + зниження max_connections.
  3. Скинути work_mem до консервативного дефолту: потім підвищувати його вибірково по ролях для контрольованих робіт.
  4. Додати таймаути: statement + idle-in-transaction + lock timeout. Зробіть відмови швидкими і діагностованими.
  5. Увімкнути видимість тимчасових файлів і повільних запитів: щоб наступний інцидент мав докази, а не фольклор.
  6. Налаштувати autovacuum для гарячих таблиць: профілактика bloat — це захист пам’яті у вбранні IO.

Малий VPS з PostgreSQL не є по своїй природі крихким. Він стає крихким, коли ви дозволяєте пам’яті бути необмеженою. Обмежте її, вимірюйте і спіть спокійно, ніби OOM killer не ваш балансувальник навантаження.

← Попередня
Зберігання в Linux: опція монтування, що може спотворити ваші очікування
Наступна →
Міграція електронної пошти: план переміщення без простою, що не втрачає повідомлень

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