Reddit 于 2005 年成立,愿景是成为”互联网的首页”。

多年来,它已发展成为地球上最受欢迎的社交网络之一,培育了数万个围绕其成员热情和兴趣建立的社区。拥有超过 10 亿月活跃用户,Reddit 是人们参与各种话题讨论的地方。

一些传达 Reddit 惊人流行度的有趣数字如下:

  • Reddit 每月有 12 亿独立访问者,使其成为虚拟城市广场
  • 自 2018 年以来,Reddit 的月活跃用户群爆炸性增长了 366%,证明了对在线社区的需求
  • 仅在 2023 年,惊人的 4.69 亿篇帖子涌入 Reddit 服务器,产生了 28.4 亿条评论和互动
  • 2023 年,Reddit 排名全球第 18 个访问量最大的网站,收入达 8.04 亿美元

看着这些数据,难怪他们最近的 IPO 发布取得了巨大成功,将 Reddit 推向了约 64 亿美元的估值。

虽然货币成功可能归因于领导团队,但如果没有帮助 Reddit 实现如此受欢迎的架构演进之旅,这是不可能的。

在这篇文章中,我们将经历这段旅程,看看一些改变 Reddit 的关键架构步骤。

早期架构

Reddit 最初是用 Lisp 编写的,但在 2005 年 12 月用 Python 重写。

Lisp 很棒,但当时的主要问题是缺乏广泛使用和测试的库。任何任务很少有一个以上的库选择,而且库没有适当的文档。

Steve Huffman(Reddit 的创始人之一)在他的博客中表达了这个问题:

“由于我们主要通过站在巨人的肩膀上构建网站,这使得事情有点困难。没有那么多肩膀可以站立。”

当涉及到 Python 时,他们最初使用了一个名为 web.py 的 Web 框架,该框架由 Swartz(Reddit 的另一位联合创始人)开发。后来在 2009 年,Reddit 开始使用 Pylons 作为其 Web 框架。

下图显示了 Reddit 高级架构的核心组件。

Reddit 高级架构

虽然 Reddit 有许多移动部件,多年来事情也在不断演变,但这个图表代表了支持 Reddit 的整体脚手架。

主要组件如下:

  • 内容分发网络:Reddit 使用 Fastly 的 CDN 作为应用的前端。CDN 在边缘处理大量决策逻辑,根据域名和路径确定特定请求将如何路由
  • 前端应用:Reddit 在 2009 年初开始使用 jQuery。后来,他们也开始使用 TypeScript 重新设计 UI,并转向基于 Node.js 的框架以拥抱现代 Web 开发方法
  • R2 单体:中间是被称为 r2 的巨大盒子。这是使用 Python 构建的原始单体应用,包括搜索等功能以及 Things 和 Listings 等实体。我们将在下一节更详细地查看 R2 的架构

从基础设施的角度来看,Reddit 在 2009 年停用了最后一台物理服务器,并将整个网站迁移到 AWS。他们一直是 S3 的早期采用者,并使用它来托管缩略图和存储日志很长一段时间。

然而,在 2008 年,他们决定将批处理迁移到 AWS EC2,以释放更多机器作为应用服务器。系统运行得很好,2009 年他们完全迁移到 EC2。

如前所述,r2 是 Reddit 的核心。

它是一个巨大的单体应用,有自己的内部架构,如下所示:

R2 单体架构

出于可扩展性原因,相同的应用代码部署并在多个服务器上运行。

负载均衡器位于前面,执行将请求路由到适当服务器池的任务。这使得可以路由不同的请求路径,如评论、首页或用户资料。

昂贵的操作(如用户投票或提交链接)通过 Rabbit MQ 延迟到异步作业队列。消息由应用服务器放置在队列中,并由作业处理器处理。

从数据存储的角度来看,Reddit 依赖 Postgres 作为其核心数据模型。为了减少数据库的负载,他们在 Postgres 前面放置 memcache 集群。此外,他们大量使用 Cassandra 用于新功能,主要是因为其弹性和可用性特性。

架构演进

随着 Reddit 越来越受欢迎,其用户群急剧增长。为了保持用户参与,Reddit 添加了许多新功能。此外,应用的规模和复杂性也增加了。

这些变化创造了在多个领域演进设计的需求。虽然设计和架构是一个不断变化的过程,小的变化每天都在发生,但在几个关键领域已经有了具体的发展。

让我们更详细地看看它们,以了解 Reddit 在架构方面采取的方向。

GraphQL 迁移

Reddit 在 2017 年开始了它的 GraphQL 之旅。在 4 年内,单体应用的客户端已经完全采用 GraphQL。

