返回知识库
构建与发布14 分钟阅读 · 最近更新 2026-05-28

从单机到百万用户:系统扩展的演进之路

从单机到百万用户:系统扩展的演进之路

几乎没有哪个系统是一开始就为百万用户设计的。它们更像生物——先以最小的形态活下来, 再在一次次流量增长的压力下,长出新的器官。这篇文章把这条演进路线拆成若干阶段: 每一步都因为「上一步的瓶颈」而出现,而不是因为「架构图好看」。

读的时候不妨问自己一个问题:我现在卡在哪一阶段? 提前一步是过度设计,落后一步是事故。

第 0 阶段:一切都在一台机器上

最初的系统极其朴素:Web 应用、数据库、缓存,全挤在同一台服务器里。一个请求从用户 到响应,大致是这样一条流水线:

这套结构能撑起一个 demo 或早期产品。瓶颈也很明显:Web 进程和数据库抢同一份 CPU、 内存与磁盘 IO,任何一方过载都会拖垮另一方;而且这台机器一旦宕机,整个系统跟着消失

第 1 阶段:把数据库搬出去

第一刀通常砍在 Web 与数据之间。把数据库挪到独立的服务器上,Web 层和数据层就能各自 按需扩容,互不挤占资源。

这里也要顺手决定用什么数据库。关系型(MySQL、PostgreSQL)用表和行建模,支持 JOIN, 适合绝大多数有结构、有关联的业务,是稳妥的默认选项。非关系型(NoSQL,如键值、文档、 列族、图)在这些场景更合适:

  • 需要极低延迟的读写;
  • 数据本身松散、没什么关联关系;
  • 只是序列化/反序列化整块对象(JSON、XML 等);
  • 数据量大到单机关系库难以承载。

选型不是信仰之争。先用关系库,等它确实顶不住某个具体需求时,再为那部分引入 NoSQL。

第 2 阶段:纵向 vs 横向,以及负载均衡

扩容有两个方向。**纵向(scale up)**是给单台机器加 CPU、加内存——简单直接,流量不大时 最省心,但有硬上限,且仍是单点:机器挂了,服务就没了。**横向(scale out)**是加机器, 上限几乎无穷,代价是工程复杂度——多台机器之间怎么分流、怎么协调,需要额外的部件。

那个部件就是负载均衡器。客户端只连负载均衡器的公网 IP,真正干活的 Web 服务器都躲在 内网,由负载均衡器把流量均匀分给它们:

加上负载均衡器,Web 层既拿到了高可用(挂一台不影响整体),又拿到了弹性(随时增减 实例)。但数据层还是单点——下一步就轮到它。

第 3 阶段:数据库读写分离

大多数应用读远多于写。主从复制(现在多称主/从或 primary/replica)正是利用这一点: 主库负责写,数据复制到若干从库,从库只负责读。想扛更多读,加从库就行。

这套结构顺带带来三个好处:读可以并行处理(性能)、数据有多份副本(可靠)、只要还有 一个实例在线就能继续服务(可用)。故障处理也有定式:某个从库挂了,读流量临时转到主库 或其它从库;主库挂了,提升一个从库当新主,再补一个新从库进来。

至此,系统大致是:DNS → 负载均衡器 → 多台 Web 服务器(写打主库、读打从库)。Web 层和 数据层都不再是单点了。

第 4 阶段:缓存,把昂贵的查询挡在门外

每次页面加载都去数据库跑一遍同样的昂贵查询,是巨大的浪费。缓存是一层临时存储, 读取速度远快于数据库,而且可以独立于数据库扩容。最常见的是「读穿透」模式:服务器先查 缓存,命中就直接返回,没命中才去查数据库、再把结果写回缓存。

缓存好用,但有几个绕不开的权衡:

  • 适用场景:读多写少的数据最划算;缓存通常重启即丢,别拿它当持久层。
  • 过期策略:太短,数据库被频繁打扰;太长,数据会变陈旧。
  • 一致性:数据库改了而缓存没更新,就会读到旧值——分布式系统里这件事不简单。
  • 单点风险:单台缓存挂了就是单点故障,要么堆内存、要么多地多副本。
  • 淘汰策略:缓存满了往里塞新数据时踢谁?LRU、LFU、FIFO 是常见选择。

第 5 阶段:CDN,让静态资源离用户更近

