我们的目标是支持 100 万用户,但原本支撑 1,000 用户的架构已不堪重负。回顾过去,以下是我希望从第一天就建立起来的架构,以及在高压下扩展的宝贵经验。
阶段 1:单体的成功(直到它失败)
最初的架构非常简单:
Spring Boot 应用
MySQL 数据库
NGINX 负载均衡器
所有组件部署在一台虚拟机(VM)上
[ Client ] → [ NGINX ] → [ Spring Boot App ] → [ MySQL ]
该架构轻松应对 500 并发用户。但当并发用户达到 5,000 时:
CPU 满载
查询变慢
系统可用性跌破 99%
监控显示问题根源:数据库锁、GC 停顿、线程竞争。
阶段 2:增加服务器(却忽略了真正的瓶颈)
我们在 NGINX 后增加了多个应用服务器:
[ Client ] → [ NGINX ] → [ App1 | App2 | App3 ] → [ MySQL ]
读请求 扩展良好,但 写请求 仍集中在 单 MySQL 实例。
负载测试结果:
| Users | Avg Response Time |
| ----- | ----------------- |
| 1000 | 120ms |
| 5000 | 480ms |
| 10000 | 3.2s |
瓶颈并非 CPU,而是 数据库。
阶段 3:引入缓存层
我们添加 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%
缓存命中的读请求响应时间降至 200ms 以内
1,000个并发用户请求的基准测试:
| Approach | Avg Latency | DB Queries |
| ---------- | ----------- | ---------- |
| No Cache | 150ms | 1000 |
| With Cache | 20ms | 50 |
阶段 4:拆分单体架构
将核心功能拆分为 微服务:
用户服务
帖子服务
动态流服务
每个服务拥有独立数据库表(初期共享同一数据库实例)。
服务间通过 REST API 通信:
public class FeedController {
"/feed/{userId}") (
public Feed getFeed( 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
"user-signed-up") (topics =
public void handleSignup(String userId) {
recommendationService.prepareWelcomeRecommendations(userId);
}
使用Kafka后,注册延迟从1.2秒降至300毫秒,因为昂贵的下游任务超出了带宽。
阶段 6:数据库扩展
当用户数达到 50 万时,即使有缓存,单 MySQL 实例仍无法支撑。
解决方案:
读写分离 → 分离读/写流量
分片 → 按用户 ID 分区(例如 0-999k、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% 流量来自 静态资源(图片、头像、JS 文件)。
我们将静态资源迁移至 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 万用户,1,000 RPS)
| Metric | Value |
| ------------------ | ------ |
| P95 API Latency | 210ms |
| Error Rate | <0.1% |
| Cache Hit Ratio | 85% |
| DB Query Rate | 50 qps |
| Kafka Consumer Lag | 0 |
总结
扩展到 100 万用户的关键不是追逐新技术,而是按正确顺序解决正确问题。
支撑前 1,000 用户的架构,无法支撑下一个百万。
在问题爆发前,提前设计应对方案。
-
你在扩展过程中踩过哪些架构大坑?欢迎分享。
作者丨The Latency Gambler 编译丨Rio
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721