您当前位置:资讯中心 >服务器 >浏览文章

浅谈B站效果广告在线推理服务的性能优化

来源:互联网 日期:2023/12/29 12:12:04 阅读量:(0)

一、引言

作为国内领先的在线视频平台,哔哩哔哩(以下简称“B站”)正经历着业务体量和用户规模的快速增长。随着访问量的持续增长和业务复杂程度的增加,在相对有限的服务器资源下如何优化在线服务性能和提高资源利用率,成为了工程研发团队面临的重要挑战之一。

本文将以笔者所在的商业技术中心为例,重点讨论效果广告引擎的在线推理部分。文章将分享笔者在实际工作中遇到的挑战及相应的优化方案。首先,将介绍项目背景和当前系统的运行状况;接着,将详细探讨性能指标量化、服务调用、CPU计算、内存治理及网络IO等方面的优化策略;最后,将总结对性能优化的一些思考,并展望未来性能优化的方向。本文的目的是回顾并总结当前在线服务性能优化的工作,同时也希望这些经验能为其他研发人员在处理类似问题时提供参考和启发。

二、项目背景

笔者所在的团队主要负责在线效果广告引擎的研发工作,该服务作为商业化系统的重要组成之一,为公司带来了实质性的商业贡献。通过精准高效的广告投放,能够为公司带来稳定且可观的广告收入,成为支撑平台发展的关键营收来源之一,进一步支持了平台的内容创新和技术研发,构成良性循环。对于广告主而言,效果广告引擎提供了精准定向用户的能力,显著提升了广告传播的效果,为其带来更高的广告转化和投资回报。对于用户而言,通过更贴近用户行为习惯的广告投放,确保了广告内容与用户兴趣和需求的高度匹配,最大限度地保障了用户体验。

随着效果广告业务的快速发展,处理的业务复杂度不断提升,对在线服务的处理效率和吞吐量提出了更高要求。同时,B站的用户规模和使用时长的持续增长也加大了这一挑战。以在线推理服务为例,它需要对广告创意候选集进行一系列预估打分,主要包括特征计算和模型计算两个环节。特征处理阶段涉及用户和广告数据的提取、过滤、拼接等操作,随着特征数据的深入挖掘和应用,所需要处理的数据量也在不断增加。在模型计算阶段,支持的模型类型从LR、FM模型逐渐升级到DNN模型,增强了模型的表达能力,但同时也加大了算力资源的消耗。类似的资源开销增长问题也存在于效果广告引擎的其他服务中。因此,工程研发团队面临的挑战在于如何有效地对效果广告引擎进行性能优化,确保在硬件资源相对有限的情况下,依然能够支持并促进业务的持续增长。

三、系统现状

首先需要介绍一下效果广告引擎的系统构成,主要包含了以下几个服务:

检索引擎:作为广告业务的入口,接受来自各个调用方的请求,并且会对流量进行预处理,其中包括对流量进行实验分组和标记。

效果广告检索服务:作为效果广告的业务核心,负责对候选集中的广告创意进行优选,并且将胜选的结果回传给检索引擎。

召回/粗排服务:根据流量的上下文信息,从所有在投的广告创意中挑选出一批符合条件的广告创意,并且进行粗排打分,将最终的Top N作为候选集返回给效果广告检索服务。

推理服务:负责对候选集中的广告创意进行一系列精排打分,将最终结果返回给效果广告检索服务。

此处需要说明的是,由于本文的重点是在线推理服务,因此对于广告引擎中的其他部分进行了大幅简化,实际的效果广告引擎要更为复杂。为了进一步便于理解,使用下图来说明简化后的效果广告引擎内部各服务之间的调用关系及主要功能模块:

图1 效果广告引擎调用关系及主要功能模块图1 效果广告引擎调用关系及主要功能模块

目前效果广告引擎的在线集群规模已经达到了数千台服务器,其中在线推理服务的CPU资源占比约为整体的45%,召回/粗排服务占比约为21%,效果广告检索服务占比约为10%。通过CPU资源的分配比例,可以直观反映出各服务之间计算复杂度的差异,同时也揭示了系统中存在的潜在性能瓶颈。推理服务作为系统资源开销最大的在线服务,对其进行性能优化的收益也是最为显著的。

在对效果广告引擎的背景及现状有了初步的了解之后,下面本文将针对推理服务的各项优化手段进行更为详细的介绍。

四、优化手段

在实际工作中,对于在线服务的性能优化首先要建立在性能度量的基础上,因此在开始优化之前,需要对在线服务的各项数据指标进行可量化的测量和分析。

性能指标量化

从宏观角度来看,可以通过埋点的方式对在线服务的各个模块耗时进行监控和分析,定位到耗时较高的模块。之后,可以通过更细粒度的埋点或者日志,来找到开销较高的操作,并进行性能优化。同时作为在线服务,效果广告引擎的各服务之间的调用耗时也是需要监控的。受益于服务使用的BRPC框架,效果广告引擎的子服务都实现了较为完善的监控指标,包括各模块之间的平均耗时、中位数耗时、97线耗时等,并且对于各类远程调用也都有对应的耗时监控指标。依靠这些能够被量化的数据,我们能够快速定位到哪些模块和调用的耗时较高,并且能够在开发人力有限的情况下,给出性能优化的先后顺序,尽可能提高单次性能优化的收益。需要特别注意的是,在确保性能指标不失真的前提下,可以对性能指标的收集和上报进行一定程度的采样操作,主要是为了防止性能度量本身给服务带来过大的额外算力开销。

