
我们刚推出产品时,每天能有 100 个用户 就很开心了。但几个月内,用户数就达到了 10000,接着又涨到了 100000。然而,规模扩张带来的问题堆积得比用户增长还快。
我们的目标是拥有100万用户,但适用于1000人的架构却无法满足需求。回首过去,以下是我希望从一开始就搭建的架构,以及我们在压力下进行规模扩展时所学到的经验。
核心要点摘要
- 百万用户架构不是一次性设计出来的,而是在 CPU、数据库、同步调用、写入争用、静态资源流量等瓶颈暴露后逐层演进。
- 单纯增加应用服务器只能缓解计算层压力,无法解决单 MySQL 写入、慢查询和锁竞争问题。
- Redis 缓存、Kafka 异步化、读写分离、数据库分片、冷数据归档和 CDN 分流,是本文复盘中真正改变瓶颈位置的关键手段。
- 可观测性应尽早建设;当用户数超过 10 万后,没有指标、日志和追踪,定位延迟峰值会变得非常困难。
- 最终架构的重点不是技术名词堆叠,而是让每一层都有明确的扩展策略和故障诊断能力。
架构演进总览
| 阶段 | 主要架构 | 暴露的问题 | 采取的动作 |
|---|---|---|---|
| 初期 | 单台虚拟机 + Spring Boot + MySQL + Nginx | 5000 并发时 CPU 打满、查询变慢、可用性下降 | 识别 CPU、数据库锁、GC 暂停、线程争用等瓶颈 |
| 应用扩展 | Nginx 后增加多台应用服务器 | 读请求改善,但写入仍集中在单 MySQL | 确认瓶颈从应用层转移到数据库层 |
| 读优化 | Redis 缓存读密集查询 | 数据库压力高、重复查询多 | 缓存用户资料等热点数据 |
| 服务拆分 | User、Post、Feed 微服务 | 同步 REST 链式调用导致延迟放大 | 引入服务边界,但继续治理通信成本 |
| 异步化 | Kafka 事件驱动 | 注册等流程被下游任务拖慢 | 把非关键下游任务移出同步路径 |
| 数据扩展 | 只读副本、分片、分区、归档 | 50 万用户后单 MySQL 跟不上 | 降低写入争用,把冷数据移出热路径 |
| 诊断能力 | Jaeger、OpenTelemetry、ELK、Prometheus、Grafana | 缺乏可见性,排查耗时长 | 用指标、日志、追踪缩短定位时间 |
| 边缘分流 | Cloudflare CDN | 静态资源占用大量源站流量 | 将图片、头像、JS 包转移到 CDN |
阶段1:曾经奏效(后来失效)的单体架构
我们最初的技术栈很简单:
- Spring Boot 应用
- MySQL
- Nginx 负载均衡
- 所有内容都部署在一台虚拟机上
[ Client ] → [ NGINX ] → [ Spring Boot App ] → [ MySQL ]
这种设置轻松应对了500个并发用户。但在5000个并发用户的情况下:
- CPU 使用率达到最大值
- 查询速度变慢
- 正常运行时间降至 99% 以下
监控显示出现了数据库锁、垃圾回收暂停和线程争用的情况。
阶段2:增加更多服务器(但忽略了真正的瓶颈)
我们在Nginx后面添加了更多应用服务器:
[ Client ] → [ NGINX ] → [ App1 | App2 | App3 ] → [ MySQL ]
它对读取操作进行了很好的扩展。但是写入操作仍然集中到一个单一的 MySQL 实例中。
在负载测试下:
| Users | Avg Response Time |
| ----- | ----------------- |
| 1000 | 120ms |
| 5000 | 480ms |
| 10000 | 3.2s |
瓶颈不在于CPU,而在于数据库。
第三阶段:引入缓存
我们添加了Redis作为针对读密集型查询的缓存层:
public User getUser(String id) {
User cached = redisTemplate.opsForValue().get(id);
if (cached != null) return cached;
User user = userRepository.findById(id).orElseThrow();
redisTemplate.opsForValue().set(id, user, 10, TimeUnit.MINUTES);
return user;
}
这将数据库负载降低了60%,并将缓存读取的响应时间缩短至200毫秒以下。
1000个并发用户配置文件请求的基准测试:
| Approach | Avg Latency | DB Queries |
| ---------- | ----------- | ---------- |
| No Cache | 150ms | 1000 |
| With Cache | 20ms | 50 |
阶段4:拆分单体架构
我们将核心功能拆分为 微服务:
- User Service
- Post Service
- Feed Service
每个都有自己的数据库 Schema(最初为相同的数据库实例)。
服务间通信使用 REST API:
@RestController
public class FeedController {
@GetMapping("/feed/{userId}")
public Feed getFeed(@PathVariable String userId) {
User user = userService.getUser(userId);
List<Post> posts = postService.getPostsForUser(userId);
return new Feed(user, posts);
}
}
但链式REST调用导致了延迟放大。一个请求会扩展为3到4个内部请求。
从规模上看,这严重影响了性能。
阶段5:消息传递与异步处理
我们为异步工作流程添加了 Kafka:
- 用户注册触发Kafka事件
- 下游服务使用事件,而非同步REST调用
// Publish
kafkaTemplate.send("user-signed-up", newUserId);
// Consume
@KafkaListener(topics = "user-signed-up")
public void handleSignup(String userId) {
recommendationService.prepareWelcomeRecommendations(userId);
}
使用Kafka后,注册延迟从1.2秒降至300毫秒,因为开销较大的下游任务异步执行了。
阶段6:扩展数据库
当用户达到50万时,我们的MySQL实例就跟不上了——即使有缓存也无济于事。
我们添加了:
- 只读副本
- 读写分离的分片
- 基于用户的分区(用户0–99.9万、100万 - 200万等)归档表
- 将冷数据从热路径中移出
示例查询路由器:
if (userId < 1000000) {
return jdbcTemplate1.query(...);
} else {
return jdbcTemplate2.query(...);
}
这减少了跨分片的写入争用和查询时间。
阶段7:可观测性
当用户数量超过10万时,在缺乏可见性的情况下进行调试简直是一场噩梦。
我们添加了:
- 分布式追踪(Jaeger + OpenTelemetry)
- 集中式日志(ELK堆栈)
- Prometheus + Grafana仪表盘
示例 Grafana 指标:
| Metric | Value |
| -------------- | ------- |
| P95 latency | 280ms |
| DB connections | 120/200 |
| Kafka lag | 0 |
在具备可观测性之前,诊断延迟峰值需要数小时。之后,仅需几分钟。
说到这,我可就不困了啊!我们在监控/可观测性领域创业四年了,如果您想索要监控/可观测性白皮书资料或申请产品演示,都可联系我们(免费哒)👇

