两台便宜 VPS 拼出高可用网站:Keepalived + 浮动IP 实战,单机宕机不再慌

便宜 VPS 经常单机故障宕机?本文手把手教你用两台低价 VPS + Keepalived/VRRP + 浮动IP 拼出高可用架构,覆盖 MySQL 主从、Syncthing 文件同步与脑裂防护,附可直接复制的配置。

一、为什么便宜 VPS 更需要高可用

咱买便宜 VPS,图的就是一个性价比:年付十几二十刀,1 核 1G 跑个小博客、API、或者给朋友搭的网盘,香得很。但便宜是有代价的——便宜机器大概率是超售严重的母鸡上分出来的,单机故障率比大厂独服高得多。我见过最离谱的,一台 RackNerd 年付机一个月里重启了三次,每次都是母鸡维护。

问题来了:你的站点、接口、机器人,全压在这一台机器上。它一挂,全部访问 502,用户骂街,你也只能半夜爬起来重启。这其实就是"把鸡蛋放在一个篮子里"。高可用(High Availability,简称 HA)的核心思路就一句话:用冗余换可用性——多准备一台机器顶着,一台挂了另一台无缝接管,用户几乎无感知。

别被"高可用"三个字吓到,它不等于要上 Kubernetes、不等于要买企业级负载均衡。对绝大多数个人和小团队项目来说,两台便宜 VPS + 一个浮动 IP + Keepalived 就能把可用性从"看运气"拉到"月宕机几分钟以内"。下面咱们一步步来。

二、先搞懂:可用性到底怎么算

常听人说 99.9%、99.99% 这些数字,到底意味着啥?其实很简单,就是"一个月允许宕机多久":

  • 99.9%(三个九):一个月最多宕机约 43 分钟
  • 99.95%:约 22 分钟。
  • 99.99%(四个九):约 4.3 分钟

单台便宜 VPS 的实际可用性往往只有 99% 甚至更低(一年能宕好几天)。但如果你有两台独立机器(最好不在同一台母鸡、甚至不同机房),同时挂的概率会大幅降低——两台各自 99% 的话,同时挂只有 1%×1%=0.01%,可用性直接到 99.99%。这就是冗余的威力。

所以 HA 第一步不是上软件,是让你的两台机器尽量"独立":不同厂商、不同机房最好;实在不行同厂商也要选不同数据中心。别两台都买同一个 RackNerd 洛杉矶机房还放同一机位,那叫"假冗余"。

三、浮动 IP(Floating IP)是什么鬼

想象一下:你有两台机器 A 和 B,用户怎么访问?如果直接用 A 的 IP,A 挂了用户还是打 A 的 IP,没用。HA 的关键就是对外只暴露"一个" IP,这个 IP 能随时从 A 解绑、绑到 B 上。这个可漂移的公网 IP,就叫浮动 IP(Floating IP / Elastic IP / 高可用 IP)。

工作原理:A 正常时,浮动 IP 绑在 A 上,流量全进 A;A 挂了,系统把浮动 IP 重新绑到 B,流量改走 B。整个过程用户只认那个 IP,不用改 DNS、不用改客户端

哪些厂商支持浮动 IP

  • Vultr:原生支持 Floating IP,控制台一键买,后台用 API 或脚本解绑/绑定。
  • DigitalOcean:叫 Floating IP,同样支持 API 重绑定。
  • AWS / 腾讯云 / 阿里云:叫弹性公网 IP(EIP / Elastic IP),不能直接用 VRRP 漂移,但可以通过厂商 API + 脚本在故障时自动换绑。
  • Bandwagon(搬瓦工)、CloudConeRackNerd 等超低价商家:大多不支持浮动 IP,也没有跨实例私有网络。这种要做 HA 就得走别的路子(见下文"云上注意点")。

所以如果你铁了心要玩 Keepalived 真漂移,优先选 VultrDigitalOcean,这俩对浮动 IP 最友好。

四、Keepalived + VRRP:让 IP 自己漂起来

Keepalived 是 Linux 上最主流的 HA 软件,它实现了 VRRP(虚拟路由冗余协议)。简单说:两台机器组成一个 VRRP 组,一台是 MASTER(持有浮动 IP),一台是 BACKUP。MASTER 每秒发一次"心跳"(advertisement),BACKUP 盯着,一旦连着几秒没收到心跳,就判定 MASTER 挂了,自己上位接管浮动 IP。整个过程秒级完成,全自动

云上必看:multicast 与 unicast

VRRP 默认用组播(224.0.0.18)通信,但很多云厂商(尤其 AWS)禁组播,而且两台机器往往不在同一个二层网络。解决办法是用 unicast(单播)模式,直接写死对端 IP。下面配置我会给 unicast 版本,适用性更广。

前置:允许绑定不存在的 IP

Keepalived 要让机器绑定一个"暂时不属于自己"的浮动 IP,内核得放行,两台机器都执行:

# 允许进程绑定尚未在本机生效的 IP(VIP 漂移必备)
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/99-keepalived.conf
sudo sysctl --system

防火墙放行 VRRP

