数据库复制比你想象的更有趣
复制是数据库在硬件故障时保持可用、在多个服务器间分配读取负载以及从灾难中恢复的方式。大多数开发者了解基础知识 — 主/从复制、只读副本、故障转移。但这个领域已经显著发展。逻辑复制、变更数据捕获(CDC)和主动-主动配置已经从仅限企业使用的功能变成了每月50美元数据库实例上可用的功能。全面了解整个情况有助于你做出更好的架构决策并避免昂贵的错误。
物理复制:基础
物理(流式)复制将原始二进制数据更改从主库复制到从库 — 它在存储级别复制所有内容,无论数据库对象如何。在PostgreSQL中,这是WAL(预写日志)传输。
# 主库上的postgresql.conf
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1GB
hot_standby = on
# 主库上的pg_hba.conf — 允许从库连接
host replication replicator 10.0.1.5/32 md5
# 在从库上:从主库创建基础备份
pg_basebackup
-h primary-db
-U replicator
-D /var/lib/postgresql/16/main
-P
-Xs
-R # --write-recovery-conf: 自动创建standby.signal
然后从库会持续流式传输WAL更改。借助PostgreSQL的热备功能,从库可以在跟随主库的同时处理读取查询。
物理复制限制
- 从库必须运行与主库相同的PostgreSQL主版本
- 从库默认是只读的(热备模式)
- 无法复制到不同的数据库引擎
- 无法复制表的子集
- 整个集群都被复制 — 无法排除高频率变更的表
逻辑复制:精确与灵活
逻辑复制将WAL更改解码为行级操作(INSERT、UPDATE、DELETE)并在订阅者上重放它们。这为你提供了更大的灵活性:
- 复制特定的表,而不是整个集群
- 复制到不同的 PostgreSQL 主版本
- 过滤正在复制的行
- 在复制过程中转换数据
- 复制到非 PostgreSQL 目标(通过 CDC 消费者)
-- 在发布者(主库)上
-- postgresql.conf: wal_level = logical
-- 为特定表创建发布
CREATE PUBLICATION user_data_pub
FOR TABLE users, orders, payments
WITH (publish = 'insert, update, delete');
-- 为所有表创建发布
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
-- 在订阅者(副本)上
CREATE SUBSCRIPTION user_data_sub
CONNECTION 'host=primary-db port=5432 dbname=myapp user=replicator password=secret'
PUBLICATION user_data_pub
WITH (
connect = true,
enabled = true,
copy_data = true, -- 初始数据同步
create_slot = true
);
-- 检查复制状态
SELECT * FROM pg_stat_subscription;
SELECT * FROM pg_replication_slots;
几乎零停机的主版本升级
逻辑复制为 PostgreSQL 主版本之间提供了一条实用的迁移路径,且停机时间极短:
- 设置一个运行 PG 17 的新集群,作为你的 PG 16 主库的逻辑订阅者
- 让它同步并追上(根据数据量可能需要数小时/天)
- 在维护窗口期间:停止向 PG 16 写入,验证 PG 17 已完全追上,提升 PG 17,更新应用连接字符串
- 停机时间:通常不到 1 分钟
变更数据捕获:将您的数据库转变为事件流
CDC 通过将每个数据库更改视为其他系统可以消费的事件,进一步扩展了逻辑复制的功能。与应用程序在数据库写入后显式发布事件到 Kafka 不同,数据库本身成为了事件的真相来源。
标准设置使用 Debezium,这是一个开源的 CDC 工具,它读取 PostgreSQL WAL 并发布到 Kafka:
-- postgresql.conf: wal_level = logical
-- 为 Debezium 创建复制槽
SELECT pg_create_logical_replication_slot(
'debezium_slot',
'pgoutput'
);
# Debezium 连接器配置(Kafka Connect)
{
"name": "postgres-source-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "primary-db",
"database.port": "5432",
"database.user": "debezium",
"database.password": "secret",
"database.dbname": "myapp",
"database.server.name": "myapp",
"plugin.name": "pgoutput",
"slot.name": "debezium_slot",
"table.include.list": "public.orders,public.payments",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.delete.handling.mode": "rewrite",
"topic.prefix": "myapp"
}
}
orders 表中的每一行变更都会成为 myapp.public.orders 主题上的 Kafka 消息:
{
"before": null,
"after": {
"id": 9821,
"user_id": 4821,
"status": "completed",
"total_cents": 4999,
"updated_at": 1742041234000000
},
"op": "c", // c=create, u=update, d=delete
"ts_ms": 1742041234123,
"transaction": {
"id": "7:45982012",
"total_order": 1,
"data_collection_order": 1
}
}
证明设置复杂性的 CDC 用例
- 缓存失效:当底层数据库行发生变化时使 Redis 缓存条目失效 — 而无需在应用程序代码中混入缓存逻辑
- 搜索索引更新:自动将变更流式传输到 Elasticsearch 或 Typesense
- 事件溯源而无事件溯源开销:使用现有数据库作为事实来源,从变更中派生事件
- 审计日志:捕获敏感表的每次变更,包含完整的前后值
- 跨服务数据同步:将特定表复制到单独的分析数据库,而无需在应用程序代码中进行双写
主动-主动复制:难题所在
物理复制和逻辑复制都是主动-被动的:一个主节点接受写入,副本节点跟随。主动-主动(多主)复制允许多个节点进行写入,变更双向复制。这听起来很有吸引力,但存在一个根本问题:写入冲突。
当节点 A 和节点 B 同时更新同一行时会发生什么?冲突解决策略:
- 最后写入获胜:具有最新时间戳的变更获胜。简单,但可能会静默丢弃写入。
- 最先写入获胜:最先到达的变更获胜。在某些用例中更安全。
- 应用程序级解决:您的应用程序定义合并逻辑。复杂但正确。
Citus(用于水平分片)和 CockroachDB(用于地理分布式主动-主动)通过不同的权衡解决了这个问题:
-- CockroachDB: 跨区域的主动-主动复制
-- 多区域无需配置 — 内置于 SQL 层中
-- 设置表的主区域并容忍区域故障
ALTER TABLE orders SET LOCALITY REGIONAL BY ROW;
-- 查询自动路由到最近的区域
-- 涉及"us-east"中行的写入会发送到 us-east 节点
监控复制延迟:关键指标
复制延迟是指在主库上写入操作与其在副本上出现之间的延迟。对于面向用户查询的只读副本,过时数据是一个正确性问题。请监控它:
-- 在主库上:检查每个副本的复制延迟
SELECT
application_name,
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
write_lag,
flush_lag,
replay_lag,
sync_state
FROM pg_stat_replication;
-- 在副本上:检查它落后多远
SELECT
now() - pg_last_xact_replay_timestamp() AS replication_lag,
pg_is_in_recovery() AS is_replica;
# 用于复制延迟的 Prometheus 告警规则
- alert: PostgresReplicationLagHigh
expr: pg_replication_lag_seconds > 30
for: 5m
labels:
severity: warning
annotations:
summary: "在 {{ $labels.instance }} 上复制延迟为 {{ $value }}s"
选择合适的复制策略
基于您需求的决策框架:
- 读取扩展 + 高可用故障转移:物理流复制。简单、成熟,内置在 Postgres 中。
- 主版本升级且最小停机时间:版本间的逻辑复制。
- 选择性表复制:使用发布物的逻辑复制。
- 数据库作为事件源:使用 Debezium + Kafka 的 CDC。
- 多区域主动-主动:如果需要,可以使用 CockroachDB 或 Spanner;大多数团队不需要。
- 分析工作负载分离:CDC 到读取优化的存储(Redshift、BigQuery、ClickHouse)。
大多数中小型应用程序只需要一件事:流式物理复制到热备用服务器以实现高可用性。当您有特定的多系统一致性要求时,逻辑复制和 CDC 的复杂性才是合理的。从简单开始,只有当您有更简单方法无法解决的具体问题时,才增加复杂性。
