Appearance
第 7 讲|单机到集群
小型网站从一台服务器起步是最自然的选择——Nginx、后端程序、数据库、静态文件全部安装在同一台机器上,目录集中、日志集中,登录一台机器就能看到整个系统。这种部署方式称为单机部署:问题少、调试简单、运维成本低,绝大多数项目都从这个状态开始。
单机的优势是直观。页面打不开 → 看 Nginx;接口 500 → 翻应用日志;数据查不到 → 登数据库看。链路短,所有问题都在一台机器上,不用跨机器切换上下文。
但随着业务增长,单机会遇到天花板:单点故障、容量上限、发布影响、扩展性不足。本讲围绕"从单机演进到集群"的过程,讲清楚每一步要解决的问题、引入的复杂度、对应的工程实践。
一、单机部署
最简单的单机架构:

单机部署的合理场景
不要认为"单机就是落后"——单机在以下场景仍然是最合理的选择:
- 内部小工具,访问量小
- 早期项目,流量不确定
- 测试环境、demo 环境
- 个人项目、博客
- 边缘节点(IoT、门店本地)
只要做好备份、监控、文档化的重建步骤,单机能稳定运行很久。
单机的根本限制
单机部署的所有问题都源于一句话:所有鸡蛋在一个篮子里。
| 问题 | 具体后果 |
|---|---|
| 单点故障 | 这台机器重启或宕机,整个网站一起停 |
| 资源争抢 | 数据库占满磁盘 IO,应用响应也变慢 |
| 容量上限 | 单机配置有物理上限(CPU 核数、内存大小) |
| 发布影响 | 部署新版本时,所有用户都跟着停一会儿 |
| 安全风险 | 一台机器被攻破,全部数据暴露 |
| 故障域大 | 任何组件出问题都波及全部业务 |
二、垂直扩容:单机加配置
业务增长后,最直觉的反应是给机器加配置:CPU 从 4 核升到 8 核、16 核;内存从 8G 升到 32G、64G;磁盘从机械盘换成 SSD、NVMe;网络从千兆换万兆。这种做法称为 垂直扩容(Vertical Scaling / Scale Up)。
垂直扩容的优势
| 优势 | 说明 |
|---|---|
| 改动小 | 应用代码、部署架构、运维流程不变 |
| 没有架构复杂度 | 不引入状态管理、分布式协调等问题 |
| 立即生效 | 升配重启即可,几分钟搞定 |
| 适合 IO 密集型 | 单机磁盘升 NVMe、内存加大,数据库性能显著提升 |
垂直扩容很适合早期顶一段时间,尤其当瓶颈确实是单机资源(CPU、内存、磁盘 IO)的时候。不要为了"看起来高大上"就跳过垂直扩容直接搞水平扩容——前者收益高、成本低。
垂直扩容的根本限制
垂直扩容解决不了的问题:
- 单点依然单点 — 机器规格再高,仍然是一台机器,硬件故障、系统升级、磁盘损坏都会让服务停摆
- 配置有上限 — 云厂商最高规格的实例也是有限的;到了那种规模,单机也撑不住
- 成本边际效应陡增 — 顶配机器价格远超低配的线性比例(顶配机器贵 10 倍,性能未必快 10 倍)
- 维护窗口风险 — 升配通常需要停机重启
到了一定规模,只有水平扩容才能继续扩展。
三、水平扩容:增加机器数量
水平扩容(Horizontal Scaling / Scale Out)的核心思路:不升单台机器的规格,而是增加机器数量。前面放负载均衡,把请求分发到多个实例:

