Categories: PostgreSQL

postgresql-wal/

PostgreSQL хранит данные в страницах , а страницы кэшируются в разделяемых буферах . Казалось бы, в случае аварийной остановки грязные страницы не будут записаны на диск, и часть данных пропадет. Чтобы такого не происходило, СУБД пишет журнал предзаписи, он же Write Ahead Log, или WAL.

Теория

Идея заключается в следующем. Перед тем, как изменить что-либо на странице, СУБД делает соответствующую запись в WAL (он же XLOG, от Transaction Log). WAL хранится на диске. Запись содержит что-то уровня «открыть такую-то страницу и записать N таких-то байт по такому-то смещению». В случае аварийной остановки при следующем запуске СУБД откроет WAL и проиграет все записи из него. То есть, сделает все измения, которые могли потеряться.

Смещение записи относительно начала журнала называется Log Sequence Number, или LSN. Записи в среднем довольно короткие, десятки байт, и писать их по одной невыгодно. Поэтому в разделяемой памяти есть кольцевой буфер для WAL-записей . Размер буфера определяется параметром wal_buffers и по умолчанию составляет 1/32 от размера разделяемых буферов, но не менее 64 Кб и не более 16 Мб.

Как правило, страница не может быть вытеснена раньше WAL-записи о последних изменениях на этой странице. Это необходимо для корректного восстановления системы после сбоя. Ведь WAL-записи не могут применяться к страницам из будущего. По этой причине в PageHeaderData в поле pd_lsn хранится LSN . Исключением из правила являются нежурналируемые таблицы .

Во время восстановления WAL-запись должна быть пропущена, если на диске записана страница с большим LSN. Эта страница была вытеснена на диск после того, как была сделана текущая запись в WAL. Страница содержит более свежие данные. Далее в WAL для нее могут быть другие записи, с большим LSN.

Примечание: В PostgreSQL пользователю доступен тип pg_lsn , а также функции pg_current_wal_lsn() , pg_walfile_name() , pg_ls_waldir() и прочие. Подробности ищите в документации.

Запись в WAL происходит с fsync(). Кроме того, записи имеют контрольные суммы. При таком подходе данные не могут быть потеряны. Конечно, если только диск и/или сервер целиком не выйдут из строя. Но это при любом раскладе будет означать восстановление из резервной копии. Заметьте, что работа с WAL — это последовательная запись на диск, что является более дешевой операцией по сравнению со случайной записью в кучу.

По умолчанию в PostgreSQL транзакция считается завершенной успешно, когда все соответствующие ей WAL-записи были записаны на диск с fsync(). Установив synchronous_commit = off можно выжать больше производительности в обмен на потенциальную потерю нескольких последних транзакций в случае сбоя. Подробности описаны в документации .

Если СУБД будет работать долго, она напишет большой WAL, что приведет к медленному восстановлению в случае сбоя. Эта проблема решается при помощи контрольных точек, или checkpoints. Успешный checkpoint означает, что состояние всех страниц на такой-то момент времени (точнее, на такой-то LSN) было записано на диск. В случае восстановления после сбоя СУБД достаточно проиграть записи WAL только с последнего checkpoint’а.

Контрольную точку можно создать вручную, выполнив команду CHECKPOINT . Также чекпоинты периодически создает фоновый процесс под названием checkpointer . Его настройки подробно описаны в официальной документации .

Когда страница изменяется первый раз после контрольной точки, в WAL пишется полная копия страницы . Это необходимо по той причине, что запись грязной страницы в кучу может быть прервана. Страница окажется испорчена и мы не сможем докатить на нее изменения из WAL. Наличие копии страницы в WAL устраняет проблему. Это поведение можно изменить, указав в postgresql.conf параметр full_page_writes = off . Разумеется, настроенная таким образом система может терять данные.

В отличие от некоторых других СУБД, пишущих в WAL как undo, так и redo записи, PostgreSQL пишет только redo. То есть, какие действия нужно совершить, чтобы докатить изменения, но не откатить. Данное решение имеет свои сильные и слабые стороны.

К сильным сторонам относится уменьшение размера WAL, а также существенное упрощение алгоритма восстановления после сбоев (факт проигрывания undo записей при восстановлении нужно логировать, в отличие от redo). Как результат, упрощается тестирование этого алгоритма, в том числе при изменениях между версиями PostgreSQL, в случаях когда СУБД падает во время процесса восстановления, и так далее. Исполнение долгих транзакций не может быть прервано по причине удаления старых undo записей, как это бывает в других системах.

Недостатком является тот факт, что в куче находятся и старые, и новые кортежи. Для удаления старых кортежей нужно периодически делать VACUUM. В СУБД с undo записями старые кортежи вытесняются из кучи в undo записи. VACUUM в таких СУБД не нужен.

