# Режимы журнала

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<span id="internal-ru-chapter-11" class="pdf-anchor"></span><span id="internal-ru-page-235" class="pdf-anchor"></span>

<span id="internal-ru-dummy-88" class="pdf-anchor"></span>

## 11.1. Производительность {#internal-ru-section-11-1}

При обычной работе сервера происходит постоянная, но последовательная запись журнальных файлов. Поскольку случайный доступ практически отсутствует, с этой задачей вполне могут справляться и HDD-диски. Но такой характер нагрузки существенно отличается от того, как происходит доступ к файлам данных. Поэтому может быть выгодно разместить журнал на отдельном физическом носителе, создав вместо каталога PGDATA/pg\_wal символьную ссылку на каталог примонтированной файловой системы. (v. 17)

> [!NOTE]
> Есть пара ситуаций, при которых журнальные файлы необходимо не только писать, но и читать. Первая — понятный случай восстановления после сбоя. Вторая возникает при использовании потоковой репликации. Процесс walsender [^internal-ru-235-1] старается читать записи WAL из журнального кеша [^internal-ru-235-2], но если реплика не успевает получать журнальные записи, пока нужные страницы еще находятся в памяти основного сервера, данные будут прочитаны с диска. Однако и в этом случае доступ будет последовательным, а не случайным.

Запись журнала происходит в одном из двух режимов:

- синхронном — при фиксации транзакции продолжение работы невозможно, пока все журнальные записи об этой транзакции не окажутся на диске;

- асинхронном — транзакция завершается немедленно, а журнал записывается в фоновом режиме.

<span id="internal-ru-page-236" class="pdf-anchor"></span>

Режим устанавливается параметром *synchronous\_commit*. (*по умолчанию:* `on`)

<span id="internal-ru-dummy-89" class="pdf-anchor"></span>

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

Для этого обслуживающий процесс, завершающий транзакцию и записывающий журнал, может делать небольшую паузу, определяемую параметром *commit\_delay*. Но происходит это только в том случае, если в системе имеется не менее *commit\_siblings* активных транзакций [^internal-ru-236-1]: за время ожидания некоторые из них могут завершиться, и тогда удастся синхронизировать все журнальные записи за один прием. Это похоже на то, как вы придерживаете двери лифта, чтобы кто-то успел заскочить в кабину. (*по умолчанию:* `0s`) (*по умолчанию:* `5`)

Со значениями по умолчанию пауза не выдерживается. Изменять значение параметра *commit\_delay* имеет смысл только в системах, выполняющих большое количество коротких OLTP-транзакций.

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

С этого момента транзакция считается надежно завершенной [^internal-ru-236-2] — гарантируется долговечность (Durability из набора требований ACID). Поэтому синхронный режим используется по умолчанию.

Обратная сторона состоит в том, что синхронная запись увеличивает время отклика (команда `COMMIT` не возвращает управление до окончания синхронизации) и уменьшает производительность системы, особенно OLTP.

<span id="internal-ru-page-237" class="pdf-anchor"></span>

**Асинхронный режим.** Чтобы перейти на асинхронную запись[^internal-ru-237-1], нужно установить для параметра *synchronous\_commit* значение «off».

<span id="internal-ru-dummy-90" class="pdf-anchor"></span>

В асинхронном режиме сброс журнальных записей выполняет процесс walwriter[^internal-ru-237-2], чередуя циклы работы с ожиданием. Продолжительность пауз устанавливается параметром *wal\_writer\_delay*. (*по умолчанию:* `200ms`)

Проснувшись после очередной паузы, процесс проверяет, не появились ли в кеше новые *полностью* заполненные страницы WAL. Если появились, то процесс записывает их на диск, игнорируя текущую страницу. Если же с прошлого раза ни одна страница журнала не заполнилась до конца, процесс записывает текущую недозаполненную страницу, раз уж все равно проснулся[^internal-ru-237-3].

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

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

