SoundCloud体系结构的演进

2020-08-04 02:39:50

这是一个关于我们如何随着时间的推移调整我们的架构以适应增长的故事。

扩展是一个奢侈的问题,令人惊讶的是,它更多地与组织有关,而不是与实施有关。对于每一个变化,我们解决了我们需要支持的下一个数量级的用户,从数千人开始,现在我们正在为数亿人设计。我们通过在我们的基础设施中引入明确的集成点来单独划分和克服每个问题,从而找出我们的瓶颈,并尽可能简单地解决它们。

通过识别规模点并将其提取到较小的问题中,并在时机到来时拥有定义良好的集成点,我们能够有机地增长。

从第一天起,我们就有一个简单的需求,那就是尽快把每个想法从我们的脑海中抹去,摆在眼球面前。在此阶段,我们使用了非常简单的设置:

Apache为我们的图像/风格/行为资源提供服务,而MySQL支持的Rails提供了一个环境,在这个环境中,我们几乎所有的产品都可以快速建模、路由和渲染。我们团队的大多数人都了解这种模式,可以很好地合作,交付与我们今天的产品非常相似的产品。

我们有意识地选择在这一点上不实现高可用性,因为我们知道当那个时候有希望到来时会采取什么措施。在这一点上,我们离开了我们的私有测试版,向公众展示了SoundCloud。

我们的主要成本优化是为了机会,避免了任何阻碍我们开发SoundCloud背后的概念的事情。例如,当一条新的评论被发布时,我们会将其屏蔽,直到所有的关注者都收到通知,知道我们以后可以将其设置为异步状态。

在早期阶段,我们有意识地确保我们不仅是在构建产品,而且是在构建一个平台。我们的新公共API从一开始就是与我们的网站一起开发的。我们现在正在使用我们向第三方集成提供的相同API来驱动网站。

Apache为我们提供了很好的服务,但是我们在多个主机上运行Rails应用服务器,而且Apache中的路由和虚拟主机配置在开发和生产之间保持同步很麻烦。

Web层的主要职责是管理和分派传入的Web请求,以及缓冲出站响应,以便尽快释放应用程序服务器用于下一个请求。这意味着我们拥有的连接池和基于内容的路由配置越好,该层就越强大。

在这一点上,我们用Nginx取代了Apache,并降低了Web层的配置复杂性,但我们的体系结构没有改变。

Nginx工作得很好,但是随着我们的发展,我们发现一些工作负载比其他工作负载花费的时间要长得多(大约几百毫秒)。

当您在快速请求到达时处理较慢的请求时,快速请求将不得不等待到较慢的请求完成,这称为“阻塞问题”。当我们有多个应用程序服务器时,每个应用程序服务器都有自己的侦听套接字积压,这类似于杂货店,您不可避免地站在一个寄存器前,看着所有其他寄存器的移动速度都快于您自己的寄存器。

大约在2008年,当我们第一次开发该架构时,Rails和ActiveRecord中的并发请求处理还相当不成熟。尽管我们确信可以审核并准备用于并发请求处理的代码,但我们不想花费时间来审核我们的依赖项。因此,我们坚持每个应用程序服务器进程一个并发的模型,并在每个主机上运行多个进程。

在Kendall的记法中,一旦我们从Web服务器向应用程序服务器发送了请求,请求处理就可以由一个M/M/1队列建模。这样一个队列的响应时间取决于所有先前的请求,因此如果我们大幅增加一个请求的平均工作时间,平均响应时间也会急剧增加。

当然,正确的做法是确保我们的工作时间对于任何Web请求都保持在较低的水平,但我们仍处于机会优化阶段,因此我们决定继续进行产品开发,并通过更好的请求分派来解决这个问题。

我们研究了在每个主机上使用多个子进程的Phusion Passenger方法,但是我们觉得我们可以很容易地用长时间运行的请求填充每个子进程。这就像有许多队列,每个队列上有几个工作者,在单个侦听套接字上模拟并发请求处理。

