> Scaling Our AWS Infrastructure
本文由Kareem Ayesh和Yasser El-Sayed撰写。
Meddy成立于2016年,自那时以来取得了巨大成功,这要归功于它的规模。 2019年,我们在A轮融资️的基础上庆祝了第100,000笔预订和300万名用户服务。
在过去的四年中,Meddy经历了许多技术更改。 本文是针对不断成长的科技创业公司的基础架构的技术建议。 我们将讨论4年前基础架构的起点,多年来我们面临的所有问题以及按照实施顺序实施的增量解决方案。
如果您是一家成长型初创公司的抱负CTO,那么本文将为您带来极大的好处。
我记得当首席执行官问我为什么说"因为成千上万的人使用它"时,CEO告诉我说他的代码比我编写的任何代码都要好。
…他是对的。
> Monolithic Infrastructure
单独拥有一个整体不是问题。 对于大多数整体而言,这是很自然的,它会优雅地进化并转变为科学怪人,而这正是我们所做的。
话虽如此,这也意味着很多问题将在项目的整个生命周期中显现出来,并且肯定有很多问题确实存在。
但是,我们担心的不是技术问题,而是无法解决的问题。
整体的存在意味着问题等于死胡同。 很自然地,要求的功能越多,就会表现出更多的问题,解决这些问题与创建功能一样自然。
最初的重构和基础结构的更新对于建立应用程序的增长至关重要,从而为各个方面的潜在问题提供了潜在的解决方案。 这样一来,新功能请求就不会产生一系列呃和或更糟的"我认为现在不可能"。
在当前的整体架构中,数据持久性是一个严重的难题。 图像上载,数据库和日志都必须备份在根文件系统上,并在需要启动新实例时使用。 由于那时没有暂存实例,因此在生产上进行测试,并且服务器始终都在发生故障,从而危及我们的数据!
需要解决的另一个大问题是部署。 部署是通过SSH和git pull从我们的Github起源完成的,没有任何类型的脚本可以自动启动新进程,运行测试和报告失败。
显然,我们需要一些改变。 我们决定,随着应用程序代码的更改,我们将进行小的基础结构更改,从而将数据库和文件上载移至单独的服务。
通过使用三个AWS服务解决了这三个问题:
· 使用RDS管理Postgres数据库
· 使用S3存储媒体上载
· 使用ElasticBeanstalk管理部署
> Separate Data Sources ✔
重申一下,我们在此项目中所做的总体更改并未带来基础架构的很大改善。 这种分离只是意味着应用程序中将来的问题不会损坏任何敏感内容。 此外,由于主EC2实例上只有一个应用程序,因此这意味着任何其他资源都可以存在于单独的服务器或服务上而不会出现问题。
这不是完美的:
· Celery和redis与应用程序本身在同一服务器上
· 前端应用程序是AngularJS和Django之间的混合渲染,有时会阻塞处理器
· 日志存储在文件服务器上。
那时,数量很少,我们没有很多服务甚至功能,因此这些问题是可以容忍的。
尽管如此,该项目为最终峰会奠定了非常重要的垫脚石。
特别感谢Yusuf Musleh的帮助
搜索很麻烦,因为它花了很长时间并且不够准确。 我们使用Postgres的Trigram相似性实现来实现搜索。 在大量记录上,它不是最快的,并且在多个字段上的搜索根本不准确。
最重要的是,您无法真正控制这些搜索的行为。 我们想跟踪这些搜索查询所遇到的所有问题。
> A google Sheet containing all the improvements to search we want to do
每当我们出现错误的行为时,我们都无法通过当前的实现解决该行为。
搜索会占用其实例中的大量资源。 我们决定不只是扩展实例并在其中运行搜索应用程序,而是决定启动一个单独的服务,该服务在EC2实例上本地运行ElasticSearch。 每当创建或更新新记录时,我们都会在EC2实例中更新索引。 还有一个cronjob会定期更新整个索引。
> Add ElasticSearch ✔
这样可以更快,更好地控制搜索,我们不断地进行迭代,而无需对ElasticSearch服务上运行的图像进行太多更改。
我们在每个部署上都经历了10秒钟的停机时间。 部署由ElasticBeanstalk负责。 在较低的级别上,EB并行运行新(ondeck)和旧(当前)应用程序,并在所有执行脚本成功后执行符号链接。 由于我们使用的是一台服务器,因此此符号链接将导致5-10秒的停机时间需要解决。
要解决此问题,我们需要在多台服务器上进行滚动部署。 这是一项相对简单的工作,只需要在由我们的ElasticBeanstalk管理的Autoscaling组前面安装一个Application Load Balancer。
> Add Load Balancing ✔
滚动部署增加了测试的开销和更长的部署时间,但完全消除了停机时间。
我们使用的是混合渲染,其中Django应用程序将提供部分渲染的html,并在客户端上运行AngularJS应用程序。 这引起了许多问题:
我们的渲染速度很慢,并阻塞了开发团队和服务器。 我们的服务器需要大量的处理能力才能进行渲染,这是我们响应时间不可避免的瓶颈。 甚至开发也变得困难,因为我们需要多次编写更改代码。 在某些时候,即使更改单个跟踪事件也很困难。
较差的构建系统导致较高的加载时间。 因为没有用于混合渲染的AngularJS构建管理器,所以我们使用了自己的构建系统,并且我们自己的系统存在很多捆绑不一致和缓存问题,导致未优化的构建和较高的加载时间。 这也是改变和改进的麻烦。
网站有时会挂起5-10分钟。 当我们收到来自用户或漫游器的大量请求时,服务器将打开与数据库的太多连接,从而导致数据库挂起。 在我们的A系列为Meddy带来更多业务之后,这种情况在2019年底每周发生一次。 原因是因为Django打开数据库连接并保持打开状态,直到HTML渲染结束! 由于数据库连接过多,服务器将挂起并最终为504服务。
在系列A结束后不久,我们便着手将代码从混合渲染器转换为纯客户端渲染器。 为此,我们必须努力对后端服务请求的方式进行很多更改。
我们决定使用S3存储桶作为前端的静态托管,CloudFront分发响应。 CloudFront还为我们提供了附加Lambda函数的选项,以对我们所需的请求进行非常精细的控制。
我们进行混合渲染的主要原因之一是担心SEO会被动态渲染弄脏。 并且我们网站被挂起的主要原因之一是由于漫游器导致的请求高峰。 这两个问题都可以通过预渲染我们的页面并将预渲染的页面提供给bot来解决,这对于SEO来说是完美的。
为此,我们使用了一个名为prerender.io的服务,该服务可预先渲染请求的页面,缓存这些页面并将这些页面提供给机器人。 我们在CloudFront发行版使用的Lambda Edge函数中添加了基于HTTP请求的用户代理的自动程序检测机制。 只要检测到漫游器,就会将其重定向到prerender.io以获取其缓存的页面。
> Add Pre-rendering and Bot Detection ✔
需要注意的是,一旦我们达到300,000页并且prerender.io的成本很高,我们便决定拥有自己的本地预渲染服务。 我们使用S3和ECS Fargate实施了自己的缓存机制来托管服务。
缓存资源未在服务器之间共享。 缓存资源无法在服务器之间共享,这意味着某些请求将被缓存而其他请求将不会被缓存。 此外,我们在服务器上存在Redis的问题; Redis已在服务器上安排了数据转储,这两次导致磁盘服务被另一服务吞噬时导致停机。
在一台服务器上运行所需的计划任务。 Celery Beat具有内置功能,可以在特定日期和时间运行任务,并提供事件流,该事件流以表的形式存储在数据库中。 这对我们非常有用,因为我们使用它在约会前后的特定时间间隔发送约会的提醒SMS。 如果将其保留在服务器上,则会执行重复的任务,这将主要在我们的通知模块内引起冲突。
由于我们现在正在负载平衡器上使用多个服务器,因此后端必须共享一个类似的缓存服务器。 为此,我们使用了ElastiCache。 设置非常简单。 更具挑战性的方面是我们的异步任务管理器,该管理器通过Celery Beat在每台服务器上进行管理。
为了消除这种耦合,我们将在新服务上创建事件,该服务将通过向后端发出请求来调用这些事件。 该事件将仅存储需要调用的函数,该函数的参数以及调用它的时间。它具有自己的数据库,后端可以根据后端的请求填充事件。
> Move Redis and Celery to Separate Services ✔
我们称此新服务为Celery Beet,是因为我们认为Celery Beat库错过了这么好的机会。
这有一个主要的缺点:调用请求是通过HTTP完成的,因此您无法泡菜。 但是,由于Celery建议不要使用Pickle,因此我们从未在代码中使用它。
缩短链接的重定向发生在客户端应用程序上。 转移到纯客户端应用程序意味着服务器无法在链接上提供301服务,因为所有请求都发送到了S3存储桶。 当必须从/ x重定向到/ y时,客户端应用程序必须向服务器检查有关/ x的信息,并通过向DOM添加一个额外的参数来通知Prerenderer该页面将被重定向。 为了确保Prerenderer能够继续使用,我们创建了一个页面,上面写着"重定向您,请稍等片刻",并添加了额外的参数。
这对于内部重定向来说很好,因为我们没有那么多。 对于缩短链接而言,这不是理想的选择,因为我们希望用户无需等待就可以直接进入那些缩短的链接,尤其是当我们需要在Meddy外部重定向时。
为了解决这个问题,我们创建了两个Lambda函数,这些函数提供了具有哈希和链接的Dynamo表。 一个Lambda函数的任务是缩短,而另一个Lambda函数的任务是重定向缩短的链接。
此外,我们使用API网关作为重定向lambda函数的入口,并将其附加到这些URL的新(较短)域中。
> Add Link Redirection using Lambda ✔
这非常有效,因为Lambda实际上不提供维护,使用情况图,并且我们的数据库不必包含这些重定向的大型表。
迫切需要更好的日志记录。 服务器请求数量的增加意味着我们不再依赖旋转文件系统上的日志,因为文件旋转得如此之快。 最重要的是,我们正在使用的服务数量正在增加,并且从不同文件系统中提取文件是一场噩梦。 为所有服务使用集中式日志记录系统是一个挑战。
我们决定使用Grafana Loki构建日志记录和监视服务。 Loki是受Prometheus启发的开源水平可伸缩的多租户日志聚合系统。 而Grafana是开源分析和交互式可视化软件。 本质上,我们使用Loki作为日志聚合工具,使用Grafana可视化这些日志。
想法是让我们的不同服务将日志推送到在Loki服务中打开的HTTP端点。 然后,Loki将为它们建立索引并将日志以二进制格式存储在S3中,并将索引存储在DynamoDB表中。 这为我们提供了一种在日志中查找某些标签的方法,例如特定的状态代码或特定的日志级别。 它还使我们能够查询日志,甚至可以对它们应用聚合功能,例如计算最近5分钟内具有特定标签的日志数量。
> Add Logging & Monitoring using Grafana Loki ✔
对于事件监视,我们使用Grafana,但这次使用AWS CloudWatch。 Grafana充当CloudWatch的客户端,并按需轮询数据以使其可视化。
为了能够如上所述将日志推送到Loki,我们必须使用来自不同服务的不同技术,例如在应用程序服务器上构建处理程序以及附加到数据转储的Lambda函数。 Lambda函数用于解析S3中的Load Balancer和CloudFront日志,然后将它们推送到Loki。
> One of our many screens on Grafana
结合使用数据处理程序和CloudWatch,我们能够为基础架构中的所有内容提供集中式日志记录和监视系统,您可以在一个位置监视所有内容。
> All done!
这可能是迄今为止我们最喜欢的文章! 由于我们经历了许多挑战,因此显示出了很大的增长。 最令人兴奋的是,这仍然是Meddy的开始。
感谢您的阅读! 敬请期待更多!
(本文翻译自Yasser的文章《Scaling Our AWS Infrastructure》,参考:https://medium.com/swlh/scaling-our-aws-infrastructure-9e64e6817b8c)