格伯纳作者
2019-08-26 17:12:54
经验教训帖:探寻Reddit广告服务系统的构建!

Reddit如何使用Go构建其广告服务系统,以及从流程中汲取经验教训。

概要

Reddit工程团队近段时间将Go引入其堆栈,以编写新的广告服务系统来取代第三方系统。 Deval Shah向我们介绍了这个新服务的架构、Reddit团队第一次使用Go的经验,以及他们使用Go构建这个广告服务器所学到的所有课程。

Reddit简介

Reddit是互联网的首页,它是一个拥有数万个兴趣社区的社交网络,人们可以在那里讨论对他们来说重要的事情。

Reddit的数字排名:

第5/18(美国/世界)Alexa排名

330M + MAU

138K活跃社区

每月1200w篇文章

每月2B投票

而Reddit构建的任何系统都必须能够处理此级别的流量。

广告架构概述

广告服务器需要处理整个广告流程。广告服务器处理从广告显示到广告之后的任何后处理的所有内容。

广告服务@ Reddit

Reddit广告服务器有几个要求:

扩展:Reddit上的每个请求都会进入广告系统,所以它必须应对大规模的需求。

速度:广告服务器必须快速。他们不希望广告成为降低用户体验的性能瓶颈。他们要求在30毫秒内回复广告。

拍卖:确保服务器能够根据出价选择最佳广告。

步调:服务器必须能够以最佳方式分配广告。

之前的广告服务@ Reddit:

之前,每当用户访问reddit.com时,reddit 的monolith后端都会向第三方广告服务器发送请求。第三方服务器将使用其选择的一个或多个广告进行响应,并将其返回给用户。

过了一会儿,他们意识到继续使用第三方广告服务器对他们来说不会有用,因为它:

可定制性较低:第三方不支持他们想要进行的许多更改。

操作上不透明:他们无法知道某些事情是如何实施的,无法控制广告的质量等。

我们决定建立一个广告服务器,建立一个由3人组成的团队。从infra开始,编写服务,然后将其推广到正在生产的系统。

广告服务基础设施:

广告服务器基础架构中使用的一些值得注意的工具:

适用于所有RPC的Apache Thrift。 Thrift自2007年以来一直存在,Reddit从一开始就一直在使用它。

RocksDB用于数据存储。它是由Facebook构建的OSS键值存储。它是一个可嵌入的数据存储,它避免了网络跳变,并针对高读取和写入进行了优化。

他们还决定使用Go作为主要的后端语言。这是Reddit第一次将Go用于生产。在此之前,Reddit主要使用Python和Java。该团队希望确保Go成为Reddit使用的语言集中的一等公民,并支持Reddit所需的一切。

广告服务器架构:

这是新广告服务器的架构:

简要概述它的工作原理:

Reddit.com调用了一个名为广告选择器的服务。这是广告投放基础架构中的第一项服务。这是一种Thrift服务,并接收来自reddit.com的请求。然后,它调用一个名为getAds的函数,该函数处理获取和返回要向用户显示的广告。然后广告选择器调用充实服务。

充实服务负责获取有关查找和选择最相关广告所需的请求、用户和其他信息的更多数据和信息。它会收集所有这些信息并将其返回给广告选择器。

收到充实服务的响应后,广告选择器会选择添加,然后将广告返回给reddit.com以显示给用户。它还将回复发送给Kafka。

向用户展示广告后,需要进行一些后期处理。客户端向事件跟踪器服务发送事件HTTP请求。此活动可确认广告已投放。此事件通知也被带到Kafka。

Kafka为两个Apache Spark作业提供数据:

事件统计流作业始终在运行,它会写入增强服务以提供用于学习选择更好的广告信息。

还有Pacing循环,它涉及Pacing Spark工作。这涉及一个流媒体工作,计算每个广告客户展示的广告数量,以及另一个确保广告最佳展示的工作。

在这种架构中,Go服务是:

广告选择器:

有30ms的P99要求

涉及用于定位和选择的复杂业务规则

进行竞价:所有的广告,业务逻辑规则,是在竞争得到广告显示,而广告选择器会处理此问题。

事件追踪:

1ms P99要求

确认日志和事件

需要高度可靠

充实服务:

节俭服务

将数据返回到广告选择器

有一个嵌入的RocksDB数据库

4毫米P99

对于每个请求,它在Go中进行前缀扫描,并获取一堆数据并进行计算和聚合。我们的想法是避免网络跳变获取信息,以确保我们快速提供响应。

Reddit的其他一些Go工具和服务则不会深入探讨:

