Skip to content

PostgreSQL 多活灾备设计

核心概念

多活灾备设计是指在多个地理位置分散的数据中心同时部署和运行数据库服务,实现业务的持续可用性和灾难恢复能力。主要目标包括:

  • 业务连续性:在任何一个数据中心发生故障时,其他数据中心可以继续提供服务
  • 负载均衡:将业务流量分布到多个数据中心,提高系统整体性能
  • 灾难恢复:实现RTO(恢复时间目标)接近零的灾难恢复能力
  • 地域就近访问:用户可以访问就近的数据中心,降低网络延迟

多活灾备设计的关键要素:

  • 数据同步:确保多个数据中心之间的数据一致性
  • 冲突处理:处理并发写入导致的数据冲突
  • 流量路由:将用户请求路由到合适的数据中心
  • 故障检测:自动检测数据中心或数据库故障
  • 自动切换:在故障发生时自动切换流量

多活灾备架构设计

1. 基于地理位置的多活架构

1.1 同城多活

多个数据中心位于同一城市,网络延迟低(< 10ms),适合对延迟敏感的业务:

数据中心A ↔ 数据中心B
  ↓          ↓
应用连接   应用连接

1.2 跨城多活

数据中心分布在不同城市,网络延迟中等(10-100ms),适合需要地域容灾的业务:

数据中心A(北京) ↔ 数据中心B(上海)
  ↓                    ↓
北京用户连接       上海用户连接

1.3 跨国多活

数据中心分布在不同国家或大洲,网络延迟高(> 100ms),适合全球化业务:

数据中心A(亚洲) ↔ 数据中心B(欧洲) ↔ 数据中心C(美洲)
  ↓                    ↓                    ↓
亚洲用户连接       欧洲用户连接       美洲用户连接

2. 基于数据同步的多活架构

2.1 双向同步架构

使用PostgreSQL逻辑复制实现双向数据同步:

数据库A(主) ↔ 数据库B(主)
  ↓               ↓
应用连接        应用连接

2.2 共享存储架构

多个数据库实例共享同一存储设备,实现数据的实时共享:

数据库A ←→ 共享存储 ←→ 数据库B
  ↓                      ↓
应用连接               应用连接

2.3 基于中间件的多活架构

使用数据库中间件实现多活部署和流量路由:

应用 → 数据库中间件 → 数据库A

                     数据库B

多活灾备实现方法

1. 基于逻辑复制的双向同步

这是最常用的多活灾备实现方法,使用PostgreSQL逻辑复制功能实现双向数据同步:

配置步骤

bash
# 1. 配置数据库A(主1)
# 修改postgresql.conf
cat >> /var/lib/postgresql/15/main/postgresql.conf << EOF
# 逻辑复制配置
wal_level = logical
max_wal_senders = 10
max_replication_slots = 10
max_worker_processes = 10
max_logical_replication_workers = 4
EOF

# 修改pg_hba.conf
cat >> /var/lib/postgresql/15/main/pg_hba.conf << EOF
# 允许逻辑复制连接
host replication replicator 0.0.0.0/0 md5
EOF

# 重启数据库A
pg_ctl restart -D /var/lib/postgresql/15/main

# 2. 配置数据库B(主2)
# 同样修改postgresql.conf和pg_hba.conf,配置与数据库A相同
# 略

# 3. 在数据库A上创建发布
psql -h 数据库A_IP -U postgres -d mydb -c "CREATE PUBLICATION pub_a FOR ALL TABLES;"

# 4. 在数据库B上创建订阅,指向数据库A
psql -h 数据库B_IP -U postgres -d mydb -c "CREATE SUBSCRIPTION sub_b_to_a CONNECTION 'host=数据库A_IP port=5432 dbname=mydb user=replicator password=rep_pass' PUBLICATION pub_a;"

# 5. 在数据库B上创建发布
psql -h 数据库B_IP -U postgres -d mydb -c "CREATE PUBLICATION pub_b FOR ALL TABLES;"