GraphQL 是一个 API 规范,允许客户端只请求他们想要的数据。这使得它成为多客户端系统的绝佳选择,其中每个客户端都有略微不同的数据需求。

2021 年初,他们也开始转向 GraphQL Federation,有几个主要目标:

  • 淘汰单体
  • 提高并发性
  • 鼓励关注点分离

GraphQL Federation 是一种将多个较小的 GraphQL API(也称为子图)组合成一个大型 GraphQL API(称为超图)的方法。超图充当客户端应用发送查询和接收数据的中心点。

当客户端向超图发送查询时,超图找出哪些子图拥有回答该查询所需的数据。它将查询的相关部分路由到那些子图,收集响应,并将组合的响应发送回客户端。

2022 年,Reddit 的 GraphQL 团队为 Subreddits 和 Comments 等核心 Reddit 实体添加了几个新的 Go 子图。这些子图接管了整体模式现有部分的所有权。

在过渡阶段,Python 单体和新的 Go 子图一起工作以满足查询。随着越来越多的功能被提取到单独的 Go 子图,单体最终可以退役。

下图显示了这种渐进式过渡。

GraphQL 迁移

Reddit 的一个主要需求是处理从单体到新 Go 子图的功能迁移。

他们希望逐渐增加流量以评估错误率和延迟,同时在出现任何问题时能够切换回单体。

不幸的是,GraphQL Federation 规范没有提供方法来支持这种流量的渐进式迁移。因此,Reddit 采用了蓝/绿子图部署,如下所示:

蓝/绿部署

在这种方法中,Python 单体和 Go 子图共享模式的所有权。负载均衡器位于网关和子图之间,根据配置将流量发送到新子图或单体。

通过这种设置,他们还可以控制由单体或新子图处理的流量百分比,从而在迁移过程中实现更好的 Reddit 稳定性。

截至上次更新,迁移仍在进行中,对 Reddit 体验的干扰最小。

数据复制

在早期阶段,Reddit 使用主数据库创建的 WAL 段支持其数据库的数据复制。

WAL 或预写日志是一个文件,记录在提交之前对数据库所做的所有更改。它确保如果在写操作期间发生故障,可以从日志中恢复更改。

为了防止这种复制拖慢主数据库,Reddit 使用一个特殊工具连续归档 PostgreSQL WAL 文件到 S3,副本可以从那里读取数据。

然而,这种方法有几个问题:

  • 由于每日快照在夜间运行,白天数据存在不一致
  • 频繁的数据库模式更改导致数据库快照和复制问题
  • 主副本数据库运行在 EC2 实例上,使复制过程脆弱。有多个故障点,如 S3 备份失败或主节点宕机

为了使数据复制更可靠,Reddit 决定使用 Debezium 和 Kafka Connect 的流式变更数据捕获(CDC)解决方案。

Debezium 是一个开源项目,提供低延迟数据流平台用于 CDC。

每当 Postgres 中添加、删除或修改行时,Debezium 监听这些更改并将它们写入 Kafka 主题。下游连接器从 Kafka 主题读取并使用更改更新目标表。

下图显示了这个过程。

CDC 流程

转向 Debezium 的 CDC 对 Reddit 来说是一个伟大的举措,因为他们现在可以对多个目标系统执行实时数据复制。

此外,代替笨重的 EC2 实例,整个过程可以由轻量级的 Debezium pod 管理。

媒体元数据存储

Reddit 托管包含各种媒体内容(如图像、视频、gif 和嵌入的第三方媒体)的数十亿篇帖子。

多年来,用户一直以加速的速度上传媒体内容。因此,媒体元数据对于增强这些资产的可搜索性和组织变得至关重要。

Reddit 管理媒体元数据的旧方法面临多个挑战:

  • 数据分布在多个系统中,分散各处
  • 存在不一致的存储格式和不同资产类型的不同查询模式。在某些情况下,他们甚至查询 S3 桶获取元数据信息,这在 Reddit 规模上效率极低
  • 没有适当的机制来审计更改、分析内容和分类元数据

为了克服这些挑战,Reddit 构建了一个全新的媒体元数据存储,有一些高级要求:

  • 将所有现有媒体元数据从不同系统移到同一个屋檐下
  • 支持每秒 10 万次读取请求的数据检索,延迟低于 50 毫秒
  • 支持数据创建和更新

数据存储的选择在 Postgres 和 Cassandra 之间。Reddit 最终选择了 AWS Aurora Postgres,因为 Cassandra 中即席查询调试的挑战,以及一些数据可能没有去规范化和不可搜索的潜在风险。

下图显示了 Reddit 媒体元数据存储系统的简化概述。

媒体元数据存储

正如你所看到的,只有一个简单的服务与数据库接口,通过 API 处理读和写。

