PostgreSQL一主一从,很多人愿意手动搭,一条命令一条命令敲。我早期也是这么干的,直到有次在客户环境里连续配了四台从库,每台都要去翻 conf、改权限、重启验证,中间还有一次因为 pg_hba.conf 里的 CIDR 写错,从库连不上主库,排查了半天。那之后我就决定把整套流程固化成一个自动化部署脚本,在 Ubuntu 上跑通,主库执行一遍、从库执行一遍,十分钟内能看到流复制正常状态。这次就把这个“实战版”脚本的完整设计思路、核心原理和每一步的实现细节全部拆开讲清楚,顺便把我踩过的坑一并放出来。
1. 为什么需要“一主一从自动化部署脚本”?项目背景与设计思路
1.1 手动搭建一主一从的真实痛点
PostgreSQL 的主从复制,表面上看核心步骤无非是“主库开 WAL 归档、配复制用户、改 pg_hba;从库做 basebackup、写 standby 配置、启动”。可真要在生产环境或测试环境里手动操作,问题往往出在细节上。我自己遇到的就有这么几类:
一是参数遗漏。比如wal_level忘记从replica而不是默认的minimal,或者max_wal_sender保留默认值 10,一旦将来要挂多个从库就得回头改。这类参数在postgresql.conf里分散在不同位置,手动编辑时容易漏。
二是版本差异带来的命令变化。PostgreSQL 12 之后,recovery.conf被废弃,改为standby.signal文件加postgresql.conf内的参数;如果你之前习惯老版本的玩法,在新版本上手动操作就会卡壳。不同大版本之间,pg_basebackup的参数细节也略有差异。
三是重复操作极易出错。一主一从两台机器,每台至少要执行二三十条命令。其中任何一条命令的 IP 写错、密码写错、目录权限不对,都可能让你在排查上花掉比安装多得多的时间。
四是缺少可复现性。手动搭建的环境,很难保证两台机器配置完全一致。今天在这台机器上多配了一个参数,明天在另一台上忘了,后续遇到问题对比配置时非常痛苦。
所以我在实践里做了这个自动化部署脚本,目标很明确:把一主一从的部署过程压缩成两个脚本,一条命令跑完,输出明确的成功或失败信息,就算换一台新机器也能直接复用。
1.2 自动化脚本的设计目标
写脚本之前,我先给自己定了几个原则,这几点也建议所有做自动化部署的朋友参考:
- 幂等性:同一个脚本在已经部署过的机器上再跑一次,不能把已有配置搞坏。所以我用了“追加配置前先检查是否已存在”的方式,避免重复写入
postgresql.conf或pg_hba.conf。 - 最小干预:脚本只改动跟复制相关的必要配置,不碰业务数据库的数据,不重装系统自带的其他组件。
- 可配置化:IP、端口、版本号、复制用户、密码、数据目录这些都用变量定义在脚本头部,改起来一目了然。
- 明确反馈:每个关键步骤执行完后用
echo输出状态,最后自动验证复制是否生效。不加这个验证,脚本跑完你不知道成没成功。
这套设计思路,其实跟写普通自动化脚本是相通的。但 PostgreSQL 有它的特殊性:它涉及系统服务管理、配置文件格式、数据目录权限、以及主从之间的网络连通性。所以在具体实现上,需要特别处理的地方不少。下面从原理开始讲,这部分理解了,后面的脚本就能读得很顺。
2. PostgreSQL主从复制核心原理与关键参数
2.1 主从复制到底是怎么工作的
很多人第一次接触 PostgreSQL 主从复制,容易被“流复制”这个术语绕晕。我用一个尽量生活化的方式解释:
主库会把每一次数据变更记录到WAL(Write-Ahead Logging)日志里。这个日志你可以理解成一本“流水账”,记录了所有修改数据页的操作。主库每写完一条 WAL 记录,就会把它发送给连接的从库。从库接收到 WAL 后,在自己的数据目录里“重放”这些操作,从而让自己的数据跟主库保持一致。整个过程是持续的、实时的,所以叫“流复制”。
关键在于从库有两种运行状态:
- hot standby(热备):从库在接收和重放 WAL 的同时,允许只读查询。这个状态需要在
postgresql.conf里开启hot_standby = on。 - warm standby:从库只接收 WAL 但不可查询,类似一个等待中的备用节点。
实际生产环境基本都会用 hot standby,既能保证高可用,又能分担一部分读流量。所以脚本里我默认开启hot_standby。
从库建立的过程,也不是说直接“连接然后开始收日志”就行。它需要先有一份跟主库一致的全量数据作为基础,再从这个时间点开始追 WAL。这个“全量数据拷贝”的操作,就是我们常说的pg_basebackup。它本质上是在主库上做一个在线基础备份,并把备份期间产生的 WAL 一并传输给从库。
2.2 六个必须理解的关键参数
脚本里配置的参数并不复杂,但每个参数都有它存在的理由。我按主库和从库分开说明:
主库postgresql.conf中的重点:
| 参数 | 作用 | 我的建议值 |
|---|---|---|
listen_addresses | 允许 PostgreSQL 监听哪些 IP 地址 | 设为机器实际 IP 或用'*'监听所有网卡 |
wal_level | 决定 WAL 记录多少信息,流复制必须设为replica(PG10+)或hot_standby(PG9.6 及更早) | replica |
max_wal_senders | 允许同时有多少个 WAL 发送进程,也就是最多多少台从库可以连接 | 10 |
wal_keep_size | 主库保留多少 WAL 日志用于尚未跟上进度的从库。PG13 之前叫wal_keep_segments,注意版本差异 | 1GB,高负载可调到4GB |
hot_standby | 从库开启只读查询能力,但从库配置也需要同步打开 | on |
从库的postgresql.conf或standby.signal相关设置:
| 参数/文件 | 作用 |
|---|---|
standby.signal | 该文件存在时,PostgreSQL 启动后会自动进入 standby 恢复模式 |
primary_conninfo | 从库连接主库所需的连接信息,包括主库 IP、端口、复制用户 |
hot_standby | 同样设为on,才能接受只读查询 |
primary_conninfo更推荐写入postgresql.conf的独立配置文件里,因为它记录了敏感的主库连接信息。这里注意,pg_basebackup加-R参数时,会自动帮你在数据目录下生成standby.signal并写入primary_conninfo。这一点脚本里会利用到,能省不少事。
2.3 同步复制还是异步复制
还有一个绕不开的选择:同步 or 异步。
- 异步复制:主库提交事务不等待从库确认,性能最好,但主库崩溃时可能丢失少量最近提交的数据。
- 同步复制:主库必须等待至少一个从库确认收到 WAL 后才向客户端返回提交成功。数据零丢失,但写入延迟会明显增加。
脚本默认采用异步复制,理由是:一主一从架构里如果配置同步,从库一旦宕机,主库的写入就会被卡住。这在高可用场景下需要有额外机制来规避(例如synchronous_commit配合synchronous_standby_names的降级策略),对刚入门的朋友来说复杂度偏高。所以我先把异步复制作为默认,并且在文档里注明如何切换。
切换同步也很简单,在主库postgresql.conf里加两行:
synchronous_commit = on synchronous_standby_names = 'standby1'同时从库的primary_conninfo里要加application_name=standby1,让主库能识别到这台从库。这部分脚本没有默认启用,留给有需要的读者自行扩展。
3. 自动化部署脚本完整实现与步骤解析
3.1 前置准备与运行环境约定
先说环境。我的脚本基于 Ubuntu 20.04/22.04/24.04 测试,PostgreSQL 版本选用 14、15、16 都没问题。但有一件事必须做:把 PGDG 官方软件源配好。Ubuntu 自带的 PostgreSQL 版本往往偏老,而且不同 Ubuntu 版本自带的 PG 版本不同——这会导致主从两台机器版本不一致的风险。主从复制强烈建议大版本完全一致,所以统一通过 PGDG 源安装指定版本。
配置 PGDG 源的命令也很简单,以 Ubuntu 22.04 和 PG16 为例:
sudo apt install -y curl ca-certificates sudo install -d /usr/share/postgresql-common/pgdg sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list' sudo apt update注意把jammy换成你 Ubuntu 版本的代号:22.04 是 jammy,20.04 是 focal,24.04 是 noble。这个细节很容易被人忽略。
然后约定以下网络环境:
- 主库 IP:
192.168.1.10 - 从库 IP:
192.168.1.20 - 复制用户:
repl_user - 复制密码:在脚本头部定义,建议部署完成后修改一次
- PostgreSQL 大版本:
16 - 数据目录:Ubuntu 包管理安装默认是
/var/lib/postgresql/16/main
我把这些全部提取为脚本顶部的变量,实际使用按环境替换即可。这也是前面说的“可配置化”原则。
3.2 主库自动化部署脚本拆解
主库脚本setup_pg_master.sh,完整结构如下:
#!/bin/bash set -e # ============ 配置区 ============ MASTER_IP="192.168.1.10" PG_VERSION="16" REPL_USER="repl_user" REPL_PASSWORD="Your_Strong_Passwd_2024" DATA_DIR="/var/lib/postgresql/${PG_VERSION}/main" CONF_DIR="/etc/postgresql/${PG_VERSION}/main" CONF_EXTRA="${CONF_DIR}/conf.d/replication.conf" HBA_FILE="${CONF_DIR}/pg_hba.conf" # =============================== echo ">>> [1/6] 安装 PostgreSQL ${PG_VERSION}" sudo apt install -y postgresql-${PG_VERSION} echo ">>> [2/6] 创建复制用户" sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${REPL_USER}') THEN CREATE ROLE ${REPL_USER} REPLICATION LOGIN PASSWORD '${REPL_PASSWORD}'; END IF; END \$\$; SQL echo ">>> [3/6] 写入复制配置" if [ ! -f "${CONF_EXTRA}" ]; then sudo tee "${CONF_EXTRA}" > /dev/null <<EOF listen_addresses = '${MASTER_IP}' wal_level = replica max_wal_senders = 10 wal_keep_size = 1GB hot_standby = on EOF else echo "已存在 ${CONF_EXTRA},跳过写入" fi echo ">>> [4/6] 写入 pg_hba 复制许可规则" if ! grep -q "${REPL_USER}" "${HBA_FILE}"; then echo "host replication ${REPL_USER} 192.168.1.20/32 scram-sha-256" | sudo tee -a "${HBA_FILE}" else echo "pg_hba 已包含复制规则,跳过" fi echo ">>> [5/6] 重启服务" sudo systemctl restart postgresql echo ">>> [6/6] 验证主库状态" sudo -u postgres psql -c "SELECT pg_is_in_recovery();"写这个脚本时,有几个容易出问题的地方必须单独说明。
第一,复制用户用 DO 块做幂等创建。直接执行CREATE USER遇到已存在时会报错,而set -e会让脚本当场退出。用pg_roles检查一次就不存在这个问题。这是我在脚本里特意加的“防御式”写法。
第二,pg_hba.conf的认证方式。PG15 之后默认password_encryption是scram-sha-256,所以我在规则里明确写scram-sha-256。如果你在 PG14 及更早版本上沿用同样的规则,需要确认主库的password_encryption是否兼容。为了避免旧版本不适配,脚本可以做一个小改进:把认证方式改成md5或保留scram-sha-256视版本而定。个人建议统一用scram-sha-256,安全性和兼容性都更好。
第三,独立配置文件conf.d/replication.conf。Ubuntu 的 PostgreSQL 包会在postgresql.conf最后一行include_dir = 'conf.d',所以放进conf.d下的文件会自动加载。这样做的好处是:我的复制配置跟发行版默认配置完全分离,将来无论是升级还是回滚都干净。手动编辑postgresql.conf的坑在于——升级安装包时可能被覆盖或冲突,而独立文件不会。
第四,listen_addresses写成具体 IP。如果写成'localhost',从库远程连不上;如果写成'*',安全性略差。具体 IP 是折中方案。如果机器有多块网卡,可以根据需要改成逗号分隔的列表。
主库脚本执行完,可以顺手确认一下pg_is_in_recovery()返回f,表示主库没有处于恢复模式,状态正确。
3.3 从库自动化部署脚本拆解
从库脚本setup_pg_standby.sh,是整个流程里最容易“半路翻车”的部分:
#!/bin/bash set -e # ============ 配置区 ============ MASTER_IP="192.168.1.10" STANDBY_IP="192.168.1.20" PG_VERSION="16" REPL_USER="repl_user" REPL_PASSWORD="Your_Strong_Passwd_2024" DATA_DIR="/var/lib/postgresql/${PG_VERSION}/main" CONF_DIR="/etc/postgresql/${PG_VERSION}/main" CONF_EXTRA="${CONF_DIR}/conf.d/replication.conf" # =============================== echo ">>> [1/7] 安装 PostgreSQL ${PG_VERSION}" sudo apt install -y postgresql-${PG_VERSION} echo ">>> [2/7] 停止 PostgreSQL 服务" sudo systemctl stop postgresql echo ">>> [3/7] 清空数据目录" sudo rm -rf "${DATA_DIR}" sudo install -d "${DATA_DIR}" -o postgres -g postgres echo ">>> [4/7] 拉取主库基础备份" sudo -u postgres pg_basebackup -h "${MASTER_IP}" -U "${REPL_USER}" -p 5432 \ -D "${DATA_DIR}" -P -R -X stream echo ">>> [5/7] 写入 standby 扩展配置" if [ ! -f "${CONF_EXTRA}" ]; then sudo tee "${CONF_EXTRA}" > /dev/null <<EOF hot_standby = on EOF else echo "已存在 ${CONF_EXTRA},跳过写入" fi echo ">>> [6/7] 启动从库" sudo systemctl start postgresql echo ">>> [7/7] 验证从库状态" sudo -u postgres psql -c "SELECT pg_is_in_recovery();"这个脚本里每一步都值得掰开讲。
pg_basebackup是关键命令,它的参数含义:
-h:主库地址-U:复制用户-D:备份目标目录,也就是从库的数据目录-P:显示进度-R:自动生成standby.signal,并写入primary_conninfo到postgresql.auto.conf-X stream:备份过程中产生的 WAL 用流复制方式传输,而不是单独拷贝文件
这里要特别强调-R这个参数。很多人手动配置从库时,还在手动创建standby.signal、手动编辑primary_conninfo。其实pg_basebackup -R一条命令把这些全干了。自动生成的postgresql.auto.conf里会包含类似这样的内容:
primary_conninfo = 'user=repl_user password=xxx host=192.168.1.10 port=5432 sslmode=prefer'唯一的问题是:自动生成的primary_conninfo会带上明文密码。这从安全角度不太理想。所以生产环境建议部署完成后,把密码改写成引用外部文件的方式,或者用pg_ident/ 其他认证手段。但作为自动化部署脚本的默认行为,明文密码换取的是部署速度,是否进一步优化取决于你的安全要求。
还有一个容易踩的坑:从库数据目录的属主必须是postgres用户。脚本第 3 步我用install -d -o postgres -g postgres先建好空目录并设好属主,避免因属主不对导致启动失败。如果你直接从/var/lib/postgresql/16/main里删掉数据后,不处理属主,后续pg_basebackup以postgres用户执行时可能因为目录所有者不对而报错。
另外,systemctl stop postgresql之后再执行备份,本质上是做“离线后再在线”的备份——其实pg_basebackup不需要停库,停库的目的是确保数据目录处于可清空状态。上面的脚本在删数据目录前停掉服务是安全的。
从库启动后,pg_is_in_recovery()应该返回t,表示它已进入恢复模式,正在追主库的 WAL。
3.4 从主库侧验证复制状态
光看从库说“我是 recovery 状态”还不够,还必须回到主库侧确认一个关键指标:WAL 发送进程是否真的建立。在主库执行:
SELECT client_addr, state, sync_state, sent_lsn, replay_lsn FROM pg_stat_replication;正常情况你会看到类似输出:
client_addr | state | sync_state | sent_lsn | replay_lsn -------------+--------+------------+----------+------------ 192.168.1.20|streaming|async | 0/3000060 | 0/3000060其中state = streaming表示流复制正在工作,sync_state = async表示异步复制。sent_lsn和replay_lsn如果保持一致,说明从库已经追上主库的最新写入位置。这个验证步骤,脚本里可以写成一条自动化的检查命令,我这里把它放到脚本之外执行,是因为有时需要人工观察两个 LSN 的变化情况,判断复制是否在持续追着走。
在部署脚本里,我倾向于在最后输出一段提示信息,告诉操作者“去主库跑pg_stat_replication确认状态”,比脚本自动判断更稳妥。毕竟自动判断很难覆盖所有异常情况。
4. 部署中的常见问题与排查技巧实录
自动化脚本解决的是“重复劳动”的问题,但脚本跑完之后,你仍可能遇到一些需要人工介入的情况。这部分是我运维 PostgreSQL 过程里积累的真实排查经验,整理成速查表供参考。
4.1 “从库一直追不上主库”怎么办
现象是pg_stat_replication里能看到state = streaming,但replay_lsn长期落后于sent_lsn。
排查方向:
- 查看主库
wal_keep_size是否太小。如果主库 WAL 产生速度快,从库短暂离线后,主库保留的 WAL 不够从库追上,从库会直接从复制状态变成waiting,然后需要重新做pg_basebackup。解决方法是加大wal_keep_size,或者配置 WAL 归档到从库可达的地方。 - 检查主库磁盘 IO。主库 WAL 写入速度是整个复制链路的上限,如果主库本身 IO 压力大,从库自然追不上。
- 看从库 CPU 负载。从库重放 WAL 是 CPU 密集型操作,大量索引更新时尤其明显。可以从库调大
max_parallel_apply_workers_per_partition等参数,但一般场景不需要过度调优。
4.2 从库连接被拒:pg_hba.conf 的细节坑
连接被拒是最常见的错误之一,日志里会出现:
FATAL: no pg_hba.conf entry for replication connection from host "192.168.1.20", user "repl_user"看到这个,先去主库检查pg_hba.conf。重点看两点:CIDR 是否正确、认证方式是否匹配。
我踩过的坑是:写成了192.168.1.0/24以为没问题,结果发现主库和从库在同一个网段的其他 VLAN 里,IP 根本对不上。正确做法是明确写/32的单机地址。另外,如果主库 PostgreSQL 版本较老,可能不支持scram-sha-256,需要把规则改成md5。改完pg_hba.conf记得 reload 而不是 restart:
sudo systemctl reload postgresqlreload不会断开现有连接,对生产环境更平滑。
4.3 pg_basebackup 报错:目录非空 / 权限不足
执行pg_basebackup时报:
pg_basebackup: error: directory "/var/lib/postgresql/16/main" exists but is not empty原因很简单:目标目录里有残留文件。脚本里我会先rm -rf再建,但如果你手动操作时没有清理干净,就会遇到。还有一类权限错误:
FATAL: could not create directory "/var/lib/postgresql/16/main": Permission denied说明当前执行pg_basebackup的用户对目标目录没有写权限。Ubuntu 下建议始终用sudo -u postgres执行,不要用root。
4.4 从库启动后一直显示 “starting” 不进入 streaming
这是一个比较隐蔽的情况。从库启动后,pg_stat_replication里看不到该从库,而从库日志里显示一直在恢复。
可能原因:primary_conninfo里的主机地址写的是主库的localhost,而不是实际 IP。手动配置时容易犯这个错。因为从库自己回环地址连不上主库,复制自然建立不起来。检查postgresql.auto.conf里的主机地址,确保是主库的 IP。
另一个可能:主库max_wal_senders已经满。如果你之前挂过其他从库,或残留的 WAL 发送进程没释放,新的从库就进不来。可以查看:
SELECT * FROM pg_stat_replication;如果发现很多废弃连接,可以在主库执行:
SELECT pg_terminate_backend(pid) FROM pg_stat_replication WHERE state = 'startup' AND pid <> pg_backend_pid();清理后从库会自动重连。
4.5 PostgreSQL 版本差异速查
不同大版本的参数或行为差异,是排查问题时的另一大坑。我整理了一张速查表:
| 变更项 | PG13 及更早 | PG14 | PG15 | PG16 |
|---|---|---|---|---|
| WAL 保留参数 | wal_keep_segments | wal_keep_size | wal_keep_size | wal_keep_size |
recovery.conf | 存在 | 移除 | 移除 | 移除 |
| 默认密码加密 | md5 | md5(可切scram-sha-256) | scram-sha-256 | scram-sha-256 |
| 数据目录默认路径 | /var/lib/postgresql/13/main | /var/lib/postgresql/14/main | /var/lib/postgresql/15/main | /var/lib/postgresql/16/main |
如果你把脚本从 PG14 迁移到 PG16,注意变量PG_VERSION对应的数据目录路径也要跟着变,脚本的DATA_DIR变量已经考虑到这一层。
5. 自动化部署脚本的扩展用法:一主多从与高可用
虽然标题只提“一主一从”,但实际把脚本的思路理清楚后,扩展成“一主多从”几乎零成本。你只需要在从库脚本里重复执行“安装、停服、清空、basebackup、启动”这几步,每台新从库都能通过同一个脚本拉起。唯一的前提是主库的max_wal_senders要大于从库数量。
如果是三台从库,把max_wal_senders改成10就完全够用。不要把数字改得太大,因为每个 WAL sender 进程都会占用主库的内存和文件描述符。10对绝大多数场景都充足。
如果你要进一步做高可用(比如主库故障自动切换),那就不是单纯部署脚本的范畴了。你会用到repmgr或 Patroni 这类工具。它们本质上也是在帮你做主从配置、监控、自动 promote。到那个阶段,本文提到的基础复制原理和参数理解仍然是地基,不会白学。
6. 脚本落地后的三条个人经验总结
关于这套部署脚本,有几个经验值得单独写出来,也是我在实际项目中反复验证过的:
第一,永远不要让脚本静默执行所有步骤。我见过很多自动化脚本,所有输出都用>/dev/null 2>&1吞掉,美其名曰“干净”。真出问题时你连在哪一步挂的都不知道。我自己的脚本里保留了足够多的关键输出,甚至最后还会弹出一段“下一步操作提示”。自动化是为了省力,不是为了制造黑盒。
第二,双节点环境里,务必记录“当前哪个是从库”。这个听起来像废话,但我真遇到过——某次主库磁盘损坏,需要立刻 promote 从库,结果几个人排查了半天才确认哪台是从库。建议在你的部署脚本里,把主从 IP、复制用户、部署日期写成一份DEPLOY_INFO文件放在两台机器上。这不是什么高深操作,但关键时刻能救命。
第三,部署完成后马上做一次“切换演练”。脚本自动部署完成后,别着急把业务切上去。手动把主库停掉,让从库 promote 成主库,看看业务是否恢复正常;再把这个“新主库”重新配置成从库,把架构拉回一主一从。这个演练能提前暴露很多配置问题,而且演练完你对这套系统的掌控力会完全不同。很多人觉得这步骤麻烦,但多节点数据库最怕的就是“没演练过切换”。
我在实际使用这套脚本时,还会在从库部署完成后,立刻在主库创建一个测试表,insert 几条数据,然后到从库查询确认数据同步。这个过程虽然手动做也不复杂,但把它纳入部署脚本的收尾环节,能在第一时间确认复制链路整体可用,比只看pg_is_in_recovery()可靠得多。
PostgreSQL 的主从复制搭建本身不难,但把它做成一键完成、可复制、可验证的脚本,才是真正提升运维效率的地方。希望这份 Ubuntu 实战脚本和背后的原理拆解,能让你在下次搭 PostgreSQL 主从时少踩几个坑。