# 6. 在数据库A上创建订阅,指向数据库B
psql -h 数据库A_IP -U postgres -d mydb -c "CREATE SUBSCRIPTION sub_a_to_b CONNECTION 'host=数据库B_IP port=5432 dbname=mydb user=replicator password=rep_pass' PUBLICATION pub_b;"

# 7. 验证双向同步
# 在数据库A上创建测试表并插入数据
psql -h 数据库A_IP -U postgres -d mydb -c "CREATE TABLE test (id serial primary key, name text); INSERT INTO test (name) VALUES ('test from A');"

# 在数据库B上验证数据是否同步
psql -h 数据库B_IP -U postgres -d mydb -c "SELECT * FROM test;"

# 在数据库B上插入数据
psql -h 数据库B_IP -U postgres -d mydb -c "INSERT INTO test (name) VALUES ('test from B');"

# 在数据库A上验证数据是否同步
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM test;"

2. 基于pgpool-II的多活部署

使用pgpool-II实现多活部署和负载均衡:

配置步骤

bash
# 1. 安装pgpool-II
# 略

# 2. 配置pgpool-II
cat > /etc/pgpool2/pgpool.conf << EOF
# 基础配置
listen_addresses = '*'
port = 9999

# 后端数据库配置
backend_hostname0 = '数据库A_IP'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/var/lib/postgresql/15/main'
backend_flag0 = 'ALLOW_TO_FAILOVER'

backend_hostname1 = '数据库B_IP'
backend_port1 = 5432
backend_weight1 = 1
backend_data_directory1 = '/var/lib/postgresql/15/main'
backend_flag1 = 'ALLOW_TO_FAILOVER'

# 负载均衡配置
load_balance_mode = on

# 复制模式配置
replication_mode = on
replicate_select = off
insert_lock = on

# 健康检查配置
health_check_period = 30
health_check_timeout = 20
health_check_user = 'postgres'
health_check_password = 'postgres_pass'
EOF

# 3. 启动pgpool-II
systemctl start pgpool2

# 4. 验证多活部署
# 连接到pgpool-II
psql -h pgpool_IP -p 9999 -U postgres -d mydb

# 插入测试数据
INSERT INTO test (name) VALUES ('test via pgpool');

# 在数据库A和数据库B上验证数据是否同步
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM test;"
psql -h 数据库B_IP -U postgres -d mydb -c "SELECT * FROM test;"

3. 基于PostgreSQL BDR的多活部署

BDR(Bi-Directional Replication)是PostgreSQL的一个扩展,提供高级的双向复制功能:

配置步骤

bash
# 1. 安装BDR扩展
# 略

# 2. 配置数据库A
# 修改postgresql.conf
cat >> /var/lib/postgresql/15/main/postgresql.conf << EOF
# BDR配置
shared_preload_libraries = 'bdr'
bdr.connections = 'db_b'
bdr.dbname = 'mydb'
bdr.node_name = 'node_a'
bdr.node_external_dsn = 'host=数据库A_IP port=5432 dbname=mydb user=postgres'
EOF

# 3. 配置数据库B
# 修改postgresql.conf
cat >> /var/lib/postgresql/15/main/postgresql.conf << EOF
# BDR配置
shared_preload_libraries = 'bdr'
bdr.connections = 'db_a'
bdr.dbname = 'mydb'
bdr.node_name = 'node_b'
bdr.node_external_dsn = 'host=数据库B_IP port=5432 dbname=mydb user=postgres'
EOF

# 4. 初始化BDR集群
# 在数据库A上初始化
psql -h 数据库A_IP -U postgres -d mydb -c "CREATE EXTENSION bdr;"
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT bdr.create_node( node_name := 'node_a', local_dsn := 'host=数据库A_IP port=5432 dbname=mydb user=postgres' );"