# VRRP 协议号是 112,放行它(用 UFW 的话)
sudo ufw allow from 10.0.0.0/24 to 224.0.0.18 proto vrrp
# 或者直接允许对端 IP
sudo ufw allow from 对端内网IP

主节点(MASTER)配置

假设你两台机器内网 IP 分别是 10.0.0.10(主)和 10.0.0.11(备),浮动 IP 用厂商给的那个公网浮动地址(这里先用占位 203.0.113.100 表示)。编辑 /etc/keepalived/keepalived.conf

global_defs {
    router_id NODE_A
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0            # 改成你实际网卡名,云上常是 ens3/eth0
    virtual_router_id 51      # 同一 HA 组必须一致, range 1-255
    priority 150              # 主节点优先级高
    advert_int 1              # 每秒发一次心跳

    # 云厂商禁组播,改用单播
    unicast_src_ip 10.0.0.10
    unicast_peer {
        10.0.0.11
    }

    authentication {
        auth_type PASS
        auth_pass 你自己设的密码
    }

    virtual_ipaddress {
        203.0.113.100/24 dev eth0
    }
}

备节点(BACKUP)配置

只有三处不同:router_idstatepriority,以及 unicast 的源/对端 IP 反过来:

global_defs {
    router_id NODE_B
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51      # 必须和主节点一致!
    priority 100              # 比主节点低
    advert_int 1

    unicast_src_ip 10.0.0.11
    unicast_peer {
        10.0.0.10
    }

    authentication {
        auth_type PASS
        auth_pass 你自己设的密码   # 必须和主节点一致!
    }

    virtual_ipaddress {
        203.0.113.100/24 dev eth0
    }
}

两台都装好并启动:

# 两台都装
sudo apt update && sudo apt install -y keepalived

# 两台都启动并开机自启
sudo systemctl enable --now keepalived

# 在主节点确认浮动 IP 已经挂上
ip addr show eth0 | grep 203.0.113.100

加健康检查:服务挂了也要漂移

光检测"机器死没死"不够——万一 Nginx 崩了但 OS 还活着,VIP 不会漂移,用户照样 502。用 vrrp_script 做应用级健康检查:

# 健康检查脚本 /usr/local/bin/check_nginx.sh
#!/bin/bash
if systemctl is-active --quiet nginx; then
    exit 0
else
    exit 1
fi
chmod +x /usr/local/bin/check_nginx.sh

然后在 vrrp_instance 里加上脚本关联(两台都加):

vrrp_script check_nginx {
    script "/usr/local/bin/check_nginx.sh"
    interval 2          # 每 2 秒检查一次
    weight -50          # 检查失败,优先级减 50
    fall 2              # 连续失败 2 次才算挂
    rise 2              # 连续成功 2 次才恢复
}

vrrp_instance VI_1 {
    # ... 前面照旧 ...
    track_script {
        check_nginx
    }
}

逻辑是这样:主节点优先级 150,Nginx 一挂,优先级变 100,低于备节点的 100?注意这里要留余量——把主节点 weight 减完必须明确低于备节点。比如主 150、备 100、weight -60,主挂了变 90 < 100,漂移触发。具体数值以你实际环境为准,自己算一下。

真刀真枪测一次

# 在主节点直接停掉 keepalived,看备节点是否接管
sudo systemctl stop keepalived
# 到备节点执行,应该能看到浮动 IP 已经漂过来
ip addr show eth0 | grep 203.0.113.100

能漂过去、再启回来能漂回来(默认开启抢占 preempt),说明成了。

五、数据库怎么同步:MySQL 主从复制

IP 漂了,但两台机器的数据得一致才有意义。数据库同步最经典就是 MySQL 主从(Master-Slave):主库负责写,binlog 记录所有改动,从库把 binlog "搬"过来在自己身上重放,数据就和主库一致了。主挂了,从库可以顶上(提升为新的主)。

主库配置(/etc/mysql/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf)

[mysqld]
server-id = 1                # 每台机器必须唯一
log-bin = mysql-bin          # 开启 binlog
binlog_format = ROW          # 行格式,最安全
gtid_mode = ON               # 强烈建议开 GTID
enforce_gtid_consistency = ON
binlog_expire_logs_seconds = 604800   # binlog 保留 7 天
sync_binlog = 1

从库配置

[mysqld]
server-id = 2                # 必须和主库不同
log-bin = mysql-bin
gtid_mode = ON
enforce_gtid_consistency = ON
read_only = ON               # 从库只读,防误写
super_read_only = ON
log_slave_updates = ON       # 方便以后从库再被提升

改完两边 systemctl restart mysql。然后主库建个专门的复制账号:

CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY '设个强密码';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
FLUSH PRIVILEGES;

从库指向主库(MySQL 8.0.23+ 用新语法):

CHANGE REPLICATION SOURCE TO
    SOURCE_HOST='10.0.0.10',
    SOURCE_PORT=3306,
    SOURCE_USER='repl',
    SOURCE_PASSWORD='设个强密码',
    SOURCE_AUTO_POSITION=1;   # GTID 自动定位位点,香
START REPLICA;

老版本 MySQL 用 CHANGE MASTER TO ... / START SLAVE;。检查同步状态:

