PostgreSQL проти RDS PostgreSQL: налаштування продуктивності, які доведеться робити (навіть у керованому)

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

«Керована база даних» звучить заспокійливо, поки p95 латентності не подвоюється під час промо-кампанії, графік CPU не починає нагадувати штрих-код, а продакт-команда не питає, чи «можна просто додати більше оперативної пам’яті». Можна. Це може допомогти. Але також може не дати жодного ефекту, якщо справжня проблема — блокування, блоат, тиск на WAL або один трагічний запит — продовжує розпилювати ваші SLO.

RDS PostgreSQL усуває цілий клас робіт: заміни обладнання, патчі мажорних версій, автоматичні бекапи й багато нудної, але критичної інфраструктури. Воно не знімає потреби в налаштуванні. Не скасовує фізики. І точно не знімає відповідальності.

Кероване не означає оптимізоване

Запуск PostgreSQL самостійно на EC2 або «голому» сервері дає вам багато ручок для налаштування. Запуск PostgreSQL на RDS дає менше ручок, але все ще достатньо способів нашкодити собі.

Різниця здебільшого в тому, хто відповідає за платформу:

  • Самокерований PostgreSQL: ви відповідаєте за все: ядро, файлову систему, диски, sysctl-параметри, параметри збірки Postgres, розширення, бекапи, відновлення, моніторинг і дзвінки о 3:00 ранку.
  • RDS PostgreSQL: AWS відповідає за хост. Ви відповідаєте за поведінку бази даних: схему, запити, індекси, стратегію autovacuum, налаштування параметрів, планування потужностей та за те, як ви спостерігаєте й реагуєте.

На практиці налаштування продуктивності все одно ваша задача, бо продуктивність — це властивість, що виникає з розподілу даних, навантаження та поведінки застосунку. RDS не знає вашого навантаження. RDS не може переписати ваш ORM. RDS не скаже продакт-команді «ні», коли вони випускають декартовий добуток під виглядом «швидкого звіту».

Суха правда: RDS ускладнює деякі низькорівневі оптимізації, але водночас робить простішим пережити відмови. Якщо ви хочете бути швидкими та надійними, ви маєте ставитися до RDS як до продакшен-системи, якою треба оперувати — просто з меншою кількістю виступаючих залізних країв.

Жарт #1: Хмара — це просто чиєсь інше комп’ютер. RDS — чиєсь інше комп’ютер з гарним UI і тими ж законами термодинаміки.

Цікаві факти та історичний контекст

  • Корені PostgreSQL сягають 1980-х (POSTGRES в UC Berkeley). Деякі ідеї щодо надійності обговорювалися ще до існування вашого CI-конвеєра.
  • MVCC — чому читання в PostgreSQL зазвичай не блокують запис. Це також причина існування vacuum і чому «просто видаляти рядки» — не план продуктивності.
  • Autovacuum не завжди був; ранній Postgres вимагав вакуумування вручну. Деякі команди «вчилися» на вакуумах дорого: продакшен-аутіджіни.
  • PostgreSQL відомий пріоритетом коректності над сюрпризно «швидко, але неправильно». Це визначає вибір планувальника і семантику конкурентності.
  • RDS для PostgreSQL з’явився на початку 2010-х і нормалізував ідею, що багатьом командам не потрібно тримати хости баз даних самостійно.
  • pg_stat_statements став стандартом у міру того, як Postgres перетворювався на СУБД з орієнтиром на спостережуваність. Ви не налаштовуєте те, чого не вимірюєте.
  • Покращення в епоху Postgres 9.6–13+ змінили поведінку vacuum, паралелізм і продуктивність індексів так, що «старі правила» стали ненадійними.
  • IO-патерни важливіші за голий CPU у багатьох OLTP-системах; СУБД може бути «спокійною по CPU», поки чекає на сховище. Графіки люблять брехати через упущення контексту.
  • Тюнінг чекпойнтів — тема, що повторюється вже два десятиліття, бо WAL і чекпойнти — серце надійності й джерело періодичних болів.

Де RDS справді допомагає — і де ні

Що RDS дає вам безкоштовно (або майже)

RDS добре справляється з «нудними інфраструктурними» речами:

  • Автоматизовані бекапи і відновлення до точки в часі.
  • Керований патчинг (в межах maintenance window і вашої апетиту до ризику).
  • Multi-AZ оркестрація відмови, що не рівнозначно «без простою», але дуже корисна.
  • Операційні метрики у CloudWatch і Performance Insights, що зберігає вас від потреби будувати все з нуля.
  • Опції масштабування сховища (залежно від типу дисків), що зменшують жанр інцидентів «закінчився диск о 2 ночі».

Що RDS не вирішує (і іноді ховає)

