# 日志模式

LLMS 索引： [llms.txt](/zh18/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]会尽量从日志缓存[^internal-ru-235-2]读取 WAL 记录；但如果副本来不及接收日志记录，而所需页面仍在主服务器内存中，数据就会从磁盘读取。不过即使在这种情况下，访问仍然是顺序的，而不是随机的。

日志写入有两种模式：

- 同步模式——提交事务时，只有等该事务的所有日志记录都写入磁盘后，才能继续工作；

- 异步模式——事务立即完成，日志在后台写入。

<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`)

使用默认值时不会发生暂停。只有在系统执行大量短 OLTP 事务时，修改 *commit\_delay* 参数才有意义。

可能的暂停结束后，负责完成事务的进程会把积累的所有日志记录刷到磁盘，并执行同步（重要的是，磁盘上必须有提交记录以及此前与该事务有关的所有记录；其余记录被写入，只是因为这样不会增加成本）。

从这一刻起，事务就被视为可靠完成[^internal-ru-236-2]，并保证满足 ACID 要求中的持久性（Durability）。因此，同步模式默认启用。

同步写入的代价是响应时间变长（`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* MB 数据后都会同步一次，并在写入周期结束时再同步一次。（*默认值：* `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)
```

停止服务器，并把表的主分支的第 0 页中的几个字节清零：

```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 KB，但底层写入按块进行，而块在多数情况下更小（通常是 512 字节或 4 KB）。因此发生故障时，数据页面可能只被部分写入。恢复时，对这样的损坏页面应用普通日志记录没有意义。

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

为了防止部分写入，PostgreSQL 会在检查点开始后第一次修改页面时把整页镜像（full page image，FPI）保存到日志中。（*第 [223](/zh18/10-write-ahead-log/#internal-ru-page-224) 页*）

这一行为由 *full\_page\_writes* 参数控制。关闭整页镜像可能导致无法修复的数据损坏，但即使关闭该参数，提示位变化也可能让日志中出现整页镜像。提示位的变化本身被认为不重要，因此不会单独记录；访问页面的第一个查询会重新设置所需的位。然而，任何位（即使是不重要的位）发生变化都会改变校验和。因此，启用校验和（或设置 *wal\_log\_hints* 参数）时，检查点之后第一次修改提示位会记录整页镜像[^internal-ru-244-1]，不受 *full\_page\_writes* 的值影响。（*默认值：* `on`）（*第 [86](/zh18/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_walinspect` 扩展的 `pg_get_wal_stats` 函数查看统计信息。（v. 15）

表中列出了每种资源类型（`Type`）的日志记录数量（`N`）、普通记录大小（`Record size`）和整页镜像大小（`FPI size`）：

```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 通常占用较少的 CPU 资源，但压缩率大致相同。Zstandard 把字典压缩与熵编码结合起来；它对 CPU 的负担更重，但压缩效果更好。（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*，这几乎总是如此），使用压缩很有意义。多种算法可以让我们在额外 CPU 负载和日志量减少之间找到折中。

## 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 级别来支持复制。设置 minimal 级别时，还必须把 *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](/zh18/08-rebuilding-tables-and-indexes/#internal-ru-page-178) 页*）

接下来是几条与系统目录有关的记录。它们反映了 `pg_class` 表的变化（`UPDATE`），以及向该表上三个索引中添加记录（`INSERT_LEAF`）。把 `rel` 列显示的文件路径与关系名称对应起来，就可以验证这一点。（*第 [30](/zh18/01-introduction/#internal-ru-page-31) 页*）

最后是事务提交记录。向表中插入数据不会被记录到日志。

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

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

系统发生故障后恢复时，会通过日志记录把磁盘上的数据更新到一致状态。从备份恢复时也是如此。但在这种情况下，还可以使用归档日志，把数据状态推进到目标恢复点，而不只是恢复一致性。这些记录的数量可能很大（例如覆盖数天），恢复期间会经过多个检查点，而不是一个。因此 minimal 日志级别不够：没有记录的操作无法重放。要支持从备份恢复，日志中必须包含*所有*操作。

复制也是如此：没有记录到日志中的内容不会传送到副本，也不会在副本上重放。

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

如果副本还要用于执行查询，事情会更加复杂。首先，需要知道主服务器上出现的排他锁，因为它们可能与副本上的查询冲突。其次，需要能够构建数据快照，这需要知道正在运行的事务。对副本来说，不仅要考虑本地事务，还要考虑主服务器上的事务。（*第 [261](/zh18/12-relation-level-locks/#internal-ru-page-262) 页）（*第 [97](/zh18/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>).