这将队列模型从3M/M/1更改为3M/M/c,其中3c是每个分派请求的子进程数。这就像邮局里的排队系统,或者“取一个号码,下一个有空的工作人员会帮你”之类的排队。此模型将队列中等待的任何作业的响应时间减少了1/3,这是更好的,但假设我们有5个孩子,我们平均只能接受5倍的缓慢请求。在接下来的几个月里,我们已经看到了10倍的增长,而且每台主机的容量有限,因此仅增加5到10名员工是不足以解决线路阻塞问题的。

我们想要一个从不排队的系统,但是如果它真的排队,排队的等待时间是最短的。把C-M/M/C模式发挥到极致,我们自问:“怎样才能把C-C做得尽可能大?”

要做到这一点,我们需要确保单个Rails应用服务器从不一次接收多个请求。这排除了TCP负载平衡,因为TCP没有HTTP请求/响应的概念。我们还需要确保如果所有应用程序服务器都很忙,请求将排队等待下一个可用的应用程序服务器。这意味着我们必须在服务器之间保持完全无状态。我们有后者,但没有前者。

我们将HAProxy添加到我们的基础架构中,为每个后端配置最大连接计数1,并跨所有主机添加我们的后端进程,以便通过将HTTP请求排队,直到任何主机上的任何后端进程可用,来减少驻留等待时间。HAProxy是作为我们的排队负载平衡器进入的,它将通过对来自应用程序或依赖的后端服务的请求进行排队来缓冲任何临时的反压力,这样我们就可以推迟在请求管道中的其他组件中设计复杂的队列。

我衷心推荐Neil J.Gunther的研究工作:使用Perl::PDQ分析计算机系统性能,以温习队列理论,并增强您对如何建模和测量从HTTP请求一直到磁盘控制器的排队系统的直觉。

一类花了很长时间的请求是社交活动通知的扇出。例如,当您将声音上传到SoundCloud时,所有关注您的人都会收到通知。对于有很多粉丝的人来说,如果我们同步做这件事,请求时间会超过几十秒。我们需要排队等待稍后处理的作业。

大约在同一时间,我们正在考虑如何管理我们的声音和图像存储增长,并选择将存储分流到Amazon S3,而将转码计算保留在Amazon EC2中。

协调这些子系统,我们需要一些中间件来在故障时可靠地排队、确认和重新提交作业单。我们经历了几个系统,但最终还是选择了AMQP,因为它有一个可编程的拓扑,由ARabbitMQ实现。

为了保持与网站中相同的域逻辑,我们加载了Rails环境,并构建了一个轻量级的Dispatcher类,每个关注点都有一个队列。所有队列都有一个名称空间来描述估计的工作时间。这在我们的异步工作器中创建了优先级系统,而不需要通过为绑定到该工作类中的多个队列的每类工作启动一个调度程序进程来增加代理的消息优先级的复杂性。我们的大多数应用程序执行的异步工作队列都使用“交互式”(低于250ms的工作时间)或“批处理”(任何工作时间)命名空间。特定于每个应用程序使用了其他名称空间。

当我们接近数十万用户大关时,我们发现我们在应用层消耗了太多的CPU,其中大部分花费在呈现引擎和Ruby运行时上。

我们没有像大多数应用程序那样引入memcached来缓解数据库中的IO争用,而是积极地缓存了部分DOM片段和整个页面。这变成了一个无效问题,我们通过维护缓存键的反向索引解决了这个问题,这些缓存键也需要在memcached中的模型更改时失效。

我们最大的请求量是一个特定的端点,该端点正在为小部件传递数据。我们在nginx中为该端点创建了一个特殊的路由,并将代理缓存添加到该堆栈中,但是希望将缓存推广到任何端点都可以生成适当的HTTP/1.1缓存控制头,并且可以由我们控制的中介很好地处理的点。现在,我们的新窗口小部件内容完全由我们的公共API提供。