RDS не:

  • Виправляє повільні запити, спричинені відсутністю індексів, поганим порядком джойнінгів, застарілою статистикою або поведінкою застосунку.
  • Запобігає конфліктам блокувань, коли ваша міграція бере ACCESS EXCLUSIVE блок о 12:00.
  • Усуває блоат у таблицях з високим обігом. MVCC все одно потребує vacuum, а vacuum потребує запасу ресурсів.
  • Робить IO безмежним. EBS має ліміти; пропускна спроможність сховища — платний і вимірюваний ресурс.
  • Обирає правильний клас інстансу для вашого навантаження. Ви можете платити за незадіяний CPU або страждати від нестачі пам’яті.
  • Захищає від «занадто багато підключень», коли ваш застосунок масштабувався горизонтально і кожен под відкриває 100 сесій, ніби 2009 рік.

Найважливіша різниця: на самокерованому Postgres ви можете «проінструментувати хост», щоб дізнатися, на що чекає база. В RDS ви більшість часу обмежені тим, що AWS відкриває: метрики движка, OS-подібні проксі, логи і Performance Insights. Цього достатньо, щоб успішно оперувати системою, але лише якщо ви серйозно ставитеся до спостережуваності.

Налаштування, які все одно потрібно робити (навіть у RDS)

1) Тюнінг запитів: планувальник не ворог, але він не екстрасенс

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

В RDS ви швидше відчуєте помилки в запитах, бо не можете «просто підправити ядро» чи прив’язати процеси до CPU. Це нормально; тюнінг ядра рідко є першим вузьким місцем.

Що робити:

  • Увімкніть pg_stat_statements і ставтеся до нього як до списку ганьби.
  • Використовуйте EXPLAIN (ANALYZE, BUFFERS) і читайте його як звіт з місця події: що торкнулося диска, що ні і чому.
  • Тримайте статистику актуальною (ANALYZE) і не думайте, що autovacuum автоматично розрулить сильно зміщені таблиці.

2) Autovacuum: різниця між «все гаразд» і «чому диск повний?»

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

  • Гіршу ефективність кешу (більше сторінок, менше корисних сторінок).
  • Більше IO (читання мертвих кортежів — це теж читання).
  • Вищу варіативність латентності (vacuum може діяти в невдалий час).
  • Можливий ризик обертання transaction ID, якщо це довго ігнорувати.

Налаштування autovacuum залежить від робочого навантаження. Таблицям з високим обігом OLTP часто потрібні індивідуальні параметри. Стандартні пороги — ввічливі. Ваше продакшен-навантаження — ні.

3) Пам’ять: shared_buffers — не чарівна кнопка швидкості

На самокерованому Postgres можна експериментувати з пам’яттю. В RDS ви все ще налаштовуєте параметри бази, але живете з вибором ОС від AWS. Ви не контролюєте page cache прямо; ви впливаєте на нього, уникаючи дурного IO.

Типові пріоритети:

  • work_mem: занадто низький — сорти/хеш-джойни виливаються на диск; занадто високий — при високій конкуренції RAM стає проблемою.
  • shared_buffers: у RDS часто налаштований розумно за замовчуванням, але «розумно» не означає «оптимально».
  • effective_cache_size: підказка планувальнику; невірні значення ведуть до неправильних планів.

4) IO: RDS робить його купованим, а не необов’язковим

Більшість «інцидентів продуктивності» на RDS PostgreSQL насправді — інциденти IO з SQL-акцентом.

Ваші основні вибори:

  • Тип і розмір сховища (general purpose проти provisioned IOPS, обмеження пропускної здатності, поведінка через burst).
  • Поведінка чекпойнтів (піки записів проти стійкого стану).
  • Підтримка vacuum і обслуговування індексів (фонове IO, що може стати болючим у пікі).

5) WAL і чекпойнти: латентні спайки приходять хвилями

Системи з інтенсивним записом рано чи пізно зустрінуться з підсистемою WAL у темному провулку. Симптоми виглядають як періодичні затримки: повільнішають commits, зріст IO і падіння пропускної здатності з періодичною регулярністю.

Що налаштовувати:

  • max_wal_size і checkpoint_timeout, щоб зменшити частоту чекпойнтів.
  • checkpoint_completion_target, щоб розподілити записи.
  • wal_compression і wal_buffers залежно від версії та навантаження.

В RDS ці зміни робляться через parameter groups. Також слід стежити за replica lag: тиск на WAL проявляється й понизу за репліками.

6) Підключення: повільна смерть від тисячі сесій

PostgreSQL використовує модель процес на підключення. Це надійно й просто. Але це дорого, якщо ви поводитеся з базою як зі статлес HTTP-сервером.

В RDS max_connections може бути великим, але це не значить, що так потрібно робити. Хвиля підключень означає переключення контексту, витрати пам’яті та додаткову конкуренцію за спільні структури.

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

