# 如何将系统从 0 扩容到 1000 万+用户
扩容是一个复杂的话题,但在大科技公司处理数百万请求的服务,以及从零开始扩展我自己的创业公司(AlgoMaster.io)之后,我意识到大多数系统在成长过程中都会经历一个惊人相似的阶段。
关键洞察是:**你不应该从一开始就过度设计**。从简单开始,识别瓶颈,然后逐步扩容。
在本文中,我将带你了解**从零到 1000 万用户及以上的 7 个系统扩容阶段**。每个阶段都针对不同增长点出现的特定瓶颈。你将学习添加什么、何时添加、为什么有帮助,以及涉及的权衡。
无论你是在构建应用程序或网站、准备系统设计面试,还是只是对大规模系统如何工作感到好奇,理解这个进程都会加深你对架构的思考方式。
> 注意:本文中的用户范围是大致指南。确切阈值会根据你的产品、工作负载和流量模式而有所不同。
—
## 阶段 1:单台服务器(0-100 用户)
当你刚刚开始时,你的首要任务很简单:**发布产品并验证你的想法**。在这个阶段过早优化会在你可能永远不会遇到的问题上浪费时间和金钱。
最简单的架构是将所有内容放在**单台服务器**上:你的 Web 应用程序、数据库和任何后台作业都在同一台机器上运行。

这就是 Instagram 的起步方式。当 Kevin Systrom 和 Mike Krieger 在 2010 年推出第一个版本时,第一天就有 25,000 人注册。
他们没有预先过度设计。通过一个小团队和简单的设置,他们根据实际需求进行扩展,随着使用量的增长增加容量,而不是为假设的未来流量而构建。
### 这种架构的样子
在实践中,单服务器设置意味着:
– 一个 Web 框架(Django、Rails、Express、Spring Boot)处理 HTTP 请求
– 一个数据库(PostgreSQL、MySQL)存储你的数据
– 后台作业处理(Sidekiq、Celery)用于异步任务
– 可能还有一个反向代理(Nginx)用于 SSL 终止
所有这些都运行在一台虚拟机上。你的云提供商账单可能是一个基本 VPS(DigitalOcean Droplet、AWS Lightsail、Linode)每月 20-50 美元。
### 为什么这对早期阶段有效
在这个阶段,简单是你最大的优势:
– **快速部署**:一台服务器意味着一个部署、监控和调试的地方。
– **低成本**:一台每月 20-50 美元的虚拟专用服务器(VPS)可以轻松处理你的前 100 个用户。
– **更快的迭代**:没有分布式系统的复杂性来减缓开发速度。
– **更简单的调试**:所有日志都在一个地方,组件之间没有网络问题。
– **全栈可见性**:你可以跟踪每个请求的端到端过程,因为只有一个执行路径。
### 你正在做出的权衡
这种简单性伴随着你有意接受的权衡:

