Механизмы сериализации, используемые при формировании и сбросе 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-сегмента и контрольную точку как можно реже:
-
Инициализировать кластер БД с максимальным размером WAL-сегмента:
initdb --wal-segsize=1024... -
Адаптировать значения параметров
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). В рамках этой функции выполняется следующая логика:
- Проверяется, не сброшен ли WAL до указанного адреса другим процессом.
- Цикл, в котором при каждой итерации:
- атомарное обновление текущих позиций записи (Write) и сброса (Flush) через макрос
RefreshXLogWriteResult(); - выход из цикла, если WAL сброшен до нужного адреса;
- проверка завершения операций записи в буфер до указанного адреса через функцию
WaitXLogInsertionsToFinish(), при необходимости — ожидание; на данном этапе также может возникнуть ожидание, так как в рамках функции для всех слотов структурыWALInsertLocksвыполняетсяLWLockWaitForVar(); - попытка захвата
WALWriteLock; в отличие от массива структурWALInsertLocks,WALWriteLockсуществует в единственном экземпляре.
- атомарное обновление текущих позиций записи (Write) и сброса (Flush) через макрос
- Для записи в WAL-буфер выполняется функция
XLogInsertRecord(), в рамках которой возможны два сценария:- Захватывается одна любая структура
WALInsertLockс помощью функцииWALInsertLockAcquire(); - Функция
WALInsertLockAcquireExclusive()захватывает все структурыWALInsertLock, блокируя стандартные операции записи в WAL-буфер до завершения контрольной точки или переключения журнала.
- Захватывается одна любая структура
При успешном захвате WALWriteLock без ожидания выполняются дополнительные проверки (отсутствие записи WAL до указанного адреса, наличие активных процессов через MinimumActiveBackends() для commit_delay > 0), далее выполняется запись данных на СХД посредством XLogWrite(). Если не получилось захватить без ожидания — возврат на начало цикла, поскольку запись могла быть выполнена другим процессом, удерживавшим WALWriteLock.
WALWriteLock покрывает:
- Потенциальное ожидание окончания записи в WAL-буфер в конкурентных процессах при
commit_delay > 0(черезWaitXLogInsertionsToFinish()). - Непосредственно операцию записи
XLogWrite().
Рекомендации по снижению конкуренции
Для снижения конкуренции на LWLock:WALWrite рекомендуется уменьшить время удержания единственной блокировки WALWriteLock:
- Увеличить размер общего WAL-буфера (wal_buffers = 64MB+) для снижения частоты наполнения буфера, когда процессы находятся в ожидании освобождения места под блокировкой.
- Включить сжатие WAL (
wal_compression = lz4/zstd), уменьшив объем данных, копируемых за один захватWALWriteLock, и сократить время удержания блокировки.
Заключение
Таким образом, ожидания на событии LWLock:WALInsert связаны с записью в WAL-буфер и вызваны недостатком структур WALInsertLock, частым переключением WAL-сегментов или контрольной точкой. Ожидания на LWLock:WALWrite связаны с конкурентным доступом к единственному WALWriteLock и часто указывают на проблемы с производительностью СХД.