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

Архитектура бэкенда

Жизненный цикл соединения: протухшие соединения и как их старить

Суть Пулевое соединение переиспользуется часами, в чём весь смысл — но база, фаерволы и балансировщики все оставляют за собой право убить его под тобой. Без max-lifetime, вытеснения по простою и валидации пул радостно выдаёт мёртвые соединения.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 15 min

Сервис работает нормально весь день, затем в 3 ночи — во время затишья трафика — первые утренние запросы падают с «connection reset by peer». Ничего не деплоили, база здорова. Случилось то, что за тихую ночь wait_timeout базы, правило простоя фаервола и балансировщик тихо сбросили долго-простаивавшие пулевые соединения, но пул не заметил. Он держал эти мёртвые сокеты и уверенно выдал один первому утреннему запросу, который записал запрос в закрытую трубу. Величайшая сила пула — держать соединения открытыми для переиспользования — это и обязательство: соединение, которое он держит, могло умереть, не сказав ему.

Удерживаемое соединение может умереть без ведома пула

Пул держит соединения открытыми именно затем, чтобы запросы пропускали рукопожатие — но «открыто на нашей стороне» не значит «живо из конца в конец». У TCP-соединения два конца и несколько middlebox-ов, любой из которых может снести его, пока сторона пула всё ещё выглядит ESTABLISHED:

  • Сама база. Postgres и MySQL применяют лимиты простоя и времени жизни на стороне сервера (idle_in_transaction_session_timeout, MySQL wait_timeout по умолчанию 8 часов). Когда сервер закрывает backend, клиентский сокет не уведомляется, пока в него снова не запишут.
  • Фаерволы и NAT-шлюзы. Stateful-фаерволы сбрасывают простаивающие потоки через несколько минут, чтобы вернуть место в таблице; типичный облачный NAT-таймаут простоя около 350 секунд. После него соединение — чёрная дыра, пакеты пропадают.
  • Балансировщики и прокси. LB перед базой (или PgBouncer) имеет свои лимиты простоя и времени жизни и перерабатывает backend-ы на деплоях.

Итог: соединение, простаивающее в пуле, может быть тихо мёртвым, и пул узнаёт это лишь когда запрос пытается его использовать и получает reset или зависание.

Три контроля держат пул свежим

Хороший пул защищается от протухания тремя кооперирующими настройками:

  • Max lifetime. Уволь и замени каждое соединение после фиксированного возраста (HikariCP maxLifetime, дефолт 30 минут) независимо от того, выглядит ли оно здоровым. Это проактивно ротирует соединения до того, как middlebox-ы или сервер их убьют. Важное правило: maxLifetime должен быть короче собственного таймаута соединения базы (напр. на несколько секунд под MySQL wait_timeout), чтобы пул всегда закрывал соединение до сервера.
  • Idle timeout. Сжимай пул назад к минимуму в тихие периоды, вытесняя соединения, простаивающие дольше idleTimeout (дефолт 10 минут). Это делает что-то лишь если minimumIdle задан ниже максимума — иначе пул держит все соединения вечно.
  • Keepalive / валидация. Периодически прощупывай простаивающие соединения (keepaliveTime) и/или тестируй соединение при взятии до выдачи. Современный пул валидирует лёгким пингом на уровне протокола; если он провалится, соединение тихо выбрасывается и создаётся свежее — поэтому запрос никогда не видит мёртвый сокет.

Валидируй в правильный момент, дёшево

У валидации есть цена: проверка на каждом взятии добавляет круговой обмен к каждому запросу, что может стереть выигрыш задержки, ради которого пул существует. Современный компромисс — валидировать при взятии, но пропускать проверку, если соединение использовалось совсем недавноaliveBypassWindow HikariCP в 500 мс значит, что соединение, возвращённое и снова взятое в пределах полусекунды, доверяется без прощупывания, по рассуждению, что оно не могло протухнуть так быстро. Это держит горячие соединения быстрыми, всё ещё ловя те, что простояли достаточно долго, чтобы быть под риском.

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