Синхронизация данных происходит через каждые *wal\_writer\_flush\_after* мегабайт и один раз в конце цикла записи. (*по умолчанию:* `1MB`)

Асинхронная запись эффективнее синхронной: фиксация изменений не ждет физической записи на диск. Однако надежность уменьшается: в случае сбоя зафиксированные данные могут пропасть, если после фиксации прошло менее 3 × *wal\_writer\_delay* единиц времени (что при настройке по умолчанию составляет 0,6 секунды).

<span id="internal-ru-page-238" class="pdf-anchor"></span>

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

В основном же непростой выбор — эффективность или долговечность — остается за проектировщиком системы.

Параметр *synchronous\_commit* можно устанавливать в том числе и для отдельных транзакций. Если на уровне приложения получается разделить транзакции на абсолютно критичные (например, работающие с финансовыми данными) и менее важные, то можно увеличить производительность, жертвуя надежностью только части транзакций.

Чтобы получить некоторое представление о том, какой выигрыш дает асинхронная фиксация, сравним время отклика и пропускную способность при двух режимах на эталонном тесте pgbench[^internal-ru-238-1].

Инициализируем необходимые таблицы и запустим 30-секундный тест в синхронном режиме:

```text {hl_lines="14 16"}
postgres$ /usr/local/pgsql/bin/pgbench -i internals
postgres$ /usr/local/pgsql/bin/pgbench -T 30 internals
pgbench (18.3)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 42850
number of failed transactions: 0 (0.000%)
latency average = 0.700 ms
initial connection time = 0.515 ms
tps = 1428.341142 (without initial connection time)
```

И затем проведем такой же тест в асинхронном режиме:

<span id="internal-ru-page-239" class="pdf-anchor"></span>

```text {label="psql" hl_lines="15 17"}
=> ALTER SYSTEM SET synchronous_commit = off;
=> SELECT pg_reload_conf();
postgres$ /usr/local/pgsql/bin/pgbench -T 30 internals
pgbench (18.3)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 68607
number of failed transactions: 0 (0.000%)
latency average = 0.437 ms
initial connection time = 0.710 ms
tps = 2286.926300 (without initial connection time)
```

При асинхронной фиксации в этом простом тесте время отклика (latency) существенно уменьшилось, а пропускная способность (tps) увеличилась. Разумеется, в каждой конкретной системе при конкретной нагрузке соотношение будет своим, но видно, что при коротких OLTP-транзакциях эффект может быть весьма значительным.

Восстановим значения параметров по умолчанию:

```text {label="psql"}
=> ALTER SYSTEM RESET synchronous_commit;
=> SELECT pg_reload_conf();
```

## 11.2. Надежность {#internal-ru-section-11-2}

Очевидно, что механизм журналирования должен гарантировать возможность восстановления после сбоя в любых ситуациях (не связанных, конечно, с разрушением носителя данных). На согласованность данных влияет много факторов, из которых наиболее важны кеширование, повреждение данных и неатомарность записи[^internal-ru-239-1].

<span id="internal-ru-page-240" class="pdf-anchor"></span><span id="internal-ru-section-star-162" class="pdf-anchor"></span>

### Кеширование {#internal-ru-subsection-star-163}

На пути данных к энергонезависимому хранилищу (такому как пластина жесткого диска) стоят многочисленные кеши.

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

Когда планировщик решает записать данные, они попадают в кеш накопителя (жесткого диска). Электроника накопителя тоже может отложить запись, например собирая данные в группы, которые выгоднее записать одновременно. RAID-контроллер добавляет еще один уровень кеширования между операционной системой и диском.

Если не предпринимать специальных мер, момент надежного сохранения данных остается неизвестным. Обычно это и не важно, поскольку есть журнал, но сами журнальные записи обязаны немедленно сохраняться надежным образом [^internal-ru-240-1]. Это верно и для асинхронного режима, поскольку иначе нельзя гарантировать, что журнальные записи попадут на диск раньше измененных данных.

