База 500 ГБ тормозит: оптимизация PostgreSQL для SaaS-стартапа

Задача клиента

В начале 2026 года к нам в АйТи Фреш пришёл московский SaaS-стартап — ребята делали платформу для управления проектами. За полтора года они выросли с 500 до 8 000 активных аккаунтов, а их PostgreSQL-база распухла до 500 ГБ. И вот тут всё начало рассыпаться.

Картина была неприятная. Клиент пришёл сразу с несколькими критическими проблемами:

  • Деградация производительности — время отклика API ползло с 50 мс до 800 мс, пользователи писали в поддержку про «тормоза» и грозились уходить
  • Тайм-ауты подключений — стоило нагрузке перевалить за 300 одновременных соединений, и база начинала отказывать. В пиковые часы — стабильно
  • Медленные аналитические запросы — отчёты формировались по 5 минут. Это не отчёт, это кофе-пауза
  • Ненадёжные бэкапы — резервное копирование делалось вручную, когда кто-то вспоминал. Примерно раз в неделю, если повезёт
  • Дефолтная конфигурация — PostgreSQL крутился с настройками out-of-the-box на сервере с 32 ГБ RAM. То есть железо куплено, а используется от силы процентов десять

Мы провели полный аудит базы и составили конкретный план: тонкая настройка postgresql.conf под реальное железо, внедрение PgBouncer для connection pooling, потоковая репликация на резервный сервер, автоматизация бэкапов и мониторинг через Prometheus + Grafana. Без общих слов — только то, что реально влияет на производительность.

Установка PostgreSQL из официального репозитория

PostgreSQL — это не просто «бесплатный MySQL». ACID-транзакции, JSONB, полнотекстовый поиск, расширения, логическая репликация — всё это есть из коробки. Но у клиента стояла версия 14, хотя к тому моменту уже была 16-я с заметными улучшениями планировщика. Первым делом обновили PostgreSQL, установив его из официального репозитория PGDG.

Вот как мы установили PostgreSQL 16

Почему именно PGDG, а не пакеты из стандартных репозиториев дистрибутива? Всё просто: PostgreSQL Global Development Group поддерживает актуальные версии и оперативно закрывает дыры безопасности. Устанавливайте отсюда:

# Добавляем официальный репозиторий PostgreSQL
sudo apt install -y curl ca-certificates gnupg
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /usr/share/keyrings/postgresql-keyring.gpg

# Для Debian 12 (bookworm)
echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list

# Обновляем и устанавливаем PostgreSQL 16
sudo apt update
sudo apt install -y postgresql-16 postgresql-client-16 postgresql-contrib-16

# Проверяем статус
sudo systemctl status postgresql
sudo -u postgres psql -c "SELECT version();"
# PostgreSQL 16.x on x86_64-pc-linux-gnu

Пересоздание базы данных с правильными параметрами

# Входим под пользователем postgres
sudo -u postgres psql

-- Создаём пользователя приложения
CREATE USER app_user WITH PASSWORD 'SecureP@ssw0rd!' LOGIN;

-- Создаём базу данных
CREATE DATABASE app_production
    OWNER = app_user
    ENCODING = 'UTF8'
    LC_COLLATE = 'ru_RU.UTF-8'
    LC_CTYPE = 'ru_RU.UTF-8'
    TEMPLATE = template0;

-- Даём права
GRANT ALL PRIVILEGES ON DATABASE app_production TO app_user;

-- Создаём отдельную схему
\c app_production
CREATE SCHEMA app AUTHORIZATION app_user;
ALTER USER app_user SET search_path TO app, public;

-- Устанавливаем полезные расширения
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS btree_gist;

\q

Тонкая настройка postgresql.conf — главный этап оптимизации

Дефолтные настройки PostgreSQL — это такой минимализм для сервера образца 2005 года с 512 МБ RAM. На сервере клиента стояло 32 ГБ RAM и SSD — и всё это богатство простаивало. PostgreSQL реально использовал процентов десять возможностей. Вот что мы поменяли:

Параметры памяти — ключевые изменения

# /etc/postgresql/16/main/postgresql.conf

# === Память ===

