13.8. Номера последовательности фиксации (CSN)#

13.8. Номера последовательности фиксации (CSN)

13.8. Номера последовательности фиксации (CSN) #

13.8.1. Обзор #

13.8.1.1. Что такое CSN? #

CSN (Commit Sequence Number) — это монотонно возрастающее 64-битное число, присваиваемое каждой транзакции в момент её фиксации. Оно отражает логический порядок фиксации транзакций в базе данных — если транзакция A фиксируется раньше транзакции B, то CSN транзакции A будет меньше, чем CSN транзакции B.

Ключевая идея CSN заключается в упрощении принятия решений о видимости. В традиционной MVCC на основе XID для определения видимости транзакции требуется проверить её статус фиксации в CLOG и сравнить идентификаторы транзакций со списком выполняющихся транзакций, зафиксированных в снимке (snapshot). С использованием CSN это сводится к простому числовому сравнению: транзакция видима, если её CSN меньше или равен CSN снимка.

Реальным узким местом масштабируемости в традиционной MVCC на основе XID является получение снимка (snapshot). Каждый раз, когда транзакции требуется снимок, необходимо захватить блокировку и пройтись по всем активным backend-процессам для сбора их идентификаторов транзакций. Эта операция становится всё более затратной по мере увеличения числа одновременных подключений — при тысячах подключений конкуренция за блокировку может серьёзно ограничить пропускную способность. CSN устраняет это узкое место, заменяя сканирование массива выполняющихся транзакций на чтение атомарных переменных, благодаря чему получение снимка по сути становится O(1) независимо от количества подключений.

13.8.1.2. Проблема традиционного MVCC на основе XID #

Традиционная MVCC на основе XID имеет несколько узких мест масштабируемости, которые усиливают друг друга при высокой конкурентности.

13.8.1.2.1. Получение снимка #

Наиболее значимым узким местом является получение снимка (Snapshot Acquisition). Каждый оператор, которому требуется согласованное представление базы данных, должен получить снимок, что требует взятия разделяемой блокировки на глобальном массиве процессов при обходе всех backend-процессов для сбора их активных идентификаторов транзакций. Стоимость этой операции растет линейно с увеличением числа подключений. Несмотря на то, что блокировка разделяемая (что позволяет одновременное чтение), она все же сериализуется с эксклюзивными захватами — а фиксация транзакции требует ту же блокировку в эксклюзивном режиме. При высокой нагрузке с тысячами короткоживущих транзакций это создает эффект конвоя, когда backend-процессы выстраиваются в очередь, ожидая получения снимка.

13.8.1.2.2. Переполнение подтранзакций #

Каждый backend-процесс может кэшировать до 64 идентификаторов подтранзакций. Когда транзакция превышает этот лимит — что легко происходит при использовании рекурсивных функций, блоков исключений PL/pgSQL или ORM-кода, активно использующего точки сохранения, — кэш подтранзакций переполняется. После этого снимок помечается как содержащий переполнение списка подтранзакций.

Когда при проверке видимости встречается снимок с переполнением, уже нельзя просто выполнить поиск по массиву subxid внутри снимка. Вместо этого системе необходимо разрешить подтранзакции, проходя по цепочке родителей с помощью поиска в pg_subtrans. Для этого требуется многократно следовать по ссылкам на родителя из пула буферов SLRU pg_subtrans, пока не будет найдена транзакция верхнего уровня.

SLRU pg_subtrans имеет пул буферов фиксированного размера. Когда нескольким backend-процессам одновременно требуются разные страницы, они конкурируют за буферные слоты. Если нужная страница отсутствует в буфере, backend-процесс должен ждать, пока она будет считана с диска; если все буферы заняты, один из них должен быть сначала вытеснен, что может потребовать записи на диск перед тем, как чтение сможет продолжиться. При большом количестве параллельных транзакций, обращающихся к разным частям pg_subtrans, это приводит к сериализации ввода-вывода и конкуренции за буферы, что значительно снижает производительность.

13.8.1.2.3. Фиксация транзакции #

Фиксация транзакции требует эксклюзивной блокировки глобального массива выполняющихся транзакций для удаления транзакции из активного списка. Хотя существуют оптимизации, такие как групповое освобождение для пакетной обработки нескольких фиксаций, фундаментальная сериализация сохраняется. Высоконагруженные OLTP-нагрузки с большим количеством коротких транзакций тратят значительное время на конкуренцию за эту блокировку.