7) Блокування і міграції: зміни схеми — це також продакшен-трафік

RDS не завадить вам виконати ALTER TABLE, що блокує записи на кілька хвилин. Воно, однак, збережe цей простій у графіках CloudWatch для вашого майбутнього самопідбадьорення.

Операційно:

  • Віддавайте перевагу онлайн-патернам: create index concurrently, backfill in batches, swap columns carefully.
  • Встановлюйте lock timeouts для сесій міграцій, щоб вони падали швидко замість утримання системи у заручниках.
  • Моніторьте графи блокувань під час деплоїв, як ви моніторите рівні помилок.

8) Реплікація та масштабування читань: репліки — не чит-код

Репліки для читання можуть зняти частину навантаження з primary. Вони також вводять нові режими відмов: застарілі читання, відставання реплік під час сплесків записів і плани запитів, що відрізняються через різну «теплоту» кешу.

Multi-AZ підвищує доступність, але може трохи вплинути на затримки, і події failover все одно будуть боліти, якщо застосунок не правильно повторює запити.

9) Спостережуваність: якщо ви не можете пояснити p95 — ви не володієте ситуацією

На самокерованому Postgres ви можете використовувати системні інструменти (iostat, vmstat, perf). В RDS ваші найкращі друзі:

  • Performance Insights (wait events, top SQL).
  • Enhanced Monitoring (OS-подібні CPU, пам’ять, load, диск).
  • Логи бази даних (логування повільних запитів, autovacuum, чекпойнти).
  • pg_stat_* views (джерело істини зсередини движка).

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

План швидкої діагностики: знайдіть вузьке місце швидко

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

Перший крок: CPU, IO, блокування чи підключення?

  1. Перевірте навантаження бази та wait events (Performance Insights на RDS; pg_stat_activity скрізь). Якщо бачите багато очікувань типу Lock, не гадуйте — шукайте блокувальників.
  2. Перевірте насичення сховища: read/write IOPS, пропускну здатність і латентність. Якщо латентність висока і IOPS обмежені, ви IO-bound незалежно від кількості vCPU.
  3. Перевірте кількість підключень і активні сесії. Якщо активних сесій набагато більше за кількість ядер, ви, ймовірно, маєте чергування і контекстні переключення.
  4. Перевірте топ SQL за загальним часом. Один запит може домінувати. Часто це не той, за яким ви підозрюєте.

Другий крок: це проблема планувальника/статистики чи проблема даних?

  1. Шукайте регресії планів: той самий запит, новий план, гірша продуктивність.
  2. Перевірте блоат таблиць і мертві кортежі. Якщо мертвих кортежів багато і vacuum відстає, ви платите IO-податок.
  3. Перевірте використання індексів проти послідовних сканів. Послідовні скани — не зло; підозрілими є несподівані послідовні скани.

Третій крок: підтвердіть одним «стріляючим» запитом і виправляйте безпечно

  1. Візьміть топ-правопорушника з pg_stat_statements.
  2. Запустіть EXPLAIN (ANALYZE, BUFFERS) в безпечному середовищі, якщо можливо; у продакшені спочатку plain EXPLAIN.
  3. Вирішіть: додати індекс, переписати запит, налаштувати work_mem, оновити статистику або змінити політику autovacuum.
  4. Підтвердіть покращення тим самим виміром, яким ви оголосили інцидент.

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

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

Завдання 1: Перевірити ідентифікацію інстансу та версію

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select version();"
                                                           version
-----------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 15.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.3.0, 64-bit
(1 row)

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

Рішення: Якщо у вас стара мажорна версія, пріоритетно плануйте оновлення перед героїчним тюнінгом. Багато проблем «налаштування» зникає в сучасних версіях Postgres.

Завдання 2: Підтвердити критичні розширення (pg_stat_statements)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select extname, extversion from pg_extension order by 1;"
      extname       | extversion
--------------------+------------
 pg_stat_statements | 1.10
 plpgsql            | 1.0
(2 rows)

Що це означає: Якщо pg_stat_statements не встановлений, ви відпочатку налагоджуєте «вслепу».

Рішення: В RDS увімкніть його в parameter group (shared_preload_libraries) і перезавантажте інстанс при необхідності; потім create extension у кожній базі, де це потрібно.

Завдання 3: Знайти поточний біль: активні сесії та wait events

cr0x@server:~$ psql "$DATABASE_URL" -X -c "\x on" -c "select pid, usename, application_name, state, wait_event_type, wait_event, now()-query_start as age, left(query,120) as query from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
-[ RECORD 1 ]-------+--------------------------------------------
pid                 | 21435
usename             | app
application_name    | api
state               | active
wait_event_type     | Lock
wait_event          | relation
age                 | 00:02:11.4123
query               | update orders set status='paid' where id=$1
-[ RECORD 2 ]-------+--------------------------------------------
pid                 | 21398
usename             | app
application_name    | migrate
state               | active
wait_event_type     | Lock
wait_event          | relation
age                 | 00:05:43.9921
query               | alter table orders add column reconciliation_id uuid;