# 在数据库B上加入集群
psql -h 数据库B_IP -U postgres -d mydb -c "CREATE EXTENSION bdr;"
psql -h 数据库B_IP -U postgres -d mydb -c "SELECT bdr.join_node( node_name := 'node_b', local_dsn := 'host=数据库B_IP port=5432 dbname=mydb user=postgres', join_using_dsn := 'host=数据库A_IP port=5432 dbname=mydb user=postgres' );"

# 5. 验证BDR复制
# 在数据库A上插入数据
psql -h 数据库A_IP -U postgres -d mydb -c "INSERT INTO test (name) VALUES ('test via bdr');"

# 在数据库B上验证数据是否同步
psql -h 数据库B_IP -U postgres -d mydb -c "SELECT * FROM test;"

多活灾备中的数据冲突处理

1. 冲突类型

  • 主键冲突:两个数据中心同时插入具有相同主键的数据
  • 唯一约束冲突:两个数据中心同时插入具有相同唯一约束的数据
  • 更新冲突:两个数据中心同时更新同一行数据
  • 删除冲突:一个数据中心删除数据,另一个数据中心更新同一数据

2. 冲突处理策略

2.1 基于时间戳的冲突解决

使用时间戳字段判断数据的新旧,保留最新的数据:

sql
-- 创建带时间戳的表
CREATE TABLE mytable (
    id serial primary key,
    data text,
    updated_at timestamp with time zone default now()
);

-- 使用触发器自动更新时间戳
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_timestamp_trigger
BEFORE UPDATE ON mytable
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();

2.2 基于来源优先级的冲突解决

为不同数据中心设置优先级,在冲突时保留高优先级数据中心的数据:

sql
-- 创建带来源标记的表
CREATE TABLE mytable (
    id serial primary key,
    data text,
    source_center varchar(10),
    updated_at timestamp with time zone default now()
);

-- 冲突解决规则:中心A优先级高于中心B

2.3 基于业务规则的冲突解决

根据具体业务逻辑设计冲突解决规则:

sql
-- 示例:电商订单冲突解决
-- 保留已支付的订单,删除未支付的重复订单
CREATE OR REPLACE FUNCTION resolve_order_conflict()
RETURNS trigger AS $$
BEGIN
    -- 检查是否已存在相同订单号的已支付订单
    IF EXISTS (SELECT 1 FROM orders WHERE order_no = NEW.order_no AND status = 'paid') THEN
        -- 存在已支付订单,忽略当前插入
        RETURN NULL;
    ELSE
        -- 不存在已支付订单,删除旧订单并插入新订单
        DELETE FROM orders WHERE order_no = NEW.order_no;
        RETURN NEW;
    END IF;
END;
$$ LANGUAGE plpgsql;

多活灾备监控与管理

1. 数据同步监控

bash
# 1. 监控逻辑复制状态
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM pg_stat_subscription;"
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM pg_stat_publication;"

# 2. 监控BDR复制状态
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM bdr.bdr_nodes;"
psql -h 数据库A_IP -U postgres -d mydb -c "SELECT * FROM bdr.bdr_connections;"

# 3. 监控数据一致性
# 使用checksum工具验证数据一致性
pg_checksums -c -D /var/lib/postgresql/15/main

2. 自动故障检测与切换

bash
# 1. 使用Patroni实现自动故障切换
# 配置Patroni监控多个数据中心的数据库
# 略

# 2. 使用pgpool-II实现自动故障检测
# pgpool-II配置文件中已包含健康检查配置
# 略

# 3. 自定义故障检测脚本
#!/bin/bash
# 检查数据库是否可用
DB_IP=$1

if psql -h $DB_IP -U postgres -c "SELECT 1;" > /dev/null 2>&1; then
    echo "$DB_IP is up"
    exit 0
else
    echo "$DB_IP is down"
    # 执行故障切换逻辑
    # 略
    exit 1
fi

3. 流量管理与路由

bash
# 1. 使用DNS实现流量路由
# 配置DNS将流量解析到多个数据中心
# 略