水平扩容带来的能力
| 能力 | 说明 |
|---|---|
| 高可用 | 某个实例挂了,负载均衡自动把流量切到其他实例 |
| 平滑发布 | 一台一台滚动替换实例,用户无感知 |
| 容量弹性 | 不够就加机器,理论上无限扩展(实际会被数据库等环节卡住) |
| 故障隔离 | 单实例的问题不会波及全部用户 |
水平扩容的代价
水平扩容引入了一系列分布式系统的复杂性,从单机切到多机不是"把代码部署到多台机器"就完事,要解决以下问题:
| 问题 | 单机时不存在的复杂度 |
|---|---|
| 状态管理 | Session、缓存、文件存储都不能放在某一台机器内 |
| 数据一致性 | 多实例同时操作同一数据需要协调 |
| 配置分发 | 改个配置要同步到所有实例,且不能一些实例新一些实例旧 |
| 日志聚合 | 日志散落在不同机器,需要集中收集 |
| 监控告警 | 监控目标从一个变多个,告警维度、聚合方式都要重新设计 |
| 部署协调 | 多实例的滚动更新、灰度策略 |
| 服务发现 | 实例 IP 动态变化,客户端如何找到可用实例 |
接下来几节展开其中最核心的几个:Session、文件存储、数据库瓶颈。
四、Session 与状态管理
多实例最先暴露的问题就是状态。最典型的是登录 Session。

