如何为从1到10万用户的应用程序,设计不同的扩展方案?

2020 年 3 月 26 日

如何为从1到10万用户的应用程序,设计不同的扩展方案?

对于创业公司来说,有用户注册是好事情,但是当用户从零扩展到成千上万之后,Web应用程序又该如何支持呢?


通常来说,这种情况的解决方案要么是来自突然爆发的紧急事件,要么是系统出现瓶颈进行升级改造。虽然方式不同,但是我们也发现了,一个边缘项目发展成高度可扩展项目,其升级方案是有一些普适的“公式”可以套用,本文以 Graminsta 为例,为大家介绍当用户从 1 位发展到 10 万,应用程序如何扩展?


1 位用户:1 台机器


无论是网站还是移动应用,应用程序几乎都包括这三个关键组件:API、数据库和客户端,其中数据库用来存储持久数据,API 服务于数据及与其有关的请求,而客户端负责将数据呈现给用户。


在现代应用程序开发中,客户端往往会被视为一个独立于 API 的实体,这样一来就可以更轻松地扩展应用程序了。


当刚开始构建应用程序时,可以让这三个组件都运行在一个服务器上,类似于我们的开发环境,一位工程师在同一台计算机上运行数据库、API 和客户端。


当然,理论上我们可以把它部署到云上的单个 DigitalOcean Droplet 或 AWS EC2 实例上,如下所示:



但是,当我们的用户未来不止 1 个的时候,其实刚开始就应该考虑是否要将数据层拆分出来。


10 个用户:拆分数据层


拆分数据层,并将其作为一个类似于 Amazon 的 RDS 或 Digital Ocean 的托管数据库的托管服务。这样做的话,虽然成本会比在一台机器上或 EC2 实例上自托管高一些,但是我们可以获得很多现成且方便的东西,例如多区域冗余、只读副本、自动备份等等。


Graminsta 现在的系统如下所示:



100 个用户:拆分客户端


当网站流量变得稳定之后,就到了拆分客户端的时候了。


需要注意的是,拆分实体是构建可扩展应用程序的关键所在。当系统中的某一部分获得了更多流量,那么就应该把它拆分出来,根据其自身的特定流量模式来处理服务的扩展。这也是我会把客户端和 API 看作是相互独立的组件的原因,这样,我们就可以轻松为多平台构建产品,例如 web、移动 web、iOS、Android、桌面应用、第三方服务等,它们都是使用相同 API 的客户端。


现在,Graminsta 的系统如下所示:



1000 个用户:负载均衡器


当新用户越来越多,如果只有一个 API 实例可能满意满足所有的流量,这时我们需要更多的计算能力。


这时,负载均衡器该上场了,我们在 API 前面添加一个负载均衡器,它会把流量路由到该服务的一个实例上,我们就可以进行水平扩展(通过添加更多运行相同代码的服务器来增加可以处理的请求数量)。


我们在 web 端和 API 前面添加了一个独立的负载均衡器,这意味着我们拥有了多个运行 API 和 web 客户端代码的实例。该负载均衡器会把请求路由到任何一个流量最小的实例上。并且,我们还可以从中得到冗余,当一个实例宕机(过载或崩溃)时,其他实例还可以继续运行,响应传入的请求,而不是整个系统宕机。


负载均衡器还支持自动扩展,在流量高峰时可以增加实例的数量,当流量低谷时,减少实例数量。借助负载均衡器,API 层实际上可以无限扩展,如果请求增加,我们只需要不断增加实例就可以了。



编者注:到目前为止,我们拥有的产品和PaaS公司(如Heroku或AWS的Elastic Beanstalk)提供的开箱即用产品非常类似。Heroku把数据库托管在单独的主机上,用自动扩展来管理负载均衡器,并允许我们把API和web客户端分开托管。对于早期初创企业来说,使用Heroku等服务来做项目是一个不错的选择,所有必需的、基本的东西都是开箱即用。


10000 个用户:CDN


对于 Graminsta 来说,处理和上传图像为服务器带来了很大的负担。所以,Graminsta 选择了使用云存储服务来托管静态内容,例如图像、视频等(AWS 的 S3 或 Digital Ocean 的 Spaces),而 API 应该避免图像处理和图像等业务。


另外,使用云存储服务,我们还可以使用 CDN,可以在遍布全球不同的数据中心自动缓存图像。我们的主数据中心可能托管在


我们从云存储服务得到的另一样东西是 CDN(在 AWS,这是一个被称为 Cloudfront 的插件,但是很多云存储服务都以开箱即用的方式提供它)。CDN 将在遍布全球不同的数据中心自动缓存我们的图像。


虽然我们的主数据中心可能托管在俄亥俄州,如果有人在日本对图像发出了请求,那么云供应商就会进行复制,将其存储在位于日本的数据中心,下一个请求该图像的日本用户就会很快收到图像。



10 万个用户:扩展数据层


负载均衡器在环境中添加了 10 个 API 实例,使得 API 的 CPU 和内存消耗都很低,CDN 帮助我们解决了世界各地图像请求的问题。但是现在,我们有一个问题需要解决,那就是请求延迟。


通过研究,我们发现数据库 CPU 的消耗占比达到了 80%-90%,因此扩展数据层成为了当务之急。数据层的扩展是一件很棘手的事情,虽然对于服务无状态请求的 API 服务器来说,只需要添加更多实例即可,但是对于大多数数据库系统来说,却不是这样。


缓存


要从数据库获得更多信息的最简单方法之一是给系统引入一个新的组件:缓存层。实现缓存最常用的方法是使用内存中的键值存储(如 Redis 或 Memcached),且大多数云厂商都会提供数据库服务的托管版本。


