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

10 Журнал предзаписи

PostgreSQL 18 · Издание 2026 года · Китайский перевод доступен для PostgreSQL 18

10.1. Журналирование

В случае сбоя, например при выключении электропитания либо при отказе СУБД или операционной системы, теряется все содержимое оперативной памяти; остается только информация, записанная на диск. Чтобы сервер мог стартовать после сбоя, необходимо восстановить согласованность данных. Если при сбое пострадал сам диск, та же задача возникает и при восстановлении из резервной копии.

Один из теоретически возможных способов — все время поддерживать данные на диске в согласованном виде. Фактически это означает необходимость постоянной записи отдельных страниц на диск (а случайная запись обходится дороже последовательной), причем в таком порядке, что ни в какой момент времени согласованность не нарушается (а это трудно, особенно для сложно организованных индексов).

PostgreSQL, как и большинство СУБД, использует другой подход.

В процессе работы часть актуальных данных хранится только в оперативной памяти и записывается на диск (или на другой энергонезависимый носитель) отложенно. Поэтому данные на диске рассогласованы в течение всего времени работы сервера — разные страницы оказываются записанными по состоянию на разные моменты времени. При этом каждое действие в оперативной памяти (например, изменение страницы в буферном кеше) журналируется: создается и сохраняется на диске журнальная запись, содержащая минимальную информацию, достаточную для повторения того же действия в случае необходимости1.

Журнальная запись об изменении страницы в обязательном порядке попадает на диск перед тем, как туда попадет измененная страница. Отсюда и название: журнал предзаписи (write-ahead log). Это требование дает возможность в случае сбоя прочитать с диска журнал и повторить те операции, которые были выполнены до сбоя, но результат которых не успел дойти до диска из оперативной памяти и пропал.

Как правило, вести журнал эффективнее, чем записывать отдельные страницы. Журнал представляет собой последовательный поток, с записью которого неплохо справляются даже HDD-диски. К тому же журнальная запись может быть меньше страницы по размеру.

Журналировать нужно все операции, при выполнении которых есть риск получить несогласованность на диске в случае сбоя. В частности, в журнал записываются следующие действия:

  • изменение страниц отношений в буферном кеше — сброс измененной страницы на диск откладывается;

  • фиксация и отмена транзакций — изменение статуса происходит в буферах clog и тоже попадает на диск не сразу;

  • файловые операции (создание и удаление файлов и каталогов, например, при создании таблицы) — такие операции должны происходить синхронно с изменением данных.

В журнал не записываются:

  • операции с нежурналируемыми (UNLOGGED) таблицами;

  • операции с нежурналируемыми последовательностями (как правило, относящимися к нежурналируемым таблицам); (v. 15)

  • операции с временными таблицами и последовательностями — время жизни таких объектов в любом случае не превышает время жизни создавшего их сеанса.

Журнал используется не только для восстановления после сбоя, но и для восстановления на произвольный момент времени из резервной копии, а также для репликации.

10.2. Устройство журнала

Логическая структура

Логически журнал 2 можно представить себе как последовательность записей различной длины. Каждая запись содержит данные о некоторой операции, предваренные стандартным заголовком3. В заголовке, в числе прочего, указаны:

  • номер транзакции, к которой относится запись;

  • менеджер ресурсов, ответственный за интерпретацию записи 4;

  • контрольная сумма для обнаружения повреждений данных;

  • длина записи;

  • ссылка на предыдущую запись журнала.

Примечание

Обычно журнал читается в прямом направлении, но, например, утилита pg_rewind просматривает его в обратном.

Сами данные имеют разный формат и смысл. Например, они могут представлять собой некоторый фрагмент страницы, который надо записать поверх ее содержимого с определенным смещением. Менеджер ресурсов «понимает», как расшифровать и воспроизвести запись. Есть отдельные менеджеры для таблиц, для каждого типа индекса, для статуса транзакций и других сущностей. Полный их список можно получить командой pg_waldump -r list.

В разделяемой памяти сервера для журнала выделены специальные буферы. Размер журнального кеша задается параметром wal_buffers. Значение по умолчанию подразумевает автоматическую настройку: выделяется 16 Мбайт (размер журнального сегмента) или меньше при совсем небольших размерах буферного кеша. (по умолчанию: -1) (с. 216)