虽然设计并不复杂,但挑战在于在确保系统继续正确运行的同时将数 TB 数据从各种来源转移到新数据库。

迁移过程包括多个步骤:

  • 在媒体元数据客户端的元数据 API 中启用双写
  • 从旧数据库回填数据到新元数据存储
  • 在服务客户端上启用媒体元数据的双读
  • 监控每次读取请求的数据比较并修复任何数据问题
  • 逐渐增加新元数据存储的读取流量

查看下图,更详细地显示了这个设置。

双写双读

迁移成功后,Reddit 为媒体元数据存储采用了一些扩展策略。

  • 使用基于范围的分区进行表分区
  • 从 Postgres 中的去规范化 JSONB 字段提供读取

最终,他们实现了令人印象深刻的读取延迟:p50 为 2.6 毫秒,p90 为 4.7 毫秒,p99 为 17 毫秒。此外,数据存储通常比以前的数据系统可用性更高,速度快 50%。

图像优化

在媒体空间内,Reddit 每天还服务数十亿张图像。

用户为他们的帖子、评论和资料上传图像。由于这些图像在不同类型的设备上消费,它们需要以几种分辨率和格式可用。因此,Reddit 为不同的用例(如帖子预览、缩略图等)转换这些图像。

自 2015 年以来,Reddit 一直依赖第三方供应商执行即时图像优化。图像处理不是他们的核心能力,因此这种方法多年来为他们服务得很好。

然而,随着用户群和流量的增加,他们决定将此功能内部化以管理成本并控制端到端用户体验。

下图显示了图像优化设置的高级架构。

图像优化架构

他们构建了两个后端服务来转换图像:

  • Gif2Vid 服务:即时调整大小并将 GIF 转码为 MP4。虽然用户喜欢 GIF 格式,但由于其较大的文件大小和较高的计算资源需求,它是动画资产的低效选择
  • 图像优化器服务:处理所有其他图像类型。它使用 govips,这是 libvips 图像处理库的包装器。该服务处理大多数缓存未命中流量,并处理图像转换,如模糊、裁剪、调整大小、叠加图像和格式转换

总体而言,将图像优化内部化非常成功:

  • Gif2Vid 转换成本降低到原始成本的仅 0.9%
  • 编码动画 GIF 的 p99 缓存未命中延迟从 20 秒降至 4 秒
  • 静态图像的字节服务减少了约 20%

内容审核

Reddit 的一个关键功能是审核违反平台政策的内容。这对于保持 Reddit 作为一个对数十亿用户安全的网站至关重要,他们将 Reddit 视为一个社区。

2016 年,他们开发了一个名为 Rule-Executor-V1(REV1)的规则引擎,以实时遏制网站上的违规内容。REV1 使安全团队能够创建规则,根据用户发布新内容或留言等活动自动采取行动。

作为参考,规则只是一个 Lua 脚本,在特定配置的事件上触发。实际上,这可以是一段简单的代码,如下所示:

在这个例子中,规则检查帖子的文本正文是否匹配字符串”some bad text”。如果是,它通过对用户执行异步操作,将操作发布到输出 Kafka 主题。

然而,REV1 需要一些重大改进:

  • 它运行在原始 EC2 实例的遗留基础设施上。这与 Reddit 上所有运行在 Kubernetes 上的现代服务不一致
  • 每个规则作为 REV1 节点中的单独进程运行,随着更多规则的启动需要垂直扩展。超过某一点后,垂直扩展变得昂贵且不可持续
  • REV1 使用已弃用的 Python 2.7
  • 规则没有版本控制,很难理解更改历史
  • 缺乏在沙盒环境中测试规则的暂存环境

2021 年,Reddit 内的安全工程团队开发了一个名为 Snooron 的新流式基础设施。它构建在 Flink Stateful Functions 之上,以现代化 REV1 的架构。新系统称为 REV2。

下图显示了 REV1 和 REV2。

REV1 vs REV2

REV1 和 REV2 之间的一些关键区别如下:

  • 在 REV1 中,所有规则配置都通过 Web 界面完成。使用 REV2,配置主要通过代码进行。然而,有 UI 工具使过程更简单
  • 在 REV1 中,他们使用 Zookeeper 作为规则存储。使用 REV2,规则存储在 Github 中以获得更好的版本控制,并持久化到 S3 用于备份和定期更新
  • 在 REV1 中,每个规则都有自己的进程,在触发时加载最新代码。然而,当太多规则同时运行时,这会导致性能问题。REV2 遵循不同的方法,使用 Flink Stateful Functions 处理事件流,以及一个单独的 Baseplate 应用执行 Lua 代码
  • 在 REV1 中,规则触发的操作由主 R2 应用处理。然而,REV2 工作方式不同。当规则触发时,它将结构化的 Protobuf 操作发送到多个操作主题。一个名为 Safety Actioning Worker 的新应用(使用 Flink Statefun 构建)接收并处理这些指令以执行操作

