从 1 到 100 万用户:我真希望早点知道的架构
我们刚推出产品时,每天能有 100 个用户 就很开心了。但几个月内,用户数就达到了 10000,接着又涨到了 100000。然而,规模扩张带来的问题堆积得比用户增长还快。
我们的目标是拥有100万用户,但适用于1000人的架构却无法满足需求。回首过去,以下是我希望从一开始就搭建的架构,以及我们在压力下进行规模扩展时所学到的经验。
阶段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万名用户的需求。
在遇到故障模式之前就针对它们制定应对计划。
原文:https://medium.com/@kanishks772/scaling-to-1-million-users-the-architecture-i-wish-i-knew-sooner-39c688ded2f1