随着美团外卖业务不断发展,外卖广告引擎团队在多个领域进行了工程上的探索和实践,目前已经取得了一些成果。我们计划通过连载的形式分享给大家,本文是《美团外卖广告工程实践》专题连载的第一篇。本文针对业务提效的目标,介绍了美团外卖广告引擎在平台化过程中的一些思考和实践。
1 前言
美团外卖已经成为公司最为重要的业务之一,而商业变现又是整个外卖生态重要的组成部分。经过多年的发展,广告业务覆盖了Feed流形式的列表广告,针对KA以及大商家的展示广告,根据用户查询Query的搜索广告,以及一些创新场景的创新广告等多个产品线,并对应十几个细分的业务场景。
从技术层面而言,一次广告请求的过程,可以分为以下几个主要步骤:广告的触发、召回、精排、创意优选、机制策略等过程。如下图所示:即通过触发得到用户的意图,再通过召回得到广告候选集,通过预估对候选集的店铺打分、排序,再对于Top的店铺再进行创意的选择,最后经过一些机制策略得到广告结果。
2 现状分析
在业务迭代的过程中,随着新业务场景的不断接入,以及原有业务场景功能的不断迭代,系统变得越来越复杂,业务迭代的需求响应逐渐变慢。在业务发展前期,开展过单个模块的架构重构,如机制策略、召回服务,虽然对于效率提升有一定的改善,但是还会存在以下一些问题:
- 业务逻辑复用度低:广告业务逻辑比较复杂,比如机制服务模块,它主要功能是为广告的控制中枢以及广告的出价和排序的机制提供决策,线上支持十几个业务场景,每种场景都存在很多差异,比如会涉及多种召回、计费模式、排序方案、出价机制、预算控制等等。此外,还有大量业务自定义的逻辑,由于相关逻辑是算法和业务迭代的重点,因此开发人员较多,并且分布在不同的工程和策略组内,导致业务逻辑抽象粒度标准不够统一,使得不同场景不同业务之间复用程度较低。
- 学习成本高:由于代码复杂,新同学熟悉代码成本较高,上手较难。此外,线上服务很早就进行了微服务改造,线上模块数量超过20个,由于历史原因,导致多个不同模块使用的框架差异较大,不同模块之间的开发有一定的学习成本。在跨模块的项目开发中,一位同学很难独立完成,这使得人员效率没有得到充分利用。
- PM(产品经理)信息获取难:由于目前业务场景较多、逻辑复杂,对于信息的获取,绝大多数同学很难了解业务的所有逻辑。PM在产品设计阶段需要确认相关逻辑时,只能让研发同学先查看代码,再进行逻辑的确认,信息获取较难。此外,由于PM对相关模块的设计逻辑不清楚,往往还需要通过找研发人员线下进行询问,影响双方的工作效率。
- QA(测试)评估难:QA在功能范围评估时,完全依赖于研发同学的技术方案,且大多数也是通过沟通来确认功能改动涉及的范围和边界,在影响效率的同时,还很容易出现“漏测”的问题。
3 目标
针对以上的问题,我们从2020年初,启动美团外卖广告引擎平台化项目,旨在通过平台化的项目达成以下目标。
提升产研效率
- 高功能复用度,提升开发效率。
- 降低研发人员(RD)、PM、QA之间的协作成本,提升产研协作的效率。
提升交付质量
- 精确QA测试的范围,提升交付的质量。
- 对业务进行赋能。
- PM可通过可视化的平台化页面,了解其他产品线的能力,互相赋能,助力产品迭代。
4 整体设计
4.1 整体思想
目前,业界已经有不少“平台化”方向的研究,比如阿里巴巴的TMF,定位于泛交易类系统的平台化领域范畴,主要建设思想是,流程编排与领域扩展分层,业务包与平台分离的插件化架构,管理域与运行域分离。而阿里巴巴的AIOS则定位于搜推平台化领域范畴,主要依赖于底层5大核心组件,以算子流程图定制的模式对组件快速组合与部署,从而实现了业务的快速交付。
美团外卖在平台化项目启动时,从业务场景和业务痛点出发,确定了我们项目的核心目标:利用平台化设计理念构建相适应的技术能力,将现有外卖广告的业务系统和产研流程转变为平台化模式,快速支持外卖广告多业务进行交付。我们借鉴了行业内平台化的成熟思想,确定了以业务能力标准化为基础、构建平台化框架技术能力为支撑、产研平台化模式升级为保障的平台化建设整体思想,整体思想可分为三部分:业务能力标准化、技术能力框架化、平台化产研新流程。
- 业务能力标准化:通过对现有逻辑的梳理,进行标准化的改造,为多业务场景、多模块代码复用提供基础保证。
- 技术能力框架化:提供组合编排能力将标准化的逻辑串联起来,通过引擎调度执行,同时完成了可视化能力的透出,帮助用户快速获取信息。
- 平台化产研新流程:为保证项目上线之后实现研发迭代的整体提效,我们对于研发流程的一些机制也进行了一些优化,主要涉及研发人员、PM、QA三方。
即通过标准化提供复用的保证,通过框架承载平台化落地的能力,通过产研新流程的运行机制保证了整体提效的持续性。整个广告引擎服务涉及到的模块都遵循了平台化的思想,支撑上游各个产品场景,如下图所示:
4.2 业务标准化
4.2.1 业务场景与流程分析
提效是平台化最重要的目标之一,而提效最重要的手段是让功能在系统中得到最大程度上的复用。我们首先针对外卖广告业务线场景和流量的现状做了统一的分析,得出以下两点结论:
第一,各业务线大的流程基本类似,都包括预处理、召回、预估、机制策略、排序、创意、结果组装等几个大的步骤;同时,不同业务相同的步骤里会有很多相似的功能和业务线特有的功能。第二,这些功能理论上都是可以整体进行复用的,但现状是这些功能都集中在业务线内部,不同的业务线之间,不同的小组之间的复用状况也不尽相同。而造成这一问题的主要原因是:
- 不同业务处在不同的发展阶段,也有着不同的迭代节奏。
- 组织结构天然存在“隔离”,如推荐和搜索业务分在两个不同的业务小组。
因此,阻碍外卖广告进一步提升复用程度的主要原因,在于整体的标准化程度不足,各业务线间没有统一的标准,所以我们要先解决标准化建设的问题。
4.2.2 标准化建设
标准化建设的广度和深度决定了系统复用能力的高低。因此,本次标准化的建设目标要覆盖到所有方面。我们对广告系统所有的服务,从业务开发的三个维度,包括实现的功能、功能使用的数据、功能组合的流程出发,来进行统一广告的标准化建设。从而使得:
- 在个体开发层面:开发同学不用关注如何流程调度,只需将重心放在新功能的实现上,开发效率变得更高。
- 从系统整体角度:各个服务对于通用的功能不用再重复开发,整体的复用程度更高,节省了大量的开发时间。
4.2.2.1 功能的标准化
针对功能的标准化问题,我们首先依据功能是否跟业务逻辑相关,将其划分为两部分:业务逻辑相关和业务逻辑无关。
① 与业务逻辑无关的功能通过双层抽象来统一共建
- 所有业务线统一共建的标准化形式是进行双层抽象。对于单个的、简单的功能点,抽象为工具层;对于可独立实现并部署的某一方面功能,比如创意能力,抽象为组件层。工具层和组件层统一以JAR包的形式对外提供服务,所有工程都通过引用统一的JAR包来使用相关的功能,避免重复的建设,如下图所示:
② 与业务逻辑有关的功能,在复用范围上进行分层复用
- 业务逻辑相关的功能是此次标准化建设的核心,目标是做到最大程度的业务复用。因此,我们将最小不可拆分的业务逻辑单元抽象为业务同学开发的基本单位,称为Action。同时根据Action不同的复用范围,将其划分为三层,分别是所有业务可以复用的基础Action,多业务线复用的模块Action,具体单一业务定制的业务Action,亦即扩展点。所有的Action都是从Base Action派生出来的,Base Action里定义了所有Action统一的基础能力。
- 不同的Action类型分别由不同类型的开发同学来开发。对于影响范围比较大的基础Action和模块Action,由工程经验丰富的同学来开发;对于仅影响单个业务的业务Action或扩展点,由工程能力相对薄弱的同学来进行开发。
- 同时我们把多个Action的组合,抽象为Stage,它是不同Action组合形成的业务模块,目的在于屏蔽细节,简化业务逻辑流程图的复杂度,并提供更粗粒度的复用能力。
4.2.2.2 数据的标准化
数据作为实现功能的基本元素,不同业务的数据来源大同小异。如果不对数据进行标准化设计,就无法实现功能标准化的落地,也无法实现数据层面的最大化复用。我们从数据来源和数据使用方式两方面来划分数据:对于业务能力的输入数据、中间数据,输出数据,通过标准化的数据上下文来实现;同时对于第三方外部数据及词表等内部数据,通过统一的容器存储和接口获取。
① 使用上下文Context描述Action执行的环境依赖
- 每个Action执行都需要一定的环境依赖,这些依赖包括输入依赖、配置依赖、环境参数、对其他Action的执行状态的依赖等。我们将前三类依赖都抽象到业务执行上下文中,通过定义统一的格式和使用方式来约束Action的使用。
- 考虑不同层级Action对于数据依赖使用范围由大到小,遵循相同的分层设计,我们设计了三层依次继承的Context容器,并将三类依赖的数据标准化存储到相应的Context中。
- 使用标准化Context进行数据传递,优势在于Action可自定义获取输入数据,以及后续扩展的便利性;同时标准化的Context也存在一定的劣势,它无法从机制上完全限制Action的数据访问权限,随着后续迭代也可能导致Context日渐臃肿。综合考虑利弊后,现阶段我们仍然采用标准的Context的模式。
② 第三方外部数据的统一处理
- 对于第三方的外部数据的使用,需要成熟的工程经验提前评估调用量、负载、性能、批量或拆包等因素,所以针对所有第三方外部数据,我们统一封装为基础Action,再由业务根据情况定制化使用。
③ 词表数据的全生命周期管理
- 词表根据业务规则或策略生成,需要加载到内存中使用的KV类数据,标准化之前的词表数据在生成、拉取、加载、内存优化、回滚、降级等能力上有不同程度的缺失。因此,我们设计了一套基于消息通知的词表管理框架,实现了词表的版本管理、定制加载、定时清理、流程监控的全生命周期覆盖,并定义了业务标准化的接入方式。
4.2.2.3 调用流程的标准化
最后,将功能和数据进行组合的是业务的调用流程,统一的流程设计模式是业务功能复用和提效的核心手段。流程设计统一的最佳方式就是标准化业务流程。其中对于第三方接口的调用方式,让框架研发的同学用集中封装的方式进行统一。对于接口的调用时机,则基于性能优先并兼顾负载,且在没有重复调用出现的原则下,进行标准化。
在具体实践中,我们首先梳理业务逻辑所使用到的标准化功能,然后分析这些功能之间的依赖关系,最后以性能优先并兼顾负载、无重复调用等原则,完成整个业务逻辑流程的标准设计。
从横向维度看,通过比较不同业务逻辑流程的相似性,我们也提炼了一定的实践经验,以中控模块为例:
- 对于用户维度的第三方数据,统一在初始化后进行封装调用。
- 对于商家维度的第三方数据,有批量接口使用的数据,在召回后统一封装调用;无批量接口使用的数据,在精排截断后统一封装调用。
4.3 技术框架
4.3.1 整体框架介绍
平台主要有两个部分组成,一部分是平台前台部分,另一部分是平台开发框架包。其中前台部分是一个给研发人员、PM以及QA三种角色使用的Web前台,主要功能是跟集成了平台开发框架包的引擎服务进行可视化的交互,我们也给这个平台起了个名字,叫Camp平台,这是大本营的意思,寓意助力业务方攀登业务高峰。平台开发框架包被引擎后台服务所集成,提供引擎调度隔离、能力沉淀、信息上报等功能,同时还能确保各个模块保持同样标准的框架和业务能力风格。
各个在线服务都需要引入平台开发框架包,服务性能与平台通用性之间如何平衡也是我们需要着重考虑的地方。这是因为,引入平台框架会对原有的代码细节进行增强性扩展;在C端大流量场景下,平台框架做得越通用,底层功能做得越丰富,与单纯的“裸写”代码相比,会带来一些性能上的折损。因此,在性能开销与平台抽象能力上,需要尽量做到一个折中。我们结合自身业务的特性,给出的安全阈值是TP999损失在5ms以内,将各个业务通用的能力下沉至框架,提供给上层的在线服务。
综上,整个系统架构设计如下:
① Camp平台提供管理控制和展示的功能,该平台由以下几个子模块包组成:
- 业务可视化包,提供各个后台系统上的能力的静态信息,包括名称、功能描述、配置信息等,这些信息在需求评估阶段、业务开发阶段都会被用到。
- 全图化编排和下发包,业务开发同学通过对已有的能力进行可视化的拖拽,通过全图化服务自动生成并行化最优的执行流程,再根据具体业务场景进行调整,最终生成一个有向无环图,图的节点代表业务能力,边表示业务能力之间的依赖关系。该图会动态下发到对应的后台服务去供执行框架解析执行。
- 统计监控包,提供业务能力、词典等运行期间的统计和异常信息,用于查看各个业务能力的性能情况以及异常情况,达到对各个业务能力运行状态可感知的目的。
② 平台开发框架包被广告引擎的多个服务引入,执行编排好的业务流程并对外提供服务,平台框架开发包由以下几个子模块包组成:
- 核心包,提供两个功能,第一个是调度功能,执行平台下发的流程编排文件,按照定义的DAG执行顺序和执行条件去依次或并行执行各个业务能力,并提供必要的隔离和可靠的性能保证,同时监控运行以及异常情况进行上报。第二个是业务采集和上报功能,扫描和采集系统内的业务能力,并上报至平台Web服务,供业务编排以及业务能力可视化透出使用。
- 能力包,业务能力的集合,这里的业务能力在前面章节“4.2.2.1 功能的标准化”中已给出定义,即“将最小不可拆分的业务逻辑单元,抽象为业务同学开发的基本单位,称为Action,也叫能力”。
- 组件包,即业务组件的集合,这里的业务组件在章节“4.2.2.1 功能的标准化”中也给出定义,即“对于可独立实现并部署的某一方面功能,比如创意能力,抽象为组件”。
- 工具包,提供业务能力需要的基础功能,例如引擎常用的词典工具、实验工具以及动态降级等工具。这里的工具在章节“4.2.2.1功能的标准化”中同样给出了定义,即单个的、简单的非业务功能模块抽象为工具。
一个典型的开发流程如上图所示 ,开发人员开发完业务能力后(1),业务能力的静态信息会被采集到Camp平台(2),同时,经过全图化依赖推导得到最优DAG图(3),业务同学再根据实际业务情况对DAG图进行调整,引擎在线服务运行期间会得到最新的DAG流程并对外提供最新的业务流程服务(4,5),同时会把业务运行的动态信息上报至Camp平台(6)。
在下面的章节中,我们将对几个比较关键的技术点进行详细描述,其中就包括了可视化相关的组件自动上报和DAG执行相关的全图化编排、执行调度等,最后,本文还会介绍一下跟广告业务强相关的、词典在平台化中统一封装的工作。
4.3.2 业务采集&上报
为了方便管理和查询已有业务能力,平台开发框架包会在编译时扫描@LppAbility注解和@LppExtension注解来上报元数据到Camp平台。业务同学可以在Camp平台中对已有组件进行查询和可视化的拖拽。
//原子能力(Action)
@LppAbility(name = "POI、Plan、Unit数据聚合平铺能力", desc = "做预算过滤之前,需要把对象打平",
param = "AdFlatAction.Param", response = "List", 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 全图化编排
在广告投放引擎服务中,每个业务的DAG图,动辄便会有几十甚至上百的Action,通过传统的人工编排或业务驱动编排,很难做到Action编排的最优并行化。因此,平台化框架包采用数据驱动的思想,通过Action之间的数据依赖关系,由程序自动推导出并行化最优的DAG图,即全图化编排,此后再由业务人员根据业务场景和流量场景进行定制化调整,动态下发到服务节点,交由调度引擎执行,这样通过自动推导+场景调优的方式便达到了场景下的最优并行。
① 全图化自动编排的基本原理
我们定义某个Action x的入参集合为该Action x执行时使用的字段,表示如下:
$input_x(A,B,C......N)$
定义某个Action y的出参集合为该Action执行后产出的字段,表示如下:
$output_y(A,B,C......M)$
当存在任意以下两种情况之一时,我们会认为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之间的依赖关系,并对生成的图进行剪枝操作。
- 依赖剪枝:生成图会有重复依赖的情况,为了减少图复杂度,在不改变图语义的前提下,对图进行了依赖剪枝。例如:
③ 全图化自动编排收益效果
自动纠正人工错误编排,并最大化编排并行度。某实际业务场景中,全图化前后的DAG对比,如下图所示:
标记蓝色的两个Action,会同时操作同一个Map,如果并发执行会有线程安全风险。由于方法调用栈过深,业务开发同学很难关注到该问题,导致错误的并行化编排。经过全图化分析后,编排为串行执行。
标记绿色、红色、黄色的三组Action,每组内的两个Action并没有数据依赖关系,业务开发同学串行化编排。经过全图化分析后,编排为并行。
4.3.4 调度引擎
调度引擎的核心功能是对上述下发后的DAG进行调度。因此引擎需要具备以下两个功能:
- 构图:根据Action的编排配置生成具体的DAG模板图。
- 调度:流量请求时,按照正确的依赖关系执行Action。
整个调度引擎的工作原理如下图:
出于对性能的考虑,调度引擎摒弃了流量请求实时构图的方法,而是采用“静态构图+动态调度”的方式。
- 静态构图:在服务启动时,调度引擎根据下发的DAG编排配置,初始化为Graph模板并加载至内存。服务启动后,多个DAG的模板会持久化到内存中。当Web平台进行图的动态下发后,引擎会对最新的图进行构图并完全热替换。
- 动态调度:当流量请求时,业务方指定对应的DAG,连同上下文信息统一交至调度引擎;引擎按照Graph模板执行,完成图及节点的调度,并记录下整个调度的过程。
由于广告投放引擎服务于C端用户,对服务的性能、可用性、扩展性要求很高。调度引擎的设计难点也落在了这三个方面,接下来我们将进行简要的阐述。
4.3.4.1 高性能实践
流程引擎服务于C端服务,与传统的硬编码调度相比,引擎的调度性能要至少能持平或在一个可接受的性能损失阈值内。下面,我们将从调度器设计、调度线程调优这两个有代表性的方面介绍下我们的性能实践。
① 调度器设计
含义:如何让节点一个一个的执行;一个节点执行完成,如何让其他节点感知并开始执行。如下图中,A节点在执行完成后,如何通知B,C节点并执行。常见的思路是,节点的分层调度,它的含义及特点如下:
- 依赖分层算法(如广度优先遍历)提前计算好每一层需要执行的节点;节点一批一批的调度,无需任何通知和驱动机制。
- 在同批次多节点时,由于各节点执行时间不同,容易出现长板效应。
- 在多串行节点的图调度时,有较好的性能优势。
另一种常见的思路是,基于流水线思想的队列通知驱动模式:
- 某节点执行完成后,立即发送信号给消息队列;消费侧在收到信号后,执行后续节点。如上图DAG中,B执行完成后,D/E收到通知开始执行,不需要关心C的状态。
- 由于不关心兄弟节点的执行状态,不会出现分层调度的长板效应。
- 在多并行节点的图调度时,有非常好的并行性能;但在多串行节点的图中,由于额外存在线程切换和队列通知开销,性能会稍差。
如上图所示,调度引擎目前支持这两种调度模型。针对多串行节点的图推荐使用分层调度器,针对多并行节点的图推荐使用队列流水线调度器。
分层调度器
依赖于上面提到的分层算法,节点分批执行,串行节点单线程执行,并行节点池化执行。
队列流水线调度器
无论是外层的图任务(GraphTask)还是内部节点任务(NodeTask)均采用池化的方式执行。
节点调度机制
- 调度机制:消费侧收到消息到节点被执行,这中间的过程。如下DAG中,节点在接收到消息后需依次完成:检验DAG执行状态、校验父节点状态、检验节点执行条件、修改执行状态、节点执行这几个过程,如下图所示:
- 这几个步骤的执行,通常存在两种方式:一种是集中式调度,由统一的方法进行处理;另一种是分散式调度,由每个后续节点独自来完成。
- 我们采用的为集中式调度:某节点执行完成后,发送消息到队列;消费侧存在任务分发器统一负责消费,再进行任务分发。
这样做的出发点是:
- 如上图中,ABC三个节点同时完成,到D节点真正执行前仍有一系列操作,这个过程中如果不加锁控制,D节点会出现执行三次的情况;因此,需要加锁来保证线程安全。而集中式任务分发器,采用无锁化队列设计,在保证线程安全的同时尽量规避加锁带来的性能开销。
- 再如一父多子的情况,一些公共的操作(校验图/父节点状态、异常检测等),各子节点都会执行一次,会带来不必要的系统开销。而集中式任务分发器,对公共操作统一进行处理,再对子节点任务进行分发。
- 分散式调度中,节点的职责范围过广,既需要执行业务核心代码,还需要额外处理消息的消费,职责非单一,可维护性较差。
因此,在项目实际开发中,考虑到实现的难度、可维护性、以及综合考量性能等因素,最终采用集中式调度。
② 调度线程调优
调度引擎在DAG执行上,提供了两种API给调用方,分别为:
- 异步调用:GraphTask由线程池来执行,并将最外层GraphTask的Future返回给业务方,业务方可以精准的控制DAG的最大执行时间。目前,外卖广告中存在同一个请求中处理不同广告业务的场景,业务方可以根据异步接口自由组合子图的调度。
- 同步调用:与异步调用最大的不同是,同步调用会在图执行完成/图执行超时后,才会返回给调用方。
而底层调度器,目前提供上述讲到两种调度器。具体如下图所示:
由此看出,调度引擎在内部任务执行上,多次用到了线程池。在CPU密集型的服务上,请求量过大或节点过多的话,大量线程切换势必会影响到服务的整体性能。针对队列通知调度器,我们做了一些调度优化,尽量将性能拉回到没有接入调度引擎之前。
调度线程模型调优
- 针对同步调用的情况,由于主线程不会直接返回,而是在等待DAG图执行完成。调度引擎利用这一特点,让主线程来执行最外层的GraphTask,在处理每个请求时,会减少一次线程的切换。
串行节点执行优化
- 如上面DAG图中,存在一些串行节点(如单向A→B→C→D),在执行这4个串行节点时,调度引擎则不会进行线程的切换,而是由一个线程依次完成任务执行。
- 在执行串行节点时,调度引擎同样不再进行队列通知,而是采用串行调度的方式执行,最大化减少系统开销。
4.3.4.2 高可用实践
在高可用上,我们从隔离和监控上简要介绍下我们的实践,它的核心原理如下图所示:
① 业务隔离
广告场景中,同一服务中经常会存在多条子业务线,每条业务线的逻辑对应一张DAG。对于同一服务内各个业务线的隔离,我们采用的是“单实例-多租户”的方案。这是因为:
- 流程引擎活跃在同一个进程内,单实例方案管理起来要更容易。
- 流程引擎内部实现过程中,针对图的粒度上做了一些多租户隔离工作,所以在对外提供上更倾向于单实例方案。
除DAG调度和Node调度为静态代码外,图的存储、DAG的选取与执行、Node节点的选取与执行、各DAG的节点通知队列都采用多租户隔离的思想。
② 调度任务隔离
调度任务主要分为:DAG任务(GraphTask)、节点任务(NodeTask)两类。其中一个GraphTask对应多个NodeTask,并且其执行状态依赖所有的NodeTask。调度引擎在执行时,采用二级线程池隔离的方式将GraphTask和NodeTask的执行进行隔离。
这样隔离的出发点是:
- 每个线程池职责单一,执行任务更加单一,相应的过程监控与动态调整也更加方便。
- 如果共用一个线程池,如果出现瞬时QPS猛增,会导致线程池全被GraphTask占据,无法提交NodeTask最终导致调度引擎死锁。
因此,无论是线程精细化管理还是隔离性上,两级线程池调度的方式都要优于一级线程池调度。
③ 过程监控
对DAG调度的监控,我们将其分成三类。分别为异常、超时、统计,具体如下:
- 异常:图/节点执行异常,支持配置重试、自定义异常处理。
- 超时:图/节点执行超时,支持降级。
- 统计:图/节点执行次数&耗时,提供优化数据报表。
4.3.4.3 高可用实践
广告业务逻辑复杂,在投放链路上存在大量的实验、分支判断、条件执行等。并且广告投放服务的迭代频率和发版频率也非常高。因此,调度引擎在可扩展上首先要考虑的是如何调度条件节点,以及编排配置如何在无发布下快速生效这两个问题。
① 节点条件执行
对于节点的条件执行,我们在配置DAG时,需要显示的增加Condition表达式。调度引擎在执行节点前,会动态计算表达式的值,只有满足执行条件,才会执行该节点。
② 配置动态下发
- 如前图所示,我们将构图与调度通过中间态Graph模板进行解耦,编排配置可以通过Web平台编辑后,动态下发到服务上。
- 由于调度引擎在调度过程中,多次用到了线程池,对于线程池的动态更新,我们借助了公司的通用组件对线程池进行动态化配置和监控。
4.3.4.4 调度引擎总结
① 功能方面
DAG核心调度
- 调度引擎提供两种常见调度器的实现,针对不同的业务场景,能较好的提供支持。
- 调度引擎采用经典的两级调度模型,DAG图/节点任务调度更具有隔离性和可控性。
节点条件执行
- 对于节点的调度前置增加条件校验功能,不满足条件的节点不会执行,调度引擎会根据上下文以及流量情况动态判断节点的执行条件。
超时处理
- 对DAG、Stage、Node节点均支持超时处理,简化内部各个业务逻辑的超时控制,将主动权交给框架统一进行处理。在保证性能的前提之下,提高内部逻辑的处理效率。
节点可配置化
- 同一个Node节点,会被对个业务场景使用,但各业务场景的其处理逻辑且不近相同。针对这种情况,增加节点的配置化功能,框架将节点的配置传入逻辑内部,实现可配置。
② 性能方面
- 在多串行节点的DAG场景下,性能基本可以持平原有的裸写方式。
- 在多并行节点的DAG场景下,由于池化的影响,在多线程池抢占和切换上,存在一些性能折损;再进行多次调优和CPU热点治理上,TP999折损值可以控制到5ms以内。
4.3.5 业务组件层沉淀
如“4.2.2.1 功能的标准化”中给出的定义,可独立实现并部署的业务功能模块抽象为业务组件。从业务逻辑中提取高内聚、低耦合的业务组件,是提升代码复用能力的重要手段。在实践中,我们发现不同业务组件包含的逻辑千差万别,具体实现方式和设计与代码风格也参差不齐。因此,为了统一业务组件的设计思路和实现方式,我们实现了一套标准化的组件框架,以减少新组件开发的重复性工作,并降低使用方的学习和接入成本。
上图左边展示了业务组件的整体框架,底层为统一的公共域和公共依赖,上层为业务组件标准的实现流程,切面能力则实现对业务逻辑的支持。右边为基于框架开发的智能出价组件示例。框架的作用是:
① 统一的公共域和依赖管理
- 公共域是指在不同的业务组件中都会使用到的业务实体。我们将业务上的公用域对象提取出来,作为基础组件提供给其他业务组件使用,以减少域对象在不同组件重复定义。
- 业务组件都有很多内部和外部的依赖。我们对公共依赖进行了统一的梳理和筛选,同时权衡各方面因素,确定了合理的使用方式。最终形成一套完整成熟的依赖框架。
② 统一的接口和流程
- 我们将业务组件抽象为三个阶段:数据和环境准备阶段Prepare、实际计算阶段Process和后置处理阶段Post。每个阶段都设计了抽象的泛型模板接口,最后通过不同的接口组合完成组件中的不同业务流程。所有类在接口设计上都提供了同步和异步两种调用方式。
③ 统一的切面能力
- 目前所有的服务模块均采用Spring作为开发框架,我们利用其AOP功能开发了一系列的切面扩展能力,包括日志采集、耗时监控、降级限流、数据缓存等功能。这些功能均采用无侵入式代码设计,减少切面能力与业务逻辑的耦合。新的业务组件通过配置的方式即可完全复用。
智能出价组件即为基于以上框架开发的业务组件。智能出价组件是对广告出价策略的抽象聚合,包括PID、CEM等多个算法。出价策略依赖的用户特征获取、实验信息解析等数据统一采用Prepare模板实现;具体PID、CEM算法的实施统一采用Process模板实现;对出价结果的校验、参数监控等后置操作则统一采用Post模板实现。整个组件所使用的公用域对象和第三方依赖也统一托管于框架进行管理。
4.3.6 工具包-词典管理
在“4.2.2.1 功能的标准化”中也定义了工具包的含义,即单个的、简单的非业务功能模块抽象为工具。工具包的建设是广告平台化工作提效的重要基础,其主要的作用是处理业务逻辑无关的辅助类通用流程或功能。例如:广告系统中存在大量的KV类数据需要加载到内存中使用,我们称之为词表文件。为了实现词表文件的全生命周期管理,广告平台化进行了词表管理工具的设计与开发,并在业务使用过程中积累了很好的实践效果。
① 词表管理的设计
上图是词表管理平台的整体架构,词表管理平台整体采用分层设计,自上而下分别五层:
- 存储层:主要用于数据的存储和流转。其中美团内部的S3完成在云端的词表文件存储,Zookeeper主要用于存储词表的版本信息,在线服务通过监听的方式获取最新的版本更新事件。
- 组件层:每个组件可以视为独立的功能单元,为上层提供通用的接口。
- 插件层:业务插件的作用主要是提供统一的插件定义和灵活的自定义实现。例如:加载器主要用途为提供统一格式的词表加载和存储功能,每个词表可以动态配置其加载器类型。
- 模块层:模块层主要是从业务角度看整体词表文件不同流程的某一环节,模块之间通过事件通知机制完成交互。例如:词表管理类模块包含词表版本管理、事件监听、词表注册、词表加/卸载、词表访问等。
- 流程层:我们将一个完整词表业务行为过程定义为流程。词表的整个生命周期可以分为新增词表流程、更新词表流程、注销词表流程、回滚词表流程等。
② 词表管理的业务收益
平台化词典管理工具在业务实践中具有的主要优势为:
- 更灵活的服务架构:词表流程的透明化。使用方无需关注词表流转过程,采用统一API访问。
- 统一的业务能力:统一的版本管理机制,统一的存储框架,统一的词表格式和加载器。
- 系统高可用:快速恢复和降级能力,资源和任务隔离、多优先级处理能力等多重系统保障功能。
4.4 产研新流程
上文中提到,由于广告业务线较多,且涉及诸多上下游,工程与策略经过几年快速迭代之后,现有业务逻辑已极为复杂,导致在日常迭代中,一些流程性问题也逐步凸显。
① PM信息获取困难
PM在进行产品调研与设计时,对涉及的相关模块当前逻辑不是很清楚,往往通过线下咨询研发人员的方式来解决,影响双方的效率,同时产品设计文档中纯以业务视角和流程来阐述,导致每次评审时,QA和研发人员很难直观获取到改动点和改动范围,中间又会花费大量时间来相互沟通,从而确认边界与现有逻辑的兼容性等问题。
② 研发人员的功能评估完全依赖经验
研发人员在方案设计时,很难直接获取到横向相关模块是否有类似功能点(可复用或可扩展),导致复用率低,同时在项目排期时完全依赖个人经验,且没有统一的参考标准,经常出现因工作量评估不准而导致项目延期的情况。
③ QA测试及评估效率低
QA在功能范围评估时,完全依赖研发同学(RD)的技术方案,且大多数也是通过口头交流的方式来确认功能改动涉及的范围和边界,在影响效率的同时,还会导致一些测试问题在整个项目周期中被后置,影响项目的进度。同时,平台化后基础JAR包的管理完全依靠人工,对一些Action,尤其是基础Action也没有统一的测试标准。以上问题可以概括如下:
4.4.1 目标
借助平台化,对项目交付的整个过程(如下图所示),实施产研新流程,以解决产品、研发与测试人员在迭代中遇到的问题,赋能业务,从而提升整体项目的交付效率与交付质量。
4.4.2 思考与落地
基于平台化实施产研新流程,即利用Stage/Action的方式来驱动整个项目的交付,如下图所示:
- 对于PM(产品):建设Stage/Action可视化能力,并在项目设计中应用。
- 对于RD(研发):统一采用新的基于Stage/Action的方案,设计及开发排期模式。
- 对于QA(测试):统一沟通协作语言-Stage/Action,并推动改进相关测试方法和测试工具
4.4.2.1 产品侧
下图所示的是产研功能建设后的应用与实践效果。前两张为建设的业务能力可视化,为PM提供一个了解各业务最新流程及详细Action能力的可视化功能,第三张图为产品设计中相关业务的调研与功能描述(出于数据安全原因,以下截图采用非真实项目举例说明)。
4.4.2.2 研发侧
根据项目开发周期中研发工作的不同阶段,我们制定了基于代码开发前后的流程规范,以保证整个开发周期中研发同学能充分利用平台的能力进行设计与开发提效。
开发前
- 技术设计:基于各业务涉及的现有Action功能与Action DAG的可视化能力,进行横向业务的调研参考与复用评估,以及新增或变更Action功能的技术设计。
- 项目排期:基于技术设计中Action能力的新增、变更、复用情况以及Action层级等,对开发工作量进行较为标准化的评估。
开发后
- Action沉淀:系统统一上报并定期评估平台Action能力的复用度和扩展情况。
- 流程反馈:追踪基于平台化的每个项目,并对交付流程中的相关指标做量化上报,同时收集项目人员反馈。
4.4.2.3 测试侧
- 采用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 总结与展望
本文分别从标准化、框架、产研新流程3个方面介绍了外卖广告平台化在建设与实践中的思考与落地方案。经过两年的摸索建设和实践,美团外卖广告平台化已经初具规模、有力地支撑了多条业务线的快速迭代。
未来,平台化会细化标准化的力度,降低业务开发同学成本;深化框架能力,在稳定性、性能、易用性方面持续进行提升。此外,我们在产研新流程方向也会持续优化用户体验,完善运营机制,不断提升产研迭代的流程。
以上就是外卖广告针对业务平台化上的一些探索和实践,在广告工程架构等其他领域的探索,敬请期待下一篇系列文章。
7 作者简介
乐彬、国梁、玉龙、吴亮、磊兴、王焜、刘研、思远等,均来自美团外卖广告技术团队。
招聘信息
美团外卖广告技术团队大量岗位持续招聘中,诚招广告后台/算法开发工程师及专家,坐标北京。欢迎感兴趣的同学加入我们。可投简历至:[email protected](邮件主题请注明:美团外卖广告技术团队)
阅读美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 安全 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2021年货】、【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至[email protected]申请授权。