Skip to content

第 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)的时候。不要为了"看起来高大上"就跳过垂直扩容直接搞水平扩容——前者收益高、成本低。

垂直扩容的根本限制

垂直扩容解决不了的问题:

  1. 单点依然单点 — 机器规格再高,仍然是一台机器,硬件故障、系统升级、磁盘损坏都会让服务停摆
  2. 配置有上限 — 云厂商最高规格的实例也是有限的;到了那种规模,单机也撑不住
  3. 成本边际效应陡增 — 顶配机器价格远超低配的线性比例(顶配机器贵 10 倍,性能未必快 10 倍)
  4. 维护窗口风险 — 升配通常需要停机重启

到了一定规模,只有水平扩容才能继续扩展。

三、水平扩容:增加机器数量

水平扩容(Horizontal Scaling / Scale Out)的核心思路:不升单台机器的规格,而是增加机器数量。前面放负载均衡,把请求分发到多个实例:

水平扩容提升容量也带来状态和一致性代价

水平扩容带来的能力

能力说明
高可用某个实例挂了,负载均衡自动把流量切到其他实例
平滑发布一台一台滚动替换实例,用户无感知
容量弹性不够就加机器,理论上无限扩展(实际会被数据库等环节卡住)
故障隔离单实例的问题不会波及全部用户

水平扩容的代价

水平扩容引入了一系列分布式系统的复杂性,从单机切到多机不是"把代码部署到多台机器"就完事,要解决以下问题:

问题单机时不存在的复杂度
状态管理Session、缓存、文件存储都不能放在某一台机器内
数据一致性多实例同时操作同一数据需要协调
配置分发改个配置要同步到所有实例,且不能一些实例新一些实例旧
日志聚合日志散落在不同机器,需要集中收集
监控告警监控目标从一个变多个,告警维度、聚合方式都要重新设计
部署协调多实例的滚动更新、灰度策略
服务发现实例 IP 动态变化,客户端如何找到可用实例

接下来几节展开其中最核心的几个:Session、文件存储、数据库瓶颈。

四、Session 与状态管理

多实例最先暴露的问题就是状态。最典型的是登录 Session。

集群后要把 Session 状态从应用实例抽出去

单机内存存 Session 的问题

用户体验是"刷新一下就掉登录",而且只在部分请求上出现——因为负载均衡轮询,有时打到登录的那台,有时打到没有 Session 的那台。这种"时好时坏"的故障最难排查。

把状态从应用实例抽出去

解决思路:把所有需要持久或共享的状态,放到独立的外部系统。应用实例本身变成"无状态"的——任何实例处理任何请求都一样。

状态类型抽到哪里
登录 SessionRedis、数据库,或改为 Token(JWT 无状态)
上传文件对象存储(OSS、S3)、共享文件系统
本地缓存Redis 集中缓存
定时任务分布式调度系统(XXL-Job、Airflow)
任务队列消息中间件(Kafka、RabbitMQ)
长连接(WebSocket)专门的网关层,或客户端层面的会话粘性

无状态应用的价值

无状态(Stateless)是云原生架构的核心原则之一。无状态的应用可以:

  • 任意扩缩容(随时加新实例、随时减实例)
  • 随时重启(Kubernetes 杀掉 Pod 不影响业务)
  • 滚动更新(不会因为 Session 集中在某一台导致用户掉线)
  • 跨可用区/跨机房部署(实例 IP 完全不固定)

这就是为什么 Kubernetes 时代特别强调"应用要做成无状态"——只有无状态,才能享受云原生的所有调度灵活性。

临时方案:Session Sticky

如果暂时无法改造应用,负载均衡可以配置"会话粘性"(session sticky)——同一个用户的请求总是分到同一个实例。

方式实现
基于 CookieLB 给浏览器塞一个标识 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. 连接池

应用侧用连接池复用连接,避免频繁建立和断开:

语言主流连接池
JavaHikariCP、Druid、c3p0
PythonSQLAlchemy 内置 pool、PgBouncer
Godatabase/sql 自带
Node.jspg-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 一步跨过去会引入大量不必要的复杂度,先用最小够用的架构,有具体瓶颈再演进。

接下来几讲展开演进过程中的具体组件:负载均衡高可用架构微服务容器和编排