← Все статьи

Как одно соединение «убивает» базу данных

Открытая транзакция после необработанного исключения блокирует миграцию и всю очередь запросов — кейс GitHub и Postgres.

Как одно соединение «убивает» базу данных
Содержание

Коротко

На GitHub когда-то падала база не из‑за взлома, а из‑за одной незакрытой транзакции: миграция ждала эксклюзивную блокировку, новые запросы вставали в очередь за ней — и всё «зависло», пока инженер не нашёл «верхнее» соединение. Тот же сценарий легко воспроизвести в Postgres: необработанное исключение в приложении оставляет транзакцию открытой, миграция блокируется, горячая таблица перестаёт отвечать.

Что произошло

Классический сценарий на MySQL: изменение схемы требует эксклюзивной блокировки таблицы, но другая сессия уже читала эту таблицу и не сделала COMMIT. Миграция ждёт. За ней — все новые запросы к той же таблице. Ни deadlock, ни таймаут сами по себе не спасают: цепочка сидит, пока не завершат проблемное соединение.

В Postgres достаточно обычного пользователя приложения. Транзакция начинает SELECT по горячей таблице orders, приложение падает с исключением — ROLLBACK не вызывается. Сессия «в транзакции», хотя запрос уже завершился. Параллельно миграция делает ALTER TABLE и ждёт ACCESS EXCLUSIVE. Все последующие SELECT встают в очередь. Для сервиса, который ждёт быстрых ответов из orders, это простой.

Причина часто в настройке по умолчанию: idle_in_transaction_session_timeout выключен — «висящие» транзакции не обрываются сами.

PlanetScale описывает инструменты для просмотра и завершения соединений в панели и CLI (pscale branch connections), в том числе когда слоты подключений исчерпаны и отладочный SELECT уже не пройти.

Почему это важно

Это не редкий edge case и не прерогатива MySQL. Любая СУБД с блокировками на уровне таблицы/схемы уязвима к «одному плохому соединению». Симптомы выглядят как полный отказ: все запросы падают, метрики приложения красные, а корень — одна сессия без COMMIT/ROLLBACK.

На собеседованиях этот кейс GitHub использовали как проверку системного мышления DBA. В проде его ловят пейджингом в пятницу вечером.

На практике

  1. В Postgres задайте idle_in_transaction_session_timeout (разумное значение в миллисекундах) — транзакции без активности закроются автоматически.
  2. В коде приложения: при любом исключении внутри транзакции — явный ROLLBACK или закрытие соединения через пул с корректной очисткой.
  3. Миграции на горячих таблицах планируйте с учётом блокировок; для MySQL на Vitess/PlanetScale онлайн-ALTER снижает риск длинной эксклюзивной блокировки.
  4. Держите способ смотреть активные сессии и «кто кого блокирует» до инцидента, не только когда слоты кончились.
  5. При разборе инцидента различайте три действия: отменить запрос, завершить транзакцию, оборвать соединение — для «зависшего» SELECT без активного запроса часто нужен именно terminate transaction или terminate connection.

Итог

Падение базы из‑за одного необработанного исключения — урок про границы приложения и СУБД. Таймауты idle-транзакций, дисциплина ROLLBACK и нормальный обзор соединений стоят дешевле, чем час простоя горячей таблицы.