Що це означає: Ви заблоковані на рівні блокування відношення; швидше за все, міграція блокує OLTP-операції.

Рішення: Ідентифікуйте блокувальника, зупиніть/відкотуйте міграцію і змініть підхід до змін схеми (онлайн-патерни, lock_timeout, порядок деплою).

Завдання 4: Виявити ланцюги блокувань (хто кого блокує)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select blocked.pid as blocked_pid, blocked.query as blocked_query, blocker.pid as blocker_pid, blocker.query as blocker_query from pg_locks bl join pg_stat_activity blocked on blocked.pid=bl.pid join pg_locks kl on kl.locktype=bl.locktype and kl.database is not distinct from bl.database and kl.relation is not distinct from bl.relation and kl.page is not distinct from bl.page and kl.tuple is not distinct from bl.tuple and kl.virtualxid is not distinct from bl.virtualxid and kl.transactionid is not distinct from bl.transactionid and kl.classid is not distinct from bl.classid and kl.objid is not distinct from bl.objid and kl.objsubid is not distinct from bl.objsubid and kl.pid <> bl.pid join pg_stat_activity blocker on blocker.pid=kl.pid where not bl.granted;"
 blocked_pid |             blocked_query              | blocker_pid |                 blocker_query
------------+----------------------------------------+------------+-----------------------------------------------
      21435 | update orders set status='paid' ...     |      21398 | alter table orders add column ...
(1 row)

Що це означає: Конкретна ідентифікація блокувальника. Тепер це не «Postgres повільний», а «цей PID блокує 40 сесій».

Рішення: Завершіть блокувальник, якщо це доречно, потім виправте процес деплою, щоб уникати ACCESS EXCLUSIVE блокувань у пікові години.

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

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select count(*) as total, sum(case when state='active' then 1 else 0 end) as active from pg_stat_activity;"
 total | active
-------+--------
  612  | 184
(1 row)

Що це означає: 612 підключень загалом не обов’язково погано; 184 активних сесій можуть бути катастрофою на інстансі з 16 vCPU.

Рішення: Якщо активні сесії стійко перевищують кількість ядер, впровадьте пулінг, зменшіть max_connections і виправте «балакучі» патерни в застосунку (N+1, транзакції на запит тощо).

Завдання 6: Знайти топ SQL за загальним часом (pg_stat_statements)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select queryid, calls, round(total_exec_time::numeric,1) as total_ms, round(mean_exec_time::numeric,3) as mean_ms, rows, left(query,120) as query from pg_stat_statements order by total_exec_time desc limit 5;"
 queryid  |  calls  | total_ms | mean_ms |  rows   |                          query
----------+---------+----------+---------+---------+-----------------------------------------------------------
 91827364 |  184920 | 812349.7 |   4.392 | 184920  | select * from users where email=$1
 11223344 |   12001 | 643221.9 |  53.599 |  12001  | select o.* from orders o join order_items i on ...
(2 rows)

Що це означає: Загальний час показує, де база провела свій час. Великий mean вказує на «повільний запит». Велика кількість викликів з помірним mean теж може домінувати.

Рішення: Почніть з запиту з найбільшим total time, якщо тільки один запит явно не спричиняє хвостової латентності. Потім аналізуйте плани й індекси.

Завдання 7: Перевірити використання індексів проти послідовних сканів

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, seq_scan, idx_scan, n_live_tup from pg_stat_user_tables order by seq_scan desc limit 10;"
    relname     | seq_scan | idx_scan | n_live_tup
---------------+----------+----------+------------
 events        |   982134 |    21011 |    4839201
 order_items   |   431201 |   892301 |     983211
(2 rows)

Що це означає: Великий seq_scan на великій таблиці може бути нормальним для аналітики; підозрілим він є для OLTP-ендпоінтів.

Рішення: Якщо seq scans корелюють з повільними запитами — додавайте цілеспрямовані індекси або переписуйте запити. Якщо навантаження аналітичне — розгляньте партиціювання, BRIN-індекси або винесіть звіти окремо.

Завдання 8: Виявити ризик блоату через мертві кортежі й історію vacuum

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze from pg_stat_user_tables order by n_dead_tup desc limit 10;"
   relname   | n_live_tup | n_dead_tup | last_vacuum |     last_autovacuum     | last_analyze |     last_autoanalyze
------------+------------+------------+-------------+--------------------------+--------------+--------------------------
 events     |    4839201 |    1720033 |             | 2025-12-30 08:12:01+00   |              | 2025-12-30 08:13:44+00
 orders     |     312001 |      91011 |             | 2025-12-29 23:02:11+00   |              | 2025-12-29 23:03:09+00