Журнальный кеш похож по своему устройству на буферный, но работает преимущественно в режиме кольцевого буфера: записи добавляются в «голову», а сохраняются на диск с «хвоста». Слишком маленький размер журнального кеша приведет к тому, что синхронизация с диском будет выполняться чаще, чем необходимо.

Позиция уже сохраненных записей («хвост» буфера) и позиция вставки («голова» буфера) в ненагруженной системе почти всегда будут совпадать:

=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
 pg_current_wal_lsn | pg_current_wal_insert_lsn
--------------------+---------------------------
 2/8121CB28         | 2/8121CB28
(1 row)

Чтобы сослаться на определенную запись, используется тип данных pg_lsn (log sequence number, LSN), представляющий 64-битное смещение в байтах до журнальной записи относительно начала журнала. LSN выводится как два 32-битных числа в шестнадцатеричной системе через косую черту.

Создадим таблицу:

=> CREATE TABLE wal(id integer);
=> INSERT INTO wal VALUES (1);

Начнем транзакцию и запомним позицию вставки в журнал:

=> BEGIN;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/81235AE8
(1 row)

Теперь выполним какую-нибудь операцию, например обновим строку:

=> UPDATE wal SET id = id + 1;

Изменение выполняется в странице, находящейся в буферном кеше в оперативной памяти. Информация об изменении сохраняется и в журнальную страницу, также в оперативной памяти. Поэтому позиция вставки увеличивается:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/81235B30
(1 row)

Чтобы гарантировать, что измененная страница данных будет вытеснена на диск строго позже, чем журнальная запись, в заголовке страницы сохраняется номер LSN последней записи, относящейся к этой странице. Позицию можно проверить с помощью расширения pageinspect:

=> SELECT lsn FROM page_header(get_raw_page('wal',0));
    lsn
------------
 2/81235B30
(1 row)

Для всего кластера баз данных используется один общий журнал, и в него все время попадают новые записи. Поэтому номер LSN на странице может оказаться меньше, чем значение, которое вернула чуть раньше функция pg_current_wal_insert_lsn. Но в системе, где ничего не происходит, цифры совпадут.

Теперь завершим транзакцию:

=> COMMIT;

Запись о фиксации также попадает в журнал, и позиция снова меняется:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/81235B58
(1 row)

Фиксация меняет статус транзакции в страницах clog, располагающихся в SLRU-кеше. Чтобы страницу clog нельзя было вытеснить на диск раньше, чем соответствующую журнальную запись, для страниц clog тоже отслеживается номер LSN последней журнальной записи. Но эта информация хранится не в самой странице, а в оперативной памяти. (с. 85) (с. 309)

В какой-то момент созданные журнальные записи окажутся на диске. После этого страницы данных и clog могут быть вытеснены из кеша. Если бы потребовалось вытеснить их раньше, это было бы обнаружено, и журнальные записи были бы принудительно сброшены на диск первыми5. (с. 235)

Зная две позиции LSN, можно получить размер журнальных записей между ними (в байтах) простым вычитанием одной позиции из другой. Надо только привести позиции к типу pg_lsn:

=> SELECT '2/81235B58'::pg_lsn - '2/81235AE8'::pg_lsn;
 ?column?
----------
      112
(1 row)

В данном случае обновление строки и фиксация потребовали около сотни байтов в журнале.

Таким же способом можно оценить, какой объем журнальных записей генерируется сервером за единицу времени при определенной нагрузке. Эта информация потребуется при настройке контрольной точки.

Физическая структура

На диске журнал хранится в каталоге PGDATA/pg_wal в виде файлов-сегментов. Параметр wal_segment_size, доступный только для чтения, показывает размер сегмента. (по умолчанию: 16MB)

Для нагруженных систем увеличение размера сегментов может снизить накладные расходы, но эта настройка выполняется только при инициализации кластера (initdb --wal-segsize).

Журнальные записи попадают в текущий файл; когда он заканчивается — начинает использоваться следующий.

Можно узнать, в каком файле мы найдем нужную позицию и с каким смещением от начала файла:

