多台服务器集群搭建方案

结论: 多台服务器搭建集群的核心不是把机器连起来,而是先做无状态化改造——把 Session、上传文件、定时任务、本地缓存从单机搬到共享层,再用 Keepalived + VIP 提供入口高可用、Nginx/Haproxy 提供流量分发。跳过无状态化直接堆机器,必然出现"用户登录状态丢失""定时任务重复执行"这类集群典型故障。

单机撑不住了,第一反应往往是"再买几台做集群"。但集群不是简单地把应用复制 N 份:当同一个用户的多次请求可能落到不同机器时,所有依赖"本机"的假设都会崩塌。 如果登录态存在本机内存、上传的图片存在本机 /upload、日志写在本地文件里,那么加机器不但不能提升容量,还会制造出随机发生的诡异 Bug。

本方案以 2026 年主流技术栈为基准(Ubuntu 24.04 LTS、Nginx 1.26、Keepalived 2.3、Kubernetes 1.32),给出一套从单机演进到多机集群的完整路径,包含集群类型选型、前置改造清单、可复制的配置文件,以及 K8s 与裸机集群的取舍依据。

第一步:先确定你要搭的是哪一种集群

"集群"是个被滥用的词,不同目标对应的架构完全不同。 选错类型会导致方案过度复杂或者根本解决不了问题。下表是四类集群的核心对照。

四类集群对照表

集群类型要解决的核心问题典型架构关键技术组件适用规模
负载均衡集群 LB单机并发不够、并发横向扩展多台同构应用节点 + 流量分发层Nginx、HAProxy、LVS、云 SLB2 ~ 数百台,最常用
高可用集群 HA单点故障导致业务中断主备/双活 + VIP + 数据同步Keepalived、Pacemaker、MHA、Patroni2 节点起步,强调剔除单点
高性能计算集群 HPC单任务算力不够,需并行拆分管理节点 + 计算节点 + 高速网络Slurm、MPI、RDMA/InfiniBand科学计算、渲染农场、AI 训练
分布式存储集群单机磁盘容量/吞吐不够、需冗余多副本 + 一致性协议Ceph、MinIO、GlusterFS、NFS 高可用数据量 TB 级以上

还有两个经常混进来的概念需要澄清:

  • 数据库集群本质上是 HA 集群叠加数据复制,主库写、从库读,靠 binlog/WAL 做复制。它不能像无状态应用那样随意横向扩容写能力,写压力到顶只能靠分库分表。
  • Kubernetes 集群是把上面四种能力统一抽象后的"集群操作系统",它会帮你调度容器、重建故障实例、挂载存储。代价是引入了相当大的学习与运维成本。

绝大多数中小企业网站的真实需求是第一类(负载均衡)+ 第二类(高可用)的组合,这也是本方案后续展开的重点。

第二步:上集群前必须完成的无状态化改造

无状态化(Stateless)是指任意一台节点宕机,都不会导致已经在线的用户数据丢失或服务行为改变。 这是能否横向扩容的分水岭,也是集群改造里工作量最大、最不能偷懒的部分。

需要逐项处理的状态清单:

状态类型单机时代的做法集群化方案改造优先级
用户会话 Session存本机文件或进程内存Redis 集中存储,或改用 JWT 无状态令牌最高
上传文件/图片存本机 /var/www/uploads对象存储(OSS/COS/S3)或 NFS/CephFS最高
本地缓存Guava Cache / 进程内缓存Redis + 本地二级缓存 + 失效广播高
定时任务crontab 每台都跑分布式锁 + 单一执行节点,或独立调度器(XXL-Job)高
锁与幂等synchronized、本地 LockRedis 分布式锁(Redisson)、数据库乐观锁中
日志写本地文件统一采集到 ELK / Loki,或落到标准输出由采集器收走中
配置文件每台各自改配置中心(Nacos/Apollo)或 Ansible 统一下发中

会话方案怎么选

