一、为什么我们需要给数据库找个“备胎”?

想象一下,你负责一个重要的在线商城系统。数据库就像这个商城的心脏,存储着所有的商品信息、用户数据和订单记录。如果这颗心脏突然停止跳动(也就是我们常说的“单点故障”),会发生什么?用户无法下单,商家无法管理商品,整个业务瞬间瘫痪,损失难以估量。

这就是高可用架构要解决的问题:为数据库准备一个或多个随时可以顶上的“备胎”(备用节点),确保在主数据库出问题时,服务能快速、自动地切换,实现业务不中断或尽可能短的中断。对于PostgreSQL来说,社区和生态提供了多种成熟的“备胎”方案,我们今天就来深入聊聊其中几种主流的设计。

二、经典搭档:流复制与故障转移

这是PostgreSQL高可用最基础、最核心的机制,可以理解为“主从实时同步”。主库(Primary)像领导一样处理所有写操作,而从库(Standby)则像秘书,实时复制领导的所有操作记录。

技术栈:PostgreSQL 原生流复制 + pg_rewind

示例:配置一个基础的流复制从库

假设我们有两台服务器:pg-primary(192.168.1.10)pg-standby(192.168.1.11)

首先,在主库上进行配置:

# 在主库上编辑 postgresql.conf
# 设置监听地址和 wal 日志级别
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10    # 允许最多10个从库连接来获取WAL日志
wal_keep_size = 1GB     # 保留至少1GB的WAL日志,防止从库落后太多时丢失数据

# 在主库上编辑 pg_hba.conf, 允许从库连接来复制数据
# 添加一行
host    replication     all             192.168.1.11/32         md5

然后,在从库服务器上,使用 pg_basebackup 工具从主库拉取一个完整的基础备份:

# 在从库服务器上执行
pg_basebackup -h 192.168.1.10 -U postgres -D /var/lib/postgresql/data -P -R

# 参数解释:
# -h: 指定主库地址
# -U: 连接使用的用户名(需有replication权限)
# -D: 从库数据目录,这里会下载主库的完整数据
# -P: 显示进度
# -R: 自动创建 standby.signal 文件和 primary_conninfo 配置,这是从库身份的标识

完成备份后,启动从库,它就会自动连接到主库,并开始持续接收和应用WAL日志,保持数据同步。但这时候,如果主库宕机,我们需要手动将从库“提升”为主库。这个过程虽然可靠,但恢复时间较长,且依赖人工操作。

三、自动化升级:引入故障转移管理器

手动切换在半夜出问题时简直是运维的噩梦。因此,我们需要一个“哨兵”来自动监控和决策。这就是故障转移管理器,比如 Patroni。Patroni 是一个流行的开源工具,它使用分布式配置存储(如 etcd、ZooKeeper)来管理整个 PostgreSQL 集群的状态,实现自动故障切换。

技术栈:PostgreSQL + Patroni + etcd

示例:Patroni 配置核心片段

Patroni 的配置文件(如 patroni.yml)定义了集群的行为:

# patroni.yml 示例片段
scope: my_pg_cluster          # 集群名称,同一个集群内的节点应相同
name: pg-node1                # 当前节点的唯一名称

restapi:                      # Patroni 的监控和管理API
  listen: 0.0.0.0:8008
  connect_address: 192.168.1.10:8008

etcd3:                        # 使用 etcd 作为分布式配置存储
  hosts: 192.168.1.100:2379,192.168.1.101:2379,192.168.1.102:2379

bootstrap:                    # 集群初始化配置
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576 # 允许从库落后的最大字节数
    postgresql:
      use_pg_rewind: true            # 启用 pg_rewind,允许旧主库恢复后重新加入集群
      parameters:
        wal_level: logical
        hot_standby: “on”
        max_connections: 100

postgresql:                   # 本机 PostgreSQL 实例配置
  listen: 0.0.0.0:5432
  connect_address: 192.168.1.10:5432
  data_dir: /var/lib/postgresql/data/pgdata
  pg_rewind:                  # pg_rewind 配置,用于角色切换后数据同步
    username: postgres
    password: securepassword
  authentication:
    replication:
      username: replicator
      password: repassword
    superuser:
      username: postgres
      password: securepassword

tags:
  nofailover: false           # 此节点是否允许故障转移
  noloadbalance: false
  clonefrom: false

在这个架构下,Patroni 会持续监控每个节点的健康状态。当主库节点故障时,etcd 中的“锁”会被释放,Patroni 会在剩余的从库中,根据优先级、数据滞后情况等条件,自动选举出一个新的主库,并完成提升和流量切换。原先的应用只需要连接到一个统一的入口(通常由负载均衡器或DNS提供),由 Patroni 集群来保证这个入口背后始终是一个可写的主库。

四、读写分离与负载均衡