服务调用

在得到较为完善的性能指标之后,就可以结合对于推理服务的业务理解,从业务流程和服务调用的角度对在线服务进行全局分析。这部分的优化思路主要是在处理一次用户请求的过程中,减少数据的重复计算,并且降低数据传输的成本。

在较早的设计中,效果广告检索服务会将候选集中的广告创意拆分成多个推理请求,并行发送给多个推理服务节点,从而确保单个请求的处理耗时不会较高。如上文所述,推理服务需要获取用户侧的数据来进行特征处理,这些数据存放在Redis集群中。因此在处理每一个推理请求时,推理服务都需要单独访问一次Redis来获取用户数据,造成了Redis服务端访问较多,并且数据重复传输的问题。通过将访问Redis的操作上移至广告检索服务,然后再发送给推理服务的方式,有效减少了对Redis服务的访问量,降低了Redis服务端的算力开销和网络IO开销。

此外,在对早期方案重构的过程中,我们对服务调用之间所使用的数据格式也进行了升级,将原本类似JSON的数据处理方式,升级成了基于Protobuf3的数据处理方式。相比于文本格式的JSON,PB编码的数据通常更小,并且拥有更快的序列化和反序列化速度,这在处理大量数据时尤其重要。同时,将推理请求中的字段类型与特征计算中所需要的数据类型进行对齐,减少了大量的字符串转化及数据校验操作,降低了CPU算力开销。

这一类问题看似比较基础,但是在早期引擎架构快速迭代的过程中,由于不同阶段的各种原因,导致各个服务之间的设计无法完全一致,一些细节问题是比较容易被忽略的。随着业务的迭代和增长,这类小问题的影响就会被逐渐放大,导致服务性能下降和算力资源浪费。因此,定期对在线服务的业务和架构进行梳理回顾,是保障服务健康稳定的重要手段之一。

CPU算力

将视角聚焦到推理服务中,对于单次推理请求,我们同样也可以使用减少数据重复计算的方式来降低CPU算力开销,并且可以使用Perf性能分析工具,来进一步优化热点函数的算力开销。

首先在进行特征计算的过程中,包含了对于用户侧特征和广告侧特征的处理,其中用户侧特征的计算结果是能够被重复使用的。在推理服务的处理过程中,单次请求中的多个广告创意,会使用多线程并行的方式进行处理,此时会先将用户侧数据与单个广告创意进行计算,将结果存储在特征计算的运行时对象中,并且通过标记来区分用户侧特征和广告侧特征。然后,将其中的用户侧特征计算结果复制到其他线程的运行时对象中,再启动线程进行并行计算。这样既可以使用多线程来提高批量广告的特征计算处理速度,又不会因为重复计算用户侧数据而造成额外的算力开销。

进一步的,通过使用Perf性能分析工具,可以观察到具体某段代码的执行效率,并且分析出主要的性能开销点。在实际工作中,由于推理服务本身的迭代较为频繁,我们会定期对服务性能进行评估和回顾。当发现存在性能热点时,会优先进行性能优化,常见的代码优化手段有:

  1. 减少分支:分支预测失败会导致CPU流水线刷新,浪费大量的CPU周期。尽可能地减少分支,或者尽量使分支预测更加准确,可以帮助提高代码的性能。
  2. 循环展开:循环展开可以减少分支和循环开销,同时也可以提高指令级并行性。但是也要注意,过度展开可能会增大代码体积,对指令缓存造成压力。
  3. 数据局部性优化:尽可能地保持数据的局部性,使得数据能够高效地利用CPU缓存。这包括空间局部性(访问相邻的数据项)和时间局部性(短时间内重复访问同一数据项)。
  4. 向量化:利用CPU的SIMD指令集,可以同时对多个数据进行操作。在编写代码时,尽可能使数据结构和算法可以利用SIMD指令进行向量化操作。

针对这些优化手段,下面笔者会提供一些实际工作中遇到的具体事例以作参考。

1. 使用__builtin_expect内建函数来提供分支预测的提示,该函数会给GCC编译器提示,告知其某个条件判断的结果更可能是true还是false,通常用于优化代码中高度可能或者不可能执行的分支。在实际编写代码的过程中,该函数通常与宏一起使用,包括Linux在内的各种代码中都封装了自己likely和unlikely宏来提高性能。

2. 使用循环展开来提高代码性能,下面这段代码是通过循环展开来优化数据构建的例子,需要注意的是,当批量处理完展开部分的循环体之后,还需要处理剩余的迭代。

// 循环展开
for (uint32_t idx = start_idx; idx + 3 < end_idx; idx += 4) {
    result[value[idx]].emplace_back(feaid, ins);
    result[value[idx + 1]].emplace_back(feaid, ins);
    result[value[idx + 2]].emplace_back(feaid, ins);
    result[value[idx + 3]].emplace_back(feaid, ins);
}
// 处理剩余的迭代
for (uint32_t idx = end_idx - (end_idx - start_idx) % 4; idx < end_idx; ++idx) {
    result[value[idx]].emplace_back(feaid, ins);
}
关键字:
声明:我公司网站部分信息和资讯来自于网络,若涉及版权相关问题请致电(63937922)或在线提交留言告知,我们会第一时间屏蔽删除。
有价值
0% (0)
无价值
0% (10)

分享转发:

发表评论请先登录后发表评论。愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。