Session 集中化有三种主流做法,取舍如下:

  • Redis 集中存储(推荐):应用接入 Spring Session / session.save_handler=redis,改造成本最低,Session 丢失即重新登录。要注意 Redis 本身也要做高可用(Sentinel 或 Cluster),否则等于把单点从应用搬到了 Redis。
  • JWT 无状态令牌:服务端完全不存状态,JWT 自带签名与过期时间。缺点是令牌一旦签发无法即时失效,需要用短有效期 + 黑名单刷新机制弥补。适合前后端分离、移动端 API。
  • 会话粘滞(Sticky Session):负载均衡层按 Cookie/IP 把同一用户固定到某台机器。它是最省事的方案,但不是真正的无状态化——节点宕机时该批用户仍会掉线,扩容时也会导致流量倾斜,只建议作为过渡手段。

Nginx 的会话粘滞配置示例(作为过渡):

nginx
upstream backend_cluster {
    # 基于 cookie 的会话粘滞
    sticky cookie srv_id expires=1h domain=.example.com path=/;

    server 10.0.0.11:8080 weight=1 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8080 weight=1 max_fails=3 fail_timeout=10s;
    keepalive 64;
}
注:sticky 指令是 Nginx 的商业订阅版(NGINX Plus)功能。开源版可选方案是按 $cookie_jsessionid 或 $remote_addr 做 hash 一致性分发:hash $cookie_jsessionid consistent;。

共享存储方案取舍

方案优点缺点适用
NFS(云 NAS)最简单,POSIX 兼容,价格低单点风险,性能受限于单连接吞吐中小规模共享文件、WP 等传统应用
对象存储 OSS/COS无限容量,天然多副本,CDN 友好不是文件系统,需改 SDK 或用挂载工具图片、视频、下载包,新项目首选
CephFS / GlusterFS真正的分布式文件系统,可横向扩展运维复杂,需要 3 节点起步大规模、有专职存储运维团队
每台独立磁盘 + 同步无网络依赖,性能最好数据一致性难保证,不推荐仅适合只读静态资源

第三步:先把网络与时钟这两块地基打牢

集群里没有"差不多就行"这件事:节点间网络必须低延迟且互通,时钟必须同步到毫秒级。 这两样出问题,症状往往表现为"偶发的认证失败""数据莫名其妙不一致",极难定位。

网络侧的硬性要求:

  • 内网互通:集群节点务必放在同一 VPC/同一内网网段,走内网 IP 通信。跨公网组集群等于把内部延迟从 0.2ms 拉到 20ms+,还要额外处理加密与安全。
  • MTU 一致:节点、交换机、隧道(VXLAN/GRE)的 MTU 必须统一。Overlay 网络通常要把 MTU 降到 1450(标准 1500 减去 50 字节隧道头),不一致会导致大包被丢弃,表现为"小请求正常、上传大文件卡住"。
  • 安全组/iptables:放行 VRRP 协议(协议号 112,不是 TCP,也不是 UDP 端口),否则 Keepalived 主备之间收不到对方心跳,会双双抢成主节点。
  • 端口收敛:健康检查、metrics、日志采集都要走内网,不要暴露到公网。

时钟同步用 chrony(RHEL 9 / Ubuntu 24.04 默认):

bash
# 安装与启动
sudo apt install -y chrony
sudo systemctl enable --now chronyd

# 查看同步状态
chronyc sources -v
chronyc tracking
text
# /etc/chrony/chrony.conf 推荐配置
pool ntp.aliyun.com iburst
pool time.cloudflare.com iburst

# 内网场景:主节点对外同步,其余节点同步主节点
# server 10.0.0.11 iburst

driftfile /var/lib/chrony/chrony.drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

# 允许内网其他节点同步本机的网段
allow 10.0.0.0/24

时钟偏差的可容忍范围:Kerberos 要求 5 分钟内,分布式数据库(如 TiDB、CockroachDB)要求通常不超过 500ms,HTTPS 证书校验偏差超过 5 分钟会直接失败。 一个稳妥的运维标准是:所有节点与标准时间的偏差控制在 100ms 以内,并在 Zabbix/Prometheus 里对 chronyd 的 offset 做告警。

第四步:Keepalived + VIP 实现入口高可用

