技术方案 · 2026-09

两台服务器的高可用方案:应用双活,数据库主备自动切换

面向中小项目的务实方案。任意一台机器宕机,业务不中断、数据不丢失,同时避开「数据库双写」带来的冲突处理成本。

目标 GOAL

两台服务器、一个域名。我们不追求「两台数据库同时可写」的真双活,因为那需要专门团队处理写冲突。取而代之的是一条更稳的路线:应用层两台同时工作,数据库层同一时刻只有一台可写,另一台实时复制并随时准备接管。

可用性任一台宕机,服务继续
数据安全主库挂掉不丢已确认的写入
维护成本不处理写冲突,运维一人可管

整体架构 ARCHITECTURE

用户 入口:Cloudflare 负载均衡 健康检查两台机器,宕机的自动摘除 服务器 A 应用实例 连接代理 HAProxy 永远指向当前主库 PostgreSQL 主库(可写) Patroni 管理 服务器 B 应用实例 连接代理 HAProxy 永远指向当前主库 PostgreSQL 从库(只读) 流式复制,随时可提升为主 流式复制 见证节点 etcd 第三票,防止脑裂
实线为流量与复制方向,虚线为控制面通信。B 的连接代理跨机指向 A 的主库;主从切换后,两台代理自动改指新主。
当前主库,唯一可写 从库,只读并实时同步 见证节点,只投票不存业务数据

各层选型 COMPONENTS

选用组件作用备注
入口Cloudflare Load Balancer健康检查两台机器,宕机的自动摘除有免费额度。同机房也可用 Keepalived 漂移虚拟 IP
应用业务服务 × 2两台同时接流量前提是无状态化,见下文
会话 / 缓存RedisSession、分布式锁或改用 JWT,不存 Session
文件对象存储(R2 / S3 / MinIO)上传文件两台都能读到不能存本地磁盘
数据库PostgreSQL 主从流式复制从库实时同步,主挂了可接管 可写 只读
自动切换Patroni探测主挂 → 提升从库 → 旧主回来自动降为从备选 pg_auto_failover
连接路由HAProxy 或 PgBouncer应用只连本机 localhost:5000,不关心谁是主跟随 Patroni 的健康接口
见证etcd 第三节点提供第三票,防止脑裂必须 两票无法多数决
定时任务Redis 锁 / PG advisory lock保证两台只有一台在跑否则每个任务执行两次

四个关键决策 DECISIONS

1. 第三票必须有,否则不要开自动切换

两台机器之间网络一断,各自都会认为对方已死,各自都想当主。如果两台都接受写入,数据就分成两个版本,事后几乎无法合并。这是整个方案里最严重的故障模式,术语叫「脑裂」。

解决办法很便宜:租一台最低配 VPS 跑 etcd,或者用机房里的第三台旧机器。它只负责投票,不存任何业务数据。如果暂时没有第三票,就保留主从复制,但切换改为「告警 + 人工执行」。

2. 同步复制还是异步复制

异步(默认)写入只等主库落盘就返回。主库突然宕机,最近几十毫秒的写可能丢失。一般业务可接受。
同步写入要两台都落盘才返回,零丢失。缺点是从库挂了主库会等待。

财务、订单类数据建议用同步,并打开 Patroni 的 synchronous_mode: true。它的好处是从库挂了会自动退化为异步,不会把主库卡死。

3. 写操作永远只走一台

这正是「不用处理数据冲突」的原因:任一时刻只有一个主库接受写,从库只读。代价是切换期间有一段写不可用的窗口,通常 10 到 30 秒。应用层要做到写失败自动重试,用户看到的只是一次略慢的请求。

4. Windows Server 上的现实取舍

Patroni、etcd、HAProxy 在 Windows 上都不顺手。建议两台机器装 Docker 或 WSL2 来跑这一套,或者把数据库层放到 Linux 小机上。

如果必须纯 Windows:保留 PostgreSQL 主从复制,用 pg_ctl promote 人工提升,配合监控告警,做到「几分钟内人工恢复」。对中小项目这通常已经够用。

应用无状态化清单 PREREQUISITE

这一步不做,后面全部白搭。「无状态」意思是任何一次请求落到 A 或 B 都得到相同结果,机器本地不保留用户相关的东西。

本地状态问题改法
内存 Session用户在 A 登录,下一次请求落到 B 就掉线Session 进 Redis,或改 JWT
本地上传目录A 上传的文件 B 读不到统一放对象存储
进程内定时任务两台各跑一遍,重复推送、重复扣款执行前先抢分布式锁
本地 SQLite根本无法两机共享迁移到 PostgreSQL
本地日志排查问题要翻两台各写各的即可,建议汇总到一处

落地顺序 ROLLOUT

按顺序做,每一步都能独立验证,任何一步停下来都比现状更好。

  1. 应用无状态化Session、文件、定时任务锁。做完后单机也能随时重启,本身就是收益。
  2. SQLite 迁移到 PostgreSQL如果现有项目用 SQLite。先在单机上跑稳一周。
  3. 搭建主从复制,先手动切换跑通验证从库数据完整,用 pg_ctl promote 手动切一次,记录耗时。
  4. 接入 Cloudflare 负载均衡两台应用同时接流量,配好健康检查。此时应用层已双活。
  5. 加 etcd 见证 + Patroni + HAProxy开启自动切换。应用改为连本机代理端口。
  6. 故障演练直接断掉主库那台机器的网络,观察恢复时间与数据是否丢失。不演练等于没做。

演练时的预期指标 EXPECTATION

主库故障探测≈10sPatroni 默认 TTL
从库提升为主10~30s写不可用窗口
应用宕机切流<60sCloudflare 健康检查间隔
数据丢失0同步复制模式下
验收标准:拔掉任一台机器的网线,60 秒内业务恢复正常,切换前已确认成功的写入全部存在于新主库。达不到就回到落地顺序对应步骤排查。

常见疑问 FAQ

为什么不做数据库双主?

双主要处理同一行被两边同时改写的冲突,方案(BDR、Galera 等)都有各自的限制和坑,需要有人长期盯。对中小项目,主备切换带来的十几秒写窗口远比冲突处理便宜。

读请求能不能分到从库?

可以,报表、列表这类只读查询可以走从库,减轻主库压力。但要接受几毫秒到几百毫秒的复制延迟,刚写完马上读的场景仍然走主库。

两台机器要不要同配置?

要。切换后任意一台都可能成为主库,配置低的那台会成为瓶颈。

Redis 挂了怎么办?

Redis 本身也是单点。用户量不大时接受「Redis 挂了所有人重新登录」;需要更稳则用 Redis Sentinel 三节点,见证机可以顺便跑一个 Sentinel。