结论: 多台服务器搭建集群的核心不是把机器连起来,而是先做无状态化改造——把 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、云 SLB | 2 ~ 数百台,最常用 |
| 高可用集群 HA | 单点故障导致业务中断 | 主备/双活 + VIP + 数据同步 | Keepalived、Pacemaker、MHA、Patroni | 2 节点起步,强调剔除单点 |
| 高性能计算集群 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、本地 Lock | Redis 分布式锁(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 的会话粘滞配置示例(作为过渡):
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 默认):
# 安装与启动
sudo apt install -y chrony
sudo systemctl enable --now chronyd
# 查看同步状态
chronyc sources -v
chronyc tracking# /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。
# 两台都要安装
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):
! 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 必须完全一致。
通知脚本用于漂移时告警与主备角色联动:
#!/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"
;;
esacsudo 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 自动摘除故障节点。 这是最轻量、也最常用的负载均衡集群形态。
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 打满导致心跳发不出去。
防御三板斧:
- 冗余心跳链路:至少两条独立的通信路径,例如业务网 + 独立的直连线/串口心跳线。云上可以再加一路走公网的 ping 检测。
- 仲裁机制(Quorum):奇数节点投票,超过半数才能成为主。3 节点集群容忍 1 个失败,5 节点容忍 2 个,偶数节点不要做 HA。
- fencing/STONITH:确认对端失联后,先想办法把对方"打死"(断电、关闭服务进程)再接管资源。数据库集群尤其关键。
Keepalived 侧的实用补充手段,是利用 nopreempt 与第三方仲裁脚本防止抖动导致的反复漂移:
vrrp_instance VI_1 {
state BACKUP # 双 BACKUP + nopreempt 是防抖动的最佳实践
nopreempt # 主节点恢复后不自动抢占,避免频繁切换
preempt_delay 300 # 若必须抢占,延迟 300 秒再执行
# 双活/多活时给每个实例不同的 virtual_router_id
# 也可配合 allow_duplicate 做双主(不推荐用于有状态服务)
}# 用 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,网络孤岛时本节点会自动降权,把服务交给网络正常的节点。
企业QQ咨询




