写点什么

扩容之旅:从 0 到 100 万用户

作者:俞凡
  • 2025-08-04
    上海
  • 本文字数:2195 字

    阅读完需:约 7 分钟

本文介绍了从支持少量用户的单体架构,通过不断迭代优化,直到实现能够支持 100 万用户的架构。原文:Scaling to 1 Million Users: The Architecture I Wish I Knew Sooner



我们刚开始运营时,仅仅 100 名日活用户就已经让我们感到欣喜了。但没过多久,用户数量就达到了 10,000 人,接着又攀升到了 100,000 人。并且随之而来的规模扩张问题比用户数量的增长速度还要快。


我们的目标是 100 万用户,但适用于 1000 名用户的架构却无法满足 100 万用户的需求。回顾过去,本文将介绍我从一开始就希望构建的架构 —— 以及我们在压力下扩容所学到的经验。

第一阶段:有效的单体(但后来也不再有效了)

第一个架构很简单:


  • Spring Boot 应用

  • MySQL 数据库

  • NGINX 负载均衡器

  • 所有东西都部署在一台虚拟机上


[ Client ] → [ NGINX ] → [ Spring Boot App ] → [ MySQL ]
复制代码


该架构能轻松应对 500 名并发用户。但在 5000 名并发用户的情况下:


  • CPU 使用率达到上限

  • 查询速度变慢

  • 正常运行时间低于 99%


监控显示存在数据库锁、垃圾回收暂停以及线程争用的情况。

第二阶段:增加更多服务器(但仍未触及真正瓶颈)

我们为 NGINX 后端添加了更多应用服务器:


[ Client ] → [ NGINX ] → [ App1 | App2 | App3 ] → [ MySQL ]
复制代码


扩容后的操作效果很好,但操作仍然集中在单一 MySQL 实例中。


负载测试下:



瓶颈不在 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% 数据库负载,并将缓存读取响应时间缩短到 200ms 以下。


1000 个并发用户请求基准测试:


第四阶段:打破单体

我们将核心功能分解为微服务


  • 用户服务

  • 发布服务

  • 推送服务


每项服务都有独立数据库(最初使用同一个数据库实例)。


服务之间通过 REST API 进行通信:


@RestControllerpublic 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 次内部请求。


一旦规模变大,就会严重影响性能。

第五阶段:消息传递与异步处理

我们引入 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 毫秒,因为昂贵的下游任务不再占用带宽。

第六阶段:扩展数据库

在用户数量达到 50 万时,MySQL 实例就无法再满足需求了 —— 即便使用了缓存也是如此。


我们引入了:


  • 读副本 → 读写操作分离

  • 分区 → 基于用户分区(用户 0 - 999k、1M - 2M 等)

  • 表归档 → 将冷数据移出热点路径


示例查询路由:


if (userId < 1000000) {    return jdbcTemplate1.query(...);} else {    return jdbcTemplate2.query(...);}
复制代码


这减少了跨分区的写争用和查询次数。

第七阶段:可观测性

在用户数量达到 10 万以上后,如果没有可观测性功能,调试工作简直就是一场噩梦。


我们引入:


  • 分布式追踪(Jaeger + OpenTelemetry)

  • 集中式日志(ELK 框架)

  • Prometheus + Grafana 报表面板


Grafana 指标示例:



在可观察性出现之前,诊断延迟峰值需要几个小时,现在只需要几分钟。

第八阶段:CDN 与边缘缓存

在用户数量达到 100 万时,40% 流量都来自于静态文件(图片、头像、JS 包)。


我们将其移入 Cloudflare CDN 并启用了强力缓存功能:



这样可以从源服务器上卸载 70% 的流量。

最终架构

如果可以重新开始,我将跳过其他阶段并更早构建:


[ Client ]  [ CDN + Edge Caching ]  [ API Gateway → Service Mesh ]  [ Microservices + Kafka + Redis Cache ]  [ Sharded Database + Read Replicas ]
复制代码


关键经验:


  • 缓存并非可选配置

  • 数据库扩展需要尽早进行设计

  • 异步处理至关重要

  • 可观测性能带来早期收益


扩容并非只是“增加服务器数量”那么简单 —— 而在于消除各个层面的瓶颈问题。

最终基准测试(100 万用户,每秒 1000 次请求):

结束语

实现用户数量达到百万级的目标,并非依靠高深复杂的技术,而是在于以正确顺序解决恰当的问题。


当初服务于首批 1000 名用户的架构,已无法满足接下来 100 万用户的需要了。


需要在遭遇失败模式之前就做好应对计划。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 2
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
扩容之旅:从 0 到 100 万用户_架构_俞凡_InfoQ写作社区