系统设计:广告投放系统
广告系统是「毫秒级竞价 + 海量检索 + 精准定向 + 严格预算」的复合体:一次广告请求要在 100ms 内从千万级广告中选出最合适的一条,且不能超预算、不能超频次、不能作弊。它是面试中考察综合能力的高频题。
1. 需求分析
功能性需求
- 广告投放:广告主创建计划、设置定向、预算、出价
- 广告检索:根据用户与上下文召回候选广告
- 排序与竞价:按 eCPM 排序并确定计费价格
- 计费:按曝光/点击计费,扣费准确
- 频次控制:同一用户看到同一广告的次数上限
- 报表:曝光、点击、转化、花费的统计
非功能性需求
- 响应延迟:P99 低于 100ms(含竞价)
- 吞吐:峰值 100 万 QPS 广告请求
- 扣费准确性:不超扣、不漏扣,可对账
- 可用性:99.99%,广告失败可降级为无广告
- 预算准确性:日预算不可超投,偏差小于 1%
核心难点
- 超低延迟:检索 + 排序 + 竞价全流程必须在百毫秒内
- 预算刚性:超投会被广告主投诉,少投则浪费
- 归因延迟:转化可能在点击后数天才发生,扣费与统计需异步补偿
2. 容量估算
- 日广告请求:200 亿次
- 平均 QPS:200 亿 / 86400 ≈ 23 万
- 峰值按 4 倍计:约 100 万 QPS
- 在投广告数:1000 万条,定向维度多样
- 单次请求召回候选:数千条 → 需粗排降到数百 → 精排几十
- 曝光日志:日曝光 100 亿条 × 200 字节 ≈ 20 TB/天
结论:检索必须分层(召回 → 粗排 → 精排),否则 1000 万候选不可能在毫秒内算完;日志量巨大,需列式存储 + 流式聚合。
3. 整体架构
用户请求(内容流/搜索)
│
▼
广告网关(解析请求、取用户画像)
│
┌──┴───────────────┐
▼ ▼
定向检索(倒排索引) 预算/频控校验(内存)
│ │
└────────┬─────────┘
▼
粗排(轻量模型,千→百)
▼
精排(pCTR 模型,百→十)
▼
竞价与分配(定价、扣费)
▼
返回广告 + 曝光监测链接
▼
曝光/点击/转化日志 ──► 流式聚合 ──► 报表 + 预算回写
请求路径
网关解析请求并拉取用户画像,检索层用倒排索引按定向条件召回,预算与频控在内存中快速校验,粗排精排后进入竞价,最终返回广告并附监测链接。
反馈路径
曝光、点击、转化日志经流式管道聚合,实时回写预算消耗与频控计数,并生成报表。
4. 数据模型
广告计划表
ad_campaign (
campaign_id BIGINT PRIMARY KEY,
advertiser_id BIGINT,
creative_id BIGINT,
bid_type TINYINT, -- CPM/CPC/CPA
bid_price INT, -- 出价(分)
daily_budget INT, -- 日预算(分)
targeting JSON, -- 定向条件
freq_cap INT, -- 频次上限
status TINYINT, -- 投放/暂停/结束
start_ts BIGINT,
end_ts BIGINT
)
定向倒排索引
index:gender:male -> [campaign_id ...]
index:city:310000 -> [campaign_id ...]
index:interest:sports -> [campaign_id ...]
内存中的预算与频控
budget:{campaign_id} -> 今日已消耗(分),定时持久化
freq:{user_id}:{campaign_id} -> 已曝光次数,TTL 24h
设计要点
- 定向条件用倒排索引加速,多条件求交集
- 预算与频控放内存(Redis/本地),避免每次查库
- 消耗数据定时落盘并对账,防止内存丢失导致超投
5. 广告检索与召回
分层检索
| 阶段 | 输入 | 输出 | 手段 |
|---|---|---|---|
| 召回 | 千万级 | 数千 | 倒排索引 + 布尔求交 |
| 粗排 | 数千 | 数百 | 轻量特征 + 线性/浅层模型 |
| 精排 | 数百 | 数十 | 深度 pCTR 模型 |
| 竞价 | 数十 | 1 到 3 | eCPM 排序 + 定价 |
召回优化
- 定向条件构建多路倒排,按「区分度」排序求交(先交小集合)
- 对热门定向(如全国 + 全年龄)预计算候选集
- 用布隆过滤器快速排除明显不匹配的广告
粗排的必要性
精排模型重、耗时高,无法对数千候选逐个打分。粗排用极轻的特征(如广告质量分、历史 CTR)快速裁剪,保证精排只处理最有希望的候选。召回与排序的分层设计可参考 推荐系统设计,高并发下的频次与预算校验则与 高并发限流器设计 同源。
6. 定向与用户画像
定向维度
- 人口属性:性别、年龄、地域
- 兴趣标签:长期兴趣 + 短期行为
- 设备与环境:机型、网络、时段
- 人群包:广告主上传的定向人群(如已购用户)
用户画像构建
- 离线:基于历史行为用批处理生成长期兴趣标签
- 近线:小时级更新短期行为标签
- 实时:请求时结合当前上下文(如刚搜索的词)
画像存储
画像特征宽表存 KV(如 Redis/HBase),按 user_id 读取;兴趣标签存倒排便于反向查询。注意高基数(user_id)不能进倒排索引做广告召回,只能作为过滤。
隐私与合规
- 画像需脱敏、加密,遵循个人信息保护要求
- 支持用户关闭个性化推荐(降级为上下文定向)
- 定向人群包不得用于识别个人身份
7. 竞价与预算控制
eCPM 排序
不同计费方式统一折算为 eCPM 排序:
eCPM(CPM 广告) = bid_price
eCPM(CPC 广告) = bid_price × pCTR × 1000
eCPM(CPA 广告) = bid_price × pCTR × pCVR × 1000
定价方式
- 一价:按出价结算,简单但易诱导虚高出价
- 二价(广义第二价格 GSP):按第二名出价 + 最小加价结算,鼓励真实出价
预算控制 Pacing
预算控制解决「预算在一天内平滑消耗」的问题:
| 策略 | 做法 | 问题 |
|---|---|---|
| 匀速 | 预算均摊到全天 | 流量波动时浪费 |
| 快投 | 尽快花完 | 早间耗尽,晚间无曝光 |
| 概率节流 | 按剩余预算/剩余时间动态决定是否参竞 | 兼顾平滑与机会 |
def should_bid(campaign, now):
elapsed = now - day_start(campaign)
ideal_spend = campaign.daily_budget * elapsed / DAY_SECONDS
actual = get_spend(campaign)
# 超速则降低参竞概率,欠速则提高
ratio = actual / ideal_spend if ideal_spend else 1
return random.random() < clamp(1.0 / max(ratio, 0.1), 0, 1)
超投防护
- 内存扣费 + 定时对账落库,双重校验
- 预算耗尽立即从召回索引摘除该广告
- 最终以数据库扣费为准,内存偏差在秒级内校正
8. 频次控制与计费归因
频次控制
- 按「用户 × 广告」计数,超过上限不再投放
- 存储用 Redis 计数 + TTL(如 24 小时窗口)
- 支持多级频控:单广告、单计划、单广告主维度
计费流程
- 广告返回时记录「待计费」事件
- 收到曝光回执(客户端上报)才真正扣费
- 扣费写内存并异步落库
- 定时对账,修正内存与数据库偏差
归因模型
| 模型 | 规则 | 适用 |
|---|---|---|
| 末次点击 | 归因给最后一次点击 | 效果广告主流 |
| 首次点击 | 归因给第一次点击 | 品牌曝光 |
| 线性 | 平均分配给所有触点 | 多触点分析 |
| 时间衰减 | 越近权重越大 | 短周期转化 |
归因延迟处理
转化可能延迟数天上报,因此:
- 计费与统计分离,扣费按点击即计(CPC),转化统计异步补齐
- 支持回溯窗口(如点击后 7 天内转化均计入)
- 报表区分「实时数」与「结算数」,避免口径混乱
9. 反作弊与效果衡量
常见作弊类型
- 曝光作弊:刷量机器伪造曝光
- 点击作弊:诱导点击、点击农场
- 转化作弊:虚假注册/下单骗取 CPA 结算
反作弊手段
- 设备指纹与 IP 聚类,识别同一设备的高频异常
- 行为序列分析,机器点击的间隔与轨迹过于规律
- 点击率异常检测:远高于同类广告的 CTR 触发人工审核
- 事后回溯:结算前批量复核可疑流量并扣减
效果衡量指标
| 指标 | 含义 | 用途 |
|---|---|---|
| CTR | 点击率 | 广告吸引力 |
| CVR | 转化率 | 落地页效果 |
| eCPM | 千次曝光收益 | 平台收入 |
| ROI/ROAS | 投入产出比 | 广告主价值 |
| 填充率 | 有广告返回的比例 | 流量变现效率 |
效果实验
- 用 A/B 实验对比不同出价/排序策略
- 用增量实验(Geo 实验、PSA)衡量广告真实增量,排除自然转化
- 关注长期价值,避免只优化短期 CTR 损害用户体验
10. 面试常见问题
Q: 千万级广告如何在 100ms 内检索完?
分层检索:倒排召回(千万→千)、粗排(千→百)、精排(百→十)。每一层都用轻量手段快速裁剪,只有最后一层才上重模型。
Q: 怎么保证不超预算?
内存扣费 + 概率节流(pacing)+ 预算耗尽立即摘除索引 + 定时对账。核心是让消耗速度与预算/时间匹配,并在多个环节做校验。
Q: 一价和二价的区别?
一价按出价结算,广告主倾向于压低出价;二价按第二名加价结算,鼓励按真实价值出价,是主流选择。
Q: pCTR 模型怎么训练?
用历史曝光点击日志做样本,特征含用户画像、广告特征、上下文,标签为是否点击;注意样本偏差(只对已曝光样本有标签)与实时性(需在线更新)。
Q: 频次控制放在哪一层?
放在召回后的校验层,用内存计数快速判断;也可在召回前用布隆过滤器粗筛,减少无效计算。
Q: 转化延迟怎么处理?
计费与统计分离,扣费即时、转化异步回填;设定归因窗口,报表区分实时与结算口径,并对超窗转化单独处理。
Q: 如何衡量广告的真实效果?
A/B 实验 + 增量实验(PSA/Geo 对照)。单纯对比「看过广告 vs 没看过」会有选择偏差,增量实验才是因果意义上的效果。
Q: 反作弊和广告主利益冲突吗?
不冲突。作弊流量损害的是广告主 ROI 与平台长期信任,反作弊是保护生态;结算前复核与扣减是行业通行做法。
总结
广告投放系统的答题主线是检索分层 + 预算刚性 + 归因延迟三件事:检索用召回/粗排/精排三级流水线把千万级候选压到个位数;预算用 pacing 概率节流 + 内存扣费 + 对账三重保证不超投;归因用实时与结算分离应对转化延迟。最后补上定向画像的隐私边界、反作弊与增量实验,就能覆盖这道题的全部考察点。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。