由 John Doe 十一月 28, 2025
借助 Spock 可以实现 PostgreSQL 中的多主(多活)数据复制,还可大幅减少多主数据库集群中出现的各类冲突。

通常情况下,PostgreSQL 中的数据复制是在一个主数据库与一个或多个备用数据库之间进行的。尽管这种方式对许多应用场景而言已足够,且能实现高可用性,但有时你需要在多个活跃数据库之间复制数据。借助多活数据库集群,你不仅可以将读查询分布到集群中的多个数据库,还能将插入和更新操作也分布到这些数据库中。这一特性支持并行工作负载,还能让数据更贴近终端用户,从而降低延迟,并打造出现代化的均衡分布式架构。
技术背景
物理复制采用精确的块地址和逐字节复制的方式。在 PostgreSQL 中,这种复制方式常被用于创建只读副本,该副本既可作为热备数据库,也能作为应用的额外只读数据库。
与之不同的是,逻辑复制通过主键来复制数据对象及其变更。它并非将包含数据库中所有对象当前状态的 WAL 预写日志文件传输到处于恢复模式的、与源数据库完全匹配的目标数据库,而是利用发布和订阅机制,对指定对象的插入、更新和删除操作进行复制。因此,逻辑复制的配置可以更精细,成为现代数据库的强大工具。
为何逻辑复制支持多主复制
逻辑复制允许你将复制范围限定在特定数据库,还提供行级过滤选项。基于此,你可以配置从数据库 A 复制到数据库 B,同时从数据库 B 复制回数据库 A。这种多向逻辑复制意味着两个数据库都无需处于恢复模式,且每个数据库都可接收写入操作,通过双向复制保持数据同步。
这也就意味着应用拥有了多个写入点。除了支持多主集群外,数据库版本的重要性也有所降低,例如,你可以让 PostgreSQL 16 版本的数据库与 17 版本的数据库进行复制,且两个版本的数据库都可接收写入操作,从而减少系统停机时间。
Spock 带来的优势
Spock 扩展引入了异步多主复制功能,并增强了冲突解决与冲突避免能力,同时还提供了更完善的管理、监控统计以及集成功能。
当多个数据库同时发生更新操作时,就需要进行冲突解决。例如,在数据库 A 中更新某一行数据,同时在数据库 B 中对同一行数据进行不同的更新,此时便会产生冲突。借助 Spock,“最后更新胜出”机制会生效,该行数据最终会保留最后一次提交的更新值,且整个过程不会出现故障。此外,Spock 还提供了一个冲突解决表,用于记录所有冲突解决情况,以便用户进行监控和分析。
另一种冲突可能出现在对递增字段或求和字段的更新操作中。例如,在数据库 A 中给某个字段增加 5,同时在数据库 B 中给同一个字段增加 10,若采用“最后更新胜出”的方式,最终结果只会是增加 5 或 10,而非预期的增加 15。Spock 通过“无冲突增量应用列”解决了这一问题,它会根据更新的增量来修改该列的值,逻辑复制会将这个增量传输到其他数据库,因此在上述示例中,该字段的最终值会正确地增加 15。
Spock 还支持分区表。你可以选择将分区表的父表或特定的分区表添加到复制范围中。这一特性支持地理分片:例如,某些分区的数据可在不同国家之间进行复制,而其他分区的数据则仅保留在原始所在国家的数据库中。
使用 Spock 创建双节点集群
在安装并初始化 Postgres 且创建好 Spock 扩展后,你可按照以下步骤配置双节点集群。在后续示例中,我们将创建一个包含两个节点(分别命名为n1和n2)的集群,这两个节点均在5432端口监听 Postgres 服务器连接。
1. 连接到每个节点,并执行以下命令初始化集群:
sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
sudo systemctl enable postgresql-17
2. 编辑postgresql.conf文件,并在文件末尾添加以下参数:
wal_level = 'logical'
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = 'spock'
track_commit_timestamp = on
3. 编辑 pg_hba.conf 文件,允许n1和n2之间建立连接。以下命令仅作为示例,不建议在生产环境中使用,因为它们会允许任何客户端连接你的系统:
host all all 0.0.0.0/0 trust
local replication all trust
host replication all 0.0.0.0/0 trust
4. 使用spock.node_create命令创建提供者节点和订阅者节点:
- 在
n1上执行以下命令:SELECT spock.node_create (node_name := 'n1', dsn := 'host=<n1_ip_address> port=<n1_port> dbname=<db_name>'); - 在
n2上执行以下命令:SELECT spock.node_create (node_name := 'n2', dsn := 'host=<n2_ip_address> port=<n2_port> dbname=<db_name>');
5. 在n1上,使用spock.repset_add_all_tables命令将public模式下的表添加到default复制集。若你使用其他模式,可根据需求自定义此命令:
SELECT spock.repset_add_all_tables('default', ARRAY['public']);
6. 在n2上,使用spock.sub_create命令创建n2与n1之间的订阅,订阅名称为sub_n2_n1:
SELECT spock.sub_create (subscription_name := 'sub_n2_n1', provider_dsn := 'host=<n1_ip_address> port=<n1_port> dbname=<db_name>');
SELECT spock.sub_wait_for_sync('sub_n2_n1');
7. 在n1上,创建一个指向n2的对应订阅,名称为sub_n1_n2:
SELECT spock.sub_create (subscription_name := 'sub_n1_n2', subscriber_dsn := 'host=<n2_ip_address> port=<n2_port> dbname=<db_name>');
8. 若要确保 DDL 语句的修改能自动复制,需使用 Postgres 客户端连接每个节点,并执行以下 SQL 命令:
ALTER SYSTEM SET spock.enable_ddl_replication=on;
ALTER SYSTEM SET spock.include_ddl_repset=on;
ALTER SYSTEM SET spock.allow_ddl_from_functions=on;
SELECT pg_reload_conf();
9. 随后,检查各节点状态:
SELECT * FROM spock.node;
node_id | node_name | location | country | info
---------+-------------+----------+---------+------
22201 | subscriber1 | | |
53107 | provider1 | | |
(2 rows)
SELECT * FROM spock.sub_show_status();
subscription_name | status | provider_node | provider_dsn | slot_name | replication_sets | forward_origins
-------------------+-------------+---------------+------------------------------------------+--------------------------------------+---------------------------------------+-----------------
subscription1 | replicating | provider1 | host=localhost port=5432 dbname=postgres | spk_postgres_provider1_subscription1 | {default,default_insert_only,ddl_sql} |
(1 row)
验证系统是否正常复制的简单方法:连接到n1上的 Postgres 服务器,创建一个对象(如一张表),然后确认该对象在n2上可访问;同理,也可在n2上创建对象,再确认其在n1上已同步创建。
参考
Spock:https://github.com/pgEdge/spock