大型广告系统架构 — 检索模块

article/2025/11/6 23:52:56

广告系统主要解决一个问题:在给定展示场景、用户的情况下,返回收益最大化的广告。下图是一个广告系统最简单的架构图。其中,Router,检索模块,排序模块一般称为广告系统的核心。同时,与之辅助的至少包含三大系统:特征计算系统,计费系统,投放系统。



先介绍一下三个辅助系统的主要功能:

  • 特征计算系统:实时计算广告展示环境 (网页,APP) 的特征,用户的特征。并提供实时查询功能。

  • 计费系统:实时处理广告的展现 (CPM)、点击 (CPC)、转化 (CPA)数据,并计算广告的剩余预算。需要包括反作弊功能。

  • 投放系统:供广告主使用,设置广告的基本信息和定向条件。


核心部分包含了三个模块:

  • Router:对外提供HTTP服务。接收请求后,依次与特征计算系统、检索模块、排序模块交互,最后返回广告。

  • 检索模块:检索模块主要解决相关性问题。首先,根据广告主设定的定向条件筛选出本次请求能否返回的广告;然后,按多种定向策略筛选出与本次请求最相关的若干个广告。

  • 排序模块:排序模块主要解决收益最大化的问题。通用的排序标准是eCPM。以CPC广告系统为例,eCPM=eCTR*CPC,CPC是广告主设定的,排序模块的核心就是使预估的eCTR尽可能地接近实际CTR。




本文讨论检索模块的架构设计。在检索模块中使用的数据一般可以分为三类:

  1. 广告本身的信息。例如ID,标题,描述,出价,投放时间,预算余额等。

  2. 广告的定向数据。例如国家,设备,人群等。

  3. 各个定向策略使用的内部数据。例如,预先计算出的各个广告的特征,人群的兴趣点扩充数据等。


同样,讨论架构先说业务特点。

  1. 大型广告系统中,单机的瓶颈首先出现在内存上,通常内存的使用率在80%以上,只剩余20%的内存空间实际上是非常危险的,后文中会阐述原因。在使用的内存中,可以假定70%是广告数据,30%是各策略使用的内部数据。

  2. 数据的更新量占数据总量的比例很小,一般更新量会比总量少两个数量级。

  3. 数据存在热点。例如,一些热门搜索词会被大量广告主购买,一些热门的人群也会被广告主大量购买。

带着这些前提,接下来讨论检索模块的几个主要问题。


  • 冷启动

检索模块在提供服务前,需要将最新的广告数据和策略数据加载到内存中。假定检索模块使用的是内存为64G的机器,按内存使用率80%计算,系统启动的时候需要把50G的数据加载到内存,可以想象这是一个耗时很长的操作。另外,广告系统一般都包含正排索引和倒排索引两种数据,倒排索引是根据正排索引建立的,所以系统启动的时候不仅仅是加载数据,还存在计算的过程,当然,在不存在指针的情况下,倒排索引也可以预先计算好,可以减少启动时间。


全量与增量。因为广告数据是实时在变化的,所以一般的方案是定期对数据库做快照,并记录下时间点。系统启动时,加载最新的一个快照的数据,并且加载完所有该快照生成时间之后的所有更新,才可以对外提供服务。快照一般称为全量,更新一般称为增量。另外,这里说的时间,不一定是真实的时间,也可以是逻辑时间。例如,创建一个高可用服务,对外提供始终自增的id,每一条数据更新都关联一个唯一的id,做快照的时候,快照的id就是最新一条记录的id。


预生成索引文件。数据库的快照一般是以文本文件的形式存储的。但是,系统在启动的时候处理文本文件,是非常耗时的。所以一般会使用一台单独的机器预先加载快照文件,将数据转换为与检索模块相同的二进制的格式,再dump到二进制文件中,这样检索模块启动的时候直接加载二进制文件,可以达到很好的性能。注意,为了提高加载数据的效率,二进制数据往往没有经过任何序列化,因此建立索引的服务必须使用和检索模块同样的操作系统和配置。


  • 内存数据结构设计

检索模块使用的数据结构必须具备两个特性:高效存储,高效更新。


高效存储是指如何使用尽可能小的内存空间存储给定的数据。因此工程师需要对内存进行精确的控制(内存中的数据排列,分配和释放),这也是为什么大多数对内存要求严格的系统都使用C/C++开发的原因(无意引起语言之争)。位操作在检索模块中很常见,同时在设计结构体时,要充分考虑数据对齐(可参考的资料很多,这里不展开)。下面重点介绍两种常见的技术。


MemoryPool

