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

Базы данных

Реальные провалы MVCC, deployment-паттерны и распределённые снимки

Суть Долгие транзакции и orphan slot пинят oldest-xmin; pg_repack, idle_in_transaction_session_timeout и маршрутизация аналитики держат bloat в рамках. История Postgres 8.3–18 и как распределённые системы расширяют MVCC через HLC.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 20 min

Sentry 2017: autovacuum отстал, XID wraparound сработал, кластер отказал в соединениях, команда делала ручную заморозку под огнём. GitLab 2017: мисклик в обычном maintenance-окне удалил не ту директорию, а окружающий bloat сделал pg_dump-восстановление на порядки длиннее. Оба инцидента — один корень: MVCC storage-налог без дисциплины.

Реальные провалы: bloat от долгих транзакций и orphan replication slot

Канонический production-паттерн провала:

  1. 6-часовая ETL или аналитическая SELECT идёт против OLTP-кластера
  2. Её backend_xmin пинит глобальный oldest-xmin
  3. Каждый autovacuum таблицы orders логирует: cannot remove tuples, oldest xmin: XXXXXXXX
  4. За ночь orders вырастает с 80 ГБ до 180 ГБ
  5. Утренняя команда находит disk-full alert в 7 утра

Orphan replication slot, когда downstream-консумер умер, — отдельный, но идентично выглядящий провал: колонка xmin slot держит глобальный oldest-xmin пока кто-то не заметит и не удалит slot.

Диагностика пина:

-- Найти самого старого держателя snapshot
SELECT pid, backend_xmin,
       now() - xact_start AS age,
       state, query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY backend_xmin
LIMIT 5;

-- Найти orphan replication slot
SELECT slot_name, xmin, confirmed_flush_lsn
FROM pg_replication_slots
WHERE xmin IS NOT NULL;

Самый маленький backend_xmin или xmin slot совпадёт с тем, что autovacuum логировал как “oldest xmin”. Лечение: pg_terminate_backend(pid) или pg_drop_replication_slot('name'). Следующий цикл autovacuum уберёт мёртвые tuple.

Стратегии митигации:

  • Маршрутизировать аналитику на реплику с hot_standby_feedback = off — primary autovacuum не удерживается
  • Использовать logical-реплику с собственной независимой политикой autovacuum
  • Ставить statement_timeout 30 минут под REPEATABLE READ — заставляет долгие запросы громко падать вместо тихого пинения

hot_standby_feedback = off на аналитических репликах — единственная наиболее важная настройка: когда она включена, реплика отсылает primary свой xmin, и первичный autovacuum не может убирать строки, нужные реплике. 6-часовой аналитический запрос на реплике с hot_standby_feedback = on неотличим от того же запроса на primary — оба пинят bloat.

Misconception: «У Postgres есть snapshot-too-old как у Oracle»

Deployment patterns: pg_repack, pgbouncer, idle_in_transaction_session_timeout

pg_repack переписывает раздутую таблицу онлайн без долгого ACCESS EXCLUSIVE:

  1. Создаёт теневую копию таблицы
  2. Применяет WAL-изменения инкрементально по мере создания копии
  3. Атомарно меняет теневую копию на оригинал (короткий ACCESS EXCLUSIVE только на финальный swap)

Ловушка: временно 2x disk usage во время переписывания. Production-дисциплина: pg_repack ночью на топ-10 самых раздутых таблиц.

pgbouncer в transaction mode мультиплексирует тысячи клиентских соединений на маленький пул Postgres-бэкендов. MVCC-взаимодействие: session-mode SET команды утекают между клиентами в transaction mode.

-- Утекает в transaction mode — неправильно:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Использовать SET LOCAL — скоупится до текущей транзакции:
SET LOCAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET LOCAL автоматически откатывается в конце транзакции, поэтому следующий клиент получает чистое соединение.

idle_in_transaction_session_timeout (по умолчанию off; рекомендация в production: 5–15 минут) убивает сессии с открытой транзакцией без активности. Многие production-инциденты ведут к приложению, открывшему транзакцию на старте запроса и забывшему закрыть её на ошибочной ветке.

-- Кластерно в postgresql.conf:
idle_in_transaction_session_timeout = '10min'
-- Или per-role:
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '5min';

Числа

Operational MVCC числа
Длина HOT-цепочки (типично)
4–8 версий до разрыва
Дефолтный fillfactor
100 (таблицы), 90 (индексы)
MultiXact wraparound
2B, отдельный от XID
Trigger заморозки
autovacuum_freeze_max_age 200M
Доля false positive SSI
0.1–1% здорово; >5% тюнить
pg_repack онлайн-стоимость
временно 2x disk
idle_in_transaction_session_timeout
рекомендация 5–15 мин
Риск logical slot lag
orphan slot закрепляет xmin

Диагностика лога autovacuum

Найди ошибку

Разбери лог autovacuum — что не так и как починить?

log
LOG:  automatic vacuum of table "production.public.orders": index scans: 1
  pages: 0 removed, 18475821 remain, 1854000 skipped due to pins
  tuples: 0 removed, 412809321 remain, 287163445 are dead but not yet removable, oldest xmin: 28391456
  index scan needed: 32140 pages from table (0.17% of total) had 281422 dead item identifiers removed
  avg read rate: 28.412 MB/s, avg write rate: 5.211 MB/s
  buffer usage: 4189213 hits, 8923214 misses, 21430 dirtied
  WAL usage: 41203 records, 0 full page images, 9241221 bytes
  system usage: CPU: user: 312.18 s, system: 18.91 s, elapsed: 1842.21 s

