美团外卖广告平台化的探索与实践
总第491篇
2022年 第008篇
随着美团外卖业务不断发展,外卖广告引擎团队在多个领域进行了工程上的探索和实践,目前已经取得了一些成果。我们计划通过连载的形式分享给大家,本文是《美团外卖广告工程实践》专题连载的第一篇。
本文针对业务提效的目标,介绍了美团外卖广告引擎在平台化过程中的一些思考和实践。我们围绕实际遇到的问题、思考过程以及具体的落地方案,从业务的标准化、技术框架、产研新流程的改造等三个方面进行展开,希望能为读者提供思路上的借鉴。1 前言
2 现状分析
3 目标
4 整体设计
4.1 整体思想
4.2 业务标准化
4.3 技术框架
4.4 产研新流程
5 效果
6 总结与展望
7 作者简介
招聘信息
1 前言
2 现状分析
业务逻辑复用度低:广告业务逻辑比较复杂,比如机制服务模块,它主要功能是为广告的控制中枢以及广告的出价和排序的机制提供决策,线上支持十几个业务场景,每种场景都存在很多差异,比如会涉及多种召回、计费模式、排序方案、出价机制、预算控制等等。此外,还有大量业务自定义的逻辑,由于相关逻辑是算法和业务迭代的重点,因此开发人员较多,并且分布在不同的工程和策略组内,导致业务逻辑抽象粒度标准不够统一,使得不同场景不同业务之间复用程度较低。 学习成本高:由于代码复杂,新同学熟悉代码成本较高,上手较难。此外,线上服务很早就进行了微服务改造,线上模块数量超过20个,由于历史原因,导致多个不同模块使用的框架差异较大,不同模块之间的开发有一定的学习成本。在跨模块的项目开发中,一位同学很难独立完成,这使得人员效率没有得到充分利用。 PM(产品经理)信息获取难:由于目前业务场景较多、逻辑复杂,对于信息的获取,绝大多数同学很难了解业务的所有逻辑。PM在产品设计阶段需要确认相关逻辑时,只能让研发同学先查看代码,再进行逻辑的确认,信息获取较难。此外,由于PM对相关模块的设计逻辑不清楚,往往还需要通过找研发人员线下进行询问,影响双方的工作效率。 QA(测试)评估难:QA在功能范围评估时,完全依赖于研发同学的技术方案,且大多数也是通过沟通来确认功能改动涉及的范围和边界,在影响效率的同时,还很容易出现“漏测”的问题。
3 目标
提升产研效率
高功能复用度,提升开发效率。 降低研发人员(RD)、PM、QA之间的协作成本,提升产研协作的效率。
提升交付质量
精确QA测试的范围,提升交付的质量。 对业务进行赋能。
PM可通过可视化的平台化页面,了解其他产品线的能力,互相赋能,助力产品迭代。
4 整体设计
4.1 整体思想
业务能力标准化:通过对现有逻辑的梳理,进行标准化的改造,为多业务场景、多模块代码复用提供基础保证。 技术能力框架化:提供组合编排能力将标准化的逻辑串联起来,通过引擎调度执行,同时完成了可视化能力的透出,帮助用户快速获取信息。 平台化产研新流程:为保证项目上线之后实现研发迭代的整体提效,我们对于研发流程的一些机制也进行了一些优化,主要涉及研发人员、PM、QA三方。
4.2 业务标准化
4.2.1 业务场景与流程分析
不同业务处在不同的发展阶段,也有着不同的迭代节奏。 组织结构天然存在“隔离”,如推荐和搜索业务分在两个不同的业务小组。
4.2.2 标准化建设
在个体开发层面:开发同学不用关注如何流程调度,只需将重心放在新功能的实现上,开发效率变得更高。 从系统整体角度:各个服务对于通用的功能不用再重复开发,整体的复用程度更高,节省了大量的开发时间。
所有业务线统一共建的标准化形式是进行双层抽象。对于单个的、简单的功能点,抽象为工具层;对于可独立实现并部署的某一方面功能,比如创意能力,抽象为组件层。工具层和组件层统一以JAR包的形式对外提供服务,所有工程都通过引用统一的JAR包来使用相关的功能,避免重复的建设,如下图所示:
业务逻辑相关的功能是此次标准化建设的核心,目标是做到最大程度的业务复用。因此,我们将最小不可拆分的业务逻辑单元抽象为业务同学开发的基本单位,称为Action。同时根据Action不同的复用范围,将其划分为三层,分别是所有业务可以复用的基础Action,多业务线复用的模块Action,具体单一业务定制的业务Action,亦即扩展点。所有的Action都是从Base Action派生出来的,Base Action里定义了所有Action统一的基础能力。 不同的Action类型分别由不同类型的开发同学来开发。对于影响范围比较大的基础Action和模块Action,由工程经验丰富的同学来开发;对于仅影响单个业务的业务Action或扩展点,由工程能力相对薄弱的同学来进行开发。 同时我们把多个Action的组合,抽象为Stage,它是不同Action组合形成的业务模块,目的在于屏蔽细节,简化业务逻辑流程图的复杂度,并提供更粗粒度的复用能力。
每个Action执行都需要一定的环境依赖,这些依赖包括输入依赖、配置依赖、环境参数、对其他Action的执行状态的依赖等。我们将前三类依赖都抽象到业务执行上下文中,通过定义统一的格式和使用方式来约束Action的使用。 考虑不同层级Action对于数据依赖使用范围由大到小,遵循相同的分层设计,我们设计了三层依次继承的Context容器,并将三类依赖的数据标准化存储到相应的Context中。 使用标准化Context进行数据传递,优势在于Action可自定义获取输入数据,以及后续扩展的便利性;同时标准化的Context也存在一定的劣势,它无法从机制上完全限制Action的数据访问权限,随着后续迭代也可能导致Context日渐臃肿。综合考虑利弊后,现阶段我们仍然采用标准的Context的模式。
对于第三方的外部数据的使用,需要成熟的工程经验提前评估调用量、负载、性能、批量或拆包等因素,所以针对所有第三方外部数据,我们统一封装为基础Action,再由业务根据情况定制化使用。
词表根据业务规则或策略生成,需要加载到内存中使用的KV类数据,标准化之前的词表数据在生成、拉取、加载、内存优化、回滚、降级等能力上有不同程度的缺失。因此,我们设计了一套基于消息通知的词表管理框架,实现了词表的版本管理、定制加载、定时清理、流程监控的全生命周期覆盖,并定义了业务标准化的接入方式。
对于用户维度的第三方数据,统一在初始化后进行封装调用。 对于商家维度的第三方数据,有批量接口使用的数据,在召回后统一封装调用;无批量接口使用的数据,在精排截断后统一封装调用。
4.3 技术框架
4.3.1 整体框架介绍
业务可视化包,提供各个后台系统上的能力的静态信息,包括名称、功能描述、配置信息等,这些信息在需求评估阶段、业务开发阶段都会被用到。 全图化编排和下发包,业务开发同学通过对已有的能力进行可视化的拖拽,通过全图化服务自动生成并行化最优的执行流程,再根据具体业务场景进行调整,最终生成一个有向无环图,图的节点代表业务能力,边表示业务能力之间的依赖关系。该图会动态下发到对应的后台服务去供执行框架解析执行。 统计监控包,提供业务能力、词典等运行期间的统计和异常信息,用于查看各个业务能力的性能情况以及异常情况,达到对各个业务能力运行状态可感知的目的。
核心包,提供两个功能,第一个是调度功能,执行平台下发的流程编排文件,按照定义的DAG执行顺序和执行条件去依次或并行执行各个业务能力,并提供必要的隔离和可靠的性能保证,同时监控运行以及异常情况进行上报。第二个是业务采集和上报功能,扫描和采集系统内的业务能力,并上报至平台Web服务,供业务编排以及业务能力可视化透出使用。 能力包,业务能力的集合,这里的业务能力在前面章节“4.2.2.1 功能的标准化”中已给出定义,即“将最小不可拆分的业务逻辑单元,抽象为业务同学开发的基本单位,称为Action,也叫能力”。 组件包,即业务组件的集合,这里的业务组件在章节“4.2.2.1 功能的标准化”中也给出定义,即“对于可独立实现并部署的某一方面功能,比如创意能力,抽象为组件”。 工具包,提供业务能力需要的基础功能,例如引擎常用的词典工具、实验工具以及动态降级等工具。这里的工具在章节“4.2.2.1功能的标准化”中同样给出了定义,即单个的、简单的非业务功能模块抽象为工具。
4.3.2 业务采集&上报
@LppAbility(name = "POI、Plan、Unit数据聚合平铺能力", desc = "做预算过滤之前,需要把对象打平",
param = "AdFlatAction.Param", response = "List<KvPoiInfoWrapper>", prd = "无产品需求", func = "POI、Plan、Unit数据聚合平铺能力", cost = 1)
public abstract class AdFlatAction extends AbstractNotForceExecuteBaseAction {
}
//扩展点
@LppExtension(name = "数据聚合平铺扩展点",
func = "POI、Plan、Unit数据聚合平铺", diff = "默认的扩展点,各业务线直接无差异", prd = "无", cost = 3)
public class FlatAction extends AdFlatAction {
@Override
protected Object process(AdFlatAction.Param param) {
//do something
return new Object();
}
}
4.3.3 全图化编排
当存在任意以下两种情况之一时,我们会认为Action x依赖于Action y。
input_x ∩ output_y ≠ ∅,即Action x的某个/某些入参是由Action y产出。 output_x ∩ output_y ≠ ∅,即Action x与Action y操作相同字段。
解析模块:通过对字节码分析,解析出每个Action的input、output集合。 字节码分析使用了开源工具ASM,通过模拟Java运行时栈,维护Java运行时局部变量表,解析出每个Action执行依赖的字段和产出的字段。 依赖分析模块:采用三色标记的逆向解析法,分析出Action之间的依赖关系,并对生成的图进行剪枝操作。 依赖剪枝:生成图会有重复依赖的情况,为了减少图复杂度,在不改变图语义的前提下,对图进行了依赖剪枝。例如:
4.3.4 调度引擎
构图:根据Action的编排配置生成具体的DAG模板图。 调度:流量请求时,按照正确的依赖关系执行Action。
静态构图:在服务启动时,调度引擎根据下发的DAG编排配置,初始化为Graph模板并加载至内存。服务启动后,多个DAG的模板会持久化到内存中。当Web平台进行图的动态下发后,引擎会对最新的图进行构图并完全热替换。 动态调度:当流量请求时,业务方指定对应的DAG,连同上下文信息统一交至调度引擎;引擎按照Graph模板执行,完成图及节点的调度,并记录下整个调度的过程。
依赖分层算法(如广度优先遍历)提前计算好每一层需要执行的节点;节点一批一批的调度,无需任何通知和驱动机制。 在同批次多节点时,由于各节点执行时间不同,容易出现长板效应。 在多串行节点的图调度时,有较好的性能优势。
某节点执行完成后,立即发送信号给消息队列;消费侧在收到信号后,执行后续节点。如上图DAG中,B执行完成后,D/E收到通知开始执行,不需要关心C的状态。 由于不关心兄弟节点的执行状态,不会出现分层调度的长板效应。 在多并行节点的图调度时,有非常好的并行性能;但在多串行节点的图中,由于额外存在线程切换和队列通知开销,性能会稍差。
节点调度机制
调度机制:消费侧收到消息到节点被执行,这中间的过程。如下DAG中,节点在接收到消息后需依次完成:检验DAG执行状态、校验父节点状态、检验节点执行条件、修改执行状态、节点执行这几个过程,如下图所示:
这几个步骤的执行,通常存在两种方式:一种是集中式调度,由统一的方法进行处理;另一种是分散式调度,由每个后续节点独自来完成。 我们采用的为集中式调度:某节点执行完成后,发送消息到队列;消费侧存在任务分发器统一负责消费,再进行任务分发。
如上图中,ABC三个节点同时完成,到D节点真正执行前仍有一系列操作,这个过程中如果不加锁控制,D节点会出现执行三次的情况;因此,需要加锁来保证线程安全。而集中式任务分发器,采用无锁化队列设计,在保证线程安全的同时尽量规避加锁带来的性能开销。 再如一父多子的情况,一些公共的操作(校验图/父节点状态、异常检测等),各子节点都会执行一次,会带来不必要的系统开销。而集中式任务分发器,对公共操作统一进行处理,再对子节点任务进行分发。 分散式调度中,节点的职责范围过广,既需要执行业务核心代码,还需要额外处理消息的消费,职责非单一,可维护性较差。
异步调用:GraphTask由线程池来执行,并将最外层GraphTask的Future返回给业务方,业务方可以精准的控制DAG的最大执行时间。目前,外卖广告中存在同一个请求中处理不同广告业务的场景,业务方可以根据异步接口自由组合子图的调度。 同步调用:与异步调用最大的不同是,同步调用会在图执行完成/图执行超时后,才会返回给调用方。
调度线程模型调优 针对同步调用的情况,由于主线程不会直接返回,而是在等待DAG图执行完成。调度引擎利用这一特点,让主线程来执行最外层的GraphTask,在处理每个请求时,会减少一次线程的切换。 串行节点执行优化 如上面DAG图中,存在一些串行节点(如单向A→B→C→D),在执行这4个串行节点时,调度引擎则不会进行线程的切换,而是由一个线程依次完成任务执行。 在执行串行节点时,调度引擎同样不再进行队列通知,而是采用串行调度的方式执行,最大化减少系统开销。
流程引擎活跃在同一个进程内,单实例方案管理起来要更容易。 流程引擎内部实现过程中,针对图的粒度上做了一些多租户隔离工作,所以在对外提供上更倾向于单实例方案。
每个线程池职责单一,执行任务更加单一,相应的过程监控与动态调整也更加方便。 如果共用一个线程池,如果出现瞬时QPS猛增,会导致线程池全被GraphTask占据,无法提交NodeTask最终导致调度引擎死锁。
异常:图/节点执行异常,支持配置重试、自定义异常处理。 超时:图/节点执行超时,支持降级。 统计:图/节点执行次数&耗时,提供优化数据报表。
如前图所示,我们将构图与调度通过中间态Graph模板进行解耦,编排配置可以通过Web平台编辑后,动态下发到服务上。 由于调度引擎在调度过程中,多次用到了线程池,对于线程池的动态更新,我们借助了公司的通用组件对线程池进行动态化配置和监控。
调度引擎提供两种常见调度器的实现,针对不同的业务场景,能较好的提供支持。 调度引擎采用经典的两级调度模型,DAG图/节点任务调度更具有隔离性和可控性。
对于节点的调度前置增加条件校验功能,不满足条件的节点不会执行,调度引擎会根据上下文以及流量情况动态判断节点的执行条件。
对DAG、Stage、Node节点均支持超时处理,简化内部各个业务逻辑的超时控制,将主动权交给框架统一进行处理。在保证性能的前提之下,提高内部逻辑的处理效率。
同一个Node节点,会被对个业务场景使用,但各业务场景的其处理逻辑且不近相同。针对这种情况,增加节点的配置化功能,框架将节点的配置传入逻辑内部,实现可配置。
在多串行节点的DAG场景下,性能基本可以持平原有的裸写方式。 在多并行节点的DAG场景下,由于池化的影响,在多线程池抢占和切换上,存在一些性能折损;再进行多次调优和CPU热点治理上,TP999折损值可以控制到5ms以内。
4.3.5 业务组件层沉淀
公共域是指在不同的业务组件中都会使用到的业务实体。我们将业务上的公用域对象提取出来,作为基础组件提供给其他业务组件使用,以减少域对象在不同组件重复定义。 业务组件都有很多内部和外部的依赖。我们对公共依赖进行了统一的梳理和筛选,同时权衡各方面因素,确定了合理的使用方式。最终形成一套完整成熟的依赖框架。
我们将业务组件抽象为三个阶段:数据和环境准备阶段Prepare、实际计算阶段Process和后置处理阶段Post。每个阶段都设计了抽象的泛型模板接口,最后通过不同的接口组合完成组件中的不同业务流程。所有类在接口设计上都提供了同步和异步两种调用方式。
目前所有的服务模块均采用Spring作为开发框架,我们利用其AOP功能开发了一系列的切面扩展能力,包括日志采集、耗时监控、降级限流、数据缓存等功能。这些功能均采用无侵入式代码设计,减少切面能力与业务逻辑的耦合。新的业务组件通过配置的方式即可完全复用。
4.3.6 工具包-词典管理
存储层:主要用于数据的存储和流转。其中美团内部的S3完成在云端的词表文件存储,Zookeeper主要用于存储词表的版本信息,在线服务通过监听的方式获取最新的版本更新事件。 组件层:每个组件可以视为独立的功能单元,为上层提供通用的接口。 插件层:业务插件的作用主要是提供统一的插件定义和灵活的自定义实现。例如:加载器主要用途为提供统一格式的词表加载和存储功能,每个词表可以动态配置其加载器类型。 模块层:模块层主要是从业务角度看整体词表文件不同流程的某一环节,模块之间通过事件通知机制完成交互。例如:词表管理类模块包含词表版本管理、事件监听、词表注册、词表加/卸载、词表访问等。 流程层:我们将一个完整词表业务行为过程定义为流程。词表的整个生命周期可以分为新增词表流程、更新词表流程、注销词表流程、回滚词表流程等。
更灵活的服务架构:词表流程的透明化。使用方无需关注词表流转过程,采用统一API访问。 统一的业务能力:统一的版本管理机制,统一的存储框架,统一的词表格式和加载器。 系统高可用:快速恢复和降级能力,资源和任务隔离、多优先级处理能力等多重系统保障功能。
4.4 产研新流程
4.4.1 目标
4.4.2 思考与落地
对于PM(产品):建设Stage/Action可视化能力,并在项目设计中应用。 对于RD(研发):统一采用新的基于Stage/Action的方案,设计及开发排期模式。 对于QA(测试):统一沟通协作语言-Stage/Action,并推动改进相关测试方法和测试工具
开发前
技术设计:基于各业务涉及的现有Action功能与Action DAG的可视化能力,进行横向业务的调研参考与复用评估,以及新增或变更Action功能的技术设计。 项目排期:基于技术设计中Action能力的新增、变更、复用情况以及Action层级等,对开发工作量进行较为标准化的评估。 开发后
Action沉淀:系统统一上报并定期评估平台Action能力的复用度和扩展情况。 流程反馈:追踪基于平台化的每个项目,并对交付流程中的相关指标做量化上报,同时收集项目人员反馈。
采用Stage/Action统一沟通协作语言:在需求设计与评审、方案设计与评审、测试用例编写与评审等多方参与的项目环节,统一采用Stage/Action为功能描述与设计的沟通语言,以便将后续流程中问题的发现尽可能前置,同时各参与方更加明确变更及测试内容,为QA更好的评估测试范围提供支撑,进而更好的保证项目测试质量。
推动基础Aaction UT全覆盖:针对基础Action,构建单元测试,在Merge代码时自动触发单元测试流水线,输出执行单测的成功率和覆盖率,并评定指标基线,保证可持续测试的效率与质量。
改进JAR管理工具化与自动化分析及测试:一级Action都集中写在平台JAR包中,对类似这种公共JAR包的管理,开发专属的管理与维护工具,解决升级公共JAR自动化单测覆盖问题以及每次升级JAR版本需要人工分析人工维护的测试效率问题,打通集成测试自动化的全流程。
5 效果
系统能力沉淀
外卖广告所有业务线已经完成平台化架构升级,并在此架构上持续的运行和迭代。 业务基础能力沉淀50+个,模块共用能力沉淀140+个,产品线共用能力沉淀500+个。 人效的提升
研发效率提升:在各业务线平台化架构迁移后,大的业务迭代20+次,业务迭代效率提升相比之前总计提升28+%。特别是在新业务的接入上,相同功能无需重复开发,提效效果更加明显:能力累计复用500+次,能力复用比52+%;在新业务接入场景中,Action复用65+%。
测试的自动化指标提升:借助于JAR自动化分析、集成测试及流程覆盖建设,广告自动化测试覆盖率提升了15%,测试提效累计提升28%,自动化综合得分也有了明显提升。
基于Action的变更以及清晰的可视化业务链路,能够帮助QA更准确的评估影响范围,其中过程问题数量及线上问题数量均呈下降趋势,下降比例约为10%。 通过系统能力的可视化透出页面,增加系统的透明度,在产品调研阶段有效帮助产品了解系统已有的能力,减少了业务咨询、跨产品线知识壁垒等问题(详情可参见4.4.2.1)。
6 总结与展望
7 作者简介
阅读更多