我们在堆栈中添加了memcached和很久以后的Varish,以处理后端部分呈现的模板缓存和大部分只读API响应。

我们的工作人池不断增加,处理更多的异步任务。它们的编程模型都是相似的:采用一个域模型,并计划在以后的状态中继续使用该模型状态进行处理。

作为对此模式的概括,我们以一种称为ModelBroadcast的方式利用了ActiveRecord模型中的保存后挂钩。其原则是,当业务域更改时,对于任何对该类更改感兴趣的异步客户端,事件都会随该更改一起丢弃到AMQP总线上。这种将写入路径与读取器分离的技术通过容纳我们未曾预见的集成,实现了下一步的增长。

After_CREATE DO|r|Broker.Publish(";Models";,";CREATE。河。Class.name";,r.tributes.to_json)endAfter_save do|r|broker.Publish(";model";,";save。河。Class.name";,r.将.to_json更改为_json)endAfter_Destroy do|r|broker.Publish(";Models";,";Destroy。河。类.name";,r.tributes.to_json)结束。

这并不完美,但它在一天的过程中添加了一个急需的无中断、通用化的应用外集成点。

我们最快的数据增长得益于我们的仪表板。仪表板是你的社交图中活动的个性化物化索引,也是个性化你关注的人发出的声音的主要地方。

此组件一直存在存储和访问问题。分别查看读取路径和写入路径,需要针对每个用户在一定时间范围内的顺序访问对读取路径进行优化。写入路径需要针对随机访问进行优化,其中一个事件可能会影响数百万用户的索引。

该解决方案需要一个系统,该系统可以将写入从随机重新排序为顺序写入,并以顺序格式存储,以便读取可以增长到多个主机。排序字符串表非常适合持久性格式,并且在组合中增加了自由分区和伸缩的承诺,我们选择Cassandra作为仪表板索引的存储系统。

中间步骤从模型广播开始,并使用RabbitMQ作为阶段性处理的队列,分三个主要步骤进行:扇出、个性化和对域模型的外键引用的序列化。

个性化检查发起者和目标用户之间的关系,以及注释或过滤索引条目的其他信号。

序列化持久化Cassandra中的索引项,以便稍后根据我们的域模型进行查找和联接,以进行显示或API表示。

我们的搜索在概念上是一个后端服务,它通过HTTP接口公开用于查询的数据存储操作的子集。索引更新的处理方式类似于通过ModelBroadcast的仪表板,但通过由Elastic Search管理的索引存储对数据库副本进行了一些增强。

为了确保用户在仪表板更新时得到正确的通知,无论是通过iOS/Android推送通知、电子邮件还是其他社交网络,我们只需在仪表板工作流程中添加另一个阶段,在仪表板索引更新时接收消息。代理可以通过消息总线将完成事件路由到自己的AMQP队列,以启动自己的逻辑。持久化完成时的可靠消息是我们在整个系统中处理的最终一致性的一部分。

我们在https://soundcloud.com/you/stats向登录用户提供的统计数据也通过代理集成,但我们不使用模型广播,而是发出特殊的域事件,这些事件在日志中排队,然后汇总到单独的数据库群集中,以便跨各种时间范围快速访问。

我们在代理中为异步写路径建立了一些清晰的集成点,在应用程序中为后端服务的同步读写路径建立了一些明确的集成点。

随着时间的推移,应用服务器的代码库收集了集成和功能职责。随着产品开发的解决,我们现在更有信心将功能与要移动到后端服务的集成解耦,这些后端服务不仅可以由应用程序使用,还可以由其他后端服务使用,每个后端服务在持久层中都有一个私有名称空间。

我们开发SoundCloud的方式是确定规模点,然后单独隔离和优化读写路径,以预测下一次的增长。

在产品推出之初,我们的读写缩放限制是消费者眼球和开发人员时长。今天,我们正在针对IO、网络和CPU有限的现实进行设计。我们已经在我们的架构中设置了集成点,为SoundCloud的持续发展做好了一切准备!