=> SELECT file_name, upper(to_hex(file_offset)) file_offset
FROM pg_walfile_name_offset('2/81235AE8');
        file_name         | file_offset
--------------------------+-------------
 000000010000000200000081 | 235AE8
 └───┬──┘└───────────┬────────────┘
   ветвь      log sequence number
  времени
(1 row)

Имя файла состоит из двух частей. Старшие восемь шестнадцатеричных разрядов показывают номер ветви времени, которая используется при восстановлении из резервной копии, а остаток соответствует старшим разрядам LSN (младшие разряды LSN показывает поле file_offset).

Журнальные файлы можно посмотреть специальной функцией:

=> SELECT *
FROM pg_ls_waldir()
WHERE name = '000000010000000200000081';
           name           |   size   |      modification
--------------------------+----------+------------------------
 000000010000000200000081 | 16777216 | 2026-04-05 20:54:38+03
(1 row)

Заглянуть в заголовки созданных журнальных записей можно либо утилитой pg_waldump, либо с помощью расширения pg_walinspect: (v. 15)

=> CREATE EXTENSION pg_walinspect;
=> SELECT start_lsn, resource_manager, xid, record_type,
  left(description,44) description, block_ref
FROM pg_get_wal_records_info('2/81235AE8', '2/81235B58') \gx
-[ RECORD 1 ]----+------------------------------------------------
start_lsn        | 2/81235AE8
resource_manager | Heap
xid              | 917
record_type      | HOT_UPDATE
description      | old_xmax: 917, old_off: 1, old_infobits: [],
block_ref        | blkref #0: rel 1663/16391/16586 fork main blk 0
-[ RECORD 2 ]----+------------------------------------------------
start_lsn        | 2/81235B30
resource_manager | Transaction
xid              | 917
record_type      | COMMIT
description      | 2026-04-05 20:54:38.825403+03
block_ref        |

Здесь мы видим заголовки двух записей.

Первая — операция HOT_UPDATE, относящаяся к менеджеру ресурсов Heap. В столбце block_ref указаны имя файла и номер обновленной табличной страницы: (с. 115)

=> SELECT pg_relation_filepath('wal');
 pg_relation_filepath
----------------------
 base/16391/16586
(1 row)

Вторая запись — COMMIT, относящаяся к менеджеру ресурсов Transaction.

10.3. Контрольная точка

Для восстановления согласованности после сбоя необходимо последовательно читать журнал и применять к страницам те записи, которые относятся к пропавшим изменениям. Чтобы их обнаружить, надо сравнить LSN страницы на диске с LSN журнальной записи. Однако остается непонятным, с какой именно записи следует начинать восстановление. Если начать слишком поздно, то к страницам, записанным на диск раньше этого момента, будут применены не все изменения, а это приведет к необратимому повреждению данных. Начать же с самого начала нереально: невозможно хранить такой потенциально огромный объем данных, и невозможно смириться со столь же огромным временем восстановления. Нужна такая постепенно продвигающаяся вперед контрольная точка, с которой можно безопасно начинать восстановление; предшествующие ей журнальные записи, соответственно, можно удалять.

Самый простой вариант получить контрольную точку — периодически приостанавливать работу системы и сбрасывать на диск все грязные страницы буферного и других кешей. Такой способ, конечно, никуда не годится, поскольку в эти моменты система будет замирать на неопределенное, но весьма существенное время.

Поэтому на практике контрольная точка растягивается во времени и фактически превращается в отрезок. Выполнением контрольной точки занимается специальный фоновый процесс checkpointer 6.

Начало контрольной точки. Сначала процесс контрольной точки сбрасывает на диск структуры, которые можно записать одномоментно благодаря их небольшому объему. К ним относятся объекты из кеша SLRU (такие как статусы транзакций clog или вложенные транзакции) и некоторые другие. (с. 309)

Выполнение контрольной точки. Основное время выполнения контрольной точки занимает запись грязных страниц буферного кеша 7.

Сначала в заголовках всех грязных на текущий момент (момент начала контрольной точки) буферов проставляется специальный флаг. Это происходит быстро, поскольку не связано с вводом-выводом.

