Прямой ответ
pgbench — штатный бенчмаркер Postgres (ставится с сервером — «свой нагрузочный инструмент уже есть» — локальный Postgres); две роли: (1) сравнительные замеры («shared_buffers 4 ГБ против 8», «NVMe против SATA», «15-я версия против 14-й» — A/B конфигураций на одинаковой нагрузке — цифры вместо холиваров) и (2) профиль своей нагрузки (свой скрипт .sql с вашими запросами — «мой типовой транзакционный цикл» — честнее встроенного TPC-B для вашего приложения); базовый прогон: pgbench -i -s 100 db (инициализация — scale 100 ≈ 10M строк в pgbench_accounts ≈ 1.6 ГБ — scale подбираем так, чтобы данные не влезали в кэш — иначе меряем память, не диск — емкость-контекст) → pgbench -c 10 -j 4 -T 120 -P 10 db (10 клиентов, 4 потока, 120 секунд, прогресс каждые 10 — интерпретация: tps (без establish) — пропускная способность, latency average — отклик, stddev/процентилями через -r/--latency-limit); правила честного замера (иначе цифры врут): прогрев (-T 300 или discard первых минут — кэши/autovacuum-фон устаканиваются — «прогнал 30 секунд — цифра» — шум); повторность (3+ прогона — медиана — разброс >5% — среда шумит — фоновые процессы/соседи по VM); фиксация всего (версия PG, конфиг-диффы, данные железа, состояние autovacuum — «сравнили конфиги» — а между прогонами checkpoint прошёл — WAL-механика — результат — артефакт); одинаковость окружения (та же БД/диск — «сравнили SSD-ноут и облако» — сравнили облака, не Postgres); TPC-B-специфика (встроенный профиль — 3 UPDATE + 1 INSERT в транзакции — write-heavy — для read-heavy приложения — свой скрипт — иначе оптимизируете не то); свой скрипт (главная техника): файл workload.sql — \set aid random(1, 10000000) + ваши SELECT/UPDATE (паттерн вашего приложения — запросы из pg_stat_statements — топ по времени — в скрипт с весами) → pgbench -f workload.sql -c 20 -T 300 db; параметризация конфигов (-D var=значение — размеры/параметры в скрипте); частые ошибки: -c 200 «чтоб наверняка» (клиенты > возможностей — очередь на соединениях — меряете не БД, а лок — блокировки/пулинг — кривая tps-от-clients покажет пик и деградацию — снимаем серию 1/4/16/64/128, не одну точку); сравнение «до/после» с разной базой (после прогона БД раздута/bloat — «после» медленнее из-за состояния, не конфига — VACUUM FULL/пересоздание между сериями); игнор латентности ради tps (tps 5000 при p99 2 секунды — «производительность» для batch, катастрофа для веба — процентилями — снимаем оба); одна точка вместо кривой (поведение при насыщении — главная информация — где излом — capacity-методика).
Практика: сравнение конфигураций и чтение результатов
Серия A/B (пример с цифрами — ноутбук, Postgres 15, scale 100): базовая серия (shared_buffers=128MB, work_mem=4MB, дефолты) — -c 16 -T 300 — tps 620 (stddev 35), lat avg 25 мс; шаг 1: shared_buffers 4 ГБ (25% RAM — конфигурация) — tps 940 (+52%), lat 16 мс — кэш-эффект (scale 100 частично в памяти — поэтому для «дискового» профиля — scale 300+); шаг 2: + effective_cache_size 12 ГБ + random_page_cost 1.1 (NVMe — стоимость случайного чтения ниже — планы меняют форму) — tps 985 (+5% — plans перестали бояться индексов); шаг 3: max_wal_size 8 ГБ + checkpoint_completion_target 0.9 (реже checkpoints — пики сглажены) — tps 990, но stddev 35→11 (латентность ровнее — выигрыш не в среднем, в хвостах — p99 240→90 мс — процентили — «среднее не изменилось» ≠ «не улучшили»); шаг 4: -c 64 на лучшем конфиге — tps 1010, lat avg 62 мс (насыщение — закон Литтла — 64 клиентов × 1/0.062 ≈ 1030 — сходится — «ёмкость ~1000 tps на этом железе» — цифра для capacity-плана); интерпретационные нюансы: tps в отчёте — транзакции скрипта (TPC-B = 3 UPDATE+INSERT — «tps 1000» ≠ «1000 ваших RPS» — пересчёт по составу транзакции); -r (per-statement latencies) — какое заявление в скрипте тормозит («UPDATE pgbench_accounts — 18 мс из 25» — hotspot — индексы/планы); --rate (целевая нагрузка — «1000 tps — какая латентность» — ближе к реальной жизни, чем «все клиенты молотят» — очередь искажает); --log (per-транзакционный лог — свои p95/p99 из CSV — гистограмма — мониторинг-культура); фоновый шум ноутбука (троттлинг/соседи — диагностика железа — «цифры плывут между прогонами» — среда, не Postgres — фиксировать частоты/питание).
Практический сценарий: выбор между «больше RAM» и «быстрее диск» за 40 тысяч
Контекст: прод-Postgres (аренда: 8 vCPU/32 ГБ/SATA SSD — 12k руб/мес), жалобы: p99 запросов 800 мс в вечерний пик; апгрейд-вилка: +32 ГБ RAM (+4k) против NVMe-диск (+7k) — спор в команде («памяти всегда мало» против «диск решает»); эксперимент (воспроизводимый, один вечер): (1) копия прода на стенд (дамп 40 ГБ — бэкап-механика — pg_dump/restore — «тестировать на проде» — нет); (2) профиль нагрузки из pg_stat_statements (топ-10 запросов с весами — скрипт pgbench -f — read-heavy 70/30 — TPC-B не годился — write-профиль соврал бы); (3) базовый прогон (-c 32 -T 600 --rate 400 --log) — tps держит, p99 780 мс (воспроизвели прод-боль на стенде — обязательное условие — иначе «улучшения» не увидеть); (4) A: +32 ГБ RAM (shared_buffers 8→16 ГБ, effective_cache_size 48) — p99 780→310 мс (рабочий набор 25 ГБ влез в page cache — память-диагностика — hit ratio буферов 92→99.2% — pg_stat_database — цифра-подтверждение); (5) B: NVMe (RAM прежний) — p99 780→420 мс (промахи кэша стали дешевле — random_page_cost 4→1.1 — планы похорошели — чтение планов — но промахов всё ещё 8%); (6) A+B: p99 190 мс — синергия (память держит набор, NVMe страхует промахи); (7) решение — A (+RAM) — 60% эффекта за 57% цены (B — фаза 2 через квартал по тренду роста данных — планирование — таблица «при +20% данных RAM-вариант деградирует раньше — дата пересмотра»); (8) прод-раскатка с проверкой (метрики — p99 до/после в Grafana — стенд-прогноз 310 мс — прод факт 340 мс — модель адекватна — доверие к методике); итог: спор «RAM против диска» решился за вечер измерением, не мнением; артефакт — скрипты+отчёты в репо (структура проекта — bench/ — следующий апгрейд — запуск make bench — make-практика — «институциональная память замеров» дороже единичной цифры).
FAQ
TPC-B из коробки или свой скрипт — что мерить? TPC-B (встроенный) — стандартизованный write-heavy профиль (3 UPDATE+1 INSERT — «синтетический OLTP» — годится для: сравнения железа/версий PG/конфигов WAL — «что быстрее само по себе»); свой скрипт — для всего, что про ваше приложение (ваше соотношение read/write, ваши запросы/индексы — read-heavy API на TPC-B — выводы неверные — «оптимизировали запись, приложение читает» — частая ошибка); золотая середина: обе серии (TPC-B — база/регрессии при апгрейдах — воспроизводимо между машинами; свой профиль — продуктовые решения — pg_stat_statements как источник запросов — веса по реальному времени); «pgbench не похож на нашу нагрузку» — не повод не пользоваться — повод написать свой .sql (30 минут — ценность на годы — сценарий выше).
Почему tps высокий, а приложение тормозит — что не так с замером?
Классические разрывы: профиль не тот (TPC-B write-heavy против вашего read-heavy — разная чувствительность к кэшу/диску — свой скрипт); латентность против пропускной (tps 5000 при p99 2 с — очередь — приложению нужен p99 — --rate+перцентили — процентили — «средняя температура по госпиталю»); соединения/пулинг (pgbench -c 100 молотит напрямую — приложение через pgbouncer с пулом 20 — другая точка кривой — бенчмаркить через пулер, если он в проде); состояние данных (чистая pgbench-таблица против вашего bloat+vacuum-состояния — планы разные — EXPLAIN-сравнение); размер выборки (scale 1 — всё в кэше — «железо быстрое» — прод-данные 100 ГБ — мимо кэша — другой режим); вывод: tps pgbench — относительная метрика конфигураций, не предсказание прод-латентности — прод-правда — APM/метрики — бенчмарк отвечает на «что быстрее», не на «как быстро у нас».
Как сравнить версии PostgreSQL (14 против 15) корректно?
Методика: одинаковое железо/ОС/конфиг (идеал — один стенд, последовательно — контейнер с нужной версией — docker-подход — postgres:14/postgres:15 — тома одинаковые); данные — restore одного дампа в обе (не -i генерация отдельно — «случайные данные чуть разные» — шум на малых эффектах); прогон — свой профиль + TPC-B (3+ повтора, медиана, прогретые — -T 300); метрики — tps + p95/p99 + размер WAL за прогон (15-я эффективнее — WAL-объём — репликация-контекст — косвенный, но важный эффект) + время vacuum (vacuum — фоновая работа — часть стоимости владения); подводные камни: дефолты конфигов меняются между версиями («честное сравнение дефолтов» против «честное сравнение тюнинга» — оба вопроса — разные серии — postgresql.conf версий — дифф обязателен); параллельные планы/новые фичи включаются на ваших данных иначе (parallel workers — ресурсы — «15-я быстрее на синтетике, на проде та же» — ваши запросы не параллелятся — планы); честный вывод обычно: 5–15% на типовых нагрузках — апгрейд ради версий — слабый аргумент, ради исправленных багов/фич — сильный — миграция-практика.
Что даёт --rate и зачем снимать кривую tps-от-clients? Режим без --rate («все клиенты молотят») мерит максимум (closed-loop — латентность растёт с клиентами — очередь — закон Литтла — «емкость»); --rate N (open-loop — запросы приходят с заданным интервалом независимо от обработки) мерит латентность при целевой нагрузке («при 400 tps p99 = X» — как в жизни — трафик не ждёт ваших ответов — capacity-планирование); при перегрузке open-loop показывает честную деградацию (очередь растёт — латентность в потолок — closed-loop «притормозит клиентов» и скроет); кривая tps-от-clients (серия 1/4/16/64/128) — паспорт системы: линейный рост (недогруз) → плато (ёмкость) → спад (перенасыщение — контекстные переключения/локи — «добавили клиентов — стало хуже» — точка излома — рабочая нагрузка = 60–70% от излома — N+1-запас); одна точка — фотография, кривая — диагноз — «сколько клиентов при 100 tps» — из closed-loop, «какая p99 при 100 tps» — из open-loop — разные вопросы, разные режимы pgbench.
Источники: документация PostgreSQL (pgbench, WAL/resource конфигурация) — ссылки в шапке.