# Общий буферный кеш — 25% от RAM
shared_buffers = 8GB

# Память для операций сортировки и хеширования на каждый запрос
work_mem = 64MB

# Память для операций обслуживания (VACUUM, CREATE INDEX)
maintenance_work_mem = 2GB

# Эффективный размер кеша (shared_buffers + кеш ОС), ~75% RAM
effective_cache_size = 24GB

# Размер WAL-буферов
wal_buffers = 64MB

# Максимальное количество подключений
max_connections = 200

# Огромные страницы — снижает overhead управления памятью
huge_pages = try

Формулы расчёта, которые мы применили:

  • shared_buffers = RAM x 0.25 (не более 8 ГБ на типичных нагрузках)
  • work_mem = (RAM - shared_buffers) / (max_connections x 2)
  • effective_cache_size = RAM x 0.75
  • maintenance_work_mem = RAM / 16 (но не более 2 ГБ)

Параметры WAL и контрольных точек

# === WAL (Write-Ahead Log) ===

# Уровень WAL — для репликации и бэкапов
wal_level = replica

# Количество WAL-файлов для репликации
max_wal_senders = 5
max_replication_slots = 5

# Контрольные точки
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min

# Максимальный размер WAL между контрольными точками
max_wal_size = 4GB
min_wal_size = 1GB

# Компрессия WAL
wal_compression = zstd

# Синхронизация записи на диск
synchronous_commit = on
fsync = on
full_page_writes = on

Параметры планировщика и параллелизма

# === Планировщик запросов ===

# Стоимость случайного чтения (для SSD — 1.1, для HDD — 4.0)
random_page_cost = 1.1

# Стоимость последовательного чтения
seq_page_cost = 1.0

# Эффективный размер кеша ОС
effective_io_concurrency = 200  # для SSD

# === Параллельные запросы ===
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
max_parallel_maintenance_workers = 4
parallel_tuple_cost = 0.01
parallel_setup_cost = 100
min_parallel_table_scan_size = 8MB
min_parallel_index_scan_size = 512kB

# === Статистика ===
default_statistics_target = 200
track_activities = on
track_counts = on
track_io_timing = on
track_wal_io_timing = on

Параметры логирования для выявления медленных запросов

# === Логирование ===
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_rotation_size = 100MB

# Логирование медленных запросов
log_min_duration_statement = 500  # мс
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 250

# Формат для pgBadger
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '
log_statement = 'ddl'

После правок в конфиге перезапускаем PostgreSQL — часть параметров применяется только при рестарте:

sudo systemctl restart postgresql
# Для параметров, не требующих рестарта:
sudo -u postgres psql -c "SELECT pg_reload_conf();"

Настройка pg_hba.conf и усиление безопасности

Когда мы открыли pg_hba.conf, брови поползли вверх. Подключения разрешались с любого IP, аутентификация — md5. Для продакшн SaaS-платформы это откровенно плохо. Ужесточили правила сразу:

Применённые правила аутентификации

# /etc/postgresql/16/main/pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# Локальные подключения через unix-сокет
local   all             postgres                                peer
local   all             all                                     scram-sha-256

# IPv4 подключения
host    all             all             127.0.0.1/32            scram-sha-256

# Приложение из локальной сети
host    app_production  app_user        10.0.0.0/24             scram-sha-256

# Репликация
host    replication     repl_user       10.0.0.2/32             scram-sha-256

# Запрещаем всё остальное
host    all             all             0.0.0.0/0               reject
# Применяем изменения
sudo -u postgres psql -c "SELECT pg_reload_conf();"

# Включаем scram-sha-256 (более безопасный, чем md5)
sudo -u postgres psql -c "ALTER SYSTEM SET password_encryption = 'scram-sha-256';"
sudo -u postgres psql -c "SELECT pg_reload_conf();"

SSL-шифрование подключений

# Генерируем самоподписанный сертификат
sudo -u postgres openssl req -new -x509 -days 3650 -nodes \
    -out /etc/postgresql/16/main/server.crt \
    -keyout /etc/postgresql/16/main/server.key \
    -subj '/CN=pg-server.example.com'

