深度文章
信息来源:blog.algomaster.io 2026.02.01 21:56 约 33 分钟 商业洞察 持续阅读

如何将系统从0扩展到超过1000万用户

RECODEX · 深度文章 商业洞察

扩展是一个复杂的话题,但在大型科技公司负责处理数百万次请求的服务并且从零开始扩展我自己的创业公司(AlgoMaster.io 后,我意识到大多数系统在成长过程中会经历一组出乎意料地相似的发展阶段。

关键见解是, 不要一开始就过度设计 。从简单开始,识别瓶颈,逐步扩展。

在这篇文章中,我将带你了解将系统从零扩展到一千万用户及以上的 7 个阶段 。每个阶段都针对在不同增长点出现的特定瓶颈。你将学到在何时添加什么、为什么有用以及涉及的权衡。

无论你是在构建应用或网站、准备系统设计面试,还是只是好奇大规模系统如何运行,理解这一演进过程将提升你对架构的思考方式。

注: 本文中的用户范围只是大致指引。具体阈值会根据你的产品、工作负载和流量模式有所不同。


第一阶段:单服务器(0–100 用户)

当你刚起步时,首要任务很简单: 尽快发布并验证你的想法 。在这一阶段过早优化只会在你可能永远不会遇到的问题上浪费时间和金钱。

最简单的架构是将一切放在一台单机服务器上:你的 web 应用、数据库以及任何后台任务都运行在同一台机器上。

如何将系统从0扩展到超过1000万用户

这就是 Instagram 的起步方式。2010 年,当 Kevin Systrom 和 Mike Krieger 推出第一个版本时,上线第一天就有 25,000 人注册。

他们没有在前期过度设计。凭借小团队和简单的架构,他们根据实际需求进行扩展,随着使用量增长增加容量,而不是为假想的未来流量进行构建。

这种架构的样子

在实践中,单服务器部署意味着:

  • 一个处理 HTTP 请求的 Web 框架(Django、Rails、Express、Spring Boot)

  • 存储数据的数据库(PostgreSQL、MySQL)

  • 用于异步任务的后台作业处理(Sidekiq、Celery)

  • 可能在前端放一个反向代理(Nginx)用于 SSL 终止

所有这些都运行在一台虚拟机上。对于一个基础 VPS(DigitalOcean Droplet、AWS Lightsail、Linode),你的云服务账单可能是每月 20–50 美元。

为什么这对早期阶段有效

在这个阶段,简单性是你最大的优势:

  • 快速部署 :一台服务器意味着一个地方可以进行部署、监控和调试。

  • 低成本 :一台每月 20–50 美元的虚拟专用服务器(VPS)就能轻松应对前 100 名用户。

  • 更快的迭代 :没有分布式系统的复杂性来拖慢开发速度。

  • 更易调试 :所有日志集中在一个地方,组件之间没有网络问题。

  • 全栈可视化 :因为只有一条执行路径,你可以对每个请求进行端到端追踪。

你所做的取舍

这种简化是基于你明知并接受的取舍:

如何将系统从0扩展到超过1000万用户

何时转变

当你注意到以下迹象时,就该知道是时候进化了:

  • 数据库查询在高峰流量时变慢 :应用和数据库争用相同的 CPU 和内存。一次耗时的查询就可能拉低所有人的 API 延迟。

  • 服务器 CPU 或内存持续超过 70–80%:你已接近单台机器能可靠承受的极限。

  • 部署需要重启并导致停机 :即便是短暂的中断也会被察觉,用户开始抱怨。

  • 一个后台任务崩溃导致网页服务器宕机 :如果没有隔离,非面向用户的工作也会影响用户体验。

  • 你不能承受哪怕短暂的停机 :你的产品已变得如此关键,甚至连维护窗口也变得不可接受。

在某个时刻,服务器开始因承担一切任务而吃不消。这时就是进行第一次架构拆分的时候。

分享


阶段二:独立数据库(100–1,000 用户)

随着流量增长,你的单台服务器开始吃力。Web 应用和数据库争夺相同的 CPU、内存和磁盘 I/O。一次耗费资源的查询就能使延迟飙升,拖慢所有 API 响应。

第一个扩展步骤很简单: 将数据库与应用服务器分离 

如何将系统从0扩展到超过1000万用户

这种两层架构为你带来若干直接好处:

  • 资源隔离: 应用程序和数据库不再为 CPU/内存相互竞争。各自可以使用分配给它们的全部资源。

  • 独立扩展: 升级数据库(更多内存、更快的存储)而无需触及应用服务器。

  • 更佳安全性: 数据库服务器可以部署在私有网络中,不对互联网暴露。

  • 专用优化: 针对每台服务器的具体工作负载进行调优。应用服务器注重高 CPU,数据库注重高 I/O。

  • 备份简化: 数据库备份不会影响应用性能,因为它们在不同的机器上运行。

托管数据库服务

在这个阶段,大多数团队使用托管数据库,例如 Amazon RDSGoogle Cloud SQLAzure Database 或 Supabase(我在 algomaster.io 上使用 Supabase)。

托管服务通常负责:

  • 自动备份(每日快照、按时间点恢复)

  • 安全补丁和更新

  • 基础监控与警报

  • 可选只读副本(我们稍后会讲到这些)

  • 切换到备用实例以实现故障切换

一旦将工程时间计算在内,自行托管与托管服务之间的成本差异通常很小。托管的 PostgreSQL 实例可能比原始虚拟机高出 $50–$100/月 ,但它每周可以节省数小时的维护时间。这些时间更应该用来交付功能。

自行管理数据库的主要原因有:

  • 在极大规模下的成本优化

  • 托管服务不支持的特定配置

  • 禁止使用托管服务的合规性要求

  • 你正在构建一个数据库产品

对于大多数团队而言,托管服务是正确的选择,直到你的数据库账单增长到每月数千美元 

连接池

在这个阶段常被忽视的一项改进是连接池。每个数据库连接都会消耗资源:

  • 连接状态的内存(在 PostgreSQL 中通常每个连接占用 5–10MB)

  • 应用服务器和数据库服务器上的文件描述符

  • 用于连接管理的 CPU 开销

建立新连接也很昂贵。在 TCP 握手、SSL 协商和数据库认证之间,每个请求可能增加 50–100 毫秒的开销。

像 PgBouncer(用于 PostgreSQL)这样的连接池可保持一小部分数据库连接处于打开状态,并在请求间重用它们。

如何将系统从0扩展到超过1000万用户

当有 1,000 名用户时,可能会有 100 个并发连接请求你的 API。没有连接池时,这就是 100 个消耗资源的数据库连接。使用连接池后,20–30 个实际的数据库连接就可以通过重用连接高效地为那 100 个应用程序连接提供服务。

连接池模式:

  • 会话池 :每个客户端连接对应一个池连接(兼容性最好,效率最低)

  • 交易池 :每次交易后将连接归还到连接池(对大多数应用而言是最佳平衡)

  • 语句池化 :每条语句后归还连接(效率最高,但可能破坏某些功能)

大多数应用与交易池化配合效果最佳,这通常能将连接效率提高 3–5 倍 

网络延迟考量

将数据库拆分到独立主机会引入网络延迟。之前应用与数据库在同一台机器时,“网络”延迟本质上为零(回环接口)。现在每个查询都会增加 0.1–1 毫秒的网络往返时间。

对于大多数应用来说,这可以忽略不计。但如果你的代码每次请求发出数百次数据库查询(这是反模式,但很常见),这些延迟会累积。解决办法不是把它们放回同一台机器,而是优化你的查询模式:

  • 尽量批量查询

  • 使用 JOIN 替代 N+1 查询模式

  • 缓存常用数据

  • 使用连接池以避免重复建立连接的开销

把数据库放到独立服务器上后,你为扩展争取了空间。但你也引入了一个新的单点故障:应用服务器现在成了薄弱环节。当它宕机,或无法跟上需求时会发生什么?


阶段 3:负载均衡 + 横向扩展(1K-10K 用户)

你分离后的架构现在能更好地应对负载,但你也带来了一个新问题:你的单个应用服务器现在是一个 单点故障 。如果它崩溃,整个应用就会宕机。随着流量增长,单台服务器也会吃不消。

下一步是将 多台应用服务器 部署在 负载均衡器 之后。

如何将系统从0扩展到超过1000万用户

负载均衡器位于你的服务器前端,负责将传入请求分配到各台服务器上。如果某台服务器发生故障,负载均衡器会通过健康检查检测到并只将流量路由到健康的服务器。单台服务器宕机时,用户不会感受到停机。

负载均衡器需要决定哪个服务器处理每个请求。常见算法包括: 轮询  加权轮询  最少连接 IP 哈希随机 

大多数团队一开始使用轮询(简单且适用于大多数情况),如果请求处理时间差异较大则会改用最少连接数。

现代负载均衡器在不同层次上运行:

  • 第 4 层(传输层):根据 IP 和端口路由。速度快,但不能检查 HTTP 头。

  • 第 7 层(应用层):根据 HTTP 头、URL、Cookie 路由。更灵活,但开销略高。

对于大多数 Web 应用,第 7 层负载均衡更可取,因为它能够实现:

  • 基于路径的路由(/api/* 到 API 服务器,/static/* 到 CDN)

  • 基于头部的路由(移动端与电脑端使用不同版本)

  • 在负载均衡器处终止 SSL

  • 请求/响应 检查以保证安全

纵向扩展 与 横向扩展

在增加更多服务器之前,你可能会问:为什么不直接换一台更大的服务器?这就是经典的纵向与横向扩展权衡。

纵向扩展 指迁移到更大的服务器。它在早期效果良好,通常不需要修改代码。但最终会遇到两个问题:硬件上限和成本快速增长。

更大的机器价格呈非线性增长,因此 CPU 或内存翻倍可能花费 3–4 倍。而且即便是最大的实例也有上限。

水平扩展 意味着增加更多服务器。起初更难,因为你的应用必须是 无状态 的,这样任何服务器都可以处理任何请求。但它实际上为你提供了无限的容量和内置的冗余。如果一台服务器故障,系统仍然会继续运行。

会话问题

这里是水平扩展变得棘手的地方。如果用户登录后他们的会话存在于 服务器 1 的内存 中,当下一次请求落到 服务器 2 时会发生什么?从应用的角度看,会话丢失了,所以用户看起来像是已登出。

这是有状态服务器问题 ,它是水平扩展的最大障碍。

有两种常见的处理方法:

1. 粘性会话(会话亲和性)

负载均衡器将同一用户的所有请求路由到同一台服务器,通常使用 Cookie 或 IP 哈希。

优点:

  • 无需对应用程序进行更改

  • 适用于任何会话存储

缺点:

  • 如果那台服务器故障,用户会丢失他们的会话

  • 如果有些用户比其他用户更活跃,会导致读档分布不均

  • 限制真正的水平扩展(无法自由在服务器之间移动用户)

  • 新服务器需要时间通过会话“预热”

2. 外部会话存储

将会话数据从应用服务器中移出,放入共享存储,如 Redis 或 Memcached

如何将系统从0扩展到超过1000万用户

现在任何服务器都可以处理任何请求,因为会话数据已集中。这是大多数大规模系统采用的模式。Redis 查找带来的额外延迟(亚毫秒级)与它提供的灵活性相比可以忽略不计。

你现在可以处理更多流量并在服务器故障时幸存。但随着用户基数增长,你会注意到一件事:无论你添加多少应用服务器,它们都在冲击同一个数据库。数据库正成为你的下一个瓶颈。


第 4 阶段:缓存 + 只读副本 + CDN(1 万–10 万用户)

当用户达到 1 万以上时,新的瓶颈出现:你的数据库。每个请求都会击中数据库,随着流量增长,查询延迟上升。能轻松应对 100 QPS(每秒查询)的数据库在 1000 QPS 时就会捉襟见肘。

以读为主的应用尤其受影响(大多数应用如此,读写比通常为10:1或更高)。

本阶段引入三种互补的解决方案: 缓存  只读副本和 CDN。它们合力可以将数据库负载降低 90%或更多。

缓存层

大多数 Web 应用遵循 80/20 规则:80% 的请求访问 20% 的数据。一个被查看 10,000 次的产品页面不需要执行 10,000 次数据库查询。每次页面加载都会出现的用户个人资料也不需要每次都重新从数据库拉取。

缓存将经常访问的数据存放在内存中,实现近乎即时的读取。数据库查询通常耗时 1–100 毫秒,而缓存读取仅需 0.1–1 毫秒。

如何将系统从0扩展到超过1000万用户

最常见的缓存模式是 cache-aside(也称为延迟加载):

  1. 应用程序先检查缓存

  2. 如果数据存在(缓存命中),立即返回

  3. 如果不存在(缓存未命中),查询数据库

  4. 将结果存入缓存以供后续请求使用(带 TTL)

  5. 返回数据

Redis 和 Memcached 是这里的标准选择。Redis 功能更丰富(支持列表、集合、有序集合等数据结构;持久化;发布/订阅;Lua 脚本),而 Memcached 更简单,对于纯键值缓存来说速度略快。

大多数团队选择 Redis,因为额外的功能很有用(用有序集合做排行榜、用列表做队列等),且性能差异可以忽略不计。

缓存什么

并非所有内容都应缓存。适合缓存的对象包括:

如何将系统从0扩展到超过1000万用户

不适合缓存的候选项:

  • 高度个性化的数据(每个用户不同,复用率低)

  • 频繁变化的数据(持续的失效开销)

  • 大体积二进制数据(占用内存但收益不成比例)

  • 会因数据陈旧而引发问题的事务性数据

缓存失效

缓存最难的部分不是添加它,而是保持其准确性。当底层数据发生变化时,缓存的数据就会变陈旧。这正是著名的“计算机科学中的两个困难问题”之一。

常见策略包括:

如何将系统从0扩展到超过1000万用户

大多数系统一开始使用基于 TTL 的过期策略(将缓存设置为 5–60 分钟后过期),并针对那些过期会导致问题的数据添加显式失效。例如:

def update_user_profile(user_id, new_data):
    # Update database
    db.update("users", user_id, new_data)
    # Invalidate cache
    cache.delete(f"user:{user_id}")

下一次读取将无法命中缓存,而是从数据库获取最新数据。

只读副本

即使使用缓存,某些请求仍会落到数据库上,特别是 写入 和 缓存未命中 。只读副本通过在多个数据库副本之间分配读取流量来提供帮助。

如何将系统从0扩展到超过1000万用户

主数据库处理所有写入。变更随后被复制(通常是异步地)到一个或多个只读副本 。您的应用将读取查询发送到副本,并将写入工作负载保留在主库上,从而减少争用并提高整体吞吐量。

复制延迟

一个重要的考虑因素是复制延迟 。由于复制常为异步(以提高性能),副本可能会落后主库毫秒到数秒不等。

对于大多数应用来说,这是可以接受的。如果社交媒体的动态滞后一秒,大多数用户不会注意到。但有些流程需要更强的一致性。

一种常见的故障模式是读写一致性(read-your-writes consistency)

用户更新了他们的个人资料并立即刷新。如果该次读取落在尚未追上的副本上,他们会看到旧数据并认为更新失败。

解决方案:

  1. 写入后从主库读取 :在写入后的短时间窗口(N 秒)内,将该用户的读取请求路由到主库。

  2. 会话级一致性 :跟踪用户的最后写入时间戳,仅从已追赶到该时间点之后的副本读取。

  3. 限制级从主库读取 :对于关键读取(查看刚更新的数据),始终命中主库。

大多数框架内置了读写分离支持。例如,Rails(ActiveRecord)、Django 和 Hibernate 可以自动将读取路由到副本、将写入路由到主库。

内容分发网络(CDN)

静态资源如图片、CSS、JavaScript 和视频很少改变,根本不需要访问你的应用服务器。它们也是你提供的最大文件,如果直接提供,将在带宽和计算方面代价高昂。

CDN 通过在称为 边缘节点 (或接入点)的全球分布式服务器上缓存静态资源来解决这个问题。

如何将系统从0扩展到超过1000万用户

下面是当东京的用户请求一张图片时会发生的情况:

  • 请求被路由到 位于东京的 CDN 边缘 (低延迟,大约 ~50 毫秒往返)。

  • 如果文件已经被缓存( 缓存命中 ),CDN 会立即提供该文件。

  • 如果未被缓存( 缓存未命中 ),CDN 会从你的源站 (可能位于美国,约 300 毫秒)获取该文件,在边缘存储一份副本,然后将其返回给用户 。

  • 下一个位于东京的用户将从边缘以约 50 毫秒的延迟获取缓存版本。

常见的 CDN 包括 Cloudflare(免费套餐优秀)、AWS CloudFrontFastly 和 Akamai

通过缓存、只读副本和 CDN,系统可以应对稳步增长。接下来的挑战是 突发流量 。一篇病毒式传播的帖子、一场营销活动,甚至凌晨 3 点与下午 3 点的差别,都可能带来 10 倍的流量波动。到那时,手动调整容量已无法奏效。


第5阶段:自动扩展 + 无状态设计(10万–50万用户)

在超过10万用户时,流量模式变得更难预测。你可能会遇到:

  • 每日峰值(美国早晨、欧洲傍晚)

  • 每周规律(B2B 在工作日更高,面向消费者的则在周末更高)

  • 营销活动流量激增(数小时内流量提升10倍)

  • 病毒式传播时刻(流量提升100倍,持续时间不可预测)

到此为止,手动增减服务器已不再可行。你需要能够自动响应的基础设施。

此阶段重点在于自动扩容 (自动调整容量),并确保你的应用真正无状态 (服务器可随意增减而不会导致数据丢失或影响用户)。

无状态架构

为了使自动扩展起作用,你的应用服务器必须是可互换的。任何请求都可以发送到任意服务器。任何服务器都可以被终止而不会丢失数据。新服务器可以立即开始处理请求。

如何将系统从0扩展到超过1000万用户

当新服务器加入集群时,通常会:

  1. 启动应用程序

  2. 在负载均衡器上注册(或被发现)

  3. 连接到 Redis、数据库和其他共享服务

  4. 立即开始处理请求

当一台服务器被移除时:

  1. 负载均衡器停止发送新请求

  2. 正在处理的请求完成(平滑关闭)

  3. 服务器终止

没有数据丢失,因为本地没有存储任何重要内容。

自动伸缩策略

自动伸缩根据指标调整容量。伸缩系统持续监控指标,并根据阈值添加或移除服务器。

大多数团队从基于 CPU 的伸缩开始。它简单、适用于大多数工作负载且便于理解。对于后台作业工作者,应增加队列深度伸缩。

伸缩参数

配置自动扩缩时,您需要设置以下参数:

Minimum instances: 2       # Always running, even at zero traffic
Maximum instances: 20      # Cost ceiling and resource limit
Scale-up threshold: 70%    # CPU percentage to trigger scale-up
Scale-down threshold: 30%  # CPU percentage to trigger scale-down
Scale-up cooldown: 3 min   # Wait time after scaling up before next action
Scale-down cooldown: 10 min # Wait time after scaling down
Instance warmup: 2 min     # Time for new instance to become fully operational

重要注意事项:

  • 最小实例数 :应至少为 2 以保证冗余。如果一个实例故障,另一个可以处理流量,同时替换实例启动。

  • 冷却期 :防止剧烈摆动(快速上下扩缩)。通常缩容的冷却期更长,因为移除容量比增加容量风险更大。

  • 实例预热 :新服务器需要时间启动、加载代码、预热缓存并建立数据库连接。在它们准备就绪之前,不要将其计入容量。

  • 非对称扩展 :扩容要果断(对负载快速反应),缩容要谨慎(不要过早移除容量)。

无状态认证的 JWT

在这个规模下,许多团队从基于会话的认证转向基于代币的认证,使用 JWT(JSON Web Tokens)。在基于会话的认证中,每个请求都需要查会话存储。使用 JWT 时,认证状态包含在代币本身中。

JWT 有三部分:

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0NTZ9.signature_here

负载包含诸如用户 ID、角色和过期时间等声明。签名确保代币未被篡改。任何服务器都可以使用共享密钥验证签名,而无需查询数据库。

使用 JWT 的权衡:

  • 优点 :真正无状态,无需为每个请求查找会话存储

  • 优点 :可跨服务使用(微服务、移动应用、第三方 API)

  • 缺点 :无法在到期前使单个代币失效(用户登出,但代币仍然有效)

  • 缺点 :代币大小会增加每次请求的负担(500 字节对比 32 字节的会话 ID)

一种常见做法是 短期访问代币 (例如 15 分钟)加上 长期刷新代币 (例如 7 天)。这样可以限制被泄露或过期代币的可用时间。

此时,你的应用层可以弹性扩展。流量激增时会启动更多服务器,流量下降时则关闭。

但新的瓶颈即将出现:数据库的写入能力有限,单体应用变得难以安全改动,有些操作同步执行太慢。这时你需要引入重型机械。


第6阶段:分片 + 微服务 + 消息队列(50万–100万用户)

当用户超过50万时,你会遇到之前的优化无法解决的新上限:

  • 写入会压垮单个主数据库,即便读取被卸载到副本也如此。

  • 巨石变得难以部署。对通知做一个小改动就迫使整个应用程序全面重新部署。

  • 以前很快的操作开始需要数秒,因为在请求路径中同步发生了太多工作。

  • 产品的不同部分需要不同的扩展配置。搜索和信息流可能需要个人资料页面10倍的容量。

这就是重型机制派上用场的地方: 数据库分片  微服务异步处理 (消息队列)。

数据库分片

只读副本解决了读取扩展的问题,但所有写操作仍然集中到一个主数据库。在高负载下,这台主库就会成为瓶颈。你的限制来自单台机器在以下方面的承载能力:

  • 写入吞吐量(插入、更新、删除)

  • 存储容量(即使是大容量磁盘也有上限)

  • 连接数(即使使用连接池)

分片将你的数据根据分片键分散到多个数据库。每个分片保存数据的一个子集,并负责该子集的读写操作。

如何将系统从0扩展到超过1000万用户

分片策略

如何将系统从0扩展到超过1000万用户

一致性哈希是对简单基于哈希分片的常见改进。与其使用 hash(key) % num_shards,你将键放在一个环上。当你添加一个新分片时,只有与其位置相邻的键会移动,而不是所有键。这意味着添加第四个分片会迁移约 25% 的数据,而不是约 75%。

何时进行分片

分片是一个单向之门 。一旦你进行分片:

  • 跨分片查询会变得昂贵或不可能(在分片之间连接数据)

  • 跨分片的事务变得复杂(要么使用两阶段提交,要么放弃原子性)

  • 必须将架构更改应用到所有分片

  • 操作(备份、迁移)按分片数量成倍增加

  • 应用程序代码变得更复杂(分片路由逻辑)

在进行分片之前,先穷尽以下选项:

  1. 优化查询 :添加缺失的索引,重写慢查询,在有帮助的地方进行反规范化

  2. 纵向扩展 :升级到更大的数据库服务器(更多 CPU、内存、更快的 SSD)

  3. 只读副本 :如果以读为主,添加副本来处理读取请求

  4. 缓存 :通过缓存经常访问的数据来减少数据库负载

  5. 归档 :将旧数据移至冷存储(独立数据库、对象存储)

  6. 连接池 :减少连接开销

只有在你确实受写入瓶颈限制且单台节点在物理上无法承受你的吞吐量,或当数据集超出单机可容纳范围时才进行分片。

微服务

随着产品和团队的增长,单体架构变得越来越难以安全演进。表明你可能需要微服务的一些常见信号:

  • 对某一部分(如通知)的更改需要重新部署整个应用。

  • 各团队无法独立交付,必须在每次发布时协调。

  • 应用的不同部分有不同的扩展需求(搜索需要10台服务器,个人资料查看需要2台)。

  • 工程师经常在同一个代码库中产生冲突。

  • 一个子系统的一个 bug 可能导致整个应用崩溃。

微服务将应用拆分为通过网络通信的独立服务。

如何将系统从0扩展到超过1000万用户

每个服务:

  • 拥有自己的数据 (只有它可以直接写入的数据库)

  • 独立部署 (发送配对通知而不触及结账)

  • 独立扩展 (搜索可以与个人资料分开扩展)

  • 使用适合用途的技术 (搜索可能使用 Elasticsearch,支付可能需要具有强一致性的 Postgres)

  • 揭示清晰的 API 合约 (其他服务通过稳定的端点集成)

代价是运维复杂度大幅上升。最稳妥的方法是从一次拆分开始:选择边界最清晰、独立伸缩需求最明确的服务。避免一开始就拆成数十个服务。

消息队列与异步处理

并非所有事情都需要在请求路径中同步完成。当用户下单时,有些步骤必须立即完成,而另一些可以在后台进行。

必须同步:

  • 验证支付方法

  • 检查库存

  • 创建订单记录

  • 退货 订单 确认

可以是异步的:

  • 发送确认电子邮件

  • 更新分析仪表板

  • 通知仓库进行履约

  • 更新推荐引擎

  • 同步至会计系统

消息队列如 KafkaRabbitMQ 或 SQS 将生产者与消费者解耦。订单服务发布诸如 OrderPlaced 的事件,下游系统独立消费该事件。

如何将系统从0扩展到超过1000万用户

异步处理的好处:

  • 弹性 :如果电子邮件服务宕机,消息会排队。订单仍然完成。服务恢复后再发送电子邮件。

  • 可扩展性 :消费者可根据队列深度独立扩展。假日高峰?可以增加更多仓库通知处理器,而无需触及订单服务。

  • 解耦 :订单服务不需要知道谁在消费事件。可以在不更改生产者的情况下添加新的消费者(欺诈检测、CRM 同步)。

  • 平滑突发流量 :队列吸收流量峰值,使下游系统以可持续的速率处理请求,而不会超载。

  • 重试处理 :失败的消息可以自动重试。死信队列捕获反复失败的消息以便调查。

一种常见的现实模式是“现在写入,繁重工作稍后处理”。

例如,在社交应用中,创建帖子通常是一次快速写入并立即返回成功响应。诸如分发、省索引、通知和动态更新等耗时工作是异步进行的,这就是为什么你有时会看到点赞数或动态传播存在小幅延迟。

此时,你的架构能够在单一区域内应对海量规模。但你的用户并非都集中在同一地点,你的基础设施也不应如此。

一旦用户分布在各大洲,延迟就会显著变得可感知,单一数据中心也会成为全球用户群的单点故障。


第7阶段:多区域 + 高级模式(100万–1000万+ 用户)

当全球拥有数百万用户时,会出现新的挑战:

  • 澳大利亚的用户访问位于美国的服务器时会遇到300毫秒的延迟。

  • 数据中心故障(火灾、网络分区、云服务商问题)导致你的整个服务宕机

  • 你的数据库模式无法高效同时支撑写密集的实时更新和读密集的分析仪表盘

  • 不同地区有不同的数据驻留要求(欧盟的 GDPR、数据本地化法律)

本阶段涵盖多区域部署  高级缓存以及像 CQRS 这样的专用模式 

多区域架构

部署到多个地理区域实现两个主要目标:

  1. 更低延迟 :用户连接到就近服务器。东京用户访问东京服务器(20ms),而不是美国服务器(200ms)。

  2. 灾难恢复 :如果某一地区发生故障,其他地区继续提供流量。真正的高可用性。

如何将系统从0扩展到超过1000万用户

主要有两种方法:

主动-被动(主-从)

一个区域(主)处理所有写入。其他区域提供读取服务,并在主区域故障时接管。

优点:

  • 更易实现

  • 无需写入冲突解决

  • 写入强一致性

缺点:

  • 距离主节点较远的用户写入延迟更高

  • 故障切换不是瞬间完成(DNS 传播、复制副本升变)

  • 主区域仍然是单点故障

主动-主动

所有区域既处理读取也处理写入。这就要求解决一个困难问题:当美国和欧盟的用户同时更新同一记录时会发生什么?

优点:

  • 所有操作的最低可能延迟

  • 真正的高可用性,任何区域故障都不会产生影响

  • 没有单点故障

缺点:

  • 冲突解决复杂(处理不当可能导致数据问题)

  • 最终一致性,不适用于所有类型的数据

  • 更复杂,难以推理和调试

大多数公司从主备(active-passive)开始。主主(active-active)需要解决分布式共识问题并接受最终一致性。

全球规模下的 CAP 定理

在全球规模下,CAP 定理变得非常现实。它指出分布式系统只能同时提供以下三项保证中的两项:

  • 一致性 :每次读取都能得到最近的写入

  • 可用性 :每个请求都会得到响应(不是错误)

  • 分区容错性 :即使发生网络分区,系统仍能继续运行

由于跨区域的网络分区不可避免(海底光缆会被切断,云服务提供商会发生故障),在发生分区时你实际上是在一致性与可用性之间做出选择。

大多数全球系统在大多数操作上选择最终一致性 

  • 用户的帖子可能需要 1-2 秒才能在其他区域的关注者处出现

  • 产品评分在不同区域可能短暂显示略有不同的平均值

  • 用户个人资料更新可能需要一段时间才能传播

只有在不一致会导致实际问题(付款、库存扣减、金融交易)的操作才需要强一致性,这类操作可能会路由到主区域。

CQRS 模式

随着系统增长,读写模式显著分化:

  • 写操作需要事务、校验、规范化数据、审计日志

  • 读取需要非规范化数据、快速聚合、全文搜索

  • 写入量可能只有读取量的 1/100

CQRS (命令-查询职责分离) 完全将这些关注点分开。

如何将系统从0扩展到超过1000万用户

写入端使用为数据完整性和事务保证优化的规范化模式。读取端使用为查询性能优化的非规范化视图。事件用于同步两者。

现实案例:Twitter 的时间线架构。

  • 写入路径 :当你发推时,会将其写入一个规范化的推文表,具备适当的索引、约束和事务。

  • 事件 :触发“推文已创建”事件。

  • 投影 :一个分发服务读取该事件,并将推文添加到每个关注者的时间线(一个反规范化的、按用户划分的数据结构,针对“展示我的动态”查询进行优化)。

  • 读取路径 :当你打开 Twitter 时,你从预先计算的时间线读取,而不是运行一个将推文、关注和用户关联起来的复杂查询。

CQRS 增加了复杂性,但带来以下能力:

  • 读写路径的独立扩展

  • 针对每种访问模式优化的架构 schema

  • 不同的技术选择(用于写入的 PostgreSQL、用于读取的 Elasticsearch)

  • 两种操作的性能更佳

高级缓存模式

在全球范围内,缓存变得更复杂:

多层缓存

如何将系统从0扩展到超过1000万用户
如何将系统从0扩展到超过1000万用户

缓存预热

当新的缓存服务器启动(或维护后缓存过期),最初的请求会遇到缓存未命中,导致延迟激增并增加源站负载。缓存预热是在流量到来之前预先填充缓存:

  • 部署时 :在启动期间将热门项加载到缓存中, 在接收流量之前

  • 在活动之前 :在营销推广前,预热可能会被访问的产品/页面缓存

  • 缓存复制 :添加新的缓存节点时,从现有节点复制状态

Netflix 在高峰时段前会使用热门内容预热边缘缓存。到晚间观影开始时,观看量最高的节目已被缓存到边缘节点。

写回缓存(Write-Behind/Write-Back)

对于写入密集型工作负载,先写入缓存并异步持久化到数据库:

  1. 写入到缓存(立即返回给用户)

  2. 缓存确认写入

  3. 后台进程定期将写入刷新到数据库

这会大幅降低写入延迟,但也带来风险:如果在刷新之前缓存失败,写入会丢失。仅在以下情况下使用:

  • 可以接受部分数据丢失(分析计数器、浏览量统计)

  • 缓存高度可用(例如带复制与持久化的 Redis)

  • 可为性能牺牲持久性

你现在已经构建了一个全球分布的系统,能够以低延迟为数百万用户提供服务。但旅程并未结束。在真正的超大规模下,即便是最成熟的现成解决方案也会显露出局限性。


超过1000万用户

当用户数达到一千万及以上时,你进入了现成解决方案并不总是可行的领域。处于这个规模的公司通常会构建针对其特定访问模式定制的基础设施。问题会变得与您的工作负载独一无二。

专用数据存储

没有任何单一数据库能很好地处理所有访问模式。“多元持久化”(polyglot persistence)的概念意味着针对不同用例使用不同的数据库:

如何将系统从0扩展到超过1000万用户
如何将系统从0扩展到超过1000万用户

每种数据库都针对特定的访问模式进行了优化。用 PostgreSQL 存储时间序列数据虽然可行,但效率低下。用 Elasticsearch 处理事务也是可行的,但很危险。

大规模定制解决方案

在极大规模下,一些公司会构建定制基础设施,因为它们的需求超出了通用系统能提供的范围:

  • Facebook 的 TAO: 一个为社交图设计的定制数据系统,旨在在现有现成选项无法满足时,满足 Facebook 在巨量规模下的延迟和吞吐需求。

  • Google Spanner: 一种全球分布的 SQL 数据库,旨在跨区域提供强一致性,结合了当时很难同时具备的特性。

  • Netflix 的 EVCache: 基于 Memcached 构建的大规模缓存层,增加了复制、可靠性和运维工具,以支持 Netflix 的流量模式。

  • Discord 的存储演进: MongoDB(2015)→ Cassandra(2017)→ ScyllaDB(2022)。每次迁移都是由前一个选择的限制驱动,Discord 对这些迁移背后的权衡有详细的说明文档。

  • Uber 的 Schemaless: 基于 MySQL 的存储层,旨在在超越单一 MySQL 部署的同时保持事务语义,并为团队提供运维上的简便性。

这些并不是你一开始就会选择的方案,但它们说明了扩展是一个持续的过程,而不是终点。适用于一百万用户的架构很少会是你在一亿用户时仍然想要的架构。

边缘计算

下一个前沿是将计算推向更靠近用户的地方。与其把所有逻辑都放在集中式数据中心,不如在全球 CDN 边缘位置运行代码:

  • Cloudflare Workers:在 250+ 个边缘节点运行 JavaScript/WASM

  • AWS Lambda@Edge:CloudFront 边缘的 Lambda 函数

  • Fastly Compute@Edge:运行在 Fastly 边缘网络的计算平台

  • Deno Deploy:全球分布的 JavaScript 运行时

边缘计算代表了一次根本性转变:许多请求不再是“请求 → CDN → 源站 → CDN → 响应”,而是“请求 → 边缘 → 响应”,因为边缘具备足够的计算能力来处理业务逻辑。

现在我们已经讲完了从单台服务器到全球级基础设施的完整演进,一个重要问题仍然存在:如何判断何时采取每一步?过早扩展会浪费资源;过迟扩展会导致宕机。


摘要

将系统从零扩展到数百万用户遵循可预测的进程。每个阶段都会在特定阈值出现时解决相应的问题:

如何将系统从0扩展到超过1000万用户

需牢记的关键原则

  1. 从简单开始 :不要为尚不存在的问题做优化。单台服务器在合适的时候完全足够。

  2. 先度量 :在增加基础设施之前先确认真正的瓶颈。CPU 限制的问题需要与 I/O 限制的问题不同的解决方案。

  3. 无状态服务器是前提 :在服务器不保存本地状态之前,不能进行水平扩展或自动扩展。

  4. 积极使用缓存 :大多数数据的读取远比写入频繁。对于读密集型工作负载,缓存能带来 10–100 倍的性能提升。

  5. 尽可能异步 :并非所有操作都必须在请求路径中完成。电子邮件发送、分析、通知都可以排队处理。

  6. 慎用分片 :数据库分片是一条单向门,带来显著复杂性。先穷尽其他选项。

  7. 接受权衡 :在网络分区时,完美的一致性和可用性无法共存。要明确哪些操作真正需要强一致性。

  8. 复杂性有代价 :你添加的每个组件都是一个可能出错的点,需要监控,并需要专业技能来运维。

扩展路径不是一次性实现所有东西,而是理解每个阶段会出现的问题,并在合适的时机应用合适的解决方案。

最好的架构是满足你当前需求的最简架构,并且当这些需求变化时,有一条清晰的演进路径。

订阅 RecodeX 创投情报 每日融资动态与原创深度报道,直达邮箱

了解 RecodeX 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读