PostgreSQL 主从复制从零搭建:本地高可用数据库怎么远程安全验证?
标签:PostgreSQL、Docker、数据库、cpolar、内网穿透
本地服务最怕的不是代码报错,而是数据库一挂,接口、后台任务、管理页面一起停摆。真要给小团队做一套高可用数据库验证环境,光把 PostgreSQL 跑起来不够,还得确认主库写入后,从库能不能稳定读到数据。
这篇不讲“数据库挂了怎么办”的大叙事,直接做一套能落地的本地实验:用 Docker Compose 启动 PostgreSQL 主库、只读从库和 pgAdmin,再用psql查复制状态。到验证阶段,再临时用 cpolar 暴露 pgAdmin 或只读从库端口,让远程同事帮忙看连接、复制状态和查询结果。
划重点:这套方案只用于开发、测试、演示和复制链路验证,不要把生产主库写入口长期暴露出去。
1 什么是 PostgreSQL 主从流复制?
PostgreSQL 的主从流复制,说白了就是主库负责写入,从库持续接收主库产生的 WAL 日志并回放。业务把数据写到主库后,从库跟着同步,适合做只读查询、备份验证、读写分离前的链路演练。
这篇里我们只做一件事:确认“主库写入 → WAL 传输 → 从库回放 → 远程只读验证”这条链路是通的。
这里先不做自动故障切换。故障切换涉及 VIP、代理层、提升从库、应用连接重试,一篇文章塞进去反而容易写乱。先把复制跑稳,后面再扩展 Patroni、repmgr 或 HAProxy 会更清楚。
本次实验的端口安排如下:
| 服务 | 容器名 | 容器端口 | 宿主机访问 |
|---|---|---|---|
| PostgreSQL 主库 | pg-primary | 5432 | 127.0.0.1:5432 |
| PostgreSQL 从库 | pg-replica | 5432 | 127.0.0.1:5433 |
| pgAdmin | pgadmin | 80 | http://127.0.0.1:5050 |
提醒一下,主库和从库在 Compose 网络里都使用 5432;只是映射到宿主机时,从库改成了 5433,避免端口冲突。
2 环境准备:目录、Docker 和账号先定好
这一步先把目录和文件放整齐。后面排错时,能一眼看出是主库初始化脚本、从库拉基线脚本,还是 Compose 配置出了问题。
2.1 创建项目目录
在一台已经安装 Docker 和 Docker Compose v2 的机器上执行:
mkdir -p ~/pg-replication-lab/primary/init mkdir -p ~/pg-replication-lab/replica cd ~/pg-replication-lab检查 Docker Compose 是否可用:
docker compose version能看到版本号就继续。这里建议直接使用docker compose,不要再用老的docker-compose命令,后面的命令都按 Compose v2 写。
2.2 准备主库初始化脚本
主库启动时要做两件关键事:创建复制账号,并允许这个账号从 Docker 网络内发起 replication 连接。
新建文件primary/init/01-primary.sh:
cat > primary/init/01-primary.sh <<'EOF' #!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'EOSQL' CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'repl_pass_2026'; CREATE ROLE app_readonly WITH LOGIN PASSWORD 'readonly_pass_2026'; EOSQL cat >> "$PGDATA/pg_hba.conf" <<'EOF_HBA' host replication replicator 0.0.0.0/0 scram-sha-256 host all app_readonly 0.0.0.0/0 scram-sha-256 EOF_HBA EOF chmod +x primary/init/01-primary.sh这里别把replicator当业务账号用,它只负责从主库拉 WAL。后面远程验证查询时,用app_readonly这个只读账号,不把主库超级用户密码发给别人。
2.3 准备从库初始化脚本
从库第一次启动时,要用pg_basebackup从主库拉一份基线数据。-R参数会写入复制配置,让从库知道以后从哪个主库继续接收 WAL。
新建文件replica/replica-entrypoint.sh:
cat > replica/replica-entrypoint.sh <<'EOF' #!/bin/bash set -euo pipefail mkdir -p "$PGDATA" chown -R postgres:postgres "$PGDATA" chmod 700 "$PGDATA" if [ ! -s "$PGDATA/PG_VERSION" ]; then echo "waiting for primary..." until gosu postgres pg_isready -h pg-primary -p 5432 -U postgres; do sleep 2 done echo "running pg_basebackup..." rm -rf "$PGDATA"/* export PGPASSWORD="$REPLICATOR_PASSWORD" gosu postgres pg_basebackup \ -h pg-primary \ -p 5432 \ -D "$PGDATA" \ -U replicator \ -v \ -P \ -R \ -X stream \ -C \ -S replica_slot unset PGPASSWORD fi exec gosu postgres postgres \ -c hot_standby=on \ -c listen_addresses='*' EOF chmod +x replica/replica-entrypoint.sh这段脚本里有一个容易卡住的点:pg_basebackup必须等主库真正 ready 后再执行,所以前面用了pg_isready循环等待。否则从库容器启动得太快,会直接连不上主库。
3 使用 Docker Compose 搭建主库、从库和 pgAdmin
现在把三个服务编排到一个文件里。主库负责写,从库负责读,pgAdmin 负责图形化验证。
新建docker-compose.yml:
cat > docker-compose.yml <<'EOF' services: pg-primary: image: postgres:16 container_name: pg-primary environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: primary_pass_2026 POSTGRES_DB: appdb command: - postgres - -c - wal_level=replica - -c - max_wal_senders=10 - -c - max_replication_slots=10 - -c - hot_standby=on - -c - listen_addresses=* volumes: - pg_primary_data:/var/lib/postgresql/data - ./primary/init:/docker-entrypoint-initdb.d:ro ports: - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"] interval: 5s timeout: 5s retries: 20 networks: - pgnet pg-replica: image: postgres:16 container_name: pg-replica environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: replica_local_pass_2026 POSTGRES_DB: appdb REPLICATOR_PASSWORD: repl_pass_2026 PGDATA: /var/lib/postgresql/data entrypoint: ["/replica-entrypoint.sh"] volumes: - pg_replica_data:/var/lib/postgresql/data - ./replica/replica-entrypoint.sh:/replica-entrypoint.sh:ro ports: - "127.0.0.1:5433:5432" depends_on: pg-primary: condition: service_healthy networks: - pgnet pgadmin: image: dpage/pgadmin4:latest container_name: pgadmin environment: PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_PASSWORD: pgadmin_pass_2026 ports: - "127.0.0.1:5050:80" depends_on: pg-primary: condition: service_healthy networks: - pgnet volumes: pg_primary_data: pg_replica_data: networks: pgnet: driver: bridge EOF启动服务:
docker compose up -d看容器状态:
docker compose ps如果从库一直重启,先看日志,不要急着删数据卷:
docker compose logs -f pg-replica正常情况下,日志里能看到pg_basebackup的进度,随后从库进入接收 WAL 的状态。这里建议第一次搭建时别开太多服务,先让主从复制跑稳,后面再接应用更轻松。
4 验证主从复制:主库写,从库读
复制链路不是看容器都在运行就算成功。我们要做三层验证:主库能看到从库连接、从库确认自己处于恢复状态、主库写入的数据能在从库查到。
4.1 在主库建表并写入数据
进入主库执行 SQL:
docker exec -it pg-primary psql -U postgres -d appdb在psql里执行:
CREATE TABLE IF NOT EXISTS demo_orders ( id BIGSERIAL PRIMARY KEY, order_no TEXT NOT NULL, amount NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO demo_orders (order_no, amount) VALUES ('ORD-20260607-001', 99.90), ('ORD-20260607-002', 128.50); GRANT CONNECT ON DATABASE appdb TO app_readonly; GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;这里顺手给app_readonly授权,是为了后面验证只读连接。真实项目里建议给业务表单独授权,不要偷懒把写权限也给出去。
4.2 在从库查询数据
从宿主机连从库端口5433:
PGPASSWORD=readonly_pass_2026 psql \ -h 127.0.0.1 \ -p 5433 \ -U app_readonly \ -d appdb \ -c "SELECT id, order_no, amount FROM demo_orders ORDER BY id;"能看到两条订单记录,就说明主库写入已经同步到从库。
再确认从库是只读恢复状态:
docker exec -it pg-replica psql -U postgres -d appdb \ -c "SELECT pg_is_in_recovery();"返回t表示当前实例是从库。这个检查很重要,别只看端口连通;有些环境里连错端口,会把主库当从库测。
4.3 在主库查看复制连接
回到主库查pg_stat_replication:
docker exec -it pg-primary psql -U postgres -d appdb \ -c "SELECT application_name, client_addr, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"能看到一行从库连接,state为streaming,说明 WAL 正在传输。
如果这里没有记录,按这个顺序查:
- 从库日志里有没有
pg_basebackup或连接认证错误; - 主库
pg_hba.conf是否允许replicator做 replication 连接; REPLICATOR_PASSWORD是否和主库创建的repl_pass_2026一致;- 两个容器是否在同一个 Compose 网络
pgnet里。
复制延迟也可以直接查 WAL 位置差异:
docker exec -it pg-primary psql -U postgres -d appdb \ -c "SELECT pg_current_wal_lsn();" docker exec -it pg-replica psql -U postgres -d appdb \ -c "SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();"本地 Docker 环境里,写入量很小时延迟通常很低。重点不是追求漂亮数字,而是要能定位卡在“主库没发、网络没通、从库没回放”哪一段。
5 用 pgAdmin 或 psql 做图形化验证
命令行验证够直接,但团队协作时,pgAdmin 这种 Web 管理界面更适合让同事快速看结果。
浏览器打开:
http://127.0.0.1:5050登录信息:
Email: admin@example.com Password: pgadmin_pass_2026在 pgAdmin 里添加主库连接时,填写:
| 项 | 主库配置 |
|---|---|
| Host name/address | pg-primary |
| Port | 5432 |
| Maintenance database | appdb |
| Username | postgres |
| Password | primary_pass_2026 |
添加从库连接时,填写:
| 项 | 从库配置 |
|---|---|
| Host name/address | pg-replica |
| Port | 5432 |
| Maintenance database | appdb |
| Username | app_readonly |
| Password | readonly_pass_2026 |
注意,pgAdmin 容器和 PostgreSQL 容器在同一个 Docker 网络里,所以这里写pg-primary、pg-replica,不是写127.0.0.1。如果你是在宿主机用本地 psql 连接,才使用127.0.0.1:5432和127.0.0.1:5433。
在 pgAdmin 的 Query Tool 里,对从库执行:
SELECT pg_is_in_recovery(); SELECT id, order_no, amount FROM demo_orders ORDER BY id;再试着执行一条写入:
INSERT INTO demo_orders (order_no, amount) VALUES ('ORD-READONLY-TEST', 1.00);从库处于恢复状态时,这条写入会失败。这个失败是好事,它证明你现在连的是只读从库,不是误连了主库。
6 用 cpolar 做远程临时验证:只暴露验证入口
本地验证通过后,常见需求是让远程同事也看一眼:从库能不能连、查询结果是不是最新、pgAdmin 里能不能看到状态。
这里 cpolar 的作用很明确:临时把本地验证入口映射出去。不要把它理解成“把数据库长期放到公网”。数据库端口直接暴露风险很高,尤其是主库写入口。
6.1 临时暴露 pgAdmin 页面
如果只是让同事看 pgAdmin 页面,优先开 HTTP 隧道,映射本地5050:
cpolar http 5050cpolar 会输出一个公网 HTTP 地址。把这个地址发给同事后,对方能打开 pgAdmin 登录页,再用你提供的 pgAdmin 账号登录。
安全提醒放前面:
- pgAdmin 密码要设置强密码,不要使用本文示例密码;
- 只在验证窗口内开启隧道,用完关闭;
- 不要把生产库连接信息配置到这个临时 pgAdmin;
- 同事验证完成后,修改临时密码或删除 pgAdmin 容器。
6.2 临时暴露只读从库端口
如果对方需要用本地数据库客户端连接从库,可以临时开 TCP 隧道,映射宿主机的从库端口5433:
cpolar tcp 5433cpolar 会给出一个公网 TCP 地址和端口。对方连接时使用 cpolar 输出的主机名和端口,数据库账号使用只读账号:
Database: appdb Username: app_readonly Password: readonly_pass_2026这里别填主库账号,也别映射5432主库端口。验证目标是“远程读取从库数据和复制状态”,不是让远程写入主库。
如果要长期固定地址,cpolar 的免费随机公网地址 24 小时内会变化;固定二级子域名需要基础套餐或以上,固定 TCP 地址需要专业套餐或以上。本文这个场景更推荐短时验证,用完关闭,减少暴露面。
6.3 用完关闭验证入口
前台运行的 cpolar 命令,在对应终端按Ctrl+C就能停止。停止后,再让同事刷新页面或重连数据库,确认外部入口已经不可用。
这一步不是形式主义。数据库验证链路越短越好,暴露时间也越短越好。尤其是主从复制这种基础设施实验,安全边界要从一开始就立住。
7 常见排查:复制没起来先看这几处
这套实验最容易卡的不是 SQL,而是初始化顺序和连接权限。
7.1 从库没有数据
先看从库是否真的处于恢复状态:
docker exec -it pg-replica psql -U postgres -d appdb \ -c "SELECT pg_is_in_recovery();"如果不是t,说明从库没有按 standby 模式启动。检查replica-entrypoint.sh是否挂载成功,以及数据卷里是否残留了旧数据。
需要从头重建实验环境时,用下面命令清理容器和数据卷:
docker compose down -v docker compose up -d提醒:down -v会删除主库和从库数据卷,只适合实验环境。生产库不要这么干。
7.2 主库看不到 pg_stat_replication
主库没有复制连接时,重点看认证和网络:
docker compose logs pg-replica日志里如果出现密码认证失败,检查repl_pass_2026是否前后一致。日志里如果出现主机无法解析,检查 Compose 服务名是不是pg-primary。
再检查主库是否开启了复制参数:
docker exec -it pg-primary psql -U postgres -d appdb \ -c "SHOW wal_level; SHOW max_wal_senders; SHOW max_replication_slots;"wal_level应该是replica,max_wal_senders和max_replication_slots都要大于 0。
7.3 只读账号查不到表
从库能连但查表失败,多半是主库没有给只读账号授权。回主库补一次:
docker exec -it pg-primary psql -U postgres -d appdb \ -c "GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;"以后新建表想自动给只读权限,就保留前面写过的默认权限语句:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;权限这块别嫌麻烦。远程验证时只给查询权限,能减少很多不必要的风险。
8 和 CloudBeaver 那篇有什么区别?
之前如果你看过 CloudBeaver 相关内容,可以把它理解成“数据库 Web 管理工具”方向:重点是怎么通过浏览器连接、管理多种数据库。
本文不做客户端选型,核心是 PostgreSQL 主从架构与复制验证。pgAdmin 只是验证工具之一,真正要确认的是:
- 主库是否正确产生并发送 WAL;
- 从库是否处于恢复状态并回放数据;
- 只读账号是否能完成远程查询验证;
- cpolar 是否只在验证阶段短时打开入口。
旧文内链位置先留在这里:CloudBeaver 数据库 Web 管理工具教程。
9 总结
到这里,我们已经用 Docker Compose 搭好了一套 PostgreSQL 主从复制实验环境:主库负责写入,从库持续接收 WAL 并提供只读查询,pgAdmin 和 psql 都能验证复制状态。远程协作验证时,cpolar 只负责短时打开 pgAdmin 或只读从库入口,不碰生产库,也不长期暴露主库写入口。
这套流程里最关键的几步是:
- 主库提前配置
wal_level=replica、max_wal_senders、max_replication_slots,并在pg_hba.conf里允许复制账号连接; - 从库用
pg_basebackup -R -X stream拉取基线数据,再通过pg_is_in_recovery()和pg_stat_replication双向确认状态; - 远程验证只开放 pgAdmin 或只读从库端口,使用强密码、只读账号和临时隧道,用完立刻关闭。
如果只是家庭服务器、小团队测试环境,这套做法已经能把“主库写、从库读、远程验证”的链路跑通。后续要继续升级,可以再接入故障切换、连接代理、备份恢复演练,把它从实验环境慢慢推到更接近生产的形态。
你更想看 PostgreSQL 故障切换、只读账号权限细分,还是 CloudBeaver/pgAdmin 管理界面对比?评论区直接点一个方向,我按实操路线继续写。