用 AWS、Scala、Akka、Play、MongoDB 和 Elasticsearch 构建社交音乐服务

  • 吴海星

2014 年 3 月 13 日

话题:ScalaAWS语言 & 开发架构

Serendip.me的前首席架构师Rotem Hermon撰文介绍了初创音乐服务 Serendip.me 在架构及扩展方面所考虑的内容。

Serendip.me 为人们提供社交音乐服务,帮助人们发现朋友们分享的优秀音乐,并为他们介绍“知音” - 那些靠近他们的社交圈子,有相似音乐品味的陌生人。

Serendip 运行在 AWS 之上,采用了如下这些技术:scala (还有一些 Java), akka (用来处理并发), Play 框架 (Web 和 API 前端), MongoDBElasticsearch

技术栈的选择

Serendip 的主要功能是从公共音乐服务中收集 Twitter 上分享的所有音乐,所以它需要处理大量的数据,所以 Serendip 在选择语言和技术时,首先要考虑它们的扩展能力。

因为 JVM 久经考验的性能和工具,并且还有很多采用这门语言开发的开源系统(比如 Elasticsearch),所以他们选择 JVM 作为系统的基石。

而在 JVM 的体系中,scala 又脱颖而出,成为了一个有趣的选择。Scala 可以用现代的方式写代码,又可以跟 Java 全面互通。此外还有一个很重要的原因,akka actor 框架是非常合适的流处理基础设施(绝对是!)。刚刚开始流行起来的 Play 框架看起来也很不错。Serendip 开始于 2011 年初,当时这些都是非常前沿的技术。到了 2011 年末,scala 和 akka 合并成Typesafe,Play 也在不久之后加入。

选择 MongoDB 是因为它对开发者友好,易用,功能集和可能的扩展能力(采用了自动分片技术)。但因为 Serendip 使用和查询数据的方式需要在 MongoDB 上创建很多大索引,而这样会很快引发性能和内存方面的问题。所以他们主要是用 MongoDB 存储键 - 值文档,还有几个需要计数器的功能依赖于它的原子增长。

事实证明,这样使用时 MongoDB 非常牢靠。还容易操作,但主要是因为尽量避免使用分片,并且只有一个复制集(MongoDB 的分片架构相当复杂)。

查询数据需要一个完全成熟的搜索系统。在开源的搜索解决方案中,Elasticsearch 是扩展性最强,并且面向云端的系统。它有动态索引机制,还提供了很多搜索和切面的可能性,可以在其上构建很多功能。因此,Elasticsearch 成为了 serendip 架构中的一个中心组件。

Serendip 决定自己管理 MongoDB 和 Elasticsearch,主要有两个原因。第一,Serendip 要完全控制两个系统。不想在软件的升级 / 降级上依赖于其它元素。第二,因为 serendip 要处理大量数据,采用托管方案要比他们直接在 EC2 上自己管理昂贵得多。

一些数字

Serendip 的“抽水泵” (处理 Twitter 公众流和 Facebook 用户订阅源的那部分) 每天要消化大概 5,000,000 条信息项。这些信息项要经过一系列的“过滤器”,对它们进行检测,并解析出受支持服务(YouTube、Soundcloud、Bandcamp 等)上的音乐链接,还要添加一些元数据上去。抽水泵和过滤器是 akka 的 actor,并且整个过程是用单个 m1.large EC2 管理的。如果需要,可以用 akka 的远程 actor 将任务分发到集群中,轻松实现系统扩展。

从这些信息项中,Serendip 每天大概能得到 850,000 条有效的信息项 (也就是真的包含相关音乐链接的信息项)。这些信息项在 Elasticsearch 中索引 (还要在 MongoDB 中备份并持续计数)。因为每条有效的信息项都要更新几个对象,所以在 Elasticsearch 中的索引率大约为 40 条 / 秒。

Serendip 在 Elasticsearch 中保留一个月的信息项索引 (微博和帖子)。每个月的索引大概包含 25M 信息项,有 3 个分片。集群运行着 4 个节点,每个都在 m2.2xlarge 实例上。这个配置有足够的内存运行 Serendip 所需的数据搜索。

Serendip 的 MongoDB 集群的操作频率大概是 100 次写 / 秒和 300 次读 / 秒,因为它处理更多的数据类型、技术和统计数据更新。复制集的主节点跑在一个 m2.2xlarge 实例上,副节点在一个 m1.xlarge 实例上。

构建订阅源

在设计 Serendip 主音乐订阅源的架构时,想让订阅源是动态的,并且可以根据用户的动作和输入作出反应。比如说,如果某位用户将一首歌标为“摇滚”,或将某位艺术家标为“趾高气昂”,那么这些动作应该马上反应到订阅源上。如果用户“不喜欢”一位艺术家,那以后就不应该再播放那些音乐。