Также, в отличие от других СУБД, PostgreSQL берет информацию об успешном checkpoint не из в WAL, а из отдельного файла pg_control. Файл содержит менее 512-и байт данных и имеет контрольную сумму. Это является слабым местом , поскольку запись в такой файл не обязана быть атомарной. Тем не менее, на практике все работает более-менее нормально. Отчасти, потому что грамотные DBA знают про такую особенность и кладут pg_control на журналируемую ФС, а простые DBA держат вообще все на ext4 с включенным журналированием.

Вместо использования отдельного файла более правильно было бы проигрывать WAL от конца к началу в поисках последнего checkpoint. На данный момент это не реализовано в PostgreSQL.

Если СУБД падает во время записи в WAL, то последняя запись может быть записана частично, и контрольная сумма для нее не сойдется. По этой причине некоторые СУБД игнорируют последнюю запись, если она повреждена. Однако повреждение WAL также может быть признаком деградации жесткого диска, или bit rot . Такие СУБД имеют больше шансов потерять данные незаметно для пользователя. Если PostgreSQL видит битую запись в WAL, он отказывается стартовать, и требует ручного вмешательства DBA. Заинтересованным читателям предлагается проверить это самостоятельно. Radare2 отлично подойдет для того, чтобы подправить пару байт в WAL.

Практика

Создадим новую базу данных с одной-единственной таблицей:

CREATE TABLE phonebook (
«name» VARCHAR ( 64 ) NOT NULL ,
«phone» INT NOT NULL ) ;

Таблица очень простая, без каких-либо индексов и без TOAST . В каталоге pg_wal при этом должен быть один файл размером 16 Мб:

$ ls -lah /home/eax/projects/pginstall/data-master/pg_wal
total 17M
drwx——  3 eax eax 4.0K Jan 08 14:08 .
drwx—— 19 eax eax 4.0K Jan 08 14:08 ..
-rw——-  1 eax eax  16M Jan 08 14:10 000000010000000000000001
drwx——  2 eax eax 4.0K Jan 08 14:08 archive_status

Также как и с кучей, это файл называется сегментом и разбит на страницы размером по 8 Кб. Размер сегмента может быть переопределен при помощи флага компиляции --wal-segsize , а размер страницы в WAL — при помощи --with-wal-blocksize . Номера сегментов строго возрастают. Никакого переиспользования (wraparound) номеров не предусмотрено, поскольку номеров много. Пройдет очень много времени прежде, чем они закончатся.

Для чтения сегментов предусмотрена утилита pg_waldump:

$ pg_waldump -p ~/projects/pginstall/data-master/pg_wal
000000010000000000000001 | wc -l

pg_waldump: error: error in WAL record at 0/1B144C0: invalid record
length at 0/1B144F8: wanted 24, got 0

22002

На сообщение об ошибке не обращайте внимания. Так утилита говорит о том, что нашла конец журнала.

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

Допишем немного данных в таблицу:

INSERT INTO phonebook ( «name» , «phone» )
VALUES ( ‘Alice’ , 123 ) , ( ‘Bob’ , 456 ) , ( ‘Charlie’ , 789 ) ;

Повторив предыдущую команду с pg_waldump видим, что в журнале теперь 22007 записей. Воспользовавшись tail -n 5 , мы можем узнать, что конкретно записал наш INSERT:

rmgr: Heap        len (rec/tot): 67/67, tx:        739,   ⏎
lsn: 0/01B144F8, prev 0/01B144C0, desc: INSERT+INI    ⏎
off 1 flags 0x08, blkref #0: rel 1663/16384/16389 blk 0
rmgr: Heap        len (rec/tot): 63/63, tx:        739,   ⏎
lsn: 0/01B14540, prev 0/01B144F8, desc: INSERT        ⏎
off 2 flags 0x08, blkref #0: rel 1663/16384/16389 blk 0
rmgr: Heap        len (rec/tot): 67/67, tx:        739,   ⏎
lsn: 0/01B14580, prev 0/01B14540, desc: INSERT        ⏎
off 3 flags 0x08, blkref #0: rel 1663/16384/16389 blk 0
rmgr: Transaction len (rec/tot): 46/46, tx:        739,   ⏎
lsn: 0/01B145C8, prev 0/01B14580, desc: COMMIT        ⏎
2023-01-08 14:14:27.585865 MSK
rmgr: Standby     len (rec/tot): 50/50, tx:          0,   ⏎
lsn: 0/01B145F8, prev 0/01B145C8, desc: RUNNING_XACTS ⏎
nextXid 740 latestCompletedXid 739 oldestRunningXid 740