# 2. 使用负载均衡器实现流量分发
# 配置负载均衡器将流量分发到多个数据中心的pgpool-II节点
# 略

# 3. 使用应用层路由
# 在应用代码中实现基于地理位置或用户属性的路由逻辑
# 略

最佳实践

1. 架构设计最佳实践

  • 避免复杂的多活拓扑:从简单的双活架构开始,逐步扩展
  • 考虑数据中心间的网络延迟:根据网络延迟选择合适的数据同步方式
  • 设计合理的冲突解决策略:在架构设计阶段就考虑数据冲突问题
  • 实现分层的多活策略:不同业务模块可以采用不同的多活策略
  • 定期测试故障切换:至少每季度测试一次完整的故障切换流程

2. 配置最佳实践

bash
# 优化逻辑复制性能
cat >> /var/lib/postgresql/15/main/postgresql.conf << EOF
# 逻辑复制优化
wal_compression = on
max_wal_size = 4GB
checkpoint_timeout = 30min
# 并行复制设置
max_worker_processes = 16
max_parallel_workers_per_gather = 8
max_logical_replication_workers = 8
EOF

3. 管理最佳实践

  • 文档化多活架构和流程:详细记录多活架构、配置和故障切换流程
  • 建立完善的监控体系:监控数据同步状态、延迟、冲突等关键指标
  • 培训运维团队:确保运维团队熟悉多活架构和故障处理流程
  • 定期演练故障切换:验证故障切换流程的有效性
  • 持续优化多活策略:根据业务发展和技术进步持续优化多活架构

常见问题(FAQ)

Q1:如何选择合适的多活架构?

A1:选择多活架构时需要考虑以下因素:

  • 业务需求:是否需要跨地域的业务连续性
  • 数据一致性要求:强一致性还是最终一致性
  • 网络条件:数据中心间的网络延迟和带宽
  • 技术成熟度:团队对不同多活技术的掌握程度
  • 成本预算:不同多活架构的成本差异

Q2:如何处理多活架构中的数据冲突?

A2:

  • 预防为主:设计业务逻辑时避免并发写入同一数据
  • 使用唯一标识符:为数据添加全局唯一标识符,避免主键冲突
  • 实现冲突检测和解决机制
    sql
    -- 使用逻辑复制的冲突解决
    ALTER SUBSCRIPTION sub_a_to_b SET (conflict_resolution = 'last_update_wins');
  • 建立冲突处理流程:定期检查和处理未自动解决的冲突

Q3:如何确保多活架构的数据一致性?

A3:

  • 选择合适的数据同步技术:根据一致性要求选择同步复制或异步复制
  • 实现数据一致性验证机制:定期验证多个数据中心的数据一致性
  • 建立数据修复流程:发现数据不一致时能够快速修复
  • 使用事务机制:确保跨数据中心的操作原子性

Q4:如何进行多活架构的容量规划?

A4:

  • 考虑峰值负载:每个数据中心应能承载全部业务流量
  • 考虑数据增长:预留足够的存储空间和计算资源
  • 考虑网络带宽:确保数据中心间的网络带宽满足数据同步需求
  • 进行性能测试:模拟真实业务负载进行性能测试

Q5:如何实现多活架构的自动故障切换?

A5:

  • 使用成熟的自动化工具:如Patroni、pgpool-II等
  • 设计合理的故障检测机制:准确检测数据中心或数据库故障
  • 实现自动化的流量切换:在故障发生时自动切换业务流量
  • 建立回切机制:故障恢复后能够安全地切回流量

Q6:如何监控多活架构的运行状态?

A6:

  • 监控数据同步状态:监控复制延迟、同步错误等
  • 监控数据库性能:监控每个数据中心的数据库性能指标
  • 监控网络状态:监控数据中心间的网络连接和延迟
  • 监控业务指标:监控业务流量分布、响应时间等
  • 设置告警机制:对异常情况及时告警