对于一家创业公司来说,有用户注册是好事,但当用户从零扩展到几万时,Web应用如何支撑?通常,这种情况的解决方案要么来自突发事件,要么是系统出现瓶颈需要升级。虽然方法不同,但我们也发现,当一个边缘项目发展成为一个高度可扩展的项目时,其升级方案有一些通用的“公式”可以套用。本文以Graminsta为例,介绍一个用户如何从一个应用扩展到10万?1.1个用户:1台机器无论是网站还是移动应用,应用几乎都包括这三个关键组件:API、数据库和客户端,其中数据库用于存储持久化数据,API为数据及其相关服务提供服务请求,而客户端负责将数据呈现给用户。在现代应用程序开发中,客户端通常被视为独立于API的实体,这使得扩展应用程序变得更加容易。当您第一次开始构建应用程序时,您可以让所有三个组件在一台服务器上运行,类似于我们的开发环境,由一名工程师在同一台计算机上运行数据库、API和客户端。当然,理论上我们可以将其部署到云端的单个DigitalOceanDroplet或者AWSEC2实例中,如下:splitout。2.10users:Splitthedatatier拆分数据层,使其成为类似于亚马逊的RDS或DigitalOcean的托管数据库的托管服务。这样做的成本比在单台机器或EC2实例上自托管要高一些,但我们获得了许多开箱即用的便利,例如多区域冗余、只读副本、自动备份等。Graminsta当前的系统如下所示:3.100个用户:拆分客户一旦站点流量稳定,就该拆分客户了。重要的是要注意拆分实体是构建可伸缩应用程序的关键。当系统的一部分获得更多流量时,应该根据其自身的特定流量模式将其拆分出来以处理服务的扩展。这也是我将客户端和API视为独立组件的原因,这样我们就可以轻松地为多个平台构建产品,例如web、移动web、iOS、Android、桌面应用程序、第三方服务等,他们都是使用相同API的客户端。现在,Graminsta的系统是这样的:4.1000个用户:负载均衡器当新用户越来越多时,如果仅一个API实例就可以满足所有流量,那么我们需要更多的计算能力。这时,负载均衡器就派上用场了,我们在API前面加一个负载均衡器,它将流量路由到服务的一个实例,我们可以水平扩展(通过添加更多运行相同代码的服务器来增加数量可以处理的请求数)。我们在Web和API之前添加了一个单独的负载均衡器,这意味着我们有多个实例运行API和Web客户端代码。负载均衡器会将请求路由到流量最少的实例。而且,我们还可以从中获得冗余,当一个实例宕机(过载或崩溃)时,其他实例可以继续运行,响应传入的请求,而不是整个系统都宕机。负载均衡器还支持自动扩容,可以在流量高峰时增加实例数,流量低时减少实例数。在负载均衡器的帮助下,API层几乎可以无限扩展,如果请求增加,我们只需要不断添加实例即可。编者注:到目前为止,我们所拥有的与Heroku或AWS的ElasticBeanstalk等PaaS公司提供的开箱即用的产品非常相似。Heroku在单独的主机上托管数据库,通过自动缩放管理负载均衡器,并允许我们将API与Web客户端分开托管。对于早期的创业公司来说,使用像Heroku这样的服务来做项目是一个不错的选择,所有必要的、基本的东西都是开箱即用的。5.10,000用户:CDN对于Graminsta,处理和上传图片会给服务器带来很大的负载。因此,Graminsta选择使用云存储服务来托管静态内容,如图片、视频等(AWS的S3或DigitalOcean的Spaces),API要避开图片处理和图片业务。另外,使用云存储服务,我们还可以使用CDN,它可以在全球不同的数据中心自动缓存图片。我们的主要数据中心可能托管在我们从云存储服务获得的另一件事上,即CDN(在AWS中,这是一个名为Cloudfront的插件,但许多云存储服务提供开箱即用的功能)。CDN会自动将我们的图像缓存在世界各地的不同数据中心。虽然我们的主要数据中心可能托管在俄亥俄州,但如果日本有人请求图像,云提供商将制作副本并将其存储在日本的数据中心,下一个请求该图像的日本用户将图像将很快收到。6.10万用户:扩容数据层负载均衡,给环境增加了10个API实例,使得API的CPU和内存消耗低,CDN帮助我们解决了来自世界各地的图片请求问题。但是现在,我们有一个问题要解决,那就是请求延迟。通过研究,我们发现数据库的CPU消耗占到80%-90%,所以扩展数据层成为当务之急。扩展数据层是一项棘手的工作,虽然服务于无状态请求的API服务器只需要添加更多实例,但对于大多数数据库系统而言并非如此。从数据库中获取更多信息的最简单方法之一是向系统引入一个新组件:缓存层。实现缓存的最常见方法是使用内存中的键值存储,如Redis或Memcached,大多数云供应商都提供其数据库服务的托管版本。当服务多次重复调用数据库以获取相同信息时,缓存就会发挥作用。当我们访问数据库一次时,缓存会保存这些信息,以后再做同样的请求时,就不用再访问数据库了。例如,如果有人想在Graminsta中访问MavidMobrick的个人资料页面,我们会在Redis中缓存关键字user:id下的数据库结果,过期时间为30秒。之后,每当有人访问MavidMobrick的个人资料时,我们首先查看Redis,如果有相关个人资料,则直接从Redis提供数据。大多数缓存服务的另一个优点是它们比数据库更容易扩展。Redis内置了Redis集群(RedisCluster)模式,它使用类似于负载均衡器的方法将我们的Redis缓存分布到多台机器上。几乎所有高度可扩展的应用程序都充分利用了缓存。缓存是构建快速API不可或缺的一部分,可提供更好的查询和更高效的代码。如果没有缓存,我们可能很难扩展到数亿用户。只读副本由于对数据库的访问比较多,所以我们需要在数据库管理系统中增加一个只读副本。使用上面提到的托管服务,只需点击一下即可。只读副本将与主数据库保持一致,可用于SELECT语句。7.未来展望随着应用的不断扩展,我们将专注于拆分独立扩展的服务。比如我们使用websockets,那么websockets处理代码会被提取出来放在新的实例上,同时会安装负载均衡器。这个负载均衡器可以根据打开或关闭的websocket连接数进行扩展和缩减,而与我们收到的HTTP请求数无关。如果以后还是遇到数据层的限制,我们就对数据库进行分区、分片。我们使用NewRelic或Datadog等服务安装监控程序,通过监控程序发现较慢的请求并进行改进。同时,随着扩展的不断进行,我们希望能够发现更多的瓶颈并加以解决。