Затем процесс контрольной точки постепенно проходит по всем буферам и сбрасывает помеченные на диск. Страницы при этом не вытесняются из кеша, а только записываются, поэтому не нужно обращать внимания ни на число обращений к буферу, ни на его закрепление.

Страницы сбрасываются по порядку номеров, чтобы запись на диск была по возможности последовательной, а не случайной. При этом запись в разные табличные пространства (которые могут соответствовать разным физическим устройствам) чередуется для равномерного распределения нагрузки. (с. 26)

Помеченные буферы могут также быть записаны и обслуживающими процессами — смотря кто доберется до буфера первым. В любом случае при записи снимается установленный ранее флаг, так что (для целей контрольной точки) буфер будет записан только один раз.

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

Завершение контрольной точки. Когда все буферы, которые были грязными на момент начала контрольной точки, оказываются записанными, контрольная точка считается завершенной. Теперь (но не раньше) момент начала используется в качестве той точки, с которой надо начинать восстановление. Все предшествующие журнальные записи больше не нужны.

Контрольная точка: начало; восстановления; необходимые файлы журнала; сбойвремя; контрольная точка; начало; восстановления; необходимые файлы журнала; сбойвремя; контрольная точкаконтрольная точка
Рисунок 10.1 Контрольная точка

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

Кроме того, в файле PGDATA/global/pg_control обновляется указание на последнюю пройденную контрольную точку. (До завершения процесса pg_control указывает на предыдущую.)

Чтобы окончательно разобраться, что на что указывает, рассмотрим небольшой пример.

Сделаем несколько страниц в буферном кеше грязными:

=> UPDATE big SET s = 'FOO';
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
 count
-------
  4131
(1 row)

Запомним текущую позицию в журнале:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/81AC6580
(1 row)

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

=> CHECKPOINT;
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
 count
-------
     0
(1 row)

Посмотрим, как контрольная точка отразилась в журнале. Запись CHECKPOINT_REDO отмечает начало контрольной точки, а CHECKPOINT_ONLINE — ее завершение: (v. 17)

=> SELECT start_lsn,
  resource_manager,
  record_type,
  left(description,46)||'...' description
FROM pg_get_wal_records_info('2/81AC6580','FFFFFFFF/FFFFFFFF')
  -- pg_get_wal_records_info_till_end_of_wal до v.16
WHERE record_type LIKE 'CHECKPOINT%'
\gx
-[ RECORD 1 ]----+--------------------------------------------------
start_lsn        | 2/81AC6580
resource_manager | XLOG
record_type      | CHECKPOINT_REDO
description      | wal_level replica...
-[ RECORD 2 ]----+--------------------------------------------------
start_lsn        | 2/81AC65D8
resource_manager | XLOG
record_type      | CHECKPOINT_ONLINE
description      | redo 2/81AC6580; tli 1; prev tli 1; fpw true; ...

Между двумя этими позициями в журнале будут располагаться другие записи, порожденные продолжающим свою работу сервером.

Примечание

До версии PostgreSQL 10 для журнала предзаписи часто использовался акроним XLOG (transaction log). Следы этого именования остались не только в названии менеджера ресурсов, но и в исходном коде.

Ту же информацию найдем и в управляющем файле:

postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | egrep 'Latest.*location'
Latest checkpoint location:           2/81AC65D8
Latest checkpoint's REDO location:    2/81AC6580
Контрольная точка: PGDATA/global/pg\_control; Latest checkpoint location:2/81AC65D8; Latest checkpoint’s REDO location:2/81AC6580; началозавершение; контрольнойконтрольной; точкиточки; REDOONLINE
Рисунок 10.2 Контрольная точка

10.4. Восстановление

При старте сервера первым делом запускается процесс postmaster. В свою очередь он запускает процесс startup 8, задача которого — обеспечить восстановление, если произошел сбой.

Чтобы определить, требуется ли восстановление, startup читает управляющий файл pg_control и проверяет статус кластера. Утилита pg_controldata позволяет заглянуть в содержимое файла:

postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | grep state
Database cluster state:               in production

