大数跨境
0
0

Spock:让PostgreSQL多主复制像搭乐高一样简单

Spock:让PostgreSQL多主复制像搭乐高一样简单 KiKi闯外贸
2025-10-11
0
导读:一、故事从“数据不同步”开始想象一下这个场景:你在公司负责一个电商平台的订单系统,数据库用的是 Postgre

一、故事从“数据不同步”开始

想象一下这个场景:

你在公司负责一个电商平台的订单系统,数据库用的是 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 版”。

主要区别在哪?

对比项
pglogical
Spock
是否支持多主
❌ 最多双主
✅ 真·多主(N 节点)
冲突检测与处理
基础支持
更完善,支持多种策略
DDL 复制
需额外配置
支持自动 DDL 同步
社区活跃度
已停止维护
持续更新(截至 2025 年仍在迭代)
安装方式
直接安装扩展
需要先打补丁再编译

所以一句话总结:

Spock = pglogical + 多主增强 + 更强的冲突管理 + 持续维护

如果你现在要做多主架构,别犹豫,直接上 Spock。


三、原理揭秘:它是怎么做到“多地可写”的?

3.1 核心思想:日志解析 + 事件广播

Spock 的底层依然是 PostgreSQL 的 逻辑复制(Logical Replication) 机制。

我们先简单回顾一下什么是逻辑复制:

  • PostgreSQL 在执行 INSERT/UPDATE/DELETE 时,会在 WAL 日志中记录变更。
  • 逻辑复制器(如 pgoutput 插件)可以“读懂”这些日志,把变更翻译成 SQL 语句或行级事件。
  • 这些事件可以通过网络发送给另一个数据库,并在那里重放。

Spock 就是在这个基础上做了深度扩展:

  1. 每个节点既是发布者(publisher),也是订阅者(subscriber)
  2. 变更事件通过“广播”方式发给所有其他节点
  3. 每个节点收到后判断是否需要应用(避免循环复制)
  4. 如果发生冲突(比如两地同时改同一行),启动冲突解决策略

这就实现了“哪里都能写,最终还能一致”。


3.2 关键组件一览

Spock 内部有几个核心概念,理解它们就等于掌握了方向盘:

📦 1. 节点(Node)

每一个参与复制的 PostgreSQL 实例就是一个 Node。

你可以给它起名字,比如 node_nynode_shnode_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 的场景:

场景
说明
多地多活
北上广深都能写,降低延迟
数据合并
分公司各自维护本地库,总部汇总
灰度发布
新旧版本数据库并行运行,逐步迁移
容灾备份
主库挂了,备库可立即接管写入

❌ 不适合的场景:

场景
原因
高频写密集型应用
如金融交易系统,冲突太多,难以控制
超大规模集群(>10 节点)
全连接拓扑开销大,管理复杂
强一致性要求
Spock 是最终一致性,不适合银行账务

🔄 替代方案对比

方案
特点
推荐指数
Spock
多主、逻辑复制、PG 原生
⭐⭐⭐⭐☆
BDR (Bi-Directional Replication)
也是多主,但已停止维护
⭐⭐
Citus
分布式 PG,适合水平分片
⭐⭐⭐⭐
Patroni + etcd + WAL
高可用主从,非多主
⭐⭐⭐⭐
TimescaleDB Native Replication
时序专用,功能受限
⭐⭐⭐

结论:要做多主,Spock 仍是目前最靠谱的选择


八、未来展望:Spock 会走向何方?

根据 GitHub 更新日志,Spock 团队正在推进几个方向:

  • 更好的自动化冲突修复(AI 辅助?)
  • 支持 JSONB 字段的部分更新同步
  • 与 Kubernetes 集成,实现云原生部署
  • 提供图形化管理界面(Web UI)

说不定哪天就能像 MongoDB Atlas 一样,一键开启多主复制。

而你现在掌握的知识,正是未来的“先发优势”。


九、结语:技术的本质是解决问题

回到开头那个问题:

“我们能不能做到多地可写、数据一致?”

答案是:能,用 Spock。

它不是银弹,也有局限,但它让你在不换数据库的前提下,拥有了接近分布式数据库的能力。

这就像给一辆家用轿车加装四驱系统——不用换 SUV,也能走烂路。

技术的魅力就在于此:用最小的成本,撬动最大的可能性

参考资料

[1] 

pgEdge: https://github.com/pgEdge/spock


【声明】内容源于网络
0
0
KiKi闯外贸
跨境分享家 | 持续更新跨境思考
内容 47304
粉丝 0
KiKi闯外贸 跨境分享家 | 持续更新跨境思考
总阅读234.5k
粉丝0
内容47.3k