13.8.1.3. Аномалия Long Fork в реплицированных кластерах #

В кластерах с репликами для чтения традиционный MVCC на основе XID приводит к аномалии Long Fork: порядок, в котором транзакции становятся видимыми, может различаться между основным сервером и репликами. Это нарушает изоляцию снимков (Snapshot Isolation). Два читателя могут наблюдать эффекты параллельных транзакций в разном порядке — например, запрос на основном сервере может видеть изменения T1, но не T2, в то время как запрос на реплике может видеть изменения T2, но не T1.

Основная причина заключается в том, что на основном сервере порядок, в котором неконфликтующие транзакции становятся видимыми (на основе момента их выхода из ProcArray, используемого для снимков), может отличаться от порядка, в котором они становятся надёжно сохранёнными (их порядок фиксации в журнале предзаписи — Write-Ahead Log). Транзакция становится надёжно сохранённой, когда она записывает свою фиксацию в WAL, и только после этого удаляет себя из ProcArray. Таким образом, одна транзакция может зафиксироваться раньше в WAL, но покинуть ProcArray позже другой. Реплики же применяют WAL в порядке фиксации, поэтому для них видимость фактически совпадает с порядком фиксации. Несовпадение между основной системой (видимость, управляемая ProcArray) и репликами (видимость, управляемая WAL) и приводит к возникновению Long Fork.

Это поведение не приводит к потере или повреждению данных, но может повлиять на приложения, которые предполагают согласованный порядок видимости между основным сервером и репликами (например, разделение чтения/записи, анализ восстановления на определённый момент времени или распределённые снимки). CSN решает эту проблему, обеспечивая соответствие порядка видимости порядку фиксации транзакций.

13.8.1.4. Как CSN решает эту проблему #

CSN устраняет эти узкие места и аномалию Long Fork, при этом наиболее значительное улучшение достигается в процессе получения снимка (snapshot acquisition).

13.8.1.4.1. Порядок видимости соответствует порядку фиксации #

Назначая номер последовательности фиксации (Commit Sequence Number, CSN) во время фиксации и используя этот CSN для определения видимости, CSN гарантирует, что порядок, в котором транзакции становятся видимыми, совпадает с их порядком фиксации. Нет отдельного глобального массива процессов, управляющего видимостью, который мог бы расходиться с порядком WAL. Реплики, применяющие WAL в том же порядке, видят тот же порядок видимости, что и основной сервер, что устраняет аномалию Long Fork. Это важно для реплик только для чтения, маршрутизации запросов или восстановления на определённый момент времени.

13.8.1.4.2. Получение снимка без блокировок #

CSN заменяет глобальное сканирование массива процессов двумя чтениями атомарными переменных для CSN снимка. Блокировки не требуются, и стоимость операции остается постоянной независимо от количества подключений. Это основное преимущество по производительности — получение снимка переходит от O(n) с конкуренцией за блокировки к O(1) без блокировок.

13.8.1.4.3. Проверка видимости #

Кортежи по-прежнему хранят xmin/xmax как идентификаторы транзакций, поэтому для проверки видимости необходимо получать CSN для этих XID из SLRU CSNLOG. Для подтранзакций CSNLOG напрямую хранит родительский XID, что позволяет выполнять поиск по цепочке родителей внутри одного и того же SLRU. Традиционный MVCC требует переключения между CLOG (для статуса фиксации) и pg_subtrans (для цепочки родителей) — двумя отдельными SLRU с независимыми пулами буферов и блокировками. CSN объединяет это в единую структуру.

13.8.1.4.4. Обработка подтранзакций #

Видимость подтранзакций включает различные компромиссы между традиционной MVCC и CSN.

Для выполняющихся подтранзакций традиционный MVCC без переполнения на самом деле проще — он просто ищет в массиве subxip снимка. CSN должен проходить вверх по цепочке родителей в CSNLOG, потому что выполняющиеся подтранзакции хранят указатель на родителя, а не CSN.

Когда происходит переполнение подтранзакции в традиционном MVCC, каждая проверка видимости для подтранзакции требует прохода по pg_subtrans. Если множество backend-процессов одновременно сталкиваются с переполнением, pg_subtrans SLRU становится серьезной точкой конкуренции. CSN полностью избегает этого — здесь нет понятия переполнения, потому что снимки CSN не отслеживают отдельные subxid. Проход по родительским транзакциям происходит в CSNLOG, том же SLRU, который используется для всего остального.

