设计一个广告投放平台

本文系统设计一个程序化广告投放平台:覆盖广告计划/创意/定向的多层级建模、实时竞价(RTB)与流量分发、广告检索召回与定向过滤、频控/预算/反作弊约束、点击与转化追踪归因、报表与计费,并给出架构图、数据表、竞价伪代码与量级估算。

广告是互联网最核心的商业化系统,广告投放平台则是一个典型的「请求级决策」系统:每一次流量请求到达时,平台要在几十毫秒内决定「要不要投、投谁的广告、出多少钱」,还要同时满足定向、频控、预算、反作弊的约束。本文按照系统设计面试的标准答题结构,设计一个程序化广告投放平台,覆盖投放管理、实时竞价、检索定向、约束执行、追踪归因与计费报表的完整链路。

一句话:广告平台是「在实时性、相关性、约束与商业价值之间做多目标最优决策」的系统——每一千次请求的决策质量直接等于收入。

一、需求澄清与量级估算

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+数仓)   │
   └────────────┘ └────────────┘ └────────────┘    └────────────────┘

整体拆为六个模块:

  1. 接入层:ADX/SSP 接入网关,负责协议解析、验签、首道反作弊。
  2. 决策引擎(热路径):检索召回 → 定向过滤 → 频控预算 → 竞价,全部内存态,目标 < 50ms。
  3. 投放管理(冷路径):计划/创意/定向/预算的配置、审核、上下线。
  4. 约束执行:频控与预算的实时计数,Redis + 本地多级缓存。
  5. 追踪与归因:曝光/点击/转化埋点接收,异步 Kafka 进数仓。
  6. 计费与报表: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 在同一拍卖里公平比较。再以反作弊与归因计费作为商业正确性的最后防线,广告平台就能在规模与准确之间同时成立。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个弹幕系统
  2. 设计一个权限系统(RBAC + ABAC)
  3. 设计一个分布式任务调度系统