(2 rows)

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

Рішення: Налаштуйте пороги autovacuum для гарячих таблиць і розгляньте агресивніше вакуумування в непікові часи. Якщо блоат вже значний — плануйте контрольовану перебудову (VACUUM FULL рідко є правильним рішенням у продакшені).

Завдання 9: Перевірити налаштування autovacuum (глобально і по таблицях)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "show autovacuum; show autovacuum_vacuum_scale_factor; show autovacuum_vacuum_threshold; show autovacuum_max_workers;"
 autovacuum
------------
 on
(1 row)

 autovacuum_vacuum_scale_factor
-------------------------------
 0.2
(1 row)

 autovacuum_vacuum_threshold
----------------------------
 50
(1 row)

 autovacuum_max_workers
-----------------------
 3
(1 row)

Що це означає: Scale factor 0.2 означає, що vacuum спрацьовує після ~20% змін + поріг. Для великих таблиць з інтенсивним обігом зазвичай це запізно.

Рішення: Для «гарячих» таблиць встановіть нижчий scale factor через ALTER TABLE ... SET (autovacuum_vacuum_scale_factor=0.02, autovacuum_vacuum_threshold=1000) і збільшіть кількість workers, якщо дозволяє IO. Перевіряйте через логи vacuum.

Завдання 10: Виявити тиск на чекпойнти

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select checkpoints_timed, checkpoints_req, round(checkpoint_write_time/1000.0,1) as write_s, round(checkpoint_sync_time/1000.0,1) as sync_s from pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | write_s | sync_s
------------------+-----------------+---------+--------
              412 |             289 |  9821.4 |  612.7
(1 row)

Що це означає: Велике checkpoints_req відносно timed вказує, що ви досягаєте ліміту WAL і вимушено викликаєте чекпойнти. Це поширена причина періодичних піків записів.

Рішення: Збільшіть max_wal_size, налаштуйте checkpoint_timeout і checkpoint_completion_target, а потім стежте за латентністю записів і checkpoints_req.

Завдання 11: Перевірити темп генерації WAL (приблизно)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) as wal_since_boot;"
 wal_since_boot
----------------
 512 GB
(1 row)

Що це означає: Це грубий проксі. Якщо WAL churn величезний, реплікація і сховище це відчують. (У деяких системах використовують точніші WAL-метрики залежно від версії.)

Рішення: Якщо обсяг WAL несподівано великий — дослідіть масові оновлення, обслуговування індексів і поведінку vacuum; розгляньте батчування записів і уникайте зайвих оновлень.

Завдання 12: Перевірити розміри таблиць і індексів (знайти слонів)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, pg_size_pretty(pg_total_relation_size(relid)) as total_size from pg_catalog.pg_statio_user_tables order by pg_total_relation_size(relid) desc limit 10;"
  relname  | total_size
-----------+------------
 events    | 219 GB
 orders    | 38 GB
(2 rows)

Що це означає: Великі об’єкти впливають на поведінку кешу й вартість вакуумування. В RDS вони також визначають вартість сховища і час знімання snapshot’ів.

Рішення: Для дуже великих таблиць з інтенсивним додаванням розгляньте партиціювання, BRIN-індекси або політику утримання даних. Якщо розмір не співпадає з уявленням — ймовірно, є блоат або неконтрольоване зберігання.

Завдання 13: Перевірити, чи сорти/хеш-джойни виливаються на диск

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select datname, temp_files, pg_size_pretty(temp_bytes) as temp_written from pg_stat_database order by temp_bytes desc;"
  datname  | temp_files | temp_written
-----------+------------+--------------
 appdb     |      18231 | 97 GB
(1 row)

Що це означає: Велике використання тимчасових файлів означає, що запити виливаються на диск (часто через низький work_mem або неправильну форму запиту).

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

Завдання 14: Підтвердити актуальність статистики для ключових таблиць

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, last_analyze, last_autoanalyze from pg_stat_user_tables where relname in ('orders','order_items','events');"
   relname    |     last_analyze      |     last_autoanalyze
--------------+-----------------------+--------------------------
 orders       |                       | 2025-12-29 23:03:09+00
 order_items  | 2025-12-28 01:12:43+00| 2025-12-30 08:13:44+00
 events       |                       | 2025-12-30 08:13:44+00
(3 rows)

Що це означає: Якщо analyze застарілий, планувальник хибно оцінює кількість рядків, що веде до поганих виборів планів і типів сканів.

Рішення: Виконайте ручний ANALYZE для критичних таблиць після масових завантажень або великих змін; налаштуйте autovacuum_analyze_scale_factor там, де потрібно.

Завдання 15: Виміряти відсоток попаду в кеш (з часткою скептицизму)