Keepalived 用 VRRP 协议在多台服务器之间管理一个虚拟 IP(VIP),主节点持有 VIP,主节点故障后备节点在 1~3 秒内接管。 它不分发流量,只做"IP 这一层的漂移",是最轻量的高可用方案。

假设有两台 Nginx 节点:10.0.0.11(主)和 10.0.0.12(备),VIP 为 10.0.0.100。

bash
# 两台都要安装
sudo apt install -y keepalived

# 允许绑定非本机 IP(VIP 必需)
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee -a /etc/sysctl.d/99-cluster.conf
sudo sysctl --system

主节点配置(/etc/keepalived/keepalived.conf):

text
! Configuration File for keepalived - MASTER node

global_defs {
    router_id nginx_lb_01          # 集群内唯一
    script_user root
    enable_script_security
}

# 健康检查脚本:Nginx 挂了就降低本节点优先级触发漂移
vrrp_script chk_nginx {
    script "/usr/bin/pgrep nginx"
    interval 2
    timeout 2
    weight -30
    fall 2
    rise 1
}

vrrp_instance VI_1 {
    state MASTER                   # 备机写 BACKUP
    interface eth0                 # 改成实际网卡名
    virtual_router_id 51           # 同一集群必须一致,0-255
    priority 150                   # 备机写 100,必须低于主机
    advert_int 1                   # VRRP 通告间隔,单位秒

    authentication {
        auth_type PASS
        auth_pass Cluster@2026     # 8 位以内,主备必须一致
    }

    unicast_src_ip 10.0.0.11       # 云服务器不支持组播时必须配单播
    unicast_peer {
        10.0.0.12
    }

    virtual_ipaddress {
        10.0.0.100/24 dev eth0 label eth0:0
    }

    track_script {
        chk_nginx
    }

    notify_master "/etc/keepalived/scripts/notify.sh master"
    notify_backup "/etc/keepalived/scripts/notify.sh backup"
    notify_fault  "/etc/keepalived/scripts/notify.sh fault"
}

备节点与主节点的差异只有三处:state BACKUP、priority 100、unicast_src_ip 10.0.0.12 / unicast_peer 指向 10.0.0.11。virtual_router_id 和 auth_pass 必须完全一致。

通知脚本用于漂移时告警与主备角色联动:

bash
#!/usr/bin/env bash
# /etc/keepalived/scripts/notify.sh
LOG=/var/log/keepalived-notify.log
case "$1" in
    master)
        echo "$(date '+%F %T') [$(hostname)] 切换为 MASTER,VIP 已接管" >> "$LOG"
        systemctl reload nginx
        curl -s -X POST "https://oapi.example.com/hook" \
             -H 'Content-Type: application/json' \
             -d "{\"msg\":\"[$(hostname)] Keepalived VIP 漂移:本机已变为 MASTER\"}"
        ;;
    backup|fault)
        echo "$(date '+%F %T') [$(hostname)] 切换为 $1" >> "$LOG"
        ;;
esac
bash
sudo chmod +x /etc/keepalived/scripts/notify.sh
sudo systemctl enable --now keepalived

# 验证:应该看到 VIP 绑定在 eth0 上
ip addr show eth0 | grep -E 'inet |10.0.0.100'

# 抓 VRRP 报文确认心跳是否正常(云上抓不到说明安全组没放行协议 112)
sudo tcpdump -i eth0 -nn proto 112

第五步:Nginx upstream 实现流量分发与故障剔除

Nginx 的 upstream 模块把多台后端节点封装成一个逻辑服务,通过 max_fails 与 fail_timeout 自动摘除故障节点。 这是最轻量、也最常用的负载均衡集群形态。

nginx
upstream app_backend {
    least_conn;                     # 最少连接调度,长连接场景优于轮询

    server 10.0.0.11:8080 weight=2 max_fails=3 fail_timeout=10s slow_start=30s;
    server 10.0.0.12:8080 weight=2 max_fails=3 fail_timeout=10s slow_start=30s;
    server 10.0.0.13:8080 backup;   # 备用节点,主节点全挂才启用

    keepalive 128;                  # 到后端的空闲连接池
    keepalive_requests 1000;
    keepalive_timeout 60s;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";        # keepalive 必需
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 3s;
        proxy_send_timeout    10s;
        proxy_read_timeout    30s;

        # 后端返回 5xx 或超时,自动重试下一个节点
        proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 5s;
    }

    # 对外健康检查端点,供云 SLB 或监控系统调用
    location /healthz {
        access_log off;
        return 200 "ok\n";
    }
}

