一、故事从“数据不同步”开始
想象一下这个场景:
你在公司负责一个电商平台的订单系统,数据库用的是 PostgreSQL,一切运行良好。
突然某天,老板说:“我们要搞全国多数据中心部署!北京、上海、深圳各建一个节点,用户就近访问,提升体验。”
你一听,好家伙,高大上啊!
但问题来了:三个城市都有用户下单,怎么保证订单数据一致?
你试了传统的主从复制——行,但只能写主库,其他地方还是得走网络回源,延迟爆炸。
你又想用双主复制——结果两个节点同时插入同一条订单 ID,冲突了,数据乱了,财务哭着找你算账。
最后你只能妥协:每个城市只读,写还得去北京总部。用户体验没提升,运维复杂度翻倍。
这不是你的错,是传统 PostgreSQL 的局限。
直到有一天,你在 GitHub 上刷到了一个叫 Spock 的项目:
“Logical multi-master PostgreSQL replication”
——逻辑型多主 PostgreSQL 复制
你点进去一看,心猛地跳了一下:这玩意儿,能解决我的痛点!
于是,你决定深入了解它。而这篇文章,就是帮你省下那几十个小时踩坑时间的“避坑指南”。
二、什么是 Spock?PostgreSQL 的“分布式外挂”
2.1 它不是魔法,但比魔法还神奇
Spock 是由 pgEdge[1] 开发的一个 PostgreSQL 扩展(extension),目标非常明确:
让 PostgreSQL 支持真正意义上的 逻辑多主复制(Logical Multi-Master Replication)。
什么意思?
-
多个 PostgreSQL 实例都可以接受写操作; -
每个实例的数据会自动同步到其他实例; -
即使两边同时改同一条记录,也能智能解决冲突; -
不依赖共享存储,纯软件实现,部署灵活。
听起来是不是像 MySQL 的 Group Replication 或者 MongoDB 的副本集?没错,但它基于 PostgreSQL 原生的 逻辑复制机制(Logical Replication) 构建,更加轻量、可控、可定制。
而且它是开源的,MIT 许可,GitHub 上已经有 453 个 star,虽然不算爆款,但在专业圈子里已经是“低调王者”。
2.2 和 pglogical 的关系:亲兄弟,升级版
你可能听说过另一个项目叫 pglogical,它是两节点逻辑复制的老牌选手。
而 Spock 正是从 pglogical 发展而来,可以说是它的“加强 Pro Max 版”。
主要区别在哪?
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
所以一句话总结:
Spock = pglogical + 多主增强 + 更强的冲突管理 + 持续维护
如果你现在要做多主架构,别犹豫,直接上 Spock。
三、原理揭秘:它是怎么做到“多地可写”的?
3.1 核心思想:日志解析 + 事件广播
Spock 的底层依然是 PostgreSQL 的 逻辑复制(Logical Replication) 机制。
我们先简单回顾一下什么是逻辑复制:
-
PostgreSQL 在执行 INSERT/UPDATE/DELETE 时,会在 WAL 日志中记录变更。 -
逻辑复制器(如 pgoutput插件)可以“读懂”这些日志,把变更翻译成 SQL 语句或行级事件。 -
这些事件可以通过网络发送给另一个数据库,并在那里重放。
Spock 就是在这个基础上做了深度扩展:
-
每个节点既是发布者(publisher),也是订阅者(subscriber) -
变更事件通过“广播”方式发给所有其他节点 -
每个节点收到后判断是否需要应用(避免循环复制) -
如果发生冲突(比如两地同时改同一行),启动冲突解决策略
这就实现了“哪里都能写,最终还能一致”。
3.2 关键组件一览
Spock 内部有几个核心概念,理解它们就等于掌握了方向盘:
📦 1. 节点(Node)
每一个参与复制的 PostgreSQL 实例就是一个 Node。
你可以给它起名字,比如 node_ny, node_sh, node_bj。
SELECT spock.create_node(
node_name := 'node_bj',
dsn := 'host=192.168.1.10 port=5432 dbname=orders'
);
🔔 2. 复制槽(Replication Slot)
这是 PostgreSQL 提供的一种机制,用来确保 WAL 日志不会被过早清理。
Spock 为每个订阅关系创建一个逻辑复制槽,保障数据不丢。
🔄 3. 订阅(Subscription)
定义了一个节点如何从另一个节点拉取数据。
SELECT spock.create_subscription(
subscription_name := 'sub_to_shanghai',
provider_dsn := 'host=192.168.2.10 port=5432 dbname=orders'
);
注意:在多主模式下,每个节点都要订阅其他所有节点,形成全连接拓扑。
🧩 4. 冲突检测与解决(Conflict Resolution)
这才是多主系统的灵魂。
举个例子:
-
北京用户把订单状态改成“已发货” -
上海用户在同一时间把状态改成“待退款” -
两边都提交了事务
这时候如果不处理,数据就会不一致。
Spock 提供了几种默认策略:
|
|
|
|---|---|
last_update_wins |
|
first_update_wins |
|
apply_remote |
|
keep_local |
|
这些策略可以在建表时指定,也可以全局设置。
而且 Spock 会记录冲突日志,方便事后审计。
四、动手实战:手把手教你搭一个多主集群
下面我们来真刀真枪地搭建一个 双节点多主复制环境。
假设你有两台服务器:
-
Node A: 192.168.1.10 -
Node B: 192.168.1.20
操作系统均为 Ubuntu 22.04,PostgreSQL 版本为 16。
第一步:准备环境
确保两台机器能互相 ping 通,并开放 5432 端口。
安装基础依赖:
sudo apt update
sudo apt install -y build-essential libpq-dev postgresql-server-dev-16 git python3-pip
第二步:下载并编译带补丁的 PostgreSQL
⚠️ 注意:Spock 不是直接 CREATE EXTENSION 就完事的,它需要对 PostgreSQL 源码打补丁!
1. 下载 PostgreSQL 16 源码
wget https://ftp.postgresql.org/pub/source/v16.0/postgresql-16.0.tar.gz
tar -xzf postgresql-16.0.tar.gz
cd postgresql-16.0
2. 下载 Spock 并应用补丁
git clone https://github.com/pgEdge/spock.git
cd spock
# 查看补丁目录
ls patches/pg16/
# 输出类似:
# pg16-015-add-spock-support.diff
# pg16-020-enable-ddl-replication.diff
把这些补丁依次打到 PG 源码中:
cd ../postgresql-16.0
patch -p1 < ../spock/patches/pg16/pg16-015-add-spock-support.diff
patch -p1 < ../spock/patches/pg16/pg16-020-enable-ddl-replication.diff
# 继续打完所有补丁……
📌 重要提示:必须按数字顺序打补丁!否则编译失败。
3. 编译安装 PostgreSQL
./configure --prefix=/usr/local/pgsql
make -j$(nproc)
sudo make install
4. 初始化数据库集群
sudo mkdir /usr/local/pgsql/data
sudo chown $USER:$USER /usr/local/pgsql/data
/usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data
5. 启动 PostgreSQL
编辑 postgresql.conf:
wal_level = logical
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
track_commit_timestamp = on # 冲突解决必需!
shared_preload_libraries = 'spock'
启动服务:
/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start
第三步:安装 Spock 扩展
回到 Spock 目录:
cd ../spock
export PATH=/usr/local/pgsql/bin:$PATH
make && make install
如果成功,你会看到:
cp sql/spock.sql sql/16/spock--1.0.sql
cp spock.so /usr/local/pgsql/lib/spock.so
cp spock.control /usr/local/pgsql/share/extension/
说明安装完成。
第四步:创建数据库和扩展
连接数据库:
/usr/local/pgsql/bin/psql -p 5432 -U postgres
执行:
CREATE DATABASE orders;
\c orders
CREATE EXTENSION spock;
另一台机器也做同样的操作。
第五步:配置多主复制
在 Node A 上创建节点
-- 创建本地节点
SELECT spock.create_node(
node_name := 'node_a',
dsn := 'host=192.168.1.10 port=5432 dbname=orders user=postgres'
);
-- 订阅 Node B
SELECT spock.create_subscription(
subscription_name := 'sub_to_node_b',
provider_dsn := 'host=192.168.1.20 port=5432 dbname=orders user=postgres password=yourpass'
);
在 Node B 上同样操作
SELECT spock.create_node(
node_name := 'node_b',
dsn := 'host=192.168.1.20 port=5432 dbname=orders user=postgres'
);
-- 订阅 Node A
SELECT spock.create_subscription(
subscription_name := 'sub_to_node_a',
provider_dsn := 'host=192.168.1.10 port=5432 dbname=orders user=postgres password=yourpass'
);
等待几秒,系统自动建立双向复制通道。
第六步:测试多主写入
随便建个表试试:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
注意:这个 DDL 会被自动复制到另一端!
然后分别在两个节点插入数据:
📍 Node A:
INSERT INTO users(name, city) VALUES ('张三', '北京');
📍 Node B:
INSERT INTO users(name, city) VALUES ('李四', '上海');
查一下两边的数据:
SELECT * FROM users;
你会发现:两边都有两条记录!🎉
这就是多主的魅力:谁都可以写,大家都知道。
五、高级玩法:DDL 复制、冲突处理、监控告警
5.1 自动 DDL 复制:再也不用手动同步结构
传统逻辑复制有个致命缺点:只复制 DML(增删改),不复制 DDL(建表、加字段等)。
这意味着你每改一次表结构,就得手动在每个节点执行一遍,容易出错。
而 Spock 支持 DDL 复制,只要你开启相关参数:
# postgresql.conf
shared_preload_libraries = 'spock'
spock.replicate_ddl_command = 'BOTH' -- BOTH 表示接收和转发 DDL
然后你在一个节点执行:
ALTER TABLE users ADD COLUMN age INT;
几秒钟后,另一个节点也会自动执行这条命令!
💡 原理是:Spock 把 DDL 包装成特殊的消息,通过复制流广播出去,接收方解析后执行。
当然,也有风险:万一误删表怎么办?
所以建议生产环境加上审核机制,或者使用 REPLICATE_DDL_COMMAND = 'LOCAL' 控制范围。
5.2 冲突处理实战:当两个人同时改同一条数据
让我们模拟一场“战争”。
📍 Node A:
UPDATE users SET city = '杭州' WHERE name = '张三';
COMMIT;
几乎同时,📍 Node B:
UPDATE users SET city = '成都' WHERE name = '张三';
COMMIT;
这两个事务几乎同时提交,谁该赢?
Spock 默认使用 last_update_wins 策略,也就是 提交时间晚的获胜。
它依靠的是 track_commit_timestamp = on 这个参数记录的真实提交时间。
你可以查看冲突日志:
SELECT * FROM spock.local_sync_status WHERE status = 'replication conflict';
输出可能是:
node_id | relation | remote_tuple | local_tuple | reason | action_taken
--------+----------+--------------+-------------+--------+-------------
node_b | users | {city:成都} | {city:杭州} | update-update | keep_remote
表示远程更新胜出,本地被覆盖。
如果你想换策略,可以这样设置:
ALTER TABLE users REPLICA IDENTITY FULL; -- 必须开启才能精确比较
SELECT spock.alter_table_configure_conflict_detection('users', 'first_update_wins');
甚至还可以自定义函数来做更复杂的决策,比如“按业务优先级裁决”。
5.3 监控你的复制集群
一个好的系统离不开监控。
Spock 提供了多个视图帮助你掌握集群状态:
|
|
|
|---|---|
spock.node_info |
|
spock.replication_set |
|
spock.subscription |
|
spock.status |
|
spock.conflict_log |
|
常用查询:
-- 查看订阅延迟
SELECT sub_name, sub_slot_name, sub_last_msg_receipt_time FROM spock.subscription;
-- 查看是否有错误
SELECT * FROM spock.subscription_status WHERE status != 'replicating';
-- 查看最近的冲突
SELECT * FROM spock.conflict_log ORDER BY log_time DESC LIMIT 10;
你还可以把这些指标接入 Prometheus + Grafana,做可视化大盘。
六、避坑指南:那些没人告诉你的“潜规则”
⚠️ 坑 1:表必须有主键!
Spock 要求所有被复制的表必须有主键(Primary Key)或唯一索引(Unique Index)且不能含空值。
为什么?
因为逻辑复制靠主键来定位哪一行被修改了。没有主键,就无法精准同步。
❌ 错误示例:
CREATE TABLE logs (msg text); -- 没主键,无法复制!
✅ 正确做法:
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
msg text
);
⚠️ 坑 2:序列(Sequence)不同步!
虽然 Spock 能复制表数据,但 序列值本身不会自动同步。
这意味着:
-
Node A 的 users_id_seq当前是 100 -
Node B 的 users_id_seq当前是 105 -
如果你不干预,可能会出现主键冲突
解决方案有两种:
方案一:使用 UUID 主键
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT
);
彻底避开自增冲突。
方案二:手动同步序列或分段分配
例如:
-
Node A 使用奇数 ID: ALTER SEQUENCE users_id_seq INCREMENT BY 2 START WITH 1; -
Node B 使用偶数 ID: INCREMENT BY 2 START WITH 2;
这样就不会撞车。
⚠️ 坑 3:网络分区怎么办?脑裂预警!
多主系统最怕的就是 网络分区(Network Partition)。
比如北京和上海之间的专线断了,两边都以为对方挂了,继续接受写请求。
等网络恢复时,发现数据已经严重不一致。
这种情况称为“脑裂(Split-Brain)”。
Spock 本身不提供自动仲裁机制,需要你自己设计:
-
使用第三方协调服务(如 etcd、Consul)投票决定哪个节点继续服务 -
或者强制关闭其中一个节点的写权限 -
或者采用“中心控制节点”模式降级为单主
📌 生产环境务必制定应急预案!
⚠️ 坑 4:大事务性能下降
Spock 是基于逻辑复制的,意味着每条变更都要被解析、打包、传输、重放。
如果你一次性更新 100 万行数据,会导致:
-
复制延迟飙升 -
WAL 日志暴涨 -
内存占用过高
建议:
-
大批量操作拆分成小批次 -
临时暂停订阅,操作完再重新同步 -
或者走“物理复制 + 切换”流程
七、适用场景 vs 替代方案
✅ 适合用 Spock 的场景:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
❌ 不适合的场景:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
🔄 替代方案对比
|
|
|
|
|---|---|---|
| Spock |
|
|
| BDR (Bi-Directional Replication) |
|
|
| Citus |
|
|
| Patroni + etcd + WAL |
|
|
| TimescaleDB Native Replication |
|
|
结论:要做多主,Spock 仍是目前最靠谱的选择。
八、未来展望:Spock 会走向何方?
根据 GitHub 更新日志,Spock 团队正在推进几个方向:
-
更好的自动化冲突修复(AI 辅助?) -
支持 JSONB 字段的部分更新同步 -
与 Kubernetes 集成,实现云原生部署 -
提供图形化管理界面(Web UI)
说不定哪天就能像 MongoDB Atlas 一样,一键开启多主复制。
而你现在掌握的知识,正是未来的“先发优势”。
九、结语:技术的本质是解决问题
回到开头那个问题:
“我们能不能做到多地可写、数据一致?”
答案是:能,用 Spock。
它不是银弹,也有局限,但它让你在不换数据库的前提下,拥有了接近分布式数据库的能力。
这就像给一辆家用轿车加装四驱系统——不用换 SUV,也能走烂路。
技术的魅力就在于此:用最小的成本,撬动最大的可能性。
参考资料
pgEdge: https://github.com/pgEdge/spock