图片、CSS、JS 这类静态资源,没必要每次都从源站千里迢迢取回。CDN 是一张地理上分散 的服务器网络,用户请求静态资源时,由离他最近的边缘节点提供。第一次没命中就回源拉取并 缓存(带一个 TTL),之后的请求在 TTL 内都由边缘节点直接应答,根本不打扰源站。

差别有多大?看这张延迟对比——同一份资源,绕到远端源站取,和从就近的边缘节点取:

CDN 也有它的账要算:它是第三方按量收费的,别把冷门数据塞进去;TTL 同样要权衡新鲜度 与回源压力;还要给客户端留一条「CDN 挂了能绕过」的降级路径,以及失效(invalidation)的手段。

第 6 阶段:让 Web 层变无状态

要想随意增减 Web 服务器,它们就不能各自记着用户的会话。有状态架构里,用户被绑死在 存了自己 session 的那台机器上——换一台就丢了登录态。负载均衡器可以用「粘性会话」硬把 用户钉回原机,但这让加减机器变得很难,故障时尤其被动。

无状态架构的做法很干净:服务器自己不存任何用户数据,所有会话状态统一放进一个共享存储 (关系库或 NoSQL),任意一台 Web 服务器都能服务任意请求。于是 Web 层可以自动伸缩、坏了 随便换,完全无所谓哪个请求落在哪台机器上。

第 7 阶段:多数据中心

用户遍布全球时,单个数据中心既慢又脆。多数据中心用 GeoDNS 按用户 IP 把他路由到最近的 机房;某个机房整体故障时,把全部流量切到健康的那个。

要真正做到这点,有三件麻烦事要解决:流量调度(用 GeoDNS 把请求送对机房)、数据同步 (故障切换后,用户在另一个机房得能找到自己的数据)、以及测试与部署(自动化保证各机房 版本一致)。这三件事任何一件没做好,「多活」都会变成「多个单点」。

第 8 阶段:用消息队列解耦

系统越大,越需要把组件拆开、让它们各自独立伸缩。消息队列是一个持久的中间件,支持 异步通信:生产者发消息,消费者订阅并处理。两边彻底解耦——消费者宕机时,生产者照样能发, 消息会在它恢复后被消费。

经典例子是图片处理:Web 服务器把任务发布到队列,一组可弹性增减的 Worker 在另一端消费。 流量高峰时多开 Worker,平时少开,生产者完全不必关心消费端的状态。

第 9 阶段:日志、监控与自动化

系统大到一定程度,「看不见」本身就是最大的风险。这一阶段的投入不直接服务用户,却决定 你能不能在凌晨三点快速定位问题:

  • 日志:错误日志集中收集,事后可查、可追溯。
  • 指标:从机器资源到业务数据,既看系统健康,也看生意走向。
  • 自动化:持续集成——自动构建、测试、部署——既能尽早暴露问题,也让团队跑得更快。

第 10 阶段:分库分表(Sharding)

数据库本身也要扩。纵向扩(给数据库加硬件,如几十 TB 内存的实例)能撑很久,但终有硬件 上限、仍是单点、且贵。横向扩的主力手段是分片(sharding):把一个大数据集切成若干小片, 每片结构相同、数据不同。最常见的分片方式是按某个键取模:

分片威力大,复杂度也随之而来:分片键的选择至关重要,要让数据尽量均匀;单片长太大就要 重新分片(resharding),一致性哈希能减少搬动的数据量;少数热点用户会造成热点(celebrity) 问题,可能得给他们单独开片;跨片 JOIN 很难做,常见做法是反范式化(de-normalize)以避开 跨片关联。

收尾:扩展是一个迭代过程

没有一步到位的「终极架构」。每一阶段都是被上一阶段的瓶颈逼出来的,而你能走到的最远处, 往往取决于这几条朴素原则:

  • Web 层保持无状态;
  • 每一层都做冗余,消除单点;
  • 把热数据放进缓存;
  • 静态资源交给 CDN;
  • 用多数据中心提升就近与容灾;
  • 数据层靠分片横向扩展;
  • 用消息队列把大系统拆成可独立伸缩的服务;
  • 持续投入监控与自动化。

把这几条刻进直觉,你就能在「现在卡在哪一阶段」这个问题上,每次都答得比上次更快一点。