Для зафиксированных подтранзакций CSN однозначно лучше. Традиционный MVCC помечает подтранзакции как SUB_COMMITTED в CLOG и должен проходить по pg_subtrans, чтобы проверить, что родитель зафиксирован. CSN записывает итоговый CSN непосредственно во все записи подтранзакций в момент фиксации, поэтому запросы возвращают результат немедленно, без обхода цепочки родителей.

13.8.1.5. Когда использовать CSN #

CSN приносит преимущества рабочим нагрузкам с высокой параллельностью и большим количеством короткоживущих транзакций. Безблокировочное получение снимков хорошо масштабируется при тысячах одновременных подключений, а устранение конкуренции при переполнении подтранзакций помогает рабочим нагрузкам, использующим точки сохранения или обработку исключений в PL/pgSQL.

Однако у CSN есть значительный компромисс при работе с долгоживущими транзакциями.

13.8.1.5.1. Проблема долгоживущих транзакций #

Снимок фиксирует две ключевые границы: snapshot->xmin (самая старая активная транзакция на момент создания снимка) и snapshot->xmax (следующий XID, который будет назначен). При проверке видимости кортежа база данных сравнивает значения xmin и xmax кортежа с этими границами.

Если xmin или xmax кортежа попадает в диапазон [snapshot->xmin, snapshot->xmax), требуется дополнительная работа для определения видимости. В традиционном MVCC это означает проверку массива xip снимка — списка транзакций в памяти, которые были в процессе выполнения на момент создания снимка. В случае CSN это означает получение CSN транзакции из CSNLOG SLRU.

Когда существует долгоживущая транзакция, она удерживает snapshot->xmin на старом значении, в то время как snapshot->xmax продолжает увеличиваться. Это делает диапазон [snapshot->xmin, snapshot->xmax) очень большим. Больше кортежей имеют свои xmin или xmax, попадающие в этот диапазон, что требует большего количества обращений к данным.

Критическое отличие: поиск по массиву xip в традиционном MVCC выполняется в памяти, поэтому увеличение диапазона не добавляет накладных расходов. CSN требует обращений к CSNLOG SLRU для каждого XID в этом диапазоне.

Если ваша рабочая нагрузка сочетает долгоживущие аналитические запросы с OLTP-транзакциями, оцените, перевешивают ли выигрыш от получения снимка увеличенные затраты проверки видимости.

13.8.1.5.2. Небольшое число подключений #

Рабочие нагрузки лишь с несколькими десятками подключений могут не ощутить значительной выгоды от получения снимков без блокировок, поскольку конкуренция за ProcArrayLock в этом случае вполне управляемая.

13.8.2. Архитектура #

13.8.2.1. Структура CSNLOG SLRU #

CSNLOG — это буфер SLRU (Simple Least Recently Used), который хранит номера последовательности фиксаций для транзакций. Он находится в каталоге pg_csnlog и устроен аналогично CLOG, но с другим размером записей.

13.8.2.1.1. Макет страницы #

Каждая страница CSNLOG имеет размер 8 КБ. Каждая запись занимает 8 байт (uint64), поэтому одна страница содержит 1024 записи о транзакциях:

    ┌────────────────────────────────────────────────────────────────┐
    │ CSNLOG Page (8KB)                                              │
    ├────────────────────────────────────────────────────────────────┤
    │ Entry 0: CSN for XID (page_number * 1024 + 0)     [8 bytes]    │
    │ Entry 1: CSN for XID (page_number * 1024 + 1)     [8 bytes]    │
    │ ...                                                            │
    │ Entry 1023: CSN for XID (page_number * 1024 + 1023) [8 bytes]  │
    └────────────────────────────────────────────────────────────────┘

    Mapping:
      Page number = XID / 1024
      Item number = XID % 1024
    
13.8.2.1.2. Значения записей #

Каждая 8-байтовая запись может содержать:

Значение Описание
0x0 (CSN_INPROGRESS) Транзакция выполняется
0x1 (CSN_ABORTED) Транзакция прервана
0x2 (CSN_COMMITTING) Транзакция в процессе фиксации (короткий промежуток)
0x3 (CSN_FROZEN) Очень старая зафиксированная транзакция
≥ 0x4 Фактический CSN (зафиксированная транзакция)
(1<<63) | parent_xid Подтранзакция с указателем на родительскую транзакцию

Старший бит (CSN_SUBTRANS_BIT) отличает указатели на родительские подтранзакции от значений CSN. Обычные значения CSN находятся в диапазоне [4, 2^63-1].

