Twitter广告之“道”

译者的话

我是酥酥,目前在Tubi用Akka和Scala搭建AdServer,此前在Twitter做广告API的开发。我在从经济系毕业生到硅谷程序员一文中提到,在我离开Twitter前,Twitter的广告平台组刚开始风风火火地对AdServer进行微服务化重构,项目名称Project Tao(道)。如今一年多过去,Twitter完成了广告平台的重构,并用博文Accelerating ad product development at Twitter记录了这一过程,对从事AdServer工作的我很有启发。我根据自己有限的知识,对全文进行了翻译,并在局部进行了重写。

I'm Su Su. At present, I'm building AdServer with akka and scala in TUBI. Before that, I did the development of advertising API on twitter. In my article from an economics graduate to a silicon valley programmer, I mentioned that before I left twitter, the advertising platform group of twitter had just started to reconstruct AdServer as a micro service. The project name was project Tao. Now more than a year later, Twitter has completed the reconstruction of the advertising platform, and recorded this process with the blog accelerating ad product development at twitter, which is very enlightening for me who work in AdServer. According to my limited knowledge, I translated the full text and rewritten it locally.

康威定律指出,“组织结构决定系统结构”。推特广告团队也不例外。2010年,广告团队收入约为15名工程师和2800万美元。到2019年,团队将增加到数百名工程师,收入约为34亿美元。在此期间,推特广告系统的功能和复杂性不断增加,团队或服务的运营模式没有显著变化。

团队初期的系统结构适合创业公司,有助于抛弃以前的决策负担,迅速反复。广告后端大致分为三个团队。平台(Platform)、 (Science)、产品(Product)。团队的 终目标是迅速构建和反复广告产品,帮助推特获利。

Twitter的广告平台由基础设施和核心组件构成,其中核心组件又包括AdServer、计费(billing)、数据基础设施、分析(analytics)等系统。其中,单体AdServer(Monolithic AdServer)是广告平台的核心服务。它从Twitter客户端获取广告请求,根据用户信息和广告主的定向(targeting)条件,对广告池里的候选广告进行排名,筛选出 符合条件的广告,然后进行次价拍卖(second price auction), 后为用户发送 相关的广告作为响应。

在团队分工方面,平台团队负责构建可靠、高性能的单体AdServer,并承担部署和oncall的责任。该产品团队的重点是根据Twitter前端产品团队提出的要求,对AdServer进行修改。实际上,平台团队起到了把关的作用,对变更和服务进行了全面的控制。而在平台团队的把关下,产品团队在代码库的各个部分进行了修改。

基于这种模式,Twitter打造了多款成功的广告产品,它们的目标各不相同,复杂度越来越高。Promoted Tweets(推广推文)首先推出,它帮助品牌覆盖广大的受众,以提高品牌产品的知名度。Promoted Trend(推广趋势,类似于微博热搜)使得品牌能够占用全部Twitter用户的探索栏全天的时间。Mobile App Promotion(移动app推广)鼓励用户下载广告主所要推广的手机app。Web Clicks(网页点击)鼓励用户访问品牌网站。Promoted Video(推广视频)让品牌通过视频这种更容易讲故事的媒介来做推广,还支持对广告覆盖面和效果的衡量。

这种模式在小规模的工程师团队中效果非常好,迭代速度很快。但随着业务和团队的增长,平台团队开始成为功能迭代的瓶颈。为了更好地说明这一点,下面首先概述一个广告请求的流程。

广告请求的工作流

为了提供 相关的广告,我们在广告请求路径中使用以下两个组件:Ad Mixer和单体AdServer。

Ad Mixer是ad serving管道的网关(gateway)服务。每当收到一个来自客户端的广告请求,它都会:

·将请求转发给单体AdServer,并接收来自AdServer的候选广告响应。

·执行次价拍卖,根据拍卖结果决定将要展示的广告。

·储存被展示的广告的信息,根据用户与广告的交互行为,向广告商收费。(译者注:例如,如果用户浏览了广告,Twitter向广告商收取x元。如果用户浏览并下载了被推广的app,Twitter可能向广告商收取x+y元。)