SHOW REPLICA STATUS\G
# 重点看 Replica_IO_Running 和 Replica_SQL_Running 是否都是 Yes

提升从库:主库真挂了,在从库执行 STOP REPLICA; RESET REPLICA ALL;,然后让应用连从库(配合 Keepalived 漂移后的新主),它就成了新主。注意提升前确保从库已经重放完所有 relay log,别丢数据。

六、网站文件怎么同步:Syncthing 实时双向同步

数据库有主从,那网站代码、用户上传的图片/附件呢?两台机器得都有同一份文件。最简单稳妥的是 Syncthing——开源、端到端加密、多机目录实时双向同步,比 rsync 定时跑省心,比 NFS 不依赖单点。

两台机器都装(以 Docker 为例,数据目录映射出来):

docker run -d \
  --name syncthing \
  --restart=unless-stopped \
  -p 8384:8384 \
  -p 22000:22000/tcp \
  -p 22000:22000/udp \
  -v /opt/syncthing:/var/syncthing \
  louislam/syncthing:latest

浏览器开 http://机器IP:8384,每台机器都有个 Device ID。在 A 上"添加远程设备"粘贴 B 的 ID,B 上确认;然后建一个共享文件夹(比如 /var/syncthing/www),Folder Type 选 Send & Receive(双向同步),勾选对方设备。两边一确认,文件改动就实时互相同步了。

踩坑提醒:

  • 务必开文件版本管理(File Versioning):比如 Trash Can 模式,误删/被覆盖的旧文件会进版本目录,能救回。网站文件被同步"误删" propagation 是很疼的。
  • 网站根目录让 Nginx 直接读 /opt/syncthing/www 就行,两台配置一致。
  • 双向同步有个经典坑:两边同时改同一个文件会冲突,生成 .sync-conflict 文件。代码部署建议只在一台机器上 git pull,另一台自动同步过去,别两头都改。

七、脑裂(Split-Brain)与仲裁,这个坑必须说

HA 最怕的就是脑裂:两台机器因为网络抖动互相收不到心跳,都以为对方挂了,于是都把自己提升成 MASTER,同时持有浮动 IP、同时写数据库。结果就是数据冲突、用户请求被分流到两台不一致的机器上,惨烈。

怎么防?

  • 不要两台都在同一个网络孤岛:脑裂往往源于心跳链路和流量链路是同一根线。条件允许用心跳走内网、业务走公网浮动 IP。
  • 用 nopreempt(非抢占):备节点接管后,即使原主恢复也不自动抢回 MASTER,避免来回横跳。在 BACKUP 节点加 nopreempt
  • 加仲裁(quorum):引入第三台廉价机器或云上探测服务做"裁判",谁拿到仲裁谁才是真主。Keepalived 本身没有内置 quorum,可以用 notify_master 脚本里调用外部检查,或用 track_script 检测到"对端其实还活着"就拒绝接管。
  • fencing( fencing / 隔离):真发生脑裂时,强制把"失联但可能还活着"的那台关机或摘掉它的 IP,确保只有一个主。云上可以用厂商 API 强制停机器。
  • 数据库层用半同步复制 + GTID:主库提交事务前至少等一个从库 ACK(无损半同步 AFTER_SYNC),能大幅降低脑裂时丢数据的概率。

八、总结 / 避坑清单

用两台便宜 VPS 拼高可用,核心就这几块:

  • IP 漂移:Keepalived + VRRP,云上用 unicast,浮动 IP 优先选 Vultr / DigitalOcean
  • 数据一致:MySQL 主从(GTID + 半同步),写主读从,主挂从提升。
  • 文件一致:Syncthing 双向实时同步,开版本管理防误删。
  • 防脑裂:nopreempt + 独立心跳 + 仲裁 + fencing,数据库上半同步。

避坑清单(都是血泪):

  1. 两台机器尽量不同厂商/不同机房,别搞"假冗余"。
  2. 云上默认禁组播,一定用 unicast,不然心跳根本不通。
  3. 浮动 IP 漂移后,注意 ARP / 路由缓存,必要时在 notify_master 脚本里 arping 一下刷新网关 ARP。
  4. priorityweight 算好余量,减完必须明确低于对端,否则漂不动。
  5. 数据库提升从库前,先确认 relay log 重放干净,别丢数据。
  6. Syncthing 别两头同时改代码,部署走单点。
  7. 真 HA 别忘了备份:HA 防的是"机器挂",防不了"误删库",定期离线备份才是底线。

最后说句大实话:HA 不是万能药,它解决的是"单机宕机",解决不了"你代码有 bug"和"没备份"。但花两台便宜机器的钱,把可用性从看运气拉到四个九级别,这波投入绝对值。

延伸阅读

更多相关攻略推荐:【VPS 硬件选型指南 (CPU 篇) 04】AMD EPYC 还是【发行版选型 07】Arch Linux 深度科普:滚动发布、pac【VPS 进阶玩法精选 014】ARM 架构 VPS 值不值得上?A【对象存储 01】对象存储怎么选:Backblaze B2 vs C【TCP优化 01】美国 VPS 跑不快?多半是没开 BBR,一个命