11 Режимы журнала
PostgreSQL 18 · Издание 2026 года · Китайский перевод доступен для PostgreSQL 18
11.1. Производительность
При обычной работе сервера происходит постоянная, но последовательная запись журнальных файлов. Поскольку случайный доступ практически отсутствует, с этой задачей вполне могут справляться и HDD-диски. Но такой характер нагрузки существенно отличается от того, как происходит доступ к файлам данных. Поэтому может быть выгодно разместить журнал на отдельном физическом носителе, создав вместо каталога PGDATA/pg_wal символьную ссылку на каталог примонтированной файловой системы. (v. 17)
Есть пара ситуаций, при которых журнальные файлы необходимо не только писать, но и читать. Первая — понятный случай восстановления после сбоя. Вторая возникает при использовании потоковой репликации. Процесс walsender 1 старается читать записи WAL из журнального кеша 2, но если реплика не успевает получать журнальные записи, пока нужные страницы еще находятся в памяти основного сервера, данные будут прочитаны с диска. Однако и в этом случае доступ будет последовательным, а не случайным.
Запись журнала происходит в одном из двух режимов:
синхронном — при фиксации транзакции продолжение работы невозможно, пока все журнальные записи об этой транзакции не окажутся на диске;
асинхронном — транзакция завершается немедленно, а журнал записывается в фоновом режиме.
Режим устанавливается параметром synchronous_commit. (по умолчанию: on)
Синхронный режим. Для надежного сохранения факта фиксации недостаточно просто передать журнальные записи операционной системе — необходимо синхронизировать их с диском. Поскольку синхронизация связана с реальным (то есть медленным) вводом-выводом, выгодно выполнять ее как можно реже.
Для этого обслуживающий процесс, завершающий транзакцию и записывающий журнал, может делать небольшую паузу, определяемую параметром commit_delay. Но происходит это только в том случае, если в системе имеется не менее commit_siblings активных транзакций 3: за время ожидания некоторые из них могут завершиться, и тогда удастся синхронизировать все журнальные записи за один прием. Это похоже на то, как вы придерживаете двери лифта, чтобы кто-то успел заскочить в кабину. (по умолчанию: 0s) (по умолчанию: 5)
Со значениями по умолчанию пауза не выдерживается. Изменять значение параметра commit_delay имеет смысл только в системах, выполняющих большое количество коротких OLTP-транзакций.
После возможной паузы процесс, завершающий транзакцию, сбрасывает на диск все накопившиеся журнальные записи и синхронизирует их с диском (важно, что на диске оказывается запись о фиксации и все предыдущие записи, относящиеся к этой транзакции; все остальное записывается просто потому, что не увеличивает стоимость).
С этого момента транзакция считается надежно завершенной 4 — гарантируется долговечность (Durability из набора требований ACID). Поэтому синхронный режим используется по умолчанию.
Обратная сторона состоит в том, что синхронная запись увеличивает время отклика (команда COMMIT не возвращает управление до окончания синхронизации) и уменьшает производительность системы, особенно OLTP.
Асинхронный режим. Чтобы перейти на асинхронную запись5, нужно установить для параметра synchronous_commit значение «off».
В асинхронном режиме сброс журнальных записей выполняет процесс walwriter6, чередуя циклы работы с ожиданием. Продолжительность пауз устанавливается параметром wal_writer_delay. (по умолчанию: 200ms)
Проснувшись после очередной паузы, процесс проверяет, не появились ли в кеше новые полностью заполненные страницы WAL. Если появились, то процесс записывает их на диск, игнорируя текущую страницу. Если же с прошлого раза ни одна страница журнала не заполнилась до конца, процесс записывает текущую недозаполненную страницу, раз уж все равно проснулся7.
Этот алгоритм нацелен на то, чтобы по возможности не сбрасывать одну и ту же страницу несколько раз, что важно при большом потоке изменений.
Хотя журнальный кеш и используется как кольцевой буфер, запись останавливается, когда достигает последней страницы кеша; следующий после паузы цикл записи начнется с первой страницы. Поэтому в самом худшем случае walwriter добирается до журнальной записи с третьей попытки: сначала будут сброшены заполненные страницы в конце кеша, затем в начале, и, наконец, дело дойдет до недозаполненной страницы, содержащей искомую запись. В большинстве же случаев хватает одного-двух циклов.
Синхронизация данных происходит через каждые wal_writer_flush_after мегабайт и один раз в конце цикла записи. (по умолчанию: 1MB)
Асинхронная запись эффективнее синхронной: фиксация изменений не ждет физической записи на диск. Однако надежность уменьшается: в случае сбоя зафиксированные данные могут пропасть, если после фиксации прошло менее 3 × wal_writer_delay единиц времени (что при настройке по умолчанию составляет 0,6 секунды).
В реальности эти режимы дополняют друг друга. Даже при синхронной фиксации журнальные записи долгой транзакции могут записываться асинхронно, чтобы освободить буферы WAL. А если при сбросе страницы из буферного кеша окажется, что соответствующая журнальная запись еще не на диске, она будет сброшена немедленно даже в асинхронном режиме, поскольку иначе невозможно продолжать работу.
В основном же непростой выбор — эффективность или долговечность — остается за проектировщиком системы.
Параметр synchronous_commit можно устанавливать в том числе и для отдельных транзакций. Если на уровне приложения получается разделить транзакции на абсолютно критичные (например, работающие с финансовыми данными) и менее важные, то можно увеличить производительность, жертвуя надежностью только части транзакций.
Чтобы получить некоторое представление о том, какой выигрыш дает асинхронная фиксация, сравним время отклика и пропускную способность при двух режимах на эталонном тесте pgbench8.
Инициализируем необходимые таблицы и запустим 30-секундный тест в синхронном режиме:
И затем проведем такой же тест в асинхронном режиме:
При асинхронной фиксации в этом простом тесте время отклика (latency) существенно уменьшилось, а пропускная способность (tps) увеличилась. Разумеется, в каждой конкретной системе при конкретной нагрузке соотношение будет своим, но видно, что при коротких OLTP-транзакциях эффект может быть весьма значительным.
Восстановим значения параметров по умолчанию:
11.2. Надежность
Очевидно, что механизм журналирования должен гарантировать возможность восстановления после сбоя в любых ситуациях (не связанных, конечно, с разрушением носителя данных). На согласованность данных влияет много факторов, из которых наиболее важны кеширование, повреждение данных и неатомарность записи9.
Кеширование
На пути данных к энергонезависимому хранилищу (такому как пластина жесткого диска) стоят многочисленные кеши.
Системный вызов записи на диск приводит лишь к тому, что операционная система переносит данные в свой кеш (также находящийся в оперативной памяти и подверженный сбоям). Фактическая запись происходит асинхронно, в зависимости от настроек планировщика ввода-вывода операционной системы.
Когда планировщик решает записать данные, они попадают в кеш накопителя (жесткого диска). Электроника накопителя тоже может отложить запись, например собирая данные в группы, которые выгоднее записать одновременно. RAID-контроллер добавляет еще один уровень кеширования между операционной системой и диском.
Если не предпринимать специальных мер, момент надежного сохранения данных остается неизвестным. Обычно это и не важно, поскольку есть журнал, но сами журнальные записи обязаны немедленно сохраняться надежным образом 10. Это верно и для асинхронного режима, поскольку иначе нельзя гарантировать, что журнальные записи попадут на диск раньше измененных данных.
Контрольная точка также должна сохранять данные надежно, чтобы грязные страницы действительно были сброшены на диск, а не остались в кеше операционной системы. Кроме того, контрольная точка синхронизирует с диском и все файловые операции, выполненные ранее другими процессами (такие как запись страницы или удаление файла): к завершению контрольной точки результаты всех этих действий тоже должны гарантированно оказаться на диске11.
Есть и некоторые другие ситуации, требующие надежной записи, например выполнение нежурналируемых операций на уровне журнала minimal.
Операционные системы предоставляют разные средства, которые должны гарантировать немедленную запись данных в энергонезависимую память.
Они сводятся к двум основным: либо после записи выполняется отдельная команда синхронизации (fsync, fdatasync), либо необходимость синхронизации (или даже прямой записи, минуя кеш операционной системы) указывается при открытии файла или записи в него.
Утилита pg_test_fsync помогает определить наиболее подходящий способ синхронизации журнала для конкретной операционной системы и конкретной файловой системы; выбранный способ указывается в параметре wal_sync_method. Для других ситуаций способ определяется автоматически12.
Тонкий момент состоит в том, что при выборе метода надо учитывать характеристики аппаратуры. Например, если используется контроллер с батареей резервного питания, нет резона не использовать его кеш, поскольку батарея позволит сохранить данные в случае сбоя электропитания.
Асинхронный режим фиксации и отсутствие синхронизации — принципиально разные режимы. Отключение синхронизации (параметр fsync) еще больше ускоряет работу системы, но при любом сбое все данные кластера будут безвозвратно потеряны. Асинхронный режим дает гарантию восстановления согласованного состояния данных после сбоя, но, возможно, часть последних транзакций будет в этом состоянии отсутствовать. (по умолчанию: on)
Повреждение данных
Оборудование несовершенно, и данные могут повредиться в памяти или на носителе, измениться при передаче по интерфейсным кабелям. Часть таких ошибок обрабатывается на аппаратном уровне, но часть — нет.
Чтобы вовремя обнаружить возникшую проблему, журнальные записи всегда снабжаются контрольными суммами.
Страницы данных также можно защитить контрольными суммами13. Это можно сделать либо при инициализации кластера, либо с помощью утилиты pg_checksums 14 при остановленном сервере15. (v. 12)
В производственной среде контрольные суммы обязательно должны быть включены, несмотря на (незначительные) накладные расходы на их вычисление и проверку. Это увеличивает шансы обнаружить сбой вовремя. Но не дает полной гарантии:
контрольные суммы проверяются только при обращении к странице, поэтому повреждение может долгое время оставаться незамеченным и попасть во все резервные копии, так что не сохранится ни одного источника верной информации;
страница, заполненная нулями, считается корректной, поэтому если файловая система по ошибке «обнулит» файл, проверка не обнаружит проблему;
контрольные суммы защищают только основной слой отношений, а остальные слои и все прочие файлы (например, статусы транзакций clog) ничем не защищены.
Убедимся, что контрольные суммы включены, проверив значение параметра data_checksums, доступного только для чтения:
Остановим сервер и обнулим несколько байтов в нулевой странице основного слоя таблицы:
Снова запустим сервер:
В принципе, сервер можно было бы и не останавливать. Достаточно, чтобы страница записалась на диск и была вытеснена из кеша (иначе сервер будет продолжать работать с закешированной страницей). Но такой сценарий сложнее воспроизвести.
Теперь пробуем прочитать таблицу:
Если данные невозможно восстановить из резервной копии, стоит хотя бы попытаться прочитать поврежденную страницу (естественно, с риском получить искаженную информацию). Для этого надо установить параметр ignore_checksum_failure: (по умолчанию: off)
В данном случае все прошло успешно, потому что мы испортили некритичную часть заголовка страницы (номер LSN последней журнальной записи), а не сами данные.
Неатомарность записи
Страница базы данных обычно занимает 8 Кбайт, а на низком уровне запись происходит блоками, которые в большинстве случаев имеют меньший размер (как правило, 512 байт или 4 Кбайта). Поэтому при сбое страница данных может записаться частично. При восстановлении бессмысленно применять к такой поврежденной странице обычные журнальные записи.
Для защиты от частичной записи PostgreSQL сохраняет в журнале полный образ страницы (full page image, FPI) при первом ее изменении после начала контрольной точки. (с. 223)
Этим поведением управляет параметр full_page_writes. Отказ от записи полных образов может привести к неустранимому повреждению данных, но полные образы могут появляться в журнале даже при отключенном параметре из-за битов-подсказок. Их изменения считаются некритичными и не журналируются сами по себе — первый же запрос, обратившийся к странице, заново установит необходимые биты. Однако изменение любого, даже несущественного бита приводит к изменению контрольной суммы. Поэтому при включенных контрольных суммах (или при установленном параметре wal_log_hints) первое изменение битов-подсказок после контрольной точки вызывает запись полного образа страницы16, невзирая на значение full_page_writes. (по умолчанию: on) (с. 86) (по умолчанию: off)
Если при восстановлении в журнале встречается полный образ страницы, он безусловно (без проверки LSN) записывается на диск, поскольку, как и всякая журнальная запись, он защищен контрольной суммой и не может быть незаметно испорчен. И уже к этому гарантированно корректному образу применяются обычные журнальные записи.
Наличие полных образов существенно увеличивает объем генерируемых журнальных записей, несмотря на то что механизм журналирования исключает из полного образа незанятое место внутри страницы17. Ситуацию можно значительно улучшить за счет сжатия полных образов, которое включается параметром wal_compression. (по умолчанию: off)
Проведем простой эксперимент с помощью утилиты pgbench. Выполним контрольную точку и сразу же запустим тест с фиксированным количеством транзакций:
Размер журнальных записей:
Полные образы в нашем случае составляют значительную часть сгенерированного объема, в чем можно убедиться, посмотрев статистику утилитой pg_waldump с ключом --stats или функцией pg_get_wal_stats расширения pg_walinspect. (v. 15)
Таблица показывает количество журнальных записей (N), объем обычных записей (Record size) и объем полных образов (FPI size) для каждого типа ресурсов (Type):
Соотношение будет меньше, если страницы успеют измениться между контрольными точками несколько раз. Это еще один повод не выполнять контрольные точки слишком часто.
Теперь посмотрим, как помогает сжатие, на том же примере:
Размер журнальных записей со сжатием:
Помимо традиционного алгоритма сжатия PGLZ, доступны два других: LZ4 и Zstandard. Алгоритм LZ4 обычно требует меньше ресурсов процессора, показывая примерно ту же степень сжатия. Алгоритм Zstandard сочетает словарное сжатие с энтропийным кодированием; он сильнее нагружает процессор, но сжимает данные лучше. (v. 15)
Размер журнальных записей со сжатием Zstandard:
Вывод: при наличии большого числа полных образов страниц (из-за контрольных сумм или full_page_writes, то есть почти всегда) имеет смысл воспользоваться сжатием. Несколько алгоритмов позволяют найти компромисс между дополнительной нагрузкой на процессор и сокращением объема журнальных записей.
11.3. Уровни журнала
Основная задача журнала предзаписи — обеспечить возможность восстановления после сбоя. Но журнал можно использовать и для решения других задач, расширив состав входящих в него записей. Есть несколько уровней журналирования: minimal, replica и logical. Каждый следующий уровень включает в себя все, что попадает в журнал предыдущего уровня, и добавляет некоторую новую информацию.
Используемый уровень задается параметром wal_level; его изменение требует перезапуска сервера. (по умолчанию: replica)
Minimal
Начальный уровень minimal гарантирует только восстановление после сбоя. Для экономии места операции с отношениями, созданными или опустошенными в текущей транзакции, не записываются в журнал, если они связаны со вставкой большого объема данных (в частности, это касается таких команд, как CREATE TABLE AS SELECT или CREATE INDEX)18. Вместо журналирования необходимые данные сразу же сбрасываются на диск, а изменения в системном каталоге становятся видимыми при фиксации транзакции.
Если сбой произойдет в процессе выполнения операции, то уже записанные на диск данные останутся невидимыми и не нарушат согласованности. Если же сбой произойдет после того, как операция завершится, то все необходимые данные для применения последующих журнальных записей уже будут находиться на диске.
Объем данных, который требуется записать в только что созданное отношение, чтобы такая оптимизация начала работать, определяется значением параметра wal_skip_threshold. (v. 13) (по умолчанию: 2MB)
Посмотрим, что попадает в журнал на уровне minimal.
По умолчанию используется более высокий уровень replica, рассчитанный на поддержку репликации. При установке минимального уровня потребуется также обнулить значение параметра max_wal_senders, который определяет допустимое количество процессов walsender: (по умолчанию: 10)
Изменение параметров требует перезапуска сервера:
Запомним текущую позицию в журнале:
Опустошим таблицу и в той же транзакции вставим новые строки, превысив предел wal_skip_threshold:
Я использую команду TRUNCATE, а не создаю отдельную таблицу, поскольку при этом генерируется меньше журнальных записей.
С помощью уже знакомого расширения pg_walinspect посмотрим содержимое журнала. (v. 15)
Первая запись (CREATE) говорит о создании нового файла для отношения (поскольку при опустошении таблица фактически перезаписывается). (с. 177)
Далее следуют несколько записей, относящихся к системному каталогу. Они отражают изменение таблицы pg_class (UPDATE) и добавление записей к трем индексам, построенным на этой таблице (INSERT_LEAF). В этом можно убедиться, сопоставив путь к файлу, показанный в столбце rel, с именем отношения. (с. 30)
Последней идет запись о фиксации транзакции. Вставка данных в таблицу не журналируется.
Replica
Когда система восстанавливается после сбоя, данные, сохраненные на диске, обновляются до согласованного состояния с помощью журнальных записей. То же самое происходит и при восстановлении из резервной копии. Но в этом случае могут использоваться заархивированные журнальные записи, чтобы не просто восстановить согласованность, а довести состояние данных до целевой точки восстановления. Количество таких записей может быть весьма велико (например, они могут охватывать несколько дней), то есть период восстановления будет содержать не одну контрольную точку, а множество. Поэтому минимального уровня журнала недостаточно — нельзя повторить операцию, если она не журналируется. Для возможности восстановления из резервной копии в журнал должны попадать все операции.
То же самое верно и для репликации: все, что не журналируется, не будет передано на реплику и не будет на ней воспроизведено.
Но если реплика используется для выполнения запросов, все еще больше усложняется. Во-первых, нужна информация об исключительных блокировках, возникающих на основном сервере, поскольку они могут конфликтовать с запросами на реплике. Во-вторых, нужно уметь строить снимки данных, а для этого необходима информация о выполняющихся транзакциях. В случае реплики нужно учитывать не только локальные транзакции, но и транзакции на основном сервере. (с. 261) (с. 97)
Единственный способ передать эту информацию реплике — периодически записывать ее в журнал19. Этим занимается процесс фоновой записи bgwriter 20: при наличии активности в системе он раз в 15 секунд (интервал не настраивается) создает запись типа RUNNING_XACTS.
Возможность восстановить данные из резервной копии и использовать физическую репликацию гарантируется на уровне журнала replica.
Именно этот уровень используется по умолчанию, поэтому просто сбросим установленные выше параметры и перезагрузим сервер:
Повторим ту же последовательность действий, что и в прошлый раз (но ограничимся вставкой одной строки, чтобы не загромождать вывод):
Теперь проверим журнальные записи.
К записям, которые появляются на уровне minimal, добавились:
записи менеджера ресурсов Standby, связанные с репликацией:
LOCK(блокировки) иRUNNING_XACTS(активные транзакции);запись об инициализации новой страницы и одновременной вставке в нее строки
INSERT+INIT.
Logical
Наконец, максимальный уровень logical обеспечивает возможность логического декодирования и логической репликации. Он должен быть включен на публикующем сервере.
С точки зрения журнальных записей этот уровень практически не отличается от уровня replica — в имеющиеся записи добавляются идентификаторы строк 21; появляются записи, связанные с источниками репликации и аннулированием кеша, а также произвольные логические записи, которые могут добавлять приложения. В основном же логическое декодирование зависит от информации о выполняющихся транзакциях (RUNNING_XACTS): она позволяет строить снимки данных, чтобы отслеживать изменения системного каталога.
backend/access/transam/xlog.c, функция WALReadFromBuffers. ↩︎
backend/access/transam/xlog.c, функция XLogFlush. ↩︎
backend/access/transam/xlog.c, функция RecordTransactionCommit. ↩︎
backend/access/transam/xlog.c, функция XLogBackgroundFlush. ↩︎
backend/access/transam/xlog.c, функция issue_xlog_fsync. ↩︎
backend/storage/file/fd.c, функция pg_fsync. ↩︎
backend/storage/buffer/bufmgr.c, функция MarkBufferDirtyHint. ↩︎
backend/access/transam/xloginsert.c, функция XLogRecordAssemble. ↩︎
include/utils/rel.h, макрос RelationNeedsWAL. ↩︎
backend/storage/ipc/standby, функция LogStandbySnapshot. ↩︎
postgrespro.ru/docs/postgresql/18/sql-altertable#SQL-ALTERTABLE-REPLICA-IDENTITY. ↩︎