MemoryPool是C++中常用的内存分配技术。大的对象或数组往往是分配在堆上,系统中时刻都有不同的对象被创建和销毁,如果每次都使用 malloc/free 去创建/销毁对象,则容易造成内存碎片。MemoryPool的作用是,为常见的对象批量申请内存并预留,在系统需要创建对象的时候,从预留的内存中选择一块空间,作为新的对象,供系统初始化和使用。在销毁对象时,并没有实际销毁这段内存空间,而是将该空间记录在MemoryPool内部的列表中 (freelist),在下次分配空间的时候优先从该列表分配。


常见的MemoryPool提供Create和Destroy两个方法。

  • Create即创建一个对象,如果MemoryPool中有预留的空间,则在预留的空间上分配,否则批量向系统申请空间。为了提供效率,Create方法常常返回指针而不是ID,因此MemoryPool不能移动其内部已经分配的空间。在Create方法返回后,常见的是使用Placement Constructor初始化对象。

  • Destroy方法接受指针作为参数,该指针指向的内存地址必须是MemoryPool分配的。Destroy方法可以调用对象的析构函数,然后将该指针加入到freelist。


以上讨论的是固定长度的MemoryPool,一般一个结构体对应一个MemoryPool。还有另一种可以存储多种长度的对象的MemoryPool,在实际使用中比较少见,想了解这方面的设计可以参考TCMalloc。


B+树

B树及其变种是数据库常用的数据结构。在内存-磁盘式数据库中,类B树的数据结构允许系统只将一部分节点加载到内存;在全内存数据库中,类B树的数据结构允许系统只将一部分节点的数据放入CPU的缓存中。类B树的数据主要有以下几个优点:

  1. 数据的插入和删除性能相对稳定。

  2. 在内存不足时,允许只将用户查询的索引数据全部或部分放入内存,提高查询效率

  3. 在执行新增和删除操作是,只需要修改很少的节点(修改次数与Fanout有关)

  4. locality性比较好,尤其在系统存在热点数据的情况下更为明显。


实际使用中,B+树是比B树更好的选择。B+树相比B树,主要有两个优点:

  1. 索引和数据相分离,内部节点只保存索引数据。内存中相同大小的内部节点,B+树可以存储更多的索引。

  2. 叶子节点存储了全部的数据,并且叶子节点通过指针关联,可以方便地按顺序遍历全部或者部分数据。


B+树在使用过程中最常见的问题是,往往由于使用方式的不当,导致节点没有被填满,导致巨大的内存浪费。


在实际实现中,B+树的节点往往会根据Fanout预留全部的内存空间。以插入数据为例,在节点发生分裂时,分裂后的两个节点都会有一定的空间未被使用,如果没有合理地选择插入数据的顺序,会造成大量的节点未被充分使用。


例如,我们要将1到10十个数字插入到一颗空的B+树种,如果顺序插入,会得到如下的结果:



如果我们熟悉B+树的分裂策略,再稍微仔细的计算一下插入顺序,会得到不同的结果:



可以看到,顺序插入使用了导致B+树生成了八个节点,打乱一下次序只需要七个节点即可。不要小看这一个节点,当Fanout很大时,会节省非常可观的内存。笔者曾经通过优化B+树的内部组织,为单机节省了7G的内存。


细心的读者可以发现,在已知全部数据的情况下,是可以计算出插入B+树的最优方案的。前文提过,广告系统的数据更新量相比全量会少两个数量级,因此系统启动结束之后,内存的布局基本也就固定了,另外系统会随着上线被频繁重启,因此,解决了系统启动时候B+树的内存布局,也就解决了大部分问题。B+树的最优插入方案留给读者去思考。


  • 数据更新

广告的数据和策略使用的数据都需要更新,但特点不同。广告数据对实时性要求非常高,广告主期望在秒级更新,但一天的更新量可能只占全量的1%不到。策略使用的数据往往需要通过大量的离线计算才能生成,往往几个小时甚至一天才需要更新一次,但更新的量很大,可能大部分数据都需要被更新。


增量更新 (INC)

广告数据对实时性要求高,因此一般采用增量更新的方案。回想本文开始的检索端架构图,投放系统和计费系统将广告数据更新到DB,同时发送给Message Queue,MQ中的每条数据都带着一个表示逻辑时间的ID。在各个检索模块的服务器上部署一个单独的服务从MQ订阅数据,该服务在接收到数据后,将数据以文件的形式持久化到磁盘。检索模块中有一个线程,定期从文件中读取更新的增量数据,并更新到内存中。


难点在于,检索模块在数据更新的同时,还需要继续提供服务,不能出现因为更新数据而锁住数据的情况。回想前文所说的,为了提高效率,系统中的数据都使用MemoryPool存储,MemoryPool对外暴露的都是指针,在一次请求过程中基本都在通过指针读取数据,很少有Copy数据的情况。因此,不能修改当前正在检索的数据。