单体AdServer负责对于每一个请求,考虑每一个符合条件的广告,并将 佳的候选广告返回给AdMixer。考虑到Twitter广告产品的规模,该服务是分片(sharded)的,这样每个分片只考虑全部候选广告的一个子集。这种分片方案还使并行计算成为可能,满足对广告请求延迟的严格要求(要求在200-500毫秒不等)。

Instagram 自动点赞

译者注:拆分前,广告请求的工作流。

译者注:在此需要简单了解广告的三级结构:广告组/campaign-广告计划/line item-广告创意/creative。一个广告组对应一到多个广告计划,一个广告计划对应一到多个广告创意。广告组描述一次推广,例如“Nike瑜伽装备2020秋季推广”。广告计划通常包含对目标群体的描述,如“男性推广”、“女性推广”和“儿童推广”,分别对应于三个目标群体。广告创意描述具体要展示的内容,可能是一段视频、一张海报、或一段文字。

每个AdServer分片,都根据它的候选广告子集,执行以下步骤。

·候选广告选择。我们为用户剔除不相关(不符合年龄、地区或兴趣等)的广告,筛选出符合条件的广告计划。(译者注:例如,若请求来自一个女性用户,则筛除有关“男性推广”的广告计划。)

·广告创意的扩展和产品逻辑。我们根据Twitter的产品规则(因产品种类而异),将每一个候选广告计划扩展为一组广告创意,并根据产品规则作进一步的过滤。(译者注:将候选广告扩展为广告创意的过程可能涉及实时竞价,即Real-time bidding,指AdServer根据广告计划的定向信息和用户信息,对广告交易所等第三方进行竞价请求,从第三方获取动态报价及创意信息。)

·候选广告排名(包括早期过滤)。我们在产品逻辑阶段后,根据用户点击广告的可能性(调用实时训练的机器学习模型)、广告商的竞价信息和拍卖特征,对候选广告进行评分和排名。

从2010年到2018年,AdServer的逻辑日渐复杂。结果是:

·每次部署有包含数百个改动;(译者注:Twitter出于各项考虑,没有使用持续部署的方式,因此一次部署包含很多改动)

·虽然平台团队管理着AdServer,但AdServer的主要改动却是来自产品团队,导致两个团队“互为掣肘”。

此外,AdServer的几个步骤之间没有划定明确的边界,缺乏有效的机制来确保属于"广告创意的扩展和产品逻辑"的代码不被加入到"候选广告选择"或"候选广告排名"中。不清晰的边界还给产品团队的工程师(译者注:以下简称产品工程师)带来了额外的复杂性:单体AdServer本就是一个黑盒子,组件间强耦合,缺了哪一块,AdServer都无法正常工作。产品工程师只了解AdServer的局部,很难测试。有时,一个很简单的改动从开始开发到部署至生产中需要一个月的时间。

Instagram 自动赞

译者注:单体AdServer的逻辑复杂。

广告产品功能的生命周期

在2018年,基于上述平台团队和产品团队的结构,一个视频广告产品功能的生命周期如下:

instagram 头像 下载

代码考古

AdServer代码的复杂逻辑与基础架构组件之间缺乏清晰的应用编程接口边界,AdServer代码的复杂性指数爆炸。产品工程师首先需要了解复杂的平台组件和产品组件,并尝试了解当前AdServer是如何工作的。

设计

随后,产品工程师将明确:需要对系统进行哪些改动?这里的挑战包括:产品工程师难以完全理解视频产品的改动对运行在同一单片AdServer中的其他广告产品的影响。此外,他们也很难估计改动对性能的影响。

咨询平台团队

一旦设计准备就绪,产品团队的下一步就是与平台团队对接,平台团队负责提供指导和代码审核。但是现实中,由于平台团队负责平台,产品团队负责产品,两个团队的激励并不总是一致的,导致平台争论甚至拒绝很多产品要求,两个团队都感到失望和沮丧。

发布

