摘要:项目上线时,我们仅有100名用户。但几个月后,用户数迅速突破 10,000,接着是 100,000。此时,扩展性问题比用户增长来得更快。
项目上线时,我们仅有100名用户。但几个月后,用户数迅速突破 10,000,接着是 100,000。此时,扩展性问题比用户增长来得更快。
我们的目标是支持 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 通信:
@RestControllerpublic class FeedController { @GetMapping("/feed/{userId}") public Feed getFeed(@PathVariable String userId) { User user = userService.getUser(userId); List posts = postService.getPostsForUser(userId); return new Feed(user, posts); }}但链式 REST 调用导致 延迟叠加:一个请求可能触发 3-4 次内部调用。
用户量激增时,性能急剧下降。
阶段 5:消息队列与异步处理
引入 Kafka 处理异步任务:
用户注册触发 Kafka 事件下游服务通过消费事件替代同步 REST 调用// PublishkafkaTemplate.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 实例仍无法支撑。
解决方案:
读写分离 → 分离读/写流量分片 → 按用户 ID 分区(例如 0-999k、100 万-200 万等)归档表 → 将冷数据移出主表分片查询路由示例:
if (userId效果:减少 写竞争,各分片查询时间显著优化。
阶段 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 |总结
扩展到 100 万用户的关键不是追逐新技术,而是按正确顺序解决正确问题。
支撑前 1,000 用户的架构,无法支撑下一个百万。
在问题爆发前,提前设计应对方案。
-
你在扩展过程中踩过哪些架构大坑?欢迎分享。
来源:dbaplus社群