cr0x@server:~$ psql "$DATABASE_URL" -X -c "select datname, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) as cache_hit_pct from pg_stat_database where datname=current_database();"
 datname | cache_hit_pct
---------+---------------
 appdb   |         98.71
(1 row)

Що це означає: Високий відсоток попаду в кеш — добре, але він може приховувати проблеми: ви можете бути 99% в кеші й при цьому повільними через блокування або CPU.

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

Завдання 16 (специфічно для RDS): витягнути останні логи движка для autovacuum і чекпойнтів

cr0x@server:~$ aws rds describe-db-log-files --db-instance-identifier prod-pg --file-last-written 1735530000000
{
  "DescribeDBLogFiles": [
    {
      "LogFileName": "postgresql.log.2025-12-30-08",
      "LastWritten": 1735543561000,
      "Size": 12839210
    }
  ]
}

Що це означає: Ви не можете tail /var/log як на самокерованому сервері. Ви використовуєте RDS API, щоб знайти потрібні сегменти логів.

Рішення: Якщо у вас не увімкнено логування autovacuum і чекпойнтів — увімкніть його зараз. Налаштування без логів — це марновірство з додатковими кроками.

Три корпоративні міні-історії з польових боїв

Міні-історія 1: Інцидент через хибне припущення

Компанія перевела основну OLTP-базу з самокерованого PostgreSQL на EC2 в RDS PostgreSQL. Міграція була перемогою: менше викликів на чергових, бекапи працювали, патчування перестало бути квартальним карнавалом страху. Через місяць настав перший справжній сплеск росту.

Команда вважала, що сховище RDS «просто масштабується». Воно масштабувалося по ємності. Продуктивність не масштабувалася так, як очікували. Під час трафікового сплеску p95 API латентності виріс, але CPU залишався помірним. Початкова реакція була класичною: «це не CPU, тож проблема в аплікації». Вони двічі відкотили деплой. Нічого не змінилося.

Performance Insights показав сесії, що чекали на IO. Латентність EBS підповзла вгору, а IOPS вперлися в ліміт тому. Робоче навантаження змінилося: більше записів на запит, більші рядки, вищий обіг. Autovacuum теж відстав, бо блоат ускладнив читання і зробив vacuum дорожчим.

Хибним було не припущення про відмову AWS. Хибним було припущення, що «кероване» означає «еластична продуктивність». Вирішення було нудним і ефективним: перейти на конфігурацію сховища з потрібними IOPS/throughput, відтонувати параметри чекпойнтів, щоб зменшити піки записів, і посилити autovacuum на найгарячіших таблицях. Також додали алерт на латентність сховища і насичення IOPS, щоб більше ніколи не відкривати фізику сховища через скарги користувачів.

Міні-історія 2: Оптимізація, що відгукнулася боком

Інша організація мала звичку: кожного разу, коли запит був повільним, хтось додавав індекс. Спочатку це працювало. Дашборди виглядали краще. Люди відчували себе продуктивними. Потім зріс записний трафік.

На RDS PostgreSQL вони додали кілька перекриваючих індексів на таблицю з високим обігом — варіації тих самих складених ключів, плюс кілька «на всякий випадок» індексів для звітності. Латентність записів повільно зростала. Autovacuum почав працювати довше. Чекпойнти стали важчими. Відставання реплік стало щоденним гостем.

Реакція була передбачуваною: кожен новий індекс збільшував write amplification. Кожен INSERT/UPDATE тепер мав більше обслуговування індексів. Vacuum мусив чистити більше сторінок індексів. Навантаження стало IO-bound, і рахунок за сховище ввічливо піднімався, коли вони «вирішували продуктивність», кидаючи IOPS на проблему.

Коли вони провели аудит використання індексів, кілька індексів мали майже нульових сканів. Команда видалила зайві індекси, замінила частину їх кращими складеними індексами, зорієнтованими на реальні предикати, і перемістила звітні запити на репліку з суворішими правилами виконання. Записи прискорилися, vacuum заспокоївся, а відставання реплік перестало бути рисою характеру.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Фінтех-компанія використовувала RDS PostgreSQL з Multi-AZ і реплікою для звітності. Нічого гучного. Їхня культура тюнінгу була ще менш гламурною: щотижневий перегляд топових SQL, щомісячні перевірки vacuum/analyze і політика, що кожна міграція повинна оголошувати свою поведінку щодо блокувань.

Потім інтеграція вендора почала бити по їхньому API сплесками трафіку, підсилюючи один ендпоінт з дорогим джойном. Запит не був катастрофічним окремо, але сплеск створив тектонічну гуртову хвилю ідентичних запитів. Кількість підключень зросла, активні сесії нагромадилися, і система ледь не дійшла до тайм-аутів.

