集群部署解决了什么,又带来了什么
集群是解决单点故障和容量瓶颈的标准手段,但它不是免费的——会话共享、分布式锁、定时任务重复执行、日志排查,这些都是从单机走向集群后必须补上的课。
一、什么时候该上集群
单机部署有个绕不开的问题:它是一台机器。
服务器宕机、进程崩溃、内存溢出、CPU 打满——任何一个环节出问题,整个系统就直接不可用。业务量小的时候这可以接受,一旦到了不能停的程度,集群就是必选项。
集群的定义很简单:多台服务器同时运行同一个项目,对外提供服务。
但它解决的问题不止一个。
二、集群解决的四个问题
1. 高可用(最重要)
单机故障 = 全站不可用。集群里一台挂掉,其他机器继续干活,故障对用户不可见。
这是集群最核心的价值。注意它和其他三个有本质区别:吞吐量可以靠升级硬件解决,但单点故障只能靠冗余解决。
2. 提升并发能力
单台机器的 CPU、内存、连接数都有上限。Nginx 把请求分发到多个实例,多台机器共同处理流量,系统吞吐量随实例数增加。
3. 不停机发布
集群可以一台一台地更新:停其中一台更新代码,其余机器继续对外服务;更新完再换下一台。
这就是滚动发布(rolling update)。用户全程无感知,不需要挑凌晨低峰期停机维护。
配合灰度发布还能更稳:先更新一台,观察监控指标,没问题再推全量。
4. 容灾与故障隔离
某台服务器硬件故障、网络异常、磁盘损坏,其他节点正常工作。这和第 1 点的区别在于:高可用强调的是"服务不中断",容灾强调的是"故障范围可控"。
三、代价:集群带来的四类新问题
集群不是免费的。它把"单机问题"换成了"分布式问题",而分布式问题的排查难度往往更高。
1. 会话共享
单机时 HttpSession 存在本机内存,集群后用户第一次请求打到 A 机、第二次打到 B 机,B 机没有这个 session,用户就被登出了。
解决方案:
| 方案 | 做法 | 评价 |
|---|---|---|
| Session 复制 | 各实例互相同步 session | ❌ 实例多了广播风暴,不推荐 |
| Session 粘滞 | Nginx ip_hash,同一 IP 固定打同一台 |
⚠️ 简单但有副作用:某台挂了会话就丢了,负载也不均 |
| 集中存储 | Session 存 Redis,所有实例共享 | ✅ 主流方案 |
| 无状态 token | 用 JWT,服务端不存状态 | ✅ 微服务下的常见选择 |
集中存储是主流。代价是每个请求都要查一次 Redis——这也是为什么后来很多人转向 JWT:用"服务端不存状态"换掉这次查询(但如前所述,JWT 有自己的代价)。
2. 单机锁失效
synchronized、ReentrantLock 只在单个 JVM 内有效。集群后:
实例 A 的线程 1 拿到 JVM 内的锁
实例 B 的线程 1 同时拿到它自己 JVM 内的锁
→ 两把不同的锁,临界区被同时进入
必须换成分布式锁(Redis / ZooKeeper / etcd)。
这是集群化最容易埋雷的地方——代码一行没改,单机测试完全正常,上线集群后突然出现并发 bug。
3. 定时任务重复执行
@Scheduled 在每台机器上都会执行。一个"每天凌晨清理过期订单"的任务,集群后会被执行 N 次。
解决方案:
| 方案 | 做法 |
|---|---|
| 分布式锁 | 任务开始前抢锁,抢到的才执行 |
| 选主 | 用 ZooKeeper / etcd 选出一个 leader,只有 leader 执行 |
| 分片执行 | 用 XXL-Job 这类调度平台,把任务分片给不同实例 |
| 数据库唯一约束 | 让重复执行天然失败(如插入带唯一键的任务记录) |
注意":任务本身幂等"也是一个解法——如果重复执行不产生副作用,那就没必要防。但清理订单这种任务通常不幂等(重复执行可能误删),所以还是要防。
4. 日志排查变复杂
单机时 tail -f 一个文件就完事。集群后请求可能落在任意一台机器上,出了问题要一台台翻日志。
必须引入链路追踪:
- 每个请求生成唯一
traceId,通过 HTTP header / MQ 属性一路透传 - 日志格式里带上
traceId - 用 ELK / Loki 之类的工具集中收集
- 排查时按
traceId一次捞出整条链路的日志
没有链路追踪的集群,排查效率比单机还低——因为你甚至不知道请求打到了哪台机器。
四、集群还会引出这些设计变化
| 单机做法 | 集群后必须改成 |
|---|---|
synchronized 加锁 |
分布式锁 |
| 本地缓存(Guava Cache) | 本地缓存 + 分布式缓存,或改用 Redis |
AtomicInteger 本地计数 |
Redis 计数 / 分片聚合 |
| 本地文件存储(上传的图片) | 对象存储(MinIO / OSS) |
@Scheduled 定时任务 |
分布式调度 + 锁 / 选主 |
| 本地 session | Redis session / JWT |
| 内存限流(令牌桶) | Redis + Lua 分布式限流 |
| 单机 ID 自增 | 雪花算法 / 号段模式 |
规律很清楚:凡是"依赖单机内存状态"的东西,集群后都会失效。 上线前按这个清单过一遍,能避免大部分坑。
五、什么时候不该上集群
小型项目不建议微服务,同样地,也不是所有系统都需要集群:
| 场景 | 建议 |
|---|---|
| 日活几百、可接受短暂停机 | 单机 + 快速重启更简单 |
| 内部管理系统、非核心时段可停 | 单机 + 定时备份 |
| 有状态且难拆分的应用 | 优先纵向扩容(升配),再考虑集群 |
| 流量波动大、要求高可用 | 集群 + 负载均衡 |
集群的运维复杂度是真实成本:多台机器的部署、监控、日志、证书、配置同步,都要额外投入。如果业务量根本到不了单机上限,上集群只是徒增麻烦。
小结
- 集群解决四件事:高可用、并发能力、不停机发布、容灾隔离
- 高可用只能靠冗余解决,升级硬件解决不了单点故障
- 集群把"单机问题"换成了"分布式问题",代价是:
- 会话共享 → Redis session 或 JWT
- 单机锁失效 → 分布式锁
- 定时任务重复执行 → 分布式锁 / 选主 / 分片
- 日志排查变难 → 链路追踪 + 集中日志
- 规律:凡是依赖单机内存状态的东西,集群后都会失效——上线前照清单过一遍
- 集群不是默认选项:业务量不到单机上限时,单机 + 快速重启更划算
从单机到集群,不只是多部署几份代码,而是一次架构范式的切换。想清楚哪些状态必须外置,比配好 Nginx 更重要。
留下点什么
评论基于 GitHub Discussions(giscus)。在项目根目录配置.env.local填入 NEXT_PUBLIC_GISCUS_* 四个变量后自动开启。