广告是互联网最核心的商业化系统,广告投放平台则是一个典型的「请求级决策」系统:每一次流量请求到达时,平台要在几十毫秒内决定「要不要投、投谁的广告、出多少钱」,还要同时满足定向、频控、预算、反作弊的约束。本文按照系统设计面试的标准答题结构,设计一个程序化广告投放平台,覆盖投放管理、实时竞价、检索定向、约束执行、追踪归因与计费报表的完整链路。
一句话:广告平台是「在实时性、相关性、约束与商业价值之间做多目标最优决策」的系统——每一千次请求的决策质量直接等于收入。
一、需求澄清与量级估算
1.1 需求澄清
面试官给出题目「设计一个广告投放平台」后,先通过提问明确边界:
- 广告类型:信息流广告、开屏广告、搜索广告、品牌展示广告,覆盖哪些?
- 交易模式:程序化实时竞价(RTB)、优先购买(Programmatic Guaranteed)、PDB,还是仅内部直投?
- 计费方式:CPM(千次曝光)、CPC(点击)、CPA(转化)、CPD(按天),哪些?
- 定向维度:人群(年龄/性别/兴趣)、地域、设备、时段、上下文,支持哪些?
- 约束:预算(日预算/总预算)、频控(单用户曝光上限)、反作弊(无效流量过滤)是否必需?
- 数据:投放后的曝光/点击/转化数据如何归因、如何计费、如何出报表?
明确假设(面向面试的合理假设):
| 需求项 | 假设 |
|---|---|
| 广告形态 | 信息流 + 开屏 + 搜索广告 |
| 交易模式 | 内部直投 + RTB 实时竞价 |
| 计费 | CPM / CPC / CPA 三种 |
| 定向 | 人群、地域、设备、时段、上下文五类 |
| 约束 | 预算、频控、反作弊三大硬约束 |
| 报表 | 小时级实时报表 + 次日 T+1 归因报表 |
1.2 量级估算
| 指标 | 估算值 | 推导 |
|---|---|---|
| 日均流量请求 | 50 亿次 | 假设日活 2 亿用户 × 每人日均 25 次广告曝光机会 |
| 峰值 QPS | ~200 万 | 50 亿 / 86400 ≈ 5.8 万,× 30~40 倍峰值系数 |
| 每次请求决策 | < 50ms | 广告响应与页面渲染并行,必须毫秒级 |
| 活跃广告计划 | 10 万 | 平台同时在投的广告计划 |
| 日曝光 | ~30 亿 | 部分请求无可投广告或未竞价成功 |
| 日点击 | ~1500 万 | 点击率约 0.5% |
| 报表行数 | 数十亿/日 | 每次曝光/点击/转化一条明细 |
一句话:200 万 QPS 的请求级决策 + 数十毫秒延迟,决定了广告平台必须是「索引 + 缓存 + 分布式决策引擎」的组合,任何落库动作都不能出现在竞价热路径上。
二、高层架构设计
┌─────────────────────────────┐ ┌────────────────────────────┐
│ 流量方(App / 媒体网站) │ │ 广告主(客户 / 代理商) │
│ 每次曝光机会发起广告请求 │ │ 创建计划/创意/定向/预算 │
└──────────────┬──────────────┘ └──────────────┬────────────┘
│ ADX/SSP 接入(协议/验签/反作弊) │ 管理后台(审核/投放)
└──────────────┬──────────────────┘
│
┌─────────────────────────────▼─────────────────────────────────┐
│ 广告投放核心服务 │
│ ┌───────────────┐ ┌────────────────┐ ┌─────────────────────┐ │
│ │ 检索召回服务 │ │ 定向过滤服务 │ │ 竞价决策服务 │ │
│ │ (计划索引/召回) │ │ (人群/地域/频控) │ │ (出价/预算/拍卖) │ │
│ └───────────────┘ └────────────────┘ └─────────────────────┘ │
│ ┌───────────────┐ ┌────────────────┐ ┌─────────────────────┐ │
│ │ 频控/预算服务 │ │ 反作弊服务 │ │ 创意/物料服务 │ │
│ │ (实时计数/扣减) │ │ (无效流量过滤) │ │ (物料检索/下发) │ │
│ └───────────────┘ └────────────────┘ └─────────────────────┘ │
└──────┬──────────────┬──────────────┬───────────────────┬──────┘
│ │ │ │
┌──────▼─────┐ ┌──────▼─────┐ ┌─────▼──────┐ ┌───────▼────────┐
│ Redis 集群 │ │ 广告索引 │ │ 点击/曝光 │ │ 大数据链路 │
│ 频控/预算/ │ │ (倒排/过滤) │ │ 追踪服务 │ │ 归因/计费/报表 │
│ 幂等/缓存 │ │ (内存态) │ │ (埋点接收) │ │ (Kafka+数仓) │
└────────────┘ └────────────┘ └────────────┘ └────────────────┘
整体拆为六个模块:
- 接入层:ADX/SSP 接入网关,负责协议解析、验签、首道反作弊。
- 决策引擎(热路径):检索召回 → 定向过滤 → 频控预算 → 竞价,全部内存态,目标 < 50ms。
- 投放管理(冷路径):计划/创意/定向/预算的配置、审核、上下线。
- 约束执行:频控与预算的实时计数,Redis + 本地多级缓存。
- 追踪与归因:曝光/点击/转化埋点接收,异步 Kafka 进数仓。
- 计费与报表:T+1 归因、计费流水、小时级报表。
2.1 热路径与冷路径分离
广告平台最关键的架构决策是把「决策热路径」与「数据冷路径」彻底分离:
热路径(每次请求):索引检索 → 定向过滤 → 频控预算 → 竞价出价 → 返回物料
冷路径(异步):埋点入 Kafka → 归因 → 计费 → 报表 → 计划状态回写
设计原则:
- 热路径只读内存索引与 Redis 计数,绝不落库、绝不跨服务 RPC 串行
- 计划上线/更新走冷路径发布到内存索引,秒级生效
- 频控/预算在本地缓存 + Redis 异步回写,允许微秒级误差
一句话:把「每一毫秒赚多少钱」的决策放到内存态,把「每一分钱花在哪」的审计放到异步态,是广告平台吞吐与准确能同时成立的根本。
三、核心组件设计
3.1 广告计划建模
广告计划是多层级的树状结构,从预算到创意逐层细化:
CREATE TABLE ad_campaign (
campaign_id BIGINT PRIMARY KEY,
advertiser_id BIGINT,
name VARCHAR(128),
budget_type TINYINT, -- 1日预算 2总预算 3不限
daily_budget DECIMAL(12,2),
total_budget DECIMAL(12,2),
start_time DATETIME,
end_time DATETIME,
status TINYINT -- 0草稿 1投放中 2暂停 3已结束
);
CREATE TABLE ad_unit (
ad_unit_id BIGINT PRIMARY KEY, -- 广告单元(投放级别)
campaign_id BIGINT,
bid_type TINYINT, -- 1CPM 2CPC 3CPA
bid_price DECIMAL(10,4), -- 出价
targeting_json JSON, -- 定向条件(人群/地域/设备/时段/上下文)
frequency_json JSON, -- 频控(人均曝光上限/间隔)
status TINYINT
);
CREATE TABLE ad_creative (
creative_id BIGINT PRIMARY KEY,
ad_unit_id BIGINT,
material_url VARCHAR(512), -- 图片/视频物料
title VARCHAR(128),
landing_url VARCHAR(512), -- 落地页
status TINYINT
);
3.2 检索召回与定向过滤
请求到达后,决策引擎先在内存索引里召回候选计划:
索引结构(倒排式,全内存):
geo_index: { "上海" → [unit1, unit2, ...] } # 地域
device_index: { "iOS" → [unit1, unit3, ...] } # 设备
slot_index: { "信息流-首页" → [...] } # 广告位
tag_index: { "游戏" → [...] } # 兴趣/上下文标签
召回流程:
请求属性(地域/设备/广告位/人群标签)→ 各索引求交集候选集
→ 定向过滤(人群标签精确匹配、时段校验)
→ 频控过滤(该用户对每 unit 的曝光是否超限)
→ 预算过滤(unit 当日预算是否已耗完)
→ 候选集送入竞价
索引更新:计划上线/改价由 MQ 异步发布到各决策节点的内存索引,使用多版本指针切换保证更新不打断在途请求:
索引版本:Index v1(当前)→ 后台构建 v2 → 原子替换指针 → 旧版本自然淘汰
在途请求读到 v1 或 v2 都合法,无需锁
3.3 竞价与出价决策
候选集确定后,进入**竞价(Auction)**环节。一次一价拍卖(GSP/次价拍卖)是主流:
;; 伪代码:次价拍卖竞价
(defn auction [candidates request]
(let [scored (map (fn [unit]
{:unit unit
:ecpm (compute-ecpm unit request)}) ; 预估千次曝光收益
candidates)
filtered (filter eligible? scored) ; 二次过滤
sorted (sort-by (comp - :ecpm) filtered)]
(if-let [winner (first sorted)]
(let [second-price (if (second sorted)
(:ecpm (second sorted))
0)]
{:ad (:unit winner)
:ecpm (:ecpm winner)
:pay_price (min (:ecpm winner)
(+ second-price 0.01))}) ; 次价成交
:no-ad)))
;; 预估:pCTR × 出价(CPC 换算成 eCPM 才可跨计费方式比较)
(defn compute-ecpm [unit request]
(case (:bid_type unit)
:CPM (:bid_price unit)
:CPC (* (predict-ctr unit request) (:bid_price unit) 1000)
:CPA (* (predict-ctr unit request)
(predict-cvr unit request) (:bid_price unit) 1000)))
要点:
pCTR(预估点击率)来自离线训练的模型(特征:用户标签 × 创意 × 上下文),热路径用特征向量查询模型而非实时训练;跨计费方式统一折算成 eCPM 才能公平拍卖。
3.4 频控与预算的实时计数
频控和预算需要每请求实时扣减,但每个 unit 的预算计数是热点 key。设计要点:
频控(人均,读多写少):
本地缓存 + Redis 计数:ad:fc:{uid}:{unit} → 曝光次数
请求时先查本地(微秒),超过阈值则阻断;写回 Redis 异步批量
预算(每 unit,写热点):
多级扣减:本地配额(每节点每秒预算分片)→ Redis 主计数
预算耗尽判断:本地配额消耗完才打 Redis,避免每请求一次 Redis
;; 预算分片:本地配额减少 Redis 访问
(defn try-deduct-budget! [unit-id amount]
(let [local (local-quota unit-id)]
(when (>= (:remaining local) amount)
(dec-local! local amount)
(async-redis-decr unit-id amount) ; 异步回写,误差可接受
true)))
;; 兜底:本地配额用尽时同步回源 Redis 校准
要点:频控与预算允许毫秒级计数误差(一两单的波动),因此本地缓存 + 异步回写换取吞吐;但总预算不能超,用「本地配额耗尽即回源」保证硬约束不破。
3.5 反作弊与无效流量过滤
反作弊在接入层和决策层双道拦截:
| 层级 | 手段 | 目标 |
|---|---|---|
| 接入层 | IP/设备指纹识别、频次过滤、UA 识别 | 拦截机器刷量、脚本点击 |
| 决策层 | 无效流量黑名单、可疑设备降权 | 减少无效曝光计费 |
| 归因层 | 点击归因、转化去重、反「刷量归因」 | 保证 CPA 计费准确 |
典型刷量模式:
- 机器曝光:同一设备高频曝光 → 频控 + 设备指纹识别
- 脚本点击:点击率异常高 → 点击率阈值告警 + 全链路点击埋点比对
- 归因污染:虚假点击抢归因窗口 → 归因窗口校验 + 转化唯一性约束
3.6 追踪、归因与计费
曝光/点击/转化埋点异步进入数据链路:
埋点 → 接入网关(去重/签名校验)→ Kafka(topic 按事件类型分)
→ 实时流(分钟级指标)→ 数仓(T+1 归因)
归因模型(最后一击归因):
同一用户 7 天内 曝光→点击→转化 归因给最后一次点击广告
点击→转化 窗口:30 分钟 / 7 天(按业务配置)
计费:
CPM:曝光即计费(曝光日志为准)
CPC:有效点击计费(点击日志为准,反作弊过滤后)
CPA:归因转化计费(转化事件为准,去重)
四、深入权衡
4.1 200 万 QPS 的存储选型
| 数据 | 存储 | 理由 |
|---|---|---|
| 广告索引 | 内存(每决策节点全量副本) | 纳秒级读取、避免跨节点 RPC |
| 频控计数 | 本地缓存 + Redis | 读多写少、允许微误差 |
| 预算计数 | 本地配额 + Redis | 写热点分片、总预算硬约束 |
| 曝光/点击日志 | Kafka + 数仓 | 高吞吐追加写、异步消费 |
| 广告主/计划主数据 | MySQL | 低频 CRUD、事务一致性 |
结论:热路径数据全部内存/Redis 化,冷路径数据全部流式化——广告平台宁可「多花内存买索引副本」,也绝不把一次请求放在 MySQL 上。
4.2 定向精度 vs 延迟
定向过滤越精细(更多人群标签、更细的频控),候选集检索越慢。平衡手段:
两阶段:
- 粗过滤(毫秒级):索引求交集 + 粗频控(本地),筛掉 95% 候选
- 精过滤(微秒级):对剩余候选做人群标签精确匹配 + 精频控
权衡:精细定向的收益体现在 pCTR 提升上,但每条额外规则都加延迟;做法是把规则按代价排序,先用低代价规则收窄候选,再对少量候选做高代价精确匹配。
4.3 归因延迟 vs 计费准确
归因要等 7 天窗口关闭才能 100% 准确,但广告主需要实时数据。解法是双轨计费:
实时口径:点击后 30 分钟内的转化先行计费(满足大多数情况)
T+1 修正:次日归因完成后生成修正账单(补/退差额)
一句话:实时与准确不可兼得,就用「实时预估 + 次日修正」双轨,把误差控制在对账可解释的范围内。
五、总结
广告投放平台是在毫秒级、百万级 QPS下做多目标决策的系统:索引召回 + 定向过滤 + 频控预算 + 次价拍卖组成热路径决策引擎(全内存态、不落库),计划配置与埋点归因走冷路径(异步流式)。核心工程决策有三:一是热冷分离,让每一分钱赚取的决策在内存里完成、每一分钱花在哪在数仓里审计;二是计数降热点,频控预算用本地配额 + Redis 异步回写换吞吐、硬约束用回源校准兜底;三是eCPM 归一竞价,让 CPM/CPC/CPA 在同一拍卖里公平比较。再以反作弊与归因计费作为商业正确性的最后防线,广告平台就能在规模与准确之间同时成立。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。