Смысл полей rmgr, len и прочих станет понятен ниже, когда мы рассмотрим структуру XLogRecord.

Что же до файла pg_control, он имеет путь $PGDATA/global/pg_control. Для его чтения есть утилита pg_controldata. Убедиться, что файл содержит информацию о последнем checkpoint’е можно так:

pg_controldata ~ / projects / pginstall / data-master / > before.txt
psql -c ‘CHECKPOINT;’
pg_controldata ~ / projects / pginstall / data-master / > after.txt
diff before.txt after.txt

Или даже так:

pg_controldata ~ / projects / pginstall / data-master / |
grep ‘Latest checkpoint’

Страницы в WAL начинаются с заголовка XLogPageHeaderData размером 20 байт. Если проставлен флаг XLP_LONG_HEADER, заголовок имеет размер 36 байт и тип XLogLongPageHeaderData. XLogLongPageHeaderData включает в себя XLogPageHeaderData, а также избыточные данные для сверки с pg_control. К избыточным данным относятся 64-х битный идентификатор системы, а также размер сегментов и страниц WAL. Длинная версия заголовка пишется в первую страницу сегмента. Описание этих структур находится в файле xlog_internal.h .

Каждая запись в WAL начинается с заголовка фиксированного размера 24 байта:

typedef struct XLogRecord
{
uint32 xl_tot_len ; /* общая длина записи */
TransactionId xl_xid ; /* ID транзакции, создавшей запись */
XLogRecPtr    xl_prev ; /* указатель на предыдущую запись */
uint8 xl_info ; /* флаги и 3 бита id операции */
RmgrId        xl_rmid ; /* resource manager этой записи */

/* здесь 2 байта 0x00 для выравнивания */

pg_crc32c     xl_crc ; /* контрольная сумма записи */
} XLogRecord ;

Следом идут данные, которые зависят от типа записи. Подробности ищите в файле xlogrecord.h .

За обработку записей конкретного типа отвечают так называемые resource managers. RmgrId кодируется одним байтом. На момент написания этих строк существует 22 разных resource managers, среди которых Heap, Btree, Generic, и другие. Каждый менеджер должен уметь проигрывать свои записи (ищите функции heap_redo, btree_redo, …), декодировать их в текстовое описание (heap_desc, btree_desc, …), и так далее. Подробности смотрите в rmgrlist.h .

Характерно, что для кучи существует два resource managers — Heap и Heap2. Так получилось по той причине, что трех бит (XLOG_HEAP_OPMASK) не хватило для кодирования всех требуемых операций. Поэтому resource manager для кучи распался на два, по 8 операций на каждый. Наверное, можно было бы изменить формат XLogRecord, но это наверняка сломает сторонние инструменты для резервного копирования и всякого такого.

Содержимое pg_control кодируется структурой ControlFileData. Ее описание находится в pg_control.h . В файлах pg_waldump.c и pg_controldata.c можно ознакомиться с исходниками одноименных утилит. Кроме того, есть расширение pg_walinspect . Оно аналогично pageinspect , только для WAL. Его исходники ищите в каталоге contrib/pg_walinspect .

Компонент, отвечающий за работу с WAL и pg_control, называется WAL manager. Его реализация находится в xlog.c , xlog.h и сопутствующих файлах. Наибольший интерес представляют функции XLogInsertRecord() , XLogFlush() , а также функция CreateCheckPoint() . Код инициализации находится в функциях XLOGShmemSize() , XLOGShmemInit() и StartupXLOG() .

Как и buffer manager , WAL manager не является каким-то выделенным процессом. Однако есть выделенный процесс walwriter, занимающийся записью WAL. Он очень похож на ранее рассмотренный bgwriter , только вызывает в цикле XLogBackgroundFlush() из xlog.c. Полная реализация находится в walwriter.c .

Заключение

Данная заметка не претендует на исчерпывающее описание всех тонкостей работы WAL в PostgreSQL. В лучшем случае, это лишь отправная точка.

Деталей действительно много — в одном только xlog.c почти 9000 строк кода. А ведь помимо него еще есть xloginsert.c , xlogrecovery.c и другие. Рассмотреть их все в рамках одного поста не представляется возможным, да и лишено особого смысла. Ведь со временем информация устареет.

В качестве дополнительных материалов можно рекомендовать:

Ну и, конечно же, читать код и комментарии к нему.

Дополнение: В продолжение темы см посты Внутренности PostgreSQL: XID wraparound , Внутренности PostgreSQL: карта видимости , Внутренности PostgreSQL: кэш системного каталога и далее по ссылкам.

admin

Share
Published by
admin

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

2 месяца ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

2 месяца ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

2 месяца ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

2 месяца ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

2 месяца ago

Как управлять журналами событий из командной строки

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

2 месяца ago