Skip to content
Go back

部署 PostgreSQL 从库并通过 Tailscale 与主库进行流复制

| 0 Views Edit page

前言

上一篇文章我们在境内服务器上部署了一台 PostgreSQL 主库,并接入了 Prometheus + Grafana 监控。
现在数据量越来越大,我希望能再起一台从库,一方面做异地灾备,另一方面也可以分担一些只读查询的压力。
由于主库虽然有公网 IP 但已经通过防火墙关闭了所有对外端口,正好可以借助 Tailscale 内网把两台机器打通,再通过 PostgreSQL 的流复制 (Streaming Replication) + 物理复制槽 (Physical Replication Slot) 实现异步同步。


方案概述

前置条件:主库已经按照 部署 PostgreSQL 并结合 Prometheus 和 Grafana 进行监控 一文部署完成,且两台机器已经加入同一个 Tailscale 网络。

角色配置说明
主库4H12G + 1TB NVMe有公网 IP 但通过防火墙关闭了所有端口,仅 Tailscale 内网可达
从库4H8G + 100GB SSD内存约为主库的一半,硬盘性能相近

步骤概览

  1. 修改主库的复制相关配置(wal_levelmax_wal_sendersmax_slot_wal_keep_sizewal_compression 等)
  2. 主库创建专用复制用户和物理复制槽,并放行 Tailscale 网段的 replication 连接
  3. 在从库服务器上以相同方式安装 PostgreSQL 18
  4. 通过 pg_basebackupTailscale 内网拉取主库基础数据
  5. 启动从库并验证主从同步
  6. 根据从库的资源情况调优配置

选用 物理复制槽 而不是 wal_keep_size 是因为:复制槽能保证 WAL 不被过早回收,断线后可以续传;同时配合 max_slot_wal_keep_size 又能限制主库为复制保留的 WAL 上限,避免从库长期掉线把主库磁盘塞满。


操作步骤

一、修改主库配置

上一篇文章里我们为了纯单机性能把 wal_level = minimalmax_wal_senders = 0,现在要做流复制就必须改回去,并且额外开启复制槽相关的参数。

1、修改 wal_level、max_wal_senders、max_replication_slots

nano /etc/postgresql/18/main/postgresql.conf
# 物理复制只需要 replica 级别
wal_level = replica
# 允许从库通过 walsender 连过来,预留几个
max_wal_senders = 10
# 允许创建的复制槽数量上限
max_replication_slots = 10

2、设置 max_slot_wal_keep_size 限制 WAL 保留上限

这一项是为我们需求量身定制的。主库每 60 秒大约会产生 60MB 的 WAL(约 1MB/s),如果从库断线后主库还无限制地保留 WAL,磁盘很快就会被吃掉。
通过 max_slot_wal_keep_size 设置上限后,当某个复制槽要求保留的 WAL 超过这个值时,该槽会被自动标记为 lost(无效),主库就会正常回收 WAL:

max_slot_wal_keep_size = 10GB

1MB/s 的产生速度,10GB 大约能容忍约 10240 / 60 ≈ 170 分钟(约 2.8 小时)的断线。超过这个时间从库就需要重新 pg_basebackup 一遍,我个人觉得这个取舍是合适的。

3、开启 wal_compression 压缩 WAL 减少复制流量

PostgreSQL 18 支持用 lz4 压缩 WAL 中的整页镜像 (full page writes, FPW),能在几乎不增加 CPU 负担的前提下减少一部分 WAL 体积,从而降低复制流量:

# 用 lz4 压缩 WAL 中的整页镜像,几乎不占 CPU
wal_compression = lz4

需要注意 wal_compression 只压缩整页镜像 (FPW) 这一部分,不会压缩全部 WAL。具体能省多少取决于负载里 FPW 的占比——checkpoint 后第一次改动某个页面时才会写 FPW,所以写入越集中、checkpoint 间隔越长,FPW 占比越高,压缩收益越大;反之如果是大量追加型 INSERT、FPW 占比本就不高,收益会比较有限。实际效果建议改完后实测对比一下。

