软件系统的一大优点是它们具有极强的适应性。然而,在复杂软件系统的演进过程中,这种可塑性会阻碍而不是促进其发展。在某种程度上,软件将进入一个不再服务于其目的——为人们提供帮助——的阶段。
这就是 2019 年初 Twitter AdServer 的情况。经过 10 年的迭代开发之后,系统的效率已经太低,无法与组织的发展保持同步。刚开始的时候,我们是一个非常小的工程师团队,只提供单一类型的广告格式( 推广推文),创造了大约 2800 万美元的收入。如今,Twitter 的收入组织包括 10 倍以上的工程师和 约30亿美元的收入,支持多种广告格式——品牌、视频、卡片。
新产品发布慢,团队之间紧密依赖,管理成本很高,这些都增加了组织的复杂性。为了进一步扩大规模,我们就得进行根本性的改革。
我们是如何投放广告的?
AdServer 漏斗(funnel)主要由 Admixer 和 Adshard 组成。Admixer 是上游客户端和 AdServer 管道之间的接口。当 Admixer 收到一个广告请求时,在将请求分发给 Adshard 之前,它会将额外的用户信息补充到请求中。Adshard 在分片架构下运行,每个 Adshard 负责一个广告子集,通过 AdServer 漏斗的 3 个主要阶段运行请求:
候选项选择 :为用户选择一组有效的活动子项(即一组具有相同目标市场选择标准的广告;有关详细信息,请参考 活动子项定义)。在这一阶段,(1)我们应用了所有标准的活动目标选择条件,最终为用户提供符合条件的广告(例如地理位置、年龄、兴趣等);(2)剔除用户可能不喜欢的不相关的广告;(3)确保我们只提供有效的广告(例如,只提供没有结束的活动)。
产品逻辑 :此阶段根据一些业务规则将来自候选项选择阶段的每个活动子项扩展为一组创意,并添加额外的产品特性丰富这些创意。(创意是实际展示给用户的广告,例如一条推广推文——请参考 创意定义了解更多细节)。
候选项排名 :完成上述阶段后,对广告进行排名。每个广告都会得到一个分数(表示用户浏览该广告的可能性),并根据这个分数对广告进行排名。我们使用一些实时训练的机器学习模型和广告客户数据来计算我们在竞价管道中使用的分数。
在这个漏斗中,不同的组件还附加了与广告请求和广告候选相关联的元数据,并会将这些元数据写入 AdMixer 中我们的底层键值存储中。稍后,在反馈循环中,分析管道将使用这些数据,用于账单、欺诈检测和其他分析。
过去,我们是通过尽可能减少网络跳数来优化系统,以最小化延迟和操作开销。这导致单个服务(即 Adshard)完成了大部分繁重的工作,进而形成了一个单体模型。
熵增
当 Twitter 只有两种广告产品——推广推文和推广账户时,这个单体平台运行得很好。然而,当我们扩大业务时,单体模式带来的挑战便多于解决方案了。
新增一个广告产品
在旧的 Adserver 中,由于遗留代码的挑战和复杂性,重用现有模式和实践就成了常态。上图是一个在旧的 AdServer 上新增一个广告产品(如推广趋势)的例子。该广告产品具有以下特点:
应该总是根据条件 Geo == Country 选取;
应该不需要竞价,从而可以跳过排名阶段。
通常,新增一个广告产品需要做一些零零碎碎的工作。考虑到现有框架的性质和其他遗留代码的约束,跳过排名阶段不是可行的选项,于是我们采用了一种不合常规的变通方法,在排名管道里向代码中添加基于产品的条件逻辑 if ( product_type == ‘PROMOTED_TREND’ ) {…} else {…}。这种基于产品的逻辑也存在于选择管道中,导致了这些阶段紧密耦合,增加了日益增多的意大利面式代码的复杂性。
开发速度
下面是所有基于大量的遗留代码进行开发的团队都面临的一些挑战。
过度膨胀的数据结构 :请求和响应对象的大小随着业务逻辑的增加而快速增长。由于请求/响应对象在这 3 个阶段中共享,所以 不变性保证是一项挑战。在候选排名阶段添加一个新特性,需要了解该特性所需的字段在上游(选择和创意阶段)和下游(Admixer)是在何处如何设置的。要想修改的话,就几乎需要了解整个服务管道。这是一个令人畏缩的过程,尤其是对新工程师来说。
数据访问挑战 :从历史上看,Admixer 一直是负责获取用户相关数据的服务,这主要是为了延迟和资源优化。(由于采用分片架构,在 Adshard 中获取相同的用户数据需要 25x RPC)。因此,要在 Adshard 中使用一个新属性,我们需要在 Admixer 中添加相应的用户数据获取器,并将其发送给 Adshard。这个过程非常耗时,并且,取决于用户属性的类型,可能会对 AdServer 的性能产生影响。这个过程也使得解耦平台与产品变得非常具有挑战性。
技术债务 :复杂的遗留代码增加了 技术债务。弃用旧字段以及清理未使用代码的风险越来越大。这通常会导致功能的意外更改,引入 bug 并拉低整体生产力。
解决方案:我们如何设计这些服务
这些长期存在的工程问题以及开发人员的生产力损失,使得我们需要改变系统设计的范式。我们在架构中缺乏明确的 关注点分离,并且不同的产品领域之间高度耦合。
在软件行业中,这些问题相当常见,而将单体分解成微服务是解决这些问题的流行方法。然而,它本身也是有利有弊,如果仓促设计,反而会导致生产率降低。让我们通过一个例子看下分解服务时可能采用的一种方法。
服务分解思考练习:每个产品一个 AdServer
由于单体 AdServer 对每个产品团队而言都是一个瓶颈,而不同的产品可能有不同的架构需求,所以我们可以选择将单个 AdServer 分解为 N 个不同的 AdServer,每个产品一个,或者一组类似的产品一个。
在上面的架构中,我们有三个不同的 AdServer,分别用于 Video Ad Product、Takeover Ad Product 和 Performance Ad Product。它们由各自的产品工程团队负责,每个团队都有自己的代码库和部署管道。这似乎提供了自主性,并有助于分离关注点,解耦不同的产品领域,然而,实际上,这样的分离可能会使事情变得更糟。
现在,每个产品工程团队都必须增加人手来维护整个 AdServer。每个团队都必须维护和运行自己的候选生成和候选排名管道,即使他们很少修改它们(这些通常是由机器学习领域专家负责修改)。对于这些领域,情况变得更糟。现在,要发布一个用于广告预测的新特性,我们需要修改三个不同服务的代码,而不是一个!最后,很难确保来自所有 AdServer 的分析数据和日志能够融合到一起,以确保下游系统的正常运行(分析是跨产品的横切关注点)。
经验总结
我们认识到,仅仅分解是不够的。我们在上面为每个产品构建的 AdServer 架构既缺少 内聚性(每个 AdServer 仍然做了太多的事情),也缺少 可重用性(例如,在所有三个服务中都运行着的广告候选排名)。我们突然认识到,如果我们要为产品工程团队提供自主性,就必须用可以跨产品重用的横向平台组件来为他们提供支持!为横切关注点提供即插即用的服务可以 为工程团队创造乘数效应。
我们构建了横向平台组件
因此,我们确定了可以被大多数广告产品直接使用的“通用广告技术功能”,包括:
候选项选择 :给定用户属性,确定可以针对用户需求展开竞逐的广告候选项。
候选项排名 :给定用户属性和广告候选项,根据与用户的相关性给广告候选项打分。
回调和分析 :定义契约,标准化所有提供广告服务的服务的分析数据集。
我们围绕这些功能构建服务,并将自己重组为平台团队,每个团队拥有其中一个功能。以前架构中的产品 AdServer 现在变成了更精简的组件,它们依赖于横向平台组件,并在其上构建特定于产品的逻辑。
好处
便于添加新产品
让我们重新审视上面提到的与聚光灯广告有关的问题,以及新架构如何处理这个问题。通过构建不同的广告候选项选择服务和广告候选项排名服务,我们可以更好地将关注点分离开来。它打破了广告产品必须采用 AdServer 管道的 3 阶段范式这一模式。现在,聚光灯广告有了灵活性,可以只与选择服务集成,使得这些广告可以跳过排名阶段。这让我们摆脱了为绕过推广趋势广告排名而采用的笨拙方法,实现了一个更干净、更健壮的代码库。
随着广告业务的持续增长,添加新产品将会很容易,只要在需要的时候引入这些横向平台服务就可以了。
提升速度
通过定义良好的 API,我们可以在团队之间实现职责分离。修改候选项排名管道不需要理解选择或创意阶段。这是一种双赢的局面,每个团队只需要理解和维护他们自己的代码,这让他们可以更快地采取行动。这也使得故障更加容易诊断,因为我们可以隔离服务中的问题并独立地测试它们。
风险与利弊
在 Twitter,这种广告模式的转变必然会伴随着风险和权衡。我们想列出其中一些,以提醒读者,在决定对现有系统进行大规模重构之前,必须识别和承认存在的弊端。
增加硬件成本 :从一个服务创建许多不同的服务无疑意味着增加运行这些系统的计算成本。为了确保增长在可接受的范围内,我们为自己设定了一个具体目标,将广告服务系统的运营成本控制在收入的 5%以内。这有助于我们在需要的时候优先考虑效率,让我们更容易做出设计决策。就计算资源而言,新架构的开销大约是前一个架构的两倍,但这在我们可以接受的限度之内。
增加了产品开发团队的运营成本 :拥有多个新服务意味着维护和运营这些服务的工程成本,其中一些新增的负担落在了产品开发团队身上(而不是像之前那样更多地落在平台团队身上)。这意味着除了要加速开发新特性外,产品开发团队还需要适当地成长以支持他们拥有的新系统。
新特性开发的暂时放缓 :这项工作需要花费超过 40 名工程师以及工程和产品经理 1.5 年的时间。我们估计,在此期间,新特性的开发速度会降低大约 15%(主要是在广告服务方面)。为了支持这个项目,组织负责人会愿意做出这样的权衡。
竞价排名的复杂性增加 :这是对新架构的技术考量——由于每个产品负责人都服务于自己的请求,我们部分失去了在更低粒度上对广告排名和竞价做出全局最优决策的能力。通过将这种逻辑转移到更高粒度的集中式平台服务上,可以在某种程度上弥补这一点。
我们评估了这些风险,并且确定,新架构的好处大于这些风险造成的影响。整体开发速度的提高和更可预测的特性改进交付,对于我们为自己设定的雄心勃勃的业务目标至关重要。新架构提供了一个模块化系统,让我们可以更快的试验,并降低了耦合度。
我们已经开始看到这种决策的好处了:
对于大多数规划好的项目,没有一个团队会成为瓶颈,而在规划好的广告服务项目中,90%以上的都可以执行。
试验速度快了许多——在广告服务空间进行的在线排名试验现在快了 50%。
迁移
多个团队每天都推送新代码这样一个部署节奏,再加上数十万 QPS 的庞大规模,使得 AdServer 的分解非常具有挑战性。
在开始迁移时,我们采用了内存内 API 优先的方法,对代码进行逻辑分离。另外,这还使我们能够运行一些初始的系统性能分析,保证与旧系统相比,CPU 和内存占用的增量是可接受的。这奠定了横向平台服务的基础,这些基本服务源自重构代码并重新安排内存版本的打包结构。
为了确保新旧服务在功能上的一致性,我们开发了一个自定义的正确性评估框架。它分别针对旧 AdServer 和新 AdServer 重放了请求,以便在可接受的阈值内比较两个系统的指标。我们在离线测试中使用了这种方法,借此我们可以了解新系统的性能。它帮助我们及早发现问题,防止错误进入生产环境。
在将代码发送到生产环境后,我们使用了一个试验框架,让我们可以洞察生产环境中的总体收益指标。许多预测和竞价相关的度量标准需要一个更长的反馈循环来消除噪音和评估变更的真实影响。因此,对于迁移的真正的端到端验证,我们依赖这个框架来保证收入指标的正常。
总结
分解 AdServer 改善了我们系统的状态,强化了 Twitter 广告业务的基础,让我们可以把时间和资源集中在解决真正的工程问题上,而不是与遗留基础设施的问题作斗争。随着广告业务和技术的发展,更多的挑战将会到来,但我们很高兴能够建立可以提高系统效率的解决方案。
如果你对解决这些挑战感兴趣,可以考虑 加入这个团队。
查看英文原文: Building Twitter’s ad platform architecture for the future
评论