|
| 1 | +# How to reduce WAL generation rates |
| 2 | + |
| 3 | +> 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! |
| 4 | +
|
| 5 | +在一个快速增长的项目中,其中一个重要优化方向便是减少生成的 WAL (预写日志) 的数量。 |
| 6 | + |
| 7 | +## 为什么这很重要 |
| 8 | + |
| 9 | +WAL 是 PostgreSQL 的核心机制,用于在发生故障时进行恢复、备份和复制。 |
| 10 | + |
| 11 | +每秒生成的 WAL 数据越多,意味着每秒需要复制和备份的数据量也越大,因此各种风险也可能会增加: |
| 12 | +复制延迟、WAL 归档延迟以及发生故障后,恢复时间也会增加。 |
| 13 | + |
| 14 | +## 如何衡量 WAL 生成速率 |
| 15 | + |
| 16 | +当新事务创建一条 WAL 记录时,它会分配一个 LSN (日志序列号)。监控当前 LSN 的位置非常简单: |
| 17 | + |
| 18 | +```sql |
| 19 | +select pg_current_wal_lsn(); |
| 20 | +``` |
| 21 | + |
| 22 | +在任何 PostgreSQL 监控中都应该包含这个指标。 |
| 23 | + |
| 24 | +两个 LSN 之间的差值就是这段时间内生成的字节数,Postgres 可以执行相关的计算 — 使用 `pg_lsn` 数据类型: |
| 25 | + |
| 26 | +```sql |
| 27 | +nik=# select pg_size_pretty('3/ED5F1E0'::pg_lsn - '0/110A1E0'); |
| 28 | + pg_size_pretty |
| 29 | +---------------- |
| 30 | + 12 GB |
| 31 | +(1 row) |
| 32 | +``` |
| 33 | + |
| 34 | +如果监控没有此功能,你可以通过查看以下内容了解每小时或每天生成了多少 WAL 数据: |
| 35 | + |
| 36 | +- `pg_wal` 目录中的 WAL 文件名 |
| 37 | +- 检查备份 (例如,检查 WAL-G 创建的两个完整备份的名称:`wal-g backup-list --detail`) |
| 38 | + |
| 39 | +这两种方法都应该可以帮助你获取与两个遥远时间点相对应的两个 LSN 值。 |
| 40 | + |
| 41 | +要了解更多细节,参照 [Day 9: How to understand the LSN values and WAL file name](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0009_lsn_values_and_wal_filenames.md) |
| 42 | + |
| 43 | +## 查询分析中的 WAL 指标 |
| 44 | + |
| 45 | +PostgreSQL 13 及以后的版本中,`pg_stat_statements` 和 `EXPLAIN` 都能提供与 WAL 相关的指标: |
| 46 | + |
| 47 | +1. `pg_stat_statements`:`wal_records`、`wal_fpi`、`wal_bytes` 等指标([docs](https://postgresql.org/docs/current/pgstatstatements.html))。一个简单的分析例子: |
| 48 | + |
| 49 | +```sql |
| 50 | +with time_period(delta_sec) as ( |
| 51 | + select extract(epoch from now() - stats_reset) |
| 52 | + from pg_stat_statements_info |
| 53 | +) |
| 54 | +select |
| 55 | + now(), |
| 56 | + delta_sec, |
| 57 | + round(wal_bytes / delta_sec) as wal_bytes_per_sec, |
| 58 | + round(wal_bytes / calls) as wal_bytes_per_call, |
| 59 | + queryid |
| 60 | +from |
| 61 | + pg_stat_statements, |
| 62 | + time_period |
| 63 | +order by wal_bytes desc |
| 64 | +limit 25; |
| 65 | +``` |
| 66 | + |
| 67 | +2. `EXPLAIN`:使用 `explain (analyze, buffers, wal)` 来查看执行计划中的 WAL 指标: |
| 68 | + |
| 69 | +```sql |
| 70 | +nik=# explain (analyze, buffers, wal) insert into t select i from generate_series(1, 100000) as i; |
| 71 | + QUERY PLAN |
| 72 | +------------------------------------------------------------------------------------------------------------------------------------- |
| 73 | + Insert on t (cost=0.00..1000.00 rows=0 width=0) (actual time=159.378..159.378 rows=0 loops=1) |
| 74 | + Buffers: shared hit=100895 dirtied=442 written=442 |
| 75 | + WAL: records=100000 fpi=1 bytes=5900343 |
| 76 | + -> Function Scan on generate_series i (cost=0.00..1000.00 rows=100000 width=4) (actual time=26.179..30.696 rows=100000 loops=1) |
| 77 | + Planning Time: 1.945 ms |
| 78 | + Execution Time: 160.483 ms |
| 79 | +(6 rows) |
| 80 | +``` |
| 81 | + |
| 82 | +## 全页写 |
| 83 | + |
| 84 | +pg_stat_statements 和 EXPLAIN 结果中的 "fpi" 指标表示发生了多少次全页镜像 (全页写)。 |
| 85 | + |
| 86 | +如果配置参数 `full_page_write` 为 on (默认情况下也是如此;通过 `show full_page_writes;` 检查),则在每个检查点之后,页面中的第一次更改会导致整个页面被写入 WAL 中。默认情况下,页面大小为 8 KiB,大多数 Postgres 安装中都是如此 (通过 `show block_size` 检查)。这意味着,如果只有很小一部分页面发生更改,在检查点之后仍需要写入整个页面。同一页面中的后续写入将正常进行 (只有更改会记录到 WAL 中),但是一旦发生新的检查点,那么需要再次先进行新的全页写。 |
| 87 | + |
| 88 | +更多信息可以参考以下文档: |
| 89 | + |
| 90 | +- Hironobu Suzuki 的 "The Internals of PostgreSQL"。第 9 章 "Write Ahead Logging – WAL",[9.1.3. Full-Page Writes](https://interdb.jp/pg/pgsql09.html#_9.1.3) |
| 91 | +- Egor Rogov 的 "PostgreSQL 14 internals","10.4 Recovery" 章节 |
| 92 | +- Postgres wiki:[Full page writes](https://wiki.postgresql.org/wiki/Full_page_writes) |
| 93 | +- Tomas Vondra, [On the impact of full-page writes (2016)](https://2ndquadrant.com/en/blog/on-the-impact-of-full-page-writes/) |
| 94 | + |
| 95 | +## 优化思想 |
| 96 | + |
| 97 | +以下是一些减少 WAL 生成量的优化建议: |
| 98 | + |
| 99 | +1. **检查点优化:增加检查点间隔** |
| 100 | + |
| 101 | + 增加检查点之间的间隔有两个好处,特别是当工作负载包含许多随机写入 (不是连续的,比如在使用 `COPY` 进行大量数据加载时)时: |
| 102 | + |
| 103 | + - 更少的全页写 |
| 104 | + - 减少对同一缓冲区的重复刷新 (刷新后,可能会由于新的写入而很快再次变脏)。 |
| 105 | + |
| 106 | + 为了增加间隔,我们只需要增加 `max_wal_size` (默认 `1GB`) 和 `checkpoint_timeout` (默认 5 分钟)。但这需要权衡利弊:检查点之间的间隔越大,意味着在各种情况下需要重放更多的 WAL 才能达到一致性点: |
| 107 | + |
| 108 | + - 崩溃后的恢复时间更长 |
| 109 | + |
| 110 | + - 从备份中配置新节点的时间更长。 |
| 111 | + |
| 112 | + 不过,这种方法对于较大的规格来说是必不可少的,因为它可以带来实质性的改进。 |
| 113 | + |
| 114 | + 调整 `max_wal_size` 和 `checkpoint_timeout` 不需要重新启动。 |
| 115 | + |
| 116 | +2. **检查点优化:开启 WAL 压缩** |
| 117 | + |
| 118 | + 考虑 `wal_compression` — 可以压缩全页写,大多数情况下,这样做是值得的 (尽管有些报告称这会导致更高的 CPU 使用率,并决定回退更改)。 |
| 119 | + |
| 120 | + 修改此参数不需要重启。 |
| 121 | + |
| 122 | +3. **优化查询** |
| 123 | + |
| 124 | + 使用 `pg_stat_statements` 和 `EXPLAIN` 来定位生成大量 WAL 的查询,并进行优化。 |
| 125 | + |
| 126 | + 优化写入的方法之一是鼓励 Postgres 使用更多的 `HOT UPDATE` (为此,我们需要确保页面有可用空间 — 令人惊讶的是,些许膨胀在此处是有益的 — 并且不会对表进行过度索引,因此我们正在更改的列不参与索引定义)。 |
| 127 | + |
| 128 | +4. **删除未使用和冗余的索引** |
| 129 | + |
| 130 | + 在查询优化期间,请记住,对于非 `HOT UPDATE` 和 `INSERT`,生成的 WAL 的量取决于表的索引数量。索引清理是一种非常有用的方法,可以减少此类写入产生的 WAL 量。 |
| 131 | + |
| 132 | +5. **分区** |
| 133 | + |
| 134 | + 对大 (100+ GiB) 表进行分区可以提高写入的数据局部性 — 例如,如果表未分区,那么一堆行的更新可能会分散在许多页面中,而使用定义了旧分区 (几乎没有接收写入) 和包含新数据分区的分区模式,大多数写入将集中在新分区中,这有助于降低 WAL 生成率。 |
0 commit comments