另外 lz4 需要编译时启用支持(官方源的包默认带),也可以用压缩率更高的 zstd,代价是多花一点 CPU。这个参数 reload 即可生效,不必重启。

如果想验证它实际省了多少,可以在改之前和改之后各测一次每分钟的 WAL 产出,用同一段方法对比:

SELECT pg_current_wal_lsn();   -- 记下返回值,等 60 秒
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '上一步的LSN'));

测的时候要让窗口跨过至少一次 checkpoint,否则窗口内没产生 FPW,压不压都看不出差别。可以先手动 CHECKPOINT; 再开始计时。

如果光靠压缩还压不下来,进一步降流量的方向通常是:拉长 checkpoint_timeout(减少 FPW 总量)、清理高写入表上几乎不用的冗余索引(每个索引更新都要写 WAL),或者评估改用逻辑复制(只传行级变更,不传 FPW / 索引内部结构 / vacuum 噪声)。这些往往比单纯压缩更能压低总流量,但限制和取舍也更多,按需取舍。

4、拉长 checkpoint 间隔进一步减少 FPW

接着上一节的思路。wal_compression 是”WAL 产出后再压”,而拉长 checkpoint 间隔则是”从源头少产 WAL”——对物理复制的流量往往更管用。
原理是:FPW(整页镜像)只在每次 checkpoint第一次改动某个页面时才会写一份。checkpoint 越频繁,同一个热点页就被反复写整页镜像越多次;把间隔拉长,等于让更多改动摊薄到一次 FPW 上,FPW 总量随之下降。

checkpoint_timeout = 30min              # 默认 5min,拉长到 15~30min
checkpoint_completion_target = 0.9      # 让刷盘在间隔内平缓摊开,避免 IO 尖峰
max_wal_size = 8GB                      # 必须配套调大,详见下
min_wal_size = 2GB                      # 顺手调大,减少 WAL 文件频繁回收/新建

这里的关键是 max_wal_size 一定要配套调大checkpoint 有两个触发条件,谁先到触发谁:一是时间到了 checkpoint_timeout,二是 WAL 累积量逼近 max_wal_size。如果 max_wal_size 太小,遇到写入峰值时 WAL 会先撑满、提前触发 checkpoint,那你拉长的 checkpoint_timeout 根本到不了,减 FPW 的效果就白费了。所以要把 max_wal_size 留足,确保 checkpoint由时间触发而不是被 WAL 量逼着提前做。

一个常见的疑问:拉长 checkpoint 间隔会不会让从库的复制延迟变大?不会。 复制延迟取决于 WAL 多快被传输和回放,而 WAL 是在事务提交时实时写出、立即推给从库的,跟 checkpoint 无关。checkpoint 管的是主库把脏页刷到自己的数据文件,是本地持久化动作,和”数据多久同步到从库”是两条线。

拉长 checkpoint 唯一的代价是主库崩溃后的恢复时间会变长(要从上一个 checkpoint 开始重放更多 WAL),日常运行的复制实时性、数据安全性都不受影响。对”想省上传流量”这个目标来说,这笔取舍通常很划算。

另外注意 max_wal_size 是软上限,实际占用峰值可能短时超过它,调大前要确认 pg_wal 所在分区放得下,留足余量。

这几个参数都是 reload 级,不用重启

sudo -u postgres psql -c "SELECT pg_reload_conf();"
sudo -u postgres psql -c "SHOW checkpoint_timeout; SHOW max_wal_size; SHOW checkpoint_completion_target;"

改完后建议确认 checkpoint 确实是按时间触发的(而不是被 WAL 量提前逼出来)。PostgreSQL 18 默认 log_checkpoints = on,直接看日志即可:

sudo tail -f /var/log/postgresql/postgresql-18-main.log | grep -i checkpoint

看到 checkpoint starting: time 就对了,说明是间隔到点触发的;如果看到 checkpoint starting: wal,说明 WAL 量先撑到了上限、提前触发了,意味着 max_wal_size 在你的峰值下还不够,需要再往上提。