这里一般使用Copy-On-Write的更新策略。即,同一个Key关联的数据有多个版本,在更新数据时,不更新原始的数据,而是Copy一份原有数据,并在此基础上最修改,新的数据比原有数据的版本号更大。在更新的过程中,老版本的数据依然可用;在下次查询中,返回的是最新版本的数据,同时在合适的时机删除老版本的数据。事实上,这个策略在目前的NoSQL系统中普遍存在。


Copy-On-Write的难点在于,如果保证Copy的数据尽量的小,最好只Copy更新的那个数据(实际上很难实现)。结合上文,读者可以考虑如何实现一个支持多版本数据的B+树,并且在Copy的时候尽可能少地Copy节点。


全量更新 (Reload)

策略数据对实时性要求不高,但更新的数量可能会很大,一般会使用Reload的方式进行更新。Reload更新最典型的方案是双Buffer切换。即,将新的数据全部导入到一个新的数据对象(Hash表)中,导入完毕后,使用新的对象替代老的对象。


Reload方法更新的数据,最大的好处是可以假定内存中的数据结构是只读的,不会出现增量更新方式中对数据结构进行插入和删除的操作。因此,可以使用一些更加简单高效的数据结构。


如果使用双Buffer切换的方式进行Reload数据,需要注意在更新的过程中,该数据的内存使用量会翻倍;如果多份数据同时进行Reload,会导致内存使用突然激增,甚至会超出系统极限,导致程序崩溃。这样的案例在生产环境中真实的出现过,虽然系统会自动重启,但如果大量机器同时触发这个情况,会出现非常危险的雪崩效应。这也是前文提到的,系统的内存使用量在80%以上,其实是有很大的风险的原因。因此,在多份数据都使用Reload的方案更新时,要注意好控制内存的使用,通过一些机制避免多个任务并行。


作为大型广告系统的核心,检索模块面临的问题非常复杂,实际的架构设计要紧密结合业务进行展开。没有统一的方案,只有不同的权衡妥协。本文重点讨论了检索模块一些常见的问题和解决方案,其中有很多细节留给读者思考。




关注我的公众号架构丛谈 最朴素地谈架构



http://chatgpt.dhexx.cn/article/uoXb3dip.shtml

相关文章

互联网智能广告系统架构(业务+系统)

互联网智能广告系统架构 (争取用最简单的图,最简洁的语言描述清楚) 一、业务简述 从业务上看整个智能广告系统,主要分为: 1)业务端:广告主的广告后台 2)展现端:用户实际访…

广告系统简易流程与架构

一、业务简述 从业务上看 整个智能广告系统,主要分为: 1)业务端:广告主的广告后台 2)展现端:用户实际访问的页面 业务端,广告主主要有两类行为: 1)广告设置行为&am…

揭秘广告系统架构

作者 | 骆俊武 来源 | IT人的职场进阶(ID:BestITer) 广告、增值服务、佣金,是互联网企业最常见的三种盈利手段。在这3大经典中,又以广告所占的市场份额最大,几乎是绝大部分互联网平台最主要的营收途径,业务…

广告系统架构