Оскільки вони дотримувалися «нудних» практик, діагностика зайняла кілька хвилин: pg_stat_statements показав сплеск запиту, Performance Insights показав CPU плюс частковий IO, а дашборди вже розбивали навантаження БД за типами очікування. Вони застосували двочастинне рішення: цілеспрямований індекс, що відповідав предикату джойну, і зміни в аплікації для коалесценції запитів та кешування для того ендпоінта.

Найкраще: їм не знадобилася «військова кімната». Без героїзму. Без «ми перепишемо все на Rust». Вони просто дотрималися плейбуку, якого дотримувалися весь рік — це найближче до магії, яку дозволяють продакшен-системи.

Жарт #2: Єдина «налаштувати і забути» база — та, якою ніхто не користується. Як тільки вона стає популярною — це знову робота.

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

  • Симптом: Періодичні спайки латентності кожні кілька хвилин
    Корінна причина: Шторм чекпойнтів через малий max_wal_size або агресивний checkpoint_timeout
    Виправлення: Збільшити max_wal_size, підвищити checkpoint_timeout, встановити checkpoint_completion_target близько 0.9 і стежити за write latency та checkpoints_req.
  • Симптом: CPU низький, запити повільні, у чаті «база в порядку»
    Корінна причина: IO-bound навантаження: висока латентність EBS, обмеження IOPS/throughput, тимчасові виливання на диск або тиск на WAL
    Виправлення: Підтвердити через wait events і метрики сховища; зменшити IO через індекси/оптимізацію запитів, налаштувати work_mem, збільшити IOPS/throughput при потребі.
  • Симптом: Використання диску постійно зростає, запити повільніють протягом тижнів
    Корінна причина: Блоат таблиць/індексів від MVCC + відставання autovacuum або неправильно налаштовані пороги
    Виправлення: Налаштувати autovacuum по таблицях; запланувати обслуговування; перебудувати найгірші випадки з онлайн-безпечними методами, де це можливо.
  • Симптом: Раптовий простій під час деплою; багато сесій чекають на блокування
    Корінна причина: Міграція взяла ACCESS EXCLUSIVE блок (ALTER TABLE, побудова індексу без CONCURRENTLY, довга транзакція)
    Виправлення: Використовуйте CREATE INDEX CONCURRENTLY, backfill у батчах, встановлюйте lock_timeout і застосовуйте інструменти/процеси, орієнтовані на уникнення блокувань.
  • Симптом: Відставання репліки під час масових оновлень
    Корінна причина: Генерація WAL перевищує можливості застосування репліки; важкий vacuum/обслуговування індексів додає WAL
    Виправлення: Батчувати записи, зменшити зайві індекси, налаштувати checkpoint/WAL, і не відправляти критично свіжі читання на репліки без гарантій.
  • Симптом: Зростаючі тайм-аути при горизонтальному масштабуванні застосунку
    Корінна причина: Шторм підключень; занадто багато сесій і витрати пам’яті; блокування посилюються конкуруючою активністю
    Виправлення: Додати PgBouncer, обмежити паралелізм аплікації, зменшити max_connections, використовувати розумні розміри пулів і прибрати створення підключень на запит.
  • Симптом: План запиту раптово став гіршим після росту даних
    Корінна причина: Застаріла статистика або зміщені розподіли; планувальник хибно оцінює кардинальності
    Виправлення: RUN ANALYZE для критичних таблиць, збільшити statistics target для сильно зміщених колонок, перевірити предикати й індекси.
  • Симптом: «Ми додали індекс і записи стали повільніші»
    Корінна причина: Надмірне використання індексів спричиняє write amplification і навантаження на vacuum
    Виправлення: Проведіть аудит використання індексів; видаліть невикористовувані/надлишкові індекси; проектуйте складені індекси під реальні предикати; розгляньте часткові індекси.

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

Тиждень 1: Зробіть продуктивність спостережуваною

  1. Увімкніть pg_stat_statements (і переконайтеся, що воно переживе рестарт через shared_preload_libraries).
  2. Увімкніть логування повільних запитів з порогом, що відповідає вашим SLO; логувати тривалість і очікування блокувань.
  3. Увімкніть логування autovacuum (принаймні для довгих вакуумів) і логування чекпойнтів.
  4. Налаштуйте дашборди: DB load, wait events, активні сесії, IOPS/throughput, латентність сховища, replica lag.
  5. Визначте, хто відповідає за «перегляд топових запитів» щотижня. Якщо це «нікому», згодом це стане «командиром інциденту».

Тиждень 2: Зупиніть кровотечу (високий ROI)

  1. З pg_stat_statements відберіть топ-5 за total time і топ-5 за mean time; виправте щонайменше два.
  2. Додавайте або виправляйте індекси тільки після аналізу EXPLAIN і підтвердження відповідності предикатам.
  3. Зменшіть кількість підключень: впровадьте пулінг і встановіть розумні розміри пулів для кожної сервісної частини.
  4. Виявте таблиці з найвищим обігом; встановіть per-table autovacuum thresholds і проаналізуйте пороги.

