PostgreSQL - 流复制的原理与实时同步配置
👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕PostgreSQL这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- PostgreSQL - 流复制的原理与实时同步配置
PostgreSQL - 流复制的原理与实时同步配置
在当今高可用、高并发的系统架构中,数据库的可靠性与容灾能力成为核心关注点。PostgreSQL 作为一款功能强大、开源且高度可扩展的关系型数据库,其内置的**流复制(Streaming Replication)**机制为构建高可用数据库集群提供了坚实基础。本文将深入探讨 PostgreSQL 流复制的工作原理,并手把手指导你完成主从实时同步的配置,同时结合 Java 应用场景,展示如何在代码层面感知和利用这种复制架构。
什么是流复制?🤔
流复制是 PostgreSQL 自 9.0 版本起引入的一种物理复制机制。它允许一个或多个“备用服务器”(Standby Server)实时地从“主服务器”(Primary Server)接收并应用 WAL(Write-Ahead Logging)日志记录,从而保持与主服务器数据的高度一致性。
与传统的基于文件的归档复制(File-based Archiving)不同,流复制是连续的、近乎实时的。主服务器在生成 WAL 记录后,会立即将其通过网络流式传输给备用服务器,而不是等待整个 WAL 文件写满后再进行归档和传输。这极大地减少了主从之间的数据延迟,通常可以将延迟控制在毫秒级别。
核心优势 🚀
- 高可用性(High Availability):当主服务器发生故障时,可以快速将一个备用服务器提升为主服务器,实现故障转移。
- 读写分离(Read/Write Splitting):备用服务器可以处理只读查询,分担主服务器的读负载,提高整体系统吞吐量。
- 数据冗余(Data Redundancy):提供数据的实时备份,防止因硬件故障导致的数据丢失。
- 零数据丢失(Zero Data Loss):在同步复制模式下,可以确保事务在主服务器提交前,其 WAL 记录已被至少一个备用服务器安全接收。
流复制的工作原理 🔍
要理解流复制,必须先了解 PostgreSQL 的 WAL(预写式日志) 机制。
WAL:一切的基础 📜
WAL 是数据库系统中一种用于保证数据一致性和持久性的标准技术。其核心思想是:任何对数据文件的修改,在写入磁盘之前,必须先将修改操作记录到日志文件中。
在 PostgreSQL 中:
- 当一个事务执行
INSERT、UPDATE或DELETE操作时,这些修改首先被记录到内存中的 WAL 缓冲区。 - 在事务提交(
COMMIT)时,WAL 缓冲区的内容会被强制刷写(flush)到磁盘上的 WAL 文件中。 - 只有在 WAL 记录成功持久化后,事务才被视为已提交。
- 后台进程(如 Checkpointer)会异步地将脏页(被修改的数据页)从共享缓冲区写入到数据文件中。
这种机制确保了即使在数据库崩溃后,也可以通过重放 WAL 日志来恢复到一致的状态。
流复制如何利用 WAL?📡
流复制正是建立在 WAL 机制之上的。其工作流程如下:
备用服务器 (Standby)主服务器 (Primary)备用服务器 (Standby)主服务器 (Primary)备用服务器处于"恢复"模式,数据文件持续更新客户端发起写事务生成WAL记录并写入本地WAL文件通过复制槽(Replication Slot)流式发送WAL记录接收WAL记录并写入本地WAL文件 (recovery.conf / standby.signal)启动WAL应用进程 (walreceiver + startup process)应用WAL记录,更新数据文件
- 主服务器角色:主服务器正常处理所有读写请求,并持续生成 WAL 记录。
- WAL 发送:主服务器上有一个特殊的后台进程
walsender,它负责监听来自备用服务器的连接请求。一旦连接建立,walsender就会从当前的 WAL 位置开始,将新产生的 WAL 记录通过 TCP 连接实时地“流式”发送出去。 - 备用服务器角色:备用服务器启动时,会进入一种特殊的“恢复”(Recovery)模式。它不会接受普通的客户端写请求(只读),而是专注于从主服务器接收 WAL 数据。
- WAL 接收与应用:备用服务器上的
walreceiver进程负责接收来自主服务器的 WAL 流,并将其写入到本地的 WAL 文件中。随后,备用服务器的启动进程(Startup Process)会立即读取这些新的 WAL 文件,并像在崩溃恢复一样,重放(Replay)其中的记录,从而更新本地的数据文件。
这个过程是连续不断的,只要主备之间的网络通畅,备用服务器就能几乎实时地跟上主服务器的最新状态。
关键组件解析 ⚙️
walsender进程:运行在主服务器上,每个连接到主服务器的备用服务器都会对应一个walsender进程。它负责读取 WAL 并通过网络发送。walreceiver进程:运行在备用服务器上,负责接收来自主服务器的 WAL 流,并将其写入本地 WAL 文件。- 复制槽(Replication Slot):这是一个非常重要的概念。它是在主服务器上创建的一个逻辑对象,用于解决两个关键问题:
- 防止 WAL 被过早清理:主服务器会根据
wal_keep_size等参数来决定保留多少旧的 WAL 文件。但如果备用服务器因为网络问题长时间断开,可能会错过一些 WAL 文件。复制槽可以告诉主服务器:“请保留我需要的所有 WAL,直到我确认收到为止”,从而避免备用服务器因找不到 WAL 而无法继续同步。 - 跟踪备用服务器的进度:主服务器可以通过复制槽知道每个备用服务器已经接收和应用到了哪个 WAL 位置。
- 防止 WAL 被过早清理:主服务器会根据
- 同步级别(Synchronous Commit):PostgreSQL 允许配置事务提交的同步级别,以平衡性能和数据安全性。
synchronous_commit = off:异步复制。主服务器在本地 WAL 刷盘后就向客户端返回成功,不等待备用服务器的确认。性能最好,但存在主服务器宕机时丢失少量已提交事务的风险。synchronous_commit = on:同步复制。主服务器必须等待至少一个同步备用服务器确认接收到 WAL 记录后,才向客户端返回成功。这可以保证“零数据丢失”,但会增加事务提交的延迟。
实战:配置 PostgreSQL 流复制 🛠️
现在,让我们动手搭建一个简单的主从流复制环境。我们将使用两台虚拟机或 Docker 容器来模拟。
前提条件 ✅
- 两台服务器(或容器),分别命名为
pg-primary和pg-standby。 - 在两台服务器上都安装好相同版本的 PostgreSQL(例如 14.x)。
- 确保两台服务器之间网络互通,特别是 PostgreSQL 的端口(默认5432)和用于复制的端口(也是5432)。
步骤一:配置主服务器 (pg-primary) 🏠
重启主服务器修改完配置后,需要重启 PostgreSQL 服务使配置生效。
sudo systemctl restart postgresql 创建复制用户登录到主服务器的 PostgreSQL,创建一个具有 REPLICATION 权限的用户。
CREATEUSER replicator WITHREPLICATION ENCRYPTED PASSWORD 'your_secure_password';编辑 pg_hba.conf在 pg_hba.conf 文件中,添加一条规则,允许备用服务器通过复制协议连接。
# TYPE DATABASE USER ADDRESS METHOD # 允许来自备用服务器IP的复制连接 host replication replicator <pg-standby-ip>/32 md5 这里我们创建了一个专门用于复制的用户 replicator。
编辑 postgresql.conf找到 PostgreSQL 的配置文件 postgresql.conf(通常位于 /etc/postgresql/14/main/ 或 $PGDATA 目录下),进行以下修改:
# 启用流复制 wal_level = replica # 允许的最大 WAL 发送进程数(即最多能有多少个备用服务器) max_wal_senders = 3 # 为备用服务器保留的 WAL 文件数量(单位:16MB/个) # 这是一个兜底策略,建议配合复制槽使用 wal_keep_size = 128MB # 监听所有地址,以便备用服务器可以连接 listen_addresses = '*' # (可选)如果要配置同步复制,需要设置 # synchronous_standby_names = 'pg-standby' # 这里的名字要和备用服务器的 application_name 一致 步骤二:配置备用服务器 (pg-standby) 🏘️
-h: 主服务器的 IP 地址。-U: 用于连接的复制用户名。-D: 备份的目标目录,即备用服务器的数据目录。-P: 显示进度。-v: 详细模式。-R: 关键参数! 它会在备用服务器的数据目录中自动生成standby.signal文件(对于 PG 12+)和在postgresql.auto.conf中追加主服务器的连接信息。这相当于自动完成了备用服务器的配置。-C: 在主服务器上创建一个名为pg-standby的临时复制槽。pg_basebackup完成后,这个槽会被删除。但我们通常希望永久保留,所以接下来我们会手动创建。-S: 指定复制槽的名称。
启动备用服务器现在,备用服务器已经配置好了。直接启动 PostgreSQL 服务即可。
sudo systemctl start postgresql (可选)手动创建永久复制槽虽然 -C 创建了临时槽,但为了长期稳定运行,建议在主服务器上手动创建一个永久复制槽。在主服务器上执行:
SELECT*FROM pg_create_physical_replication_slot('pg-standby');然后,你需要编辑备用服务器数据目录下的 postgresql.auto.conf 文件,确保 primary_conninfo 中包含了 slot_name=pg-standby。
primary_conninfo = 'user=replicator password=your_secure_password host=<pg-primary-ip> port=5432 sslmode=prefer slot_name=pg-standby' 使用 pg_basebackup 进行基础备份pg_basebackup 是 PostgreSQL 提供的一个工具,可以从正在运行的主服务器上获取一个完整的、一致的基础备份。这是初始化备用服务器最推荐的方式。
sudo -u postgres pg_basebackup -h <pg-primary-ip> -U replicator -D /var/lib/postgresql/14/main/ -P -v -R -C -S pg-standby 参数解释:
清空备用服务器的数据目录在配置备用服务器之前,必须确保其数据目录是干净的。通常我们会先停止 PostgreSQL 服务,然后清空 $PGDATA 目录。
sudo systemctl stop postgresql sudorm -rf /var/lib/postgresql/14/main/* # 请根据你的实际路径调整验证复制状态 ✅
配置完成后,我们需要验证主从是否正常同步。
测试数据同步在主服务器上创建一个表并插入数据:
CREATETABLE test_replication (id SERIAL,dataTEXT);INSERTINTO test_replication (data)VALUES('Hello from Primary!');稍等片刻(通常是瞬间),在备用服务器上查询:
SELECT*FROM test_replication;-- 应该能看到刚插入的数据如果能看到,恭喜你,流复制配置成功!
在备用服务器上检查
-- 查看是否处于恢复模式SELECT pg_is_in_recovery();-- 应该返回 true-- 查看 WAL 接收状态SELECT*FROM pg_stat_wal_receiver;在主服务器上检查
-- 查看 WAL 发送进程SELECT*FROM pg_stat_replication;如果看到一条记录,state 为 streaming,说明流复制正在进行中。
Java 应用如何与流复制协同工作?☕
流复制为数据库层提供了高可用和读写分离的能力,但我们的 Java 应用程序也需要能够感知和利用这种架构。主要有两个场景:读写分离和故障转移。
场景一:读写分离 💾
在读多写少的应用中,我们可以将写操作(INSERT, UPDATE, DELETE)路由到主服务器,而将读操作(SELECT)路由到备用服务器,以分担主库压力。
使用 HikariCP + 自定义路由
HikariCP 是一个高性能的 JDBC 连接池。我们可以创建两个数据源:一个指向主库,一个指向从库。
importcom.zaxxer.hikari.HikariConfig;importcom.zaxxer.hikari.HikariDataSource;importjavax.sql.DataSource;importjava.sql.Connection;importjava.sql.SQLException;publicclassReplicationDataSource{privatefinalDataSource primaryDataSource;privatefinalDataSource standbyDataSource;publicReplicationDataSource(){this.primaryDataSource =createDataSource("jdbc:postgresql://pg-primary:5432/mydb","primary_user");this.standbyDataSource =createDataSource("jdbc:postgresql://pg-standby:5432/mydb","standby_user");}privateDataSourcecreateDataSource(String jdbcUrl,String username){HikariConfig config =newHikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(username); config.setPassword("password"); config.setMaximumPoolSize(10);returnnewHikariDataSource(config);}// 获取用于写操作的连接(主库)publicConnectiongetWriteConnection()throwsSQLException{return primaryDataSource.getConnection();}// 获取用于读操作的连接(从库)publicConnectiongetReadConnection()throwsSQLException{return standbyDataSource.getConnection();}}在业务代码中,根据操作类型选择不同的连接:
publicclassUserService{privatefinalReplicationDataSource dataSource;publicUserService(ReplicationDataSource dataSource){this.dataSource = dataSource;}publicvoidcreateUser(String name)throwsSQLException{try(Connection conn = dataSource.getWriteConnection();PreparedStatement stmt = conn.prepareStatement("INSERT INTO users(name) VALUES (?)")){ stmt.setString(1, name); stmt.executeUpdate();}}publicList<String>getAllUserNames()throwsSQLException{List<String> names =newArrayList<>();try(Connection conn = dataSource.getReadConnection();PreparedStatement stmt = conn.prepareStatement("SELECT name FROM users");ResultSet rs = stmt.executeQuery()){while(rs.next()){ names.add(rs.getString("name"));}}return names;}}更高级的方案:使用 ShardingSphere
对于复杂的读写分离需求,手动管理连接会很繁琐。Apache ShardingSphere 是一个强大的分布式数据库中间件,它原生支持读写分离。
通过简单的 YAML 配置,ShardingSphere 就能自动将 SQL 路由到正确的数据源。
# config-sharding.yamldataSources:primary_ds:url: jdbc:postgresql://pg-primary:5432/mydb username: primary_user password: password connectionTimeoutMilliseconds:30000idleTimeoutMilliseconds:60000maxLifetimeMilliseconds:1800000maxPoolSize:50standby_ds:url: jdbc:postgresql://pg-standby:5432/mydb username: standby_user password: password connectionTimeoutMilliseconds:30000idleTimeoutMilliseconds:60000maxLifetimeMilliseconds:1800000maxPoolSize:50readwriteSplitting:rules:-name: rw_splitting type: Static props:write-data-source-name: primary_ds read-data-source-names: standby_ds 在 Java 代码中,你只需要像使用单个数据源一样使用 ShardingSphere 提供的 DataSource,它会在内部完成智能路由。
场景二:故障转移 🔄
当主服务器发生故障时,我们需要将一个备用服务器提升为新的主服务器,并通知应用程序更新其连接地址。
手动故障转移
- 更新应用程序配置手动修改应用程序的配置文件,将主库地址指向原来的
pg-standby,然后重启应用。
在备用服务器上执行提升命令
# 在 pg-standby 服务器上执行sudo -u postgres pg_ctl promote -D /var/lib/postgresql/14/main/ 或者在备用服务器的 PostgreSQL 中执行 SQL:
SELECT pg_promote();执行后,该服务器将退出恢复模式,成为一个可读写的主服务器。
这种方式简单直接,但需要人工干预,不适合要求高自动化的生产环境。
自动故障转移:使用 Patroni
Patroni 是一个用 Python 编写的 PostgreSQL 高可用模板。它使用一个分布式配置存储(如 etcd, Consul, ZooKeeper)来协调主从状态,并在主库故障时自动选举新的主库。
Patroni 的工作原理可以用下面的图表表示:
Cluster
Heartbeat & State
Heartbeat & State
Heartbeat & State
Leader Election
Node 1
PostgreSQL + Patroni
etcd
Node 2
PostgreSQL + Patroni
Node 3
PostgreSQL + Patroni
- 每个节点上都运行着 Patroni 进程。
- Patroni 会定期向 etcd 报告自己的健康状态和 PostgreSQL 的角色(主/从)。
- etcd 负责维护集群的全局状态,并进行 Leader 选举。
- 如果当前主节点失联,etcd 会触发选举,Patroni 会在剩余的健康从节点中选出一个新的主节点并执行
pg_promote()。
对于 Java 应用来说,Patroni 通常会配合一个 HAProxy 或 VIP(虚拟IP)来使用。应用程序只需要连接到 HAProxy 的固定地址,HAProxy 会根据 Patroni 提供的后端状态,将写请求路由到当前的主库,将读请求路由到从库。
这样,无论底层的主从如何切换,应用程序的连接字符串都无需改变,实现了无缝的故障转移。
深入探讨:同步 vs 异步复制 ⚖️
在配置流复制时,一个核心决策点就是选择同步复制还是异步复制。这直接影响到系统的数据安全性和性能。
异步复制(Asynchronous Replication)
- 工作方式:主服务器在本地 WAL 刷盘后,立即向客户端返回事务成功。同时,它会将 WAL 记录异步地发送给备用服务器。
- 优点:性能极高,事务提交延迟极低,几乎不受网络状况影响。
- 缺点:存在数据丢失窗口。如果主服务器在 WAL 发送给备用服务器之前就发生灾难性故障(如磁盘损坏),那么这部分已提交但未同步的事务就会丢失。
- 适用场景:对性能要求极高,且能容忍极少量数据丢失的场景,如日志分析、非核心业务系统。
同步复制(Synchronous Replication)
- 工作方式:主服务器在本地 WAL 刷盘后,会等待至少一个(或多个,取决于配置)同步备用服务器确认已接收到该 WAL 记录,然后才向客户端返回成功。
- 优点:提供零数据丢失(Zero Data Loss)保证。即使主服务器完全宕机,已提交的事务也一定存在于至少一个备用服务器上。
- 缺点:增加了事务提交的延迟。延迟时间取决于主备之间的网络往返时间和备用服务器的 I/O 性能。如果同步备用服务器宕机或网络中断,主服务器的写操作将会被阻塞(除非配置了超时或降级策略)。
- 适用场景:对数据一致性要求极高的金融、支付、核心交易系统。
如何配置同步复制?
在备用服务器的 primary_conninfo 中(通常在 postgresql.auto.conf):
primary_conninfo = '... application_name=pg-standby1 ...' application_name 必须与 synchronous_standby_names 中列出的名称匹配。
在主服务器的 postgresql.conf 中:
# 设置同步备用服务器的名称列表 # 'FIRST' 表示等待列表中的第一个响应的备用服务器 # '*' 表示等待所有列出的备用服务器 synchronous_standby_names = 'FIRST 1 (pg-standby1, pg-standby2)' # 设置同步提交级别 synchronous_commit = on 同步复制的权衡艺术 🎨
同步复制虽然安全,但也带来了复杂性。一个常见的实践是采用“多数派同步”或“异步+监控”的混合模式。
- 多数派同步:在拥有三个或以上节点的集群中,配置
synchronous_standby_names = 'ANY 2 (node1, node2, node3)'。这样,只要任意两个节点确认,事务就算成功。即使一个节点故障,系统仍能继续提供同步服务。 - 异步+强监控:使用异步复制以获得最佳性能,但部署严密的监控系统,实时跟踪主从延迟(
pg_stat_replication中的write_lag,flush_lag,replay_lag)。一旦延迟超过阈值,就发出告警,甚至自动将应用切换到只读模式,防止在主库故障时丢失过多数据。
常见问题与最佳实践 🛡️
在实际运维流复制集群时,会遇到各种挑战。以下是一些常见问题和最佳实践。
问题1:主从延迟过大
现象:备用服务器的数据明显落后于主服务器。
原因:
- 网络带宽不足或延迟高。
- 备用服务器的 I/O 性能差(尤其是磁盘写入速度慢)。
- 主服务器上有大量写入,产生 WAL 的速度超过了备用服务器应用的速度。
- 备用服务器上开启了
hot_standby_feedback,但主服务器上有长时间运行的查询,导致备用服务器无法及时应用某些 WAL(因为要保留被查询引用的旧版本数据)。
解决方案:
- 升级网络和磁盘。
- 在备用服务器上增加
max_standby_streaming_delay和max_standby_archive_delay的值,但这会增加延迟。 - 优化主服务器上的大查询。
- 监控
pg_stat_replication视图中的 lag 字段。
问题2:WAL 文件被过早清理
现象:备用服务器报错 requested WAL segment ... has already been removed。
原因:主服务器上的 wal_keep_size 设置过小,或者没有使用复制槽,导致在备用服务器断开期间,主服务器已经回收了它需要的 WAL 文件。
解决方案:
- 强烈建议使用复制槽。这是最可靠的方式。
- 如果不用复制槽,务必设置足够大的
wal_keep_size。
问题3:如何安全地升级或维护?
滚动升级:
- 先升级备用服务器。
- 执行一次手动切换(Switchover),让升级后的备用服务器成为主服务器。
- 再升级原来的主服务器(现在是备用)。
- (可选)再切回。
这种方式可以实现几乎零停机的版本升级。
最佳实践清单 ✅
- 始终使用复制槽来管理 WAL 保留。
- 监控是生命线:监控主从延迟、复制进程状态、磁盘空间(WAL 会占用大量空间)。
- 定期进行故障转移演练,确保在真实故障发生时团队能熟练应对。
- 为备用服务器配置
hot_standby = on,使其能提供只读服务。 - 考虑使用
pg_rewind工具。当主库故障后被修复,想重新加入集群作为从库时,pg_rewind可以高效地将主库(原从库)的数据同步回新主库的状态,避免了耗时的全量pg_basebackup。
结语 🌈
PostgreSQL 的流复制是一项强大而成熟的技术,它为构建企业级高可用数据库解决方案奠定了基石。通过理解其基于 WAL 的物理复制原理,我们可以更自信地进行配置和排错。结合 Java 应用,通过读写分离和故障转移策略,我们能够构建出既高性能又高可靠的后端服务。
从手动配置到借助 Patroni、ShardingSphere 等现代化工具,流复制的运维和集成方式也在不断演进。无论你是数据库管理员还是后端开发者,掌握这项技术都将为你的系统架构增添一份稳健与从容。
希望这篇详尽的指南能助你在 PostgreSQL 高可用之路上走得更远。Happy Coding! 💻✨
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