Статус аккуратно остановленного сервера — «shut down». Статус «in production» у неработающего сервера свидетельствует о сбое. В этом случае процесс startup автоматически выполняет восстановление с позиции начала последней пройденной контрольной точки из того же файла pg_control.

Примечание

Если в каталоге PGDATA присутствует файл backup_label из резервной копии, позиция начала восстановления читается из него.

Процесс последовательно читает журнал, начиная с найденной позиции, и применяет журнальные записи к страницам данных, если LSN страницы меньше LSN записи. Если же LSN страницы оказался больше, то запись применять не нужно, а точнее говоря — нельзя, поскольку записи рассчитаны на строго последовательное применение.

Однако некоторые записи формируются как полный образ страницы (full page image, FPI). Полный образ можно применить к любому состоянию страницы, поскольку оно все равно будет стерто. Такие изменения называют идемпотентными. Другой пример идемпотентного действия — запись об изменении статуса транзакции: каждой транзакции соответствуют отдельные фиксированные биты в clog, установка которых не зависит от их предыдущего значения. Поэтому внутри страниц clog нет необходимости хранить LSN последнего изменения.

Журнальные записи применяются к страницам в буферном кеше, как это происходит и при обычной работе.

Аналогично журнальные записи применяются и к файлам: например, если запись говорит о том, что файл должен существовать, а его нет, — файл создается.

После окончания восстановления все нежурналируемые отношения перезаписываются с помощью образов в init-файлах. (с. 30)

В заключение выполняется контрольная точка, чтобы зафиксировать на диске восстановленное состояние.

На этом работа процесса startup заканчивается.

Примечание

В классическом виде процесс восстановления состоит из двух этапов. На первом (roll forward) накатываются журнальные записи, при этом сервер повторяет потерянную при сбое работу. На втором (roll back) откатываются транзакции, которые не были зафиксированы на момент сбоя.

PostgreSQL не нуждается во втором этапе. После восстановления в clog не будет установлен ни бит фиксации, ни бит обрыва (что формально соответствует выполняющейся транзакции), но поскольку точно известно, что транзакция уже не выполняется, она будет считаться оборванной9.

Можно сымитировать сбой, принудительно остановив сервер в режиме immediate:

postgres$ pg_ctl stop -m immediate

Проверим состояние кластера:

postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | grep 'state'
Database cluster state:               in production

При запуске сервера процесс startup понимает, что произошел сбой, и запускает восстановление:

postgres$ pg_ctl start -l /home/postgres/logfile
postgres$ tail -n 8 /home/postgres/logfile
LOG:  database system was interrupted; last known up at 2026-04-05
20:54:39 MSK
LOG:  database system was not properly shut down; automatic recovery in
progress
LOG:  redo starts at 2/81AC6580
LOG:  invalid record length at 2/81AC6650: expected at least 24, got 0
LOG:  redo done at 2/81AC65D8 system usage: CPU: user: 0.00 s, system:
0.00 s, elapsed: 0.00 s
LOG:  checkpoint starting: end-of-recovery immediate wait
LOG:  checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU
buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s,
sync=0.001 s, total=0.007 s; sync files=2, longest=0.001 s,
average=0.001 s; distance=0 kB, estimate=0 kB; lsn=2/81AC6650, redo
lsn=2/81AC6650
LOG:  database system is ready to accept connections

При нормальном останове сервера процесс postmaster отключает всех клиентов и затем выполняет финальную контрольную точку, чтобы сбросить грязные страницы на диск.

Запомним текущую позицию в журнале:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/81AC66C8
(1 row)

Теперь аккуратно остановим сервер:

postgres$ pg_ctl stop

Проверим состояние кластера:

postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | grep state
Database cluster state:               shut down

Посмотрев журнал, обнаружим в конце запись о финальной контрольной точке (CHECKPOINT_SHUTDOWN):

postgres$ /usr/local/pgsql/bin/pg_waldump \
-p /usr/local/pgsql/data/pg_wal -s 2/81AC66C8
rmgr: XLOG        len (rec/tot):    114/   114, tx:          0, lsn:
2/81AC66C8, prev 2/81AC6650, desc: CHECKPOINT_SHUTDOWN redo 2/81AC66C8;
tli 1; prev tli 1; fpw true; wal_level replica; xid 0:920; oid 24778;
multi 1; offset 0; oldest xid 744 in DB 1; oldest multi 1 in DB 1;
oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown
pg_waldump: error: error in WAL record at 2/81AC66C8: invalid record
length at 2/81AC6740: expected at least 24, got 0

