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

Базы данных

Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базу

Суть Ожидающий ALTER TABLE блокирует каждый последующий SELECT на таблице — FIFO-очередь блокировок превращает один медленный запрос в полный простой таблицы.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Команда запускает ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active' в 14:00. На PG 16 этот запрос занимает 8 мс. Тем не менее таблица users недоступна 90 секунд, connection pool исчерпан, и сайт возвращает 503. Сама миграция была мгновенной. Что же произошло?

Матрица блокировок Postgres

Каждая операция с таблицей берёт блокировку. Существует восемь режимов блокировки от слабейшего к сильнейшему:

ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE

Ключевые факты:

  • SELECT берёт ACCESS SHARE (слабейший).
  • Большинство DDL (ALTER TABLE, DROP TABLE, TRUNCATE) берёт ACCESS EXCLUSIVE (сильнейший — конфликтует со всем).
  • CREATE INDEX CONCURRENTLY и VALIDATE CONSTRAINT берут SHARE UPDATE EXCLUSIVE — не блокируют обычный DML.

Матрица блокировок определяет, что может выполняться параллельно. ACCESS EXCLUSIVE конфликтует с каждым другим режимом блокировки, включая ACCESS SHARE. Это означает: любой DDL с ACCESS EXCLUSIVE должен ждать завершения всех текущих читателей — и блокирует всех новых читателей, пока ждёт.

FIFO-очередь блокировок — реальный режим отказа

Postgres выдаёт блокировки в порядке поступления (FIFO для объекта). Это создаёт классический инцидент с миграцией:

ВремяСобытиеСостояние блокировки на users
T=0сМедленный аналитический SELECT начинается (ожидаемо: 60с)ACCESS SHARE выдана
T=10сМиграция запускает ALTER TABLE (lock_timeout по умолчанию = 0)ACCESS EXCLUSIVE ждёт — конфликтует с удерживаемой ACCESS SHARE
T=10с+Новые SELECT поступают (API-запросы, фоновые задачи)ACCESS SHARE ждёт — в очереди ПОСЛЕ запроса ALTER на ACCESS EXCLUSIVE
T=70сМедленный SELECT завершаетсяALTER получает блокировку, выполняется за 8 мс, коммит. Очередь дренируется.
Итоговый ущерб60с без новых запросов к users. Connection pool исчерпан. 503.

Критическое понимание: новые SELECT встают в очередь после ожидающего ALTER, потому что выдача им блокировок не по порядку лишила бы ALTER шанса когда-либо дождаться своей очереди. Запрос ALTER на ACCESS EXCLUSIVE уже в очереди; менеджер блокировок отказывается выдавать более лёгкие блокировки поздним прибывшим, конфликтующим с ним. Таблица заморожена для всех новых запросов на полные 60 секунд медленного SELECT — даже несмотря на то, что сама миграция выполняется за 8 мс.

Решение: SET lock_timeout

-- В начале каждой сессии миграции:
SET lock_timeout = '2s';
SET statement_timeout = '30s';

С lock_timeout = '2s':

  • Миграция ждёт блокировку не более 2 секунд.
  • Если блокировка не получена, миграция чисто завершается с ошибкой и освобождает своё место в очереди.
  • Новые SELECT немедленно получают свои более лёгкие блокировки — таблица разморожена.
  • Инструмент миграции повторяет попытку с экспоненциальным откатом (1с, 2с, 4с, 8с…).
  • В конечном счёте медленный SELECT завершается или открывается тихое окно, и миграция успешно выполняется.

Без lock_timeout (умолчание Postgres = 0 — ждать вечно): один медленный запрос может заморозить таблицу на всё своё время выполнения.

Числа инцидента с очередью блокировок
Умолчание lock_timeout в Postgres
0 (ждать вечно)
Рекомендованный lock_timeout для миграций
1–5 с
Рекомендованный statement_timeout для DDL
30–60 с
Количество повторных попыток миграции
3–5 с экспоненциальным откатом
Длительность заморозки таблицы без lock_timeout
= длительность блокирующего запроса
Заморозка с lock_timeout = 2с
2 с, затем очередь очищается
Почему это работает

Почему Postgres не позволяет более лёгким блокировкам пропустить вперёд ожидающую ACCESS EXCLUSIVE? Потому что это могло бы морить DDL-запрос голодом бесконечно — на занятой таблице ACCESS SHARE блокировки поступают непрерывно и никогда не освобождаются все одновременно. FIFO гарантирует, что DDL в конечном счёте получит свою очередь. Операционное следствие: нужно ограничивать время ожидания.

Викторина

Миграция запускает ALTER TABLE на занятой таблице. Идёт долгий SELECT. Что происходит с новыми SELECT, которые поступают?

Викторина

Вы установили `SET lock_timeout = '2s'` на сессию миграции. Миграция не может получить блокировку в течение 2 секунд. Что происходит?

Викторина

Какую блокировку берёт CREATE INDEX CONCURRENTLY, и почему это безопаснее для продакшна?

Вспомните перед уходом
  1. 01
    Объясните точно, почему мгновенный ALTER TABLE может заморозить продакшн-таблицу на 60+ секунд.
  2. 02
    Объясните точную роль lock_timeout в предотвращении заморозки очереди блокировок и что делает инструмент миграции после таймаута.
  3. 03
    В чём разница между lock_timeout и statement_timeout, и почему сессии миграции нужны оба?
Итог

Матрица блокировок Postgres назначает ACCESS EXCLUSIVE большинству DDL и ACCESS SHARE для SELECT. Они конфликтуют, поэтому ожидающий ALTER TABLE блокирует все новые SELECT на той же таблице через FIFO-очередь блокировок — даже если сама миграция выполнится за миллисекунды. На занятой таблице с запущенным медленным аналитическим запросом это может заморозить таблицу на всё время его выполнения. Обязательное решение — SET lock_timeout = '2s' в каждой сессии миграции: если блокировка недоступна в течение 2 секунд, миграция чисто завершается с ошибкой, очередь разблокируется, и инструмент миграции повторяет попытку с откатом. CREATE INDEX CONCURRENTLY и VALIDATE CONSTRAINT берут SHARE UPDATE EXCLUSIVE вместо ACCESS EXCLUSIVE и поэтому безопасны для DML в продакшне.

Связанные уроки
встречается в258
Продолжить восхождение ↑Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операций
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.