13.8.2.1.3. Сравнение с pg_subtrans #
CSNLOG pg_subtrans
Размер записи 8 байт (CommitSeqNo) 4 байта (TransactionId)
Записей на странице 8KB 1024 2048
Содержимое CSN или указатель на родителя Только указатель на родителя
Используется для Видимости + подтранзакций Только подтранзакций

CSNLOG менее плотный, но совмещает две функции: хранение CSN для зафиксированных транзакций и указателей на родителя для выполняющихся подтранзакций.

13.8.2.2. Компоненты общей памяти #

CSN использует несколько структур общей памяти для координации между backend-процессами без блокировок.

13.8.2.2.1. Буферы CSNLOG SLRU #

Сам CSNLOG SLRU поддерживает пул буферов в общей памяти, управляемый параметром csnlog_slot_size. Доступ координируется с помощью блокировки.

13.8.2.3. Интеграция со структурой снимков (Snapshot) #

CSN добавляет поле snapshot_csn в структуру снимка:

13.8.2.3.1. Правило видимости #

Для традиционного MVCC:

  • Видимо, если XID < xmin, ИЛИ

  • Видимо, если XID отсутствует в xip[] и XID < xmax и транзакция зафиксирована

Для CSN:

  • Видимо, если CSN транзакции < snapshot_csn снимка

xmin и xmax всё ещё используются для быстрой фильтрации перед обращением к CSNLOG. Если xid >= xmax, сразу понятно, что запись не видима, без проверки CSNLOG.

13.8.2.4. Восстановление и WAL #

Изменения CSNLOG записываются в WAL для обеспечения надежности и корректного восстановления. Используются два типа WAL-записей:

  • XLOG_CSNLOG_ZEROPAGE — записывается в журнал при выделении новых страниц CSNLOG

  • XLOG_CSNLOG_TRUNCATE — записывается в журнал при удалении старых страниц CSNLOG

Во время восстановления после сбоя функции повтора (redo) повторно применяют эти записи, чтобы восстановить состояние CSNLOG.

Отдельные записи CSN (при фиксации транзакции) не записываются отдельно в WAL — они являются частью записи фиксации. Значение CSN вычисляется из атомарного счетчика, который восстанавливается путем сканирования зафиксированных транзакций во время запуска.

13.8.3. Конфигурация #

13.8.3.1. csn_enable #

Включает функцию CSN.

Тип boolean
Значение по умолчанию true
Контекст postmaster (требуется перезапуск)

Когда включено, получение снимка использует чтение атомарных переменных вместо массива выполняющихся транзакций, а проверка видимости использует CSNLOG вместо CLOG + pg_subtrans.

Транзакции, зафиксированные до включения CSN, обрабатываются с помощью резервного механизма CLOG: если XID транзакции старше самой старой активной границы снимка, get_csn() проверяет CLOG вместо CSNLOG и возвращает CSN_FROZEN для зафиксированных транзакций. Это позволяет безопасно включать CSN на существующих кластерах без миграции.

13.8.3.2. csn_elog_panic_enable #

Управляет обработкой ошибок при несоответствиях состояния CSN.

Тип boolean
Значение по умолчанию true
Контекст postmaster

Когда параметр включён, система вызовет PANIC, если состояние CSN подтранзакции не совпадает с состоянием её родительской транзакции (например, дочерняя транзакция отмечена как зафиксированная, а родительская — как выполняющаяся). Когда параметр отключён, вместо этого в журнал записывается предупреждение. Включённое состояние помогает раньше обнаруживать ошибки, но в отдельных пограничных случаях может вызывать лишние аварийные завершения.

13.8.3.3. csnlog_slot_size #

Устанавливает размер пула буферов CSNLOG SLRU.

Тип integer
Значение по умолчанию 8192
Диапазон 128 - 131072
Контекст postmaster

Количество страниц CSNLOG для буферизации в общей памяти. Большие значения уменьшают количество операций ввода-вывода на диск при обращениях к CSNLOG, но потребляют больше общей памяти. Размер каждой страницы — 8 КБ.

13.8.3.4. csnlog_upperbound_enable #

Включает оптимизацию кэширования верхней границы.

Тип boolean
Значение по умолчанию true
Контекст postmaster

Когда включено, поддерживает кольцевой буфер в разделяемой памяти из 4 194 304 слотов (~32 МБ), хранящий максимальный CSN для каждой группы из 1024 XID. Проверки видимости для зафиксированных кортежей сначала обращаются к кэшу; если значение слота меньше CSN снимка, поиск в SLRU полностью пропускается.

