Статья продолжает цикл публикаций о технологиях, реализованных в машине баз данных Tantor XData Gen3:
Что такое CSN?
CSN (Commit Sequence Number) – монотонно возрастающее 64-битное число, присваиваемое каждой транзакции в момент её фиксации. Оно представляет собой логический порядок фиксации транзакций в базе данных: если транзакция A фиксируется раньше транзакции B, то CSN транзакции A будет меньше, чем CSN транзакции B.
Ключевая идея CSN заключается в упрощении решений, касающихся видимости транзакций. В традиционном PostgreSQL MVCC определение видимости транзакции требует проверки ее статуса фиксации в CLOG и сравнения идентификаторов транзакций со списком выполняющихся транзакций, зафиксированных в снимке (snapshot). С CSN это сводится к простому числовому сравнению: транзакция видна, если ее CSN меньше или равен CSN снимка.
Ниже мы расскажем о том, как CSN устраняет эту проблему, какие еще преимущества дает его использование и как он реализован в СУБД Tantor Polar, используемой в новом поколении нашей МБД, а также в ожидаемом 18 релизе СУБД Tantor Postgres.
CSN и проблема с традиционным MVCC на основе XID
Традиционная архитектура PostgreSQL MVCC имеет ряд проблем с масштабируемостью, которые усугубляются при высокой параллельности. Одна из таких – получение снимков. Каждый раз, когда транзакции требуется снимок, она должна получить ProcArrayLock и пройтись по всем активным бэкендам, чтобы собрать их идентификаторы транзакций. Эта операция становится все более затратной по мере роста числа одновременных соединений: при тысячах соединений конкуренция за блокировку может серьезно ограничить пропускную способность. CSN устраняет это узкое место, заменяя сканирование ProcArray атомарным чтением переменных, что делает получение снимков по сути O(1) независимо от количества соединений.
Во время тестирования нашей МБД Tantor XData Gen2 у одного из клиентов мы столкнулись с деградацией производительности СУБД, связанной с структурой ProcArray и ожиданиях на блокировках ProcArrayLock. Кстати, в этом сценарии сравнивалась производительность с старой системой на Oracle Exadata.

Проблема в том, что GetSnapshotData и XidInMVCCSnapshot линейно проходят по массивам, поэтому рост числа соединений и подтранзакций с переполнением приводит к существенной деградации СУБД.