### 何时进入下一阶段
当你注意到这些迹象时,你就会知道是时候进化了:
– **高峰流量期间数据库查询变慢**:应用程序和数据库竞争相同的 CPU 和内存。一个沉重的查询会拖慢所有人的 API 延迟。
– **服务器 CPU 或内存持续超过 70-80%**:你正在接近单台机器可以可靠处理的极限。
– **部署需要重启并导致停机**:即使是短暂的中断也变得明显,用户开始抱怨。
– **后台作业崩溃导致 Web 服务器宕机**:没有隔离,非面向用户的工作会影响用户体验。
– **你无法承受即使是短暂的停机**:你的产品变得足够关键,以至于维护窗口也不再可接受。
在某些时候,服务器开始难以承受做所有事情的重负。那时是你进行第一次架构拆分的时候了。
—
## 阶段 2:分离数据库(100-1K 用户)
随着流量的增长,你的单台服务器开始挣扎。Web 应用程序和数据库竞争相同的 CPU、内存和磁盘 I/O。一个沉重的查询会使延迟激增并减慢每个 API 响应。
第一个扩容步骤很简单:**将数据库与应用服务器分离**。
这种两层架构给你几个直接的好处:
– **资源隔离**:应用程序和数据库不再竞争 CPU/内存。每个都可以使用其分配资源的 100%。
– **独立扩展**:升级数据库(更多 RAM、更快的存储)而不触及应用服务器。
– **更好的安全性**:数据库服务器可以位于私有网络中,不暴露给互联网。
– **专业化优化**:针对特定工作负载优化每台服务器。应用服务器需要高 CPU,数据库需要高 I/O。
– **备份简单性**:数据库备份不会影响应用程序性能,因为它们在不同的机器上运行。
### 托管数据库服务
在这个阶段,大多数团队使用托管数据库,如 **Amazon RDS**、**Google Cloud SQL**、**Azure Database** 或 **Supabase**(我在 **algomaster.io** 使用 Supabase)。
托管服务通常处理:
– 自动备份(每日快照、时间点恢复)
– 安全补丁和更新
– 基本监控和警报
– 可选的只读副本(我们稍后会介绍)
– 故障转移到备用实例
一旦考虑工程时间,自托管和托管之间的成本差异通常很小。一个托管的 PostgreSQL 实例可能比原始虚拟机每月多花费 **50-100 美元**,但它可以每周节省数小时的维护时间。这些时间最好用于发布功能。
自管理数据库的主要原因包括:
– 非常大规模的成本优化
– 托管服务不支持的特殊配置
– 禁止托管服务的合规性要求
– 你正在构建一个数据库产品
对于大多数团队,托管服务是正确的选择,直到你的数据库账单增长到每月 **数千美元**。
### 连接池
在这个阶段,一个经常被忽视的改进是连接池。每个数据库连接都消耗资源:
– 连接状态的内存(PostgreSQL 中每个连接通常 5-10MB)
– 应用程序和数据库服务器上的文件描述符
– 连接管理的 CPU 开销
打开新连接也很昂贵。在 TCP 握手、SSL 协商和数据库身份验证之间,你可以为每个请求增加 **50-100 毫秒**的开销。
像 **PgBouncer**(用于 PostgreSQL)这样的连接池器保持一小部分打开的数据库连接并在请求之间重用它们。
对于 1,000 个用户,你可能有 100 个并发连接访问你的 API。没有池化,那就是 100 个数据库连接消耗资源。有了池化,20-30 个实际数据库连接可以通过连接重用高效地服务这 100 个应用程序连接。
**连接池模式:**
– **会话池化**:每个客户端连接一个池连接(最兼容,效率最低)
– **事务池化**:每个事务后连接返回到池(大多数应用程序的最佳平衡)
– **语句池化**:每个语句后连接返回(最有效,但可能会破坏功能)
大多数应用程序使用**事务池化**效果最好,这通常可以将连接效率提高 **3-5 倍**。
### 网络延迟考虑因素
分离数据库引入了网络延迟。当应用程序和数据库在同一台机器上时,”网络”延迟基本为零(环回接口)。现在每个查询增加 0.1-1ms 的网络往返时间。
对于大多数应用程序,这是可以忽略不计的。但如果你的代码每个请求进行数百个数据库查询(一种反模式,但很常见),这种延迟会累积。解决方案不是把它们放回同一台机器,而是优化你的查询模式:
– 尽可能批量查询
– 使用 JOINs 而不是 N+1 查询模式
– 缓存频繁访问的数据
– 使用连接池以避免重复的连接设置开销
通过数据库在自己的服务器上,你为自己赢得了成长空间。但你也创建了一个新的单点故障:应用服务器现在是薄弱环节。当它宕机时,或者当它根本无法跟上需求时会发生什么?
—
## 阶段 3:负载均衡器 + 水平扩展(1K-10K 用户)
你分离的架构现在可以更好地处理负载,但你引入了一个新问题:你的单个应用服务器现在是一个**单点故障**。如果它崩溃,你的整个应用程序就会宕机。随着流量的增长,那台服务器无法跟上。
下一步是在**负载均衡器**后面运行**多个应用服务器**。
负载均衡器位于你的服务器前面,并将传入的请求分发到它们。如果一台服务器失败,负载均衡器会检测到这一点(通过健康检查)并仅将流量路由到健康的服务器。当单台服务器失败时,用户不会遇到停机。
负载均衡器需要决定哪台服务器处理每个请求。常见算法包括:**轮询**、**加权轮询**、**最少连接**、**IP 哈希**和**随机**。
大多数团队从轮询开始(简单,适用于大多数情况),如果他们的请求具有不同的处理时间,则切换到最少连接。
现代负载均衡器在不同的层运行:
– **第 4 层(传输)**:基于 IP 和端口路由。快速,但无法检查 HTTP 标头。
– **第 7 层(应用)**:基于 HTTP 标头、URL、cookies 路由。更灵活,开销稍大。
对于大多数 Web 应用程序,第 7 层负载均衡更可取,因为它支持:
– 基于路径的路由(`/api/*` 到 API 服务器,`/static/*` 到 CDN)
– 基于标头的路由(移动版与桌面版的不同版本)
– 负载均衡器处的 SSL 终止
– 安全的请求/响应检查
### 垂直扩展 vs 水平扩展
在添加更多服务器之前,你可能会问:为什么不直接获取更大的服务器?这是经典的垂直扩展与水平扩展权衡。
**垂直扩展**意味着转移到更大的服务器。它在早期效果很好,通常不需要代码更改。但你最终会遇到两个问题:硬硬件限制和快速增加的成本。
更大的机器是非线性定价的,因此双倍 CPU 或内存的成本可能高出 3-4 倍。即使是最大的实例也有上限。
**水平扩展**意味着添加更多服务器。起初它更难,因为你的应用程序必须是**无状态**的,所以任何服务器都可以处理任何请求。但它给你有效的无限容量和内置冗余。如果一台服务器失败,系统继续运行。
### 会话问题
这就是水平扩展变得棘手的地方。如果用户登录并且他们的会话位于**服务器 1 的内存**中,当下一个请求落在**服务器 2** 上时会发生什么?从应用程序的角度来看,会话丢失,所以用户看起来已注销。
这是**有状态服务器问题**,它是水平扩展的最大障碍。
有两种常见的方法来处理它:
#### 1. 粘性会话(会话亲和性)
负载均衡器将来自同一用户的所有请求路由到同一台服务器,通常使用 cookie 或 IP 哈希。
**优点:**
– 不需要应用程序更改
– 适用于任何会话存储
**缺点:**
– 如果该服务器失败,用户将丢失其会话
– 如果某些用户比其他用户更活跃,负载分布不均
– 限制真正的水平扩展(不能自由地在服务器之间移动用户)
– 新服务器需要时间来”预热”会话
#### 2. 外部会话存储
将会话数据从应用服务器移动到共享存储中,如 **Redis** 或 **Memcached**。
现在任何服务器都可以处理任何请求,因为会话数据是集中的。这是大多数大规模系统使用的模式。Redis 查找的额外延迟(亚毫秒级)与其提供的灵活性相比可以忽略不计。
你现在可以处理更多流量并在服务器故障中幸存。但随着你的用户群增长,你会注意到一些事情:无论你添加多少应用服务器,它们都在冲击同一个数据库。数据库正在成为你的下一个瓶颈。
—
## 阶段 4:缓存 + 只读副本 + CDN(10K-100K 用户)
对于 10,000+ 用户,一个新的瓶颈出现:你的数据库。每个请求都访问数据库,随着流量的增长,查询延迟增加。处理 100 QPS(每秒查询)的数据库在 1,000 QPS 时开始挣扎。
读密集型应用程序(大多数是,读与写比例为 10:1 或更高)受苦尤其严重。
这个阶段引入了三个互补的解决方案:**缓存**、**只读副本**和 **CDN**。它们可以将数据库负载减少 90% 或更多。
### 缓存层
大多数 Web 应用程序遵循 80/20 规则:80% 的请求访问 20% 的数据。一个查看 10,000 次的产品页面不需要 10,000 次数据库查询。每次页面查看时加载的用户个人资料不需要每次都重新获取。
缓存将频繁访问的数据存储在内存中以进行近乎即时的检索。虽然数据库查询需要 1-100ms,但缓存读取需要 0.1-1ms。
最常见的缓存模式是**缓存旁路**(也称为延迟加载):
– 应用程序首先检查缓存
– 如果数据存在(缓存命中),立即返回
– 如果不存在(缓存未命中),查询数据库
– 将结果存储在缓存中以供将来请求(带 TTL)
– 返回数据
Redis 和 Memcached 是此处的标准选择。Redis 功能更丰富(支持列表、集合、有序集合等数据结构;持久性;发布/订阅;Lua 脚本),而 Memcached 对于纯键值缓存更简单且稍快。
大多数团队选择 Redis,因为附加功能很有用(使用有序集合进行排行榜,使用列表进行队列等),并且性能差异可以忽略不计。
#### 缓存什么
并非所有内容都应该缓存。好的缓存候选者包括:
**糟糕的缓存候选者:**
– 高度个性化的数据(每个用户都不同,重用率低)
– 频繁变化的数据(持续的失效开销)
– 大型块(消耗内存而没有成比例的好处)
– 陈旧性会导致问题的交易数据
#### 缓存失效
缓存最难的部分不是添加它,而是保持其准确性。当底层数据发生变化时,缓存的数据会变得陈旧。这是著名的”计算机科学中的两个难题之一”。
**常见策略包括:**
大多数系统从基于 TTL 的过期开始(设置缓存在 5-60 分钟后过期)并为陈旧性会导致问题的数据添加显式失效。例如:
“`python
def update_user_profile(user_id, new_data):
# 更新数据库
db.update(“users”, user_id, new_data)
# 使缓存失效
cache.delete(f”user:{user_id}”)
“`
下一次读取将错过缓存并从数据库中获取新数据。
### 只读副本
即使有缓存,一些请求仍然会访问数据库,特别是**写入**和**缓存未命中**。只读副本通过在数据库的多个副本之间分发读流量来帮助。

主数据库处理所有写入。然后更改被复制(通常是异步的)到一个或多个**只读副本**。你的应用程序将读查询发送到副本,并将写工作负载保留在主数据库上,这减少了争用并提高了整体吞吐量。
#### 复制延迟
一个重要的考虑因素是**复制延迟**。由于复制通常是异步的(为了性能),副本可能比主数据库落后几毫秒到几秒。
对于大多数应用程序,这是可以接受的。如果社交媒体源落后一秒,大多数用户不会注意到。但某些流程需要更强的一致性。
一种常见的失败模式是**读你写一致性**:
用户更新他们的个人资料并立即刷新。如果该读取落在尚未赶上的副本上,他们会看到旧数据并假设更新失败。
**解决方案:**
– **写入后从主数据库读取**:在写入后的短窗口(N 秒)内,将该用户的读取路由到主数据库。
– **会话级一致性**:跟踪用户的最后写入时间戳,并仅从已经赶上该点的副本读取。
– **显式从主数据库读取**:对于关键读取(查看刚刚更新的数据),始终访问主数据库。
大多数框架都有内置的读/写拆分支持。例如,Rails(ActiveRecord)、Django 和 Hibernate 可以自动将读取路由到副本,将写入路由到主数据库。
### 内容分发网络(CDN)
静态资产(如图像、CSS、JavaScript 和视频)很少更改,根本不需要访问你的应用服务器。它们也是你提供的最大文件,如果你直接提供它们,这在带宽和计算方面都很昂贵。
**CDN** 通过在全球分布的服务器(称为**边缘位置**或接入点)上缓存静态资产来解决这个问题。
以下是东京用户请求图像时发生的情况:
– 请求被路由到东京的 **CDN 边缘**(低延迟,约 50ms 往返)。
– 如果文件已经被缓存(**缓存命中**),CDN 立即提供它。
– 如果未被缓存(**缓存未命中**),CDN 从你的**源**(可能在美国,约 300ms)获取它,在边缘存储副本,然后将其返回给用户。
– 东京的下一个用户从边缘获取缓存版本,再次约 50ms。
流行的 CDN 包括 **Cloudflare**(强大的免费层)、**AWS CloudFront**、**Fastly** 和 **Akamai**。
有了缓存、只读副本和 CDN,你的系统可以处理稳定的增长。下一个挑战是**峰值流量**。病毒式帖子、营销活动,甚至是凌晨 3 点和下午 3 点之间的差异都可能产生 10 倍的流量变化。在那时,手动调整容量不再起作用。
—
## 阶段 5:自动扩展 + 无状态设计(100K-500K 用户)
对于 100K+ 用户,流量模式变得不那么可预测。你可能有:
– 每日峰值(美国早晨,欧盟晚上)
– 每周模式(B2B 工作日更高,消费者周末更高)
– 营销活动峰值(数小时 10 倍流量)
– 病毒式时刻(100 倍流量,持续时间不可预测)
在这一点上,手动添加和删除服务器不再可行。你需要自动响应的基础设施。
这个阶段专注于**自动扩展**(自动调整容量)并确保你的应用程序真正**无状态**(服务器可以自由添加或删除,而不会丢失数据或影响用户)。
### 无状态架构
为了使自动扩展工作,你的应用服务器必须可以互换。任何请求都可以去任何服务器。任何服务器都可以终止而不会丢失数据。新服务器可以立即开始处理请求。
当新服务器加入集群时,它通常:
– 启动应用程序
– 向负载均衡器注册(或被发现)
– 连接到 Redis、数据库和其他共享服务
– 立即开始处理请求
当服务器被删除时:
– 负载均衡器停止发送新请求
– 进行中的请求完成(优雅关闭)
– 服务器终止
不会丢失数据,因为没有任何重要内容存储在本地。
### 自动扩展策略
自动扩展根据指标调整容量。扩展系统持续监控指标并根据阈值添加或删除服务器。
大多数团队从基于 CPU 的扩展开始。它简单,适用于大多数工作负载,并且很容易推理。为后台作业工作者添加队列深度扩展。
#### 扩展参数
配置自动扩展时,你将设置这些参数:
“`
最小实例:2 # 即使在零流量时也始终运行
最大实例:20 # 成本上限和资源限制
扩展阈值:70% # 触发扩展的 CPU 百分比
收缩阈值:30% # 触发收缩的 CPU 百分比
扩展冷却时间:3 分钟 # 扩展后下次操作前的等待时间
收缩冷却时间:10 分钟 # 收缩后的等待时间
实例预热:2 分钟 # 新实例变为完全运行的时间
“`
**重要考虑因素:**
– **最小实例**:应至少为 2 以实现冗余。如果一个失败,另一个在替代实例启动时处理流量。
– **冷却时间**:防止抖动(快速扩展和收缩)。收缩冷却时间通常更长,因为删除容量比添加容量风险更大。
– **实例预热**:新服务器需要时间启动、加载代码、预热缓存、建立数据库连接。在它们准备好之前不要将它们计入容量。
– **不对称扩展**:激进地扩展(对负载快速反应),保守地收缩(不要过早删除容量)。
### 用于无状态身份验证的 JWT
在这个规模下,许多团队从基于会话的身份验证转移到使用 JWT(JSON Web Tokens)的基于令牌的身份验证。使用基于会话的身份验证,每个请求都需要会话存储查找。使用 JWT,身份验证状态包含在令牌本身中。
JWT 有三个部分:
“`
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0NTZ9.signature_here
“`
有效载荷包含用户 ID、角色和过期等声明。签名确保令牌未被篡改。任何服务器都可以使用共享密钥验证签名,而无需查询数据库。
**JWT 的权衡:**
– **优点**:真正无状态,每个请求不需要会话存储查找
– **优点**:跨服务工作(微服务、移动应用程序、第三方 API)
– **缺点**:无法在过期前使单个令牌失效(用户注销,但令牌仍然有效)
– **缺点**:令牌大小添加到每个请求(500 字节 vs 32 字节会话 ID)
一个常见的模式是**短期访问令牌**(例如,15 分钟)加上**长期刷新令牌**(例如,7 天)。这限制了被盗或陈旧令牌可以使用的时间。
在这一点上,你的应用程序层弹性扩展。流量峰值,更多服务器启动。流量下降,它们关闭。
但是一个新的天花板即将到来:数据库只能处理那么多写入,单体变得更难安全更改,某些操作太慢而无法同步运行。那时你需要重型机械。
—
## 阶段 6:分片 + 微服务 + 消息队列(500K-1M 用户)
对于 500K+ 用户,你将达到以前的优化无法解决的新天花板:
– 写入使单个主数据库不堪重负,即使读取被卸载到副本。
– 单体变得痛苦难以发布。对通知的小改动迫使整个应用程序完全重新部署。
– 以前快速的操作开始需要几秒钟,因为在请求路径中同步发生了太多工作。
– 产品的不同部分需要不同的扩展配置文件。搜索和提要可能需要个人资料页面容量的 10 倍。
这就是重型机械的用武之地:**数据库分片**、**微服务**和**异步处理**(消息队列)。
### 数据库分片
只读副本解决了读取扩展,但所有写入仍然到一个主数据库。在高容量下,这个主数据库成为瓶颈。你在以下方面受到单台机器可以处理的限制:
– 写入吞吐量(插入、更新、删除)
– 存储容量(即使是大型磁盘也有限制)
– 连接计数(即使有池化)
**分片**根据**分片键**将你的数据分割到多个数据库中。每个分片保存数据的一个子集,并处理该子集的读取和写入。

#### 分片策略

**一致性哈希**是对简单基于哈希的分片的流行改进。不是 `hash(key) % num_shards`,而是将键放在环上。当你添加新分片时,只有与其位置相邻的键移动,而不是所有键。这意味着添加第四个分片会移动约 25% 的数据,而不是约 75%。
#### 何时分片
分片是一个**单行门**。一旦你分片:
– 跨分片查询变得昂贵或不可能(连接跨分片的数据)
– 跨分片事务很复杂(两阶段提交或放弃原子性)
– 架构更改必须应用于所有分片
– 操作(备份、迁移)乘以分片数量
– 应用程序代码变得更复杂(分片路由逻辑)
在分片之前,用尽这些选项:
1. **优化查询**:添加缺失的索引,重写慢查询,在有帮助的地方去规范化
2. **垂直扩展**:升级到更大的数据库服务器(更多 CPU、RAM、更快的 SSD)
3. **只读副本**:如果是读密集型,添加副本来处理读取
4. **缓存**:通过缓存频繁访问的数据来减少数据库负载
5. **归档**:将旧数据移动到冷存储(单独的数据库、对象存储)
6. **连接池**:减少连接开销
仅当你真正受写入限制且单个节点在物理上无法处理你的吞吐量,或者当你的数据集超过一台机器所能容纳的内容时才分片。
### 微服务
随着产品和团队的增长,单体变得更难安全进化。你可能受益于微服务的常见信号:
– 对一个区域(如通知)的更改需要重新部署整个应用程序。
– 团队无法在不协调每次发布的情况下独立发布。
– 应用程序的不同部分有不同的扩展需求(搜索需要 10 台服务器,个人资料查看需要 2 台)
– 工程师经常在同一个代码库中发生冲突。
– 一个子系统中的一个错误使整个应用程序宕机。
微服务将应用程序拆分为通过网络通信的独立服务。

