· 王思腾 · 系统架构 · 10 分钟阅读

从 0 到 100 万用户:架构演进与系统扩展性设计

没有一步到位的架构,只有随规模不断演化的架构。本文按用户量级(单机 → 垂直扩展 → 水平扩展 → 服务化 → 多机房)梳理系统在不同阶段遇到的瓶颈与应对,讲清扩展性的两个方向(垂直/水平)、无状态化、分库分表、缓存与读写分离,以及“什么时候该做什么”的取舍。

没有一步到位的架构,只有随规模不断演化的架构。本文按用户量级(单机 → 垂直扩展 → 水平扩展 → 服务化 → 多机房)梳理系统在不同阶段遇到的瓶颈与应对,讲清扩展性的两个方向(垂直/水平)、无状态化、分库分表、缓存与读写分离,以及“什么时候该做什么”的取舍。

很多团队一上来就想设计“能撑百万用户”的架构,结果是技术复杂度远超业务需要,几个人的团队被微服务、中间件拖得寸步难行。架构不是设计出来的,是演化出来的——正确的做法是在每个阶段用“刚好够用”的方案解决“当前最痛的瓶颈”,同时为下一步留好演化空间。

一、扩展性的两个方向

先厘清概念,扩展(Scale)无非两条路:

  • 垂直扩展(Scale Up):升级单机配置——更强的 CPU、更多内存、更快的 SSD。简单、零改造,但有硬件上限,且成本非线性增长(顶配机器贵得离谱);
  • 水平扩展(Scale Out):加更多机器,把负载分摊到多个实例。理论上无上限、成本线性,但要求系统能被“拆开”,引入了分布式复杂度。

早期几乎总是先垂直扩展(花钱省时间),等单机到顶或成本不划算,再转向水平扩展(花复杂度换规模)。水平扩展的前提是无状态化。

二、阶段一:单体 + 单库(0 ~ 1 万用户)

刚起步,一台应用服务器 + 一个数据库就够了:

flowchart LR
    U["用户"] --> App["应用服务器<br/>单体应用"]
    App --> DB[("MySQL 单库")]
    App --> FS["本地文件存储"]
  • 所有功能在一个代码库、一个进程,本地事务、本地日志,开发部署都简单;
  • 这个阶段不要过度设计:不上微服务、不上消息队列、不搞分库分表。
  • 唯一要提前留的习惯:把应用做成无状态的(会话存 Redis/Token 而不是进程内存、文件别只存在本机),这是将来水平扩展最便宜的准备。

瓶颈信号:应用 CPU/内存打满,或数据库查询变慢。

三、阶段二:垂直扩展 + 缓存 + 读写分离(1 万 ~ 10 万用户)

流量上来后,最先扛不住的通常是数据库。按性价比依次优化:

  1. 加缓存:热点数据(商品、用户资料、配置)放 Redis,挡掉大部分读请求——这是收益最大的一步;
  2. 数据库读写分离:主库写、从库读,读负载摊到多个副本;
  3. 应用服务器垂直扩容,并开始部署多个实例 + 负载均衡。
flowchart TB
    U["用户"] --> LB["负载均衡 Nginx"]
    LB --> A1["应用实例 1"]
    LB --> A2["应用实例 2"]
    A1 --> Cache[("Redis 缓存")]
    A2 --> Cache
    A1 --> M[("MySQL 主库 · 写")]
    A2 --> M
    M -. 复制 .-> S1[("从库 · 读")]
    M -. 复制 .-> S2[("从库 · 读")]
    A1 -. 读 .-> S1
    A2 -. 读 .-> S2

注意两个坑:

  • 无状态化是加多实例的前提:会话、本地缓存、定时任务都不能绑死在某台机器上;
  • 读写分离引入主从延迟:刚写完立刻读可能读到旧数据,核心链路(如支付后查订单)要走主库或做延迟兜底。

四、阶段三:水平扩展 + 服务化(10 万 ~ 50 万用户)

单库写入和单表数据量到达极限(经验上单表数千万行、单库 QPS 数千就要警惕),单体也臃肿到团队难以并行开发。这一步做两件大事:

1. 数据库水平拆分

  • 分库分表:按用户 ID 等维度哈希取模,把数据分散到多个库、多张表。拆分后单表数据量和写入压力都下降;
  • 带来的代价要提前想清楚:跨片查询、分布式事务、全局唯一 ID、分页排序都变复杂,通常靠中间件(ShardingSphere)或在应用层路由解决;
  • 能不拆就不拆,先靠“垂直拆库”(按业务把订单、用户、商品拆到不同库)缓解,它比分表简单得多。

2. 应用服务化