阶段8:CDN 与边缘缓存
当用户达到100万时,40% 的流量会访问静态文件(图片、头像、JavaScript 包)。
我们将它们转移到了 Cloudflare CDN:
| Asset | Origin Latency | CDN Latency |
| ------------------ | -------------- | ----------- |
| /static/app.js | 400ms | 40ms |
| /images/avatar.png | 300ms | 35ms |
这将70%的流量从源服务器分流。
我会尽早构建的最终架构
如果可以重新开始,我会跳过一些阶段,更早地构建这个:
[ Client ]
↓
[ CDN + Edge Caching ]
↓
[ API Gateway → Service Mesh ]
↓
[ Microservices + Kafka + Redis Cache ]
↓
[ Sharded Database + Read Replicas ]
关键经验教训:
- 缓存必不可少
- 数据库扩展需要尽早规划
- 异步处理至关重要
- 可观测性尽早投入必有回报
扩展不是“增加更多服务器”,而是要消除每一层的瓶颈。
最终基准测试(100万用户,每秒1000次请求):
| Metric | Value |
| ------------------ | ------ |
| P95 API Latency | 210ms |
| Error Rate | <0.1% |
| Cache Hit Ratio | 85% |
| DB Query Rate | 50 qps |
| Kafka Consumer Lag | 0 |
结语
扩展到百万用户并非依赖花哨的技术,而是要按正确的顺序解决恰当的问题。
为最初1000名用户提供服务的架构,无法满足接下来100万名用户的需求。
在遇到故障模式之前就针对它们制定应对计划。
FAQ
百万用户架构是不是一开始就要做微服务?
不一定。本文的经验是:早期单体架构可以支撑验证,但要尽早识别哪些模块会成为扩展瓶颈。真正需要提前规划的是数据库扩展、缓存、异步处理和可观测性,而不是为了微服务而微服务。
只增加应用服务器为什么不够?
增加应用服务器可以提高读请求和计算层吞吐,但如果写入仍然落到同一个 MySQL 实例,数据库锁、连接数、慢查询和写入争用仍会限制整体扩展能力。
缓存、Kafka 和 CDN 分别解决什么问题?
Redis 缓存减少重复读请求对数据库的压力;Kafka 把注册后的推荐计算等下游任务移出同步路径;CDN 将图片、头像、JavaScript 包等静态资源从源站分流出去。三者分别作用在读路径、异步流程和边缘流量层。
可观测性为什么要提前做?
当用户数超过 10 万后,问题不再只发生在单一进程内。没有分布式追踪、集中日志和指标仪表盘,延迟峰值可能需要数小时排查;具备可观测性后,定位时间可以缩短到分钟级。
原文:https://medium.com/@kanishks772/scaling-to-1-million-users-the-architecture-i-wish-i-knew-sooner-39c688ded2f1