每个服务:
– **拥有其数据**(只有它直接写入的数据库)
– **独立部署**(在不触及结账的情况下发布通知)
– **独立扩展**(搜索可以与个人资料分开扩展)
– **使用适合目的的技术**(搜索可能使用 Elasticsearch,支付可能需要具有强一致性的 Postgres)
– **暴露清晰的 API 合同**(其他服务通过稳定端点集成)
权衡是运营复杂性的大幅跳跃。最安全的方法是从**一次提取**开始:选择边界最清晰且独立扩展需求最清晰的服务。避免预先拆分为数十个服务。
### 消息队列和异步处理
并非所有事情都需要在请求路径中同步发生。当用户下订单时,某些步骤必须立即完成,而其他步骤可以在后台进行。
**必须同步:**
– 验证付款方式
– 检查库存
– 创建订单记录
– 返回订单确认
**可以异步:**
– 发送确认电子邮件
– 更新分析仪表板
– 通知仓库进行履行
– 更新推荐引擎
– 同步到会计系统
像 **Kafka**、**RabbitMQ** 或 **SQS** 这样的消息队列将生产者与消费者解耦。订单服务发布一个事件,如 `OrderPlaced`,下游系统独立消费它。

**异步处理的好处:**
– **弹性**:如果电子邮件服务宕机,消息会排队。订单仍然完成。电子邮件在服务恢复时发送。
– **可扩展性**:消费者根据队列深度独立扩展。假期高峰?添加更多仓库通知处理器而不触及订单服务。
– **解耦**:订单服务不需要知道谁消费该事件。你可以在不更改生产者的情况下添加新消费者(欺诈检测、CRM 同步)。
– **平滑峰值**:队列吸收峰值并允许下游系统以可持续的速率处理,而不是被过载。
– **重试处理**:失败的消息可以自动重试。死信队列捕获重复失败的消息以供调查。
一个常见的现实世界模式是”现在做写入,稍后做繁重的工作”。
例如,在社交应用程序中,创建帖子通常是快速写入和立即成功响应。昂贵的繁重工作,如扇出、索引、通知和源更新异步发生,这就是为什么你有时会看到喜欢计数或源传播的小延迟。
在这一点上,你的架构可以在单个区域内处理大规模。但你的用户并非都在一个地方,你的基础设施也不应该。
一旦你有跨大陆的用户,延迟变得明显,单个数据中心成为整个全球用户群的单点故障。
—
## 阶段 7:多区域 + 高级模式(1M-1000 万+ 用户)
对于全球数百万用户,新挑战出现:
– 澳大利亚用户访问美国服务器时经历 300ms 延迟
– 数据中心故障(火灾、网络分区、云提供商问题)导致整个服务宕机
– 你的数据库架构无法高效地为写入密集型实时更新和读取密集型分析仪表板提供服务
– 不同区域有不同的数据驻留要求(欧盟 GDPR、数据本地化法律)
这个阶段涵盖**多区域部署**、**高级缓存**和**专业模式**,如 CQRS。
### 多区域架构
部署到多个地理区域实现两个主要目标:
– **更低延迟**:用户连接到附近的服务器。东京用户访问东京服务器(20ms)而不是美国服务器(200ms)。
– **灾难恢复**:如果一个区域失败,其他区域继续提供服务。真正的高可用性。
有两种主要方法:
#### 主从(主备)
一个区域(主)处理所有写入。其他区域服务读取,如果主区域失败,可以接管。
**优点:**
– 更容易实现
– 不需要写入冲突解决
– 强一致性…
(注:文章部分内容在原网页被截断,但已涵盖核心的 7 个阶段扩容策略。)
—
### 总结
系统扩容不是一个一蹴而就的过程,而是一个渐进的演进过程。从单台服务器开始,随着用户增长逐步引入新的技术和架构:
1. **0-100 用户**:单台服务器,简单为王
2. **100-1K 用户**:分离数据库,资源隔离
3. **1K-10K 用户**:负载均衡 + 水平扩展,处理单点故障
4. **10K-100K 用户**:缓存 + 只读副本 + CDN,减轻数据库压力
5. **100K-500K 用户**:自动扩展 + 无状态设计,应对流量峰值
6. **500K-1M 用户**:分片 + 微服务 + 消息队列,解决写入瓶颈
7. **1M-1000 万+ 用户**:多区域部署,实现全球高可用
关键原则是:**不要过早优化**。从简单开始,识别真正的瓶颈,然后有针对性地解决。每个阶段都为下一个阶段奠定了基础,过度设计只会增加不必要的复杂性。
记住,这些用户范围是指导原则。确切阈值会根据你的产品、工作负载和流量模式而有所不同。最重要的是持续监控你的系统,了解其当前的限制,并规划下一步。
—
*原文链接:https://blog.algomaster.io/p/scaling-a-system-from-0-to-10-million-users*
发表回复