WAL MODES

当数据库服务正常运行时,wal文件持续不断的写入磁盘。这种写入时顺序的(sequential):几乎没有随机访问,所以即便时hdd硬盘也能处理这样的任务。由于这种负载和典型的数据文件访问非常不一样,值得为WAL文件设置单独的物理存储,并通过符号链接替换PGDATA/PG_WAL目录,该目录链接到mount的文件系统中的目录。

在某些情况下,需要同时写和读取WAL文件。第一种情况是崩溃恢复。第二个是流复制。 Walsender流程直接从文件中读取WALENTRIES。因此,如果必需的页面仍位于主服务器的OS缓冲区中(不在shared_buffer中),replica未接收WAL条目,则必须从磁盘中读取数据。但是访问仍然是顺序而不是随机的。

wal日志条目写入有以下两种模式:

  • 同步模式:在事务提交之前,必须把所有相关的 WAL 记录写入磁盘,否则不允许继续执行后续操作。
  • 异步模式:事务提交会立刻返回成功,相关的 WAL 记录则由后台进程稍后再写入磁盘。

当前使用的模式由参数:synchronous_commit 确定

同步模式:为了可靠地记录一次提交,仅仅将WAL条目传递给操作系统是不够的;你必须确保磁盘同步已经成功完成。由于同步涉及实际的I/O操作(这相当慢),因此最好尽可能少地执行它。

为此,完成事务并将WAL条目写入磁盘的后端可以进行一次小的暂停,该暂停由commit_delay参数定义。但是,只有当系统中至少有commit_siblings个活跃事务时,这种情况才会发生:在这次暂停期间,其中一些事务可能会完成,而服务器将设法一次性同步所有WAL条目。这很像为某人赶进来而按住电梯门。

默认情况下,没有暂停。仅针对执行大量短时OLTP事务的数据库系统修改commit_delay参数是有意义的。

在可能出现的暂停之后,完成事务的进程会将所有累积的 WAL 条目刷新到磁盘并执行同步操作(关键是要保存提交记录以及与该事务相关的所有前置记录;至于其他记录,则只是顺便写入,因为它们不会增加额外开销)。

从这一刻起,ACID 的持久性要求便得到保证——事务被认为已经可靠提交。这就是为什么默认使用同步模式的原因。

同步提交的缺点在于延迟更长(在同步完成之前,COMMIT 命令不会返回控制权),并且系统吞吐量较低,尤其对于 OLTP loads。

异步模式:要启用异步模式,必须关闭 synchronous_commit 参数。
在异步模式下,WAL 条目由 walwriter 进程写入磁盘,该进程在“工作—休眠”之间交替运行。休眠时长由 wal_writer_delay 参数决定(默认 200ms)。

当 walwriter 从休眠中唤醒时,它会检查缓存中是否存在新的、已经完全填满的 WAL 页面。如果存在,就将这些页面写入磁盘,同时跳过当前未写满的页面;否则,它会写入当前半空的页面,因为既然已经被唤醒了。

这种算法的目的在于避免同一个页面被多次刷盘,这在数据变更频繁的负载下能带来显著的性能提升。

虽然 WAL 缓存采用环形缓冲区(ring buffer)的形式,但 walwriter 在到达缓存的最后一页时会停止;在经过一次休眠后,下一轮写入循环会从缓存的第一页重新开始。因此,在最坏的情况下,walwriter 可能需要 三次循环才能处理某个特定的 WAL 记录:

第一次,它会写出缓存尾部的所有已填满页面;(当前位置之后的所有满块)
第二次,它回到开头;(当前位置之前的所有满块)
第三次,才会处理包含目标记录的那个未填满页面。
不过,在大多数情况下,只需要 一到两次循环 就能完成。

每当写入的数据量达到 wal_writer_flush_after 时,就会执行一次同步操作;在写入循环结束时,也会再次进行同步。

与同步提交相比,异步提交更快,因为它不需要等待物理写入磁盘完成。但可靠性有所下降:在发生故障前的 3 × wal_writer_delay 时间内提交的数据可能会丢失(默认值为 0.6 秒)

在实际应用中,这两种模式是互补的。

  • 同步模式 下,与长事务相关的 WAL 条目仍然可以 异步写入,以释放 WAL 缓冲区。
  • 反之,即使在 异步模式 下,如果某个 WAL 条目所在的页即将被 从缓冲区淘汰,该条目也会被 立即刷盘,否则系统无法继续正常操作。
    在大多数情况下,系统设计者必须在 性能和持久性之间做出权衡。

synchronous_commit 参数也可以针对特定事务进行设置。如果能够在应用层将所有事务分类为 绝对关键(如处理财务数据)或 非关键,就可以在只冒非关键事务丢失风险的前提下,提升整体性能

为了了解 异步提交 可能带来的性能提升,我们可以通过 pgbench 测试,比较两种模式下的 延迟(latency)和吞吐量(throughput)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
postgres@lavm-bar1guved6:/root$ pgbench -i test
dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 1.54 s (drop tables 0.11 s, create tables 0.49 s, client-side generate 0.54 s, vacuum 0.18 s, primary keys 0.23 s).

postgres@lavm-bar1guved6:/root$ pgbench -T 30 test
pgbench (19devel)
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: 3522
number of failed transactions: 0 (0.000%)
latency average = <strong>8.518</strong> ms
initial connection time = 4.025 ms
tps = <strong>117.400485</strong> (without initial connection time)

修改参数后跑异步模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
postgres@lavm-bar1guved6:/root$ psql test
psql (19devel)
Type "help" for help.

test=# ALTER SYSTEM SET synchronous_commit = off;
ALTER SYSTEM
test=# SELECT pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)

postgres@lavm-bar1guved6:/root$ pgbench -T 30 test
pgbench (19devel)
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: 9510
number of failed transactions: 0 (0.000%)
latency average = <strong>3.154</strong> ms
initial connection time = 4.383 ms
tps = <strong>317.038330</strong> (without initial connection time)

异步模式 下,这个简单的基准测试显示出 显著更低的延迟(latency)和更高的吞吐量(tps)。当然,每个具体系统的数值会根据当前负载有所不同,但可以清楚地看出,对于 短事务,性能提升是相当明显的。

恢复参数:

1
2
3
4
5
6
7
8
9
10
11
postgres@lavm-bar1guved6:/root$ psql test
psql (19devel)
Type "help" for help.

test=# ALTER SYSTEM RESET synchronous_commit;
ALTER SYSTEM
test=# SELECT pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals