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

从单体应用、Nginx、MySQL 到 Redis、Kafka、分库分片、可观测性和 CDN,复盘一个产品从 100 用户增长到 100 万用户时的架构演进路径与瓶颈治理经验。

作者 译文

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

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云