一:广告系统整体架构 用户通过浏览器访问网页,网页上的广告位贴了广告请求代码,广告请求发送到投放机,投放机上DE进行处理,选择出合理的广告进行投放。(或者网站上贴的是ssp的代码,ssp将请求转发…

大型广告系统架构概述

在互联网江湖中,始终流传着三大赚钱法宝:广告、游戏、电商。三杰之中,又以大哥广告的历史最为悠久,地位也最为不可撼动。君不见很多电商和游戏公司,也通过广告业务赚的盆满钵满。其发迹于Y公司,被G公司发扬…

广告管理系统

软件工程与UML 大作业 课题:广告管理系统 学号:3158126157 姓名:徐先森 专业班级:网络工程 指导老师:杨财英 目录 一、 系统的需求分析 2 1.1功能性需求 2 1.2广告管理系统介绍 2 1.3.1用例图如图: 3 1.3.2 用例描述 3 二、 概要设计 5 2.1总体结构图&…

三大视角,聊聊我眼中的广告系统

作者 | wulc 整理 | NewBeeNLP 从实习到工作,接触过一些大大小小的广告系统,有麻雀虽小但五脏俱全的小 dsp,也有把 ssp、adx、dsp 都打包了的大媒体 ,算是对业界的广告系统有了一个初步的了解。趁着放假这几天,简单地…

十分钟理解广告系统

什么是广告系统 广告是以“把合适的内容推送给合适的受众”为目的的商业交易过程,它同时为三种人群服务:第一种是广告主,即出钱购买广告的人,需要通过广告获取顾客;第二种是媒体,即提供投放平台以换取广告费…

广告系统架构浅谈

写在前面 最近即将入职字节跳动的广告系统部门,因此花了一些时间了解了一下现代广告系统的一般架构,在这里分享给大家。 广告系统一般架构 整体上来看,广告系统由三个主体部分构成: 1、在线的高并发投放引擎(Ad server)。 2、离…

广告系统实现

一、系统架构 二、准备工作 1、开发工具:IDEA 2、数据库:MySQL 3、环境:JDK1.8、 Maven 3 4、系统目录结构 三、广告系统的功能 1、广告投放系统 -> 既然是广告系统,一定得有广告数据,数据当然是由广告主或代理…

逻辑位移和算术位移

在C语言标准中&#xff0c;有两种位移分别为算术位移和逻辑位移。 逻辑位移&#xff1a;在位移运算符&#xff08;>>和<<&#xff09;之前的数是无符号数&#xff0c;编译产生的汇编指令是逻辑位移。 算术位移&#xff1a;在位移运算符之前的数是有符号数&#xff…

带符号位移运算,无符号位移运算,位运算

带符号位移运算&#xff1a; &#xff08;符号位也参与移动&#xff09; &#xff08;除了负数右移高位补1&#xff0c;其他情况空位均补0&#xff09; &#xff08;左移右移后可能结果正负都变了&#xff09; >>右移 &#xff08;向右移一位约等于除以2&#xff0c;注意…

显卡和异构计算

显卡和异构计算 本文采用知识共享署名 4.0 国际许可协议进行许可&#xff0c;转载时请注明原文链接&#xff0c;图片在使用时请保留全部内容&#xff0c;可适当缩放并在引用处附上图片所在的文章链接。 显卡GPU 显卡分类 集成显卡独立显卡核芯显卡 显卡性能 架构流处理器核心频…

带符号移位运算详解

十进制正整数有符号左移 示例&#xff1a;10 << 2 40 Java代码&#xff1a; public class ShiftTest {public static void main(String []args){int leftShiftBegin 10;System.out.println("十进制数&#xff1a;" leftShiftBegin " , " &quo…

机器对移位运算的看法

1.先说一个运算口诀叫“左乘右除”&#xff0c;如k<<1>k*2; k>>1>k/2; 2.左移 先看左移运算&#xff0c;因为相对于右移较为简单&#xff1b; x向左移动K位&#xff0c;会丢弃最高的K位&#xff0c;并在右端补K个0&#xff0c;移位运算是从左到右课结合的&a…

定点运算——位移运算

位移运算 前提&#xff1a;下述的移位运算推理过程是建立在合理的移位运算基础上的&#xff0c;即移位运算的结果和实际运算结果一致 位移运算的数学意义 位移运算&#xff0c;相当于小数点的移动&#xff0c;对数值进行扩大或者缩小进制数倍 左移运算&#xff0c;小数点右移…

【逻辑位移和算数位移】

<< 运算符 && >> 运算符 正数位移 当 x>>n 中 x 为正数时&#xff0c;会将x的所有位右移x位&#xff0c;同时左边高位补0 显而易见&#xff0c;运算结束后&#xff0c;值为1 。 可知右移n位&#xff0c;结果就是 x / 2^n&#xff1a;7 / 2 ^2 1;…

算术位移和逻辑位移(一篇懂)

位运算程序员的基本功&#xff0c;但是不得不说这一块儿确实挺让人头疼的。不过还好&#xff0c;你遇到了我&#xff0c;哈哈... 文章目录 必备知识算术移位逻辑移位用例子说话总结 必备知识 计算机是以二进制方式来进行运算的,也就是0和1 。所有数据必须转化成0、1代码计算机才…

矩阵乘测试显卡算力

由于pytorch和tensorflow不支持int8 int16的gemm&#xff0c;因此只能测试fp32 fp16 bf16等精度的tflops&#xff0c;如果要测试int8 int16精度下的数值&#xff0c;需要编写cublas脚本&#xff0c;目前不会CUDA编程&#xff0c;可参考大佬的脚本&#xff1a; cuBLAS矩阵乘法性…

逻辑运算和位移指令

逻辑运算指令 AND OR NOT XOR TEST 逻辑位移指令 SHL SHR 算术位移指令 SAL SAR 小循环位移指令 ROL ROR 大循环位移指令 RCL RCR AND 逻辑与指令 汇编格式&#xff1a;AND 目的操作数&#xff0c;源操作数 执行操作&#xff1a;&#xff08;目的操作数&#xff09;&…