Последнее сообщение от pg_waldump говорит о том, что утилита дочитала журнал до конца.

Снова запустим экземпляр:

postgres$ pg_ctl start -l /home/postgres/logfile

10.5. Фоновая запись

Если обслуживающему процессу потребуется вытеснить из буфера грязную страницу, ему придется самостоятельно записать ее на диск. Это нехорошая ситуация, приводящая к ожиданиям, — гораздо лучше, когда запись происходит асинхронно, в фоновом режиме.

Частично работу по сбросу грязных страниц берет на себя процесс контрольной точки, но этого недостаточно.

Поэтому существует также процесс фоновой записи bgwriter10, который применяет тот же алгоритм поиска буферов, что и механизм вытеснения, но с двумя отличиями:

  • используется собственная «часовая стрелка», которая может опережать «часовую стрелку» вытеснения, но никогда не отстает от нее;

  • при обходе буферов счетчик обращений не уменьшается.

Грязная страница сбрасывается на диск, если буфер не закреплен и имеет нулевое число обращений. Таким образом, фоновый процесс записи идет впереди вытеснения и заранее сбрасывает те буферы, которые с большой вероятностью вскоре будут вытеснены.

Это увеличивает шансы на то, что буферы, выбранные при вытеснении, не будут грязными.

10.6. Настройка

Настройка контрольной точки

Продолжительность контрольной точки (точнее говоря, продолжительность этапа записи грязных буферов) определяется значением параметра checkpoint_completion_target. Оно показывает, какую часть времени между началами двух соседних контрольных точек будет происходить запись. Увеличивать значение до единицы не рекомендуется, поскольку может получиться так, что следующая контрольная точка должна будет начаться до того, как полностью завершилась предыдущая. Катастрофы не случится, поскольку в любом случае в один момент времени выполняется только одна контрольная точка, но нормальный ритм работы может быть нарушен. (по умолчанию: 0.9) (v. 14)

К настройке остальных параметров можно подойти следующим образом. Сначала определимся, какой объем журнальных файлов допустимо сохранять между двумя последовательными контрольными точками. Чем больше объем, тем меньше накладных расходов, но в любом случае это значение будет ограничено доступным свободным местом и допустимым временем восстановления.

Можно посчитать, за какое время при обычной нагрузке будет генерироваться этот объем. Для этого надо запомнить начальную позицию вставки в журнал и периодически проверять разность текущей и запомненной позиций.

Полученное время будем считать обычным интервалом между контрольными точками и запишем его в параметр checkpoint_timeout. Значение по умолчанию, скорее всего, слишком мало; обычно время увеличивают, например, до получаса. (по умолчанию: 5min) (с. 245)

Однако возможно (и даже вероятно), что иногда нагрузка будет выше, и за указанное в параметре время будет сгенерирован слишком большой объем журнальных записей. В этом случае контрольная точка должна выполняться чаще. Для этого параметром max_wal_size ограничим объем журнальных файлов, необходимых для восстановления. При превышении этого предела 11 сервер инициирует внеплановую контрольную точку. (по умолчанию: 1GB)

Журнальные файлы, необходимые для восстановления, содержат записи за прошедшую завершенную контрольную точку и за текущую, еще не завершенную. Поэтому для оценки общего объема надо умножить известный объем между контрольными точками на (1 + checkpoint_completion_target).

При такой настройке бóльшая часть контрольных точек происходит по расписанию раз в checkpoint_timeout единиц времени. Но при повышенной нагрузке контрольная точка вызывается чаще, по достижении журналом объема max_wal_size.

Процесс контрольной точки постоянно сопоставляет свой фактический прогресс с ожидаемым 12:

фактический прогресс определяется долей уже просмотренных страниц буферного кеша;

ожидаемый прогресс по времени определяется долей уже потраченного времени, исходя из того, что контрольная точка должна завершиться за интервал checkpoint_timeout × checkpoint_completion_target;