sudo chmod 600 /etc/postgresql/16/main/server.key
sudo chown postgres:postgres /etc/postgresql/16/main/server.*

# В postgresql.conf
ssl = on
ssl_cert_file = '/etc/postgresql/16/main/server.crt'
ssl_key_file = '/etc/postgresql/16/main/server.key'
ssl_min_protocol_version = 'TLSv1.3'

sudo systemctl restart postgresql

В pg_hba.conf заменили host на hostssl — теперь все подключения идут только по SSL, без вариантов.

Потоковая репликация для отказоустойчивости

8 000 пользователей на платформе — это не тот случай, когда можно работать без реплики. Мы настроили потоковую репликацию на второй сервер. При падении мастера replica поднимается в роль primary за считанные минуты. На практике это разница между «упали на час» и «никто ничего не заметил».

Вот что мы настроили на мастере и реплике

На мастере (primary):

# Создаём пользователя для репликации
sudo -u postgres psql -c "CREATE USER repl_user WITH REPLICATION PASSWORD 'ReplP@ss123!';"

# postgresql.conf на мастере
wal_level = replica
max_wal_senders = 5
max_replication_slots = 5
hot_standby = on
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'

# Создаём слот репликации
sudo -u postgres psql -c "SELECT pg_create_physical_replication_slot('replica1');"

На реплике (standby):

# Останавливаем PostgreSQL на реплике
sudo systemctl stop postgresql