报告服务

Vault管理工具

广告事件生成服务

我们的Go经验

这是Reddit与Go的第一次体验。德瓦尔表示,到目前为止,这段经验很棒。这项工作始于使用Go的两到三名工程师,现在已经发展到大约十几名在Go方面工作的工程师。

他们在Go看到的主要优势是:

提高开发人员的速度:新工程师可以加入并快速熟悉代码。 Go强调简单性、快速部署和编译时间意味着紧密的反馈循环,这有很大帮助。

开箱即用的出色表现:除了遵循最佳实践外,没有太多的工具或优化来快速运行。与他过去调整JVM和处理垃圾收集的经验相比,这对Deval来说是一次不错的体验。

易于专注于业务逻辑:业务逻辑是困难的部分,Go的简单性和开箱即用的性能有助于团队专注于它。

最后,广告服务会延迟大幅下降:响应时间从90毫秒降至10毫秒以下。

得到教训

这是一系列面临的问题,Reddit如何处理这些问题,以及从这些挑战中学到的知识。

问题1:如何构建生产就绪的微服务?

Reddit以前有过为Python做过的经验,但不是Go。

最初的原型通过大量的StackOverflow读取和谷歌搜索工作,但显然不会与开发人员一起扩展。

他们看到的一些问题是:

记录、指标等都到处都是

改变传输层很难

我们需要可重复的模式

他们意识到Go社区已经解决了这些问题,因此他们研究了解决这些问题的现有框架。他们遇到的一些选择:

他们认为Go-Kit最有意义。 Reddit选择Go-Kit的主要原因是:

支持Thrift

是灵活的,不是非常有描述性。如果Reddit想要转移到gRPC,他们希望能够轻松迁移。

具有用于记录、度量、速率限制、跟踪、断路等工具,这些是在生产中运行微服务时的标准要求。

Go-Kit @ Reddit。这是使用Go-Kit的图:

这个架构有一些值得注意的事情。中心服务有2个实现:内存实现(这很好并可以用于原型),以及用于生产实现的RocksDB实现。本地开发仍然存在内存中实现。

有几个中间件层:跟踪、日志记录和度量。最后,Thrift运输处于顶层。这种结构使得更改变得容易。例如,如果他们想要将传输层从Thrift更改为gRPC,他们只需要更改顶层。

使用Go-Kit是有益的,因为它为团队提供了如何构建Go代码的良好例证。他们以前没有这方面的经验,因此使用Go-Kit有助于理解Go服务的典型结构。

教训1:使用框架/工具包。对于您使用Go的所有内容而言,并不是必需的,但对于需要度量、日志记录等的生产服务,请使用已解决问题的库而不是尝试自己完成。

问题2:如何安全快速地推出新系统?

最终目标是推出新的广告服务器,对Reddit用户、支付广告客户、依赖广告团队的其他内部团队影响最小。第三方广告服务器是一个黑盒子,Reddit需要一种快速迭代、学习和改进的方法。

这就像在飞行途中改变飞机。他们慢慢地在他们的第三方服务周围添加了新的基础设施,当它准备就绪时,他们会把它撕掉:

他们首先将广告选择器注入请求路径,将其纯粹作为代理。系统执行的操作与以前相同,但广告选择器就位。这使他们可以通过广告选择器扩展请求,而无需实际执行任何操作。

然后,他们不仅仅是代理,而是在广告选择器服务中实施并推出了原生广告选择。现在,广告选择器将在内部处理请求,但仍充当代理并将请求传递给第三方,系统仍将使用第三方响应。

然后,他们添加了Event Logger来实现本机响应的日志记录,并设置Kafka。

他们继续构建其余的服务,从存根服务开始,并在此过程中添加逻辑。

最终,一旦一切就绪,他们就会切断第三方广告服务器。

在这些Go特性的帮助下,Go允许他们安全轻松地迁移到新的广告服务器:

Go编译器很快

支持跨平台编译

自包含二进制文件

强并发原语

教训2:Go使快速迭代变得简单而安全。

问题3:如何调试延迟问题?

部署新广告服务器后,他们确实看到了一些缓慢,网络故障,部署不良等问题。

如果你确切知道哪个服务有问题,pprof就很棒。另一方面,分布式跟踪使您可以查看服务。他们没有支持广告方面的分布式跟踪,但他们确实在Reddit的堆栈上的其他地方支持它。

为什么跟踪有用?

识别导致高总体延迟的热点

帮助发现其他错误/意外行为