Контрольная точка также должна сохранять данные надежно, чтобы грязные страницы действительно были сброшены на диск, а не остались в кеше операционной системы. Кроме того, контрольная точка синхронизирует с диском и все файловые операции, выполненные ранее другими процессами (такие как запись страницы или удаление файла): к завершению контрольной точки результаты всех этих действий тоже должны гарантированно оказаться на диске[^internal-ru-240-2].

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

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

<span id="internal-ru-page-241" class="pdf-anchor"></span>

Они сводятся к двум основным: либо после записи выполняется отдельная команда синхронизации (fsync, fdatasync), либо необходимость синхронизации (или даже прямой записи, минуя кеш операционной системы) указывается при открытии файла или записи в него.

Утилита pg\_test\_fsync помогает определить наиболее подходящий способ синхронизации журнала для конкретной операционной системы и конкретной файловой системы; выбранный способ указывается в параметре *wal\_sync\_method*. Для других ситуаций способ определяется автоматически[^internal-ru-241-1].

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

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

<span id="internal-ru-section-star-164" class="pdf-anchor"></span>

### Повреждение данных {#internal-ru-subsection-star-165}

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

Чтобы вовремя обнаружить возникшую проблему, журнальные записи всегда снабжаются контрольными суммами.

Страницы данных также можно защитить контрольными суммами[^internal-ru-241-2]. Это можно сделать либо при инициализации кластера, либо с помощью утилиты pg\_checksums [^internal-ru-241-3] при остановленном сервере[^internal-ru-241-4]. (v. 12)

<span id="internal-ru-page-242" class="pdf-anchor"></span>

В производственной среде контрольные суммы обязательно должны быть включены, несмотря на (незначительные) накладные расходы на их вычисление и проверку. Это увеличивает шансы обнаружить сбой вовремя. Но не дает полной гарантии:

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

- страница, заполненная нулями, считается корректной, поэтому если файловая система по ошибке «обнулит» файл, проверка не обнаружит проблему;

- контрольные суммы защищают только основной слой отношений, а остальные слои и все прочие файлы (например, статусы транзакций clog) ничем не защищены.

Убедимся, что контрольные суммы включены, проверив значение параметра *data\_checksums*, доступного только для чтения:

```text {label="psql"}
=> SHOW data_checksums;
 data_checksums
----------------
 on
(1 row)
```

Остановим сервер и обнулим несколько байтов в нулевой странице основного слоя таблицы:

```text {label="psql"}
=> SELECT pg_relation_filepath('wal');
 pg_relation_filepath
----------------------
 base/16391/16586
(1 row)
postgres$ pg_ctl stop
postgres$ dd if=/dev/zero of=/usr/local/pgsql/data/base/16391/16586 \
oflag=dsync conv=notrunc bs=1 count=8
8+0 records in
8+0 records out
8 bytes copied, 0.0036238 s, 2.2 kB/s
```

<span id="internal-ru-page-243" class="pdf-anchor"></span>

Снова запустим сервер:

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

В принципе, сервер можно было бы и не останавливать. Достаточно, чтобы страница записалась на диск и была вытеснена из кеша (иначе сервер будет продолжать работать с закешированной страницей). Но такой сценарий сложнее воспроизвести.

Теперь пробуем прочитать таблицу:

```text {label="psql"}
=> SELECT * FROM wal LIMIT 1;
ERROR:  invalid page in block 0 of relation "base/16391/16586"
```

Если данные невозможно восстановить из резервной копии, стоит хотя бы попытаться прочитать поврежденную страницу (естественно, с риском получить искаженную информацию). Для этого надо установить параметр *ignore\_checksum\_failure*: (*по умолчанию:* `off`)

```text {label="psql"}
=> SET ignore_checksum_failure = on;
=> SELECT * FROM wal LIMIT 1;
WARNING:  ignoring checksum failure in block 0 of relation
"base/16391/16586"
 id
----
  2
(1 row)
```

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