# Удаляем старые данные
sudo rm -rf /var/lib/postgresql/16/main/*

# Создаём базовую копию с мастера
sudo -u postgres pg_basebackup -h 10.0.0.1 -U repl_user \
    -D /var/lib/postgresql/16/main \
    -Fp -Xs -P -R -S replica1

# Ключ -R автоматически создаёт standby.signal и настраивает подключение

# Запускаем реплику
sudo systemctl start postgresql

# Проверяем статус репликации на мастере
sudo -u postgres psql -c "SELECT * FROM pg_stat_replication;"

Мониторинг отставания реплики

-- На мастере: проверяем отставание реплик
SELECT
    client_addr,
    state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes,
    pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replay_lag
FROM pg_stat_replication;

-- На реплике: проверяем статус
SELECT
    pg_is_in_recovery() AS is_replica,
    pg_last_wal_receive_lsn() AS received_lsn,
    pg_last_wal_replay_lsn() AS replayed_lsn,
    pg_last_xact_replay_timestamp() AS last_replay_time,
    now() - pg_last_xact_replay_timestamp() AS replay_delay;

Автоматизация резервного копирования

Бэкапы раз в неделю вручную — это, если честно, не бэкапы, а лотерея. Для SaaS с тысячами пользователей потеря даже суточных данных означает катастрофу. Мы настроили автоматическое ежедневное копирование с ротацией, чтобы это больше не зависело от памяти конкретного разработчика.

Логические бэкапы с pg_dump

# Бэкап одной базы данных в custom-формате (сжатый, параллельный)
pg_dump -U postgres -Fc -Z 9 -j 4 \
    -f /backup/app_production_$(date +%Y%m%d_%H%M%S).dump \
    app_production

# Бэкап в plain SQL (читаемый формат)
pg_dump -U postgres --no-owner --no-privileges \
    -f /backup/app_production.sql \
    app_production

# Бэкап всех баз данных
pg_dumpall -U postgres -f /backup/all_databases_$(date +%Y%m%d).sql

# Бэкап только схемы (без данных)
pg_dump -U postgres --schema-only -f /backup/schema.sql app_production

# Бэкап конкретных таблиц
pg_dump -U postgres -t 'app.orders' -t 'app.users' \
    -Fc -f /backup/tables_$(date +%Y%m%d).dump app_production

Восстановление из бэкапа

# Восстановление из custom-формата
pg_restore -U postgres -d app_production -j 4 --clean --if-exists \
    /backup/app_production_20260331.dump

# Восстановление из SQL
psql -U postgres -d app_production -f /backup/app_production.sql

# Восстановление в новую базу
createdb -U postgres app_production_restored
pg_restore -U postgres -d app_production_restored -j 4 \
    /backup/app_production_20260331.dump

Скрипт автоматического резервного копирования, который мы написали

#!/bin/bash
# /usr/local/bin/pg-backup.sh

set -euo pipefail

BACKUP_DIR="/backup/postgresql"
RETENTION_DAYS=14
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/pg-backup.log"

mkdir -p "$BACKUP_DIR"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Начало резервного копирования"

# Получаем список баз данных
DATABASES=$(sudo -u postgres psql -At -c "SELECT datname FROM pg_database WHERE NOT datistemplate AND datname != 'postgres';")

for DB in $DATABASES; do
    BACKUP_FILE="${BACKUP_DIR}/${DB}_${DATE}.dump"
    log "Бэкап базы: $DB"

    sudo -u postgres pg_dump -Fc -Z 6 -j 4 -f "$BACKUP_FILE" "$DB" 2>&1 | tee -a "$LOG_FILE"

    SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
    log "Готово: $BACKUP_FILE ($SIZE)"
done

# Удаляем старые бэкапы
log "Удаление бэкапов старше ${RETENTION_DAYS} дней"
find "$BACKUP_DIR" -name "*.dump" -mtime +${RETENTION_DAYS} -delete

# Бэкап глобальных объектов (роли, табличные пространства)
sudo -u postgres pg_dumpall --globals-only -f "${BACKUP_DIR}/globals_${DATE}.sql"

log "Резервное копирование завершено успешно"

# Опционально: отправка на удалённое хранилище
# rsync -az "$BACKUP_DIR/" backup-server:/backup/pg/
# Добавляем в cron (ежедневно в 2:00)
0 2 * * * /usr/local/bin/pg-backup.sh

VACUUM и настройка автовакуума

Когда мы заглянули в таблицу событий (events), обнаружили более 50 миллионов dead tuples. Это классика: интенсивная запись плюс дефолтный автовакуум, который просто не успевает за темпом. Bloat копился месяцами и стал одной из главных причин, почему всё так тормозило.

Оптимизация автовакуума

# postgresql.conf — параметры автовакуума
autovacuum = on
autovacuum_max_workers = 4
autovacuum_naptime = 30s

# Пороги для запуска VACUUM
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.05  # 5% изменённых строк

# Пороги для запуска ANALYZE
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.02  # 2%

# Ограничение нагрузки автовакуума
autovacuum_vacuum_cost_delay = 2ms
autovacuum_vacuum_cost_limit = 1000

Для таблицы events дефолтные параметры автовакуума не работают — слишком высокая интенсивность записи. Настроили индивидуальные пороги прямо для этой таблицы:

-- Более агрессивный автовакуум для таблицы с высокой нагрузкой
ALTER TABLE app.events SET (
    autovacuum_vacuum_scale_factor = 0.01,
    autovacuum_analyze_scale_factor = 0.005,
    autovacuum_vacuum_cost_delay = 0
);

Мониторинг dead tuples и bloat

-- Таблицы с наибольшим количеством dead tuples
SELECT
    schemaname || '.' || relname AS table_name,
    n_live_tup,
    n_dead_tup,
    ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS dead_pct,
    last_vacuum,
    last_autovacuum,
    last_analyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

-- Оценка bloat таблиц
SELECT
    schemaname || '.' || tablename AS table_name,
    pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
    pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size,
    pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) AS index_size
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 20;

Внедрение PgBouncer для connection pooling

В пиковые часы приложение открывало до 300 подключений к PostgreSQL. Казалось бы, немного — но каждое соединение съедает около 10 МБ RAM, и в сумме это ощутимо. Плюс накладные расходы на установку и разрыв соединений. Решение — PgBouncer в режиме transaction pooling. Поставили между приложением и базой, и проблема тайм-аутов ушла.

Применённая конфигурация PgBouncer

# Установка
sudo apt install -y pgbouncer

# Конфигурация /etc/pgbouncer/pgbouncer.ini
[databases]
app_production = host=127.0.0.1 port=5432 dbname=app_production
* = host=127.0.0.1 port=5432

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

# Режим пула: session, transaction или statement
pool_mode = transaction

# Размер пула
default_pool_size = 25
max_client_conn = 1000
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3

# Таймауты
server_idle_timeout = 600
client_idle_timeout = 0
query_timeout = 120
client_login_timeout = 15

# Логирование
logfile = /var/log/pgbouncer/pgbouncer.log
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60

# Админ-консоль
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats
# Создаём файл аутентификации
echo '"app_user" "SCRAM-SHA-256$4096:salt$stored_key:server_key"' | sudo tee /etc/pgbouncer/userlist.txt

# Проще — генерируем хеш
sudo -u postgres psql -Atc "SELECT '\"' || usename || '\" \"' || passwd || '\"' FROM pg_shadow WHERE usename = 'app_user';" >> /etc/pgbouncer/userlist.txt

# Запускаем
sudo systemctl enable --now pgbouncer

# Приложения подключаются к порту 6432 вместо 5432
psql -h 127.0.0.1 -p 6432 -U app_user app_production

Мониторинг PgBouncer

# Подключаемся к админ-консоли PgBouncer
psql -h 127.0.0.1 -p 6432 -U pgbouncer_admin pgbouncer

-- Статистика пулов
SHOW POOLS;

-- Активные клиенты
SHOW CLIENTS;

-- Серверные соединения
SHOW SERVERS;

-- Общая статистика
SHOW STATS;

-- Статистика по базам данных
SHOW STATS_TOTALS;

Мониторинг производительности через Prometheus и Grafana

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

Встроенные представления для диагностики

-- Топ-20 самых тяжёлых запросов (требуется pg_stat_statements)
SELECT
    queryid,
    LEFT(query, 100) AS query_preview,
    calls,
    ROUND(total_exec_time::numeric / 1000, 2) AS total_time_sec,
    ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
    rows,
    ROUND(shared_blks_hit::numeric / NULLIF(shared_blks_hit + shared_blks_read, 0) * 100, 2) AS cache_hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- Текущие активные запросы
SELECT
    pid,
    now() - pg_stat_activity.query_start AS duration,
    usename,
    datname,
    state,
    LEFT(query, 200) AS query
FROM pg_stat_activity
WHERE state != 'idle'
    AND pid != pg_backend_pid()
ORDER BY duration DESC;

-- Блокировки
SELECT
    blocked.pid AS blocked_pid,
    blocked.usename AS blocked_user,
    blocking.pid AS blocking_pid,
    blocking.usename AS blocking_user,
    LEFT(blocked.query, 100) AS blocked_query,
    LEFT(blocking.query, 100) AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_locks AS bl ON bl.pid = blocked.pid
JOIN pg_locks AS 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.pid != bl.pid
    AND NOT kl.granted
JOIN pg_stat_activity AS blocking ON blocking.pid = kl.pid
WHERE NOT bl.granted;

-- Размеры баз данных
SELECT
    datname,
    pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
ORDER BY pg_database_size(datname) DESC;

Интеграция с Prometheus и Grafana

# Устанавливаем postgres_exporter
wget https://github.com/prometheus-community/postgres_exporter/releases/latest/download/postgres_exporter-linux-amd64.tar.gz
tar xzf postgres_exporter-linux-amd64.tar.gz
sudo mv postgres_exporter /usr/local/bin/

# Создаём пользователя мониторинга
sudo -u postgres psql -c "
CREATE USER monitoring WITH PASSWORD 'MonitorP@ss!';
GRANT pg_monitor TO monitoring;"

# Создаём systemd unit
sudo cat > /etc/systemd/system/postgres-exporter.service <

Подключили postgres_exporter, настроили Prometheus и импортировали в Grafana готовый дашборд ID 9628 (PostgreSQL Database). На нём сразу видно всё важное: количество подключений, cache hit ratio, TPS, размер WAL, задержка репликации, bloat таблиц. Никакой магии — просто нормальная видимость.

Результаты внедрения

Вся оптимизация заняла 5 рабочих дней. Продакшн ни разу не останавливали. Результаты замерили до и после — цифры говорят сами за себя:

  • Время отклика API снизилось с 800 мс до 45 мс — почти в 18 раз. Это результат правильно выставленных shared_buffers, work_mem и добавленных индексов
  • Cache hit ratio вырос с 85% до 99.7% — здесь сработала связка shared_buffers и effective_cache_size. 85% — это когда каждый седьмой запрос идёт на диск
  • Поддержка 1000+ одновременных соединений — PgBouncer в режиме transaction pooling полностью снял проблему тайм-аутов при пиковой нагрузке
  • Аналитические отчёты за 8 секунд вместо 5 минут — подключили параллельные запросы и вплотную занялись планировщиком
  • Ежедневные автоматические бэкапы с ротацией 14 дней и шифрованием — настроили один раз, работает само
  • Горячая реплика — при аварии переключаемся на standby меньше чем за 30 секунд
  • Полный мониторинг — дашборды в Grafana с алертами на всё, что реально важно
  • Dead tuples сократились на 99% — после того как правильно настроили автовакуум, таблицы перестали «пухнуть»

Через месяц клиент написал: жалоб от пользователей на тормоза больше нет. Ноль. И платформа теперь спокойно вырастет до 50 000 пользователей — железо менять не придётся.

Часто задаваемые вопросы

Мы держимся правила 25% от общего объёма RAM — и за 15 лет оно нас ни разу не подводило. Для сервера с 16 ГБ RAM ставим shared_buffers = 4GB, для 32 ГБ — 8GB, для 64 ГБ — 16GB. Почему не больше? PostgreSQL активно использует кеш операционной системы, и если жадничать со shared_buffers, эффект будет обратным. После изменения сразу смотрим cache_hit_ratio — если ниже 99%, копаем дальше.

Клиенты часто спрашивают: чем отличаются pg_dump и pg_basebackup и что выбрать? Отвечаю. pg_dump — это логический бэкап в виде SQL-команд для конкретной базы. Удобен для миграции, клонирования и восстановления отдельных таблиц. pg_basebackup снимает физическую копию всего кластера на уровне файлов — незаменим для создания реплик и PITR. На production мы всегда используем оба инструмента: pg_basebackup для быстрого восстановления после катастрофы, pg_dump — когда нужно поднять конкретную таблицу или базу.

PgBouncer — не роскошь, а необходимость, как только одновременных подключений становится больше 100–200 или приложение плодит короткоживущие соединения. Из трёх режимов на практике чаще всего выбираем transaction: соединение возвращается в пул после каждой транзакции — отлично работает для большинства веб-приложений. Режим session берём для legacy-приложений, которые используют PREPARE, LISTEN/NOTIFY или временные таблицы. Режим statement самый экономный, но с транзакциями он не дружит — только простые запросы.

Как понять, что база начинает задыхаться? Смотрим на несколько вещей сразу. cache_hit_ratio упал ниже 99% — уже тревожный звонок. Таблицы пухнут от dead tuples. В pg_stat_activity запросы зависают в состоянии «waiting» — блокировки. На больших таблицах лезет seq_scan там, где его быть не должно. Типичные запросы выполняются дольше, чем неделю назад. Начинаем разбор с pg_stat_statements — он сразу показывает самые тяжёлые запросы. Дальше — память и индексы.

Для обновления мажорной версии используем pg_upgrade с флагом --link — данные не копируются, всё проходит быстро. Но бэкап перед этим обязателен, без вариантов. Порядок действий такой: 1) снимаем полный бэкап, 2) устанавливаем новую версию PostgreSQL, 3) останавливаем старый кластер, 4) запускаем pg_upgrade -b /usr/lib/postgresql/15/bin -B /usr/lib/postgresql/16/bin -d /old/data -D /new/data --link, 5) стартуем новый кластер, 6) запускаем vacuumdb --all --analyze-in-stages. И ещё одно железное правило — сначала всё это прогоняем на staging. Всегда.

Нужна помощь с настройкой?

Если не хочется разбираться со всем этим самостоятельно — команда АйТи Фреш возьмёт на себя внедрение и настройку. Больше 15 лет работаем с PostgreSQL в боевых условиях, обслуживание от 15 000 ₽/мес.

📞 Связаться с нами
#PostgreSQL установка#настройка PostgreSQL#оптимизация PostgreSQL#postgresql.conf настройка#PostgreSQL репликация#pg_dump резервное копирование#pgbouncer connection pooling#PostgreSQL VACUUM