验证流量降幅时,要让新间隔实际跑过至少两个 checkpoint 周期(约 1 小时)再用上一节那段测 WAL 的方法量,否则旧节奏还没切换过来,测不准。测量窗口也尽量落在两次 checkpoint 中间、别紧贴 checkpoint(刚做完那段 FPW 最密集,会偏高)。

5、放宽 wal_sender_timeout 适配 Tailscale 网络抖动

wal_sender_timeout 是主库 walsender 进程的心跳超时,默认 60s。在 Tailscale 网络下,DERP 中转、节点休眠唤醒、移动网络切换等情况都可能造成几十秒级别的延迟,按默认值容易触发不必要的重连。这里放宽到 180s

wal_sender_timeout = 180s

从库侧对应的 wal_receiver_timeout 也要一起调大保持一致,那部分放在 “四、从库的资源适配优化” 里再处理。

6、配置 pg_hba.conf 放行 replication 连接

我现在的 pg_hba.conf 末尾已经有:

host  all  all 0.0.0.0/0 scram-sha-256
host  all  all ::/0 scram-sha-256

但需要注意:pg_hba.confreplication 是一个特殊的伪库名,all 并不会匹配 replication 连接,必须显式新增一条规则。
Tailscale 默认的 IPv4 网段是 100.64.0.0/10,我们只放行这个网段的复制连接:

nano /etc/postgresql/18/main/pg_hba.conf
# 在尾部追加,只允许 Tailscale 内网的 replicator 用户做复制
host  replication  replicator  100.64.0.0/10  scram-sha-256

7、重启主库使配置生效

wal_levelmax_wal_senders 的修改必须重启才能生效,下一步的 “创建物理复制槽” 又依赖这些参数已经生效,所以在创建用户和复制槽之前先重启一次:

sudo systemctl restart postgresql@18-main.service
sudo systemctl status postgresql@18-main.service

重启完成后顺手确认一下新的 wal_level 已经生效:

sudo -u postgres psql -c "SHOW wal_level;"
# 应该输出 replica

8、创建专用复制用户

登录主库:

sudo -u postgres psql
-- REPLICATION 角色属性是关键
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'replicator_password';

9、创建物理复制槽

仍然在 psql 中:

SELECT pg_create_physical_replication_slot('replica_slot_1');

查看一下:

SELECT slot_name, slot_type, active FROM pg_replication_slots;

此时 active 应为 f(因为还没有从库连上来),slot_typephysical


二、在从库上安装 PostgreSQL 18

完全沿用上一篇文章 部署 PostgreSQL 并结合 Prometheus 和 Grafana 进行监控 中 “安装 PostgreSQL 数据库” 一节的步骤,从官方源装一个 18 版本即可:

sudo apt install postgresql-18

安装完成后不需要做任何 postgresql.conf / pg_hba.conf 的初始化(包括 listen_addresses),因为这些配置很快就会被 pg_basebackup 从主库拷贝过来的版本整体覆盖。


三、使用 pg_basebackup 初始化从库

1、停止从库的 PostgreSQL 服务

sudo systemctl stop postgresql@18-main.service

2、清空数据目录

pg_basebackup 要求目标目录为空:

