在企业进行营销推广时,广告投放是不可或缺的一环。为了确保资金的有效使用,企业和广告优化师在大规模投放前,都希望找到效果最好的策略。然而,早期的决策往往只能依靠直觉或者简单的分流测试来做出。随着火山引擎推出的AB测试广告投放系统,这一局面得到了根本性的改善。该系统能够逐步帮助企业快速、科学地验证不同投放策略的平均转化成本数据效果,并根据实验报告得到计划中不同素材、不同落地页、不同人群包、不同预算等变量到底哪种更好。 广告投放AB实验背后的数据能力支撑繁琐而复杂,开启广告实验后,如果数据不能够及时准确的送达,会对报告结论造成影响,甚至影响最终决策,而这均依赖于AB实验平台底层的基础投放能力。基础投放能力主要包括账号授权管理、计划创编和数据查询三块。 账号授权是将广告账号授权给开发者应用;计划创编包括物料管理、落地页管理、应用管理、广告编辑;数据查询指广告投放数据的事实查询分析。一个广告投放AB实验的顺利开展,需要上述三个模块的紧密配合,才可保证最终结果的准确性。 早期,由于广告投放业务流程繁琐,火山引擎DataTester在广告投放AB实验项目的迭代中遇到了如下问题:1.需要支持多个广告平台,授权逻辑日益杂乱;2.授权、数据抓取和业务逻辑耦合严重,出现问题不易排查;3.一类数据抓取就对应一个定时任务,导致定时任务过多,难以维护;4.数据模型设计不合理,报表数据越来越多,查询变得缓慢;5.定制特性太多,代码难以维护。上述问题的积累导致实验平台的开发和维护成本越来越高,线上问题频发。为了保证平台实验数据的科学性和准确性,火山引擎DataTester决定对广告投放基础能力进行重构。 上图展示了火山引擎DataTester重构后的广告投放模块交互图,主要解决了以下问题:1.针对耦合严重、定时任务过多问题:服务拆分,根据业务功能拆分为授权服务、数据抓取服务、业务后端服务和少量定时任务,各类服务各司其职,职责单一;2.针对查询缓慢问题:重新设计数据模型,使用MySQL和ClickHouse存储元数据和报表数据,兼顾修改和查询效率;3.针对代码难以维护问题:引入DDD领域驱动设计思想,面向接口编程,不同广告平台分别实现接口,方便维护;4.针对代码质量问题。 在构建SaaS/私有化部署时,使用同一套代码并通过环境变量进行兼容是降低开发成本的关键。此外,授权服务、数据抓取服务和业务后端的协同工作确保了投放平台与广告平台数据的一致性,并提供了实时抓取接口以获取实时数据。
5. 针对SaaS/私有化部署问题:
使用同一套代码:通过底层利用环境变量实现不同环境的兼容,降低开发成本。
授权服务:对接各个广告平台的授权逻辑,将广告账号授权给预定义的开发者账号,保存Token或密码凭证,然后调用抓取服务下发账号粒度的抓取任务。
数据抓取服务:保证投放平台与广告平台数据一致性,添加天粒度和小时粒度的数据抓取任务,保证元数据和报表数据的及时更新;同时提供自定义间隔时间的Access Token刷新任务,以及实时抓取接口,方便实时数据的获取。
业务后端:使用授权的账号完成计划创编工作,对数据进行汇总查询。
授权分类
Oauth2授权:基于令牌Token的授权方式,无需暴露用户密码即可使应用获取对用户数据的有限访问权限,每个平台的Token过期时间不同,需要定时刷新以保证Token的可用性。
账号密码授权:简单直接,但存在密码泄露的风险。
OAuth2授权流程示例
注册开发者账号,并将开发者信息预先保存至数据库中。
将权限信息、开发者账户信息以及希望回调时带回的数据统一拼装至授权链接后跳转至广告平台。
用户点击授权,广告平台回调开发者账号填写的回调地址,并携带auth_code。
回调地址对应的服务处理该请求,根据auth_code获取Access Token和Refresh Token并保存至数据库。 DAG 是一种有向无环图,常用于表示任务调度和执行的流程。在数据抓取服务中,我们用 DAG 来定义任务对象的生成和执行过程。DAG 负责管理任务对象的生成和写入,Manager 负责管理 DAG 的生成和写入,Scheduler 根据 DAG 中的参数和时间生成任务下发至消息队列,Worker 负责具体任务的执行。 / 数据模型 / 广告数据可以分为两类,元数据和报表数据。元数据是指广告各个层级的属性数据,包括 ID、名称、创建时间等属性字段,而报表数据是指点击、展示、消耗等指标数据。对于各个广告平台的广告层级,各不相同。巨量引擎旧版曾使用账户-广告组-计划-创意四个层级,2.0则使用账号-项目-广告层级,百度是账户-推广计划-推广单元-创意四个层级,快手是账号-广告计划-广告组-广告创意。为了对接多个广告平台,需要拉齐广告数据。由于元数据需要经常的查询更新,可以存储在MySQL中。对于报表数据,每个渠道的指标数量和名称差异更大,同时多账号、小时级+天级的数据拉取会保存大量数据,为了保证拓展性和查询效率,可以将投放报表数据存储在 ClickHouse 中,ClickHouse 中的 Map 字段可以很好的支持报表类多字段的拓展性。在最终的查询分析时,需要综合MySQL和ClickHouse数据得到报告。 有向无环图(DAG)是一种网络结构,其中从一个顶点出发的路径无法经过多条边回到该顶点。这种图可以用于定义一组相互依赖的操作单元,并根据这些依赖性、容错性、并发性以及调度方式来扩展。在广告数据抓取领域中,报表数据的获取依赖于元数据的抓取。如果元数据不存在,那么报表数据也就无从谈起。因此,我们可以利用这种依赖关系来构造一个有向无环图。 在构建DAG时,可以添加一些属性字段来描述任务之间的关系。例如,利用JSON组织任务的依赖关系是一个不错的选择。以下是一个示例,展示了如何定义四个任务:dummy_task、account_meta_task和ad_meta_task,这三个任务是并列关系,可以同时执行;而ad_daily_insight_task则需要等待ad_meta_task完成后才会执行。
{
"schedule_interval": "*/60 * * * *",
"dag_id": "${account_id}_today_insights",
"tasks": [
{
"task_id": "dummy_task",
"downstream_task_ids": ["account_meta_task", "ad_meta_task"],
"is_dummy": true,
"operator_name": "DummyOperator",
},
{
"task_id": "account_meta_task",
"operator_type": "basic",
"operator_name": "ad_meta_operator",
},
{
"task_id": "ad_meta_task",
"downstream_task_ids": ["ad_daily_insight_task"],
"operator_name": "ad_meta_operator",
},
{
"task_id": "ad_daily_insight_task",
"operator_name": "insight_operator",
}
]
}
在处理大数据量的定时任务时,时间轮算法作为一种高效的任务调度机制显得尤为重要。通过将任务运行频率以Cron表达式的形式定义,时间轮算法能够显著提升任务执行的效率,尤其是在处理大量、高频率的任务时。 首先,时间轮算法的核心在于其独特的任务调度方式。传统的任务调度往往需要遍历所有的任务来寻找可执行的任务,而时间轮算法则通过在时间刻度上进行轮询的方式,避免了这一低效的遍历过程。例如,在一个包含10万任务的时间周期内,如果使用传统方法,可能需要遍历所有10万个任务才能找到可执行的任务,而时间轮算法仅需遍历24个时间刻度即可完成任务的选择。这种优化不仅提高了任务执行的速度,也大大减少了系统资源的消耗。 然而,当任务的执行精度要求更高时,如秒级甚至更细的时间粒度,时间轮算法的局限性便显现出来。此时,分层时间轮算法成为了解决这一问题的有效途径。通过构建多个时间轮,如天级和秒级的时间轮,可以在不同的时间尺度上并行处理任务,从而实现更加精细的时间控制和任务调度。例如,对于周粒度到秒粒度的任务,可以使用一个7*24小时的天级时间轮和一个3600秒级的秒级时间轮,通过精确的时间轮转换,确保每个任务都能在最合适的时间点被执行。 此外,数据抓取服务支持从周粒度到秒粒度的任务调度,这得益于采用了分层时间轮算法。通过将任务分散到不同的时间轮中,即使在面对海量任务的情况下,也能保证系统的稳定运行和数据的实时性。这不仅满足了对实时性要求极高的广告数据抓取需求,也保障了平台的数据报告能够及时、准确地生成。 对于业务代码来说,除了稳定性和可拓展性之外,领域驱动设计(DDD)也是提升系统性能和可维护性的关键。DDD通过将关注点集中在核心的业务逻辑上,使得代码结构更加清晰,逻辑关系更加明确。这对于处理复杂业务逻辑和紧密前后逻辑关联的广告创建尤为关键。通过领域模型的设计和实现,可以有效降低代码间的耦合,减少错误,提高系统的可维护性和可拓展性。 总的来说,时间轮算法和分层时间轮算法的结合,以及领域驱动设计的引入,共同构成了一个高效、稳定且易于维护的定时任务调度系统。这不仅满足了对实时性要求极高的广告数据抓取的需求,也为后续的业务迭代和扩展提供了坚实的基础。 在构建一个软件系统时,将业务概念和规则转换为可操作的软件类型及其属性和行为是至关重要的。通过合理运用面向对象的封装、继承和多态等设计原则,我们不仅能够降低系统的复杂性,还能增强其扩展性和适应多变现实业务问题的能力。 首先,领域层的设计至关重要。这一层负责数据和行为的绑定,将实体对象转化为充血模型,使得处理复杂业务逻辑变得容易。用户接口层则主要负责接收外部输入并返回结果,不包含业务逻辑,但可以进行简单的入参校验和数据封装。应用层则是用户接口层的直接使用者,它根据实际的use case调用领域层提供的能力,相当于工作编排。而基础设施层则提供了技术实现的具体细节,如数据库选择、数据存储方式以及缓存和消息队列的使用等。 接下来是单元测试的重要性。单元测试对于提高开发效率、代码结构设计以及代码质量都具有重要意义。它能帮助我们更高效地调试代码,缩小Bug的范围,并在修改代码时无需每次都回归全量测试。此外,单元测试还迫使我们编写高内聚低耦合的代码,并从使用方的角度重新思考接口设计是否合理。 然而,对于多人协作的项目,项目创建时的单测规划尤为关键。严格设置单测覆盖率和增量覆盖率门槛,确保没有达到目标则不允许合入。同时,在写单测时,我们需要破除外部依赖,确保测试的独立性和准确性。 总的来说,通过合理运用DDD设计方法,结合面向对象的封装、继承和多态等设计要素,我们可以有效地降低或隐藏整个系统的业务复杂性,使系统具有更好的扩展性,以应对纷繁多变的现实业务问题。 单元测试的独立性是确保其有效性的关键。在Go项目中,通过引入Mock和Stub工具,可以模拟真实依赖下的行为,从而避免外部依赖对测试过程的影响。同时,为了提高编写效率,我们可以使用断言工具来增强单测的能力。此外,对于数据库和缓存的依赖测试,Docker容器化提供了一种解决方案,使得构建Service、MySQL和Redis镜像成为可能。 在提升开发效率方面,利用gitlab和github提供的流水线工具,每次提交时自动运行单测,生成全量覆盖率和增量覆盖率,这可以显著提高开发效率。火山引擎DataTester作为字节跳动长期沉淀的成果,截至2023年6月已累计做过240万余次AB实验,日新增实验4000余个,同时运行实验5万余个。 广告实验是DataTester的重要一环,已经积累了丰富的广告场景实验经验。通过本次重构,服务的稳定性和可拓展性得到了大幅提升,并进一步增强了广告A/B实验能力。目前,DataTester服务了包括美的、得到、凯叔讲故事等在内的上百家企业,为业务的用户增长、转化、产品迭代、运营活动等各个环节提供科学的决策依据,将成熟的“数据驱动增长”经验赋能给各行业。 火山引擎A/B测试,限时免费,立即申请!A/B测试摆脱猜测,用科学的实验衡量决策收益,打造更好的产品,让业务的每一步都通往增长。火山引擎首度发布增长助推「火种计划」,火山引擎A/B测试作为「火种计划」产品之一,将为您免费提供2亿事件量和5万MAU,以及高达12个月的使用权。后台回复数字“8”了解产品。