Twitter 广告之"道"
译者的话
我在从经济系毕业生到硅谷程序员一文中提到,在我离开 Twitter 前,Twitter 的广告平台组刚开始风风火火地对 AdServer(广告服务器)进行微服务化重构,项目名称Project Tao(道)。如今一年多过去,Twitter 完成了广告平台的重构,并用博文Accelerating ad product development at Twitter记录了这一过程,对从事 AdServer 工作的我很有启发。我根据自己有限的知识,对全文进行了翻译,并在局部进行了重写。
康威定律指出,"组织的架构决定系统的架构"。Twitter 广告团队也不例外。2010 年,广告团队有约 15 名工程师和 2800 万美元的收入,到了 2019 年,团队增长至数百名工程师,有约 34 亿美元的收入。期间,Twitter 广告系统的功能和复杂性一直在增加,而团队或服务的运营模式并没有大幅改变。
团队早期的系统架构很适合初创公司,有利于抛下之前的决策包袱,快速迭代。广告后端大致分为三个团队。平台(Platform)、科学(Science)和产品(Product)。团队的最终目标是:快速构建和迭代广告产品,帮助 Twitter 实现盈利。
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 毫秒不等)。
配图:拆分前,广告请求的工作流。
译者注:在此需要简单了解广告的三级结构:广告组/campaign - 广告计划/line item - 广告创意/creative。一个广告组对应一到多个广告计划,一个广告计划对应一到多个广告创意。广告组描述一次推广,例如“Nike 瑜伽装备 2020 秋季推广”。广告计划通常包含对目标群体的描述,如“男性推广”、“女性推广”和“儿童推广”,分别对应于三个目标群体。广告创意描述具体要展示的内容,可能是一段视频、一张海报、或一段文字。
每个 AdServer 分片,都根据它的候选广告子集,执行以下步骤。
- 候选广告选择。我们为用户剔除不相关(不符合年龄、地区或兴趣等)的广告,筛选出符合条件的广告计划。(译者注:例如,若请求来自一个女性用户,则筛除有关“男性推广”的广告计划。)
- 广告创意的扩展和产品逻辑。我们根据 Twitter 的产品规则(因产品种类而异),将每一个候选广告计划扩展为一组广告创意,并根据产品规则作进一步的过滤。(译者注:将候选广告扩展为广告创意的过程可能涉及实时竞价,即 Real-time bidding,指 AdServer 根据广告计划的定向信息和用户信息,对广告交易所等第三方进行竞价请求,从第三方获取动态报价及创意信息。)
- 候选广告排名(包括早期过滤)。我们在产品逻辑阶段后,根据用户点击广告的可能性(调用实时训练的机器学习模型)、广告商的竞价信息和拍卖特征,对候选广告进行评分和排名。
从 2010 年到 2018 年,AdServer 的逻辑日渐复杂。结果是:
- 每次部署有包含数百个改动;(译者注:Twitter 出于各项考虑,没有使用持续部署的方式,因此一次部署包含很多改动)
- 虽然平台团队管理着 AdServer,但 AdServer 的主要改动却是来自产品团队,导致两个团队“互为掣肘”。
此外,AdServer 的几个步骤之间没有划定明确的边界,缺乏有效的机制来确保属于"广告创意的扩展和产品逻辑"的代码不被加入到"候选广告选择"或"候选广告排名"中。不清晰的边界还给产品团队的工程师(译者注:以下简称产品工程师)带来了额外的复杂性:单体 AdServer 本就是一个黑盒子,组件间强耦合,缺了哪一块,AdServer 都无法正常工作。产品工程师只了解 AdServer 的局部,很难测试。有时,一个很简单的改动从开始开发到部署至生产中需要一个月的时间。
译者注:单体 AdServer 的逻辑很复杂。
广告产品功能的生命周期
在 2018 年,基于上述平台团队和产品团队的结构,一个视频广告产品功能的生命周期如下:
代码考古
由于产品和平台的耦合,业务逻辑和基础架构组件之间缺乏清晰的 API 边界,AdServer 代码的复杂度指数爆炸。产品工程师首先需要理解错综复杂的平台组件和产品组件,并试图弄清楚当前 AdServer 是怎样工作的。
设计
随后,产品工程师将明确:需要对系统进行哪些改动?这里的挑战包括:产品工程师难以完全理解视频产品的改动对运行在同一单片 AdServer 中的其他广告产品的影响。此外,他们也很难估计改动对性能的影响。
咨询平台团队
一旦设计准备就绪,产品团队的下一步将是与平台团队对接,由平台团队负责提供指导和代码审查。然而现实中,由于平台团队负责平台,而产品团队负责产品,两个团队的激励并不总是一致的,这导致产品的很多请求被平台争论甚至驳回,两个团队都感到失望和沮丧。
发布
一旦代码通过审查,被合并,产品团队需要再次依靠平台团队进行部署。新功能的测试往往十分困难,有时一个部署要包括多达 200 个改动。此外,由于多个产品的逻辑运行在同一个强耦合的单体 AdServer 中,一个错误的改动可能阻塞整个部署,从而影响到其它产品团队。 最终的测试要到发布后才能完成,且需要不同团队的参与,因此修复突发 bug 的时间难以预测。
解决方案
引入产品竞价器,加速产品迭代
我们从多个方面解决这些问题。我们首先将服务于不同广告产品的基础设施组件分离出来(例如选择服务 Selection Service,排名服务 Ranking Service 等,更多信息见我们之前的博文)。此外,提高产品团队迭代速度的关键在于引入产品竞价器(Product Bidder),并确保不同产品的竞价器共用一个框架,以保持乘法效应。(译者注:将拆分后的 AdServer 命名为 bidder,可能因为竞价是 AdServer 的一个重要环节,不同产品之间的竞价逻辑可能差异较大。)
译者注:拆分后,广告请求的工作流。
将单体服务的每个模块都拆分为一个微服务看似会大大提高产品迭代效率,但这也使得跨服务改动经常发生,降低开发效率。因此,在拆分过程中,我们做了适当的取舍:
- 将单体 AdServer 拆分成不同的"产品竞价器"服务,负责返回每个产品品种的"最佳"广告。例如,视频竞价器只负责返回最佳的视频广告,手机 app 推广竞价器只负责返回最好的手机 app 推广广告。因此,每个竞价器可以有自己的部署节奏、oncall、性能和运营特点。
- 将 AdServer 代码库拆分成竞价器框架和产品专用逻辑,每个竞价器的实现是竞价器框架和产品逻辑的可配置组合。
译者注:平台和产品组分别负责的模块。
竞价器框架
如上图所示,每个管道(pipeline)都包含 Filter 和 Enricher 等多个步骤。竞价器框架规定了这条流水线的结构和执行机制,而每个产品竞价器可以根据具体产品需求做定制化执行。
这种方法解耦了服务与产品,从而使平台团队只负责竞价器框架,产品团队负责竞价器的具体实现。它最大程度地提高了开发的敏捷性和速度,同时保留了乘数效应。例如,框架升级会同时作用于所有产品竞价器;所有产品竞价器还能共享一套分析数据采集机制。
不同产品竞价器之间的相互独立,使得工程师可以对特定产品(如视频广告)进行更改,而完全不影响与视频广告不相关的产品。例如,如果一个新的视频功能需要额外的计算资源,视频产品竞价器可以增大其允许延时,而不改变其它广告产品的行为。
数据依赖框架
(译者注:本段大量根据自己的理解写成,可能与原文有出入)在 AdServer 的整条流水线中,每个步骤都涉及对通用数据结构的直接改动,这使得我们很难找出数据具体是在哪一个步骤被修改的。我们开发了一套数据依赖框架,严格规定了每个步骤的输入输出格式,禁止对通用数据结构的直接改动,而需要通过 immutable 的方式做改动,这样能使每个步骤的职责更为清晰。
分片服务的数据获取与传输
译者注:在 Ad Mixer 处进行数据抓取。
在原有系统中,Ad Mixer 在每次广告请求中,都会一次性获取所需数据(比如用户的个人信息、兴趣、活动等),并将数据传递给各个分片 AdServer。在拆分后,每个竞价器需要不同的数据源,然而我们希望避免每个拆分的服务都各自进行数据获取,因为这可能会使数据的获取量增加 400 倍。我们选择仍然在 Ad Mixer 处做一次性数据获取,并使用以下方法确保竞价器高效地获取各自所需数据:
- 对于不需要类型检查的数据,我们实现了直通(passthrough),中间服务只负责将原始字节传输到指定的下游服务,而不需要知道数据的格式和内容。(译者注:直通避免了序列化,降低数据传输成本。)
- 对于需要进行类型检查的数据,我们对其进行分类,并标记它们的消费者,以避免泄漏封装,并确保下游服务不会错误地使用数据。(译者注:若将某数据的消费者标记为视频广告竞价器,则手机 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 Trend 竞价器中的选择服务配置一个缓存,实现延迟的降低,而完全不影响其它产品的行为。我们还看到产品功能的迭代不再被平台团队所阻塞,解决了一个主要的痛点。
总结
所有定律都有局限性,康威定律也不例外。它适用于所有的组织,但我们也可以共同努力打破它,确保服务和组织都适合未来的需求,而不是受制于历史。切分并不能解决所有问题,但对团队以及服务确实有积极的影响,其结果是产品工程师在新架构中添加新功能的工作流程得到了简化。当我们开始这个项目的时候,这似乎是一个几乎不可能完成的任务,但回过头来看,它不仅帮助我们搭建了更健康的系统,还加深了对系统工作方式的理解,为实现 Twitter 广告部门的下一个宏伟目标奠定了基础。
Feedback is a gift! Please send your feedback via email or Twitter.
© Yik San Chan. Built with Vercel and Nextra.