跟踪通常很容易,你有一个客户端和服务器。在客户端,您提取跟踪标识符,并将它们注入您发送的服务器的请求中。在服务器端,当您获得请求和标识符时,将它们放入上下文对象并传递它们。使用HTTP和gRPC非常简单,没有理由不这样做。

但是,reddit正在处理Thrift,所以他们遇到了一些问题。

他们看了一下Thrift替代品,Facebook Thrift和Apache Thrift。他们正在寻找的两个关键功能是对标题和上下文对象的支持:

他们尝试使用FB thrift,但是存在一些问题,主要是缺少上下文对象,导致代码混乱和复杂化。在Apache thrift中,支持上下文对象,但它不支持头文件。因此,解决方案是:向Apache Thrift添加标头。这已经针对其他语言完成,但不适用于Go。因此,他们将THeader添加到Apache Thrift。这意味着现在支持上下文对象,并且头文件可以存储跟踪标识符。

如果您想查看这些更改,可以查看https://github.com/devalshah88/thrift。 Deval希望通过贡献流程获得更改并将其合并到上游。

这是一个跟踪代码。客户端包装器只从上下文对象中提取跟踪信息,并将其添加到headers:

服务器包装器从头部获取信息并将其注入上下文对象,以便它可以传递:

此代码来自https://github.com/devalshah88/thrift-tracing。

完成所有这些工作后,分布式跟踪被证明在调试延迟问题方面非常有用。然而,我们得到的结论是第三课:使用节俭和Go进行分布式跟踪是很困难的。

问题4:如何处理缓慢/超时?

在Reddit,他们希望系统能够优雅地处理缓慢。他们从不希望用户受到影响,因此如果速度缓慢,Reddit宁愿不展示广告也不愿降低用户体验。

他们的两个目标是:

不要让用户等待太久

不要浪费资源做不必要的工作

使用上下文对象来强制服务中的超时:这是来自充实服务的代码,用于向上下文对象添加截止日期,传递它,并在截止日期到期时提前退出。

这样的结果是好的,但还不够:

第一张图显示了从充实服务获得响应所需的时间。这个特定的时间框架有一些缓慢,但它没有让用户等待超过25毫秒。

第二个图表显示,在服务器端,增强服务正在处理最长70毫秒的请求,因此服务器在客户端已经超时并且在不再需要响应之后,有些浪费资源。

通常要做的是使用HTTP传播截止日期。此代码添加了一个超时,它通过上下文对象传递给服务器:

Thrift使这很难。这里没有使用上下文对象。如果客户端超时,goroutine不知道并且不退出:

这个方法不是特别好,但有办法来解决这个问题:

一种选择是为请求有效负载添加截止时间。客户需要在请求中包含截止日期。服务器会将截止日期注入上下文对象,并使用它。这并不是很好,因为必须在所有端点进行此更改。

相反,他们通过截止日期作为节俭标题。这与它们传递跟踪标识符的方式类似。在此更改之后,在服务器端,他们看到类似于客户端的延迟:

教训4:在服务内部和服务之间使用截止日期。

问题5:如何确保新功能不会降低性能?

快速迭代和复杂的业务逻辑可能导致性能问题。广告服务团队需要流程和工具,以确保他们能够快速移动而不会违反延迟SLA。为此,他们使用了负载测试和基准测试。

使用弯曲器进行负载测试:

这就是你从Bender那里得到的回应:

负载测试对于在重负载下测试更改非常有用,并且允许开发人员再推送到生产之前优化新功能以实现高负载。

他们还利用所有关键系统的基准测试。此基准测试代码:

获取此输出:

基准测试有助于:

通过改变减慢速度来防止降级

让您了解事情随时间的变化情况

告知开发人员有关不同实现存在的权衡

教训5:基准测试和负载测试很容易。做吧!

回顾:

使用框架/工具包

Go使快速迭代变得简单而安全

使用Thrift和Go进行分布式跟踪很难

在服务内部和跨服务使用截止日期

使用负载测试和基准测试

结论:

Go帮助reddit构建和扩展新的广告服务平台 - 易于构建和快速

我们分享了我们在此过程中学到的5个重要经验教训

尝试在下一个Go项目中至少使用其中一个

2
0
AI中国
创建时间:2019-08-22 17:24:09
我将分享我关注的人工智能资讯、采访报道、技术干货文章等
展开
购买事项

付费用户可享受文章永久阅读权限;

本课程为虚拟产品,付费后不可退换;

您拥有向专栏作者进行答疑的机会,专栏作者利用业余时间选择性回答;

作者

  • 格伯纳
    作者
戳我,来吐槽~