ожидаемый прогресс по объему определяется долей уже заполненных журнальных файлов, планируемое количество которых вычисляется исходя из значения max_wal_size × checkpoint_completion_target.

Если запись грязных страниц опережает график, она приостанавливается; если отстает хотя бы по одному из двух параметров — догоняет без задержек13. Учет и времени, и объема позволяет однотипно управлять скоростью выполнения контрольных точек как по расписанию, так и по требованию.

После прохождения контрольной точки сервер избавляется от ненужных для восстановления (и других задач) журнальных записей14. При этом часть файлов удаляется, а остальные переименовываются и используются заново. Их количество зависит от интенсивности генерации журнальных записей, но в любом случае в PGDATA/pg_wal поддерживается минимальный объем min_wal_size15. (по умолчанию: 80MB)

Обычно переименование позволяет сэкономить на постоянном создании и удалении файлов, но при необходимости такое поведение можно отключить с помощью параметра wal_recycle. (v. 12) (по умолчанию: on)

Рисунок показывает, как меняется объем необходимых серверу журнальных файлов в нормальных условиях.

Настройка контрольной точки: объем WAL; max\_wal\_size; время; checkpoint\_timeoutобъем WAL, генерируемый за время; между началами контрольных точек
Рисунок 10.3 Настройка контрольной точки

Важно понимать, что реальный объем журнальных файлов на сервере может быть больше, а в некоторых случаях может и превысить значение max_wal_size:

  • Параметр max_wal_size задает ориентир, но не служит жестким ограничением. Если нагрузка возрастет, запись может отстать от графика.

  • Сервер не имеет права стирать журнальные файлы, еще не переданные через слоты репликации и еще не записанные в архив при непрерывном архивировании. Этот функционал требует постоянного мониторинга, потому что можно легко переполнить дисковую память сервера.

  • Некоторое количество файлов может быть зарезервировано администратором с помощью параметра wal_keep_size. (v. 12) (по умолчанию: 0MB)

Настройка фоновой записи

Процесс фоновой записи имеет смысл настраивать после контрольной точки. Совместно эти процессы должны успевать записывать грязные буферы до того, как они потребуются обслуживающим процессам.

Процесс фоновой записи выполняет работу циклами, засыпая в промежутках на bgwriter_delay единиц времени. (по умолчанию: 200ms)

Количество страниц, которые будут записаны за один цикл работы, определяется по среднему количеству буферов, запрошенных обслуживающими процессами с прошлого запуска (используется скользящее среднее, чтобы сгладить неравномерность между запусками, но при этом не зависеть от давней истории). Вычисленное количество дополнительно умножается на коэффициент bgwriter_lru_multiplier. Но в любом случае за один цикл работы не будет записано более bgwriter_lru_maxpages страниц. (по умолчанию: 2) (по умолчанию: 100)

Если процесс не обнаружил ни одного грязного буфера (то есть в системе ничего не происходит), он «впадает в спячку». После первого же обращения серверного процесса за буфером процесс просыпается и продолжает работать обычным образом.

Мониторинг

Настройку контрольной точки и фоновой записи можно и нужно корректировать, получая обратную связь от мониторинга.

Если контрольные точки, вызванные превышением объема журнальных файлов, выполняются чаще, чем указано в параметре checkpoint_warning, в журнал сообщений выводится предупреждение. Значение параметра стоит привести в соответствие с предполагаемой пиковой нагрузкой. (по умолчанию: 30s)

Детальная информация о ходе выполнения контрольных точек выводится в журнал сообщений сервера при включенном параметре log_checkpoints. (по умолчанию: on)

Поменяем что-нибудь в данных и инициируем контрольную точку:

=> UPDATE big SET s = 'BAR';
=> CHECKPOINT;

В журнале сообщений мы увидим, сколько буферов было записано, как изменился состав журнальных файлов после контрольной точки, сколько времени заняла контрольная точка и расстояние (в байтах) между началами соседних контрольных точек:

