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