Зачем уволять соединения по maxLifetime проактивно, а не просто валидировать при взятии? Потому что валидация ловит лишь соединение, уже мёртвое в момент взятия — она ничего не делает для соединения, умирающего после выдачи, посреди запроса, когда фаервол наконец сбрасывает долгоживущий поток или база перерабатывает backend. Проактивная ротация по времени жизни атакует корень: она гарантирует, что ни одно соединение не живёт достаточно долго, чтобы вообще достичь этих внешних лимитов, поэтому опасная смерть в использовании становится исчезающе редкой. Два контроля дополняют, не дублируют — валидация обрабатывает соединение, протухшее простаивая в пуле, а maxLifetime обрабатывает соединение, протухшее бы в удержании запросом. Есть и польза стабильности: ротация соединений ровно размазывает цену переподключения по времени, а не даёт всему пулу состариться вместе и затем переподключиться стадом, когда база наконец закроет их все разом. Ограничение возраста соединения — та же дисциплина, что ограничение очереди ожидания — ограничиваешь ресурс намеренно, а не даёшь внешней системе ограничить его за тебя в худший момент.

КонтрольЧто предотвращаетКлючевое ограничение
maxLifetime (~30 мин)Соединение убито БД/фаерволом в удержанииДолжен быть < wait_timeout БД
idleTimeout (~10 мин)Удержание лишних простаивающих соединений вне пикаДействует лишь если minimumIdle < max
keepalive-прощупПростаивающий поток сброшен NAT/фаерволомИнтервал < лимита простоя middlebox
валидация при взятииВыдача мёртвого сокета запросуПропуск в пределах aliveBypassWindow (500 мс)
Викторина

После ночного затишья первые утренние запросы падают с 'connection reset', хотя ничего не деплоили и база здорова. Что случилось?

Викторина

Почему maxLifetime должен быть короче собственного таймаута соединения базы (напр. MySQL wait_timeout)?

Викторина

Почему современные пулы валидируют при взятии, но пропускают проверку в коротком окне вроде aliveBypassWindow (500 мс)?

Вспомните перед уходом
  1. 01
    Почему пулевое соединение может быть мёртвым, хотя пул думает, что оно открыто?
  2. 02
    Какие три контроля держат соединения пула свежими и какое ключевое ограничение у каждого?
  3. 03
    Зачем уволять соединения по maxLifetime проактивно вместо опоры на одну валидацию?
Итог

Переиспользование, делающее пулинг быстрым, делает его и хрупким: соединение, которое пул держит, может быть тихо убито базой (MySQL wait_timeout по умолчанию 8 часов), stateful-фаерволами и NAT, сбрасывающими простаивающие потоки через минуты, или балансировщиками, перерабатывающими backend-ы, всё пока сокет пула выглядит ESTABLISHED — поэтому он одалживает мёртвое соединение и запрос падает с reset, классически первый запрос после тихой ночи. Три контроля держат пул свежим: maxLifetime (~30 мин) проактивно ротирует соединения и должен быть короче собственного таймаута базы, чтобы пул всегда закрывал первым; idleTimeout (~10 мин) сжимает пул вне пика, но лишь когда minimumIdle ниже максимума; и валидация при взятии плюс keepalive-прощупы ловят уже-мёртвые сокеты, пропуская проверку в окне aliveBypassWindow 500 мс, чтобы горячие соединения оставались быстрыми. Проактивная ротация и валидация дополняют друг друга — одна ловит соединения, умирающие в простое, другая те, что умерли бы в использовании — и ровная ротация также избегает переподключения стадом. Свежие, ограниченные, валидированные соединения держат счастливый путь здоровым; следующий урок встречает то, что случается, когда они кончаются по неправильной причине: утечки, исчерпание и метрики, ловящие их раньше пользователей.

Связанные уроки
Продолжить восхождение ↑Исчерпание пула: утечки и почему больший пул не спасёт
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.