Суть Читай реальный конфиг pool, код хендлера и строку лога Postgres, предсказывай отказ и выбирай фикс с наибольшим рычагом — формула размера, leak, idle-in-transaction и конфиг таймаутов.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Инциденты с pool диагностируются в конфиг-файлах, коде хендлеров и логе базы — не в абстракции. Прочитай каждый сниппет, предскажи отказ, который он даёт под нагрузкой, и выбери фикс, который senior-инженер сделает первым.
Цель
Отработай цикл каждого инцидента с пулингом: прочитай конфиг и горячий путь, предскажи, где соединения голодают, текут или протухают, и тянись за структурным фиксом раньше, чем за большим числом.
Сниппет 1 — размер pool
# Postgres на 4 vCPU, один SSD. Сервис держит ~2000 req/s.# Запаниковавший инженер выставил это после алерта "DB is the bottleneck":spring.datasource.hikari.maximum-pool-size: 200spring.datasource.hikari.minimum-idle: 200
Викторина
Completed
На этой 4-vCPU, одно-SSD базе, каким на самом деле должен быть maximum-pool-size и почему 200 вредны?
Heads-up Соединение — не единица параллелизма; ядро — да. За пределами примерно (cores x 2) + spindles лишние соединения лишь конкурируют, а не выполняются.
Heads-up max_connections — серверный потолок, общий для всех клиентов, а не цель на приложение. Максимизирующее пропускную способность число здесь однозначное.
Heads-up Высокий rate запросов обслуживается дешёвым ожиданием избытка в очереди pool, а не большим числом backend'ов. Больший pool молотит базу — rate запросов не меняет число ядер.
Сниппет 2 — хендлер
async function getUser(id) { const conn = await pool.acquire(); const rows = await conn.query("SELECT * FROM users WHERE id = $1", [id]); conn.release(); // возвращает соединение в pool return rows[0];}
Викторина
Completed
Этот хендлер работает в тестах, но pool осушается до пустого за часы в проде, чинится только рестартом. В чём дефект и фикс?
Heads-up Acquire рассчитан на конкурентных вызывающих; баг в том, что release пропускается на error-пути, утекая одно соединение каждый раз, когда запрос кидает.
Heads-up Число колонок не осушает pool. Leak — это негарантированный release на throw-пути; именно он ратчетит pool до пустого.
Heads-up Больший pool лишь оттянет ту же exhaustion, потому что leak осушает со своей скоростью. Структурный фикс — try/finally, а не больше соединений.
Сниппет 3 — строка лога Postgres
LOG: duration: 612000.244 ms state: idle in transactionDETAIL: process 48213 has been idle in transaction for 00:10:12HINT: Sessions idle in transaction hold their connection and any locks.
Викторина
Completed
Несколько backend'ов показывают 'idle in transaction' минутами, и pool продолжает истощаться. Что происходит и какая защита верна?
Heads-up Сессия простаивает, а не выполняет запрос — соединение сидит внутри открытой транзакции, ничего не делая на базе. Индекс не помогает соединению, которое придерживается через несвязанную работу.
Heads-up Здесь ничто не указывает на давление по памяти; 'idle in transaction' значит, что клиент открыл транзакцию и перестал слать стейтменты, пиннящ backend и его локи.
Heads-up Больше соединений лишь дают больше сессий, чтобы застрять idle-in-transaction, держа больше локов. Ограничь длительность открытой транзакции вместо этого.
Сниппет 4 — конфиг таймаутов
spring.datasource.hikari.connection-timeout: 30000 # 30 с, дефолтspring.datasource.hikari.max-lifetime: 28800000 # 8 ч# Бэкенд MySQL имеет wait_timeout = 28800 (8 ч, дефолт)
Викторина
Completed
В этом конфиге прячутся две проблемы, которые укусят при медленной зависимости и после простоев. Какой фикс корректно адресует обе?
Heads-up Acquisition timeout в 30 с пиннит потоки веб-сервера сквозь замедление, пока слой не заморится. А max-lifetime, равный wait_timeout, гоняется с сервером — он должен быть строго короче, чтобы pool всегда закрывал первым.
Heads-up 50 мс валит безобидные микровсплески, которые рассосались бы за десятки мс. А max-lifetime в 8 ч, равный wait_timeout, всё ещё даёт серверу убить соединения первым, раздавая stale-сокеты.
Heads-up Acquisition timeout — единственное число, решающее fail-fast vs thread-starvation при медленной зависимости — это самая критичная для отказа ручка здесь.
Итог
Каждый инцидент с пулингом читается в конфиге, коде и логах: pool, заданный в 200 на 4 ядрах, — это contention, а не конкурентность (эвристика (cores x 2) + spindles ставит его около 9); release() вне try/finally течёт на error-пути и осушает pool за часы; ‘idle in transaction’ в логе значит соединение, придержанное внутри открытой транзакции через несвязанную работу, держащее локи; а 30-секундный acquisition timeout плюс max-lifetime, не подрезающий DB wait_timeout, — ловушка starvation-и-stale-сокетов. Диагностируй по уликам, чини структурно (правильный размер, гарантированный return, узкие транзакции, ограниченные ожидания), затем перемеряй.