VACUUM шёл 30 минут, удалил 0 мёртвых tuple, и 287 миллионов «not yet removable». Корневая причина и как подтвердить + починить?

История Postgres: 8.3 → 18

ВерсияГодMVCC-значимое изменение
8.32008HOT-обновления
9.12011SSI (настоящий SERIALIZABLE)
9.52016INSERT ... ON CONFLICT (upsert, тонко взаимодействует с MVCC через внутренний retry)
122019Декларативное партиционирование — multi-billion-row таблицы наконец практичны
132021Incremental sort, ниже стоимость VACUUM на heavy-update таблицах
142021Bottom-up index deletion — резко снижает index bloat для UPDATE-нагрузок где индексированные колонки редко меняются
162023Улучшена производительность cold-start заморозки
172024Vacuum переархитектурирован на TID-store вместо массивов — убрано ограничение maintenance_work_mem на мёртвые tuple за проход (та самая строка “VACUUM has reached the maintenance_work_mem limit, restarting”)
182025Улучшен skip-scan при заморозке

Cross-protocol: logical replication, BDR, Citus

Logical replication использует MVCC под капотом. WAL-декодер читает изменения tuple из WAL и продуцирует поток, который subscriber применяет. Subscriber запускает собственный независимый autovacuum. Logical slot закрепляет xmin publisher — idle subscriber это медленная bloat-бомба. Монитори pg_replication_slots.xmin и алертируй на orphan slot.

BDR (BiDirectional Replication, EDB) реализует eventual consistency поверх logical replication с резолвером конфликтов, понимающим MVCC: last-update-wins по timestamp xmin tuple.

Citus шардирует таблицу по worker-нодам. Каждый шард — настоящая Postgres-таблица с собственным MVCC. Snapshot координатора прокатывается в воркеры как «глобальный snapshot» в worker-соединении. Distributed serializable транзакции на Citus требуют, чтобы все worker-ноды согласились на SSI-зависимости — реализовано через предикатно-блокировочную таблицу координатора.

Распределённые снимки и глобальная сериализуемость

Single-node Postgres имеет глобальный snapshot per backend в shared memory. При шардировании на несколько Postgres-инстансов (Citus, YugabyteDB, предки Postgres-XC) единого shared-memory snapshot не существует.

Стандартный подход — hybrid logical clocks (HLC): каждый инстанс держит логический таймстамп, монотонно растущий, транзакции метятся HLC-значениями на старте, и решения видимости на удалённых шардах используют HLC-порядок. HLC должен согласовываться с wall-clock-порядком достаточно, чтобы люди могли рассуждать о «транзакции в 12:00:01» — TrueTime в Spanner это одно решение; HLC в YugabyteDB — другое.

SSI по шардам требует либо:

  • Централизованной таблицы предикатных блокировок (корректно, но медленно)
  • Детерминированного протокола concurrency-control (стиль Calvin; FaunaDB; гибрид CockroachDB), который сериализует на старте, а не на коммите

Citus выбирает прагматику: distributed transactions по умолчанию — read-committed-with-prepared-commit, а приложения, нуждающиеся в более сильной изоляции, ко-локируют зависимые строки на одном шарде.

Проверь себя

Викторина

Orphan replication slot вызывает неконтролируемый bloat. Какое системное представление подтверждает xmin slot и показывает насколько он отстал?

Викторина

В pgbouncer transaction mode почему нужно использовать SET LOCAL вместо SET для изменения уровня изоляции?

Викторина

pg_repack переписывает таблицу онлайн. Какова основная операционная стоимость по сравнению с VACUUM FULL?

Вспомните перед уходом
  1. 01
    200 ГБ таблица orders не убирается несмотря на работающий autovacuum. В логе 287M tuple 'not-yet-removable'. Пройди диагностику и лечение.
  2. 02
    Почему hot_standby_feedback = on опасен для primary bloat, и когда он приемлем?
  3. 03
    Объясни почему Citus по умолчанию использует read-committed-with-prepared-commit для distributed transactions, а не distributed SSI.
Recap
  • Долгие транзакции и orphan replication slot пинят oldest-xmin и блокируют autovacuum — одна форма провала, разные источники
  • Диагностика: pg_stat_activity WHERE backend_xmin IS NOT NULL и pg_replication_slots WHERE xmin IS NOT NULL
  • idle_in_transaction_session_timeout (5–15 мин) убивает забытые открытые транзакции до накопления bloat
  • hot_standby_feedback = off на аналитических репликах отвязывает primary autovacuum от lifecycle реплика-запросов
  • pg_repack переписывает раздутые таблицы онлайн ценой временного 2x disk; VACUUM FULL требует долгую блокировку
  • В pgbouncer transaction mode: SET LOCAL не SET — session-mode настройки утекают между pooled клиентами
  • Postgres 17 убрал ограничение maintenance_work_mem на vacuum, заменив массивы TID-store
  • Distributed MVCC использует HLC для cross-shard видимости; SSI по шардам требует централизованных предикатных блокировок или детерминированного CC
Связанные уроки
встречается в140
Продолжить восхождение ↑MVCC и изоляция: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources6
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06

Trademarks belong to their respective owners. Editorial reference only.