Тиждень 3–4: Зробіть це рутинним

  1. Створіть політику міграцій: lock_timeout, statement_timeout, create index concurrently, backfills у батчах.
  2. Плануйте потужності сховища: IOPS/throughput; налаштуйте алерти до насичення, а не після.
  3. Перегляньте налаштування WAL/чекпойнтів; усуньте шаблони, що примушують чекпойнти.
  4. Встановіть культуру «бюджету запитів»: ендпоінти з важкими операціями в БД отримують тестування продуктивності та обмеження.

FAQ

1) Чи RDS PostgreSQL повільніший за самокерований PostgreSQL?

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

2) Які ручки я втрачаю на RDS, що мають значення?

Ви втрачаєте контроль на рівні хоста: вибір файлової системи, параметри ядра, прямі дискові інструменти і деяку гнучкість розширень. Більшість перемог у продуктивності все-таки приходить від дизайну запитів/індексів, стратегії autovacuum і налаштувань IO/WAL — ручок, які у вас залишаються.

3) Чи справді потрібен pg_stat_statements на RDS?

Так. Без нього ви все ще можете гадати за логами, але витратите час і виправлятимете напівзаходами. pg_stat_statements дає ранжування за total time і mean time — саме те, за чим пріоритезують роботу.

4) Чи варто «накрутити» work_mem, щоб уникнути тимчасових виливів?

Тільки з підрахунками. work_mem — це на операцію, на запит, на сесію. Якщо його підняти сильно при високій конкуренції, ви можете OOMнути інстанс або змусити ОС до неприємного свопінгу. Віддавайте перевагу цільовим виправленням: переписанню запитів, індексам або перелікам налаштувань для конкретних ролей.

5) Чому CPU низький, а латентність висока?

Тому що база чекає, а не працює. Очікування блокувань, IO та мережеві затримки не відображаються як CPU. Використовуйте wait events і метрики сховища, щоб знайти реальне обмеження.

6) Чи безпечні read replicas для масштабування читань?

Вони корисні, але не безкоштовні. Відставання реплік реальне, і ваш застосунок має толерувати застарілість або направляти критично свіжі запити на primary. Також важкі звітні запити можуть нашкодити репліці і створити операційний шум.

7) Яка найбільша помилка з autovacuum на RDS?

Покладання на значення за замовчуванням для таблиць з високим обігом. За замовчуванням параметри консервативні, аби уникнути несподіваного навантаження. Продакшен-робочі навантаження рідко бувають консервативними. Встановіть per-table пороги для таблиць, що мають значення, і перевіряйте логи та тренди мертвих кортежів.

8) Чи можу я «просто збільшити» інстанс RDS, щоб виправити продуктивність?

Іноді так. Масштабування вгору допомагає у CPU- та memory-bound випадках. Воно не вирішує блокування, погані запити, вимушені чекпойнти або обмеження сховища. Масштабуйте як частину діагностики, а не замість неї.

9) Як запобігти падінню бази під час міграцій?

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

10) Яка перша метрика для алерту «база повільна»?

Навантаження бази за типом очікування (з Performance Insights або еквіваленту) плюс латентність сховища. «CPU > 80%» сам по собі — чудовий спосіб швидко помилитися.

Висновок: кроки, які можна зробити цього тижня

Самокерований PostgreSQL дає більше важелів; RDS дає менше важелів і більше запобіжних поручнів. У будь-якому випадку база — це жива система. Вона накопичує історію (MVCC), регулює конкурентність (блокування) і залежить від фізики сховища (IO і WAL). Налаштування продуктивності залишаються вашою відповідальністю, бо навантаження — ваше.

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

  1. Увімкніть pg_stat_statements і логування повільних запитів. Зробіть неможливим сперечання про те, що саме повільно.
  2. Під час наступного уповільнення пройдіть план швидкої діагностики і зафіксуйте результат: CPU, IO, блокування чи підключення.
  3. Виправте два топ-запити за допомогою EXPLAIN-орієнтованих змін, а не фольклору.
  4. Виберіть три таблиці з найвищим обігом і налаштуйте autovacuum для них; підтвердіть за мертвими кортежами й логами vacuum.
  5. Перевірте співвідношення checkpoints_req до checkpoints_timed; налаштуйте WAL/чекпойнти, щоб зменшити піки записів.
  6. Обмежте й пулінгуйте підключення. Ваша база — не чат.
← Попередня
Nginx для WordPress: помилки конфігурації, що спричиняють 5xx (і як їх виправити)
Наступна →
Обмін файлами по VPN між офісами: стабільний SMB без постійних відключень

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