Feed 架构

Feed 是社交媒体和基于社区的网站的支柱。

每天有数百万人使用 Reddit 的 Feed,它是网站整体可用性的关键组件。在开发 Feed 架构时有几个关键目标:

  • 架构应支持高开发速度并支持可扩展性。由于许多团队与 Feed 集成,他们需要能够快速理解、构建和测试它们
  • TTI(交互时间)和滚动性能应令人满意,因为它们对用户参与度和整体 Reddit 体验至关重要
  • Feed 应在不同平台(如 iOS、Android 和网站)之间保持一致

为了支持这些目标,Reddit 构建了一个全新的服务器驱动 Feed 平台。Feed 的后端架构进行了一些重大更改。

早期,每个帖子由一个 Post 对象表示,该对象包含帖子可能有的所有信息。这就像通过电线发送厨房水槽,随着新帖子类型,Post 对象随着时间的推移变得相当大。

这对客户端也是一个负担。每个客户端应用包含一堆繁琐的逻辑来确定应该在 UI 上显示什么。大多数时候,这个逻辑跨平台不同步。

随着架构的更改,他们远离大对象,转而只发送客户端将渲染的确切 UI 元素的描述。后端控制元素的类型和它们的顺序。这种方法也称为服务器驱动 UI。

例如,帖子单元由一个包含 Cell 对象数组的通用 Group 对象表示。下图显示了 Annoucement 项目和 Feed 中第一个帖子的响应结构更改。

gRPC 迁移

在早期,Reddit 采用 Thrift 构建其微服务。

Thrift 使开发人员能够定义一个公共接口(或 API),使不同服务能够相互通信,即使它们是用不同的编程语言编写的。Thrift 获取语言无关的接口并为每种特定语言生成代码绑定。

这样,开发人员可以使用对其编程语言看起来自然的语法从他们的代码进行 API 调用,而不必担心底层的跨语言通信细节。

多年来,Reddit 的工程团队构建了数百个基于 Thrift 的微服务,虽然它为他们服务得很好,但 Reddit 不断增长的需求使得继续使用 Thrift 成本高昂。

gRPC 在 2016 年登上舞台,并在云原生生态系统中取得了显著采用。

gRPC 的一些优势如下:

  • 它提供对 HTTP2 作为传输协议的原生支持
  • 在几个服务网格技术(如 Istio 和 Linkerd)中有对 gRPC 的原生支持
  • 公共云提供商也支持 gRPC 原生负载均衡器

虽然 gRPC 有几个好处,但切换成本并非微不足道。然而,这是一次性成本,而在 Thrift 中构建功能对等将是一个持续的维护活动。

Reddit 决定过渡到 gRPC。下图显示了他们用来开始迁移过程的设计。

gRPC 迁移

主要组件是 Transitional Shim。它的工作是充当新 gRPC 协议和现有基于 Thrift 的服务之间的桥梁。

当 gRPC 请求进来时,shim 将其转换为等效的 Thrift 消息格式,并像原生 Thrift 一样传递给现有代码。当服务返回响应对象时,shim 将其转换回 gRPC 格式。

这个设计有三个主要部分:

  • 接口定义语言(IDL)转换器,将 Thrift 服务定义翻译成相应的 gRPC 接口。此组件还负责适应框架习语和差异(如适当)
  • 代码生成的 gRPC 服务者,处理 Thrift 和 gRPC 之间传入和传出消息的消息转换
  • 可插拔模块,供服务支持 Thrift 和 gRPC

这个设计允许 Reddit 通过重用其现有的基于 Thrift 的服务代码逐渐过渡到 gRPC,同时控制迁移所需的成本和努力。

结论

Reddit 的架构之旅是一个不断演进的过程,由其惊人的增长和多年来不断变化的需求驱动。最初作为单体 Lisp 应用开始,用 Python 重写,但这种单体方法无法跟上 Reddit 人气爆炸的步伐。

公司进行了雄心勃勃的向基于服务的架构过渡。他们面临的每个新功能和问题都促使整体设计在各个领域发生变化,如用户保护、媒体元数据管理、通信渠道、数据复制、API 管理等。

在这篇文章中,我们试图捕捉 Reddit 架构从早期到最新变化的演进,基于可用信息。

参考

本文为学习目的的个人翻译,译文仅供参考。

原文链接:Reddit’s Architecture: The Evolutionary Journey

版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。