← Назад к проектам

Соединение с базой, у которого нет памяти

После переезда на три сервера я стал ловить странное: фоновая чистка базы шла в несколько параллельных проходов, хотя по задумке её должен был делать ровно один процесс. Выбор «главного» держался на блокировке уровня сессии Postgres — классический приём, который годами работал на одной машине. Я сделал замер прямо на боевой инфраструктуре: запустил выбор лидера на всех живых процессах и посчитал, сколько из них считают лидером себя. Получилось трое из четырёх, а на расширенной пробе — четверо из восьми. Причина оказалась не в коде, а в том, что боевой доступ к базе идёт через пулер соединений в транзакционном режиме: каждая транзакция получает произвольный серверный процесс из общего пула, и блокировка, привязанная к сессии, достаётся всем желающим сразу. Хуже того, такая блокировка ещё и «протекает» — пулер по таймауту простоя закрывает соединение и снимает её молча.

Починил не подкруткой, а сменой механизма: лидерство стало арендой с TTL — обычной строкой в таблице, которую держатель продлевает, а мёртвого лидера по истечении срока сменяет живой. Аренда перепроверяется на каждом тике, а не фиксируется один раз на старте. Заодно вскрылась вторая, более неприятная вещь: стартовый параметр соединения, которым выставлялся предохранитель на длительность запросов, пулер просто отбрасывает — он в списке игнорируемых. То есть защита, которая числилась в документации проекта как работающая, по факту была выключена, и её отсутствие никак не проявлялось. Лечится на стороне базы — дефолтом роли, который применяется при старте серверного процесса и переживает пулинг. Правило на будущее записал прямо в заметки: проверять факт, а не намерение — спрашивать значение у боевого контейнера, а не читать код.

Фактура

  • Режим пулера — транзакционный: соединение выдаётся на транзакцию, не на сессию.
  • Замер: 3 из 4 рабочих процессов считали себя лидером; на пробе в 8 процессов старый механизм дал 4 лидера, аренда с TTL — ровно 1.
  • Побочный эффект старого механизма: чистка базы выполнялась втрое чаще положенного.
  • Предохранитель на длительность запроса по факту был равен нулю у всех трёх сервисов, хотя в документации проекта числился как включённый (15 секунд).
  • Что через пулер работает: блокировки, ограниченные транзакцией (ими сериализуется применение схемы при старте нескольких воркеров), и строковые блокировки с TTL.
  • Что не работает: сессионные блокировки, установка параметров вне транзакции, временные таблицы, стартовые параметры соединения.
  • Отвергнутые гипотезы: «воркеры стартуют слишком быстро и гонятся» (нет, лидерство выдавалось стабильно всем), «падает соединение» (соединения живые).
  • Регрессия закрыта отдельным набором тестов на аренду лидерства.
  • Отдельная история сбоку: эти фиксы какое-то время жили только на боевых машинах (правились патчем на месте) и не были в репозитории — любой деплой откатил бы их молча. Пришлось отдельным проходом возвращать боевые правки в историю.