sudo -u postgres rm -rf /var/lib/postgresql/18/main/*

3、通过 Tailscale 内网执行 pg_basebackup

sudo -u postgres pg_basebackup \
  -h <your-master-tailscale-hostname> \
  -U replicator \
  -D /var/lib/postgresql/18/main \
  -P -v -R \
  -X stream \
  -S replica_slot_1 \
  -c fast

执行时会提示输入 replicator 用户的密码。
几个参数说明:

参数含义
-h主库地址。可以是 Tailscale IP(如 100.x.x.x),也可以直接用 Tailscale MagicDNS 的主机名
-R写入 standby.signal 文件,并把 primary_conninfoprimary_slot_name 追加到 postgresql.auto.conf,省去手动配置
-X stream在拉取基础备份的同时开一个连接持续接收 WAL,避免大数据库备份过程中 WAL 被回收
-S replica_slot_1复用我们在主库上创建好的物理复制槽
-P -v显示进度和详细日志
-c fast让主库立即做一次 fast checkpoint,避免按 checkpoint_completion_target 慢慢摊开(默认 spread checkpoint 在 checkpoint_timeout = 15min 时最坏要等十几分钟才开始真正传数据)

4、确认从库的复制配置

看一下 pg_basebackup -R 帮我们写好的连接信息:

sudo cat /var/lib/postgresql/18/main/postgresql.auto.conf

应该能看到类似这样的两行:

primary_conninfo = 'user=replicator password=replicator_password host=<your-master-tailscale-hostname> ...'
primary_slot_name = 'replica_slot_1'

同时 standby.signal 应该存在:

ls /var/lib/postgresql/18/main/standby.signal

5、启动从库

sudo systemctl start postgresql@18-main.service
sudo systemctl status postgresql@18-main.service

观察日志确认 streaming 已经建立:

sudo tail -f /var/log/postgresql/postgresql-18-main.log

看到 started streaming WAL from primary 字样即代表成功。


四、从库的资源适配优化

从库只有 4H8G(约为主库的一半),存储依旧是 SSD,所以内存类参数按主库的一半设置,存储类参数和主库保持一致。

1、调小 shared_buffers

按主库 2048MB 的一半设置:

nano /etc/postgresql/18/main/postgresql.conf
shared_buffers = 1024MB

2、调小 work_mem

按主库 16MB 的一半:

work_mem = 8MB

3、调小 maintenance_work_mem

按主库 512MB 的一半:

maintenance_work_mem = 256MB

4、effective_io_concurrency 和 random_page_cost 保持一致

两台机器存储都是 SSD(一台 NVMe、一台普通 SSD,性能差异并不影响这两个参数的取值),保持和主库一致即可:

effective_io_concurrency = 200
random_page_cost = 1.1

5、hot_standby 保持默认开启

PostgreSQL 18 默认 hot_standby = on,从库会自动接受只读查询,无需额外配置。

如果将来要在从库上跑长查询(比如分析类报表),可能会和 WAL 回放冲突而被 cancel,那时再考虑调高 max_standby_streaming_delay,目前不动。

6、放宽 wal_receiver_timeout 与主库的 wal_sender_timeout 对齐

和主库的 wal_sender_timeout 保持一致,避免 Tailscale 网络抖动时双方对超时的判断不同步:

wal_receiver_timeout = 180s

7、重启从库

sudo systemctl restart postgresql@18-main.service

五、验证主从复制

1、主库视角

在主库上执行:

SELECT client_addr, state, sync_state, write_lag, flush_lag, replay_lag
FROM pg_stat_replication;

应该能看到一条记录,client_addr 是从库的 Tailscale IP,state = streamingsync_state = async

再看复制槽状态:

SELECT slot_name, active, wal_status, safe_wal_size
FROM pg_replication_slots;

active 应该变成 twal_statusreserved

2、从库视角

SELECT pg_is_in_recovery();
-- 应该返回 t
SELECT status, sender_host, written_lsn, flushed_lsn, latest_end_lsn
FROM pg_stat_wal_receiver;

status = streaming 即正常。

3、写入测试

主库上:

CREATE TABLE replica_test (id serial PRIMARY KEY, msg text, created_at timestamptz DEFAULT now());
INSERT INTO replica_test (msg) VALUES ('hello from primary');

几秒后从库上查询,应该能立刻看到数据:

SELECT * FROM replica_test;

顺便确认从库确实是只读的:

INSERT INTO replica_test (msg) VALUES ('try write on replica');
-- ERROR: cannot execute INSERT in a read-only transaction

六、复制槽失效后的恢复

如果从库长时间断线导致复制槽要保留的 WAL 超过 max_slot_wal_keep_size,主库上 pg_replication_slots.wal_status 会变成 lost,从库再连回来时会报:

ERROR: requested WAL segment ... has already been removed

此时的恢复方式就是按 “三、使用 pg_basebackup 初始化从库” 重新拉取一次基础备份,复制槽继续沿用即可。如果你希望复制槽也重建,先在主库上:

SELECT pg_drop_replication_slot('replica_slot_1');
SELECT pg_create_physical_replication_slot('replica_slot_1');

再执行 pg_basebackup

结束。


Edit page