Reddit广告系统使用Druid替代Redis架构


Reddit是全球最大的社交新闻站点,这是他们用Druid列数据库替代Redis的架构迁移:
Reddit 的广告业务在过去几年取得了惊人的增长,并且不断发展以满足我们不断增长的广告客户群的需求。为广告商提供关于用户如何与他们的广告互动的透明度是任何广告平台的一项关键功能。为了获得广告商关心的信息,我们需要授权他们对大量数据执行特定的聚合和细分。我们的广告每小时生成数十 GB 的事件数据。允许广告商查询过去六个月的数据意味着我们有数千亿个原始事件需要筛选。我们还需要扩大规模,因为我们预计明年的活动量将显着增加。
 
Redis旧系统
随着我们的广告业务的扩展,我们已经超出了旧的报告系统。
根据我们的旧系统,以下是事件如何从 Reddit 客户端流向我们的报告仪表板的简化版本:

  1. Reddit 客户端向我们的服务器触发事件​​(查看、点击等)。这些事件在收到时被写入 Kafka。
  2. Spark 作业验证传入事件并将它们作为 parquet 文件写入 Amazon S3。
  3. 另一个 Spark 作业根据我们需要完成的报告查询预先聚合数据,并将输出插入 Redis。
  4. 报告服务通过计算客户请求的细分和聚合来响应 UI 请求。

预聚合数据使用以下架构存储在 Redis 中:
<advertiserId:date>: {
  <breakdown1:breakdown2:breakdown3>: aggregatedMetrics,
  <breakdown1:breakdown4>: aggregatedMetrics,  <breakdown1:breakdown5>: aggregatedMetrics,
}

Redis 每天为每个广告商存储一个条目。这些条目中的每一个都是一个包含更多键/值的 Thrift 序列化字典。该字典中的键由一组可能的细分值组成,例如广告 ID 和社区。最后,这些键中的每一个都将指向汇总的报告数据。聚合数据包含超过 50 个数据点的值,例如观看次数和点击次数。 
让我们考虑一个简单的查询,其中广告商希望了解他们的广告今天在特定社区中的表现。在这种情况下,报告系统将执行以下操作:
  1. 使用广告商 ID 和日期作为查找键MGET Redis 数据。
  2. 将Redis返回的Thrift数据反序列化为字典。
  3. 使用请求的广告 ID 和社区作为查找键从字典中提取聚合数据。
  4. 使用聚合数据响应请求。

对于我们可以预先聚合的简单查询,该系统速度很快。
 
Redis问题
然而,随着我们的数据和产品的增长,它出现了问题:
Redis作为键值存储, 不提供对数据进行分组或跨多个键执行聚合的解决方案。Redis 中缺乏查询功能意味着我们必须在我们的应用程序代码中实现这个逻辑。
想象一个场景,广告商想要生成一份报告,显示他们的每个广告在过去六个月中获得的点击次数。我们的应用程序代码必须MGET多个 Redis 值(每天一个),将它们全部反序列化,并对这些数据执行任何额外的聚合,因为我们的预聚合数据更加细化。必须自己实现这个逻辑增加了维护负担和错误的表面积。
该系统还存在内存使用问题。必须从 Redis 获取许多键的查询会导致我们的报告服务反序列化大量 Thrift 数据。反序列化所有这些 Thrift 数据很慢并且需要大量内存。我们不得不过度提供报告服务以预测大型查询。随着我们的广告商变大,该系统导致体验下降。
该系统的另一个问题是它不灵活。添加新的细分功能通常意味着在将数据插入 Redis 之前添加新的预聚合。然后我们必须在报告服务中实现相应的查询逻辑。添加新字段还需要大量工作才能将它们连接到整个管道。很明显,我们的数据存储远没有我们需要支持的产品那么灵活。
 
使用 Apache Druid 升级
作为我们始终如一地满足广告客户需求的承诺的一部分,很明显需要一个解决方案来更好地适应我们不断增长的广告业务。在调查了潜在的解决方案后,团队决定转向Druid。Druid 是一个列式数据库,旨在通过摄取时间聚合摄取大量原始事件,并在这些事件中执行亚秒级查询时间聚合,使其非常适合我们的用例。此外,我们与Imply的合作伙伴合作,快速运行该系统。
更新后,我们的报告仪表板的事件流现在看起来像这样:
  1. Reddit 客户端向我们的服务器触发事件​​(查看、点击等)。这些事件在收到时被放置在 Kafka 上。
  2. Spark 作业验证传入事件并将它们作为 parquet 文件放入 Amazon S3。
  3. 另一个 Spark 作业对这些 parquet 文件执行微小的转换,使它们适合 Druid 摄取。
  4. 报告服务通过查询 Druid 来响应 UI 请求。

Druid 的摄取时间聚合意味着不再需要编写自定义逻辑来根据我们的产品需求预先聚合数据。我们可以告诉 Druid 如何通过摄取规范预聚合我们的数据,并从我们的管道中删除所有预聚合逻辑。此外,我们的报告服务不再负责实现任何分组或聚合逻辑,因为它现在只是发送给 Druid 的查询的一部分。这大大减轻了我们报告管道的维护负担,并使我们能够提供更好的产品。
数据源中使用的模式很简单。对于需要分组的每个维度,我们都有一列,还有一列指示事件类型。我们现在可以轻松解决上述用例,即广告商希望使用单个 SQL 查询为其所有广告生成六个月的报告:
SELECT COUNT(*) FROM reporting_events
WHERE advertiser_id == $advertiser_id
AND event_type == ‘click’
AND date >= $start_date AND date <= $end_date
GROUP BY date, ad_id;

尽管我们摄取了大量原始事件,但响应此查询对 Druid 来说不是问题。Druid 面向列的存储和事件汇总使其非常适合提供这些大型聚合查询。
 
结果
将我们的报告管道迁移到 Druid 显着减少了我们的错误表面积,并使系统更灵活地适应不断变化的产品需求。此外,我们在整个报告服务中使用一致的内存。我们不再需要过度提供我们的报告服务,以防万一大型广告商登录。
从我们的可用性和延迟图表中可以看出这种迁移的最大好处:
  1. 我们的旧系统有时难以保持 99.5% 的可用性,而我们的新服务通常能够保持 99.9% 的可用性。
  2. 我们的旧系统的 p99 接近两秒。新的 Druid 支持的报告系统的响应速度通常快 2-3 倍。

Druid 非常适合让我们的报告产品灵活适应广告商想要的新功能。它使我们能够更轻松地为我们的广告商带来新的细分,我们甚至可以在未来提供基于时区的查询。
我们也很高兴开始使我们的报告平台接近实时。目前,随着我们的 Spark 作业管道运行,我们的仪表板每小时更新一次。然而,德鲁伊可以直接从 Kafka 摄取事件,这几乎可以立即将性能数据提供给广告商。