Перейти к основному содержимому

Механизмы сериализации, используемые при формировании и сбросе WAL

В статье рассматриваются причины ожидания на событиях LWLock:WALInsert и LWLock:WALWrite (LWLock — Lightweight Lock, облегченная блокировка), описаны причины их возникновения и приведены рекомендации по снижению конкуренции. Внутренние механизмы записи WAL (Write-Ahead Log, журнал предварительной записи) рассматриваются исключительно в рамках основного узла (primary). Вопросы Point-In-Time Recovery (PITR) и репликации в статье не рассматриваются. Материал позволяет понять, на каком этапе записи WAL могут возникнуть проблемы, и выбрать способ их устранения.

При эксплуатации СУБД Pangolin/PostgreSQL подсистема WAL используется для следующих целей:

  • Обеспечение свойства Durability (D) из ACID (Atomicity, Consistency, Isolation, Durability). При аварийной остановке экземпляра (instance) из-за сбоя электропитания или аппаратной составляющей решения (кроме сбоя СХД — системы хранения данных) экземпляр содержит в WAL-сегментах достаточное количество информации для восстановления. После применения WAL с момента последней контрольной точки (checkpoint) экземпляр приводится в целостное состояние на момент остановки, в рамках которого сохранены изменения от всех зафиксированных транзакций.

  • Point-In-Time Recovery (PITR). При восстановлении экземпляра в случае пользовательской ошибки позволяет восстановить состояние системы на заданный момент времени путем применения части WAL к резервной копии.

  • Репликация. Данные WAL передаются на standby-машину, где запущен экземпляр, предварительно созданный из резервной копии. WAL применяется к кластеру БД этого экземпляра, поддерживая данные максимально близкими к данным на основном узле.

Большая часть низкоуровневой логики работы с WAL сосредоточена в файле src/backend/access/transam/xlog.c.

Сценарии работы с WAL​

На основном узле работа с WAL происходит в рамках трех сценариев. Помимо них, существуют сценарии, связанные с архивированием WAL-файлов, репликацией на резервном сервере (standby-узел) и PITR-восстановлением.

Внесение изменений в данные БД (DDL — Data Definition Language, DML — Data Manipulation Language), требующих предварительной записи. Для операций SELECT предварительная запись требуется из-за простановки WAL hints. Данные записываются в буфер в общей памяти экземпляра XLogCtl->pages (WAL-буфер), размер которого определяется параметром wal_buffers. Логика реализована в функции:

XLogRecPtr XLogInsertRecord(
XLogRecData *rdata,
XLogRecPtr fpw_lsn,
uint8 flags,
int num_fpi,
bool topxid_included
)

Фиксация транзакции. При выполнении команды COMMIT или ROLLBACK в режиме синхронной фиксации synchronous_commit=on экземпляр возвращает управление пользователю только после того, как данные о статусе транзакции из CLOG (commit log) и все записи из WAL-буфера, относящиеся к транзакции, окажутся на СХД. Логика реализована с помощью функции:

void XLogFlush(XLogRecPtr record)

Фоновая запись. Периодический, не привязанный к конкретной пользовательской активности сброс информации из WAL-буфера на СХД процессом WALWriter выполняется функцией:

bool XLogBackgroundFlush(void)

LWLock:WALInsert​

Событие LWLock:WALInsert возникает при вставке записей в WAL-буфер.

Для отслеживания статуса записи в WAL-буфер PostgreSQL использует массив структур WALInsertLock:

typedef struct
{
LWLock lock;
pg_atomic_uint64 insertingAt;
XLogRecPtr lastImportantAt;
} WALInsertLock;

Путь доступа к массиву: XLogCtlData XLogCtl -> XLogCtlInsert Insert -> WALInsertLockPadded *WALInsertLocks.

Количество структур задается константой на этапе компиляции NUM_XLOGINSERT_LOCKS (значение по умолчанию — 8):

NUM_XLOGINSERT_LOCKS=8

Каждая структура не блокирует ресурсы напрямую — она отслеживает статус конкретной операции записи в WAL-буфер через поле insertingAt. Для записи данных в WAL-буфер необходимо захватить одну из свободных структур.

Причины ожидания​

В функции XLogInsertRecord() присутствуют два сценария работы со структурами WALInsertLock.

Стандартные операции (class=WALINSERT_NORMAL). Функция WALInsertLockAcquire() выбирает структуру, начиная с индекса:

lockToTry = MyProcNumber % NUM_XLOGINSERT_LOCKS