Из проведенного нами анализа было видно, что самые высокочастотные запросы, это запросы SAVEPOINT $1 и RELEASE SAVEPOINT $1 суммарно 16% из всех запросов. Длительное время работы функций GetSnapshotData и XidInMVCCSnapshot непосредственно связано с большим количеством подтранзакций, которые порождаются точками сохранения (SAVEPOINT).
Анализ результатов профилирования показал, что для MVCC снимков происходило переполнение массива подтранзакций (SnapshotData→subxip), размер массива ограничен 64 элементами, и такой снимок отмечается, как suboverflowed. При работе с таким снимком для проверки видимости кортежей в текущей транзакции требуется дополнительное обращение к SLRU хранящему данные о подтранзакциях (SlruCtlData) в целях поиска родительской транзакции.
Алгоритмы функций GetSnapshotData и XidInMVCCSnapshot линейно проходят по массивам, поэтому рост числа соединений и подтранзакций с переполнением приводит к существенному замедлению работы.
Прикладное ПО, которое создавало нагрузку, работает через JDBC-драйвер, а в параметрах подключения использовалось автоматическое создание SAVEPOINT на каждый выполняемый запрос (autosave=always, jdbc.postgresql.org).
Похожие проблемы производительности описаны по ссылкам:
-
Performance question about using autosave=always and cleanupSavepoints=true
-
suboverflowed subtransactions concurrency performance optimize
Далее мы разберем эти узкие места более подробно.
Получение моментального снимка
Блокировка GetSnapshotData(). Каждое выражение, требующее согласованного представления базы данных, должно получить снимок, а это требует удержания ProcArrayLock в режиме совместного доступа (shared mode) во время итерации по всем бэкенд-процессам для сбора их активных идентификаторов транзакций. Стоимость этой операции линейно возрастает с количеством соединений. Даже несмотря на то, что блокировка является shared (разрешает одновременное чтение), она все равно сериализуется при эксклюзивном получении, и фиксация транзакции требует эксклюзивную блокировку ProcArrayLock через ProcArrayEndTransaction(). При высокой нагрузке с тысячами мелких транзакций это создает эффект конвоя, когда бэкэнды выстраиваются в очередь в ожидании получения снимка.
Переполнение подтранзакций
Каждый бэкенд может кэшировать до 64 идентификаторов подтранзакций ( PGPROC_MAX_CACHED_SUBXIDS) в своем PGPROC. Когда транзакция превышает этот лимит, кэш подтранзакций переполняется. В этом случае снимок помечается как suboverflowed = true. Переполнение PGPROC_MAX_CACHED_SUBXIDS часто происходит с рекурсивными функциями, блоками исключений PL/pgSQL или сгенерированным ORM кодом, активно использующим SAVEPOINT-ы.
Когда при проверке видимости обнаруживается переполненный снимок, она не может просто выполнить поиск в массиве subxid снимка. Вместо этого XidInMVCCSnapshot() необходимо вызвать функцию SubTransGetTopmostTransaction()для обхода цепочки родительских подтранзакций с помощью поиска pg_subtrans. Каждый вызов функции SubTransGetParent()считывает данные из буферного пула pg_subtrans SLRU.
В pg_subtrans SLRU используется буферный пул фиксированного размера. Когда нескольким бэкендам одновременно требуются разные страницы, они конкурируют за буферные слоты. Если необходимая страница отсутствует в буфере, бэкенд должен ожидать ввода-вывода (SimpleLruWaitIO); если все буферы заняты, один из них должен быть освобожден первым (SlruSelectLRUPage), что потенциально может потребовать записи на диск прежде чем чтение сможет продолжиться. При большом количестве параллельных транзакций, затрагивающих разные части pg_subtrans, это создает сериализацию ввода-вывода и конкуренцию за буферы, и это значительно снижает производительность.
Подтверждение транзакции
Для подтверждения транзакции требуется эксклюзивная блокировка ProcArrayLock, чтобы удалить транзакцию из списка активных. Хотя PostgreSQL имеет оптимизации, такие как групповая очистка для пакетной фиксации нескольких транзакций, фундаментальная сериализация остается неизменной. На борьбу за эту блокировку высокопроизводительные OLTP-нагрузки с большим количеством коротких транзакций тратится достаточно много времени.
Аномалия Long Fork в кластерах с репликацией
В кластерах с репликами для чтения традиционный MVCC на основе XID приводит к аномалии Long Fork: порядок, в котором транзакции становятся видимыми, может различаться между основным сервером и репликами. Это нарушает изоляцию снимков. Два пользователя могут наблюдать эффекты параллельных транзакций в разном порядке — например, запрос к основному серверу может видеть изменения T1, но не T2, в то время как запрос к реплике может видеть изменения T2, но не T1.
Дело здесь в том, что на основном сервере порядок, в котором неконфликтующие транзакции становятся видимыми, т.е. visible (в зависимости от того, когда они покидают ProcArray, используемый для создания снимков), может отличаться от порядка, в котором они становятся устойчивыми, т.е. durable (порядок их фиксации в журнале WAL). Транзакция становится устойчивой, когда она записывает свою фиксацию в WAL, и только позже удаляет себя из ProcArray. Таким образом, одна транзакция может быть зафиксирована раньше в WAL, но покинуть ProcArray позже, чем другая. Однако реплики применяют WAL в порядке фиксации, поэтому они фактически видят информацию, выровненную с порядком фиксации. Несоответствие между основным сервером (видимость, определяемая ProcArray) и репликами (видимость, определяемая WAL) является причиной возникновения аномалии Long Fork.
О таком поведении в сообществе PostgreSQL знают как минимум с 2013 года. Оно затрагивает все уровни изоляции, при этом не приводит к потере или повреждению данных, но может повлиять на приложения, которые предполагают согласованный порядок видимости между основным сервером и репликами (например, разделение операций чтения/записи, анализ восстановления на определенный момент времени или распределенные снимки). CSN решает эту проблему, обеспечивая соответствие порядка видимости порядку фиксации. Подробнее об этом можно прочесть в блоге AWS Database Understanding transaction visibility in PostgreSQL clusters with read replicas.
Как CSN решает эту проблему
CSN устраняет эти узкие места и аномалию Long Fork, и дает значительную оптимизацию для получения моментальных снимков (snapshots). Порядок отображения оказывается выровнен относительно порядка фиксации. Присваивая во время фиксации CSN (с помощью атомарного “fetch-and-add” в polar_next_csn) и используя его для обеспечения видимости, гарантируется, что порядок, в котором транзакции становятся видимыми, совпадает с порядком их фиксации. Отдельной видимости, управляемой массивом ProcArray, которая могла бы отличаться от порядка WAL, не существует. Таким образом, реплики, применяющие WAL в определенном порядке, “видят” тот же порядок видимости, что и основная реплика, и это исключает Long Fork аномалию. Это важно для реплик, маршрутизации запросов или восстановления на определенный момент времени.
Получение моментальных снимков без блокировки
GetSnapshotDataCSN()заменяет сканирование ProcArray двумя атомарными операциями чтения переменных: polar_oldest_active_xid для xmin и polar_next_csn для CSN снимка. Блокировки не требуются, и стоимость остается постоянной независимо от количества соединений. Основное преимущество – в производительности: получение снимка сокращается с O(n) и конкуренцией за блокировки до O(1) без блокировок.
Проверка видимости
Кортежи по-прежнему хранят xmin/xmax в качестве идентификаторов транзакций, поэтому проверка видимости должна искать CSN для этих XID в CSNLOG SLRU. Это сравнимо с традиционным поиском в CLOG, поскольку включает доступ через SLRU. Фактически, страницы CSNLOG менее плотные, чем pg_subtrans — CSN занимает 8 байт против 4 байт для TransactionId, поэтому CSNLOG вмещает 1024 записи на 8K-странице по сравнению с 2048 в pg_subtrans.
Однако CSNLOG является самодостаточным. Для подтранзакций CSNLOG напрямую хранит XID родительской транзакции (помеченный символом POLAR_CSN_SUBTRANS_BIT), что позволяет выполнять поиск в родительской цепочке в рамках одного и того же SLRU. Традиционный MVCC требует переключения между CLOG (для статуса фиксации) и pg_subtrans (для родительской цепочки) — двумя отдельными SLRU с независимыми пулами буферов и блокировками. CSN объединяет это в единую структуру.
Также имеется кэш для одного элемента (polar_cached_csn_xid) и опциональный верхний кэш для уменьшения количества повторных запросов CSNLOG для одной и той же транзакции.
Обработка подтранзакций
Прозрачность подтранзакций предполагает некоторые компромиссы между традиционным MVCC и CSN. Для незавершенных подтранзакций традиционный MVCC без их переполнения на самом деле проще — он просто ищет в массиве subxip снимка. CSN должен подниматься по цепочке родительских транзакций в CSNLOG, потому что незавершенные подтранзакции хранят указатель на родительский объект, а не CSN.
В традиционном MVCC при переполнении подтранзакций каждая проверка видимости подтранзакции требует обхода pg_subtrans SubTransGetTopmostTransaction(). При одновременном возникновении переполнения у многих бэкендов проверка SLRU для pg_subtrans становится серьезной проблемой.
CSN полностью избегает этого – концепция переполнения как таковая отсутствует, поскольку снимки CSN не отслеживают отдельные идентификаторы подтранзакций. Обход родительского узла происходит в CSNLOG, т.е. том же самом SLRU, который используется для всего остального.
Для подтвержденных подтранзакций CSN однозначно лучше. Традиционный MVCC помечает подтранзакции так же, как и SUB_COMMITTED в CLOG, и должен пройти по цепочке pg_subtrans, чтобы проверить, подтверждена ли родительская транзакция. Финальный же CSN записывается во все записи подтранзакций во время подтверждения, поэтому поиск возвращает результат немедленно, без какого-либо обхода цепочки родительских транзакций.
Когда использовать CSN
CSN выгоден для нагрузок с высокой степенью параллелизма и множеством кратковременных транзакций. Получение моментальных снимков без блокировок хорошо масштабируется при тысячах одновременных подключений, а устранение конфликтов переполнения подтранзакций помогает нагрузкам, использующим точки сохранения (SAVEPOINT-ы) или обработку исключений PL/pgSQL. Однако использование CSN сопряжено со значительными компромиссами, связанными с длительными транзакциями.
Для нагрузок со всего лишь десятками соединений существенной пользы от получения моментальных снимков без блокировок может не быть, поскольку конкуренция за блокировку ProcArrayLock в таком масштабе сама по себе вполне управляема.
Конфигурация CSN
|
GUC |
Type |
Default |
Context |
Описание |
|
polar_csn_enable |
boolean |
true |
postmaster |
Включает CSN. Для получения снимков используются атомарные операции чтения переменных вместо ProcArrayLock, а для проверки видимости используется CSNLOG вместо CLOG + pg_subtrans. |
|
polar_csn_xid_snapshot |
boolean |
false |
superuser |
Управляет форматом снимков в режиме CSN. Если значение равно false, снимки содержат только значение CSN и пустые массивы XID. Если же значение равно true, снимки также заполняют массивы XID, как в традиционном MVCC, обеспечивая совместимость с операциями, которые ожидают снимки на основе XID. Массивы XID создаются на основе данных CSN, а не путем сканирования ProcArray. |
|
polar_csn_elog_panic_enable |
boolean |
true |
postmaster |
Управляет обработкой ошибок, возникающих при несоответствии состояний CSN. При включении этой функции система выдаст ошибку PANIC, если состояние CSN дочерней транзакции не соответствует состоянию родительской (например, дочерняя транзакция отображается как «завершена», а родительская — как «в процессе»). При отключении вместо этого будет регистрироваться предупреждение. |
|
polar_csnlog_upperbound_enable |
boolean |
false |
postmaster |
Включает оптимизацию верхнего предела кэша. При включении этой функции поддерживается кэш в общей памяти, хранящий максимальное значение CSN для каждой группы из ~1024 XID. Проверки видимости кортежей с битами подсказок подтверждения могут пропускать поиск CSNLOG SLRU, если верхняя граница ниже значения CSN для снимка. Это полезно для рабочих нагрузок с интенсивным чтением, при которых одни и те же кортежи обращаются к ним многократно. |
|
polar_csnlog_slot_size |
integer |
8192 |
postmaster |
Задает размер буферного пула CSNLOG SLRU. Количество страниц CSNLOG для буферизации в разделяемой памяти. Большие значения уменьшают дисковый ввод-вывод при поиске в CSNLOG, но потребляют больше разделяемой памяти. Каждая страница имеет размер BLCKSZ (обычно 8 КБ). |
|
polar_csnlog_max_local_cache_segments |
integer |
256 |
postmaster |
Устанавливает размер локального кэша для сегментов CSNLOG в режиме общего хранилища. Количество сегментов CSNLOG для локального кэширования при использовании архитектуры Shared Storage в Tantor Polar. Каждый сегмент имеет размер |
Совместимость и ограничения
По умолчанию в снимках CSN используется polar_snapshot_csn – значение для отображения видимости, то-есть массивы xip[]и subxip[]пусты. Это наиболее эффективный режим. При polar_csn_xid_snapshot = true снимки также заполняют массивы XID, как и в традиционном MVCC. Проверка видимости использует алгоритм XidInMVCCSnapshotCSN(), который выполняет поиск в этих массивах вместо сравнения CSN. Гибридный режим предусмотрен для совместимости с операциями, которые ожидают семантику снимков на основе XID.
В этом режиме массивы XID по-прежнему создаются без удержания ProcArrayLock (они формируются на основе данных CSN), поэтому и сохраняется большинство преимуществ масштабируемости, и одновременно повышается совместимость.
Логическое декодирование
При включенном CSN работает логическое декодирование. CSN не меняет его основной механизм. Изменения по-прежнему декодируются на основе порядка подтверждения транзакций, и это естественным образом соответствует порядку CSN.
Известные ограничения
Длительные транзакции ухудшают производительность:
-
При наличии длительной транзакции происходит задержка
snapshot->xmin. Это расширяет диапазон [xmin, xmax), и в итоге больше проверок видимости попадают в категорию «обязательно проверить CSNLOG» вместо того чтобы разрешаться быстрыми сравнениями xmin/xmax. В рабочих нагрузках, сочетающих длительные аналитические запросы с OLTP-транзакциями, преимущества CSN могут нивелироваться.
CSNLOG менее плотный, чем pg_subtrans:
-
CSNLOG хранит 8-байтовые записи (1024 на страницу), в то время как pg_subtrans хранит 4-байтовые записи (2048 на страницу). Это означает, что CSNLOG использует больше места в хранилище и буферном пуле для того же диапазона XID. Однако CSNLOG объединяет то, что в противном случае было бы отдельными операциями поиска в CLOG и pg_subtrans.
Отсутствует прямой доступ к внутренним механизмам CSN через SQL:
-
Атомарные переменные (
polar_oldest_active_xid, polar_next_csn, polar_latest_completed_xid) через функции SQL недоступны. Мониторинг ограничивается статистикой верхнего предела кэша черезpolar_csnlog().
Тестирование CSN
Для наглядной демонстрации деградации производительности при большом количестве SAVEPOINT мы написали небольшой скрипт, который запускается через pgbench. Каждая транзакция запускает 10 операций UPDATE между которыми от 0 до 256 точек сохранения. Структура транзакции выглядит следующим образом:
BEGIN;SAVEPOINT sp1;SAVEPOINT sp2;...UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;SAVEPOINT spN;UPDATE ...SAVEPOINT spN+1;UPDATE ...END;
Тест запускается в 256 потоков, на каждом шаге увеличивается количество точек сохранения. Каждый шаг длится 60 секунд. Количество точек сохранения на каждом шаге: 0, 16, 32, 64, 96, 128, и 256.
На графике ниже представлены результаты теста с включенным и выключенным CSN. Как видно, с включенным CSN система практически не деградирует.
Заключение
В статье рассмотрен механизм CSN и то, какие ограничения он помогает преодолеть в традиционном PostgreSQL. Механизм реализован в новой СУБД Tantor Polar, и еще он будет доступен в СУБД Tantor Special Edition 18, которая выйдет в ближайшее время. За скобками осталось описание архитектуры – ее мы опубликуем в документации.
Важно, что CSN полностью сохраняет возможность включения/выключения “на лету”, при которой после рестарта сервера вы можете перейти на использование CSN или традиционного MVCC на основе XID. Имеется и гибридный режим, при котором обеспечивается совместимость с операциями, ожидающими снимки на основе XID (при влюченном polar_csn_xid_snapshot = true), но с использованием CSN. Таким образом удается увеличивать производительность, сохраняя полную обратную совместимость.
Другие статьи из цикла публикаций о технологиях, реализованных в МБД Tantor XData Gen3:
ссылка на оригинал статьи https://habr.com/ru/articles/1023250/