<span id="internal-ru-section-star-166" class="pdf-anchor"></span>

### Неатомарность записи {#internal-ru-subsection-star-167}

Страница базы данных обычно занимает 8 Кбайт, а на низком уровне запись происходит блоками, которые в большинстве случаев имеют меньший размер (как правило, 512 байт или 4 Кбайта). Поэтому при сбое страница данных может записаться частично. При восстановлении бессмысленно применять к такой поврежденной странице обычные журнальные записи.

<span id="internal-ru-page-244" class="pdf-anchor"></span>

Для защиты от частичной записи PostgreSQL сохраняет в журнале полный образ страницы (full page image, FPI) при первом ее изменении после начала контрольной точки. (*с.* [223](/ru/10-write-ahead-log/#internal-ru-page-224))

Этим поведением управляет параметр *full\_page\_writes*. Отказ от записи полных образов может привести к неустранимому повреждению данных, но полные образы могут появляться в журнале даже при отключенном параметре из-за битов-подсказок. Их изменения считаются некритичными и не журналируются сами по себе — первый же запрос, обратившийся к странице, заново установит необходимые биты. Однако изменение любого, даже несущественного бита приводит к изменению контрольной суммы. Поэтому при включенных контрольных суммах (или при установленном параметре *wal\_log\_hints*) первое изменение битов-подсказок после контрольной точки вызывает запись полного образа страницы[^internal-ru-244-1], невзирая на значение *full\_page\_writes*. (*по умолчанию:* `on`) (*с.* [86](/ru/03-pages-and-tuples/#internal-ru-page-87)) (*по умолчанию:* `off`)

Если при восстановлении в журнале встречается полный образ страницы, он безусловно (без проверки LSN) записывается на диск, поскольку, как и всякая журнальная запись, он защищен контрольной суммой и не может быть незаметно испорчен. И уже к этому гарантированно корректному образу применяются обычные журнальные записи.

Наличие полных образов существенно увеличивает объем генерируемых журнальных записей, несмотря на то что механизм журналирования исключает из полного образа незанятое место внутри страницы[^internal-ru-244-2]. Ситуацию можно значительно улучшить за счет сжатия полных образов, которое включается параметром *wal\_compression*. (*по умолчанию:* `off`)

Проведем простой эксперимент с помощью утилиты pgbench. Выполним контрольную точку и сразу же запустим тест с фиксированным количеством транзакций:

<span id="internal-ru-page-245" class="pdf-anchor"></span>

```text {label="psql"}
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/87C95BC8
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8928F088
(1 row)
```

Размер журнальных записей:

```text {label="psql"}
=> SELECT pg_size_pretty('2/8928F088'::pg_lsn - '2/87C95BC8'::pg_lsn);
 pg_size_pretty
----------------
 22 MB
(1 row)
```

Полные образы в нашем случае составляют значительную часть сгенерированного объема, в чем можно убедиться, посмотрев статистику утилитой pg\_waldump с ключом `--stats` или функцией `pg_get_wal_stats` расширения `pg_walinspect`. (v. 15)

Таблица показывает количество журнальных записей (`N`), объем обычных записей (`Record size`) и объем полных образов (`FPI size`) для каждого типа ресурсов (`Type`):

```text {hl_lines="14"}
postgres$ /usr/local/pgsql/bin/pg_waldump --stats \
-p /usr/local/pgsql/data/pg_wal -s 2/87C95BC8 -e 2/8928F088
WAL statistics between 2/87C95BC8 and 2/8928F088:
Type            N      (%)   Record size      (%)   FPI size      (%)
----            -      ---   -----------      ---   --------      ---
XLOG         1810 (  1.48)         88690 (  1.11)   14572848 ( 98.95)
Transaction 20001 ( 16.38)        680114 (  8.50)          0 (  0.00)
Storage         1 (  0.00)            42 (  0.00)          0 (  0.00)
Standby         3 (  0.00)           222 (  0.00)          0 (  0.00)
Heap2       20233 ( 16.57)       1317150 ( 16.47)      16384 (  0.11)
Heap        80015 ( 65.54)       5911923 ( 73.91)      77844 (  0.53)
Btree          15 (  0.01)           888 (  0.01)      59920 (  0.41)
           ------               --------            --------
Total      122078                7999029 [35.20%]   14726996 [64.80%]
```

<span id="internal-ru-page-246" class="pdf-anchor"></span><span id="internal-ru-dummy-91" class="pdf-anchor"></span>

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

Теперь посмотрим, как помогает сжатие, на том же примере:

```text {label="psql"}
=> ALTER SYSTEM SET wal_compression = on; -- или pglz
=> SELECT pg_reload_conf();
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8928F158
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/89D256F8
(1 row)
```

Размер журнальных записей со сжатием:

```text {label="psql"}
=> SELECT pg_size_pretty('2/89D256F8'::pg_lsn - '2/8928F158'::pg_lsn);
 pg_size_pretty
----------------
 11 MB
(1 row)
postgres$ /usr/local/pgsql/bin/pg_waldump --stats \
-p /usr/local/pgsql/data/pg_wal -s 2/8928F158 -e 2/89D256F8
WAL statistics between 2/8928F158 and 2/89D256F8:
Type            N      (%)   Record size      (%)   FPI size      (%)
----            -      ---   -----------      ---   --------      ---
XLOG         1793 (  1.47)         91399 (  1.14)    2792815 ( 98.79)
Transaction 20001 ( 16.39)        680114 (  8.50)          0 (  0.00)
Storage         1 (  0.00)            42 (  0.00)          0 (  0.00)
CLOG            1 (  0.00)            34 (  0.00)          0 (  0.00)
Standby         4 (  0.00)           276 (  0.00)          0 (  0.00)
Heap2       20227 ( 16.57)       1320696 ( 16.50)        255 (  0.01)
Heap        80005 ( 65.56)       5909689 ( 73.85)      17798 (  0.63)
Btree           5 (  0.00)           275 (  0.00)      16283 (  0.58)
           ------               --------            --------
Total      122037                8002525 [73.89%]    2827151 [26.11%]
```

<span id="internal-ru-page-247" class="pdf-anchor"></span>

Помимо традиционного алгоритма сжатия PGLZ, доступны два других: LZ4 и Zstandard. Алгоритм LZ4 обычно требует меньше ресурсов процессора, показывая примерно ту же степень сжатия. Алгоритм Zstandard сочетает словарное сжатие с энтропийным кодированием; он сильнее нагружает процессор, но сжимает данные лучше. (v. 15)

```text {label="psql"}
=> ALTER SYSTEM SET wal_compression = zstd;
=> SELECT pg_reload_conf();
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/89D257C8
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8A6C6378
(1 row)
```

Размер журнальных записей со сжатием Zstandard:

```text {label="psql"}
=> SELECT pg_size_pretty('2/8A6C6378'::pg_lsn - '2/89D257C8'::pg_lsn);
 pg_size_pretty
----------------
 9859 kB
(1 row)
```

Вывод: при наличии большого числа полных образов страниц (из-за контрольных сумм или *full\_page\_writes*, то есть почти всегда) имеет смысл воспользоваться сжатием. Несколько алгоритмов позволяют найти компромисс между дополнительной нагрузкой на процессор и сокращением объема журнальных записей.

## 11.3. Уровни журнала {#internal-ru-section-11-3}

<span id="internal-ru-page-248" class="pdf-anchor"></span>

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

Используемый уровень задается параметром *wal\_level*; его изменение требует перезапуска сервера. (*по умолчанию:* `replica`)

<span id="internal-ru-section-star-168" class="pdf-anchor"></span>

### Minimal {#internal-ru-subsection-star-169}

Начальный уровень minimal гарантирует только восстановление после сбоя. Для экономии места операции с отношениями, созданными или опустошенными в текущей транзакции, не записываются в журнал, если они связаны со вставкой большого объема данных (в частности, это касается таких команд, как `CREATE TABLE AS SELECT` или `CREATE INDEX`)[^internal-ru-248-1]. Вместо журналирования необходимые данные сразу же сбрасываются на диск, а изменения в системном каталоге становятся видимыми при фиксации транзакции.

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

Объем данных, который требуется записать в только что созданное отношение, чтобы такая оптимизация начала работать, определяется значением параметра *wal\_skip\_threshold*. (v. 13) (*по умолчанию:* `2MB`)

Посмотрим, что попадает в журнал на уровне minimal.

По умолчанию используется более высокий уровень replica, рассчитанный на поддержку репликации. При установке минимального уровня потребуется также обнулить значение параметра *max\_wal\_senders*, который определяет допустимое количество процессов walsender: (*по умолчанию:* `10`)

```text {label="psql"}
=> ALTER SYSTEM SET wal_level = minimal;
=> ALTER SYSTEM SET max_wal_senders = 0;
```

<span id="internal-ru-page-249" class="pdf-anchor"></span>

Изменение параметров требует перезапуска сервера:

```bash
postgres$ pg_ctl restart -l /home/postgres/logfile
```

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

```text {label="psql"}
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8A6C82B0
(1 row)
```

Опустошим таблицу и в той же транзакции вставим новые строки, превысив предел *wal\_skip\_threshold*:

```text {label="psql"}
=> BEGIN;
=> TRUNCATE TABLE wal;
=> INSERT INTO wal
     SELECT id FROM generate_series(1,100_000) id;
=> COMMIT;
```

> [!NOTE]
> Я использую команду `TRUNCATE`, а не создаю отдельную таблицу, поскольку при этом генерируется меньше журнальных записей.

С помощью уже знакомого расширения `pg_walinspect` посмотрим содержимое журнала. (v. 15)

```text {label="psql"}
=> SELECT start_lsn,
  resource_manager AS rmgr,
  record_type,
  (regexp_match(block_ref, '[0-9]+\/[0-9]+\/[0-9]+'))[1] AS rel
FROM -- pg_get_wal_records_info_till_end_of_wal до v.16
        pg_get_wal_records_info('2/8A6C82B0','FFFFFFFF/FFFFFFFF');
 start_lsn  |    rmgr     | record_type |       rel
------------+-------------+-------------+-----------------
 2/8A6C82B0 | Storage     | CREATE      |
 2/8A6C82E0 | Heap        | UPDATE      | 1663/16391/1259
 2/8A6C8360 | Btree       | INSERT_LEAF | 1663/16391/2662
 2/8A6C83A0 | Btree       | INSERT_LEAF | 1663/16391/2663
 2/8A6C83E0 | Btree       | INSERT_LEAF | 1663/16391/3455
 2/8A6C8420 | Transaction | COMMIT      |
(6 rows)
```

<span id="internal-ru-page-250" class="pdf-anchor"></span>

Первая запись (`CREATE`) говорит о создании нового файла для отношения (поскольку при опустошении таблица фактически перезаписывается). (*с.* [177](/ru/08-rebuilding-tables-and-indexes/#internal-ru-page-178))

Далее следуют несколько записей, относящихся к системному каталогу. Они отражают изменение таблицы `pg_class` (`UPDATE`) и добавление записей к трем индексам, построенным на этой таблице (`INSERT_LEAF`). В этом можно убедиться, сопоставив путь к файлу, показанный в столбце `rel`, с именем отношения. (*с.* [30](/ru/01-introduction/#internal-ru-page-31))

Последней идет запись о фиксации транзакции. Вставка данных в таблицу не журналируется.

<span id="internal-ru-section-star-170" class="pdf-anchor"></span>

### Replica {#internal-ru-subsection-star-171}

Когда система восстанавливается после сбоя, данные, сохраненные на диске, обновляются до согласованного состояния с помощью журнальных записей. То же самое происходит и при восстановлении из резервной копии. Но в этом случае могут использоваться заархивированные журнальные записи, чтобы не просто восстановить согласованность, а довести состояние данных до целевой точки восстановления. Количество таких записей может быть весьма велико (например, они могут охватывать несколько дней), то есть период восстановления будет содержать не одну контрольную точку, а множество. Поэтому минимального уровня журнала недостаточно — нельзя повторить операцию, если она не журналируется. Для возможности восстановления из резервной копии в журнал должны попадать *все* операции.

То же самое верно и для репликации: все, что не журналируется, не будет передано на реплику и не будет на ней воспроизведено.

<span id="internal-ru-dummy-92" class="pdf-anchor"></span>

Но если реплика используется для выполнения запросов, все еще больше усложняется. Во-первых, нужна информация об исключительных блокировках, возникающих на основном сервере, поскольку они могут конфликтовать с запросами на реплике. Во-вторых, нужно уметь строить снимки данных, а для этого необходима информация о выполняющихся транзакциях. В случае реплики нужно учитывать не только локальные транзакции, но и транзакции на основном сервере. (*с.* [261](/ru/12-relation-level-locks/#internal-ru-page-262)) (*с.* [97](/ru/04-snapshots/#internal-ru-page-98))

<span id="internal-ru-page-251" class="pdf-anchor"></span>

Единственный способ передать эту информацию реплике — периодически записывать ее в журнал[^internal-ru-251-1]. Этим занимается процесс фоновой записи bgwriter [^internal-ru-251-2]: при наличии активности в системе он раз в 15 секунд (интервал не настраивается) создает запись типа `RUNNING_XACTS`.

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

Именно этот уровень используется по умолчанию, поэтому просто сбросим установленные выше параметры и перезагрузим сервер:

```text {label="psql"}
=> ALTER SYSTEM RESET wal_level;
=> ALTER SYSTEM RESET max_wal_senders;
```

```bash
postgres$ pg_ctl restart -l /home/postgres/logfile
```

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

```text {label="psql"}
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8ACE9650
(1 row)
```

```text {label="psql"}
=> BEGIN;
=> TRUNCATE TABLE wal;
=> INSERT INTO wal VALUES (42);
=> COMMIT;
```

Теперь проверим журнальные записи.

<span id="internal-ru-page-252" class="pdf-anchor"></span>

```text {label="psql" hl_lines="9 15 16 17"}
=> SELECT start_lsn,
  resource_manager AS rmgr,
  record_type,
  (regexp_match(block_ref, '[0-9]+\/[0-9]+\/[0-9]+'))[1] AS rel
FROM pg_get_wal_records_info('2/8ACE9650','FFFFFFFF/FFFFFFFF');
 start_lsn  |    rmgr     |   record_type   |       rel
------------+-------------+-----------------+------------------
 2/8ACE9650 | Heap2       | PRUNE_ON_ACCESS | 1663/16391/1259
 2/8ACE9688 | Standby     | LOCK            |
 2/8ACE96B8 | Storage     | CREATE          |
 2/8ACE96E8 | Heap        | UPDATE          | 1663/16391/1259
 2/8ACE9768 | Btree       | INSERT_LEAF     | 1663/16391/2662
 2/8ACE97A8 | Btree       | INSERT_LEAF     | 1663/16391/2663
 2/8ACE97E8 | Btree       | INSERT_LEAF     | 1663/16391/3455
 2/8ACE9828 | Heap        | INSERT+INIT     | 1663/16391/24814
 2/8ACE9868 | Standby     | LOCK            |
 2/8ACE9898 | Standby     | RUNNING_XACTS   |
 2/8ACE98D0 | Transaction | COMMIT          |
(11 rows)
```

К записям, которые появляются на уровне minimal, добавились:

- записи менеджера ресурсов Standby, связанные с репликацией: `LOCK` (блокировки) и `RUNNING_XACTS` (активные транзакции);

- запись об инициализации новой страницы и одновременной вставке в нее строки `INSERT+INIT`.

<span id="internal-ru-section-star-172" class="pdf-anchor"></span>

### Logical {#internal-ru-subsection-star-173}

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

С точки зрения журнальных записей этот уровень практически не отличается от уровня replica — в имеющиеся записи добавляются идентификаторы строк [^internal-ru-252-1]; появляются записи, связанные с источниками репликации и аннулированием кеша, а также произвольные логические записи, которые могут добавлять приложения. В основном же логическое декодирование зависит от информации о выполняющихся транзакциях (`RUNNING_XACTS`): она позволяет строить снимки данных, чтобы отслеживать изменения системного каталога.

<span id="internal-ru-page-253" class="pdf-anchor"></span>

[^internal-ru-235-1]: [backend/replication/walsender.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/replication/walsender.c;hb=REL_18_STABLE>).
[^internal-ru-235-2]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция WALReadFromBuffers.
[^internal-ru-236-1]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция XLogFlush.
[^internal-ru-236-2]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция RecordTransactionCommit.
[^internal-ru-237-1]: [postgrespro.ru/docs/postgresql/18/wal-async-commit](<https://postgrespro.ru/docs/postgresql/18/wal-async-commit>).
[^internal-ru-237-2]: [backend/postmaster/walwriter.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/walwriter.c;hb=REL_18_STABLE>).
[^internal-ru-237-3]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция XLogBackgroundFlush.
[^internal-ru-238-1]: [postgrespro.ru/docs/postgresql/18/pgbench](<https://postgrespro.ru/docs/postgresql/18/pgbench>).
[^internal-ru-239-1]: [postgrespro.ru/docs/postgresql/18/wal-reliability](<https://postgrespro.ru/docs/postgresql/18/wal-reliability>).
[^internal-ru-240-1]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция issue\_xlog\_fsync.
[^internal-ru-240-2]: [backend/storage/sync/sync.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/sync/sync.c;hb=REL_18_STABLE>).
[^internal-ru-241-1]: [backend/storage/file/fd.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/file/fd.c;hb=REL_18_STABLE>), функция pg\_fsync.
[^internal-ru-241-2]: [backend/storage/page/README](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/page/README;hb=REL_18_STABLE>).
[^internal-ru-241-3]: [postgrespro.ru/docs/postgresql/18/app-pgchecksums](<https://postgrespro.ru/docs/postgresql/18/app-pgchecksums>).
[^internal-ru-241-4]: [commitfest.postgresql.org/51/5323](<https://commitfest.postgresql.org/51/5323>).
[^internal-ru-244-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция MarkBufferDirtyHint.
[^internal-ru-244-2]: [backend/access/transam/xloginsert.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xloginsert.c;hb=REL_18_STABLE>), функция XLogRecordAssemble.
[^internal-ru-248-1]: [include/utils/rel.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/utils/rel.h;hb=REL_18_STABLE>), макрос RelationNeedsWAL.
[^internal-ru-251-1]: [backend/storage/ipc/standby](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/standby;hb=REL_18_STABLE>), функция LogStandbySnapshot.
[^internal-ru-251-2]: [backend/postmaster/bgwriter.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/bgwriter.c;hb=REL_18_STABLE>).
[^internal-ru-252-1]: [postgrespro.ru/docs/postgresql/18/sql-altertable#SQL-ALTERTABLE-REPLICA-IDENTITY](<https://postgrespro.ru/docs/postgresql/18/sql-altertable#SQL-ALTERTABLE-REPLICA-IDENTITY>).
