从 1 到 100 万用户:我真希望早点知道的架构

译文 2025-07-10 11:16:25

我们刚推出产品时,每天能有 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

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云
OpenSource
开源版
Flashcat
Flashcat