一旦代码通过审查,被合并,产品团队需要再次依靠平台团队进行部署。新功能的测试往往十分困难,有时一个部署要包括多达200个改动。此外,由于多个产品的逻辑运行在同一个强耦合的单体AdServer中,一个错误的改动可能阻塞整个部署,从而影响到其它产品团队。 终的测试要到发布后才能完成,且需要不同团队的参与,因此修复突发bug的时间难以预测。

解决方案

引入产品竞价器,加速产品迭代

我们从多个方面解决这些问题。我们首先将服务于不同广告产品的基础设施组件分离出来(例如选择服务Selection Service,排名服务Ranking Service等,更多信息见我们之前的博文)。此外,提高产品团队迭代速度的关键在于引入产品竞价器(Product Bidder),并确保不同产品的竞价器共用一个框架,以保持乘法效应。(译者注:将拆分后的AdServer命名为bidder,可能因为竞价是AdServer的一个重要环节,不同产品之间的竞价逻辑可能差异较大。)

Instagram 自动点赞

译者注:拆分后,广告请求的工作流。

将单体服务的每个模块都拆分为一个微服务看似会大大提高产品迭代效率,但这也使得跨服务改动经常发生,降低开发效率。因此,在拆分过程中,我们做了适当的取舍:

1.将单个AdServer分成不同的“产品竞价器”服务,负责返回每个产品品种的“ 佳”广告。比如视频竞价器只负责返回 好的视频广告,手机应用推广竞价器只负责返回 好的手机应用推广广告。所以每个竞价器都可以有自己的部署节奏,oncall,性能和运营特点。

2.将AdServer代码库拆分成竞价器框架和产品专用逻辑,每个竞价器的实现是竞价器框架和产品逻辑的可配置组合。

instagram 网页 版

译者注:拆分后的AdServer流水线。

竞价器框架

如上图所示,每个管道(pipeline)都包含Filter和Enricher等多个步骤。竞价器框架规定了这条流水线的结构和执行机制,而每个产品竞价器可以根据具体产品需求做定制化执行。

该方法解耦了服务和产品,使得平台团队只负责投标框架,产品团队负责投标框架的具体实现。它 大限度地提高了开发的灵活性和速度,同时保持了乘数效应。例如,框架升级将同时作用于所有产品投标人。所有产品投标人也可以共享一套分析数据采集机制。

各产品竞价器相互独立,使工程师能够更改特定产品(如视频广告),而完全不影响与视频广告无关的产品。举例来说,如果新的视频功能需要额外的计算资源,视频产品竞价器可以增加其允许延迟,而不会改变其他广告产品的行为。

数据依赖框架

(译者注:本段大量根据自己的理解写成,可能与原文有出入)在AdServer的整条流水线中,每个步骤都涉及对通用数据结构的直接改动,这使得我们很难找出数据具体是在哪一个步骤被修改的。我们开发了一套数据依赖框架,严格规定了每个步骤的输入输出格式,禁止对通用数据结构的直接改动,而需要通过immutable的方式做改动,这样能使每个步骤的职责更为清晰。

分片服务的数据获取与传输

instagram 网页 版

译者注:在Ad Mixer处进行数据抓取。

1.对于不需要类型检查的数据,我们实现了直通(passthrough),中间服务只负责将原始字节传输到指定的下游服务,而不需要知道数据的格式和内容。(译者注:直通避免了序列化,降低数据传输成本。)

