近年来爱奇艺快速发展,优质内容层出不穷,爱奇艺广告也随之发展和壮大,广告在线服务同时服务于品牌、中小、DSP 等不同客户,形成了可以满足不同需求类型的较为完善的商业广告变现布局,广告库存涵盖视频、信息流、泡泡社交(爱奇艺的社交平台)和开机屏等多种场景。爱奇艺效果广告是 2015 年开始全新搭建的一个广告投放平台,随着信息流业务的增长,整个投放平台也经历了一次大的架构调整和多次重要的升级优化。
爱奇艺广告投放平台的概要架构如下图所示。本文主要介绍在线服务相关的内容,在线投放服务即图中虚线所框出的部分,主要包括在线的投放和计费服务。
架构肯定是为业务需求而生的,先来看看我们面对的业务需求及其特点。
爱奇艺效果广告投放平台目前采用代理商模式,平台主要满足两大类业务需求:面向代理商(广告主)的和面向产品及运营团队的需求。具体来看看。
1、面向代理商的需求: 本质上是要帮助代理商降低转化成本
-
支持多种广告位:贴片、暂停、浮层、信息流、视频关联位和推荐位等
-
支持多种结算类型:支持 CPC、CPM 和 CPV 等广告结算类型,oCPC 结算方式在规划中
-
丰富的定向功能:常用定向维度(平台、地域等)及人群精准定向(地域定向 - 支持区县级别、人群属性定向和 DMP 人群定向),关键词定向
-
灵活的排期及预算设置:支持分钟粒度的排期设置,支持日预算的任意增减
-
特殊的业务功能:广告去重功能、动态创意、创意优选和平滑消耗等,都是为了提升广告的转化效果
-
频次控制:避免对相同用户短时间的大量曝光
2、面向产品及运营团队:主要是提升产品控制能力,促进整体系统的良好运转
-
流量控制:通过黑白名单控制某些流量上不可以 / 可以投放哪些广告
-
AB 测试功能:影响较大的功能全量发布之前需要进行 AB 测试以确认效果符合预期
-
计费相关:延迟曝光不计费,曝光、点击异常检测及过滤
-
负反馈:根据用户反馈自动调整广告投放策略优化用户体验,同时也是对广告主的一种制约
从上面描述的业务需求可以看出,业务的特点有:
-
业务逻辑复杂:流程包括很多环节(场景信息获取,广告召回,预算控制,频次控制,点击率预估,创意优选,平滑消耗,广告去重,结果排序,结果筛选,概率投放,AB 测试);下图中绿框的部分仅展示投放服务的主要流程:
-
业务变更非常快:平均每周 5 次的系统功能变更;
-
广告主数量多,订单量大,订单平均预算较小,并且订单设置会频繁变化。
爱奇艺效果广告于 2016 年正式上线。起步伊始,业务逻辑简单,广告和订单数量较少,整体架构相对比较简单。为了快速完成系统的搭建和上线应用,复用了品牌广告投放平台的架构,并做了剪裁,系统架构图如下:
接入层
包括 QLB(iQiYi Load Balance)、Nginx 前端机,主要做流量的反向代理和整体的限流与降级功能。
流量分发层
:包括策略服务和流量平台服务;策略服务支持公司层面的策略控制和日常的运营需求;流量平台服务主要控制流量在各投放平台上的分配和请求逻辑,投放平台包括品牌广告投放平台,效果广告投放平台和外部 DSP。
投放服务
:前文介绍的业务逻辑都包含在这里,由单一的模块来实现。
日志收集
:接收曝光点击等日志,主要完成计费、频控和去重等业务逻辑,也是由单一的模块来实现。
计费系统
:利用 Redis 主从同步机制把订单的实时消耗数据同步到投放服务。
频次系统
:使用 Couchbase 机群来做用户数据存储。
数据同步层
:这一层涉及的数据种类很多,其中相对较重要的有两种:业务数据和日志数据,业务数据主要包括广告的定向、排期和预算等内容。
我们利用业务数据做了两方面的优化工作:
-
通过业务数据分发一些对时效性要求不高的数据给到投放服务,避免了一些网络 IO;
-
在业务数据中进行空间换时间的优化,包括生成索引及一些投放服务所需要的数据的预计算,譬如提前计算计费系统中的 key 值。
随着业务增长,架构也遇到了一些挑战。
-
流量增长:系统上线之后很好地满足了广告主对转化效果的要求,这个正向的效果激发了广告主对流量的需求,为此产品和运营团队不断地开辟新的广告位,同时爱奇艺的用户数和流量也在持续增长,这些原因共同为效果广告平台带来了巨大的流量。
-
广告主数量和订单数量增长:这个增长包括两方面,一方面与流量增长相辅相成,相互促进;爱奇艺的优质流量和良好的转化效果吸引了更多的广告主;另一方面,由于商务政策上的原因,广告主和订单量在季度末会有阶段性的增长。
-
性能问题:流量和订单量的增长使得系统的负载快速增加,因为订单是全量召回的,当订单量增长到一定数量之后,会使得长尾请求增多,影响整体服务性能,无法通过水平扩容解决。
-
超投问题:由于曝光和点击的延迟,以及投放计费环路的延迟,不可避免的存在超投问题,这也是广告系统的固有问题;品牌广告是先签订合同,投放量达到即可按照合同收款,超出部分不会对广告主收费,品牌广告预定量都很大,超投比率较小;和品牌广告不同,效果广告实时扣费,如果沿用品牌思路的话,超投部分会造成多余的扣费,而中小广告主对此非常敏感,也会增加技术团队问题分析排查工作,同时因为效果广告的预算少,预算调整变化很快,使得超投比率要比品牌广告大;针对效果广告的超投问题,技术团队要做的事情分成两个层面,一是保证超投的部分不会计费,不给广告主带来损失,二是从根本上减少超投,即减少我们自己的收入损失;分别称为
超投不计费
和
减少超投
;
针对上面的几个情况,我们的架构做了调整,如下图:
对比上线伊始的架构,此阶段架构调整体现在以下几个方面:
-
投放服务性能优化 – 包括索引分片和增加粗排序模块,主要解决了上述流量增长、广告主数量订单增长等方面带来的性能问题
-
索引分片是把原来的一份索引拆分成多份,对应的一个请求会被拆分成多个子请求并行处理,这样每个请求的处理时间会减少,从而有效减少长尾请求数量。
-
粗排序:全量召回的好处是收益最大化,缺点是性能会随着订单量增加而线性下降;粗排序在召回阶段过滤掉没有竞争力的低价值的(ECPM 较低的)广告,低价值广告被投放的概率和产生转化的概率很低,因此粗排序的过滤对整体收入影响很小,同时能有效减少进入后续核心计算逻辑(包括精排序及其他的业务逻辑)的订单数量,使得服务压力不随订单量而线性增长。
-
计费服务架构优化 - 主要是提升系统的可扩展性和解决超投问题
可扩展性通过服务拆分来解决,把单一模块的计费服务拆分成三个模块,拆分之后日志收集模块对外提供服务,主要职责是接收日志请求并立即返回,保证极低的响应时间;然后对计费日志和非计费日志进行不同的处理;检测过滤模块主要职责是进行定向检查和异常日志识别。计费服务把有效计费数据更新到计费系统。拆分成三个模块之后,每个模块都很简单,
符合微服务基本原则之一:单一职责
。
关于超投, 先看第一个问题:
超投不计费。
主要难点在于:
-
同一个广告的计费请求是并发的;
-
计费系统是分布式的,出于性能考虑,请求的处理流程需要是无锁的。
我们在计费系统中解决这个问题的思路如下:
首先,要严格准确地计费,就要对并行的请求进行串行处理,Redis 的单线程模型天然满足串行计费的需求,我们决定基于 Redis 来实现这个架构,把计费的逻辑以脚本的形式在 Redis 线程中执行,避免了先读后写的逻辑,这样两个根本原因都消除了。
接下来的任务就是设计一个基于 Redis 的高可用高性能的架构。我们考虑了两种可选方案。
方案 1:数据分片,架构中有多个主 Redis,每个主 Redis 存储一个分数分片,日志收集服务处理有效计费请求时要更新主 Redis;每个主 Redis 都有对应的只读从 Redis,投放服务根据分片算法到对应的从 Redis 上获取广告的实时消耗数据。
该方案的优点是可扩展性强,可以通过扩容来解决性能问题;缺点是运维复杂,要满足高可用系统架构还要更复杂;
方案 2:数据不分片,所有的计费请求都汇聚到唯一的主 Redis,同时只读从 Redis 可以下沉到投放服务节点上,可以减少网络 IO,架构更加简洁;但主 Redis 很容易成为性能的瓶颈;
在实践中我们采用了第二种
不分片
的方案。主要基于以下考虑:
在业务层面,效果广告中有很大比率的是 CPC 广告,而点击日志的数量相对较少,基本不会对系统带来性能压力;对于剩下的 CPM 计费的广告,系统会对计费日志进行聚合以降低主 Redis 的压力;因为从 Redis 是下沉到投放上的,可以不做特殊的高可用设计;主 Redis 的高可用采用 Redis Sentinel 的方案可以实现自动的主从切换,日志收集服务通过 Sentinel 接口获取最新的主 Redis 节点。
在串行计费的情形下,最后一个计费请求累加之后还是可能会超出预算,这里有一个小的优化技巧,调整最后一个计费请求的实际计费值使得消耗与预算刚好吻合。
关于超投的第二个问题
减少超投
,这个问题不能彻底解决,但可以得到缓解,即降低超投不计费的比率,把库存损失降到最低;我们的解决方案是在广告的计费消耗接近广告预算时执行按概率投放,消耗越接近预算投放的概率越小;该方法有一个弊端,就是没有考虑到广告的差异性,有些广告的 ECPM 较低,本身的投放概率就很小,曝光(或点击)延迟的影响也就很小;针对这一点,我们又做了一次优化:基于历史数据估算广告的预算消耗速度和计费延迟的情况,再利用这两个数据来修正投放概率值。
这个方案的最大特点是实现简单,在现有的系统中做简单的开发即可实现,不需要增加额外的系统支持,不依赖于准确的业务场景预测(譬如曝光率,点击率等),而且效果也还不错;我们还在尝试不同的方式继续进行优化超投比率,因为随着收入的日渐增长,超投引起的收入损失还是很可观的。
微服务架构现在已经被业界广泛接受和推广实践,我们从最初就对这个架构思想有很强的认同感; 广告在线服务在 2014 年完成了第一版主要架构的搭建,那时的微观架构(虚框表示一台服务器)是这样的:
在同一台机器上部署多个服务,上游服务只请求本机的下游服务,服务之间使用 http 协议传输 protobuf 数据,每个机器都是一个完备的投放系统。
这个架构有很多的优点:结构清晰,运维简单,网络延迟最小化等。
当然也有一些缺点,同一台机器上可部署的服务数量是有限的,因而会限制架构的增长,多个模块混合部署不利于整体的性能优化,一个服务的异常会影响整个机器的服务质量;这个架构在微观上满足了单一服务的原则,但在宏观上还不是真正的微服务化,为了解决上面的一些问题,按照自然的演进我们必然走上微服务化这条路;我们从 16 年中开始进行微服务化的实践。
微服务化过程中我们也遇到了很多问题,分享一下我们的解决方法及效果:
1. 技术选型问题
RPC 选型,必须满足的条件是要支持 C++、protobuf 协议和异步编程模型。最初的可选项有 sofa-pbrpc、pbrpc 和 grpc,这三者中我们选中了 grpc,主要看中了它通用(多语言、多平台和支持代理)、流控、取消与超时等特性;在我们选定 grpc 之后不久百度开源了它的高性能 rpc 框架 brpc,相比之下 brpc 更具有优势:健全的文档,高性能,内置检测服务等非常多的特性;为此我们果断地抛弃了 grpc 和已经在上面投入的一些开发成本,快速地展开了 brpc 相关的基础功能开发和各服务的改造。
名字服务选型,排除了 zookeeper,etcd 等,最终选定的是 consul+consul template 这个组合,它很完美地支持了我们的业务需求;除服务注册与发现外,还有健康检查,服务列表本地备份,支持权重设置等功能,这些功能可以有效地减少团队成员的运维工作量,增强系统的可用性,成为服务的标准配置。
2. 运维成本增加
这是微服务化带来的问题之一,微服务化要做服务拆分,服务节点的类型和数量会增多,同时还要额外运维一些基础服务(譬如,名字服务的 Agency)。考虑到大部分运维工作都是同一个任务在多个机器上重复执行,这样的问题最适合交由机器来完成,所以我们的解决方案就是自动化运维。我们基于 Ansible 自研了一个可视化的自动运维系统。其实研发这个系统最初目的并不是为了支持微服务化,而是为了消除人工运维事故,因为人的状态是不稳定的(有时甚至是不靠谱的),所以希望由机器来替代人来完成重复的标准动作;后来随着微服务化的推进,这个系统很自然地就接管了相关的运维工作。现在这个系统完成了整个团队 90% 以上的运维工作量。
1. 问题发现和分析定位
业界通用的方式是全链路追踪系统(dapper & zipkip)和智能运维,我们也在正在进行这方面的工作;除此之外,我们还做了另外两件事情:异常检测和 Staging 环境建设;
-
异常检测:主要是从业务层面发现各种宏观指标的异常,对于广告投放系统、库存量、曝光量、点击率和计费率等都是非常受关注的业务指标;异常检测系统可以预测业务指标在当前时刻的合理范围值,然后跟实时数据作对比;如果实时数据超出预测范围就会发出报警并附带分析数据辅助进行问题分析;这部分工作由在线服务和数据团队共同完成,这个系统有效地提高了问题发现的效率。
-
Staging 环境建设:系统变更(包括运维和新功能发布)是引起线上故障的主要原因,所以我们需要一个系统帮助我们以很小的代价快速发现变更异常。
在功能发布时大家都会采用梯度发布的方法,譬如先升级 5% 的服务,然后观察核心指标的变化,没有明显异常就继续推进直到全量;这个方法并不是总能有效发现问题,假如一个新功能中的 bug 会导致 1% 的订单曝光下降 50%,那么在全量发布之后系统的整体曝光量也只有 0.5% 的变化,也可能因为其他订单的填充使得整体曝光量没有变化,所以仅通过整体曝光量很难发现这个问题。只有对所有订单的曝光量进行对比分析才能准确地发现这个问题。