并且这个订阅源应该是几个源头的组合,比如朋友分享的音乐,喜爱的艺术家的音乐,以及有相同音乐品味的“建议”用户分享的音乐。

这些需求意味着那种“fan-out-on-write”式的订阅源创建方式可能并不合适。应该实时构建订阅源,把跟用户相关的所有信号都用上。Elasticsearch 的功能可以构建出这种实时的订阅源生成器。

订阅源算法有几种选择信息项的“策略”,在每次订阅源的获取上都根据不同的比率动态组合。每个策略都会考虑到用户最近的动作和信号。策略的组合被转换成几种对鲜活数据的搜索,这些数据是不断地由 Elasticsearch 索引的。因为这些数据是基于时间的,并且索引每月创建一次,所以只需要查询全部数据中的一小部分子集。

Elasticsearch 非常擅于处理这些搜索。它还提供了一种扩展这种架构的著名路径 - 通过增加分片数量扩展写操作。通过增加更多的复制和物理节点扩展搜索。

寻找“知音”的过程(根据用户的音乐品味进行匹配)充分利用了 Elasticsearch 的切面(聚合)能力。作为持续不断的社交流处理的一部分,系统通过计算顶级分享的艺术家来为它遇到的社交网络用户准备数据(在他们分享的音乐上使用切面搜索)。

当 Serendip 用户给出一个信号(播放音乐或跟订阅源交互)时,它能为那位用户重新触发一次知音计算过程。这个算法按照喜爱艺术家(这个是不断在更新的)列表来寻找匹配程度最高的其他用户,并用一些额外的参数作为权重,比如流行程度、分享次数等。然后再用另一组算法过滤掉垃圾邮件发送者(是的,有音乐垃圾邮件发送者)和异常值。

实践证明,这种处理能得出很好的结果,并不需要再用一套系统来运行更加复杂的聚类或推荐算法。

监测与部署

Serendip 用ServerDensity做系统监测和报警。对于初创公司而言,它是一种易于使用的托管方案,有像样的功能集和合理的价格。ServerDensity 原生提供了服务器和 MongoDB 的监测。Serendi 还大量使用了它报告定制指标的能力来报告内部系统统计数据。

内部统计数据采集机制负责采集系统内发生的所有动作,并把它们保留在一个 MongoDB 集合内。一个定期任务每隔一分钟从 MongoDB 中读取一次这些统计数据,并报告给 ServerDensity。这样就可以用 ServerDensity 对 Elasticsearch 及运营数据进行监测和报警。

服务器的管理和部署是用亚马逊 Elastic Beanstalk 完成的。Elastic Beanstalk 是 AWS 的受限 PaaS 解决方案。很容易上手,但它不是功能完整的 PaaS,对于大部分常见用例而言,它的基本功能已经足够了。它提供了易用的自动扩展配置,还可以通过 EC2 完整访问。

应用程序的构建是由 EC2 上的 Jenkins 实例执行的。Play 程序被打包成 WAR。一个构建后置脚本将这个 WAR 作为新版本推送到 Elastic Beanstalk 上。这个新版本不会自动部署到服务器上 - 它的部署是手动完成的。通常是先部署到临时环境中进行测试,然后经过证实后再部署到生产环境中。

外卖

作为结论,这里有一些在构建 Serendip 的过程中得到的最重要的经验教训,重要程度跟顺序没什么关系。

  1. 知道如何扩展。一开始你可能并不需要扩展,但你得知道系统的每一部分能够如何扩展,以及能扩展到什么程度。如果扩展需要时间,要预先给你自己留出充足的时间。
  2. 为峰值做好准备。特别是在创业阶段,如果你总是接近满负荷运行,一个 lifehacker 或 reddit 帖子就能把你的系统宕掉。保留充足的边界空间,这样才能应对突发负载,或时刻准备好真正快速地扩展。
  3. 选择一门不会拖你后腿的语言。确保你所采用的技术在你的语言中有原生客户端,或者至少有维护得很活跃的一些。不要被等着类库更新给拖住。
  4. 相信炒作。你想要技术跟你的产品共同成长,不会过早死掉。一个充满活力的活跃社区,以及跟该项技术有关的一些噪音,是这种技术存活的良好迹象。
  5. 不要相信炒作。看看那些批判你正在评估的技术的帖子。它们可以告诉你它的弱点。但也不要太认真,当事情不能按照期望工作时,人们通常会变得情绪化。
  6. 玩得开心点。选择会让你兴奋的技术。要能让你觉得“哦,我能用它做的事情太酷啦”。毕竟那(也)是我们来这里的目的。

查看英文原文:Building a social music service - the technology behind serendip.me

ScalaAWS语言 & 开发架构