PostgreSQL: архитектура и инструменты разработчика
PostgreSQL — многопроцессная клиент-серверная СУБД, в которой каждое соединение обслуживается отдельным OS-процессом. psql, pg_stat_activity, pg_stat_statements и системные представления — ключевые инструменты ежедневной работы и диагностики. Понимание внутренней архитектуры — postmaster, WAL, shared buffers, autovacuum — позволяет правильно настраивать и эффективно диагностировать проблемы производительности. Без этого фундамента невозможно объяснить, почему VACUUM важен, зачем нужен PgBouncer, и как читать pg_stat_activity.
Почему это важно: Знание архитектуры PostgreSQL напрямую влияет на стоимость эксплуатации и скорость устранения инцидентов. Команда, не понимающая как работают процессы и соединения, регулярно получает проблемы: memory overcommit при 500 соединениях, table bloat из-за отключённого autovacuum, невозможность point-in-time recovery при неправильном бэкапе. Инструменты pg_stat_* дают полную видимость в реальном времени: какие запросы выполняются, что блокирует, сколько мёртвых туплов накоплено. Разработчик, умеющий читать эти представления, находит причину инцидента за минуты, а не часы.
Главная идея
Архитектура PostgreSQL построена вокруг нескольких ключевых процессов. Postmaster — главный процесс, который принимает TCP-соединения и fork'ает для каждого нового соединения отдельный backend-процесс. Это означает, что 500 соединений = 500 OS-процессов, каждый с ~5MB памяти только для стека. WAL writer асинхронно сбрасывает записи журнала на диск; checkpoint-процесс периодически синхронизирует dirty pages из shared buffers на диск. Autovacuum — фоновый daemon, который отслеживает таблицы с накопившимися мёртвыми туплами (результат UPDATE и DELETE) и запускает VACUUM/ANALYZE по расписанию. Системные представления pg_stat_* — это окно внутрь работающего PostgreSQL в реальном времени. pg_stat_activity показывает каждый backend-процесс: текущий запрос, состояние (active/idle/idle in transaction), событие ожидания (Lock, IO, Client). pg_stat_user_tables хранит статистику по таблицам: счётчики seq_scan vs idx_scan, количество мёртвых туплов (n_dead_tup), время последнего VACUUM и ANALYZE. pg_stat_statements (расширение) агрегирует статистику по нормализованным запросам: суммарное и среднее время, количество вызовов, объём I/O.
Как это выглядит на практике
- Подключение через psql: выполни psql -U postgres -d mydb. После подключения используй мета-команды: \dt выводит все таблицы схемы, \d users показывает структуру таблицы users со всеми столбцами, типами и индексами, \timing включает вывод времени каждого запроса.
- Настройка psql для комфортной работы: добавь в ~/.psqlrc строки \set PROMPT1 '%[%033[1m%]%/%[%033[0m%]> ' и \pset pager off чтобы отключить постраничный вывод. Опция \x auto включает расширенный режим вывода для широких таблиц.
- Найди все активные запросы через pg_stat_activity: SELECT pid, usename, state, wait_event_type, wait_event, left(query, 80) AS query FROM pg_stat_activity WHERE state != 'idle' ORDER BY query_start. Это первый шаг при диагностике любого замедления.
- Убей зависший запрос: SELECT pg_cancel_backend(pid) для мягкой отмены (как Ctrl+C) или SELECT pg_terminate_backend(pid) для принудительного завершения. Всегда предпочитай cancel — terminate завершает соединение полностью.
- Проверь статистику по таблицам: SELECT relname, seq_scan, idx_scan, n_dead_tup, n_live_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10. Таблицы с большим n_dead_tup и редким autovacuum — кандидаты на ручной VACUUM ANALYZE.
- Включи и используй pg_stat_statements: добавь shared_preload_libraries = 'pg_stat_statements' в postgresql.conf, перезапусти PostgreSQL, выполни CREATE EXTENSION pg_stat_statements. После этого SELECT query, calls, total_exec_time/calls AS avg_ms, rows/calls AS avg_rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10 показывает самые дорогие запросы.
- Сброс статистики: SELECT pg_stat_statements_reset() очищает накопленную статистику. Удобно делать в начале нагрузочного теста, чтобы видеть только профиль нового кода.
- Проверь использование памяти: SELECT sum(pg_database_size(datname))/1024/1024 AS total_db_mb FROM pg_database показывает размер всех БД. SELECT pg_size_pretty(pg_total_relation_size('orders')) показывает полный размер таблицы включая индексы.
- Мониторинг блокировок: SELECT bl.pid AS blocked_pid, ka.query AS blocking_query, a.query AS blocked_query FROM pg_catalog.pg_locks bl JOIN pg_catalog.pg_stat_activity a ON a.pid = bl.pid JOIN pg_catalog.pg_locks kl ON kl.transactionid = bl.transactionid AND kl.pid != bl.pid JOIN pg_catalog.pg_stat_activity ka ON ka.pid = kl.pid WHERE NOT bl.granted. Этот запрос показывает заблокированные процессы и запросы, которые их блокируют.
- Настрой autovacuum для активных таблиц через ALTER TABLE: ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.005). Для больших таблиц с частыми UPDATE снижай scale_factor — autovacuum будет запускаться чаще и не даст накапливаться мёртвым туплам.
- Используй \watch для периодического выполнения: SELECT count(*) FROM pg_stat_activity WHERE state = 'active'; \watch 2 выполняет этот запрос каждые 2 секунды. Удобно для наблюдения за нагрузкой в реальном времени.
- Установи PgBouncer: в pgbouncer.ini задай pool_mode = transaction, max_client_conn = 1000, default_pool_size = 20. Это позволяет 1000 клиентских соединений поддерживать всего 20 реальных соединений к PostgreSQL.
Примеры кода
Мониторинг активных запросов и блокировок
-- Все активные (не idle) соединения
SELECT
pid,
usename,
application_name,
state,
wait_event_type,
wait_event,
now() - query_start AS duration,
left(query, 100) AS query
FROM pg_stat_activity
WHERE state != 'idle'
AND pid != pg_backend_pid()
ORDER BY duration DESC NULLS LAST;
Показывает все активные запросы с длительностью выполнения и текущим событием ожидания. wait_event_type='Lock' означает ожидание блокировки от другого процесса — первый признак deadlock или contention.
Топ медленных запросов через pg_stat_statements
SELECT
round(total_exec_time::numeric / calls, 2) AS avg_ms,
calls,
round(total_exec_time::numeric, 2) AS total_ms,
rows / calls AS avg_rows,
left(query, 120) AS query
FROM pg_stat_statements
WHERE calls > 50
ORDER BY total_exec_time DESC
LIMIT 15;
Находит запросы с наибольшим суммарным временем выполнения. Это не обязательно самые медленные по разу — это запросы, которые суммарно отнимают больше всего ресурсов БД. calls > 50 отфильтровывает редкие одноразовые запросы.
Таблицы с накопившимися мёртвыми туплами
SELECT
schemaname,
relname AS table_name,
n_dead_tup,
n_live_tup,
round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
last_autovacuum,
last_autoanalyze
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC
LIMIT 20;
Выявляет таблицы с высокой долей мёртвых туплов. dead_pct > 10% — признак того, что autovacuum не справляется или отключён. Для таких таблиц нужен ручной VACUUM ANALYZE или настройка autovacuum_vacuum_scale_factor.
Размеры таблиц и индексов
SELECT
relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
pg_size_pretty(pg_indexes_size(relid)) AS indexes_size,
pg_size_pretty(
pg_total_relation_size(relid) - pg_relation_size(relid)
) AS bloat_estimate
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;
Показывает реальный размер таблиц и индексов. Если indexes_size > table_size, стоит проверить, все ли индексы действительно используются через pg_stat_user_indexes.
Cache hit ratio — эффективность shared buffers
SELECT
relname,
heap_blks_read AS disk_reads,
heap_blks_hit AS cache_hits,
round(
heap_blks_hit * 100.0 /
NULLIF(heap_blks_hit + heap_blks_read, 0), 2
) AS cache_hit_pct
FROM pg_statio_user_tables
WHERE heap_blks_hit + heap_blks_read > 0
ORDER BY heap_blks_read DESC
LIMIT 10;
Cache hit ratio < 90% для горячих таблиц означает, что данные часто читаются с диска. Нужно увеличить shared_buffers или проверить, не вытесняются ли данные большими аналитическими запросами.
Что происходит под капотом
- Postmaster — главный процесс PostgreSQL. При новом TCP-соединении postmaster вызывает fork(), создавая дочерний backend-процесс, который полностью обслуживает это соединение. Каждый backend имеет свой стек (~5MB), локальные переменные и указатель в shared buffers. Это архитектурное решение даёт изоляцию, но ограничивает масштабирование при тысячах соединений.
- Shared buffers — общий кеш страниц данных, доступный всем backend-процессам через общую память (shared memory). Дефолт 128MB катастрофически мал для продакшена — рекомендуется 25% RAM. При cache miss PostgreSQL читает 8KB-страницу с диска в shared buffers, откуда её читают все последующие запросы.
- WAL (Write-Ahead Log) — журнал изменений. Каждое изменение данных сначала записывается в WAL, и только потом в shared buffers (dirty page). WAL writer периодически сбрасывает WAL-буфер на диск. Checkpoint-процесс раз в несколько минут (max_wal_size) синхронизирует все dirty pages с диска. WAL — основа ACID durability и физической репликации.
- Autovacuum daemon периодически сканирует pg_stat_user_tables и запускает VACUUM для таблиц, где n_dead_tup > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tup. По умолчанию scale_factor = 0.2, т.е. VACUUM запускается при накоплении 20% мёртвых туплов. Для больших таблиц (10M+ строк) это слишком поздно — нужно снижать scale_factor до 0.01–0.05.
- pg_stat_statements — расширение, которое перехватывает каждый SQL-запрос, нормализует его (заменяет литералы на $1, $2), и обновляет счётчики в общей памяти. Нормализация позволяет группировать SELECT * FROM orders WHERE id = 1 и SELECT * FROM orders WHERE id = 999 как один тип запроса. Требует shared_preload_libraries = 'pg_stat_statements' и перезапуска.
- Background writer (bgwriter) — процесс, который проактивно записывает dirty pages из shared buffers на диск, чтобы checkpoint не создавал резкого всплеска I/O. bgwriter_lru_maxpages и bgwriter_delay — параметры настройки.
- Процесс stats collector собирает статистику (счётчики таблиц, запросов, I/O) через UDP-сокеты от каждого backend и записывает в shared memory. Именно он питает все pg_stat_* представления.
- Logical replication worker и WAL sender — процессы для репликации. WAL sender читает WAL и отправляет изменения на standby-серверы. Logical replication worker применяет изменения на подписчиков при публикации/подписке.
- Walwriter flush на каждый COMMIT по умолчанию (synchronous_commit = on): PostgreSQL ждёт подтверждения записи WAL на диск перед ответом клиенту. Это гарантирует durability, но добавляет latency. synchronous_commit = off даёт ~10x больший throughput для некритичных данных ценой потери последних ~1-3 секунд при crash.
- PgBouncer в transaction pooling mode: соединение из пула выдаётся backend-процессу только на время транзакции и немедленно возвращается в пул после COMMIT/ROLLBACK. Это позволяет 1000 приложений работать через 20-50 реальных PostgreSQL-процессов. Несовместимо с SET LOCAL, временными таблицами, LISTEN/NOTIFY — для этих случаев используй session pooling mode.
Типичные ошибки и заблуждения
- Ошибка: PostgreSQL нормально работает при тысячах прямых соединений. Каждое соединение — это OS-процесс с 5-10MB памяти только для стека и структур. 1000 соединений = 5-10GB RAM только на обслуживание соединений, плюс context switching overhead. PostgreSQL начинает деградировать уже при 200-300 активных процессах. PgBouncer — не опция, а необходимость для продакшена.
- Ошибка: VACUUM нужен только когда таблица явно тормозит. Autovacuum нужен постоянно для трёх целей: очистка мёртвых туплов (bloat), обновление статистики для планировщика (ANALYZE), и предотвращение transaction ID wraparound (критическая ошибка через 2 миллиарда транзакций). Его отключение приводит к катастрофе в продакшене.
- Ошибка: pg_dump — достаточный бэкап для продакшена. pg_dump создаёт логический снимок — если он упадёт в 23:00, ты потеряешь данные с 00:00. Point-in-time recovery требует pg_basebackup (физический снимок) + WAL archiving. Только тогда можно восстановиться на любой момент времени.
- Ошибка: shared_buffers нужно ставить как можно больше (50-80% RAM). OS page cache тоже кеширует файлы PostgreSQL, и иногда более эффективно. Золотая середина: shared_buffers = 25% RAM, остальное оставить OS для page cache. Значения выше 40% часто дают меньший выигрыш и могут навредить.
- Ошибка: EXPLAIN ANALYZE достаточно без BUFFERS. EXPLAIN (ANALYZE, BUFFERS) — намного информативнее: видно сколько блоков прочитано из кеша (shared hit) и с диска (shared read). Это ключевая информация при диагностике I/O-bound запросов.
- Ошибка: idle соединения не нагружают PostgreSQL. Idle-соединения удерживают slot в pg_stat_activity, потребляют память процесса, и при достижении max_connections новые соединения отклоняются с ошибкой. idle_in_transaction_session_timeout = '5min' — обязательная настройка.
Ключевые выводы
- Каждое соединение к PostgreSQL — отдельный OS-процесс с ~5-10MB памяти; при > 100 соединениях используй PgBouncer в transaction mode.
- pg_stat_statements — первый инструмент при любом расследовании производительности; ищи запросы с наибольшим total_exec_time, а не просто avg_ms.
- Autovacuum должен работать постоянно и успевать за темпом изменений; следи за n_dead_tup в pg_stat_user_tables и снижай autovacuum_vacuum_scale_factor для горячих таблиц.
- Cache hit ratio (heap_blks_hit / (heap_blks_hit + heap_blks_read)) должен быть > 95% для горячих таблиц; ниже — признак недостаточного shared_buffers или отсутствия нужных индексов.
- WAL — фундамент надёжности: без него нет durability, репликации и PITR-бэкапов. synchronous_commit = off только для данных, потеря которых допустима.
- psql с метакомандами (\dt, \d, \timing, \x, \watch) — полноценная среда для ежедневной работы; GUI нужен только для визуализации ERD.
- idle_in_transaction_session_timeout обязателен в продакшене: транзакции, зависшие в idle state, удерживают snapshot и блокируют VACUUM.
- pg_basebackup + WAL archiving = настоящий бэкап; pg_dump = логический снимок без point-in-time recovery.
Термины урока
Связь с работой backend-разработчика
Обязательный минимум для продакшен-PostgreSQL: включи pg_stat_statements в shared_preload_libraries (добавляется один раз при деплое), поставь PgBouncer перед сервером при > 30 одновременных соединениях, настрой мониторинг n_dead_tup и cache_hit_ratio. Эти три действия дают видимость и защиту от наиболее распространённых проблем. Для диагностики любого инцидента производительности используй следующий алгоритм: 1) pg_stat_activity — что происходит прямо сейчас, есть ли блокировки. 2) pg_stat_statements — какие запросы суммарно отнимают больше всего времени. 3) EXPLAIN (ANALYZE, BUFFERS) для конкретного проблемного запроса. 4) pg_stat_user_tables — нет ли bloat. Этот цикл закрывает 90% проблем производительности. Для бэкапа: pg_dump подходит для разработки и небольших проектов, но в продакшене нужен pg_basebackup + WAL archiving (через pgBackRest или Barman). Только так обеспечивается point-in-time recovery — возможность восстановиться на любой момент, а не только на момент последнего снимка.
Мини-разбор реальной ситуации
SaaS-платформа на Rails с PostgreSQL на AWS RDS: приложение деплоилось с 50 Puma-воркерами, каждый с пулом из 5 соединений = 250 соединений к PostgreSQL. max_connections = 200 — при пиковой нагрузке новые запросы получали 'FATAL: sorry, too many clients already'. Команда увеличила max_connections до 500, потребление памяти возросло на 3GB, PostgreSQL начал деградировать из-за context switching. Правильное решение: PgBouncer в transaction mode с pool_size=20. Реальных соединений к PostgreSQL стало 20, все 250 воркеров обслуживаются через пул. Потребление памяти уменьшилось на 2.5GB, latency p99 снизилась с 800ms до 90ms. Дополнительно включили pg_stat_statements и нашли три запроса, которые суммарно занимали 70% всего времени PostgreSQL — добавление составных индексов убрало ещё 60% нагрузки.
Что запомнить
- PgBouncer в transaction mode — обязателен при > 30 одновременных соединениях; увеличение max_connections не решает проблему, а усугубляет её.
- pg_stat_statements показывает total_exec_time, а не только avg_ms — ищи запросы с наибольшим суммарным временем.
- Autovacuum — не враг производительности; медленный autovacuum лучше, чем его отсутствие; снижай scale_factor для горячих таблиц.
- Cache hit ratio < 95% — сигнал к увеличению shared_buffers или пересмотру запросов, делающих full scan.
- EXPLAIN (ANALYZE, BUFFERS) всегда информативнее чем EXPLAIN ANALYZE — добавляй BUFFERS чтобы видеть disk vs cache reads.
- pg_dump != бэкап продакшена; используй pg_basebackup + WAL archiving для point-in-time recovery.
- idle_in_transaction_session_timeout = '5min' — установи в postgresql.conf; зависшие транзакции блокируют autovacuum для всей базы.
Итог
Понимание архитектуры PostgreSQL — фундамент для правильной настройки, диагностики и масштабирования. Инструменты pg_stat_* дают полную видимость происходящего внутри; умение их читать отличает разработчика, который угадывает причину проблем, от того, кто точно знает, где искать.