postgres$ tail -n 2 /home/postgres/logfile
LOG:  checkpoint starting: immediate force wait
LOG:  checkpoint complete: wrote 4097 buffers (25.0%), wrote 3 SLRU
buffers; 0 WAL file(s) added, 1 removed, 0 recycled; write=0.012 s,
sync=0.007 s, total=0.025 s; sync files=3, longest=0.006 s,
average=0.003 s; distance=9213 kB, estimate=9213 kB; lsn=2/823C5CB0,
redo lsn=2/823C5C58

Но наиболее полезная информация для целей настройки — статистика работы процесса контрольной точки в представлении pg_stat_checkpointer. (v. 17)

Примечание

До версии 17 контрольная точка и фоновая запись разделяли одно представление pg_stat_bgwriter, поскольку раньше обе задачи выполнялись процессом фоновой записи; в версии 9.2 контрольную точку выделили в отдельный процесс, но представление долгое время оставалось общим.

=> SELECT * FROM pg_stat_checkpointer \gx
-[ RECORD 1 ]-------+------------------------------
num_timed           | 0
num_requested       | 26
num_done            | 21
restartpoints_timed | 0
restartpoints_req   | 0
restartpoints_done  | 0
write_time          | 106014
sync_time           | 2660
buffers_written     | 78463
slru_written        | 26
stats_reset         | 2026-04-05 20:51:34.261075+03

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

  • num_timed — по расписанию (по истечении интервала времени checkpoint_timeout);

  • num_requested — по требованию (в том числе по достижении объема журнала max_wal_size).

Большое значение в столбце num_requested (по сравнению с num_timed) говорит о том, что в реальности контрольные точки происходят чаще, чем предполагалось.

Примечание

Поля, начинающиеся на restartpoints, актуальны только для реплики.

Представление pg_stat_bgwriter показывает статистику фоновой записи:

=> SELECT * FROM pg_stat_bgwriter \gx
-[ RECORD 1 ]----+------------------------------
buffers_clean    | 20899
maxwritten_clean | 192
buffers_alloc    | 513174
stats_reset      | 2026-04-05 20:51:34.261075+03

Для настройки полезно значение maxwritten_clean: это число показывает, сколько раз процесс фоновой записи прекращал цикл работы из-за превышения bgwriter_lru_maxpages.

В хорошо настроенной системе обслуживающие процессы практически не выполняют запись; основной ее объем приходится на процессы контрольной точки (buffers_written) и фоновой записи (buffers_clean).

Кроме подробных сведений о контрольной точке и фоновой записи, важно иметь информацию и о журнале предзаписи в целом. Самую общую статистику — количество журнальных записей и полных образов страниц, а также занимаемый ими объем — можно получить из представления pg_stat_wal: (v. 14)

=> SELECT wal_records, wal_fpi, pg_size_pretty(wal_bytes) wal_bytes
FROM pg_stat_wal;
 wal_records | wal_fpi | wal_bytes
-------------+---------+-----------
    44246365 |  608009 | 10126 MB
(1 row)

Наиболее детальная информация о вводе-выводе, связанном с журналом (в том числе и разбивка по типам процессов, хотя здесь я ее не показываю), содержится в уже знакомом представлении pg_stat_io: (v. 18) (с. 204)

=> SELECT context, writes, pg_size_pretty(write_bytes) write_bytes,
  pg_size_pretty(write_bytes/writes) bytes_per_write, fsyncs
FROM (
  SELECT context, sum(writes) writes, sum(write_bytes) write_bytes,
    sum(fsyncs) fsyncs
  FROM pg_stat_io
  WHERE object = 'wal'
  GROUP BY context
);
 context | writes | write_bytes | bytes_per_write | fsyncs
---------+--------+-------------+-----------------+--------
 normal  | 887267 | 10 GB       | 12 kB           |   3880
 init    |     85 | 1360 MB     | 16 MB           |     85
(2 rows)

Для журнала поле context может принимать следующие значения:

  • init — инициализация сегмента WAL (а также заполнение его нулями при установленном параметре wal_init_zero — отсюда и 16 Мбайт данных, записанных каждой операцией); (по умолчанию: on)

  • normal — операции с уже существующими сегментами.

Статистику отдельного обслуживающего процесса во время его работы можно получить функцией pg_stat_get_backend_wal. (v. 18)