调度策略的选择建议:短请求、节点性能接近用轮询(默认);长连接、请求耗时差异大用 least_conn;要会话粘滞用 hash $cookie_xxx consistent(一致性哈希,节点增减时影响最小,避免全量重分布)。

第六步:K8s 还是裸机集群?划清适用边界

判断标准很简单:如果你的服务数量超过 10 个、需要频繁扩缩容与灰度发布、有专职或半专职运维,上 Kubernetes;如果只有 3~5 个稳定服务、追求简单可控,Keepalived + Nginx 的裸机集群更划算。

维度裸机集群(Keepalived + Nginx + systemd)Kubernetes 集群
学习成本低,会 Linux 命令即可高,需掌握 Pod/Service/Ingress/PV 等抽象
部署速度分钟级上线首次搭建 1~3 天
故障自愈需自己做脚本与监控联动原生支持,容器挂了自动重建与重新调度
弹性伸缩需手动加机器改配置HPA 可按 CPU/QPS 自动扩缩
资源利用率低,每台需留冗余高,混部提升密度
稳定期间故障排查直连机器看日志,直观需要 kubectl、多一层网络抽象
最低节点数2 台即可生产建议 3 master + 3 worker 起步
适用团队规模1~3 人运维有平台团队的中大型团队

一个务实的演进路径是:先用裸机集群跑起来,把无状态化改造做扎实(这一步无论是否上 K8s 都必须做),等服务数量与发布频率增长到裸机模式扛不住时再迁移到 K8s。 反过来先上 K8s 却带着有状态设计,只会把问题藏得更深。

第七步:脑裂是怎么产生的,怎么防

脑裂(Split-Brain)是指集群节点间心跳中断后,两个或多个节点都认为自己是主节点并同时对外服务,导致数据互相破坏。 它是所有 HA 集群最危险的状态,也是必须用工程手段主动防御的。

典型触发场景:主备之间的交换机抖动、VRRP 报文被安全组拦截、主节点 CPU 打满导致心跳发不出去。

防御三板斧:

  1. 冗余心跳链路:至少两条独立的通信路径,例如业务网 + 独立的直连线/串口心跳线。云上可以再加一路走公网的 ping 检测。
  2. 仲裁机制(Quorum):奇数节点投票,超过半数才能成为主。3 节点集群容忍 1 个失败,5 节点容忍 2 个,偶数节点不要做 HA。
  3. fencing/STONITH:确认对端失联后,先想办法把对方"打死"(断电、关闭服务进程)再接管资源。数据库集群尤其关键。

Keepalived 侧的实用补充手段,是利用 nopreempt 与第三方仲裁脚本防止抖动导致的反复漂移:

text
vrrp_instance VI_1 {
    state BACKUP           # 双 BACKUP + nopreempt 是防抖动的最佳实践
    nopreempt              # 主节点恢复后不自动抢占,避免频繁切换
    preempt_delay 300      # 若必须抢占,延迟 300 秒再执行

    # 双活/多活时给每个实例不同的 virtual_router_id
    # 也可配合 allow_duplicate 做双主(不推荐用于有状态服务)
}
bash
# 用 ping 网关作为第三方仲裁:网关都 ping 不通说明是自己断网,主动降权退出主角色
cat > /etc/keepalived/scripts/chk_gateway.sh <<'EOF'
#!/usr/bin/env bash
GATEWAY=$(ip route | awk '/default/ {print $3; exit}')
ping -c 2 -W 2 "$GATEWAY" >/dev/null 2>&1
exit $?
EOF
chmod +x /etc/keepalived/scripts/chk_gateway.sh

把它作为第二个 vrrp_script 加入 track_script,网络孤岛时本节点会自动降权,把服务交给网络正常的节点。