awesome-everything EN
↑ Обратно к восхождению

Базы данных

Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночи

Суть Исчерпание пула почти всегда — проблема времени удержания, а не размера пула; idle-in-transaction — тихий убийца, который опустошает пул за секунды и одновременно блокирует VACUUM.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

В 02:14 срабатывает алерт PgBouncer: pool = 24, cl_waiting = 180. Приложение возвращает 503. pg_stat_activity показывает 19 backend’ов в состоянии “idle in transaction” с максимальным возрастом 8 минут. Пул был правильно настроен этим утром.

Что на самом деле означает исчерпание пула

Исчерпание пула происходит, когда каждый backend в пуле занят и новое соединение не может быть получено. На уровне приложения: pool.acquire() таймаутит или отклоняется. На уровне Postgres: backend’ы за этими занятыми соединениями находятся в одном из состояний:

  • active — выполняет медленный запрос или ждёт блокировки
  • idle in transaction — между операторами внутри BEGIN, который не был COMMIT или ROLLBACK
  • idle — действительно простаивает (тогда почему пул полон? ответ: неверный pool_size или утечка соединений)

Пул исчерпан в смысле отсутствия свободных слотов. База данных не исчерпана — она ждёт приложение.

Четыре корневые причины

  1. Долгие запросы — отсутствующий индекс делает запрос за 5 с; при 1 000 QPS это 5 000 соединений в полёте. Исправление: EXPLAIN ANALYZE, добавить индекс, пагинировать большие сканы.

  2. Idle-in-transaction — самая частая. Приложение открыло транзакцию (BEGIN) и не выполнило COMMIT или ROLLBACK: необработанное исключение, внешний API-вызов внутри транзакции, отсутствие try/finally. Backend сидит “idle in transaction”, удерживая блокировки, MVCC-снапшот и слот пула.

  3. Всплеск трафика — спрос превышает размер пула при стабильной нагрузке. Исправление: reserve_pool_size в PgBouncer, автомасштабирование на уровне приложения, или очередь с деградацией (возвращать 503 с Retry-After до исчерпания пула).

  4. Внешняя зависимость внутри транзакции — вызов Stripe, загрузка в S3, доставка вебхука — всё внутри BEGIN…COMMIT. Транзакция остаётся открытой на время внешнего вызова. Если тот сервис замедляется, каждая транзакция в полёте удерживает слот пула секунды или минуты.

Корневая причинаСигнал в pg_stat_activityИсправление
Отсутствующий индекс / медленный запросМного строк state=active со старым query_startEXPLAIN ANALYZE + добавить индекс
Баг idle-in-transactionМного строк state=idle in transaction со старым xact_startИсправить код + idle_in_transaction_session_timeout
Внешний API внутри транзакцииstate=idle in transaction, последний запрос = BEGINВынести вызов за транзакцию; использовать outbox-паттерн
Конкуренция за блокировкиSELECT * FROM pg_locks WHERE NOT grantedНайти и завершить блокирующий backend; пересмотреть DDL

Idle-in-transaction — двойной сбой

Backend в состоянии idle in transaction удерживает:

  1. Слот пула — виден немедленно; вызывает исчерпание пула
  2. Блокировки строк — заблокированные строки не могут быть изменены другими транзакциями
  3. MVCC-снапшот — VACUUM не может очистить мёртвые кортежи старше xmin этого backend’а; таблицы раздуваются

Один баг, открывающий транзакцию и забывающий COMMIT на пути ошибки, может опустошить 20-соединённый пул за секунды и вызвать раздутие таблиц, нарастающее часами.

Диагностика исчерпания пула за пять шагов

Проследи
1/5

Алерт об исчерпании пула сработал в 02:14. Пройдите диагностику.

1
Step 1 of 5
Шаг 1: что показывает PgBouncer SHOW POOLS?
2
Locked
Шаг 2: что показывает pg_stat_activity?
3
Locked
Шаг 3: найдите проблемные запросы.
4
Locked
Шаг 4: проверьте конкуренцию за блокировки.
5
Locked
Шаг 5: долгосрочные меры.

Страховочная сеть: idle_in_transaction_session_timeout

Этот Postgres GUC убивает любой backend, который остаётся в idle in transaction дольше порогового значения:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();

При таком значении сценарий со Stripe из хука превращается в: таймауты Stripe вызывают прерывание отдельных транзакций (залогированных, алертируемых), а не исчерпание пула. Пул выживает; приложение логирует 504 и повторяет попытку.

Это бесплатная страховочная сеть. Каждое production-развёртывание Postgres должно её иметь. Устанавливайте значение выше вашей самой долгой легитимной транзакции (обычно 30–120 с для OLTP).

Почему это работает

Почему idle-in-transaction также блокирует VACUUM? MVCC (04-mvcc-isolation) показал, что каждая транзакция удерживает xmin-снапшот — горизонт, ранее которого VACUUM не может очищать мёртвые кортежи. Backend в состоянии idle-in-transaction удерживает этот снапшот бесконечно. Один забытый COMMIT в редко выполняемом коде может не давать VACUUM чистить горячую таблицу часами, вызывая безграничное раздутие. Исчерпание пула — видимый симптом; раздутие таблицы — тихий, нарастающий со временем.

Числа безопасности для idle-in-transaction
Рекомендованный idle_in_transaction_session_timeout
30–60 с
Рекомендованный statement_timeout
10–30 с
Рекомендованный lock_timeout
5 с
PgBouncer query_wait_timeout
10–30 с
Алерт: порог возраста idle-in-tx
> 30 с
Алерт: устойчивый cl_waiting
> 0 на протяжении 60 с
Викторина

Какова наиболее частая корневая причина исчерпания пула PgBouncer в production?

Викторина

Приложение вызывает сторонний платёжный API внутри блока BEGIN...COMMIT. Платёжный API начинает таймаутить через 30 с. Что произойдёт с пулом соединений?

Вспомните перед уходом
  1. 01
    Опишите, что означает исчерпание пула на каждом уровне — приложение, пулер, Postgres — и почему увеличение pool_size обычно не помогает.
  2. 02
    Почему backend в idle-in-transaction вредит больше, чем просто занимает пул?
  3. 03
    Какие настройки Postgres и PgBouncer защищают от исчерпания из-за idle-in-transaction?
Итог

Исчерпание пула срабатывает, когда все backend’ы заняты и нет свободных слотов. Доминирующая корневая причина — время удержания: транзакция, остающаяся открытой во время медленного запроса, ожидания блокировки или внешнего API-вызова, держит backend занятым. Увеличение pool_size не исправляет время удержания — оно даёт больше транзакций в полёте при том же удержании, добавляя конкуренцию в Postgres. Правильная последовательность: выявить удержание через pg_stat_activity и SHOW POOLS; исправить причину (индекс, вынести API-вызов за транзакцию, добавить COMMIT/ROLLBACK на путях ошибок); добавить idle_in_transaction_session_timeout = 60s как постоянную страховку; алертировать при cl_waiting > 0 и возрасте idle-in-transaction > 30 с. Backend в idle-in-transaction также закрепляет MVCC-снапшот, незаметно блокируя VACUUM и увеличивая раздутие таблиц — исчерпание пула и раздутие MVCC имеют одну корневую причину.

Связанные уроки
встречается в258
Продолжить восхождение ↑Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.