SoundCloud 是一个新兴的社会化音乐创建和分享平台,前不久,他们研发团队的 Sean Treadway 在 SoundCloud 的博客上谈到了 SoundCloud 的架构演变。
Sean 开门见山指出:
扩展是一个奢侈的问题,它与组织架构的关系远超与具体技术实现的关系。在每个变化阶段,我们都会预测用户的下一个数量级,从数千开始,我们的设计现在支持数亿用户。我们识别出瓶颈,解决它们的方法也很简单:在基础设施中加入明确的集成点,并以分而治之的方式处理各个问题。
识别扩展点,将其转为一系列更小的问题;有良好定义的集成点;这些方法让我们能够以有机而系统的方式增长。
产品初期
SoundCloud 最开始的架构简单而直接:
互联网 ->Web 层(Apache)-> 应用层(Rails)-> 数据层(MySQL)
Apache 支持图片、风格和行为资源,由 MySQL 支持的 Rails 提供一个环境,几乎所有的产品都在其中有 model,可以快速完成路由和呈现。大多数团队成员都可以理解这种模型,交付的产品与我们现在的产品很类似。
我们有意没有在这时处理高可用问题,也知道到时可能面临哪些问题。 此时我们版本脱离 private beta,面向公众发布了 SoundCloud。
我们的主要成本是机会成本,只要是阻碍了我们开发 SoundCloud 产品理念的东西,都被规避了。
在早期,我们有意确保构建的不仅是一个产品,而是一个平台。从一开始,我们的公开 API 与网站同步开发。现在,我们与第三方集成者使用的 API 完全相同,并以之推动网站开发。
后来,SoundCloud 用 Nginx 替换了 Apache,主要原因有两个:
Rails 应用服务器运行在多个主机之上,而 Apache 处理多个虚拟主机的配置和路由很繁琐,特别是要保持开发和生产环境之间的同步时;
为了更好地提供连接池和基于内容的路由配置,以便管理、分配接收到的 web 请求、缓存向外的响应,并可以空出一个应用服务器,尽快处理后续请求。
负载分布与排队理论
接下来,SoundCloud 发现有些负载耗去的时间比其他要多几百个毫秒,一些较快的请求必须要等较慢的请求处理完成。2008 年,他们研发架构时,Rails 和 ActiveRecord 中的并行请求处理还不够成熟。他们也开发了一些并行请求处理的代码,但是为了不占用更多时间去检查依赖,他们使用的方法是:
在每个应用服务器进程上运行一个并行进程,并在每个主机上运行多个进程。
Sean 接下来用到了排队理论中的肯德尔记录法(Kendall’s notation)。
我们从 web 服务器向应用服务器发送一个请求,这个请求过程可以建模为一个 M/M/1 队列。该队列的响应时间由之前所有的请求决定,因此,如果大幅提升一个请求的平均处理时间,那么平均响应时间也会大幅提升。
由于当时仍然处于机会成本最高的阶段,所以 Sean 和他的团队决定:在继续开发产品的同时,用更好的请求分发方法来解决这个问题。
我们研究了 Phusion 旅客方法,也就是在每个主机上使用多个子进程,可是认为这样可能很快会在每个子进程上填满长时间运行的请求。这就像有多个队列,每个队列上有几个工作者,模拟在单个监听端口上的并发请求处理。
这就把 M/M/1 队列模型变成了 M/M/c 队列模型,c 是子进程数目。
该模型类似于银行中使用的排号系统。
该模型降低的响应时间由 c 决定,如果有 5 个子进程,对缓慢请求的处理速度能快 5 倍。但是,我们当时已经预计未来几个月用户会有 10 倍增长,而且每个主机上的处理能力有限,因此,只加入 5 到 10 个工作者并不足以解决排队阻塞问题。
我们希望系统中不要有等待队列,如果有,队列中的等待时间也要降到最低。如果将 M/M/c 用到极致,我们自问:“如何才能让 c 尽可能大?”
为了达到该目的,我们需要确保单个 Rails 应用服务器每次绝不接收超过 1 个请求,TCP 方式的负载均衡就此出局,因为 TCP 无法区分 HTTP 请求和响应。我们也要保证:如果所有的应用服务器都是忙碌状态,请求将会被排队到下一个可用的应用服务器中。这就意味着我们必须保证所有的服务器做到完全无状态。当时,我们做到了后者,但未实现前者。
我们在基础设施中加入了 HAProxy,将每个后端配置的最大连接数为 1,并在所有主机中加入了后端进程,保证 M/M/c 在等待时间上的减少,生成 HTTP 请求队列,当任何主机上的后端进程可以处理时再发送过去。
将 HAProxy 作为队列负载均衡器,Sean 他们就可以把其他组件中复杂的队列设计推到请求管道中处理。此时架构如下图:
Sean 全力推荐 Neil J. Gunther 的书籍《使用 Perl::PDQ 分析计算机系统性能》,帮助大家复习排队理论,更多了解如何针对 HTTP 请求队列系统进行建模和度量,并可以深入到磁盘控制器的层面。
走向异步
为了解决用户通知和存储增长方面的问题,SoundCloud 决定加入中间层,以有效地解决工作队列的失败处理问题。最后他们选择了 AMQP,因为它提供可编程的拓扑,由 RabbitMQ 实现。
为了不修改网站中的业务逻辑,我们调整了 Rails 环境,并为每个队列构建了一个轻量级的分发器。队列的命名空间描述了预估的工作次数,这在异步工作者中创建起一个优先级系统,同时不需要向 broker 中加入信息优先级的处理复杂性,因为每一类工作的分发器进程只处理该类工作中的多个队列。应用服务器中的绝大多数异步工作队列,其命名空间中要么有“interactive”(工作时间小于 250ms),要么是“batch”(任何工作时间)。其他命名空间被用于特定应用。
此时他们的架构图如下:
缓存
当用户达到十万数量级时,Sean 发现应用层占用了过多 CPU,主要用在呈现引擎和 Ruby 运行时上。
不过,他们没有用 Memcached,而是缓存了大量 DOM 片段和完整页面。由此引发的失效问题,他们通过维护缓存主键的反向索引解决,使用 Memcached,当应用中的 model 发生改变从而导致失效时,同样需要该方法解决。
我们最高的海量请求,来自于某个特定的服务,它向页面微件(widge)交付数据。我们在 Nginx 中为该服务创建了特定路由,并为其加入代理缓存。不过我们希望缓存功能能够通用化,做到任何服务都能创建正确的 HTTP/1.1 缓存控制头,并可以由我们控制的某个中介正确处理。现在,我们的微件内容完全由公开 API 提供。
此后,为了处理后端部分呈现模板缓存和大多数只读 API 响应,他们加入了 Memcached,并在很晚之后加入了 Varnish 。此时,他们的架构是:
通用化
SoundCloud 后来的处理模型变成:针对某个领域 model,为其状态设定连续处理方式,以便处理后续状态。
为了通用化这个模式,他们利用了 ActiveRecord 的 after-save 钩子,加入了 ModelBroadcast 方式。原则是:当业务领域变化时,事件会丢入 AMQP 总线中,任何对此变化感兴趣的异步客户端会得到该变化。
把写路径从阅读者中解耦出来,这种技术容纳了我们从未想到过的集成点,为未来的增长和演化提供了更大空间。
以下是示例代码:
after_create do |r| broker.publish("models", "create.#{r.class.name}", r.attributes.to_json) end after_save do |r| broker.publish("models", "save.#{r.class.name}", r.changes.to_json) end after_destroy do |r| broker.publish("models", "destroy.#{r.class.name}", r.attributes.to_json) end
Dashboard 功能
数据的快速增长引出了 Dashboard 功能。在 SoundCloud 中,用户可以在 Dashboard 中看到个人和的社会化活动索引,并可以个人化来自自己关注的人制作的音乐片段。SoundCloud 一直受困于 Dashboard 组件带来的存储和访问问题。
读写的路径各自不同,读操作路径需要针对每个用户提供一定时间范围内的顺序读并做优化,写操作路径需要对任意访问做优化,而且一个事件就有可能影响几百万用户的索引。
解决方法是重新排序任意读操作,将其变为顺序方式,并以顺序格式存储供未来的读取使用,这可能会扩展到多个主机上。排序字符串表非常适合用持久化格式,考虑到需要自由分区和扩展,我们选择了 Cassandra 存储 Dashboard 的索引。
我们从 model 广播开始,然后用 RabbitMQ 做队列完成步骤处理,主要包括三步:扇出(fan-out)、个性化、指向领域模型的外键引用串行化。
- 扇出会找出一个活动应该传播到的社会化图谱中的区域。
- 个性化查看发起者和目的用户之间的关系,以及其他注解或过滤索引项目的信号。
- 串行化把 Cassandra 中的索引条目持久化,供以后查找使用,并与领域模型做联接,以供显示或 API 展示。
此时他们的架构如下:
搜索
SoundCloud 的搜索通过 HTTP 接口暴露数据集操作子集,供查询使用。索引的更新与 Dashboard 类似,通过 ModelBroadcase 完成,并使用了由 Elastic Search 管理的索引存储,在复制数据库方面有提升。
通知和状态
为确保用户得到 Dashboard 的通知,SoundCloud 在 Dashboard 的工作流中加入了一个阶段,用来接收 Dashboard 索引更新的消息。Agent 可以通过消息总线得到路由到它们自己的 AMQP 队列中的事件完成通知。他们的状态和统计通过 broker 中介集成,但是没有 ModelBroadcast,他们会发出在日志的队列中的特定领域事件,然后保存在隔离的数据库集群中,以满足不同时间段快速访问需要。
未来预期
Sean 这样描述他们对未来的规划:
我们已经建立起了明确的集成点,包括供异步写路径的 broker 中介中,包括向后端服务完成同步读和写的路径的应用中。
随着时间演变,应用服务器的数据库已经承担了集成和功能两方面的职责。产品开发基本尘埃落定,我们现在有信心将功能与集成分开,转移到后端服务中,供应用层还有其他后端服务使用,各自都可以在持久化层中有自己的命名空间。
我们在 SoundCloud 的研发方式,是识别扩展点,然后分别隔离、优化读路径和写路径,并预估下一个成长阶段的数量级。
最后,Sean 指出了 SoundCloud 以前和现在在架构上的约束:
在产品开发开始阶段,我们读写扩展的限制来自消费者的关注和开发人员的时间。现在,我们的工程方向是 I/O、网络和 CPU 的限制。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论