При неудачной попытке захвата без ожидания в следующей итерации будет инициирована попытка захватить структуру с номером lockToTry+1. Таким образом, нагрузка будет равномерно распределяться по всем структурам. При активной OLTP (Online Transaction Processing) нагрузке, когда сотни сеансов одновременно изменяют данные, количества структур WALInsertLock не хватает, что приводит к ожиданиям на LWLock:WALInsert.

Операции контрольной точки и переключения WAL-сегмента. Функция WALInsertLockAcquireExclusive() захватывает LWLock у всех элементов массива WALInsertLock. Этот сценарий блокирует все стандартные операции записи в WAL-буфер на время выполнения контрольной точки или переключения журналов, что негативно сказывается на производительности.

Рекомендации по снижению конкуренции​

Выполнять переключение WAL-сегмента и контрольную точку как можно реже:

  1. Инициализировать кластер БД с максимальным размером WAL-сегмента:

    initdb --wal-segsize=1024...
  2. Адаптировать значения параметров checkpoint_timeout и max_wal_size под профиль нагрузки.

LWLock:WALWrite​

Событие LWLock:WALWrite возникает при сбросе данных из WAL-буфера в активный WAL-сегмент на СХД.

В отличие от Oracle, где запись из оперативной памяти redo log buffer в online redo log выполняли только LGWR и его slave-процессы, в PostgreSQL сброс данных из WAL-буфера в активный WAL-сегмент могут выполнять все бекенды — процессы, обслуживающие клиентские подключения.

При выполнении операций COMMIT или ROLLBACK вызывается XLogFlush(XLogRecPtr record). В рамках этой функции выполняется следующая логика:

  1. Проверяется, не сброшен ли WAL до указанного адреса другим процессом.
  2. Цикл, в котором при каждой итерации:
    • атомарное обновление текущих позиций записи (Write) и сброса (Flush) через макрос RefreshXLogWriteResult();
    • выход из цикла, если WAL сброшен до нужного адреса;
    • проверка завершения операций записи в буфер до указанного адреса через функцию WaitXLogInsertionsToFinish(), при необходимости — ожидание; на данном этапе также может возникнуть ожидание, так как в рамках функции для всех слотов структуры WALInsertLocks выполняется LWLockWaitForVar();
    • попытка захвата WALWriteLock; в отличие от массива структур WALInsertLocks, WALWriteLock существует в единственном экземпляре.
  3. Для записи в WAL-буфер выполняется функция XLogInsertRecord(), в рамках которой возможны два сценария:
    • Захватывается одна любая структура WALInsertLock с помощью функции WALInsertLockAcquire();
    • Функция WALInsertLockAcquireExclusive() захватывает все структуры WALInsertLock, блокируя стандартные операции записи в WAL-буфер до завершения контрольной точки или переключения журнала.

Логика записи в WAL-буфер и выполнения операций COMMIT или ROLLBACK

При успешном захвате WALWriteLock без ожидания выполняются дополнительные проверки (отсутствие записи WAL до указанного адреса, наличие активных процессов через MinimumActiveBackends() для commit_delay > 0), далее выполняется запись данных на СХД посредством XLogWrite(). Если не получилось захватить без ожидания — возврат на начало цикла, поскольку запись могла быть выполнена другим процессом, удерживавшим WALWriteLock.

WALWriteLock покрывает:

  1. Потенциальное ожидание окончания записи в WAL-буфер в конкурентных процессах при commit_delay > 0 (через WaitXLogInsertionsToFinish()).
  2. Непосредственно операцию записи XLogWrite().

Рекомендации по снижению конкуренции​

Для снижения конкуренции на LWLock:WALWrite рекомендуется уменьшить время удержания единственной блокировки WALWriteLock:

  1. Увеличить размер общего WAL-буфера (wal_buffers = 64MB+) для снижения частоты наполнения буфера, когда процессы находятся в ожидании освобождения места под блокировкой.
  2. Включить сжатие WAL (wal_compression = lz4/zstd), уменьшив объем данных, копируемых за один захват WALWriteLock, и сократить время удержания блокировки.

Заключение​

Таким образом, ожидания на событии LWLock:WALInsert связаны с записью в WAL-буфер и вызваны недостатком структур WALInsertLock, частым переключением WAL-сегментов или контрольной точкой. Ожидания на LWLock:WALWrite связаны с конкурентным доступом к единственному WALWriteLock и часто указывают на проблемы с производительностью СХД.

Дополнительная информация​

  1. Работа с WAL-файлами
  2. Настройка WAL
  3. Внутреннее устройство WAL
  4. Журнал предзаписи (WAL)
  5. Журнал записи с опережением (WAL)