当该服务正在进行对数据库相同信息的大量重复调用时,就是缓存大显身手的时候了。当我们访问数据库一次时,缓存就会保存信息,之后再进行相同请求时,就不必再访问数据库了。


例如,如果有人想在 Graminsta 中访问 Mavid Mobrick 的个人资料页面时,我们把从数据库中得到的结果,缓存在 Redis 中关键字 user:id 下,到期时间为 30 秒。之后,每当有人访问 Mavid Mobrick 的个人资料时,我们会首先查看 Redis,如果存在相关资料,那就直接从 Redis 提供数据。


大多数缓存服务的另一个优点是,与数据库相比,更容易扩展。Redis 有个内建的 Redis 集群(Redis Cluster)模式,用的是跟负载均衡器类似的方式,可以把我们的 Redis 缓存分布到多台机器上 。


所有高度扩展的应用程序几乎都充分利用了缓存的优势,缓存是构建快速 API 不可或缺的部分,可以提供更好的查询和更高效的代码,如果没有缓存,我们可能很难扩展到数百万用户的规模。


只读副本


由于对数据库的访问相当多,因此我们需要在数据库管理系统来添加只读副本。借助上面提到的托管服务,只需要点击一下就可以完成。只读副本将和主数据库保持一致,并且能够用于 SELECT 语句。



未来展望


随着应用的不断扩展,我们会把重点放在拆分独立扩展的服务。例如,如果我们使用了 websockets,那么会把 websockets 处理代码抽取出来,放在新的实例上,同时安装负载均衡器。该负载均衡器可以根据 websocket 连接打开或关闭的数量来上下扩展,与我们收到的 HTTP 请求数量无关。


如果未来还会遇到数据层的限制,我们就会对数据库进行分区和分片。


我们会使用 New Relic 或 Datadog 等服务安装监控程序,并通过监控程序发现比较慢的请求,改进它。同时,随着扩展的不断进行,我们希望能够发现更多的瓶颈并解决它。


原文链接:


https://alexpareto.com/scalability/systems/2020/02/03/scaling-100k.html


2020 年 3 月 26 日 09:262592

评论 3 条评论

发布
用户头像
这个对于一般的应用基本够用了,不过10万用户还得说说活跃用户及可以支持的并发访问数量,毕竟僵尸用户也很多
2020 年 03 月 30 日 10:53
回复
用户头像
清晰易懂,我这个做客户端的都能明白。
2020 年 03 月 26 日 15:14
回复
有点类似于“扩展公式”,希望有用^_^
2020 年 03 月 26 日 16:28
回复
没有更多评论了
发现更多内容

无价值人生记录.0:浪费1000%时间去做一个用来节省1%时间的“轮子玩具”(上:因缘)

八苦-瞿昙

C# 程序员人生 随笔 随笔杂谈 aop

DOM 树的构建

法正

html DOM 前端进阶训练营

图说前端-内存管理(1/3)

梦见君笑

前端 内存 前端进阶训练营

分布式系统的一些基础理论

俊俊哥

分布式事务 CAP Base

不会有人还不知道全文检索工具Lucene怎么用吧?文字长文教程

给你买橘子

Java 搜索引擎 lucene 程序员 开发工具

《精益思想》读后感分享

zhongzhq

精益 精益思想 精益生产方式 高效工作方式

基于Kubernetes实现的大数据采集与存储实践总结

岿然独存5

Docker Kubernetes S3 EFK Fluentd

游戏夜读 | 如何分析游戏体验?

game1night

RESTful 架构及实践

pingan8787

JavaScript 前端 RESTf

redis系列之——Redis为什么这么快?

诸葛小猿

Java redis 程序员

基础篇:JAVA基本类型

csc

java classpath Java 25 周年

如果你想写自己的Benchmark框架

程序那些事

JVM 性能调优 GC benchmark

玩转Redis高可用 - 哨兵(Sentinel)模式

Man

高可用 redis高可用 中间件

如何搭建一个HBase集群

Rayjun

HBase

ARTS 打卡 第2周

Scotty

图说前端-使用Atomics避免SharedArrayBuffers中的race conditions(3/3)

梦见君笑

前端 内存管理 前端进阶训练营

那些让程序员目瞪口呆的Bug

Java小咖秀

程序员 程序员人生 bug

Java 线程的生老病死

武培轩

Java 线程 多线程 并发 线程状态

计算机的时钟(一):NTP协议

ElvinYang

基础篇:Object对象

csc

java string pool Java 25 周年

如何基于 BitMap 进行海量数据分析

GrowingIO技术专栏

互联网 数据分析 科技互联网 数据化

redis里的数据结构

流沙

redis

java 后端博客系统文章系统——No3

猿灯塔

架构师训练营第六周作业

张明森

猿灯塔:spring Boot Starter开发及源码刨析(三)

猿灯塔

Java 猿灯塔

架构师必须知道的架构知识

Chank

架构 架构师 Architecture Architect

啃碎并发(九):内存模型之基础概述

猿灯塔

Java 猿灯塔

图说前端-ArrayBuffers 和 SharedArrayBuffers(2/3)

梦见君笑

前端 内存管理 前端进阶训练营

【计算机网络】网络层——路由器与路由选择协议

烫烫烫个喵啊

计算机网络 网络层

使用 Dockerfile 创建镜像 | Docker 系列

AlwaysBeta

Docker 容器 镜像 Dockerfile 容器技术

给 Spring Boot 项目减减肥!18.18M 到 0.18M 是如何做到的?

给你买橘子

Java 程序员 Spring Cloud 编码 SpringBoot 2

如何为从1到10万用户的应用程序,设计不同的扩展方案?-InfoQ