单机内存存 Session 的问题
用户体验是"刷新一下就掉登录",而且只在部分请求上出现——因为负载均衡轮询,有时打到登录的那台,有时打到没有 Session 的那台。这种"时好时坏"的故障最难排查。
把状态从应用实例抽出去
解决思路:把所有需要持久或共享的状态,放到独立的外部系统。应用实例本身变成"无状态"的——任何实例处理任何请求都一样。
| 状态类型 | 抽到哪里 |
|---|---|
| 登录 Session | Redis、数据库,或改为 Token(JWT 无状态) |
| 上传文件 | 对象存储(OSS、S3)、共享文件系统 |
| 本地缓存 | Redis 集中缓存 |
| 定时任务 | 分布式调度系统(XXL-Job、Airflow) |
| 任务队列 | 消息中间件(Kafka、RabbitMQ) |
| 长连接(WebSocket) | 专门的网关层,或客户端层面的会话粘性 |
无状态应用的价值
无状态(Stateless)是云原生架构的核心原则之一。无状态的应用可以:
- 任意扩缩容(随时加新实例、随时减实例)
- 随时重启(Kubernetes 杀掉 Pod 不影响业务)
- 滚动更新(不会因为 Session 集中在某一台导致用户掉线)
- 跨可用区/跨机房部署(实例 IP 完全不固定)
这就是为什么 Kubernetes 时代特别强调"应用要做成无状态"——只有无状态,才能享受云原生的所有调度灵活性。
临时方案:Session Sticky
如果暂时无法改造应用,负载均衡可以配置"会话粘性"(session sticky)——同一个用户的请求总是分到同一个实例。
| 方式 | 实现 |
|---|---|
| 基于 Cookie | LB 给浏览器塞一个标识 Cookie,后续请求按此分发 |
| 基于 IP | 按客户端 IP 哈希,同一 IP 总到同一实例 |
但这是临时方案,不是长期方案。会话粘性的问题:
- 某个实例挂掉时,该实例上的用户全部掉线
- 滚动发布时,被替换的实例上的用户掉线
- 单一实例可能承担不平衡的流量
- 客户端 IP 经过 NAT 后,大量用户来自同一 IP,负载不均
正确的方向仍然是把 Session 抽到外部。
五、文件存储
多实例后,本地文件存储的问题立刻暴露。
本地存文件的问题
用户的体验:"明明上传成功,头像怎么时有时无"。
把文件存储抽到外部
把文件存储与应用实例解耦的几种方式:
| 方式 | 代表 | 适用场景 |
|---|---|---|
| 共享文件系统 | NFS、NAS、CephFS | 简单,但性能与可靠性有上限 |
| 对象存储 | AWS S3、阿里 OSS、MinIO、Ceph RGW | 主流选择,适合图片、附件、备份、静态资源 |
| 分布式存储 | Ceph、GlusterFS | 大规模、高可靠性要求 |
| CDN | 接入对象存储后端 | 公网分发静态资源 |
主流 Web 系统的标准做法:
- 后端直接把文件传到对象存储(或让前端直接上传到对象存储,后端只签发上传凭证)
- 数据库里只保存文件的 URL、大小、类型、归属用户等元数据
- 应用实例的本地磁盘只用于临时文件、日志,业务文件全部外置
这种架构下:
- 应用实例随时可重建、扩容、迁移
- 文件不依赖任何单一机器
- 对象存储自身有副本和容灾(由云厂商保证)
六、数据库瓶颈
应用层水平扩容后,瓶颈往往转移到数据库层——前面挂 10 个应用实例,每个实例都开几十个数据库连接,每个连接都在跑查询,数据库的连接数、CPU、IO 很快达到上限。
数据库优化的顺序
按"先简单后复杂"的优化顺序:
1. 连接池
应用侧用连接池复用连接,避免频繁建立和断开:
| 语言 | 主流连接池 |
|---|---|
| Java | HikariCP、Druid、c3p0 |
| Python | SQLAlchemy 内置 pool、PgBouncer |
| Go | database/sql 自带 |
| Node.js | pg-pool、mysql2 |
合理配置连接池大小很关键——不是越大越好。连接数过多反而拖垮数据库 CPU。经验值:每个应用实例 10-30 个连接,根据数据库实际负载调整。
2. 索引优化
慢 SQL 的大头都是缺索引或索引未生效。加合适的索引,查询时间可以从秒级降到毫秒级。具体方法在 第 6 讲 数据库的出现 已展开。
3. 缓存
热点数据放 Redis,数据库压力直接下降一个数量级。同样在第 6 讲展开。
4. 读写分离
写请求只打到主库,读请求分摊到多个从库:
读写分离的关键认知:主从复制是有延迟的(通常毫秒级,极端情况秒级)——刚写入主库的数据,可能从从库读不到。
应对方式:
- 写后立即读 → 直接打主库,不走从库
- 关键路径(支付、订单)→ 全部走主库,避免不一致
- 可容忍延迟(列表、统计)→ 走从库,降低主库压力
5. 分库分表
单表数据量到千万级、写入量也很大时,前面所有优化做完了,才考虑水平拆分——把一张大表拆成多个库或多张表:
| 拆分方式 | 含义 |
|---|---|
| 垂直分库 | 按业务把不同表拆到不同库(用户库、订单库、商品库) |
| 垂直分表 | 一张大表按列拆分(常用字段一张表,不常用字段另一张) |
| 水平分库 | 按 ID/hash 把同一张表的数据分散到多个库 |
| 水平分表 | 同库内,按 ID/时间把同一张表分成多张 |
分库分表是重大架构调整,会带来:
- 跨库 JOIN 不可用
- 全局事务变难
- 跨库统计变难
- 路由层(中间件)成为新组件,本身有复杂度
- 代码层面要大量适配
不到万不得已不要分库分表。优先用其他手段——能加机器解决就加机器,能拆业务就拆业务。
数据库优化的排查流程
接口慢且怀疑数据库:
bash
# 1. 看慢查询日志中是否有反复出现的 SQL
mysql -e "SHOW VARIABLES LIKE 'slow_query%';"
tail -100 /var/log/mysql/mysql-slow.log
# 2. 看 EXPLAIN 执行计划
EXPLAIN SELECT ...;
# type 列是 ALL 表示全表扫描,基本就是索引问题
# 3. 看当前正在执行的查询
SHOW FULL PROCESSLIST;
# 找出耗时长的 query
# 4. 看连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';按这个顺序排查,绝大多数数据库性能问题能定位到具体 SQL 或具体配置。
七、从单机到集群的演进顺序
总结整个演进过程的常见路径:

每一步都引入新的复杂度,不要跳跃式演进——能用简单方案解决的不要上复杂方案。从单机到 Kubernetes 一步跨过去会引入大量不必要的复杂度,先用最小够用的架构,有具体瓶颈再演进。