把单体按业务边界拆成若干服务(订单、用户、商品、支付……),前面加 API 网关,服务间用消息队列异步解耦。详见微服务架构篇。

flowchart TB
    U["用户"] --> GW["API 网关"]
    GW --> Svc
    subgraph Svc["服务集群(各自多实例 + 独立扩容)"]
        S1["订单服务"] --- S2["用户服务"] --- S3["商品服务"] --- S4["支付服务"]
    end
    Svc --> MQ["消息队列 Kafka"]
    Svc --> C[("Redis 集群")]
    S1 --> D1[("订单库 分库分表")]
    S2 --> D2[("用户库")]
    S3 --> D3[("商品库 + ES")]

关键原则:不同服务按各自压力独立扩容——搜索服务吃内存就给它加内存型实例,订单服务吃 IO 就单独扩,不必整体一起扩。

五、阶段四:多机房与弹性化(50 万 ~ 100 万+ 用户)

到这个量级,单点机房故障、活动大促脉冲流量、跨地域访问延迟成为新问题:

  • CDN + 静态化:图片、静态资源、甚至热点页面全部边缘加速,源站压力骤降;
  • 弹性伸缩:跑在 Kubernetes 上,按 CPU/QPS 自动扩缩容,大促前预置、过后回收;
  • 限流降级:网关层和服务层都要有限流,非核心功能(推荐、评论)可降级,保住下单、支付主链路;
  • 多可用区 / 多机房部署:单机房故障能切流,数据跨机房备份;
  • 异步化削峰:秒杀、下单等写洪峰用消息队列缓冲,后端按能力消费,避免直接压垮数据库。
flowchart LR
    U["全国用户"] --> CDN["CDN 边缘节点"]
    CDN --> GW["网关集群 · 限流"]
    GW --> K8s["K8s 服务集群<br/>自动扩缩容"]
    K8s --> MQ["MQ 削峰"]
    MQ --> W["后端消费者(按能力处理)"]
    W --> DB[("分库分表 + 多级缓存")]

六、贯穿始终的扩展性原则

无论哪个阶段,这几条都成立:

  1. 无状态化:应用实例不存会话、不存本地关键状态,随时可加可减可重启;
  2. 让瓶颈可观测:没有监控(QPS、延迟 P99、错误率、资源水位)就不知道该扩哪里,扩容全靠猜;
  3. 优先垂直扩展、优先缓存、优先异步——这三件事性价比最高,往往能把“必须分布式化”的时间点往后拖很久;
  4. 容量规划:压测摸清单机/单库上限,按“峰值 × 冗余系数(通常 2~3 倍)”预留容量,别等打挂了才扩;
  5. 为演化留接口,不为假想需求做实现:代码里把数据访问、会话、存储抽象好,将来分库、换中间件时改动可控。

七、小结

  1. 架构是演化出来的:每个阶段解决当前最痛的瓶颈,用刚好够用的方案,别提前背上分布式复杂度;
  2. 扩展两条路:早期垂直扩展省时间,后期水平扩展换规模,水平扩展的前提是无状态化;
  3. 典型演进主线:单体单库 → 缓存 + 读写分离 + 多实例 → 分库分表 + 服务化 → CDN/弹性伸缩/多机房;
  4. 缓存、异步、垂直扩容是性价比最高的三板斧,能拖就拖“重型分布式改造”的到来;
  5. 可观测性和容量规划要先行——看得见瓶颈,才谈得上精准扩展。

从 0 到百万用户,拼的不是一开始用了多炫的技术,而是每一步有没有把钱和复杂度花在刀刃上。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
电商系统架构设计:从下单到履约的全链路

电商系统架构设计:从下单到履约的全链路

电商系统的复杂性藏在库存、订单、支付与履约的每一次协同里。本文从业务域划分出发,带你走一遍下单主链路,并重点讨论超卖、幂等、分布式事务与大促高并发的应对方案。

消息队列原理与选型:解耦、削峰与异步化

消息队列原理与选型:解耦、削峰与异步化

消息队列是分布式系统的“减震器”:异步解耦、流量削峰、事件驱动。本文讲清 MQ 的三大作用、投递语义(至多/至少/恰好一次)、消费幂等、顺序消息、延迟/死信队列,以及 Kafka、RocketMQ、RabbitMQ 的适用场景与常见坑。

缓存设计实战:穿透、击穿、雪崩与一致性

缓存设计实战:穿透、击穿、雪崩与一致性

缓存是高并发系统的第一道防线,但用不好会引入数据不一致、缓存穿透、击穿、雪崩等问题。本文系统梳理缓存读写策略(Cache Aside/Read/Write Through)、三大经典问题的成因与解法、热点 key、缓存与数据库一致性,以及 Redis 在生产中的实践要点。