高可用不仅意味着“不停机”,还意味着“高性能”。在大多数业务中,读请求远多于写请求。我们可以利用从库来分担主库的读压力。这需要结合连接池工具,如 PgBouncerHAProxy

技术栈:PostgreSQL + HAProxy

示例:HAProxy 配置实现读写分离

HAProxy 是一个高性能的负载均衡器。我们可以配置它,将写请求(识别为“非只读事务”)定向到主库,将读请求分发到多个从库。

# haproxy.cfg 相关配置片段
global
    maxconn 100

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

# 定义一个用于健康检查的PostgreSQL后端
backend pg_health_check
    mode tcp
    option pgsql-check user postgres
    server postgres_check 127.0.0.1:5432 check

# 写服务监听端口(指向主库)
listen postgres_write
    bind *:5433                     # 应用通过5433端口连接进行写操作
    mode tcp
    balance leastconn
    option pgsql-check user postgres
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg-primary 192.168.1.10:5432 check port 5432
    # 注意:这里通常只配置一个主库地址,故障转移后IP会变,需结合Patroni API动态更新或使用虚拟IP

# 读服务监听端口(指向所有从库)
listen postgres_read
    bind *:5434                     # 应用通过5434端口连接进行读操作
    mode tcp
    balance roundrobin              # 使用轮询算法在从库间分配读请求
    option pgsql-check user postgres
    default-server inter 3s fall 3 rise 2
    server pg-standby1 192.168.1.11:5432 check port 5432 maxconn 100
    server pg-standby2 192.168.1.12:5432 check port 5432 maxconn 100
    # 可以添加更多从库

应用层代码需要根据操作类型,选择连接写端口(5433)或读端口(5434)。这样,读请求就被均匀地分摊到了各个从库上,极大地提升了系统的整体吞吐量。

五、方案对比与应用场景

没有一种架构是万能的,选择取决于你的具体需求。

  1. 基础流复制(手动切换)

    • 优点: 简单,内置于PostgreSQL,无需第三方组件,学习和维护成本低。
    • 缺点: 故障切换需要手动干预,恢复时间长,无法保证高可用性。
    • 适用场景: 对可用性要求不高的测试环境、小型内部应用,或作为更复杂架构的数据同步基础。
  2. Patroni + 流复制 + 分布式存储(自动故障转移)

    • 优点: 实现了真正的自动故障转移(通常可在30秒内完成),具备集群状态管理,支持旧主库自动重新加入。
    • 缺点: 架构复杂,引入了etcd/ZooKeeper等外部依赖,运维复杂度增加。
    • 适用场景: 对可用性要求高的核心生产系统(如金融、电商交易),能够接受一定的架构复杂性以换取自动化。
  3. 结合读写分离负载均衡

    • 优点: 在保证高可用的同时,显著提升了系统的读性能和处理并发的能力。
    • 缺点: 架构最为复杂,需要应用层配合(区分读写连接),可能存在“读延迟”(从库数据比主库稍旧)。
    • 适用场景: 读多写少且对读性能有高要求的互联网应用(如内容网站、社交平台、报表系统)。

六、实施中的关键注意事项

在搭建高可用架构时,务必留心以下陷阱:

  • 数据一致性是底线: 任何高可用方案都不能以牺牲数据一致性为代价。确保你的故障转移机制(如Patroni的maximum_lag_on_failover)能防止数据丢失过多的从库被提升为主库。
  • 网络分区(脑裂)问题: 当集群节点间网络出现问题时,可能导致出现两个“主库”。使用可靠的分布式共识系统(如etcd的Raft协议)是避免脑裂的关键。
  • 监控与告警: 必须对数据库集群的每个组件(PostgreSQL实例、Patroni、etcd、HAProxy)进行全方位监控。不仅要监控是否“活着”,还要监控复制延迟、连接数、负载等关键指标。
  • 定期演练: 定期模拟主库故障,进行故障转移演练。这能检验你的自动化流程是否真的有效,并让团队熟悉应急流程。
  • 应用连接处理: 故障转移期间,应用原有的数据库连接会中断。应用必须具备重连机制。使用连接池并配置合理的超时和重试参数至关重要。

七、总结

设计PostgreSQL高可用架构,是一个从“有备胎”到“自动换胎”,再到“让备胎也能拉货”的演进过程。核心在于理解业务对**RTO(恢复时间目标)RPO(数据恢复点目标)**的真实要求。

对于绝大多数严肃的生产环境,采用 Patroni 等工具实现自动故障转移的流复制集群,已成为事实上的标准选择。它很好地平衡了可靠性、自动化和复杂度。在此基础上,如果读负载成为瓶颈,再引入PgBouncer或HAProxy来实现读写分离。

记住,高可用不是一劳永逸的产品,而是一个持续运维的过程。扎实的基础配置、清晰的架构设计、完善的监控和定期的演练,共同构成了数据库系统坚如磐石的可靠性保障。