Не имеет эффекта, если также не включён csn_enable. Включайте этот параметр, когда нагрузка преимущественно на чтение, и к кортежам часто обращаются повторно после установки их hint-битов. Эффективность можно отслеживать с помощью csnlog_upperbound_stats() из расширения csn.

13.8.4. Совместимость и ограничения #

13.8.4.1. Логическое декодирование #

Logical decoding работает с включённым CSN. CSN не изменяет фундаментальный механизм декодирования — изменения по-прежнему декодируются на основе порядка фиксации транзакций, который естественным образом совпадает с порядком CSN.

13.8.4.2. Известные ограничения #

13.8.4.2.1. Долгоживущие транзакции ухудшают производительность #

Когда существует долгоживущая транзакция, она удерживает значение snapshot->xmin. Это расширяет диапазон [xmin, xmax), из-за чего больше проверок видимости попадает на путь "необходимо проверить CSNLOG", вместо того чтобы быстро разрешаться сравнением xmin/xmax. Нагрузки, сочетающие длительные аналитические запросы с OLTP-транзакциями, могут получать меньшую выгоду от CSN.

13.8.4.2.2. CSNLOG менее плотный, чем pg_subtrans #

CSNLOG хранит записи размером 8 байт (1024 на страницу) по сравнению с 4-байтовыми записями pg_subtrans (2048 на страницу). Это означает, что CSNLOG использует больше дискового пространства и пространства пула буферов для одного и того же диапазона XID. Однако CSNLOG объединяет то, что в противном случае потребовало бы отдельных обращений к CLOG и pg_subtrans.

13.8.5. Устранение неполадок #

13.8.5.1. Распространённые проблемы #

13.8.5.1.1. Снижение производительности при долгоживущих транзакциях #

Если вы замечаете увеличение задержки при выполнении запросов на чтение во время активных долгоживущих транзакций, вероятной причиной является расширенный диапазон [xmin, xmax), который вынуждает выполнять больше обращений к CSNLOG. Симптомы:

  • Задержка выполнения запросов на чтение увеличивается при выполнении аналитических запросов или операций обслуживания

  • Производительность возвращается к норме после фиксации или отката долгой транзакции

Решения:

  • Не смешивайте долгоживущие запросы с OLTP-нагрузками на одном и том же экземпляре

  • Для аналитических запросов используйте реплики только для чтения

  • По возможности разбивайте длинные транзакции на более мелкие части

13.8.5.2. Диагностические запросы #

Проверка, включен ли CSN:

    SHOW csn_enable;
    

Поиск долгоживущих транзакций:

    SELECT
        pid,
        now() - xact_start AS duration,
        state,
        query
    FROM pg_stat_activity
    WHERE xact_start IS NOT NULL
      AND state != 'idle'
    ORDER BY xact_start;
    

13.8.5.3. Справочник по сообщениям об ошибках #

"Wrong csn state, xid: %u, parent_xid: %u, csn: %lu"

Указывает, что состояние CSN подтранзакции не согласовано с её родительской транзакцией. Например, дочерняя транзакция выглядит зафиксированной, но родительская отображается как находящаяся в процессе выполнения. Обычно это указывает на ошибку или повреждение данных.

  • Если csn_elog_panic_enable = on (по умолчанию): сервер выполнит PANIC

  • Если csn_elog_panic_enable = off: будет записано предупреждение в журнал, и выполнение продолжится

Если вы столкнулись с этим в рабочей среде, сохраните подробности из журнала и сообщите о проблеме.

"cannot mark transaction committed without CSN"

Внутренняя ошибка, указывающая на то, что транзакция пытается зафиксироваться без присвоенного CSN. Это не должно происходить при нормальной работе и указывает на наличие ошибки.

"pg_csnlog contains invalid entry: xid %u points to parent xid %u"

Указатель на родительскую транзакцию подтранзакции в CSNLOG указывает на недопустимого родителя (родительский XID не старше дочернего). Это указывает на повреждение CSNLOG и о такой проблеме следует сообщить.

13.8.6. Конфликты #

13.8.6.1. Расширение pg_ivm #

Расширение pg_ivm несовместимо с MVCC на основе CSN и не может быть использовано, когда CSN включён.

13.8.6.2. Временные таблицы на репликах #

GUC enable_temp_table_on_replica несовместим с MVCC на основе CSN и не может быть использован, когда CSN включён.