广告系统整体架构:从接入到归因的完整链路
大家好,我是老张。今天我们来聊聊广告系统的整体架构。说实话,我刚入行那会儿,觉得广告系统不就是「用户搜个关键词,系统返回几条广告」嘛。后来真正参与了一个日请求量过亿的广告平台重构,才发现这里面的门道比想象中深得多。
嗯,咱们今天就把这套架构拆开来看。我会从三个层面来讲:分层架构、核心模块、以及它们之间的交互流程。你想想看,一个广告请求从用户点击到最终扣费,中间经历了什么?
一、广告系统分层架构
我个人习惯把广告系统分成三层:接入层、业务层、数据层。为什么这么分?说白了,就是为了解耦。每一层只关心自己的事,出了问题也好定位。
1. 接入层:系统的「门卫」
接入层是广告系统的最前端。它负责接收外部请求,做最基础的校验和分发。我在项目中遇到过,有一次流量突增,接入层没做好限流,直接把业务层打挂了。从那以后,我对接入层的设计格外谨慎。
接入层主要干这几件事:
- 协议解析:把HTTP、RPC等不同协议的请求统一成内部格式
- 流量控制:限流、降级、熔断,防止系统被冲垮
- 基础校验:检查请求参数是否合法,比如广告位ID是否存在
- 路由分发:根据请求类型,把请求转发到对应的业务模块
核心要点:接入层要轻量、无状态。千万别在这里做复杂的业务逻辑,否则一旦出问题,影响面会非常大。
2. 业务层:系统的「大脑」
业务层是广告系统的核心。所有广告检索、排序、竞价、出价的逻辑都在这里。这一层也是我花时间最多的地方。
业务层通常包含以下子模块:
- 广告检索:从海量广告中找到与当前请求匹配的候选集
- 广告排序:对候选广告进行打分,决定展示顺序
- 竞价与出价:根据广告主的出价策略,计算最终扣费
- 归因模块:把转化事件归因到对应的广告点击
这里要注意,业务层是有状态的。比如广告检索需要加载索引,排序模型需要加载模型参数。所以业务层的扩容和缩容,要比接入层复杂得多。
避坑指南:我曾经在业务层直接操作数据库,结果QPS一上来,数据库连接池就爆了。后来我强制要求:业务层只能通过数据层获取数据,绝不能直连数据库。
3. 数据层:系统的「仓库」
数据层负责存储和提供所有数据。包括广告物料数据、用户画像数据、模型特征数据、日志数据等等。
数据层通常采用混合存储方案:
| 数据类型 | 存储方案 | 特点 |
|---|---|---|
| 广告物料 | MySQL + Redis | 强一致性,支持事务 |
| 用户画像 | HBase / Cassandra | 高并发,列式存储 |
| 模型特征 | Redis / 本地缓存 | 低延迟,高频访问 |
| 日志数据 | Kafka + HDFS | 高吞吐,离线分析 |
数据层的设计原则是:读写分离、冷热分离。热数据放缓存,冷数据放数据库或大数据平台。
二、核心模块总览
好,分层架构讲完了。接下来我们看看业务层里那几个核心模块,到底在干什么。
1. 广告检索模块
广告检索,说白了就是「大海捞针」。一个广告系统可能有几百万甚至上千万条广告,但一次请求只需要返回几十条。怎么快速找到最相关的那些?
常见的检索策略:
- 倒排索引:根据关键词快速定位广告
- 标签匹配:根据用户标签和广告定向条件做匹配
- 地域过滤:只展示用户所在地区的广告
- 频次控制:避免同一个用户反复看到同一条广告
经验之谈:检索阶段不要追求「精准」,而是追求「召回率」。宁可多召回一些不相关的广告,也不能漏掉潜在的好广告。排序阶段再去做精细化筛选。
2. 广告排序模块
排序模块是广告系统的「裁判」。它决定哪条广告排在第一位,哪条排在最后。
排序通常分两阶段:
- 粗排:用轻量级模型(比如LR、FM)快速过滤掉明显不相关的广告
- 精排:用复杂模型(比如DNN、GBDT)对候选广告进行精确打分
排序的核心指标是eCPM(千次展示预估收入)。公式很简单:
eCPM = pCTR * CPC * 1000
其中pCTR是预估点击率,CPC是广告主的出价。排序模块的目标就是让eCPM高的广告排在前面。
3. 竞价与出价模块
竞价模块负责「算钱」。广告主出价是一回事,实际扣费是另一回事。
最经典的竞价方式是GSP(广义第二价格):
- 广告主A出价10元,广告主B出价8元
- A排第一,但实际扣费是8元 + 0.01元
- B排第二,实际扣费是下一个出价(如果有的话)
为什么用第二价格?说白了,是为了鼓励广告主出真实价格。如果按第一价格扣费,广告主都会往低了报,反而对平台不利。
注意:实际生产环境中,竞价逻辑远比这个复杂。比如要考虑预算控制、频次控制、流量质量等因素。我曾经见过一个广告主出价很高,但预算只有100块,结果系统在10分钟内就把预算花光了,后面的流量全浪费了。
4. 归因模块
归因模块解决的是「功劳归谁」的问题。用户点击了广告,然后下载了App,这个转化算谁的?
常见的归因模型:
- 最后点击归因:把100%的功劳归给最后一次点击
- 首次点击归因:把功劳归给第一次点击
- 线性归因:所有点击平分功劳
- 时间衰减归因:越靠近转化的点击,功劳越大
归因的难点在于数据打通。广告点击数据在广告平台,转化数据在广告主那边。怎么安全、准确地匹配这两部分数据?
我建议的做法是:
- 广告点击时生成一个唯一的click_id
- 用户跳转到广告主落地页时,带上这个click_id
- 广告主在转化事件中回传click_id
- 广告平台根据click_id做匹配
三、系统交互流程
好了,模块都介绍完了。我们串起来走一遍完整的交互流程。你想想看,一个广告请求从发起到扣费,到底经历了什么?
我把它总结成7个步骤:
- 用户发起请求:用户在App或网页上触发广告展示
- 接入层接收:接入层做基础校验,生成请求ID,转发给业务层
- 广告检索:业务层根据用户信息和广告位信息,从索引中召回候选广告
- 广告排序:对候选广告进行粗排和精排,选出Top N条
- 竞价出价:根据广告主出价和竞价规则,计算每条广告的扣费金额
- 返回结果:把排序后的广告列表返回给前端,前端渲染展示
- 异步归因:用户点击广告后,归因模块在后台匹配转化数据
关键点:第1-6步是同步链路,必须在几十毫秒内完成。第7步是异步链路,可以延迟几小时甚至几天。设计系统时,一定要把同步和异步逻辑分开,避免慢操作阻塞主流程。
嗯,这就是广告系统的整体架构。从接入层到数据层,从检索到归因,每一步都有它的设计逻辑。我在实际项目中,光是优化检索模块的响应时间,就花了两周时间。但当你看到系统稳定运行、广告收入稳步增长的时候,那种成就感是无可替代的。
下一章,我们会深入广告检索模块,聊聊倒排索引和实时索引更新的那些坑。到时候见。