2.对于需要类型检查的数据,我们对其进行分类,并将其标记为消费者,以避免泄露包装,并确保下游服务不会错误使用数据。(译者注:如果将某些数据的消费者标记为视频广告投标者,则手机app推广广告投标者的逻辑将不会消耗该数据。

挑战与经验

可靠的逐步迁移

在我们进行拆分之前,Twitter的单体AdServer上运行着价值30亿美元的广告业务。因此,在做拆分的同时,我们必须保持现有业务的运行,以及支持正在进行的产品开发。

在项目过程中,我们进行了广泛的A/B实验,新系统支持少量流量,原系统支持大部分流量,详细分析产品指标,确保两个系统在收入和各项指标上基本一致,不影响正常广告业务

在长达4周的时间里,我们逐渐为新系统增加了流量比例。此外,我们还为每个产品竞价器部署了约20%的额外容量,并启用了保证在30秒内完成滚动的机制。 后,我们在长达一个季度的时间里,在以前的单个AdServer上运行小流量,以研究季节性对各种指标的影响。

代码复杂度

拆分必然需要面对非常复杂的古代代码,要搞清楚这些几年没人碰过的代码在做什么。我们努力将数据处理逻辑分类为核心组件,如候选广告选择、候选广告排名或预算计算、频率管理。在技术债务的泥潭里,做这些改动并不容易。

揭露潜藏的bug

在过去的八年里,团队一直在对AdServer进行渐进的修改,这样做的缺点是很难发现大bug。这次拆分对系统进行了根本性的改变,帮助我们发现软件中隐藏的bug。这些bug的发现、诊断和修复花费了我们一个季度的时间,但是这使得我们的系统更加健康。

权衡取舍

任何解决方案都不是万能的,我们的团队充分认识到这个决定的风险和收益,做出以下的“舍”:

失去对候选广告做全局排名的能力

当我们将单体AdServer垂直拆分为多个产品竞价器时,每个产品竞价器都会分别给候选广告排名。从而,我们失去了在AdServe内以接近全局的方式对所有候选广告进行排名的能力。我们清楚利弊,决定通过增加 终拍卖所考虑的候选广告的数量来弥补局部决策带来的损失,以确保我们不会过滤掉原本有资格的候选广告。(译者注:假设有两种广告产品,视频广告和卡片广告,二者分别有10个候选广告。假定全局 优的5个广告中,有4个视频广告和1个卡片广告。那么,如果我们在局部排名中,分别对视频广告和卡片广告取前3名,汇总后再进行全局排名,则我们错误地过滤掉了一条视频广告。Twitter可能通过对二者各取前5名的方式来减少这种情况的发生。)

下游服务的流量增大

考虑到AdServer的规模,我们必选在项目开始时就估计好服务的流量情况,并提前6个月告知基础设施团队。我们使用一些原型(prototype)以及大量的压力测试来确保估计的准确。

AdServer从单个AdServer改为多产品竞价架构后,对直接下游服务的要求增加,从而增加了所有下游服务的压力和成本。鉴于此,我们仔细选择了要分割的垂直产品领域,以获得 大的投资回报。

发布平台改动的发布周期变长

把AdServer按产品切分还意味着每一个平台层面的改动都要在所有产品上完成测试和发布。我们清楚认识到、并且愿意承担这个成本。

即使存在上述不足,在竞价机架构正式上线之后,我们仍然可以看到产品隔离的好处。举例来说,我们通过在Promoted上选择服务来配置缓存,从而实现延迟的减少,而完全不影响其他产品的行为。同时,我们也看到产品功能的迭代不再被平台团队所阻挡,解决了一大难题。

总结

所有定律都有局限性,康威定律也不例外。它适用于所有的组织,但我们也可以共同努力打破它,确保服务和组织都适合未来的需求,而不是受制于历史。切分并不能解决所有问题,但对团队以及服务确实有积极的影响,其结果是产品工程师在新架构中添加新功能的工作流程得到了简化。当我们开始这个项目的时候,这似乎是一个几乎不可能完成的任务,但回过头来看,它不仅帮助我们搭建了更健康的系统,还加深了对系统工作方式的理解,为实现Twitter广告部门的下一个宏伟目标奠定了基础。

All laws have limitations, and Conway's law is no exception. It applies to all organizations, but we can also work together to break it and ensure that services and organizations are suited to the needs of the future, rather than constrained by history. Segmentation doesn't solve all the problems, but it does have a positive impact on the team and services. As a result, the workflow of product engineers adding new functions to the new architecture is simplified. When we started this project, it seemed to be an almost impossible task, but looking back, it not only helped us build a healthier system, but also deepened our understanding of how the system works, laying the foundation for achieving the next grand goal of twitter advertising department.