ADX 系统设计与技术方案

ADX(Ad Exchange)把媒体的每一次广告请求变成一场实时交易:在一百到几百毫秒内向多家 DSP 询价, 选出对媒体收益最高、且合规安全的广告返回,再完成计量、结算与对账。 本文是一份完整的系统设计与技术方案:从需求与指标出发,给出总体架构、领域模型、各模块的详细设计、 DSP 高效接入体系、数据与结算链路、容量规划、高可用与降级、安全合规、测试发布和实施计划。

适用范围:媒体自有 ADX(媒体自己的 App 矩阵 + 外部联盟流量),需求方同时包括标准 OpenRTB DSP、 国内头部广告平台的 API / 服务端竞价接入、品牌合约广告。技术选型以 Go + Kafka + Flink + ClickHouse + Redis/Aerospike + MySQL + etcd 为例。

说明:文中的 QPS、延迟、机器数、阈值都是设计假设或估算,上线前必须以压测为准; OpenRTB 字段以 2.6 版本为准;没有可运行代码,配置和伪代码均为示意,未实测。

相关文章:RTB 基础概念(角色、交易类型、一价与 bid shading)见 广告投放系统 第 5 节; 分布式基础(限流、熔断、幂等、对账)见 分布式系统全景; 倒排索引与布尔表达式匹配见 检索系统全景。


目录

# 章节 主题
1 背景、目标与范围 业务定位、目标、非目标、术语
2 需求 功能需求、非功能需求与量化指标
3 总体架构 服务划分、部署拓扑、技术选型
4 领域模型与数据模型 核心实体、管理库表结构、内部请求模型
5 在线竞价链路 端到端时序、延迟预算、Pipeline 设计、局部并行与提前询价
6 接入层 SDK 协议、鉴权、防重放、限流、协议标准化
7 请求处理与流量管控 请求过滤、补全、流量屏蔽规则、ID 映射、标签与人群包、Roaring Bitmap、Aerospike、频控
8 DSP 选择与流量筛选 预定向(位图、Conjunction 算法)、QPS 与曝光上限、出价概率模型
9 询价扇出 异步并发、连接池、隔离、熔断、截止时间
10 DSP 接入体系 接入模式、适配器框架、映射 DSL、自助接入平台、自动验收
11 竞价与排序 候选归一、过滤链、排序分、定价、多广告返回、品牌与 Deal
12 底价与收益优化 底价用途、分层与按 DSP 折算、链路中的作用、动态底价、运营与模型分工、PDB 保量、实验平台
13 响应组装与广告交互 样式、交互类型、宏替换、监测链接、开屏预加载
14 上报与追踪 事件体系、计费与监测分离、去重、防伪造
15 配置管理与分发 管理平台、审批、快照构建、双缓冲热加载、灰度与回滚
16 创意审核 素材入库、机审人审、屏蔽、敏感词
17 反作弊 请求级、事件级、媒体级
18 数据链路、报表与结算 日志设计、Kafka、Flink、ClickHouse、结算与对账
19 容量规划 QPS、CPU、带宽、连接数、存储估算
20 高可用与降级 故障域、降级矩阵、客户端兜底、多机房
21 性能工程 序列化、内存、并发、GC、网络
22 安全与合规 通信安全、价格加密、权限审计、隐私法规、广告法
23 可观测性 指标、日志、链路追踪、调试模式、告警
24 测试与发布 契约测试、回放、影子流量、压测、灰度
25 从旧系统迁移 双跑比对、配置迁移、切流
26 实施计划与风险 分期、里程碑、风险与对策
— 附录 OpenRTB 字段对照、错误码、配置示意、术语表、面试题、常见问题

1. 背景、目标与范围

1.1 业务定位

媒体自有 ADX 的位置:

               ┌────────────── 媒体方 ──────────────┐
  自有 App 矩阵 ─┐                                   │
  (开屏、信息流、 ├─► ADX SDK ─► ADX ─┬─► 标准 RTB DSP(OpenRTB)
   插屏、激励视频) │                   ├─► 头部广告平台(API / 服务端竞价)
  联盟外部媒体 ───┘                   ├─► 品牌合约(PD / PDB 订单)
                                     └─► 自营广告(媒体自己的 DSP / 直客)
               └────────────────────────────────────┘

它同时扮演两个角色:

  • 对媒体(自有 App 和联盟媒体)是卖方,目标是最大化收入和填充率,同时保证用户体验和内容安全
  • 对 DSP 是交易平台,目标是让 DSP 低成本接入、稳定拿量、数据可对账

1.2 目标

目标 衡量
收益最大化 千次请求收入(RPM)、eCPM、填充率提升
DSP 高效接入 标准协议 DSP 1 个工作日上线,私有协议 DSP ≤ 2 周;接入无需核心服务发版
稳定低延迟 服务端 P99 ≤ 300 ms(含 DSP 询价),可用性 ≥ 99.95%
可运营 过滤规则、样式、DSP 开关、底价、流量屏蔽全部平台化,秒级生效,有审批和变更记录
可对账 计费事件不丢不重,日级对账差异可解释,可追溯到单次请求
安全合规 素材审核、敏感词、特殊时期屏蔽、隐私合规、广告标识

1.3 非目标

  • 不做 DSP 侧的出价、预算、频控(品牌 PDB 保量除外)
  • 不做广告主投放平台(自营广告由独立 DSP 接入,与外部 DSP 同等对待)
  • 不做客户端渲染 SDK 的设计(只定义服务端与 SDK 的协议和职责边界)

1.4 术语

术语 含义
媒体 / App 流量来源,一个媒体可有多个 App
场景 App 内的一类广告展示场景,如开屏、首页信息流、详情页插屏
广告位(slot) ADX 侧的最小售卖单元,属于某个场景,绑定一组可用样式
DSP 代码位 DSP 分配给 ADX 的广告位 ID(DSP 侧的 slot),一个 ADX 广告位可绑定多个 DSP 代码位
样式(style) 广告的展示形态规格:尺寸、元素(标题、图片、视频、图标)、交互
现金比 DSP 收入中可结算为现金的比例(其余可能是返点、抵扣或非现金权益),用于把出价折算为真实收入
对账系数 历史上 DSP 结算金额 / ADX 预估金额,用于修正预估收入
RPM 每千次请求收入 = 收入 / 请求数 × 1000
eCPM 每千次曝光收入
tmax 给 DSP 的最大响应时间

2. 需求

2.1 功能需求

模块 功能
请求接入 SDK 协议解析与校验、签名验证、防重放、限流;联盟媒体 API / OpenRTB 接入
请求过滤 无效请求过滤(字段缺失、非法设备、黑名单);策略性过滤(按城市、渠道、App 版本、送审版本、特殊事件屏蔽)
请求补全 设备与网络信息标准化、IP 转地理、设备 ID 映射、用户标签、广告位与样式配置
DSP 筛选 广告位绑定的 DSP 代码位、DSP 开关、人群定向、QPS 上限、每日请求量上限、每日曝光上限、流量筛选模型
询价 按 DSP 协议并发询价、统一截止时间、DSP 级隔离与熔断
广告过滤 底价过滤、审核屏蔽素材(素材 MD5)、敏感词、样式不匹配、重复广告(短时间窗内同一广告重复出现)、无效广告(价格为 0、必传字段缺失)
排序与定价 按 有效价格 = 出价 × 现金比 × 对账系数 排序;品牌广告优先;按请求条数返回前 N 个
响应组装 样式映射、交互类型(落地页、下载、deeplink、小程序)、宏替换、监测链接
上报 请求到跳转全链路打点;计费上报与链路监控上报分离;下载、安装、唤起等转化链路上报
配置平台 应用、场景、广告位、DSP 代码位绑定;DSP 开关与参数;样式与交互功能配置;过滤规则;流量屏蔽;全部有审批与变更记录
DSP 管理 DSP 接入、协议配置、适配器映射、密钥、配额、预定向、联调工具、自动验收
审核平台 素材入库、机审、人审、屏蔽、申诉
报表 按媒体、广告位、DSP、代码位、样式等维度的请求、填充、曝光、点击、收入报表;实时与离线
结算 预估收入、DSP 结算数据回填、对账、媒体分成

2.2 非功能需求

维度 指标(设计目标)
容量 峰值入口 30 万 QPS,设计容量 100 万 QPS(水平扩展)
延迟 服务端(不含客户端网络)P50 ≤ 150 ms、P99 ≤ 300 ms;不含询价的内部处理 P99 ≤ 20 ms
可用性 竞价服务 ≥ 99.95%;上报服务 ≥ 99.99%(计费链路)
数据 计费事件零丢失、零重复(端到端幂等);报表实时延迟 ≤ 1 分钟
配置生效 普通配置 ≤ 10 秒全量生效;紧急屏蔽 ≤ 5 秒
扩展性 新增 DSP 不改核心代码;新增过滤规则、样式、交互类型以配置为主
成本 出向带宽与机器成本按"每千次请求成本"考核,占收入比例可控
可追溯 任一请求可按 request_id 查到完整决策过程(抽样全量快照 + 成交全量日志)

2.3 核心业务约束

  • 品牌广告优先:已下单的品牌合约(PD / PDB)在满足条件时必须优先胜出
  • 排序只看价格:除品牌优先外,所有候选统一按有效价格排序,不做人为分层,避免策略复杂化
  • 过滤规则平台化:过滤规则是运营手段,不能写死在代码里
  • 计费链路与监控链路分离:计费事件要求强可靠,监控事件允许采样和降级

3. 总体架构

3.1 服务划分

flowchart TB
    subgraph Client["客户端"]
        SDK["ADX SDK<br/>请求、渲染、上报、兜底"]
    end
    subgraph Edge["接入层"]
        LB["四层负载均衡<br/>LVS / 云 LB"]
        GW["adx-gateway<br/>TLS、验签、解密、限流、协议标准化"]
    end
    subgraph Online["在线竞价"]
        AS["adx-server<br/>请求处理 → DSP 选择 → 询价 → 竞价 → 组装"]
        AD["DSP 适配器<br/>进程内插件"]
        KV[("ID 映射 / 用户标签<br/>Aerospike 或 Redis Cluster")]
        CNT[("配额计数<br/>Redis Cluster")]
    end
    subgraph Track["上报"]
        TS["adx-tracker<br/>计费事件"]
        MS["adx-monitor<br/>链路监控事件"]
        NS["notice<br/>通知服务(nurl/burl/lurl)"]
    end
    subgraph Config["配置"]
        ADMIN["adx-admin<br/>管理平台"]
        DB[("MySQL<br/>配置主库")]
        BUILDER["config-builder<br/>快照构建"]
        OSS[("对象存储<br/>快照文件")]
        ETCD[("etcd<br/>版本指针")]
        AUDIT["creative-audit<br/>审核平台"]
        PORTAL["dsp-portal<br/>DSP 自助接入"]
    end
    subgraph Data["数据"]
        KAFKA[("Kafka")]
        FLINK["Flink<br/>实时统计、配额、反作弊、保量"]
        CH[("ClickHouse<br/>多维报表")]
        HIVE[("Hive / 数据湖<br/>结算、训练")]
        SETTLE["settlement<br/>结算与对账"]
    end
    DSP["外部 DSP / 广告平台"]

    SDK --> LB --> GW --> AS
    AS --> AD --> DSP
    AS --> KV
    AS --> CNT
    SDK --> TS
    SDK --> MS
    NS --> DSP
    AS --> KAFKA
    TS --> KAFKA
    MS --> KAFKA
    KAFKA --> FLINK --> CH
    FLINK -- "去重后的计费事件" --> NS
    KAFKA --> HIVE --> SETTLE
    FLINK --> CNT
    ADMIN --> DB
    AUDIT --> DB
    PORTAL --> DB
    DB --> BUILDER --> OSS
    BUILDER --> ETCD
    ETCD -.->|"watch"| AS
    OSS -.->|"拉取快照"| AS
服务 职责 状态
adx-gateway TLS 终止、验签、解密、防重放、媒体级限流、协议解析并转成内部模型 无状态
adx-server 竞价核心:过滤、补全、DSP 选择、询价、竞价、组装、写竞价日志 无状态(内存中只有配置快照和本地缓存)
DSP 适配器 内部模型与各 DSP 协议的双向转换、价格与宏处理 进程内插件
adx-tracker 计费相关事件(曝光、点击):验签、校验过期、写 Kafka(去重由 Flink 完成,见 14.4) 无状态
adx-monitor 链路监控事件(请求、渲染、下载、安装、唤起等):可采样、可降级 无状态
notice 向 DSP 发送 nurl、burl、lurl:按 DSP 限速、失败退避重试、记录发送结果;burl 以 Flink 去重后的计费事件为输入 无状态 + 重试队列
adx-admin 管理平台:媒体、广告位、DSP、样式、规则、Deal、报表 有状态(MySQL)
config-builder 从 MySQL 构建不可变的配置快照,发布版本 无状态
creative-audit 素材入库、机审、人审、屏蔽 有状态
dsp-portal DSP 自助接入、联调工具、验收报告 有状态
settlement 预估收入、结算数据回填、对账、分成 有状态

为什么 gateway 与 server 分开:gateway 承担 TLS、解密等 CPU 密集且与业务无关的工作,可独立扩缩容;server 专注竞价逻辑。两者之间走内网 gRPC 或同机进程间通信。流量较小时可以合并部署,接口保持分离。

为什么适配器是进程内插件而不是独立服务:询价路径对延迟极敏感,多一跳 RPC 就多几毫秒和一次序列化。适配器以插件形式加载在 adx-server 内,通过接口隔离和资源配额控制故障影响(见 9.4、10.3)。个别需要重型依赖(如 DSP 提供的专用 SDK)的适配器,才单独部署为 connector 服务。

3.2 部署拓扑

            ┌───────────────── 地域 A(主) ─────────────────┐    ┌──── 地域 B ────┐
 GSLB ──►   │ 可用区 A1              可用区 A2                │    │ 同构部署        │
 按 SDK 所在 │  LB → gateway ×N       LB → gateway ×N         │    │ (承接就近流量   │
 区域就近    │       server ×N              server ×N         │    │   与容灾切换)   │
            │       tracker ×N             tracker ×N        │    │                │
            │  Redis Cluster / Aerospike(跨可用区副本)        │    │                │
            │  Kafka 集群(跨可用区副本,rack-aware)            │    │ 本地 Kafka      │
            └───────────────────────────────────────────────┘    └──── MirrorMaker ─┘
                                   │
                           中心数据平台(Flink、ClickHouse、Hive、结算)
  • 每个地域独立完成竞价闭环,不跨地域同步调用
  • 询价出口靠近主要 DSP 的机房;与头部 DSP 可拉专线或走同城 BGP
  • 配置全局统一,数据汇总到中心

3.3 技术选型

组件 选型 理由
在线服务语言 Go 高并发网络 IO 开发效率高、协程模型适合扇出询价;GC 需要调优(见第 21 章)。C++ 性能更高但开发和适配器扩展成本高
内部 RPC gRPC gateway → server、server → 模型服务(如有)
SDK 协议 HTTPS + Protobuf 体积小、解析快、前后兼容
DSP 协议 OpenRTB 2.5/2.6(JSON / Protobuf)+ 私有协议适配 行业标准
KV Aerospike 或 Redis Cluster ID 映射、用户标签:读多、低延迟、大容量;Aerospike 在 SSD 上存大数据量成本更低(见 7.5)
计数 Redis Cluster 配额、去重,原子自增 + 过期
配置库 MySQL 管理数据强一致、事务
配置分发 快照文件(对象存储)+ etcd 版本指针 大配置不走 etcd,只用它通知版本
消息 Kafka 竞价日志、事件日志
实时计算 Flink 实时报表、配额统计、反作弊、保量
OLAP ClickHouse 多维报表
离线 Hive / Spark / 数据湖 结算、对账、训练
可观测 Prometheus + Grafana、OpenTelemetry、Loki / ELK 指标、追踪、日志

4. 领域模型与数据模型

4.1 核心实体关系

erDiagram
    MEDIA ||--o{ APP : "拥有"
    APP ||--o{ SCENE : "包含"
    SCENE ||--o{ AD_SLOT : "包含"
    AD_SLOT }o--o{ AD_STYLE : "支持"
    AD_SLOT ||--o{ SLOT_BINDING : "绑定"
    DSP ||--o{ DSP_CODE_SLOT : "分配"
    DSP_CODE_SLOT ||--o{ SLOT_BINDING : "被绑定"
    DSP ||--o{ CREATIVE : "提交"
    DEAL }o--|| DSP : "属于"
    DEAL }o--o{ AD_SLOT : "覆盖"
    FILTER_RULE }o--o{ AD_SLOT : "作用于"
    TRAFFIC_RULE }o--o{ APP : "作用于"

4.2 管理库表结构(主要字段)

媒体与流量

表 主要字段
media id, name, type(自有 / 联盟), 分成比例, 状态
app id, media_id, 平台(Android / iOS / 鸿蒙), 包名 / bundle, 商店链接, 类目, app-ads.txt 校验状态, 密钥 id, 状态
scene id, app_id, 类型(开屏 / 信息流 / 插屏 / 激励视频 / Banner), 名称
ad_slot id, scene_id, 支持样式列表, 单次请求最多返回条数, 默认底价, 超时(给服务端的总预算), 是否允许下载类广告, 屏蔽行业 / 广告主, 状态
style id, 形态, 尺寸, 元素规格(标题长度、图片比例、视频时长范围、是否需要图标), 渲染模板 id
style_feature style_id × 维度(App / 场景 / 版本 / 渠道), 交互开关(摇一摇、滑动、倒计时、跳过按钮位置、下载确认弹窗等)

需求方

表 主要字段
dsp id, 名称, 接入模式(RTB / API / 服务端竞价), 协议(openrtb-2.5 / openrtb-2.6 / 私有), 编码(json / protobuf), 压缩, 适配器 id 与版本, 各地域 endpoint, 超时, QPS 上限, 每日请求上限, 每日曝光上限, 现金比, 结算口径(以 DSP 数据 / 以 ADX 数据), 价格加密密钥 id, 状态, 放量档位
dsp_code_slot id, dsp_id, DSP 侧代码位 ID, 对应形态, DSP 侧底价要求, 状态
slot_binding ad_slot_id, dsp_code_slot_id, 优先级 / 权重, 底价覆盖, 流量比例, 开关, 生效时间段
dsp_targeting dsp_id, 定向条件(地域、系统、版本、网络、人群包、设备 ID 必须存在等)
deal id, 类型(PDB / PD / PA), dsp_id, 价格, 覆盖广告位, 定向, 日期范围, 目标曝光量(PDB), 优先级, 状态

规则与审核

表 主要字段
filter_rule id, 类型(底价 / 素材屏蔽 / 敏感词 / 样式 / 重复 / 无效 / 行业 / 广告主 / 落地页域名), 作用范围(全局 / 媒体 / App / 广告位 / DSP), 参数, 优先级, 状态
traffic_rule id, 条件(城市、渠道、App 版本、送审版本标记、时间段), 动作(全部屏蔽 / 只屏蔽某些 DSP / 只屏蔽下载类), 原因, 生效时间段
sensitive_word id, 词, 分类, 匹配方式(精确 / 模糊)
creative id, dsp_id, DSP 侧创意 ID, 素材 MD5 列表, 标题, 描述, 落地页, 下载包名, 广告主, 行业, 审核状态, 审核记录 id
creative_block 素材 MD5 或 创意 id, 屏蔽范围, 原因, 操作人

变更与发布

表 主要字段
change_request id, 对象类型, 对象 id, 变更前后 diff, 申请人, 审批人, 状态, 生效方式(立即 / 定时)
config_version version, 快照地址, 校验和, 构建时间, 包含的变更单列表, 发布状态(灰度 / 全量 / 回滚)
audit_log 操作人, 时间, 对象, 动作, diff

4.3 内部请求模型

内部模型按 OpenRTB 2.6 的结构组织,加上 ADX 自己的上下文字段。所有适配器都在"内部模型 ↔ 外部协议"之间转换。

对象 关键字段
AdContext request_id(全局唯一)、trace_id、接收时间、deadline(绝对时间)、地域、实验分桶
Media media_id, app_id, 包名, App 版本, 渠道, 是否送审版本
Imp(可多个) slot_id, scene 类型, 支持样式列表, 请求条数, 底价, 屏蔽列表, 是否允许下载, 是否开屏预加载
Device 系统, 系统版本, 厂商, 机型, 屏幕, UA, IP(v4/v6), 网络类型, 运营商, 设备 ID(OAID / IDFA / CAID / Android ID,按授权状态), 是否限制追踪
Geo 国家, 省, 市, 经纬度(授权时), 来源
User ADX 用户 ID, 标签, 人群包命中, 各 DSP 的买方 ID(ID 映射结果)
Regs 个性化推荐开关, 未成年人标记, 地区法规信号
Candidates 询价结果归一化后的候选广告列表(见 11.1)
Trace 各阶段耗时、各 DSP 状态(未发送原因 / 超时 / 无出价 / 出价被过滤原因)

request_id 设计:地域(4 bit) + 机器(12 bit) + 毫秒时间戳(41 bit) + 序号(7 bit) 的 64 位 ID,或 128 位 UUIDv7。要求全局唯一、趋势递增(便于日志按时间分区),并贯穿请求、询价、上报、结算全链路。


5. 在线竞价链路

5.1 端到端时序

sequenceDiagram
    participant SDK
    participant GW as adx-gateway
    participant AS as adx-server
    participant KV as ID/标签 KV
    participant D1 as DSP-1(RTB)
    participant D2 as DSP-2(API)
    participant K as Kafka
    SDK->>GW: 广告请求(Protobuf,签名 + 加密)
    GW->>GW: 验签、解密、防重放、限流、解析
    GW->>AS: 内部请求(gRPC,携带 deadline)
    AS->>AS: 请求过滤(无效、流量屏蔽规则)
    AS->>KV: 批量查询 ID 映射与标签(本地缓存未命中时)
    AS->>AS: 补全、底价、DSP 选择(预定向、配额、流量筛选)
    par 并发询价,统一截止时间
        AS->>D1: OpenRTB BidRequest
        AS->>D2: 私有协议请求
    end
    D1-->>AS: BidResponse
    D2-->>AS: 超时(丢弃)
    AS->>AS: 归一化、过滤链、排序、定价、组装
    AS-->>GW: 广告响应
    GW-->>SDK: 广告响应(带追踪链接)
    AS--)K: 竞价日志(异步批量)
    SDK->>SDK: 渲染、曝光判定
    SDK->>K: 曝光 / 点击等上报(经 adx-tracker)

5.2 延迟预算

以服务端总预算 300 ms(P99)为例:

阶段 预算 说明
gateway 处理 ≤ 3 ms 验签、解密、解析
gateway → server ≤ 1 ms 同可用区内网
请求过滤 + 补全 ≤ 10 ms 本地缓存优先;KV 批量查询一次,超时 8 ms 即放弃补全
DSP 选择 ≤ 2 ms 全部内存计算
询价 按 DSP 配置,150–250 ms 截止时间 = min(DSP 超时, 请求 deadline − 预留尾部时间)
竞价 + 组装 ≤ 5 ms 含宏替换、监测链接生成
预留尾部 10–20 ms 应对 GC、调度抖动,保证整体不超时

deadline 传递:gateway 收到请求即计算绝对截止时间 deadline = 接收时间 + 广告位超时 − 预估回程网络时间,随请求下传;每个阶段开始前检查剩余时间,不足就跳过可选步骤(补全、模型打分),直接进入下一阶段。

开屏的特殊处理:开屏对首屏耗时极敏感,客户端总等待通常只有几百毫秒到 1 秒,且素材需要提前下载。开屏采用"预加载 + 实时"两段式(见 13.5)。

5.3 adx-server 的 Pipeline

竞价核心按阶段组织成 Pipeline,每个阶段是一个独立的处理器,读写同一个 AdContext:

Parse → RequestFilter → Enrich → Floor → DspSelect → Fanout → Normalize → AdFilter → Rank → Price → Assemble → Log
阶段 输入 输出 可跳过
RequestFilter 请求 是否继续;命中规则 id 否
Enrich 请求 设备、地理、用户、ID 映射 是(超时或 KV 故障时)
Floor 广告位、上下文 每个 Imp 的有效底价 是(退回静态底价)
DspSelect 广告位绑定、配置、配额、模型 本次要询价的 (DSP, 代码位) 列表 否(模型部分可跳过)
Fanout 询价列表 各 DSP 原始响应 否
Normalize 原始响应 统一候选列表 否
AdFilter 候选 过滤后候选 + 过滤原因 否
Rank / Price 候选 胜出列表与结算价 否
Assemble 胜出列表 SDK 响应 否
Log 全部上下文 竞价日志 异步,不阻塞响应

设计原则:

  • 阶段之间只通过 AdContext 通信,便于单测、回放和插入新阶段
  • 每个阶段记录耗时和结论到 Trace
  • 所有阶段读取的配置来自同一个配置快照版本,请求开始时获取快照引用,避免请求中途配置切换导致不一致

实现约束:

约束 做法 避免的问题
上下文按阶段拆分 AdContext 拆成若干子结构,每个阶段声明读取哪些、写入哪些,只写自己负责的部分 上下文变成任何阶段都能改的"万能对象",阶段间依赖藏在字段读写里
必需与可选 每个阶段标明必需或可选:可选阶段出错只记录并降级,必需阶段出错才终止请求 各阶段各自决定失败还是降级,行为不一致
统一短路 任何阶段都可以终止处理;终止后只执行"组装空响应"和"写日志"两个收尾阶段 终止后后续阶段是否执行、日志是否写入各处约定不同
固定顺序 阶段顺序在代码中固定,只用开关控制某阶段是否执行,不做运行时动态编排 通用编排框架带来的调试困难和额外内存分配
请求内执行 一个请求的阶段在同一个协程内顺序执行,询价阶段内部再并发扇出;不在阶段之间放队列和独立线程池 阶段间队列(SEDA 式)让每过一个队列就多一次排队和调度,在百毫秒级链路中累积并放大尾延迟,截止时间和取消也难以统一

DSP 之间的资源隔离由询价阶段的按 DSP 并发限制和熔断保证(9.4),不需要把整个流程拆成多段队列。宏观上是阶段链,局部可以并行(5.4)。

5.4 局部并行:KV 查询与 CPU 计算重叠

补全阶段的远程 KV 查询要等几毫秒网络,这段时间里可以先做不依赖用户数据的工作,缩短询价之前的关键路径。

依赖分析

不依赖用户数据(可以先做) 依赖用户数据(必须等 KV)
流量屏蔽规则匹配(城市、渠道、版本来自请求本身) 人群包定向(预定向中的人群维度)
IP 转地理(进程内 IP 库) 场景级频控(可能让请求直接返回空)
广告位配置、样式准备 重复广告判定所需的最近广告记录
静态底价 按用户价值分层的动态底价
预定向中的非用户维度(广告位绑定、系统、版本、网络、省份) 流量筛选模型中的用户历史特征
配额检查、熔断状态 需要 DSP 买方 ID 的 DSP
各 DSP 请求中与用户无关部分的预编码 需要下发用户标签的 DSP 的请求体

三种执行方式

以 KV 查询 P99 为 6 ms、左列 CPU 工作 1 ms、右列 0.5 ms 为例(示意):

① 串行
   |--KV 查询 6ms--|-左列 1ms-|-右列 0.5ms-|→ 开始询价                约 7.5 ms 后开始询价

② KV 与 CPU 重叠
   |--KV 查询 6ms--|
   |-左列 1ms-|      等待     |-右列 0.5ms-|→ 开始询价                约 6.5 ms 后开始询价

③ 重叠 + 用户无关的 DSP 提前询价
   |--KV 查询 6ms--|
   |-左列 1ms-|→ 向"用户无关"的 DSP 立即询价(提前约 5.5 ms 出发)
                 |-右列 0.5ms-|→ 再向"用户相关"的 DSP 询价
  • 方式 ② 只省下左列的 CPU 时间(1–2 ms),相对总预算很小
  • 方式 ③ 的收益更大:很多 DSP 只使用设备 ID,不需要 ADX 的标签和买方 ID(按 API 方式接入的 DSP 大多如此),它们的请求不必等待 KV。这些 DSP 相当于多出几毫秒响应时间;当 DSP 响应时间贴近超时线时,能直接降低超时率

执行流程

Parse → 发起 KV 查询(异步)→ 本地补全 → 规则匹配 → 静态底价 → DSP 选择·第一阶段
      → [向用户无关组的 DSP 扇出]
      → 等待 KV(带截止时间)→ 用户相关补全 → DSP 选择·第二阶段 → [向用户相关组的 DSP 扇出]
      → 收集全部响应 → 归一化 → 过滤 → 排序 → 组装
  • 尽早发起 KV 查询:解析出设备 ID 后立即异步发起批量查询;本地缓存命中时走同步快路径,不产生异步开销
  • DSP 选择·第一阶段:用非用户维度的位图算出候选集,并按 DSP 接入配置中的"是否需要标签、买方 ID、人群定向"把候选分为用户无关组和用户相关组
  • DSP 选择·第二阶段:KV 返回后,对用户相关组再与人群维度位图做按位与,并用完整特征执行流量筛选模型
  • 等待点的截止时间:最长等待 = min(KV 超时, 剩余预算中留给询价的部分)。超时则按无用户数据继续:用户相关组照常询价但不带标签和买方 ID,人群定向不满足的条目不发送
  • 重复广告判定在响应回来后的广告过滤阶段执行,此时 KV 早已返回,不受提前询价影响

提前询价与场景级频控的冲突

KV 返回后,如果发现用户已触达场景级频控(例如当天开屏次数已满),整个请求应返回空,提前发出的询价就浪费了 DSP 的 QPS 配额。处理方式:

  • 取消在途询价,并统计被取消的询价比例
  • 按广告位决定是否开启提前询价:没有场景级频控的广告位直接开启;有场景级频控但主要由 SDK 本地频控拦截(SDK 已达上限就不会发起请求)的广告位,取消比例很低,也可以开启;取消比例高的广告位不开启

实现注意事项(伪代码,未实测)

fut = 启动协程执行 KV 批量查询:结果写入独立的结果对象,完成后关闭 done 通道
... 执行不依赖用户数据的阶段,向用户无关组扇出 ...
select {
  case done 已关闭:  使用 fut.result
  case 到达等待截止时间:  按无用户数据继续,放弃 fut
}
问题 做法
数据竞争 KV 协程只写自己的结果对象,主协程在 done 关闭后才读取;KV 协程不直接写共享的请求上下文
被放弃的协程 等待超时后 KV 协程仍在运行,由 KV 客户端自身的超时和 context 取消保证其结束
对象池生命周期 请求上下文从对象池取出并在请求结束时归还,被放弃的 KV 协程可能仍在写结果对象。结果对象不能与请求上下文同生命周期地回收,或者等 done 关闭后再回收——这是此类优化最隐蔽的缺陷来源
在途询价的取消 提前发出的询价要能被取消(关闭对应的 HTTP 请求),并正确释放该 DSP 的并发许可

度量与决策

  • 新增指标:主协程在等待点的实际阻塞时间(理想接近 0,说明 CPU 工作已覆盖 KV 延迟)、提前询价被取消的比例
  • 用 A/B 实验比较串行与方式 ③ 的各 DSP 超时率、出价率和 RPM,按实验结果决定是否全面开启
  • 方式 ② 实现简单,在分阶段耗时显示补全阶段位于关键路径上时实施;方式 ③ 需要 DSP 分组、两阶段选择和取消逻辑,复杂度明显更高,在开屏等预算紧张的场景或 DSP 超时率偏高时收益最明显

6. 接入层

6.1 SDK 协议

项 设计
传输 HTTPS(TLS 1.2+,优先 TLS 1.3),HTTP/2 连接复用
编码 Protobuf;字段只增不删,新字段设默认值,保证新旧 SDK 兼容
压缩 请求体一般 < 2 KB,不压缩;响应体含多个广告和监测链接时 gzip
版本 请求头带协议版本和 SDK 版本;服务端按版本决定可下发的样式和交互
签名 sign = HMAC-SHA256(app_secret, 时间戳 + nonce + body 摘要),app_secret 按 App 分配、可轮换
防重放 时间戳与服务器时间偏差 ≤ 5 分钟;nonce 在 5 分钟窗口内去重(按 nonce 哈希到 Redis,或在 gateway 本地用分片的时间轮 + 布隆过滤器近似判重)
敏感字段 设备 ID 等字段在应用层再加密一次(信封加密:随机对称密钥加密数据,服务端公钥加密该密钥),防止抓包和中间设备记录

6.2 联盟媒体与上游接入

方式 说明
ADX SDK 首选,设备信息完整,曝光判定可控
S2S API 媒体服务端调用,按 App 发放密钥;约定曝光判定与上报由谁负责;要求媒体透传真实 IP 和 UA
OpenRTB 上游 接其他 SSP 的流量,此时 ADX 是被询价方;需要校验 app-ads.txt 和 schain

所有接入方式在 gateway 统一转换为内部模型,下游不感知接入来源。

6.3 限流

层级 维度 算法 动作
四层 LB 源 IP 连接数 连接限制 防 SYN Flood 与连接耗尽
gateway 全局、每个 App、每个广告位 本地令牌桶(总配额按实例数均分,实例数变化时由配置中心下发更新) 超限直接返回空广告,不进入竞价
gateway 单设备 滑动窗口(本地,按设备 ID 哈希分片) 防单设备异常高频请求
server 过载保护 并发请求数上限 + 排队时间阈值 排队超过阈值直接快速失败(在 deadline 前就返回空,而不是超时)

过载时按流量价值分级丢弃:先丢低 eCPM 广告位和联盟流量,保自有 App 的核心场景。

6.4 协议标准化

gateway 负责把各种来源的字段统一:

字段 标准化
系统与版本 统一枚举,版本转为可比较的整数
厂商与机型 按机型库归一(大小写、别名)
网络 统一为 2G/3G/4G/5G/WiFi/未知;运营商用 MCC-MNC
IP 取真实客户端 IP(识别代理头,只信任自己 LB 添加的头);IPv6 统一格式
UA 保留原始值,同时解析出浏览器内核版本(部分 DSP 用于渲染兼容判断)
设备 ID 校验格式(OAID、IDFA 为 UUID 格式;全 0 视为无效);按系统授权状态决定是否保留
时间 统一毫秒时间戳,服务端时间为准

7. 请求处理与流量管控

7.1 无效请求过滤

规则 判定
必填字段缺失 广告位 id、App id、系统、IP 缺失
配置无效 广告位不存在或已下线、App 未审核通过
设备无效 设备 ID 格式非法;UA 为空或命中爬虫库
黑名单 设备、IP、IP 段命中黑名单(来自反作弊,见第 17 章)
版本不兼容 SDK 版本低于最低支持版本

被过滤的请求直接返回空广告,并记录原因码(附录 B),用于媒体侧排查。

7.2 流量屏蔽规则(策略性过滤)

流量屏蔽规则由媒体、运营、合规方设置,决定"某类流量不出广告,或不出某些广告、某些 DSP"。它和 DSP 的预定向(8.2)方向相反:预定向是买方声明想要什么,屏蔽规则是卖方声明不卖什么。

使用场景

场景 规则示例
应用商店送审 App 送审版本不出广告,或不出下载类广告,避免审核被拒
特殊时期 全国哀悼日全部屏蔽;重大活动、突发事件期间在指定城市屏蔽某些类型广告
地域合规 某些地区不出医疗、金融等行业广告
渠道合同 预装渠道、合作厂商渠道包按合同不出第三方广告,或只允许指定 DSP
版本缺陷 某 App 版本的某个样式渲染异常,在修复前对该版本屏蔽该样式或相关 DSP
用户体验 新安装用户首日不出开屏;未成年人模式屏蔽不适宜行业
竞品与商务 某些渠道或场景不出竞品广告主的广告
测试 内部测试设备只出测试广告

规则模型:条件(城市 ∈ S1 ∧ 渠道 ∈ S2 ∧ 版本 ∈ S3 ∧ 时间 ∈ T ∧ ...)→ 动作

动作 效果 生效阶段
全部屏蔽 直接返回空 请求过滤
屏蔽指定 DSP 从候选 DSP 中剔除 DSP 选择
屏蔽广告类型 如只屏蔽下载类、某些行业、某些样式 广告过滤

一次匹配得到全部命中的动作,分别在对应阶段使用。多条规则同时命中时按优先级决定,例如全国性的"特殊时期全部屏蔽"与某 App 的"允许内部测试广告"冲突时,由优先级决定哪条生效。

匹配实现:规则只有几百到几千条,但每个请求都要评估,30 万 QPS 下单次只能花几微秒。

构建(配置快照构建时):
  规则按优先级从高到低编号 0..M-1(编号越小优先级越高)
  每个维度的每个取值建一个 M 位的规则位图;规则在某维度"不限",该维度所有取值的位图中这一位都为 1
  版本区间:把所有规则中的版本端点排序切段,每段一个位图
  时间段:不在请求中判断,由后台定时器每秒计算"当前生效的规则位图"

在线:
  命中 = 生效位图 AND 城市位图[c] AND 渠道位图[ch] AND App 位图[a] AND 版本段位图[v] AND ...
  最高优先级规则 = 命中位图中最低位的 1(找到第一个非零的 64 位字,再取尾部零的个数)

M = 4096 时每个位图只有 64 个字,七八个维度的按位与加一次找最低位只需几百纳秒。进一步优化:

  • 按 App 预切分:大多数规则只作用于特定 App,构建时为每个 App 生成"该 App 规则 + 全局规则"的子集,M 从几千降到几十
  • 结果缓存:影响规则的属性组合(App、版本段、渠道、城市、系统、广告位)有限,缓存"属性组合 → 动作",key 带配置版本号
  • 属性只处理一次:城市在补全阶段由 IP 得到,版本号和渠道在网关转为整数,规则匹配只处理整数
  • 紧急规则走独立的小快照段,5 秒内生效(15.5)

正确性保障:管理平台提供规则模拟器(输入请求属性,显示命中哪条规则及原因);发布前检测规则冲突;竞价日志记录命中的规则 ID,新规则影响的请求量远超预期时告警。

7.3 请求补全

补全项 来源 实现
地理位置 IP 库 进程内加载 IP 段表(按起始 IP 排序的数组,二分查找),随配置快照每周更新
广告位配置 配置快照 内存查表
用户标签 标签 KV 按设备 ID 查询;本地 LRU 缓存 5–10 分钟
DSP 买方 ID ID 映射 KV 按设备 ID 查询该设备在各 DSP 的 ID(Web 场景的 cookie 映射;App 场景多数直接使用设备 ID)
频次信息 计数存储 该设备在当天的请求数、曝光数(用于媒体侧频控和反作弊)

KV 访问原则:

  • 一个请求只做一次批量查询(多个 key 合并为一次 MGET / batch read)
  • 设独立超时(如 8 ms),超时就放弃补全,继续竞价——缺少标签只影响 DSP 出价,不影响流程
  • 异步发起:解析出设备 ID 后立即发起查询,等待期间先执行不依赖用户数据的阶段,必要时对不需要用户数据的 DSP 提前询价(见 5.4)
  • 本地缓存前置。若负载均衡按设备 ID 做一致性哈希,同一设备的请求总落到同一实例,本地缓存命中率可以很高;代价是实例扩缩容时缓存大量失效,需要容忍短时命中率下降

7.4 用户与设备标识

场景 标识 注意
Android OAID(主流)、Android ID(受限) 用户可重置或关闭;不同厂商的 OAID 获取方式不同,由 SDK 适配
iOS IDFA(需用户通过 ATT 授权)、CAID(国内行业方案) 未授权时 IDFA 为全 0;CAID 需按规范生成和版本管理
Web Cookie 需要与 DSP 做 cookie 映射(像素同步)
无设备 ID IP + UA 等弱标识 仅用于反作弊和统计,不作为用户 ID 下发

个性化开关:用户关闭个性化推荐时,请求中打标 personalized=0,不下发用户标签和人群包命中信息,DSP 不得做个性化投放(见 22.3)。

7.5 用户数据存储:ID 映射、标签与人群包

在线竞价需要按设备读取三类用户数据:统一用户 ID 与各 DSP 的买方 ID、用户标签、频控计数(7.6)。它们都按用户组织,放在同一条用户记录里,一个请求一次批量读完。

ID 映射

层次 内容 来源
设备 ID OAID、Android ID、IDFA、CAID(可能多个版本并存)、IDFV SDK 按授权状态采集
统一用户 ID(uid) 把同一设备、同一用户的多个 ID 归并为一个内部 ID 离线 ID 图谱
DSP 买方 ID 同一用户在各 DSP 侧的 ID App 场景多数直接透传设备 ID;Web 场景靠 cookie 映射;少数 DSP 走服务端 ID 同步。三种方式的具体流程见附录 F.1

ID 图谱:同一次请求里同时出现的多个 ID(如 OAID + Android ID + CAID)属于同一设备。离线每天用请求日志建图:节点是 ID,边是"同时出现",用并查集求连通分量,每个分量分配一个 uid。

  • 防超级节点:全 0 的 IDFA、默认值 OAID、被批量伪造的 ID,会把大量设备连成一个分量。建图前过滤黑名单值和出现频次异常高的 ID,并限制分量大小
  • ID 变化:OAID 可重置、IDFA 需要授权,同一设备的 ID 会变化;图谱每天增量更新,新旧 ID 通过同时出现的其他 ID 接上
  • 在线:请求带多个设备 ID 时一次批量查询,合并得到 uid;查不到按新设备处理,不阻塞竞价

用户标签

类型 例子 生产方式
设备属性 机型价位段、系统版本、运营商 请求时实时计算,不存储
地理 常驻城市 离线按天
兴趣与行为 内容偏好、活跃时段、已安装 App 类别 离线按天 + Flink 实时增量
预测属性 年龄段、性别、消费能力 离线模型按天
人群包 广告主或 DSP 上传的设备包、运营圈选的人群 按需导入
  • 标签统一编码为 32 位整数:高位是标签类型,低位是取值,与定向条件使用同一套编码(8.2)
  • 带权重的标签存为 (标签 ID, 权重),权重随时间衰减
  • 一个用户的全部标签序列化为紧凑的 Protobuf:标签 ID 排序后差值编码 + varint,通常几百字节

人群包:正排与倒排

人群包是一组设备(用户)的集合,用于定向(只投给包内用户)、排除(不投给包内用户)或作为种子做相似人群扩展。常用的人群包:

类别 常用人群包 主要来源 常见用法
基础属性 男性 / 女性;18–24、25–30、31–40、41–50、50 岁以上;学生;职场白领;孕期与宝妈 预测模型 定向
地理 一线 / 新一线 / 二线 / 下沉市场城市人群;常驻某城市;商圈、景区、机场、高校等地理围栏的到访人群;异地出行人群 常驻地计算、定位与 IP 历史 定向
设备 高端机(机型价位 ≥ 3000 元);iOS 用户;近 90 天换机人群;5G 用户 请求中的设备信息 定向
兴趣 游戏(重度 / 休闲)、网购、美妆、母婴、汽车(看车 / 购车意向)、金融理财、教育(K12 家长 / 考研 / 职业教育)、旅游、美食、影视与短剧、体育 内容浏览与行为模型 定向
App 安装与活跃 已安装某类 App(如竞品 App 安装人群);近 7 天活跃;沉默 30 天以上;高频使用人群 App 安装列表(需授权)、活跃记录 定向、排除
广告行为 近 30 天点击过某行业广告;近 7 天曝光未点击;已下载未激活;已激活;已付费 竞价日志与事件日志 再营销、排除
广告主一方人群 已注册用户、已付费用户、会员、流失用户、种子高价值用户 广告主或 DSP 上传(设备 ID,加密或哈希) 召回、排除、种子
相似扩展(Lookalike) 以种子人群扩展出的 1 倍 / 3 倍 / 5 倍相似人群 基于种子人群的模型扩展 定向
排除类 已安装本 App(拉新时排除);已转化;作弊与黑名单设备;未成年人 安装、转化、反作弊数据 排除
运营圈选 节日活动参与人群、特定活动曝光人群 运营按条件圈选 定向、排除
  • 标签与人群包的区别:标签是用户的属性(用户 → 属性值,平台统一定义、语义明确,可带权重);人群包是用户的集合(集合 → 用户列表,可由任何一方定义,往往只归创建者使用)。表中"基础属性""兴趣"等类别是由标签物化生成的系统预置人群包,便于做交并差和作为种子;"广告主一方人群""相似扩展""运营圈选"是标签无法表达的独立人群包。在线定向时,性别、年龄、兴趣等标准维度直接用标签判断,只有不对应标准维度的集合才按人群包判断成员关系;粗粒度标签可以按约定下发给 DSP,人群包命中信息只下发给该包的所有者
  • 命名与编号:人群包在线使用整数 ID,名称便于人工识别,建议统一格式,例如 {来源}_{主体}_{类别}_{时间窗}(adv1024_paid_30d、media_game_heavy_7d)
  • 规模差异大:地理、设备类人群包可达上亿设备;广告主一方人群从几万到几千万不等;排除类通常较小但查询最频繁
  • 合规限制:健康状况、宗教信仰、政治倾向等敏感属性不得建包或用于定向;未成年人只能用于排除;广告主上传的数据需确认用户授权,并只用于约定的用途(22.3)

一个人群包可能有几百万到上亿个设备,人群包总数可达数千个。存储有两个方向:

方向 结构 适合 不适合
正排 用户记录中存"所属人群包 ID 列表" 在线请求:一次读取就拿到用户所属的全部人群包 人群包更新时要改写几百万条用户记录
倒排 每个人群包一个 Roaring Bitmap,存属于它的 uid 人群包的导入、更新、交并差运算、覆盖人数估算;在内存中 O(1) 判断成员关系 人群包很多时内存大;在线逐个人群包判断开销大

混合方案:

  • 数量少、查询热的人群包(如品牌订单定向用的包)以 Roaring Bitmap 形式随配置快照加载进 adx-server 内存,在线直接判断成员关系,不走网络
  • 数量多、体量大的人群包离线转成正排,写入用户记录
  • 人群包带版本号:新版本全部写完后,在配置里把"逻辑人群包 → 物理版本"的指针切过去,避免更新过程中读到一半新一半旧

倒排要求 uid 是稠密的整数编号。设备 ID 字符串必须先映射为连续整数,否则编号过于分散,Roaring Bitmap 的每个桶只有一两个元素,压缩效果会变差。

Roaring Bitmap

Roaring Bitmap 是存储 32 位无符号整数集合的压缩位图,支持快速的交、并、差运算。

普通位图用 1 bit 表示一个整数是否存在,32 位取值空间的位图固定占 2³² / 8 = 512 MB,数据稀疏时浪费极大。Roaring 把整数拆成高 16 位和低 16 位:

高 16 位 → 桶(最多 65536 个,只为存在的桶分配空间,桶的 key 放在有序数组中)
低 16 位 → 桶内的容器,按桶内元素密度三选一:

  数组容器   元素 ≤ 4096 个:有序 uint16 数组,每个元素 2 字节,最多 8 KB
  位图容器   元素 > 4096 个:固定 65536 bit = 8 KB 的位图
  run 容器   连续区间多时:存 (起点, 长度),例如 [1000, 1999] 只需 4 字节

4096 是数组容器与位图容器大小相等的分界点:4096 × 2 字节 = 8 KB。每个桶按实际密度选择更省空间的表示,所以稀疏、稠密、连续的数据都能压缩得很好。

运算

  • 成员判断:二分查找高 16 位所在的桶,再在容器内查低 16 位
  • 交集:先对两边的桶 key 求交,只比较公共桶里的容器
    • 位图 ∧ 位图:按机器字做 AND
    • 数组 ∧ 数组:归并或跳跃查找
    • 数组 ∧ 位图:遍历数组,在位图中逐个判断
  • 运算结果按元素个数重新选择容器类型

空间对比(估算,未实测):

数据 普通位图 uint32 有序数组 Roaring
100 万个在 2³² 上随机分布的 ID 512 MB 4 MB 约 2 MB(大多是很小的数组容器)
0 到 999999 的连续 ID 512 MB 4 MB 约 128 KB(16 个位图容器),run 优化后只需几十字节

64 位整数的常见做法是以高 32 位为 key,映射到一个 32 位 Roaring Bitmap(Java 的 Roaring64NavigableMap、C/C++ 的 Roaring64Map)。Lucene / Elasticsearch、Druid、ClickHouse(groupBitmap 系列函数)、Doris 和 StarRocks 的 BITMAP 类型、Spark、Pinot 都使用 Roaring,各语言实现共享同一种序列化格式。倒排求交中的应用见 检索系统全景 3.3、3.4 节。

存储选型:Aerospike

选型 特点
Aerospike 索引在内存、数据在 SSD,适合亿级用户、几百字节的记录,按 key 点查延迟稳定,成本低
Redis Cluster 全内存,延迟最低;亿级数据成本高
按天生成的只读数据文件 标签按天全量更新时,离线生成只读文件由服务直接加载,没有在线写入压力;不适合实时更新的数据

Aerospike 的存储引擎:Aerospike 用 C 实现了自己的存储引擎(混合内存架构),不基于 RocksDB,也不是 LSM 树。

  • 主索引在内存:每条记录的索引项约 64 字节,key 是对用户 key 做 RIPEMD-160 得到的 20 字节摘要
  • 数据在 SSD:直接写裸设备,绕过文件系统;写入先攒成大块(write block),整块顺序写入
  • 碎片整理:记录更新或删除后旧版本所在的块出现空洞,块内有效数据比例低于阈值时,后台把剩余有效数据搬到新块并回收旧块
  • 也支持纯内存命名空间;较新版本统一了内存与 SSD 的存储引擎,并允许把索引放到闪存以节省内存(实现相关,以所用版本为准)
  • 集群按 4096 个分区自动分布数据并维护副本,客户端缓存分区表,请求一跳直达数据所在节点;支持跨机房复制和可选的强一致模式
维度 Aerospike RocksDB(LSM)
点查 一次内存索引查找 + 一次 SSD 读,延迟稳定 可能逐层查找多个 SSTable,靠布隆过滤器和缓存弥补
写入 攒块顺序写;更新即写入新版本,旧版本由碎片整理回收 MemTable + WAL,后台多层 compaction,写放大较高
内存 每条记录约 64 字节索引常驻内存,10 亿条约 64 GB(单副本) 索引与过滤器按块组织,内存占用更低
范围扫描 弱:按 key 摘要分布,不保持 key 的顺序 强:按 key 有序
形态 分布式数据库 嵌入式存储引擎库

ADX 的用户数据是"按 key 点查、记录几百字节、数据量大、要求延迟稳定"的负载,与 Aerospike 的设计吻合。需要范围扫描、有序遍历的数据更适合 LSM 类存储(RocksDB、TiKV、HBase)。

容量估算:5 亿设备 × 约 300 字节 ≈ 150 GB 数据,主索引约 5 亿 × 64 字节 ≈ 32 GB(单副本),按 2 副本部署,几台大内存 SSD 机器即可承载。

写入

  • 离线全量:写入新版本的集合(set 或命名空间),全部写完后切换读指针;写入限速,避免冲击在线读延迟
  • 实时增量:Flink 写入,限速
  • 频控计数:Flink 微批合并后写入(7.6)

读取:一次批量读、独立超时(5–8 ms)、超时降级为无标签竞价、本地 LRU 缓存前置,见 7.3。

合规:用户关闭个性化推荐或处于未成年人模式时,不读取、不下发标签和人群包命中信息;分析用数据中设备 ID 加盐哈希(22.3)。

7.6 频控

ADX 里的频控类型

类型 例子 负责方
媒体体验频控 开屏每天最多 N 次;插屏两次之间至少间隔 X 分钟;同一场景每小时最多 M 条 ADX(服务端)+ SDK(本地)
广告重复控制 同一广告 5 分钟内不对同一用户重复出现;同一广告每天最多 N 次 ADX(见 11.2 的重复广告过滤)
品牌订单频控 PDB 订单每人每天最多 3 次 ADX
请求频率 单设备、单 IP 请求频率异常 ADX(限流与反作弊,见 6.3、第 17 章)
广告主频控 某广告活动每人最多 N 次 DSP 自己负责,ADX 不感知

基本实现

  • 计数来源:曝光发生在客户端,已确认计数由 Flink 去重后的曝光事件流写入;下发时另记"待确认"计数(见下文"两个写入者:拆分字段"与"常见问题与对策"中的上报延迟)
  • 存储结构:每个用户一条记录,记录内是 频控维度 ID → 计数 的紧凑 map(Aerospike 的 map bin 或 Redis 的一个 hash),按天分桶并设 TTL。不要一个维度一个 key:key 数量会爆炸,每个 key 的元数据开销也远大于计数本身。具体设计见下文"数据结构设计"
  • 读取:与 ID 映射、用户标签放在同一条用户记录里(7.5),一次批量读取,不增加网络往返
  • 判断:达到上限的维度,过滤对应的候选(广告、订单)或直接返回空(场景级上限)

数据结构设计:以广告 ID 维度为例

频控数据结构的选择取决于三件事:一次请求要同时检查十几到几十个候选广告;每次曝光给某个广告加一;计数按天或按滑动窗口自动失效。推荐以用户为主键的 KKV:一个用户一条记录,记录内是"广告 → 计数"的紧凑小表。它用一次读取检查全部候选,并让所有更新都落在单条记录上。

三种模型

模型 结构 优点 缺点
KV(每个 用户×广告 一个 key) uid:ad → count,带 TTL 实现最简单,INCR 原子加一 key 数量 = 用户数 × 广告数,每个 key 的存储元数据开销远大于计数本身;检查 N 个候选要读 N 个 key,且可能分散在不同分片
KKV(用户 → 广告 → 值) 一级 key 为 uid,二级 key 为广告 ID 一次读取拿到全部计数;更新都在单条记录内;整条记录设 TTL 记录随用户看过的广告增多而变大,需要上限
用户级紧凑二进制块 一个 uid 对应一个自定义编码的字节数组 最省内存,布局完全可控 编码、合并和并发控制都要自己实现

KKV 可以用 Redis Hash(HINCRBY、HMGET)、Aerospike 的 map bin(一次 operate 内完成 map 增量、按 key 批量读取、条件删除),或支持 KKV 模型的自研存储实现。规模较大时常用"KKV 语义 + 紧凑二进制值"。

条目结构(每个 用户×广告 一个条目,定长编码,示意)

字段 大小 说明
ad_key 4 字节 广告的整数编号;ID 为字符串时取 32 位哈希,冲突的后果只是偶尔多拦截一次
day 2 字节 以某个起点计的天序号,用于自然日窗口
count 2 字节 当天曝光次数
last_ts 4 字节 最近一次曝光时间(秒,相对某个起点)

每个条目 12 字节。用户记录 = 头部(版本、条目数)+ 按 ad_key 排序的条目数组。

不同频控规则的存储方式

规则 存储 判断
每自然日最多 N 次 day + count day 为今天且 count ≥ N 则拦截;day 不是今天视为 0
两次曝光间隔至少 X 分钟 last_ts 当前时间 − last_ts < X 则拦截
任意滚动 24 小时最多 N 次(N 较小) 最近 N 次曝光时间戳的环形数组 其中最早的时间戳仍在 24 小时内,说明窗口内已有 N 次,拦截。只存 N 个时间戳即可精确实现滑动窗口
滚动窗口且 N 较大 按小时分桶的计数 最近 24 个桶求和,近似滑动窗口

广告主、活动、行业等更粗维度的频控复用同一结构,在 ad_key 高位加类型前缀区分。

控制记录大小

  • 只记录配置了频控的广告:没有频控需求的广告不写入,这是最有效的瘦身手段
  • 条目数上限 M(如 256):满了淘汰 last_ts 最早的条目。上限按真实数据分布设定,使绝大多数用户不触发淘汰;被淘汰的条目少计,相当于放宽频控,比丢失整条记录安全
  • 整条记录 TTL:等于最长频控窗口加余量(如 2 天),每次写入刷新 TTL,不活跃用户的记录自动消失

插入、删除、计数

M 通常为几十到几百,整条记录几 KB,完全在 CPU 缓存内,"有序数组 + 二分查找"最快:

操作 做法 复杂度
批量查询 每个候选在有序数组中二分查找;或候选排序后与条目数组归并扫描 每个候选 O(log M);归并 O(M + N)
计数加一 二分找到条目:day 为今天则 count+1,否则重置为 1;更新 last_ts O(log M)
插入新条目 找到位置后整体后移;满了先淘汰最旧条目 O(M),只是几百字节的内存移动
删除 惰性删除:写入时顺带压缩掉过期条目(day 不是今天且超出所有窗口);整条记录靠 TTL 过期 摊销到写入中

在线读路径只读不改;整理和压缩只在写入时进行。

使用 Redis Hash 时注意:字段数和值都较小时 Redis 使用紧凑的 listpack 编码(Redis 7 默认字段数不超过 128、值不超过 64 字节),超过后转为普通哈希表,内存明显增加,条目上限要参考这个阈值;按字段过期(HEXPIRE)从 Redis 7.4 起支持,更早的版本把日期拼进字段名(ad:day),再给整个 hash 设 TTL。以上为实现相关的细节,以所用版本为准。

原子更新:多线程与多机

原则:每次更新只涉及一条记录,并且这条记录只有一个写入者,或者在存储端原子执行。

单条记录原子

  • Redis:单个命令原子;"判断后修改"用 Lua 脚本,整个脚本原子执行。集群模式下多 key 操作要求落在同一槽,key 带 hash tag(如 {uid}:freq)
  • Aerospike:一次 operate 中的多个操作在记录锁内原子执行,可以配合过滤表达式做条件更新
  • 乐观并发(CAS):读出记录与版本号(Aerospike 的 generation 或自研存储的版本号),本地修改后要求版本号不变才写回,冲突则重试。适合存储端无法理解内部结构的自定义二进制块

同一用户的全部计数在一条记录中,不需要分布式事务。KV 模型要"原子地检查多个广告"很困难,因为它们分散在不同的 key 甚至不同分片。

单写者:按用户分区

做法 说明
Flink 按 uid 分区写入 曝光事件在 Kafka 中按 uid 分区,Flink 按 uid keyBy,同一用户的计数只在一个 task 中更新,把完整计数值覆盖写入 KV
在线服务按用户路由 负载均衡按设备 ID 一致性哈希,同一用户只落到一个实例,由持有该分片的协程更新

覆盖写比增量写安全:Flink 从 checkpoint 恢复后会重放部分事件,用 INCR 写外部存储会重复加一;覆盖写只会把同一个值再写一次,天然幂等。

两个写入者:拆分字段

在线服务在下发时预占(解决上报延迟与并发请求),Flink 在曝光确认后写入。两者若写同一字段,Flink 的覆盖写会冲掉预占。拆成两个字段:

  • confirmed:只由 Flink 覆盖写
  • pending:只由在线服务写入,每条预占带时间戳,超过确认期限自动失效
  • 判断:confirmed + 未过期的 pending 与上限比较

进程内多线程

做法 说明
分段锁 按 uid 哈希分成若干段(如 256 段),每段一把锁,避免全局锁
单写者协程 每段由一个协程独占,其他协程通过通道提交更新,完全无锁
写时复制 写入方复制记录、修改后原子替换指针,读者无锁读取
原子变量 只适用于定长的简单计数器,不适用于变长的条目数组

在线的原子"检查并预占"(伪代码,未实测)

输入:uid,候选广告及各自上限,本次选中的广告
原子执行(Redis Lua 或 Aerospike 条件更新):
  读取该用户记录
  对每个候选:confirmed + 未过期 pending ≥ 上限 → 标记为超限
  对选中且未超限的广告:追加一条 pending(带时间戳)
  返回超限列表

也可以把检查和预占分开:读取阶段只检查,允许极少数并发超限;竞价确定胜出广告后,只对胜出广告做一次原子预占。性能更好,代价是极少数并发请求略微超频。

容量估算与选型

日活 3 亿、平均每人 30 个有频控的广告条目、每条 12 字节(示意,未实测):3 亿 × 30 × 12 字节 ≈ 108 GB,不含记录头、存储引擎开销和副本。只记录有频控的广告、紧凑编码、整条 TTL 是控制容量的三个关键手段。

规模 方案
中小规模 Redis Hash(key 带 {uid}),HINCRBY + Lua 实现检查并预占
大规模 Aerospike map bin,或 KV 中存用户级紧凑二进制块;Flink 按 uid 单写者覆盖写 confirmed,在线服务写 pending
延迟要求极高 按用户路由 + 进程内分段计数 + 异步回写,接受实例故障时丢失少量计数

常见问题与对策

问题 现象 对策
上报延迟 曝光上报晚几秒到几分钟(弱网、批量上报、开屏预加载),期间的请求读到的计数偏小,导致超频 下发时先记一条"待确认"计数(短 TTL),曝光到达后转为正式计数、超时未曝光则作废;或按"已下发 × 历史展示率"估算
并发请求 信息流一次加载多个广告位,同一用户的多个请求同时读到"未超限",同时下发 下发时原子地"检查并预占"(Redis Lua:计数小于上限才加一并返回成功);或按用户一致性哈希路由到同一实例,在本地串行判断
窗口定义 自然日还是滚动 24 小时、时区、跨零点边界 滚动窗口用分桶近似:按小时分桶,取最近 24 个桶之和,不存每次曝光的时间戳
标识不稳定 OAID 重置、限制广告追踪、多 ID 并存,同一用户的计数分散到多个 key 以 ID 图谱归并后的统一用户 ID 计数;没有设备 ID 时依赖 SDK 本地频控
重复计数 事件重复上报或重试导致计数虚高、提前停投 只消费去重后的事件流(14.4);写入带事件 ID,保证幂等
缓存广告绕过服务端 预加载或缓存的广告在本地展示,没经过服务端判断 SDK 展示前执行本地频控;服务端在曝光时计数
客户端时间不可信 用户修改系统时间,本地频控被绕过 服务端频控以服务端时间为准;本地频控只做体验保护
存储故障 计数存储不可用 按类型区分:体验类频控放行,由 SDK 本地频控兜底;合同类频控(品牌订单)保守处理,暂停投放该订单
多层频控叠加 媒体、场景、广告、订单多层叠加,语义混乱 明确每层的维度和判断顺序;管理平台展示某个广告位实际生效的全部频控

优化手段

手段 说明
合并读取 频控计数与标签放在同一条用户记录或同一批次里读取
按用户路由 + 本地计数 同一用户的请求一致性哈希到同一实例,计数放本地内存并异步回写。延迟最低、无并发问题;实例宕机或扩缩容时丢失少量计数,适合体验类频控
固定长度的最近广告环 每个用户只存最近 K 个广告指纹(64 位哈希)和时间,判断重复时扫描这 K 个,比每个广告一个 key 省得多
只统计有上限的维度 没有配置频控的维度不计数
写入合并 Flink 按用户做秒级微批,把多次加一合并为一次写入,降低计数存储的写 QPS
SDK 本地频控 媒体体验类频控在客户端先执行,精确且无延迟;服务端负责跨设备和防篡改兜底
近似计数 反作弊用的高基数计数(每个 IP 的请求数)用 Count-Min Sketch 等概率结构,以很小的内存换可接受的误差

8. DSP 选择与流量筛选

DSP 选择决定"这次请求问谁"。它直接决定三件事:收入(问得越全,越可能拿到高价)、成本(每次询价都要消耗带宽和 CPU)、DSP 体验(发给 DSP 大量它不要的流量,会拉低出价率,DSP 会压低 QPS 甚至断开)。

8.1 选择流程

flowchart LR
    A["广告位绑定的<br/>DSP 代码位列表"] --> B["开关与时间段<br/>放量档位"]
    B --> C["预定向匹配<br/>地域、系统、版本、网络、<br/>人群、设备 ID 要求"]
    C --> D["流量屏蔽规则<br/>(屏蔽指定 DSP)"]
    D --> E["配额检查<br/>QPS、日请求、日曝光"]
    E --> F["熔断状态"]
    F --> G["流量筛选模型<br/>出价概率 × 期望价格"]
    G --> H["最终询价列表"]

8.2 预定向索引

预定向(pretargeting)是什么:DSP 在接入时告诉 ADX"只把满足这些条件的流量发给我",ADX 在询价之前按这些条件筛掉不符合的 DSP。DSP 的广告主结构决定了它只买某些流量:游戏类 DSP 只要 Android 高版本和好网络,本地生活类 DSP 只要少数城市,品牌 DSP 只要开屏。把这些流量之外的请求发过去,DSP 一定不出价,白白消耗双方的带宽、DSP 的 QPS 配额和机器资源。

它和另外两种"定向"的区别:

概念 谁声明 作用在哪 例子
预定向 DSP ADX 决定是否向该 DSP 询价 "只要北上广深的流量"
DSP 内部广告定向 广告主(在 DSP 内) DSP 决定用哪个广告出价,ADX 看不到 "这个游戏广告只投 18–30 岁男性"
流量屏蔽规则 媒体 / ADX 运营 ADX 决定某类流量不出某些广告(见 7.2) "送审版本不出下载类广告"

例子:4 个 (DSP, 代码位) 条目,编号 e0–e3。

条目 DSP 与代码位 预定向条件 绑定的广告位
e0 DSP-A(游戏)· 信息流代码位 系统 = Android,系统版本 ≥ 9,网络 ∈ {WiFi, 5G},必须有设备 ID 首页信息流
e1 DSP-B(本地生活)· 信息流代码位 省份 ∈ {北京, 上海, 广东} 首页信息流
e2 DSP-C(品牌)· 开屏代码位 机型价位 ≥ 3000 元 开屏
e3 DSP-D(综合)· 信息流代码位 必须有设备 ID 首页信息流

构建(配置快照构建时):每个维度的每个取值对应一个 4 位的位图,第 i 位为 1 表示条目 ei 接受这个取值;某条目在某维度"不限",该维度所有取值的位图里这一位都为 1。位图从左到右依次是 e0 e1 e2 e3:

广告位绑定   首页信息流      → 1 1 0 1     (e2 只绑定开屏)
系统         android         → 1 1 1 1
             ios             → 0 1 1 1     (e0 只要 Android)
系统版本段   [9, +∞)         → 1 1 1 1     (版本区间按所有规则的端点切段,每段一个位图)
             [0, 9)          → 0 1 1 1
网络         wifi / 5g       → 1 1 1 1
             4g / 3g / 未知  → 0 1 1 1
省份         广东            → 1 1 1 1
             浙江            → 1 0 1 1     (e1 只要北上广)
设备 ID      有              → 1 1 1 1
             无              → 0 1 1 0     (e0、e3 要求有设备 ID)
机型价位段   ≥ 3000          → 1 1 1 1
             < 3000          → 1 1 0 1     (e2 只要高端机)

这些位图由 DSP 的预定向配置构建,与用户无关:在配置快照构建时生成一次,所有请求共享,数量 = 各维度取值数之和,每个位图长度 = 条目数(例如 34 个省份 × 2000 个条目 ≈ 8.5 KB),不随用户数增长。每个请求只是按自己的属性值取出对应的几个位图做按位与,得到一个临时的结果位图,用完即丢弃。人群维度是唯一与用户数据有关的维度:请求补全阶段读出用户所属的人群包 ID 列表,把这些人群包对应的位图按位或,再参与按位与。

查询:请求为"首页信息流、Android 12、4G、广州、有 OAID、机型 2000 元":

  1 1 0 1   广告位绑定(首页信息流)
& 1 1 1 1   系统 android
& 1 1 1 1   系统版本 [9, +∞)
& 0 1 1 1   网络 4g
& 1 1 1 1   省份 广东
& 1 1 1 1   有设备 ID
& 1 1 0 1   机型价位 < 3000
= 0 1 0 1   → 候选:e1(DSP-B)、e3(DSP-D)

DSP-A 因为网络是 4G 被排除;如果同一个用户在 WiFi 下发起请求,DSP-A 也会进入候选。DSP-C 在广告位绑定这一步就被排除。候选集接着进入配额检查、熔断和流量筛选(8.3、8.4)。

规模与性能:位图按条目编号排列,千级规模的条目每个位图只有几十个 64 位字,十个维度的按位与在微秒以内完成。含"不在集合中"的否定条件,在构建时展开为"该维度所有其他值"的位图;请求缺少某维度取值时,使用该维度的"未知"位图。定向条件达到数万乃至百万级(品牌订单、自营广告)时,位图过长,改用下面的布尔表达式索引。

布尔表达式索引:Conjunction 算法

算法出自 Whang 等人的 Indexing Boolean Expressions(VLDB 2009)。定向条件写成析取范式,每个合取式是若干 属性 ∈ {值} 或 属性 ∉ {值} 条件的"与"。

原理:把每个合取式按其中 ∈ 条件的个数 K 分组,以"属性=值"为 key 建按合取式 ID 排序的倒排链。请求在每个属性上只有一个取值,所以一个合取式成立,等价于恰好有 K 条命中的链同时指向它、且没有 ∉ 链指向它。查询时拿第 K 小的链头当目标,把其余链直接跳过去,做"K 路求交",不可能成立的合取式整段跳过。

例子

合取式(K = 其中 ∈ 条件的个数):

ID 条件 K
c1 地域 ∈ {北京} ∧ 系统 ∈ {android} 2
c2 地域 ∈ {北京, 上海} ∧ 年龄 ∈ {25-30} 2
c3 系统 ∈ {ios} ∧ 年龄 ∈ {25-30} 2
c4 地域 ∈ {北京} ∧ 年龄 ∈ {25-30} ∧ 系统 ∉ {android} 2
c5 系统 ∈ {android} 1
c6 地域 ∉ {上海} 0

倒排索引(按 K 分区;链内按 ID 排序,同一 ID 下 ∉ 排在 ∈ 前面):

K=2 分区:
  (地域,北京)    → c1∈, c2∈, c4∈
  (地域,上海)    → c2∈
  (系统,android) → c1∈, c4∉
  (系统,ios)     → c3∈
  (年龄,25-30)   → c2∈, c3∈, c4∈
K=1 分区:
  (系统,android) → c5∈
K=0 分区:
  (地域,上海)    → c6∉
  Z              → c6∈        (K=0 的合取式都挂在特殊 key Z 上,按 K=1 处理)

请求:地域 = 北京,系统 = android,年龄 = 25-30。

K=2 分区,取出请求对应的 3 条链:

A (地域,北京)    : c1  c2  c4
B (系统,android) : c1  c4∉
C (年龄,25-30)   : c2  c3  c4

每一步把链按当前指向的 ID 排序:前 K = 2 条链指向同一个 ID,说明该合取式有 2 个条件同时满足;否则把第 1 条链直接跳到第 2 条链的 ID。

步骤 各链当前位置(排序后) 判断 动作
1 A:c1, B:c1, C:c2 前两条都是 c1,且是 ∈ c1 满足;A、B 前进
2 A:c2, C:c2, B:c4∉ 前两条都是 c2,且是 ∈ c2 满足;A、C 前进
3 C:c3, B:c4∉, A:c4 前两条 c3 ≠ c4 c3 只有 1 条链指向它,凑不够 2 条;C 直接跳到 ≥ c4
4 B:c4∉, A:c4, C:c4 前两条都是 c4,但第一条是 ∉ c4 被否定(系统是 android);所有指向 c4 的链跳过它
5 链都到末尾 — 结束

c3 在第 3 步被直接跳过:请求里没有"系统=ios",只有年龄一条链指向它,永远凑不够 2 条,不需要检查。链很长时,这种跳跃能整段略过大量不可能成立的合取式。

K=1 分区:只有 (系统,android) → c5∈ 一条链,1 条链指向同一 ID 即成立,c5 满足。

K=0 分区:请求里没有"地域=上海",否定链 (地域,上海) → c6∉ 不参与;Z 链指向 c6∈,c6 满足。

结果:c1、c2、c5、c6 满足;c3 因系统不匹配被跳过,c4 因否定条件被排除。

请求中的多值属性(兴趣标签、人群包)会让同一属性命中同一合取式的多个值,造成重复计数,查询前要先把同一属性的多条链合并成一个去重的迭代器。倒排链的跳跃方式与倒排求交集相同,见 检索系统全景 3.4 节;广告引擎中的应用见同文 11.2 节。

8.3 QPS 与每日上限

DSP 通常有三类限制:瞬时 QPS 上限、每日请求上限、每日曝光上限。它们都是分布式计数问题:几十台 server 实例共同消耗同一个配额。

限制 方案
瞬时 QPS 本地令牌桶,总 QPS 按实例数均分;实例数变化时,配置中心按注册实例数重新下发每实例配额。精度要求不高,不需要全局协调
每日请求上限 配额租约:实例从 Redis 批量领取额度(如每次领 1000 次),本地扣减,用完再领;Redis 用原子 DECRBY 扣减总额度。实例宕机会损失未用完的零头,误差可控
每日曝光上限 曝光发生在客户端,晚于请求几秒到几分钟。Flink 实时统计各 DSP 当日曝光,写回 Redis;server 读取(本地缓存 1–5 秒)。接近上限时开始按比例减少请求,达到上限后停止,避免因统计延迟而超出过多
每日曝光上限的平滑收敛(示意):
  已曝光比例 r = 当日曝光 / 上限
  r < 0.9          :正常询价
  0.9 ≤ r < 1.0    :询价概率 = (1 − r) / 0.1,线性下降
  r ≥ 1.0          :停止询价

跨天重置按 DSP 约定的时区;计数 key 带日期,自然过期。

8.4 流量筛选模型

目标:在每家 DSP 的 QPS 配额内,把最可能出高价的流量发给它;对 DSP 大概率不出价的流量,不浪费询价。

模型:每家 DSP(或每个代码位)一个轻量模型,预测:

  • p_bid:该 DSP 对这次请求出价(且出价有效)的概率
  • E[price | bid]:出价时的期望价格

特征:广告位、场景、样式、App、系统、系统版本、厂商、网络、省市、时段、是否有设备 ID、用户标签的粗粒度桶、该设备近期在该 DSP 的出价和曝光历史。

训练:竞价日志中每条"已询价"记录都有标签(出价与否、出价金额),天然就是训练样本。按天训练,按小时增量更新。模型用 LR 或小型 GBDT,导出为 server 内可直接计算的参数(LR 权重表、树结构),随快照下发,推理在进程内完成,每个请求每个候选 DSP 只需几微秒。

决策(伪代码):

期望价值 v = p_bid × E[price | bid]
对每家 DSP 维护一个动态阈值 θ:
  v ≥ θ     → 询价
  否则      → 以探索概率 ε(如 3%)随机询价,其余不询价

θ 由控制器调节:实际发送 QPS 高于配额 → 提高 θ;低于配额且有富余 → 降低 θ

要点:

  • 探索流量必须保留,否则模型只看到自己选中的流量,会越来越保守(选择偏差)。探索样本在训练时加权纠偏
  • 期望价格按该 DSP 折算后的底价计算:只有不低于其 bidfloor 的出价才有价值,底价越高,同一 DSP 在这类流量上的期望价值越低(12.3)
  • 新接入的 DSP 没有历史数据,先按预定向 + 固定比例放量,积累一周数据后再启用模型
  • 模型故障或特征缺失时退回"预定向 + 配额",不影响主流程
  • 收益评估用 A/B 实验(见 12.7),核心指标是 RPM 和出向请求量

8.5 同一 DSP 多个代码位

一个广告位可能绑定同一 DSP 的多个代码位(不同底价档、不同形态)。询价策略:

  • 同一 DSP 默认只询价一个代码位,选择规则为"最高优先级"或"模型预估价值最高"
  • DSP 明确允许时,可以并发询价多个代码位(相当于在 DSP 内部做多档底价竞争);会成倍增加该 DSP 的 QPS,需要单独配置

9. 询价扇出

9.1 并发模型

伪代码:

每个请求:
  ctx = 带截止时间的上下文(请求 deadline − 尾部预留)
  对询价列表中每个 (DSP, 代码位):
      从该 DSP 的并发信号量取许可(取不到立即记为"本地排队满",不等待)
      启动协程:适配器编码 → 发送 → 等待响应 → 适配器解码 → 写入结果通道
  主协程:收集结果,直到"全部返回"或"ctx 到期"
  ctx 到期后仍未返回的请求被取消,释放许可
  • 每个 DSP 的超时 = min(DSP 配置的超时, 剩余 deadline)
  • 全部 DSP 都已返回(或都已无出价)时提前结束等待,不必等到 deadline
  • 迟到的响应直接丢弃,并计入该 DSP 的超时统计

9.2 连接管理

项 设计
连接池 每个 (DSP, 地域 endpoint) 独立的长连接池,池中是 ADX 实例到该 DSP 的 TCP 连接(HTTP keep-alive)。池大小 ≈ 该 DSP 的单实例 QPS × P99 响应时间 × 1.5(推导见下文)。例:单实例对某 DSP 2000 QPS,P99 150 ms → 约 450 个连接
协议 HTTP/1.1 keep-alive;DSP 支持时用 HTTP/2(单连接多路复用,连接数大幅减少)
预热 启动时和扩容后先建立连接再接流量,避免首批请求承担 TCP + TLS 握手
DNS 本地缓存 DSP 域名解析结果,定期刷新;解析失败沿用旧结果
空闲回收 空闲连接超时略小于 DSP 服务端的 keep-alive 超时,避免使用已被对端关闭的连接

连接数公式的推导

HTTP/1.1 的一条连接同一时刻只能承载一个请求(请求流水线在实践中基本不用),所以需要的连接数等于同时在途的请求数。按 Little 定律:

同时在途请求数 L = 到达速率 λ × 每个请求占用连接的时间 W
  • λ:单实例发往该 DSP 的 QPS
  • W:从发出请求到收完响应的时间。取 P99 而不是平均值,是为了在响应变慢时连接池仍然够用;否则请求要排队等连接,直接变成超时。W 的上界是超时时间(到点即取消),所以最坏情况是 QPS × 超时
  • × 1.5:留给流量突发、实例间流量不均、连接重建期间的余量

池过小:请求排队等连接,超时率上升;池过大:大量空闲连接占用文件描述符、内存和 DSP 侧的连接配额,还会逼近本地端口上限(19.3)。

HTTP/1.1 下超时会导致连接重建:请求超时被取消时,响应可能已经在传输中,这条连接无法继续复用,只能关闭,下一个请求重新做 TCP + TLS 握手。超时率高的 DSP 会出现大量连接重建,进一步推高延迟。HTTP/2 用流(stream)承载请求,取消时只重置这一个流、连接保留;一条连接可以同时承载多个流(上限由服务端的最大并发流数决定,常见为 100 左右),所需连接数约为 在途请求数 ÷ 单连接并发流数,所以对超时较多或 QPS 很高的 DSP 优先推动使用 HTTP/2。

9.3 编码与压缩

项 做法
JSON 编码 用高性能编码库;按 DSP 预编译字段映射,避免反射
字段裁剪 按 DSP 声明需要的字段下发,去掉无关的扩展字段
压缩 gzip 通常能把 OpenRTB JSON 压到原来的几分之一,但每次压缩消耗 CPU。出向带宽成本高于 CPU 时开启;按 DSP 配置
Protobuf DSP 支持时优先使用,体积和编解码开销都更小

9.4 隔离与熔断

机制 做法
并发隔离(舱壁) 每家 DSP 一个并发信号量,上限 = 连接池大小。一家 DSP 变慢只会耗尽自己的许可,不影响其他 DSP
熔断 每家 DSP 按 10 秒滑动窗口统计超时率和错误率(5xx、连接失败、解析失败),超过阈值(如 50%,且窗口内请求数 ≥ 100)打开熔断,30 秒后半开,放行 1% 请求试探
慢启动 熔断恢复后按 10% → 30% → 100% 逐步恢复流量,避免瞬间把刚恢复的 DSP 打挂
适配器异常 适配器 panic 被捕获并记录,该次询价记为失败;同一适配器连续异常时自动熔断该 DSP
不重试 询价失败不重试:没有时间,而且可能造成 DSP 重复计数

9.5 询价结果分类

每一次询价都归入一个明确的结果状态,写入竞价日志,供 DSP 自查和报表统计:

状态 含义
NOT_SENT_* 未发送,附原因:预定向不匹配、配额不足、熔断、模型筛除、流量屏蔽、本地排队满
TIMEOUT 超时
HTTP_ERROR 非 200/204 状态码
PARSE_ERROR 响应无法解析
NO_BID 明确不出价(HTTP 204 或空 seatbid,附 DSP 返回的 nbr 原因)
BID 有出价,进入后续过滤

10. DSP 接入体系

DSP 接入效率由三件事决定:协议尽量标准(大多数 DSP 零开发)、工具自助(DSP 自己完成联调,不依赖人工沟通)、上线自动化(按指标自动放量)。

10.1 接入模式

国内外 DSP 的接入方式差异很大,ADX 需要同时支持:

模式 流程 价格 典型对象
RTB(OpenRTB) ADX 发 BidRequest,DSP 返回出价和素材 实时出价 海外 DSP、国内中小 DSP
API 取广告 ADX 按 DSP 的 API 请求广告,DSP 返回广告;部分返回价格,部分不返回 有价格按价格;无价格按历史 eCPM 预估 国内部分平台、自营直客
服务端竞价(S2S Bidding) ADX SDK 内集成 DSP 的 SDK,由 DSP SDK 生成一次性的买方 token,随请求上传;ADX 把 token 发给 DSP 服务端,DSP 返回价格;胜出后由 DSP SDK 在客户端渲染 实时出价 国内头部广告平台
客户端竞价 各 DSP SDK 在客户端各自请求,结果与 ADX 结果在客户端比价 实时出价 过渡方案,不推荐(延迟高、难以统一过滤和审核)
品牌订单 无需询价,由 ADX 按 Deal 配置直接投放;或询价 DSP 确认是否投放 固定价 PD / PDB

无价格候选的定价:API 模式下 DSP 不回传价格时,用该代码位最近 N 天的结算 eCPM(按时段、系统等维度细分,数据来自结算回填)作为预估价参与排序。这样所有候选都在同一个价格维度上比较,替代传统的瀑布流分层。预估价要定期校准,偏差过大的代码位降低优先级或改为固定比例放量。

10.2 协议分层

层级 适用 做法 接入成本
L1 标准协议 严格遵循 OpenRTB 2.5 / 2.6 配置 endpoint、版本、编码、压缩即可 零开发,约 1 天
L2 映射配置 基本遵循 OpenRTB,扩展字段、枚举、字段位置不同 在平台上编写声明式映射规则,热加载 零发版,2–3 天
L3 代码插件 完全私有协议、特殊签名、特殊加密 实现统一适配器接口,作为插件发布 1–2 周

10.3 适配器框架

统一接口:

方法 输入 输出
BuildRequest 内部请求模型、代码位配置 HTTP 请求(URL、头、体)
ParseResponse HTTP 响应 统一候选列表(价格、素材、交互、监测链接、创意 ID、Deal ID)或错误
BuildNotice 胜出 / 落败 / 计费事件、结算价 需要回调的 URL 列表(替换宏、加密价格后)
DecryptPrice(可选) DSP 回传的加密价格 明文价格
Capabilities — 支持的样式、交互、编码、是否支持多广告返回等

生命周期:

  • 适配器随配置快照发布,带版本号;同一 DSP 可以同时存在新旧两个版本的适配器,按流量比例灰度
  • 适配器在独立的协程和资源配额内执行;执行超过 CPU 时间阈值、发生 panic 或返回非法结果,都记为失败并计入熔断
  • L3 插件以独立的代码仓库和发布流程管理,编译进 adx-server 的插件注册表;上线必须通过契约测试(见 24.2)

10.4 声明式映射(L2)

映射规则描述"内部模型字段 → DSP 请求字段"和"DSP 响应字段 → 统一候选字段"。配置示意(字段名为举例):

request:
  - { to: "imp[].tagid",     from: "code_slot.dsp_slot_id" }
  - { to: "imp[].bidfloor",  from: "imp.floor_cpm_yuan",  transform: "yuan_to_fen" }
  - { to: "device.os",       from: "device.os",           enum: { android: "Android", ios: "iOS" } }
  - { to: "device.ext.oaid", from: "device.oaid",         when: "device.oaid != ''" }
  - { to: "ext.media_type",  const: 1 }
response:
  - { to: "price_cpm_fen",    from: "seatbid[0].bid[0].price" }
  - { to: "creative_id",      from: "seatbid[0].bid[0].crid" }
  - { to: "title",            from: "seatbid[0].bid[0].ext.title" }
  - { to: "click_trackers",   from: "seatbid[0].bid[0].ext.clk_urls" }
  - { to: "interaction_type", from: "seatbid[0].bid[0].ext.action", enum: { "1": "landing", "2": "download", "3": "deeplink" } }
  • 支持:路径映射、常量、枚举映射、单位转换、条件、数组展开、简单模板字符串
  • 不支持任意脚本:映射规则必须可静态校验、执行时间可预测
  • 规则在快照构建时预编译为字段访问函数表,运行时不解析字符串路径
  • 平台提供"映射调试器":输入一条内部请求样例,实时显示映射后的 DSP 请求;输入一条 DSP 响应,显示解析出的候选

10.5 自助接入平台(dsp-portal)

flowchart LR
    A["1 注册与合同<br/>主体资质、结算信息"] --> B["2 技术配置<br/>endpoint、协议、编码、<br/>超时、QPS、定向、密钥"]
    B --> C["3 协议映射<br/>L1 直接通过<br/>L2 编写映射<br/>L3 提交插件"]
    C --> D["4 素材预审<br/>API 批量上传创意"]
    D --> E["5 沙箱联调<br/>测试请求、响应校验器"]
    E --> F["6 影子流量<br/>真实流量复制,不参与竞价"]
    F --> G["7 自动放量<br/>1% → 5% → 20% → 100%"]
    G --> H["8 运行期自助<br/>报表、过滤原因、对账明细"]

文档与样例:按场景和样式提供真实请求样例(已脱敏)、字段字典、枚举值、宏列表、监测链接规范、价格加密说明、错误码。

沙箱联调:

  • ADX 向 DSP 的测试 endpoint 发送带 test=1 的请求,DSP 返回的出价不计费
  • 请求构造器:DSP 在平台上选择场景、样式、系统、地域,生成请求并实时发送,查看原始请求和响应
  • 响应校验器:DSP 粘贴或触发一个响应,平台按与线上完全一致的校验链逐项给出结论,例如:
    • 价格低于代码位底价
    • crid 未审核或已被屏蔽
    • 样式 id 不在广告位支持列表中
    • 素材尺寸或视频时长不符合样式规格
    • 标题含敏感词
    • 缺少曝光监测链接
    • 落地页不是 https
    • adm 不是合法的 VAST 或原生 JSON

影子流量:按设定比例复制真实请求发给 DSP,响应只做校验和统计,不参与竞价、不下发、不计费。它用真实流量验证 DSP 的延迟、出价率和出价合法率,是上线前最有价值的一步。

自动验收与放量:每个档位观察至少 24 小时,自动检查:

指标 门槛(示例)
超时率 < 5%
P99 响应时间 < DSP 配置超时的 80%
出价合法率(通过过滤链的出价 / 全部出价) > 98%
HTTP 错误率 < 0.5%
曝光差异率(DSP 统计 vs ADX 统计,放量后日级) < 10%

达标自动进入下一档位;不达标停在当前档位,平台生成诊断报告并通知 DSP 和对接人。

运行期自助:

  • DSP 在平台上实时查看:发送 QPS、出价率、胜出率、超时率、平均出价与胜出价、各过滤原因占比、熔断事件
  • 竞价日志明细按 DSP 维度脱敏后提供下载(按天、按抽样),供 DSP 自行排查和对账
  • 报表 API 供 DSP 系统拉取

10.6 素材预审接口

DSP 通过 API 批量提交创意:DSP 侧创意 ID、素材 URL(图片、视频)、标题、描述、落地页、下载包名、广告主、行业、资质文件。审核结果异步回调 DSP,或由 DSP 轮询查询。线上出价只能使用已通过审核的创意;对于动态创意(每次出价素材都可能不同的 DSP),采用"素材 MD5 实时查审核缓存 + 未审核素材进入快速审核队列、本次不下发"的策略(见第 16 章)。

10.7 通知与回调约定

通知 触发时机 用途
胜出通知(nurl) ADX 决出胜者时,服务端发起 DSP 确认赢得竞价;不作为计费依据
计费通知(burl) 客户端曝光判定成功后,经 adx-tracker 发起 计费依据
落败通知(lurl) 竞价结束后异步批量发起 带落败原因码与(可选)胜出价区间,帮助 DSP 调整出价
DSP 自带监测 曝光、点击、转化时,由 SDK 或 tracker 按 DSP 提供的链接上报 DSP 自己的统计和反作弊

服务端发起的通知都经过一个异步通知队列:失败按退避重试(最多若干次),重试记录可查;不阻塞竞价。


11. 竞价与排序

11.1 候选归一化

不同接入模式返回的结果差异很大,先归一为统一的候选结构:

字段组 字段
来源 来源类型(RTB / API / S2S / 品牌订单)、dsp_id、代码位、对应的 imp
价格 原始价格、价格类型(实时出价 / 固定价 / 历史预估)、计价方式(CPM / CPC)、币种、Deal ID
创意 DSP 创意 ID、样式 id、标题、描述、图片列表、视频(地址、时长、尺寸)、图标、品牌名、素材 MD5 列表
交互 交互类型、落地页、deeplink、下载信息(包名、应用名、开发者、版本、隐私政策链接、权限列表链接、功能介绍)、小程序信息
监测 曝光、点击、视频进度、下载与安装、唤起等各事件的 DSP 监测链接;nurl、burl、lurl
属性 广告主、行业、落地页域名

所有金额在系统内部统一用整数表示(如"分 / 千次"或"微元"),不用浮点数,避免舍入误差在结算时累积。

11.2 广告过滤链

过滤链按成本从低到高排列,便宜的判断先做:

顺序 过滤器 判定 数据来源
1 无效广告 价格 ≤ 0、必传字段缺失(标题、素材、落地页或下载信息)、URL 非法 —
2 底价 有效价格低于该 imp 的有效底价 Floor 阶段结果
3 样式匹配 样式 id 不在广告位支持列表中;素材尺寸、比例、视频时长不符合样式规格 快照:样式规格
4 素材屏蔽 任一素材 MD5 命中屏蔽列表;创意 ID 被屏蔽 快照:屏蔽集合(哈希集合)
5 审核状态 创意未通过审核,或审核结论不覆盖当前媒体 快照:审核结果集合
6 行业 / 广告主 / 域名 命中广告位或媒体的屏蔽行业、屏蔽广告主、屏蔽落地页域名 快照
7 下载合规 下载类广告缺少应用名、开发者、版本、隐私政策、权限列表、功能介绍等要素;广告位不允许下载类 —
8 敏感词 标题、描述、应用名、品牌名命中敏感词 快照:AC 自动机
9 流量屏蔽规则 命中"屏蔽某类广告"的规则 7.2 的规则结果
10 重复广告 同一设备在短时间窗口(如 5 分钟)内已下发过相同广告;同一次请求返回多条时相互重复 本地 LRU / Redis

实现要点:

  • 每个过滤器是一个独立组件,按配置的顺序和作用范围(全局 / 媒体 / App / 广告位 / DSP)组合,新增过滤器不改流程
  • 每个被过滤的候选都记录第一个命中的过滤器和规则 id,写入竞价日志并汇总到报表,DSP 可以在平台上看到自己的出价被过滤的原因分布
  • 敏感词用 AC 自动机,一次扫描匹配全部词。匹配前做归一化(全角转半角、大小写、繁简、去除插入的空格和符号),降低简单规避手段的效果
  • 重复广告判定:广告指纹 = MD5(DSP + 创意 ID + 主素材 MD5 + 标题)。以 (设备 ID, 广告指纹) 为 key,下发时写入、TTL 5 分钟。负载均衡按设备 ID 一致性哈希时用本地 LRU,否则用 Redis(与补全阶段的批量查询合并为一次访问)

11.3 排序

有效价格把所有候选统一到"ADX 实际可得收入"上:

CPM 出价:   有效价格 = 出价 × 现金比 × 对账系数
CPC 出价:   有效价格 = CPC 出价 × pCTR × 1000 × 现金比 × 对账系数
预估价候选: 有效价格 = 历史结算 eCPM(已是结算口径,不再乘对账系数)
  • 现金比:按 DSP 在管理平台配置(例如某 DSP 的收入中有一部分以非现金方式结算,则现金比 < 1)
  • 对账系数:按 DSP 代码位滚动计算 = 最近 N 天结算收入 / 同期 ADX 预估收入,截断在 [0.5, 1.2] 之类的合理区间内,防止单日异常数据造成剧烈波动(见 18.5)
  • pCTR:CPC 出价需要 ADX 自己预估点击率,模型按广告位、样式、DSP、行业等特征训练

排序键:(优先级层, 有效价格, 随机数),降序。

优先级层 内容
1 PDB 保量订单中、当前应当投放的订单(由保量控制器决定,见 12.6)
2 PD 优先交易
3 其他所有候选(公开竞价、PA、API、预估价候选),只按有效价格排序

除品牌订单外不设人为分层,所有候选都在同一价格维度上竞争。有效价格相同时随机打散,避免总是同一家 DSP 胜出。

11.4 定价

规则 结算价 适用
一价 出价本身 默认,RTB 与服务端竞价
二价 max(第二名有效价格折算回该 DSP 的出价口径, 底价) + 最小加价单位 与 DSP 合同约定为二价时
固定价 Deal 价格 PD / PDB
预估价 不向 DSP 回传价格,按 DSP 结算 API 模式无价格候选

"折算回该 DSP 的出价口径":第二名有效价格要除以胜出 DSP 自己的现金比和对账系数,才能与胜出 DSP 的出价比较。

每个胜出广告记录三个价格:DSP 结算价(回传给 DSP、DSP 据此计费)、ADX 预估收入 = 结算价 × 现金比 × 对账系数、媒体预估分成 = ADX 预估收入 × 媒体分成比例(联盟媒体)。

一价拍卖与 bid shading

二价拍卖中胜出者付第二高价,DSP 按真实价值出价就是最优策略。一价拍卖中胜出者付自己的出价,按真实价值 v 出价,赢了也没有任何剩余(v − v = 0),所以 DSP 会把出价压到真实价值以下,这就是 bid shading(出价压低):

最优出价 b* = argmax_b  (v − b) × P(赢 | b)

出价越低,赢了之后的剩余越大,但赢的概率越小。DSP 需要从历史竞价结果估计赢率曲线 P(赢 | b):赢了只知道胜出门槛不高于自己的出价,输了只知道门槛高于自己的出价(或从落败通知中得到最低胜出价区间),这是带删失的数据,常用生存分析类方法建模。

一价会不会导致出价不断下降:理论上不会无限下降。每家 DSP 压价的同时也在和其他 DSP 竞争,压得太低就会输给别人,最终停在低于真实价值、但不会趋近于零的均衡点;在对称、独立私有价值的理想条件下,一价与二价的期望收入相同(收入等价定理)。实践中出价持续下行的风险来自:

风险 说明
竞争稀疏 某类流量上只有一两家 DSP 出价时,压价几乎没有代价,出价会持续下探
学习过程的正反馈 DSP 试探性降价后发现仍然能赢,就继续降,直到输掉;竞争稀疏时这个过程会走得很远
反馈信息过细 ADX 回传精确的胜出价,DSP 能精确地压到"刚好赢"的位置,卖方剩余被充分挤压

ADX 的应对:

  • 提高每次竞价的竞争密度:流量筛选要保证高价值流量上有足够多的 DSP 参与(8.4),接入更多需求方,引入 PMP / Deal
  • 底价:一价下底价的主要作用正是限制压价空间。监控各流量分段的出价分布,出价明显下移的分段提高底价(12.2、12.4)
  • 控制反馈粒度:落败通知只回传胜出价区间而不是精确值,或按合同约定的粒度回传(11.7)
  • 监控:按 DSP、按流量分段跟踪平均出价、胜出价与第二高价的差距、出价率的长期趋势,异常下行时告警并排查
  • 固定价交易:高价值资源用 PD / PDB 固定价售卖,不参与竞价压价

11.5 多广告返回

信息流等场景一次请求返回 N 条:

  • 按排序键依次取候选,跳过与已选广告重复(同一广告指纹、同一广告主超过上限、同一 DSP 超过上限)的候选,直到取满 N 条或候选耗尽
  • 每条广告独立定价、独立生成监测链接和竞价记录(带 ad_index)
  • 同一 DSP 在一次响应中返回多条广告时,每条都作为独立候选参与排序

11.6 品牌订单(PD / PDB)

类型 流程
ADX 直投品牌订单 素材由 ADX 管理,订单命中定向且保量控制器决定投放时直接入选,不询价
程序化品牌订单(DSP 执行) ADX 在 BidRequest 的 imp.pmp.deals 中带上 Deal ID 和固定价;DSP 返回带该 Deal ID 的出价即优先胜出。DSP 不出价时,记录为"Deal 放弃"并回落到公开竞价,按合同统计 DSP 的放弃率

11.7 竞价结果的记录与反馈

  • 竞价日志记录每个候选的最终状态:胜出(附结算价)、被过滤(附原因)、竞价落败(附胜出价区间)
  • 落败通知(lurl)按 DSP 需要异步批量发送,原因码采用 OpenRTB 的 loss reason(如 100 系列为出价低于胜出价、200 系列为创意被过滤)
  • 胜出价区间可以按合同决定是否反馈:反馈越细,DSP 出价越精准,整体市场效率越高;但也可能让 DSP 更容易压低出价

12. 底价与收益优化

12.1 底价的含义与业务用途

底价(floor price / reserve price)是这次展示机会 ADX 愿意卖的最低价格:有效价格低于底价的出价直接淘汰;所有出价都低于底价时,本次请求不填充或使用兜底广告。底价只拦截低价,不保证一定有人按这个价格买,它不是保底收入。

底价不只在广告位上设置:它按层级覆盖、按 DSP 折算,并在 DSP 选择、请求生成、广告过滤、定价多个环节生效(12.2、12.3)。

业务用途

场景 做法 目的
区分资源价值 开屏 30 元、激励视频 20 元、信息流 8 元(CPM,示意) 优质资源不低价出售
保护直客价格体系 程序化售卖的底价不低于直客(品牌合约)价格 防止广告主绕开直客渠道,在程序化市场低价购买同一批流量
保护体验与品牌形象 核心场景设较高底价 挡住大量低价、低质量广告
季节与时段调整 春节、大促等高峰期上调,深夜下调 跟随市场价格变化
合同约定 PD 订单固定价;与某 DSP 约定的最低价 执行商务条款
联盟媒体 按媒体的收入要求设置 保证媒体分成
收益优化 动态底价 + A/B 实验 在填充率和单价之间找收入最大点

取舍

  • 底价过高:填充率下降,大量请求无人购买,总收入反而下降
  • 底价过低:单价被拉低,低质量广告增多;二价合同下直接降低成交价
  • 不填充时的处理:有的媒体宁愿开屏空着也不接受低价广告;有的用兜底广告(自家推广)填充。按广告位配置

12.2 分层配置与按 DSP 折算

底价按层级覆盖,越具体的层级优先级越高:

全局默认 < 媒体 < App < 场景 < 广告位 < 广告位 × DSP 代码位 < Deal

同一广告位还可以按时段、地域、系统、用户价值分层设置不同底价(12.4)。

按 DSP 折算:ADX 的有效底价 F 是"ADX 实际拿到的钱"(与 11.3 的有效价格同一口径)。各 DSP 的现金比、对账系数不同,同样的出价 ADX 实际所得不同,所以下发给每家 DSP 的 bidfloor 要分别折算:

DSP_i 的 bidfloor = max( F ÷ (现金比_i × 对账系数_i) ,  DSP_i 代码位的最低价要求 )

例:广告位有效底价 F = 10 元 CPM。

DSP 现金比 × 对账系数 折算值 代码位最低价要求 下发 bidfloor
A 1.0 10 无 10 元
B 0.8 12.5 无 12.5 元
C 1.0 10 15 元 15 元

DSP-B 出价 12 元看似高于 10 元,但 ADX 实得 12 × 0.8 = 9.6 元,低于底价,所以要给它下发 12.5 元,否则它会给出"看似合格、实际不达标"的价格。不同 DSP 看到不同的 bidfloor 是按结算口径统一后的正常结果。

配置注意

  • 与 DSP 侧代码位配置一致:DSP 后台也会给代码位设底价,两边不一致时 DSP 可能自行过滤流量,表现为出价率异常低。接入验收和日常巡检时核对
  • 单位统一:CPM 或 CPC、元或分、币种;系统内部统一用整数的"分 / 千次"或"微元"
  • 多 imp 请求:每个 imp 的底价分别计算

12.3 底价在竞价链路中的作用

Pipeline 中 Floor 阶段在 DspSelect 之前执行(5.3),算出每个 imp 的有效底价 F,之后的四个环节都使用它:

Floor 阶段:算出有效底价 F(ADX 实得收入口径)
    │
    ├─① DSP 选择:用 F 判断某 DSP / 代码位值不值得询价
    ├─② 生成请求:把 F 折算为各 DSP 的 bidfloor(12.2)
    │
询价返回
    ├─③ 广告过滤:有效价格 < F 的出价淘汰(11.2 过滤链第 2 步)
    └─④ 定价:二价合同下成交价不低于底价(11.4)

① DSP 选择

情况 处理
某代码位的折算底价高于这类流量上它的出价水平 流量筛选模型给出很低的期望价值,不发送
API 模式、不回传价格的 DSP 以历史结算 eCPM 为预估价;预估价明显低于 F 的代码位不询价,因为询价后也会在过滤阶段被淘汰
流量筛选模型 期望价值 = P(出价 ≥ 该 DSP 的 bidfloor) × 期望价格(8.4);底价越高,同一 DSP 在这类流量上的期望价值越低

② 生成请求:按 12.2 的公式为每家 DSP 写入 imp.bidfloor 和 bidfloorcur。

③ 广告过滤:用有效价格(出价 × 现金比 × 对账系数)与 F 比较。下发时已折算,这一步主要拦截没有遵守 bidfloor 的 DSP、按历史 eCPM 预估的候选,以及对账系数变化后不再达标的出价。

④ 定价:一价拍卖中成交价就是出价,底价只起拦截作用;二价合同中成交价 = max(第二高价, 底价) + 最小加价单位,底价直接影响收入。

Deal 的底价:PMP / PA 类 Deal 在 imp.pmp.deals[].bidfloor 中带自己的底价,只对该 Deal 的买方生效,通常高于公开竞价底价;PD 是固定价,不再与底价比较。

一价拍卖下底价对 DSP 行为的影响:DSP 做 bid shading 时会参考底价。调高底价可能让 DSP 整体抬价,也可能让它减少出价,所以底价调整的效果只能通过实验确认(12.4、12.7)。

12.4 动态底价

动态底价按广告位 × 流量分段(系统、地域、时段、用户价值分层)设定。

方法:

  1. 离线统计各分段中最高有效出价的分布
  2. 以候选底价 f 模拟:期望收入(f) = E[最高有效出价 × 1(最高有效出价 ≥ f)],同时考虑底价过高导致的填充率下降和用户体验成本
  3. DSP 会对底价做出反应,模拟结果只能作为初值,最终以 A/B 实验结果为准
  4. 按天更新,变动幅度设上限(如单日 ±10%),防止震荡

对二价合同的 DSP,底价会直接抬高成交价,收益影响更明显,需要单独实验。

12.5 底价的来源:运营配置与模型的分工

底价既不是纯运营配置,也不是纯模型输出,而是三类来源分层组合:

来源 内容 负责方 更新频率
静态配置 各层级的基础底价(12.2)、Deal 价格、合同最低价 运营、商务 按需,走审批
规则策略 按时段、节假日、地域的调价系数(如春节期间开屏 × 1.3);特殊时期的临时调整 运营 按活动或季节
模型 按流量分段计算的动态底价(12.4) 算法 按天

组合方式:运营给出每个广告位(或分段)的下限和上限,模型在区间内给出建议值,最终取值被夹在区间内;合同约束和 Deal 价格优先于模型结果:

有效底价 = Deal 或合同约束存在时取其约束值;
           否则 clamp( 模型建议值 × 规则系数 , 运营下限 , 运营上限 )
模型未启用或不可用时:模型建议值 = 静态配置值
  • 运营下限来自业务约束:直客价格保护、品牌保护、媒体收入要求。模型不能把底价压到这条线以下
  • 运营上限防止模型给出过高底价导致填充率骤降
  • 演进路径:上线初期只用静态配置和规则策略;积累足够的竞价日志后,模型先在小流量实验组启用,按实验结果逐步扩大到全量
  • 控制权:运营可以冻结某个广告位的模型结果,退回静态配置;模型产出随配置快照的模型段下发(15.2),可以灰度和回滚;底价下限、上限的调整属于高风险变更,走双人审批(15.1)
  • 可追溯:竞价日志记录每个 imp 的底价来源(静态配置、规则、模型、Deal)和各层取值,报表可以按来源分析底价对填充率和收入的影响

12.6 PDB 保量控制

PDB 订单承诺在指定时间内、在指定定向上交付约定曝光量。ADX 需要做到不欠量、不超量、匀速。

库存预测:按广告位 × 小时 × 定向维度,用历史同期流量预测未来可用库存;多个订单共享库存时,先做分配(按订单优先级、下单时间、定向的稀缺程度)。

实时控制(伪代码):

每个订单每分钟更新一次:
  剩余目标      = 订单总量 − 已曝光(Flink 实时统计)
  剩余可用库存  = 预测的剩余时间内、满足定向的库存
  基础投放概率  p0 = 剩余目标 / 剩余可用库存
  实际投放概率  p = clamp(p0 × 修正系数, 0, 1)
  修正系数由 PID 控制器根据"实际进度 − 理想进度"调节
在线:请求命中订单定向时,以概率 p 进入优先级层 1
  • 接近结束仍欠量时逐步提高概率,必要时使用更多库存(放宽频控)
  • 超量控制:已曝光达到目标的 100% 立即停止;由于统计有延迟,目标按 99% 左右收敛
  • 频控:品牌订单常要求单用户每天曝光不超过 N 次,用设备 ID 计数(Redis,按天过期)

12.7 实验平台

所有收益策略(流量筛选、底价、排序参数、样式)都通过实验评估:

设计 说明
分桶 按 hash(设备 ID, 实验层盐值) mod 1000 分桶;无设备 ID 时按请求随机分桶
分层正交 不同层(流量筛选层、底价层、排序层、样式层)用不同盐值,层与层之间正交,同一层内实验互斥
配置 实验配置属于配置快照的一部分,与其他配置一起灰度和回滚
日志 竞价日志和事件日志都带实验分桶 id,报表可按实验组聚合
指标 核心:RPM、填充率、eCPM;成本:出向请求量、带宽;生态:DSP 出价率、胜出率;体验:开屏跳过率、关闭率
护栏 任一核心指标恶化超过阈值自动停止实验
AA 测试 定期做 AA 测试,校验分桶均匀和指标波动范围
组间干扰 DSP 的预算和出价策略跨实验组共享:实验组和对照组在争夺同一份 DSP 预算,底价、流量筛选类实验的结果会有偏差(例如实验组抬高底价后,DSP 把预算花到对照组,对照组收入虚高)。对策:按时间片轮换实验、按 DSP 分组实验,或保留长期对照观察整体收入

13. 响应组装与广告交互

13.1 样式映射

DSP 返回的素材要映射到 ADX 广告位支持的样式:

情况 处理
DSP 返回样式 id 直接校验该样式是否在支持列表中
DSP 只返回素材元素 按元素组合(有无视频、图片数量与比例、有无图标)匹配最合适的样式模板
素材与样式有小偏差 按样式规则处理:标题超长截断、图片比例在容差内居中裁剪;超出容差则过滤

服务端响应中只下发样式 id + 元素数据,渲染模板由 SDK 内置或按版本动态下发。样式的交互功能(倒计时、跳过按钮、摇一摇、滑动跳转、下载确认弹窗等)按"样式功能配置"表在 App、场景、版本、渠道维度上开关,随响应下发。

13.2 交互类型

类型 行为 需要上报的事件
落地页 应用内 WebView 或外部浏览器打开 点击、落地页加载成功 / 失败
下载(Android) 应用内下载 APK 或跳转应用商店;下载前展示应用名、开发者、版本、隐私政策、权限列表、功能介绍,并二次确认 点击、下载开始、下载完成、下载失败、安装开始、安装完成
下载(iOS) 跳转 App Store 点击、跳转成功
deeplink 已安装则唤起目标 App 的指定页面;未安装则回退到落地页或下载 唤起尝试、唤起成功、唤起失败、未安装回退
小程序 唤起微信小程序 唤起成功 / 失败
快应用 唤起快应用 唤起成功 / 失败

交互类型和回退链由候选中的交互信息决定,SDK 按顺序执行:deeplink → (失败) 下载或落地页。

13.3 宏替换

宏分两类,分别在服务端和客户端替换:

类别 例子 替换时机
服务端宏 结算价(加密)、请求 ID、imp ID、出价 ID、Deal ID、时间戳 响应组装时
客户端宏 曝光时间、点击时间、点击按下与抬起坐标、广告位实际宽高、屏幕宽高、视频播放进度、视频时长、播放行为(自动 / 手动) SDK 触发上报时

不同 DSP 对同一含义的宏命名不同(如点击坐标有多种写法)。每个 DSP 的适配器声明自己的宏字典(宏名 → 标准含义)。服务端在响应中把 DSP 的宏统一改写为 ADX 的标准宏名(或附带宏字典),SDK 只需实现一套标准宏替换逻辑。

13.4 监测链接

每个事件的上报目标:

  • ADX 自己的追踪链接:https://track.xxx/t/{事件}?p=<加密载荷>&s=<签名>,载荷含 request_id、ad_index、dsp、代码位、结算价(加密)、下发时间、实验分桶;带签名和过期时间(见 14.3)
  • DSP 的监测链接:多条,按 DSP 要求替换宏后并行请求

SDK 对所有监测链接并行发出,不使用 302 跳转链(跳转会把点击到落地页的时间拉长,且任一跳失败都会丢数据)。每条链接独立重试,失败的请求进入持久化队列(见 14.7)。

13.5 开屏预加载

开屏在冷启动时展示,客户端最多等待几百毫秒,素材来不及实时下载。采用两段式:

sequenceDiagram
    participant SDK
    participant ADX
    participant DSP
    Note over SDK: 阶段一:预加载(后台、非冷启动时)
    SDK->>ADX: 预加载请求(开屏广告位)
    ADX->>DSP: 询价(标记为预加载)
    DSP-->>ADX: 候选广告 + 素材 + 有效期
    ADX-->>SDK: 候选列表(素材地址、有效期、预估价)
    SDK->>SDK: 下载素材到本地缓存
    Note over SDK: 阶段二:冷启动展示
    SDK->>ADX: 实时请求(附本地已缓存的素材列表)
    ADX->>DSP: 实时询价(支持实时竞价的 DSP,附已缓存素材 ID)
    DSP-->>ADX: 在已缓存素材中选择并出价
    ADX-->>SDK: 胜出广告(引用本地已缓存素材)
    Note over SDK: 实时请求超时则从本地缓存中选有效期内、预估价最高的广告展示

要点:

  • 预加载的广告只下载素材、不计费;实际展示时才触发曝光和计费
  • 缓存有有效期和容量上限,过期素材清理
  • 实时请求的超时要短(例如 300–500 ms 内),超时就用本地缓存兜底
  • 预加载不要求 DSP 必须支持实时阶段:不支持的 DSP 只参与预加载,展示时用预加载时的价格参与本地比价

13.6 响应瘦身

  • 只下发 SDK 渲染和上报需要的字段
  • 重复出现的长 URL 前缀(如同一 DSP 的多条监测链接)可以在协议中用模板 + 参数表示
  • 响应超过阈值(如 16 KB)时 gzip

14. 上报与追踪

14.1 事件体系

阶段 事件 产生方
请求 请求、下发(含填充数) 服务端(竞价日志)
渲染 渲染开始、渲染成功、渲染失败(原因) SDK
曝光 曝光(有效曝光:展示面积 ≥ 50% 且持续 ≥ 1 秒;视频为开始播放;开屏为展示成功) SDK
互动 点击、关闭、跳过、视频播放进度(25% / 50% / 75% / 100%) SDK
转化 下载开始、下载完成、安装开始、安装完成、deeplink 唤起成功 / 失败、小程序唤起 SDK
通知 nurl、burl、lurl 的发送结果 服务端

14.2 计费链路与监控链路分离

维度 计费链路(adx-tracker) 监控链路(adx-monitor)
事件 曝光、点击、计费通知 渲染、视频进度、下载、安装、唤起、各类失败
可靠性 不丢不重:SDK 持久化重试,服务端幂等,Kafka acks=all 且 min.insync.replicas=2 尽力而为:允许采样、允许降级
延迟 实时 可批量上报(攒批或定时)
容量规划 按峰值 × 2 冗余 按均值,过载时采样
故障影响 直接影响收入和对账 影响分析和排查

两条链路分开部署、分开扩容:监控链路的流量波动或故障不会影响计费。

14.3 追踪链接设计

https://track.example.com/t/imp?p=<载荷>&s=<签名>

载荷(序列化后加密,示意):
  request_id, ad_index, dsp_id, code_slot_id, slot_id, 结算价, 预估收入,
  下发时间, 实验分桶, 配置版本
签名 = HMAC-SHA256(追踪密钥, p)
  • 载荷加密:价格等敏感信息不以明文出现在 URL 中
  • 签名:防止篡改载荷(例如改价格、改 DSP)
  • 过期:下发时间超过阈值(如 24 小时)的事件判为无效
  • 追踪密钥按周期轮换,服务端同时接受当前和上一个密钥

14.4 去重

去重 key 为 (request_id, ad_index, 事件类型)。追踪链接 24 小时过期(见 14.3),所以去重窗口只需 24 小时。

层级 实现 目的
tracker 接收 不做全局去重,只校验签名和过期后写 Kafka;同一实例内用小容量本地缓存拦截秒级的客户端重复提交 保持接收链路简单、低延迟、不依赖外部存储
Flink 去重作业 按去重 key 分区,RocksDB 状态后端,状态 TTL 24 小时;输出"去重后的计费事件"主题 计费与 burl 回调的唯一数据源
离线结算 按 key 再去重一次后汇总 最终结算口径的兜底校验

不用 Redis 做 24 小时去重的原因:峰值每秒十万级的计费事件,24 小时会累积百亿级 key,内存成本过高;Flink 的 RocksDB 状态放在本地磁盘,按 key 分区水平扩展,并随 checkpoint 持久化。代价是 burl 回调有秒级延迟,DSP 可以接受。

14.5 防伪造与异常事件

规则 判定
签名错误或过期 直接丢弃,计入异常统计
没有对应的下发记录 事件引用的 request_id 在竞价日志中不存在(离线校验)
顺序异常 点击早于曝光;曝光早于下发
时间异常 曝光到点击间隔过短(机器行为);下载完成到安装完成间隔异常
环境不一致 事件的设备 ID、IP 与下发时差异过大
频率异常 同一设备、同一 IP 的事件频率超出阈值

异常事件照常记录,但打标为无效,不计费、不回调 DSP,并进入反作弊分析(见第 17 章)。

14.6 DSP 回调

  • burl 由服务端发送:Flink 去重作业输出有效曝光后,通知服务替换加密价格并发送 burl。服务端发送比客户端发送更可靠,也不在客户端暴露价格
  • DSP 的普通监测链接(曝光、点击等)由 SDK 在客户端发送,因为 DSP 需要客户端的真实 IP 和 UA 做反作弊
  • 通知服务按 DSP 限速、失败按退避重试;发送结果记录可查,用于对账时排查"DSP 没收到计费通知"的问题

14.7 SDK 可靠上报

  • 事件先写入本地持久化队列(数据库或文件),再发送;发送成功才删除
  • 失败按退避重试,网络恢复或 App 重新启动时继续发送
  • 每个事件带客户端生成的事件 ID,服务端可用于去重
  • 队列设容量上限和事件有效期,超期丢弃(服务端也会按过期判为无效)

15. 配置管理与分发

15.1 管理平台

模块 功能
媒体管理 媒体、App、场景、广告位的创建与维护;密钥管理
DSP 管理 DSP 基本信息、接入参数、代码位、广告位绑定关系、开关、配额、现金比、放量档位
样式与功能 样式规格、样式在各维度上的交互功能开关
规则管理 过滤规则、流量屏蔽规则、敏感词、屏蔽素材
底价与 Deal 各层级底价、动态底价开关、Deal 与品牌订单
实验 实验层、实验组、分桶、护栏
审核 创意审核工作台(见第 16 章)
报表与结算 报表查询、结算数据导入、对账、分成

权限:RBAC,按角色(运营、商务、审核、研发、财务)和数据范围(媒体、DSP)授权。

变更流程:

风险级别 例子 流程
高 全局过滤规则、DSP 全量开关、现金比、分成比例、底价大幅调整 申请 → 双人审批 → 定时或立即生效 → 灰度发布
中 单个广告位的 DSP 绑定、样式功能 申请 → 单人审批 → 生效
紧急 屏蔽违规素材、特殊事件屏蔽 直接生效(紧急通道),事后补审批

每次变更都记录 diff、申请人、审批人、生效时间和所属配置版本,可以按对象查看完整历史,并一键回滚到任意历史值。

15.2 快照构建

flowchart LR
    DB[("MySQL")] -->|"一致性读<br/>单个事务内读取全部表"| V["校验<br/>引用完整性、规则冲突、<br/>取值范围检查"]
    V --> C["编译<br/>预定向位图、规则索引、<br/>AC 自动机、哈希集合、<br/>映射函数表、模型参数"]
    C --> S["序列化<br/>分段 + 校验和"]
    S --> O[("对象存储")]
    S --> E[("etcd<br/>版本号 + 各段地址")]
  • 一致性读:在一个可重复读事务里读取全部配置表,保证快照内部一致(不会出现广告位已删除但绑定关系还在的情况)
  • 校验:引用的样式、DSP、代码位必须存在;规则之间不能有矛盾;底价、现金比等数值在合理范围内。校验失败不发布,通知变更人
  • 分段:快照拆成若干段,各段独立版本:
段 更新频率 大小量级
核心配置(媒体、广告位、DSP、绑定、样式、底价、Deal) 分钟级 MB 级
规则(过滤规则、流量屏蔽、敏感词) 分钟级 MB 级
审核与屏蔽(审核结果、屏蔽素材 MD5) 秒到分钟级 数十 MB
模型(流量筛选、动态底价、pCTR) 小时到天级 数十 MB
IP 库、机型库 周级 数十 MB

server 只加载版本变化了的段。

15.3 加载与双缓冲

  • server watch etcd 中的版本号;发现新版本后在后台下载对应段、校验校验和、构建内存结构
  • 构建完成后原子替换全局快照指针;新请求使用新快照,正在处理的请求继续持有旧快照的引用,处理完后旧快照自然释放
  • 构建失败(校验和错误、反序列化失败、内存不足)时保持旧快照,并上报告警
  • 每个请求在开始时获取一次快照引用,整个请求内只使用这一个版本;竞价日志记录快照版本号,便于回放和排查

15.4 灰度与回滚

步骤 做法
灰度 新版本先推送到带"金丝雀"标签的实例(如 5%);观察 5–10 分钟
自动检查 金丝雀实例的错误率、填充率、RPM、各 DSP 询价量与全量实例对比,偏差超过阈值自动停止发布并回滚
全量 检查通过后全量推送
回滚 把 etcd 中的版本指针改回上一个版本,所有实例秒级切回
本地兜底 实例在本地磁盘保留最近若干个快照;配置中心或对象存储不可用时,用本地最新的可用快照启动

15.5 紧急通道

紧急屏蔽(下架违规素材、特殊事件屏蔽)要求 5 秒内全量生效。紧急规则作为一个很小的独立段,内容直接写在 etcd 的值里(不经过对象存储),server watch 到后立即生效,并在下一次常规快照构建时合并进规则段。


16. 创意审核

16.1 审核流程

flowchart LR
    IN["素材入库<br/>DSP 预审 API<br/>或实时出价中的新素材"] --> DL["下载素材<br/>计算 MD5 与感知哈希"]
    DL --> DUP["去重<br/>已审素材直接复用结论"]
    DUP --> M["机审<br/>图像与视频内容识别、OCR、<br/>敏感词、落地页抓取与安全检测、<br/>下载应用信息核验、行业识别"]
    M --> R{"风险分级"}
    R -->|低风险| PASS["自动通过"]
    R -->|中风险| H["人审队列"]
    R -->|高风险| REJ["自动拒绝"]
    H --> PASS
    H --> REJ
    PASS --> DB[("审核结果库")]
    REJ --> DB
    DB --> SNAP["进入快照<br/>审核与屏蔽段"]
  • 感知哈希(如 pHash)识别"换了几个像素、重新压缩"的近似重复素材,复用已有审核结论,也能识别被拒素材的变体
  • 审核范围:审核结论可以按媒体区分,自有核心 App 的审核标准可以比联盟媒体更严格
  • 人审 SLA:普通素材按小时级完成,加急素材按分钟级
  • 行业资质:医疗、金融、教育、游戏等特殊行业需要资质文件,与广告主绑定

16.2 动态创意

部分 DSP 每次出价的素材组合不同,无法全部预审。处理策略:

  • 创意 ID 已审核,但素材 MD5 未见过:本次不下发;该素材进入快速审核队列,审核通过后后续出价可以使用
  • 对长期合作、历史违规率低的 DSP,可以配置"先发后审":低风险类型(纯图片、非敏感行业)的新素材允许下发,同时进入审核,发现违规立即屏蔽并追责

16.3 复审与下架

  • 落地页会变化:已通过的广告按周期重新抓取落地页并检测
  • 用户投诉、监管通知:通过紧急通道一键屏蔽素材 MD5、创意 ID、广告主或落地页域名,5 秒内全量生效
  • 屏蔽记录保留原因、操作人和证据,DSP 可以在平台上申诉

17. 反作弊

17.1 请求级(实时,前置过滤)

手段 说明
IP 信誉 数据中心 IP、代理和 VPN 出口、已知作弊 IP 段
设备信誉 历史作弊设备、同一设备 ID 在大量不同设备环境中出现
环境检测 SDK 采集模拟器、root / 越狱、调试器、Hook 框架等信号
请求完整性 SDK 签名校验、防重放(见 6.1),识别伪造请求
频率 单设备、单 IP 的请求频率超过正常范围
设备指纹一致性 设备 ID、机型、系统版本、屏幕等字段组合不合理

命中的请求直接过滤,或打标"疑似"后仍参与竞价但不发给要求干净流量的 DSP。

17.2 事件级(实时 + 离线)

手段 说明
事件合法性 见 14.5
点击特征 点击坐标总在同一点或为 (0,0);曝光到点击的间隔过短;点击率远超同广告位均值
下载与安装 点击注入(安装完成前极短时间内出现点击);安装时间分布异常
群控识别 大量设备在相同时间执行相同行为序列;设备之间共享 IP、共享设备属性

实时规则在 Flink 中执行,结果写入 Redis 黑名单供在线使用;离线用更复杂的模型(行为序列、图关联聚类)按天识别作弊团伙。

17.3 媒体级

手段 说明
流量突增 联盟媒体的请求量、点击率、转化率突然偏离历史
包名伪造 请求声称的包名与 SDK 获取的实际包名、签名不一致
不可见曝光 广告堆叠、隐藏展示、1×1 像素展示;靠 SDK 的可见性检测
无效流量比例 按媒体统计 IVT 比例,超阈值自动降权或暂停

17.4 处置

动作 适用
实时过滤 明确作弊的请求和事件
扣量 事后判定的无效曝光和点击从结算中扣除;对 DSP 退款,对联盟媒体扣减分成
黑名单 设备、IP、媒体
媒体处罚 降权、暂停、结算冻结、清退

无效流量的判定口径要与 DSP 在对账协议中约定,并在对账单中单列扣量明细。


18. 数据链路、报表与结算

18.1 日志设计

日志 产生方 内容 保留策略
竞价日志 adx-server 请求上下文、各 DSP 询价状态与耗时、全部出价、过滤原因、胜出结果、价格、实验分桶、快照版本 有填充的请求全量;无填充的请求抽样(如 5%),其余只保留分钟级聚合
计费事件日志 adx-tracker 曝光、点击、计费通知,含载荷解密后的字段和有效性判定 全量,长期保留
监控事件日志 adx-monitor 渲染、视频进度、转化等 全量或采样
通知日志 通知服务 nurl / burl / lurl 的发送时间、结果、重试次数 全量
配置变更日志 adx-admin 变更单、diff、审批、版本 永久

日志格式用 Protobuf,写入 Kafka 时按 request_id 分区,便于下游按请求关联。竞价日志中的 DSP 原始响应体很大,只在抽样调试时保留原文,常规只保留解析后的结构化字段。

18.2 Kafka 主题

主题 分区 key 可靠性配置 保留
adx.bid request_id acks=1(允许极少量丢失) 3 天
adx.billing_event request_id acks=all,min.insync.replicas=2,生产者幂等 7 天
adx.billing_dedup request_id 同上;Flink 事务写入 7 天
adx.monitor_event request_id acks=1 3 天
adx.notice dsp_id acks=all 7 天

18.3 实时计算(Flink)

作业 输入 输出
计费事件去重 billing_event 去重后的计费事件 → adx.billing_dedup(下游计费、burl 回调、配额与保量统计的唯一输入)
实时报表 bid、billing_dedup、monitor_event 分钟级聚合 → ClickHouse
配额统计 billing_dedup 各 DSP、各代码位当日曝光 → Redis(DSP 选择使用)
保量统计 billing_dedup 各品牌订单实时曝光 → Redis(保量控制器使用)
样本拼接 bid ⋈ billing_dedup ⋈ monitor_event(按 request_id,窗口 2 小时) 流量筛选、底价、pCTR 模型的训练样本 → 数据湖
实时反作弊 billing_event、bid 黑名单 → Redis;异常事件标记
告警指标 全部 各维度的收入、填充率、超时率等时序 → Prometheus

精确一次:Flink checkpoint + 幂等写入(ClickHouse 使用可去重的表引擎,按事件唯一键去重;Redis 计数使用带事件 ID 的去重逻辑或按窗口覆盖写)。

18.4 报表

ClickHouse 表设计:

  • 明细表:按天分区,排序键 (广告位, DSP, 时间),保留 30–90 天
  • 聚合表:物化视图按分钟、小时预聚合(请求数、询价数、出价数、胜出数、下发数、曝光、点击、预估收入),长期保留

报表维度:媒体、App、场景、广告位、样式、DSP、代码位、系统、地域、实验分桶、小时 / 天。

核心指标:

指标 定义
填充率 下发广告数 / 请求广告数
展示率 曝光 / 下发
点击率 点击 / 曝光
出价率(按 DSP) 出价次数 / 询价次数
胜出率(按 DSP) 胜出次数 / 有效出价次数
超时率(按 DSP) 超时次数 / 询价次数
eCPM 收入 / 曝光 × 1000
RPM 收入 / 请求 × 1000

报表中的收入分两列:预估收入(实时,按结算价 × 现金比 × 对账系数计算)和结算收入(按 DSP 结算数据回填,T+1 或更晚)。

18.5 结算与对账

结算数据回填:

  • DSP 结算数据通过 API 或文件导入,粒度通常是"日期 × 代码位"
  • 回填时按 ADX 预估收入的比例把结算收入分摊到更细的维度(广告位、小时、系统等),使报表中细维度的结算收入加总等于 DSP 结算总额

对账系数更新(闭环):

对账系数(代码位) = 最近 N 天结算收入 / 最近 N 天预估收入
截断到合理区间,并做平滑(如指数加权),供 11.3 排序使用

这样,预估偏高的 DSP(例如 DSP 实际结算打了折扣)会在排序中自动降权,排序结果逐步向"真实收入最大化"收敛。

日常对账:

步骤 内容
汇总比对 按 DSP × 代码位 × 日,比对曝光数、点击数、金额
阈值判断 差异在约定范围内(如 5%)自动确认;超出进入差异处理
差异下钻 按小时、广告位、系统下钻,定位差异集中的时间和流量
明细比对 必要时与 DSP 交换明细(request_id 级),逐条比对
结论 记录差异原因、责任方和处理方式(补结算、扣量、调整口径)

常见差异原因:时区与跨天、曝光判定口径不同、去重规则不同、反作弊扣量口径不同、计费通知丢失或重复、DSP 侧统计延迟、测试流量未剔除。这些口径必须在接入协议中事先约定。

媒体分成(联盟媒体):

  • 媒体应得 = 结算收入 × 分成比例 − 无效流量扣量
  • 按月出具对账单,媒体确认后付款;有争议的部分单独挂账

19. 容量规划

以下是按假设参数做的估算,用于确定数量级和资源结构;实际机器数以压测结果为准。

19.1 假设

参数 取值
峰值入口 QPS 30 万
流量筛选后平均每请求询价 DSP 数 6
单个 bid request 大小 约 2 KB(JSON 未压缩)
DSP 出价率 30%;有出价的响应约 1 KB,无出价约 0.1 KB
填充率 50%(每秒下发约 15 万条广告)
曝光率(曝光 / 下发) 80%(每秒约 12 万次曝光)
点击率 2%
平均每条广告的监控事件 5 个

19.2 出向请求与带宽

项目 计算 结果
出向询价 QPS 30 万 × 6 180 万 QPS
出向带宽(未压缩) 180 万 × 2 KB 约 3.6 GB/s ≈ 29 Gbps
出向带宽(gzip,按压缩到约 1/3 估算) — 约 10 Gbps
DSP 响应带宽 180 万 × (0.3 × 1 KB + 0.7 × 0.1 KB) 约 0.67 GB/s ≈ 5.3 Gbps

结论:带宽是首要成本项。流量筛选把平均扇出从"广告位绑定的全部 DSP"(例如 20 家)降到 6 家,相当于把出向带宽和 DSP 侧压力降为约 1/3。

19.3 adx-server

项目 估算
单请求 CPU 假设约 1.5 ms(解析、补全、6 次编码与压缩、解析响应、过滤排序、组装、日志)
总 CPU 30 万 × 1.5 ms = 450 核持续繁忙
机器 32 核机器按 50% 目标利用率,约 28 台;两个可用区,单可用区故障时另一侧需承担全部流量(利用率上限 80%),每个可用区约 18 台,共约 36 台
单实例在途请求 每台约 8300 QPS × 0.2 s ≈ 1700 个,协程数约 1 万量级
单实例出向连接 每台约 5 万出向 QPS,按平均 RTT 0.15 s × 1.5 冗余 ≈ 1.1 万个连接
内存 配置快照双份(约 2 × 200 MB)+ 本地缓存 2–4 GB + 连接缓冲(1.1 万 × 约 50 KB ≈ 0.55 GB)+ 请求对象;按 16–32 GB 配置

出向连接的端口约束:一台机器到同一个目标 IP:端口的连接数受本地临时端口范围限制(Linux 默认 ip_local_port_range 约 2.8 万个端口)。某个大 DSP 只暴露一个 VIP 时,单机连接数可能逼近上限,需要扩大端口范围、使用多个源 IP,或推动 DSP 支持 HTTP/2。

19.4 其他服务

服务 负载 估算
adx-gateway 30 万 QPS;TLS(会话复用 + HTTP/2 后握手占比低)、验签、解密、解析 单请求约 0.2 ms,约 60 核,含冗余约 6–8 台
adx-tracker 曝光 12 万 + 点击约 0.24 万 + 计费通知 ≈ 12.5 万 QPS 按 2 倍冗余设计 25 万 QPS
adx-monitor 约 15 万 × 5 = 75 万事件 / 秒(批量上报后请求数更少) 可采样,按均值规划
通知服务 burl 约 12 万 / 秒 + nurl、lurl 异步,按 DSP 限速

19.5 存储

存储 估算
防重放 nonce 30 万 × 300 s ≈ 9000 万 key,约 7 GB
重复广告判定 15 万 × 300 s ≈ 4500 万 key,约 4 GB
计费去重(Flink RocksDB 状态) 约 12.5 万 / 秒 × 86400 s ≈ 108 亿 key/天,每 key 约 50 B,约 500 GB,分布在 Flink 集群本地磁盘
竞价日志 有填充请求 15 万 / 秒 × 约 1.5 KB ≈ 225 MB/s,约 19 TB/天(原始);Kafka 用 zstd 压缩后约 1/4;无填充请求 5% 抽样可忽略
计费事件日志 12.5 万 / 秒 × 约 0.5 KB ≈ 60 MB/s,约 5 TB/天(原始)
ClickHouse 明细 30–90 天 + 聚合长期;按压缩后数据量和查询并发规划分片与副本

20. 高可用与降级

20.1 故障域与冗余

层级 冗余方式
实例 无状态服务多实例,健康检查自动摘除
可用区 在线服务、Redis、Kafka 跨可用区部署;单可用区故障时剩余容量可承担全部流量(见 19.3)
地域 多地域各自闭环;地域故障时 GSLB 把流量切到其他地域(跨地域的网络延迟会增加,开屏等敏感场景依赖客户端兜底)
依赖服务 每个外部依赖都有超时、熔断和降级路径(见 20.2)

20.2 降级矩阵

故障 检测 自动处理 影响
单个 DSP 慢或故障 该 DSP 超时率、错误率 熔断该 DSP 该 DSP 的收入
大量 DSP 同时超时(出口网络问题) 全局超时率 缩短等待时间、优先保证有响应的 DSP;告警 收入下降
ID 映射 / 标签 KV 故障 KV 超时率 跳过补全 DSP 出价率和价格下降
计数 Redis 故障 超时率 QPS 退回本地令牌桶;日请求上限按"最后已知额度"本地扣减;日曝光上限按最后读数继续 配额精度下降
模型不可用(快照中模型段缺失或异常) 加载校验 流量筛选退回"预定向 + 固定比例";动态底价退回静态底价 成本上升或收益下降
配置中心 / 对象存储故障 watch 断开 保持当前快照;新实例用本地磁盘快照启动 配置无法更新
Kafka 故障(竞价日志) 发送失败 本地磁盘缓冲(有上限),恢复后补发;超出上限后丢弃无填充请求的日志 报表延迟
Kafka 故障(计费事件) 发送失败 tracker 写本地磁盘预写日志,恢复后按序补发;绝不丢弃 计费延迟
整体过载 排队时间、CPU 按流量价值分级拒绝(先丢联盟和低价值广告位) 低价值流量无广告
adx-server 全部不可用 SDK 请求失败 SDK 客户端兜底(见 20.3) 收入大幅下降,但 App 可用

原则:任何依赖故障都不能让竞价主流程失败;计费数据可以延迟,但不能丢失、不能重复。

20.3 客户端兜底

SDK 在本地保存一份兜底配置(随正常响应定期更新):

  • ADX 请求失败或超时时,按兜底配置直接请求已集成的 DSP SDK(按预设优先级的瀑布流)
  • 开屏使用本地缓存中仍在有效期内的广告
  • 连续失败时退避,避免在 ADX 故障恢复时形成重试风暴
  • 兜底期间产生的曝光同样上报 tracker,保证数据完整

20.4 发布安全

  • 服务发布:金丝雀 → 分批,按指标自动回滚(见 24.5)
  • 配置发布:快照校验 + 金丝雀 + 自动检查(见 15.4)
  • 适配器发布:按 DSP 灰度,新旧版本并存
  • 开关:每个新功能都有独立开关,可以在不发版的情况下关闭

21. 性能工程

21.1 序列化

项 做法
JSON 使用代码生成或 JIT 的高性能库(如 easyjson、sonic),避免反射;对各 DSP 的请求结构预先生成编码器
Protobuf 使用生成代码优化版本(如 vtprotobuf),支持对象复用
解析响应 只解析需要的字段;大字段(如 adm)保留原始字节,需要时再解析
压缩 gzip 使用可复用的压缩器对象,避免每次分配压缩字典

21.2 内存与 GC(Go)

  • 分配率决定 GC 开销:请求上下文、缓冲区、候选对象用 sync.Pool 复用;切片和 map 预分配容量
  • 设置 GOMEMLIMIT(例如容器内存的 80%)配合 GOGC 调整,在内存充足时减少 GC 次数
  • 配置快照等长期存活的大对象尽量使用少指针的结构(数组、整数 ID 代替指针和字符串),减轻 GC 扫描负担
  • 目标:GC 占 CPU < 10%,GC 引起的延迟尾部可控;持续用 pprof 或持续性能剖析观察分配热点

21.3 并发与定时器

  • 每个询价一个协程,数量可控(见 19.3),无需额外的协程池
  • 使用 context 的截止时间统一控制超时,避免每次询价单独创建定时器造成的定时器堆压力
  • 热路径上避免全局锁:计数用分片的原子变量,配置用不可变快照 + 原子指针

21.4 网络与系统

项 做法
HTTP 客户端 自定义 Transport:每个 DSP 独立的最大连接数、空闲连接数、空闲超时;关闭不必要的功能(如自动解压由自己控制)
内核参数 扩大 ip_local_port_range、somaxconn、文件描述符上限;开启 tcp_tw_reuse(出向短连接场景)
网卡 多队列网卡 + RSS,中断绑定到多个核;单机 25 Gbps 网卡
日志 异步批量写入,热路径上不做字符串格式化;采样的调试日志单独通道

21.5 性能指标

压测时关注:单机最大 QPS(P99 延迟不超标前提下)、每千次请求 CPU 秒、每千次请求出向字节数、GC 占比、各阶段耗时分布。这些指标纳入持续监控,任何版本出现劣化都要在发布前发现。


22. 安全与合规

22.1 通信与认证

链路 做法
SDK → gateway TLS + 请求签名 + 防重放 + 敏感字段加密(见 6.1)
服务间 内网 + mTLS(按安全等级要求)
ADX → DSP TLS;按 DSP 要求携带认证 token 或签名;ADX 出口 IP 固定,便于 DSP 设置白名单
DSP → ADX(回调、API) IP 白名单 + API Key 或 mTLS

22.2 价格加密

结算价出现在 URL 中,必须加密,防止中间环节窥探竞争对手价格或篡改价格。可以参考 Google 公开的方案:

每个 DSP 分配两把密钥:加密密钥 e_key、完整性密钥 i_key
iv        = 16 字节(时间戳 + 随机数)
pad       = HMAC-SHA1(e_key, iv) 的前 8 字节
密文价格   = 价格(8 字节整数)XOR pad
签名      = HMAC-SHA1(i_key, 价格 || iv) 的前 4 字节
最终值    = base64url(iv || 密文价格 || 签名)   (28 字节,编码后 38 个字符)
  • DSP 用同样的密钥解密并校验签名
  • 密钥在 dsp-portal 中生成和轮换,轮换期间新旧密钥并存
  • 同样的方案用于 ADX 自己追踪链接中的价格字段

22.3 隐私合规

要求 做法
最小必要 只采集和下发广告投放必需的字段;精确位置只在用户授权后采集
用户同意 SDK 在用户同意隐私政策前不采集设备信息、不请求广告
个性化推荐开关 用户关闭个性化时,请求打标,不下发用户标签和人群信息,DSP 按非个性化方式投放
未成年人 识别到未成年人模式时,不做个性化广告,屏蔽不适宜行业
数据共享 与 DSP 签署数据处理协议,约定用途、保存期限和安全要求
日志脱敏 分析用的日志中设备 ID 加盐哈希;原始数据访问需审批并留痕
数据保留 按法规和合同设置保留期限,到期删除
海外流量 GDPR:透传 TCF consent string;美国:GPP / US Privacy 字符串;COPPA:儿童流量打标。OpenRTB 2.6 在 regs 和 user 中标准化了这些字段

22.4 广告合规

要求 做法
广告标识 所有广告显著标明"广告"
一键关闭 弹出类广告(插屏、开屏)显著展示关闭按钮,确保一键关闭,不使用虚假关闭标志
下载类广告 展示应用名称、开发者、版本号、隐私政策、权限列表、功能介绍,下载前二次确认
交互式广告 摇一摇、滑动等交互的触发阈值按监管要求设置(例如摇一摇的加速度、角度、持续时间阈值),防止误触发跳转;阈值以最新监管文件为准,并做成可配置项
特殊行业 医疗、药品、金融、教育等需要资质,由审核平台校验
内容安全 素材审核、敏感词、紧急屏蔽通道(见第 16 章)

22.5 管理面安全

  • 管理平台 RBAC、敏感操作二次验证、所有操作留审计日志
  • 密钥(SDK 密钥、价格加密密钥、追踪密钥、DSP 凭证)存放在密钥管理服务中,服务启动时获取,不写入配置文件和代码仓库
  • 生产数据访问走审批,查询留痕

23. 可观测性

23.1 指标

层级 指标
业务 请求量、填充率、曝光、点击、预估收入、RPM、eCPM(按媒体、广告位、DSP 维度),与上周同期对比
DSP 询价 QPS、出价率、胜出率、超时率、HTTP 错误率、解析错误率、P50/P99 响应时间、平均出价、各过滤原因占比、熔断状态
服务 各阶段耗时分布、端到端 P99、错误率、在途请求数、协程数、GC 占比、连接池使用率
依赖 KV 延迟与超时率、Redis、Kafka 发送延迟与积压、Flink 作业延迟
配置 当前快照版本(按实例)、最近一次加载耗时、加载失败次数
追踪 各事件量、签名失败率、过期率、去重命中率、下发到曝光的转化漏斗

23.2 链路追踪与调试

  • 链路追踪:OpenTelemetry;采用尾部采样,慢请求和出错请求全量保留,正常请求低比例采样
  • 调试模式:在管理平台上把测试设备 ID 加入白名单;这些设备的请求记录完整决策过程(每家 DSP 的请求与响应原文、每个候选的过滤原因、排序分、最终价格),在平台上按 request_id 查询
  • 回放工具:用竞价日志中的请求快照和对应的配置快照版本,离线重放竞价流程,复现当时的决策;DSP 响应使用日志中记录的结果

23.3 告警

告警 条件(示例) 级别
收入异常 5 分钟收入较上周同期下降 > 20% 紧急(电话)
填充率异常 核心广告位填充率 5 分钟下降 > 15% 紧急
服务错误 竞价服务错误率 > 1% 或 P99 超过预算 紧急
计费链路 tracker 错误率上升、Kafka 发送失败、计费去重作业延迟 > 5 分钟 紧急
DSP 异常 头部 DSP 超时率 > 20% 或熔断 高
配置发布失败 快照构建或加载失败 高
证书与密钥 证书 30 天内到期、密钥轮换失败 中
数据延迟 报表延迟 > 10 分钟 中

收入告警能最快反映大部分故障(大 DSP 断连、配置错误、追踪链接失效、SDK 版本问题),应作为最高优先级的业务告警。


24. 测试与发布

24.1 单元与模块测试

每个 Pipeline 阶段、过滤器、排序和定价函数都有独立的单元测试,覆盖边界条件:底价相等、有效价格相等、全部候选被过滤、deadline 已过、配置缺失等。

24.2 适配器契约测试

  • 每个适配器配套一组金样本:给定内部请求 → 期望的 DSP 请求;给定 DSP 响应(来自真实抓包,已脱敏)→ 期望的归一化候选
  • 金样本覆盖:各样式、各交互类型、多广告返回、无出价、异常响应(缺字段、非法价格、超大响应)
  • 适配器或映射规则的任何修改都必须通过契约测试才能发布

24.3 集成测试与模拟 DSP

搭建模拟 DSP 集群:可以按配置返回不同出价、不同延迟分布、错误和超时。用它做端到端集成测试和压测,不依赖真实 DSP。

24.4 流量回放与对比

  • 抽取线上真实请求(脱敏),在测试环境回放
  • 差异对比:新旧版本对同一批请求的决策结果(候选 DSP 集合、过滤原因、胜出广告、价格)逐条比对,除预期的差异外必须一致
  • 压测:用回放流量 + 模拟 DSP,压到设计峰值的 1.5 倍,验证延迟和资源

24.5 发布流程

步骤 内容
预发 预发环境接入少量真实流量(不计费或计费隔离),跑通全链路
金丝雀 1% 实例,观察 15–30 分钟;自动对比金丝雀与基线的错误率、P99、填充率、RPM
分批 10% → 50% → 100%,每批观察
自动回滚 任一指标超出阈值自动回滚
故障演练 定期演练:DSP 大面积超时、KV 故障、Kafka 故障、可用区故障、配置中心故障,验证降级矩阵

SDK 发布按 App 版本灰度,服务端保持对旧版本 SDK 的协议兼容;新功能用服务端开关按 SDK 版本启用。


25. 从旧系统迁移

25.1 配置迁移

步骤 内容
盘点 列出旧系统全部配置项:应用、场景、广告位、DSP 与代码位绑定、底价、样式、过滤规则、敏感词、屏蔽素材、流量屏蔽规则、实验
转换 编写迁移脚本,把旧配置转换为新模型;无法自动转换的项目列入人工处理清单
校验 迁移后逐项比对:数量一致、关键字段一致、引用关系完整;形成迁移检查清单并签字确认
冻结 切换期间冻结旧系统配置变更,或建立双写

25.2 双跑比对

新系统以影子模式接收复制的线上请求:完整执行竞价流程,但不返回给客户端、不计费、不发送通知(询价可以只发给模拟 DSP,或只对部分 DSP 真实询价并在 DSP 侧标记为测试流量)。

逐条比对新旧系统在同一请求上的决策:

  • 请求过滤结果是否一致
  • 候选 DSP 集合是否一致
  • 广告过滤原因是否一致(使用相同的 DSP 响应时)
  • 胜出广告与价格是否一致

不一致的请求分类归因,确认是预期内的改进还是缺陷。一致率达到预定目标(例如 99%)后再切流。

25.3 切流

  • 按 App → 广告位逐步切换:1% → 10% → 50% → 100%
  • 每一步对比新旧系统所服务流量的 RPM、填充率、延迟、DSP 各项指标,以及次日的结算数据
  • 保留一键切回旧系统的能力,直到新系统稳定运行一个完整的结算周期
  • 报表口径:切换期间两套报表并行,确认指标定义一致后下线旧报表

26. 实施计划与风险

26.1 分期

阶段 范围 目标
一期:核心竞价 gateway、adx-server Pipeline、头部 DSP 适配器(L1 / L3)、广告过滤链、按有效价格排序、品牌优先、样式映射、tracker 计费链路与去重、配置平台核心功能与快照分发、基础报表 新系统承接全部自有流量,收入不低于旧系统
二期:流量管控与接入效率 请求过滤与流量屏蔽规则、DSP 预定向与配额、监控链路、样式功能配置平台、dsp-portal(沙箱、响应校验器、映射调试器)、L2 映射 标准 DSP 接入 ≤ 1 天,运营配置全部平台化
三期:收益与质量 流量筛选模型、动态底价、实验平台、创意审核平台、反作弊体系、PDB 保量、影子流量与自动放量、结算与对账自动化 RPM 提升、出向带宽下降、对账自动化

每期都以"可灰度、可回滚、指标可观测"为上线前提。

26.2 风险与对策

风险 对策
头部 DSP 响应慢,拖累整体延迟 按 DSP 设超时;提前结束等待;开屏走预加载;推动 DSP 就近部署
DSP 数据差异大,对账困难 接入协议明确口径;burl 服务端发送并记录;对账系数闭环;明细级比对工具
配置错误导致收入损失 快照校验、金丝雀发布、收入告警、一键回滚
计费重复或丢失 端到端幂等、持久化重试、Flink 去重、离线兜底校验
模型策略劣化 所有策略经 A/B 实验上线;护栏指标;可快速关闭并退回规则
出向带宽成本超预期 流量筛选、字段裁剪、压缩、Protobuf;按"每千次请求成本"持续考核
监管要求变化 合规相关参数(交互阈值、屏蔽规则、隐私字段)全部配置化,可快速调整
SDK 版本碎片化 协议前后兼容;服务端按 SDK 版本控制下发能力;gateway 适配旧协议
迁移期间收入波动 双跑比对、小比例切流、保留回切能力

附录

A. OpenRTB 2.6 常用字段与内部模型对照

BidRequest

OpenRTB 字段 含义 内部模型
id 请求 ID AdContext.request_id
imp[].id imp ID Imp 序号
imp[].tagid 广告位(对 DSP 而言是代码位 ID) 代码位的 DSP 侧 ID
imp[].banner / video / native 形态与规格 样式规格
imp[].bidfloor / bidfloorcur 底价与币种 折算后的底价(见 12.2)
imp[].pmp.deals[] Deal 列表(id、bidfloor、wseat) 品牌订单
imp[].secure 是否要求 https 素材 固定为 1
app.bundle / app.cat 包名、类目 Media
device.ua / ip / ipv6 / os / osv / make / model / connectiontype / carrier / ifa / lmt 设备信息 Device
device.geo 地理位置 Geo
user.id / buyeruid ADX 用户 ID、DSP 侧用户 ID User
tmax DSP 最大响应时间 按 DSP 配置和剩余 deadline
at 拍卖类型(1 一价,2 二价) DSP 合同约定
cur 允许的币种 配置
bcat / badv / battr 屏蔽行业、广告主、创意属性 广告位与媒体屏蔽配置
source.schain 供应链 联盟流量需要
regs 法规信号(COPPA、GDPR、GPP) Regs
test 测试请求 沙箱联调

BidResponse

OpenRTB 字段 含义
id 对应请求 ID
seatbid[].seat 买方席位
seatbid[].bid[].impid 对应的 imp
bid[].price 出价(CPM)
bid[].adm 素材标记(HTML、VAST、原生 JSON)
bid[].nurl / burl / lurl 胜出、计费、落败通知
bid[].crid / adid / cid 创意 ID、广告 ID、活动 ID
bid[].adomain / cat / attr 广告主域名、行业、创意属性
bid[].dealid Deal ID
bid[].w / h / mtype 尺寸、素材类型(2.6 新增 mtype)
nbr 不出价原因

宏:${AUCTION_ID}、${AUCTION_BID_ID}、${AUCTION_IMP_ID}、${AUCTION_SEAT_ID}、${AUCTION_AD_ID}、${AUCTION_PRICE}、${AUCTION_CURRENCY}、${AUCTION_LOSS}。

B. 原因码

段 类别 例子
R1xx 请求过滤 R101 必填字段缺失、R102 广告位无效、R103 设备无效、R104 黑名单、R105 SDK 版本过低、R110 命中流量屏蔽规则、R120 限流
S2xx DSP 未发送 S201 预定向不匹配、S202 QPS 配额不足、S203 日请求上限、S204 日曝光上限、S205 熔断、S206 流量筛选模型筛除、S207 流量屏蔽、S208 本地排队满、S209 放量档位未覆盖
D3xx DSP 响应 D301 超时、D302 HTTP 错误、D303 解析错误、D304 无出价、D305 适配器异常
F4xx 广告过滤 F401 无效广告、F402 低于底价、F403 样式不匹配、F404 素材屏蔽、F405 未审核、F406 行业或广告主屏蔽、F407 下载要素缺失、F408 敏感词、F409 流量屏蔽规则、F410 重复广告
A5xx 竞价结果 A501 胜出、A502 价格落败、A503 多广告去重淘汰、A504 Deal 放弃

原因码在竞价日志、报表、DSP 平台中统一使用,与 OpenRTB 的 nbr 和 loss reason 建立映射表,在落败通知中按 OpenRTB 编码回传。

C. DSP 配置示意

dsp:
  id: 1024
  name: "示例 DSP"
  mode: rtb                     # rtb | api | s2s_bidding
  protocol: openrtb-2.6         # openrtb-2.5 | openrtb-2.6 | private
  adapter: { id: "openrtb-std", version: "1.4.0" }
  encoding: protobuf            # json | protobuf
  compression: gzip
  endpoints:
    - { region: north, url: "https://bid-north.example.com/rtb" }
    - { region: south, url: "https://bid-south.example.com/rtb" }
  timeout_ms: 180
  qps_limit: 50000
  daily_request_limit: 2000000000
  daily_impression_limit: 30000000
  auction_type: first_price
  cash_ratio: 0.95
  settlement_basis: dsp         # dsp | adx
  price_encryption_key_id: "k-2026-03"
  targeting:
    os: [android, ios]
    require_device_id: true
    exclude_provinces: []
  ramp_stage: 100               # 放量档位(百分比)
  circuit_breaker: { window_s: 10, min_requests: 100, error_rate: 0.5, open_s: 30 }

D. 术语表

术语 含义
ADX 广告交易平台,组织实时竞价
DSP 需求方平台,代表广告主出价
SSP 供给方平台,代表媒体管理流量
RTB 实时竞价
PD / PDB / PA 优先交易(固定价不保量)/ 程序化保量 / 私有竞价
代码位 DSP 分配给 ADX 的广告位 ID
现金比 DSP 收入中以现金结算的比例
对账系数 结算收入 / 预估收入
有效价格 统一到 ADX 实际收入口径的价格
流量筛选 按出价概率与配额决定是否向某 DSP 询价
快照 不可变的配置全集,按版本发布
RPM / eCPM 每千次请求收入 / 每千次曝光收入
IVT 无效流量

E. 面试题

概念与业务

# 问题 答案
1 ADX、SSP、DSP、DMP 的区别?一次 RTB 的完整时序? SSP 代表媒体管理流量、追求媒体收入;DSP 代表广告主,对每次曝光决定是否出价、出多少;ADX 是交易平台,组织竞价、决出胜者并负责结算;DMP 提供人群和标签数据。时序:媒体请求 → ADX 过滤与补全 → 选择 DSP → 并发询价(统一截止时间)→ 过滤出价、排序定价 → 返回广告 → 客户端渲染与曝光 → 服务端发送计费通知(burl)
2 RTB、PMP、PD、PDB 的区别?品牌广告如何与竞价广告共存? RTB 公开竞价;PMP 只邀请部分买方竞价,有 Deal 底价;PD 固定价、买方有优先购买权但不保量;PDB 固定价且保量。共存方式:排序键分层,PDB 中保量控制器判定当前应投放的订单进入最高层,PD 次之,其余所有候选按有效价格统一竞争
3 一价与二价拍卖的区别?为什么行业转向一价? 一价胜出者付自己的出价;二价胜出者付第二高价加最小加价单位。二价在"多层竞价链"(Header Bidding、多个 ADX 串联)中不透明,买方无法知道自己真正参加的是哪种拍卖,行业因此转向规则透明的一价。一价下按真实价值出价赢了也没有剩余,DSP 会估计赢率曲线把出价压到价值以下(bid shading),最优出价 = argmax (价值 − 出价) × 赢率。出价不会无限下降,因为压得太低会输给其他 DSP;但竞争稀疏时会持续下探,ADX 用提高竞争密度、底价、控制胜出价反馈粒度、监控出价趋势、固定价交易来应对
4 nurl、burl、lurl 分别是什么?计费以哪个为准? nurl 是胜出通知,ADX 决出胜者时发出;burl 是计费通知,客户端曝光判定成功后发出;lurl 是落败通知,带落败原因码。计费以 burl 为准,因为胜出不等于曝光(可能渲染失败或未展示)。burl 由服务端在去重后发送并记录发送结果,便于对账
5 瀑布流、客户端竞价、服务端竞价各自的优缺点? 瀑布流按优先级依次询价,前一家不要才问下一家,延迟高且不是价高者得;客户端竞价由各 DSP SDK 在客户端分别请求再比价,延迟高、难以统一过滤和审核;服务端竞价由 DSP SDK 生成买方 token 随请求上传,ADX 在服务端统一询价比价、统一过滤,胜出后由 DSP SDK 渲染,是目前的主流
6 底价怎么定?一价拍卖下底价还有什么作用? 底价按 全局 → 媒体 → App → 场景 → 广告位 → 代码位 → Deal 分层覆盖;下发给各 DSP 时按 F ÷ (现金比 × 对账系数) 折算,并不低于代码位自身的最低价。来源上由运营设下限(直客价格保护、品牌保护)和上限,模型在区间内按流量分段调整,合同和 Deal 价格优先。一价下底价不直接抬高成交价,主要作用是拦截低价、低质量广告并影响 DSP 的出价策略,效果必须用实验验证
7 现金比、对账系数是什么?不回传价格的 DSP 怎么排序? 现金比是 DSP 收入中可按现金结算的比例;对账系数是最近一段时间 DSP 结算收入与 ADX 预估收入之比,按代码位滚动更新并截断平滑。有效价格 = 出价 × 现金比 × 对账系数,所有候选统一用它排序。不回传价格的 DSP 用该代码位的历史结算 eCPM 作为预估价参与排序,并定期校准

架构与性能

# 问题 答案
8 设计一个 30 万 QPS、P99 300 ms 的 ADX 服务拆为接入网关(TLS、验签、限流、协议标准化)、竞价服务(过滤、补全、选 DSP、询价、竞价、组装)、上报服务(计费与监控分离)、配置平台与快照分发、数据链路(Kafka、Flink、ClickHouse、结算)。延迟预算:网关约 3 ms,过滤与补全约 10 ms,选 DSP 约 2 ms,询价 150–250 ms,竞价组装约 5 ms,留 10–20 ms 尾部余量;接收时计算绝对截止时间并逐级下传。容量:出向询价约 180 万 QPS、约 29 Gbps(未压缩),带宽是首要成本;竞价服务约 36 台 32 核机器,可承受单可用区故障
9 如何控制扇出询价的尾延迟?一家 DSP 变慢如何不拖垮全局? 所有询价共用一个截止时间,到点即开拍,迟到的出价丢弃;全部 DSP 已返回时提前结束等待。每家 DSP 独立的连接池和并发信号量(舱壁),一家变慢只耗尽自己的许可;按超时率和错误率熔断,恢复时慢启动;询价失败不重试
10 DSP 连接池怎么设置?出向连接为什么会耗尽本地端口? 池中是 ADX 实例到该 DSP 的 TCP 连接。HTTP/1.1 一条连接同一时刻只承载一个请求,所需连接数 = 同时在途请求数,按 Little 定律等于 QPS × 每个请求占用连接的时间;取 P99 保证响应变慢时仍够用,× 1.5 留突发余量,即池大小约等于 单实例对该 DSP 的 QPS × P99 响应时间 × 1.5,按 DSP、按地域 endpoint 独立配置,启动时预热,空闲超时略小于对端的 keep-alive 超时。一台机器到同一个目标 IP:端口的连接数受本地临时端口范围限制(Linux 默认约 2.8 万),大 DSP 只暴露一个 VIP 时单机连接可能逼近上限;对策是扩大端口范围、使用多个源 IP 或改用 HTTP/2 多路复用
11 为什么要做流量筛选?如何在配额内最大化收入? 广播给所有 DSP 会浪费带宽和 DSP 资源,且 DSP 对大部分请求不出价,出价率低会让 DSP 压低 QPS。每家 DSP 一个轻量模型预测出价概率和期望价格,期望价值高于动态阈值才询价;阈值由控制器按实际发送量与配额的差距调节。保留 3%–5% 的随机探索流量,防止模型只在自己选中的流量上学习而越来越保守
12 配置如何秒级热更新且不加锁?请求中途配置切换怎么办? 配置由构建器从 MySQL 一致性读取、校验、编译成不可变的分段快照,版本号写入 etcd;服务 watch 到新版本后后台加载构建,完成后原子替换全局指针。每个请求开始时获取一次快照引用,整个请求只用这一个版本,旧快照在无人引用后释放。发布先灰度到少量实例,指标异常自动回滚
13 适配器放在进程内还是独立服务?如何做故障隔离? 放在进程内,省去一跳网络和一次序列化。每个适配器在独立协程和资源配额内执行,panic 被捕获,超时或返回非法结果都记为失败并计入该 DSP 的熔断;依赖重型 SDK 的少数适配器才单独部署为 connector 服务
14 新 DSP 接入如何不发版、如何自动验收放量? 协议分三层:标准 OpenRTB 只需配置;字段有差异的用声明式映射配置并热加载;完全私有的协议才写代码插件。接入平台提供请求构造器、响应校验器、映射调试器;上线前用影子流量(复制真实请求、不参与竞价、不计费)验证;再按 1% → 5% → 20% → 100% 放量,每档自动检查超时率、P99、出价合法率、错误率和曝光差异率,达标才进入下一档

算法与数据结构

# 问题 答案
15 预定向、DSP 内部定向、流量屏蔽规则有什么区别? 预定向由 DSP 声明"只要哪些流量",ADX 据此决定是否向它询价;DSP 内部定向由广告主在 DSP 内设置,决定用哪个广告出价,ADX 看不到;流量屏蔽规则由媒体或运营设置,决定某类流量不出哪些广告或哪些 DSP
16 预定向用位图怎么实现?什么时候换成 Conjunction 算法? 给每个 (DSP, 代码位) 编号;每个维度的每个取值建一个位图,第 i 位表示条目 i 接受该取值,"不限"的条目在该维度所有取值上都置 1,区间条件按端点切段;位图由 DSP 的预定向配置在快照构建时生成,与用户无关、所有请求共享;请求时按自己的属性值取出各维度对应的位图逐一按位与,得到临时结果,剩下的位就是候选(人群维度先把用户所属人群包的位图按位或)。千级条目在微秒内完成。条件达到数万乃至百万级(品牌订单、自营广告)时位图过长,改用布尔表达式索引
17 Conjunction 算法中 K 的含义?为什么能跳过大量合取式?多值属性怎么处理? K 是合取式中 ∈ 条件的个数。索引按 K 分区,以"属性=值"为 key 建按合取式 ID 排序的倒排链。请求在每个单值属性上只有一个取值,所以一个合取式成立等价于恰好有 K 条命中的链同时指向它且没有 ∉ 链指向它;查询时用第 K 小的链头作为目标,其余链直接跳过去,凑不够 K 条链的合取式被整段跳过。多值属性会导致同一属性重复计数,查询前把同一属性的多条链合并成一个去重的迭代器
18 敏感词过滤为什么用 AC 自动机?如何应对插入符号等规避? AC 自动机在 Trie 上加失配指针,一次扫描就能匹配全部敏感词,复杂度与文本长度和命中数相关、与词库大小无关。用双数组 Trie 实现以获得紧凑内存和缓存友好性。匹配前归一化:全角转半角、大小写统一、繁转简、去掉插入的空格和符号;误伤用白名单长词覆盖;同一创意反复出现,按创意指纹缓存匹配结果
19 流量屏蔽规则用在什么场景?如何在微秒级内按优先级匹配? 场景:送审版本不出广告或下载类广告、特殊时期全国或按城市屏蔽、地域合规、渠道合同不出第三方广告、某版本样式缺陷临时屏蔽、新用户首日不出开屏、竞品屏蔽。匹配:规则按优先级编号,每个维度的每个取值建规则位图,请求时逐维按位与,结果中最低位的 1 就是最高优先级的命中规则;时间段由后台定时计算"当前生效规则位图"再参与按位与。进一步按 App 预切分规则子集,并按"影响规则的属性组合"缓存匹配结果,缓存 key 带配置版本号
20 千万级素材 MD5 屏蔽列表如何在内存中高效判断? 把 MD5 取 64 位哈希,放进紧凑的开放寻址哈希集合(每条约 8–16 字节,千万条约百 MB 级),随配置快照构建和加载,在线 O(1) 查找;紧急屏蔽走独立的小快照段秒级生效
21 人群包的正排和倒排各适合什么场景?Roaring Bitmap 为什么省空间? 正排(用户 → 所属包 ID 列表)适合在线请求一次读取;倒排(包 → 用户位图)适合包的导入、更新、交并差和覆盖人数估算。常用混合方案:少量热门包以 Roaring Bitmap 加载进内存直接判断,大量大包离线转为正排。Roaring 把 32 位整数按高 16 位分桶,桶内元素不超过 4096 个时用 uint16 有序数组,超过时用 8 KB 位图,连续区间用 run 编码,每个桶按密度选最省的表示;前提是用户有稠密的整数编号

分布式与数据

# 问题 答案
22 DSP 每日请求上限、曝光上限在几十台实例上怎么控制? 瞬时 QPS 用本地令牌桶,总额按各实例实际流量比例分配;每日请求上限用配额租约,实例从 Redis 批量领取额度、本地扣减,接近上限时每次领取量递减;每日曝光上限由 Flink 统计去重后的曝光写回 Redis,服务读取后在接近上限时按比例减少询价,避免统计延迟导致超出
23 频控用什么数据结构?上报延迟和并发请求会导致什么问题?多机多线程如何原子更新? 以用户为主键的 KKV:一个用户一条记录,记录内按 (维度类型, 维度 ID) 有序的定长条目(天序号、计数、最近时间,滚动窗口加最近 N 次时间戳),二分查找、惰性删除、整条记录 TTL、只记录有频控的维度。上报延迟和并发请求会让计数偏小而超频,下发时预占 pending 计数。原子性靠单条记录原子操作(Redis Lua、Aerospike operate)或 CAS;已确认计数由 Flink 按 uid 单写者覆盖写,预占由在线服务写入另一个字段,两者互不覆盖;进程内用分段锁
24 计费事件怎么保证不丢不重?为什么不用 Redis 存 48 小时去重 key? 不丢:SDK 本地持久化队列重试,tracker 写 Kafka 用 acks=all 与 min.insync.replicas=2,Kafka 故障时写本地磁盘预写日志再补发。不重:以 (request_id, ad_index, 事件类型) 为 key 由 Flink(RocksDB 状态,TTL 24 小时,与追踪链接过期时间一致)去重,离线结算再去重兜底。峰值每秒十万级事件,存 48 小时会累积百亿级 key,内存成本过高
25 与 DSP 对账差异的常见原因?如何定位? 常见原因:时区与跨天、曝光判定口径不同、去重规则不同、反作弊扣量口径不同、计费通知丢失或重复、DSP 统计延迟、测试流量未剔除。定位:按 DSP × 代码位 × 日比对,超阈值后按小时、广告位、系统下钻,必要时交换 request_id 级明细逐条比对;口径要在接入协议中事先约定
26 竞价日志量太大怎么办? 有填充的请求全量保留;无填充的请求按比例抽样,其余只保留分钟级聚合;DSP 原始响应体只在调试抽样中保留原文,常规只存解析后的结构化字段;Kafka 用 zstd 压缩;明细和聚合在 ClickHouse 中分别设置不同的保留期
27 PDB 保量如何不欠量、不超量、匀速? 用历史同期流量预测各广告位各小时的可用库存,多个订单共享库存时先分配;每个订单每分钟计算 基础投放概率 = 剩余目标 / 剩余可用库存,再用 PID 控制器按实际进度与理想进度的偏差修正;接近结束欠量时提高概率,按目标的约 99% 收敛防止因统计延迟超量;按用户做订单频控
28 Kafka 在计费链路上怎么配置? 生产者 acks=all 并开启幂等;主题副本数 3、min.insync.replicas=2,跨可用区分布;去重后的计费事件由 Flink 以事务方式写入单独的主题,作为计费、burl 回调、配额与保量统计的唯一输入

稳定性与质量

# 问题 答案
29 KV、Redis、Kafka、配置中心逐个故障时如何降级? 原则是依赖故障不阻塞竞价、计费数据可延迟不可丢。KV 故障跳过补全,按无标签竞价;计数 Redis 故障时 QPS 退回本地令牌桶、日上限按最后已知额度本地扣减;Kafka 故障时竞价日志写本地磁盘缓冲、超限后丢弃无填充日志,计费事件写本地预写日志绝不丢弃;配置中心故障时保持当前快照,新实例用本地磁盘快照启动
30 ADX 整体不可用时客户端怎么兜底? SDK 保存随正常响应更新的兜底配置:ADX 请求失败或超时时按预设优先级直接请求已集成的 DSP SDK;开屏使用本地缓存中仍在有效期内的广告;连续失败时退避,防止恢复时形成重试风暴;兜底期间的曝光照常上报
31 反作弊有哪些层次?点击注入怎么识别? 请求级:IP 与设备信誉、模拟器与 root 检测、SDK 签名与防重放、请求频率、设备指纹一致性;事件级:签名与过期、事件顺序、点击坐标与时间间隔、点击率异常、群控识别;媒体级:流量突增、包名伪造、不可见曝光、无效流量比例。点击注入的特征是点击出现在安装完成前极短的时间内,通过点击到安装的时间差分布识别
32 开屏为什么要预加载?预加载的广告什么时候计费? 开屏在冷启动时展示,客户端只能等待几百毫秒,素材来不及实时下载。先在后台预加载候选广告并下载素材;冷启动时发起实时请求,支持的 DSP 在已缓存素材中出价,超时则从本地缓存中选有效期内、预估价最高的广告展示。预加载只下载素材不计费,实际展示时才触发曝光和计费
33 ADX 上的 A/B 实验有什么特殊偏差? DSP 的预算和出价策略跨实验组共享,实验组和对照组在争夺同一份预算:例如实验组抬高底价,DSP 把预算转到对照组,对照组收入虚高,实验结论被放大。对策是按时间片轮换实验、按 DSP 分组实验,或保留长期对照观察整体收入
34 旧 ADX 迁移到新系统如何保证收入不下降? 配置迁移:盘点、脚本转换、逐项比对并形成检查清单;双跑比对:新系统以影子模式处理复制的请求,逐条比对过滤结果、候选 DSP、过滤原因、胜出广告和价格,一致率达标后再切流;切流按 App 和广告位 1% → 10% → 50% → 100%,每步比对收入和延迟以及次日结算,保留一键回切直到稳定运行一个完整结算周期

F. 常见问题

F.1 同一用户在各 DSP 侧的 ID 从哪来?是否需要在竞价时请求 DSP 转换?

不需要。竞价链路上绝不为 ID 映射请求 DSP:一次额外的外部调用会占用几十毫秒预算,还会让 DSP 的可用性变成 ADX 的依赖。ADX 需要的映射要么提前存入自己的 KV,要么由 DSP 在自己的系统里完成。按场景分三种方式:

App 场景:透传设备 ID,由 DSP 自己映射

App 中的设备 ID(OAID、IDFA、CAID)对所有 SDK 相同。ADX 在 bid request 中直接携带设备 ID(OpenRTB 的 device.ifa 或扩展字段),DSP 在自己的系统中把它映射为内部用户 ID,ADX 不参与,也不维护各 DSP 的 ID。按 API 或 OpenRTB 接入的国内 DSP 基本采用这种方式。服务端竞价模式下,DSP SDK 生成的 token 已包含 DSP 自己的身份信息,同样不需要映射。

Web 场景:Cookie 映射,由用户浏览器完成

Cookie 按域名隔离:ADX 域名下的 cookie 是 ADX 的用户 ID,DSP 读不到;DSP 域名下的 cookie,ADX 也读不到。双方需要事先建立"ADX 用户 ID ↔ DSP 用户 ID"的对应关系,即 cookie 映射(cookie sync / ID sync)。它借助用户浏览器的跳转完成,不是服务器之间的调用:

1. 用户访问媒体页面,页面上的 ADX 代码加载一个 1×1 像素:
   浏览器 → ADX 同步接口(带上 ADX 域名的 cookie:adx_uid=A123)
2. ADX 返回 302,跳转到 DSP 的同步地址,URL 中带上 adx_uid:
   浏览器 → https://dsp.example.com/sync?adx_uid=A123(带上 DSP 域名的 cookie:dsp_uid=D789)
3. 两种存法:
   a) DSP 保存映射:DSP 在自己的库中记下 A123 → D789,流程结束
   b) ADX 保存映射:DSP 再 302 跳回 ADX:https://adx.example.com/back?dsp=7&dsp_uid=D789
      ADX 在自己的 KV 中记下 A123 → {DSP7: D789}
4. 之后竞价时:
   a) 方式:ADX 在请求中携带 adx_uid,DSP 自己查映射
   b) 方式:ADX 查自己的 KV,在 user.buyeruid 中填入 D789
  • 同步发生在用户浏览页面时,异步进行,与竞价请求无关,不增加竞价延迟
  • 不是每次都同步:映射有有效期,过期或首次见到该用户时才触发;同一页面一次只同步少数几家 DSP,避免拖慢页面
  • 映射覆盖率(match rate)不可能达到 100%:用户没访问过 DSP 的域名、清除 cookie、浏览器拦截第三方 cookie 都会造成缺失。Safari、Firefox 默认拦截第三方 cookie,Chrome 的相关政策也几经变化,Web 端 cookie 映射的可靠性在下降
  • 同步前需要取得用户同意,符合隐私合规要求(22.3)

服务端 ID 同步:离线批量交换

少数合作场景需要双方直接交换 ID,例如 DSP 要使用自己的人群包、ADX 需要知道对应的设备,或者 DSP 使用自己体系下的 ID。双方通过离线批量方式交换映射:定期交换加密或哈希后的设备 ID 文件,或调用对方的批量接口,结果写入 ADX 的 KV(7.5)。同样不在竞价时实时请求对方。

场景 做法 是否在竞价时请求 DSP
App 透传设备 ID,由 DSP 自己映射 否
Web 浏览器跳转完成 cookie 映射,映射存在 DSP 或 ADX 一侧 否(同步发生在页面浏览时)
特殊合作 离线批量交换映射文件 否

F.2 什么是 bid shading?行业转向一价拍卖后,出价会不会不断下降?如何规避?

bid shading(出价压低):二价拍卖中胜出者付第二高价,DSP 按真实价值出价就是最优策略。一价拍卖中胜出者付自己的出价,按真实价值 v 出价,赢了也没有任何剩余(v − v = 0),所以 DSP 会把出价压到真实价值以下:

最优出价 b* = argmax_b  (价值 v − 出价 b) × P(赢 | b)

出价越低,赢了之后的剩余越大,但赢的概率越小,最优出价在两者之间取得平衡。P(赢 | b) 是赢率曲线,DSP 要从历史竞价结果中估计。难点是数据带删失:赢了只知道胜出门槛不高于自己的出价,输了只知道门槛高于自己的出价(或从落败通知中得到胜出价区间),常用生存分析类方法建模。

出价会不会不断下降:理论上不会无限下降。每家 DSP 压价的同时也在与其他 DSP 竞争,压得太低就会输,最终停在低于真实价值、但不会趋近于零的均衡点;在对称、独立私有价值的理想条件下,一价与二价的期望收入相同(收入等价定理)。实践中出价持续下行的风险来自:

风险 说明
竞争稀疏 某类流量上只有一两家 DSP 出价,压价几乎没有代价
学习过程的正反馈 DSP 试探性降价后发现仍然能赢就继续降,竞争稀疏时会一路降下去
反馈过细 ADX 回传精确的胜出价,DSP 能精确压到"刚好赢"的位置,卖方剩余被挤压干净

ADX 的规避手段:

  • 提高竞争密度:流量筛选保证高价值流量上有足够多的 DSP 参与,接入更多需求方,引入 PMP / Deal
  • 底价:一价下底价的主要作用就是限制压价空间;出价分布明显下移的流量分段提高底价
  • 控制反馈粒度:落败通知只回传胜出价区间,不回传精确值
  • 监控:按 DSP、按流量分段跟踪平均出价、胜出价与第二高价的差距、出价率的长期趋势,异常下行时告警
  • 固定价交易:高价值资源用 PD / PDB 固定价售卖,不参与竞价

F.3 DSP 连接池"池大小 ≈ QPS × P99 × 1.5"指的是 TCP 连接数吗?为什么是这个公式?

是 一个 ADX 实例到某家 DSP 的 TCP 长连接数(HTTP keep-alive)。

HTTP/1.1 的一条连接同一时刻只能处理一个请求,所需连接数等于同时在途的请求数。按 Little 定律:

同时在途请求数 = 到达速率(QPS)× 每个请求占用连接的时间
  • 用 P99 而不是平均响应时间:保证 DSP 响应变慢时连接仍然够用;连接不够时请求要排队等连接,直接变成超时
  • 上界:请求到超时即被取消,最坏情况是 QPS × 超时时间
  • × 1.5:留给流量突发、实例间流量不均、连接重建的余量
  • 例子:单实例对某 DSP 2000 QPS、P99 为 150 ms,在途约 300 个请求,× 1.5 ≈ 450 个连接

池过小:请求排队等连接,超时率上升;池过大:浪费文件描述符和内存,占用 DSP 侧的连接配额,并可能逼近本机临时端口上限。

HTTP/1.1 下超时会导致连接重建:请求超时被取消时响应可能已在传输中,这条连接通常只能关闭,下一个请求重新做 TCP + TLS 握手,超时多的 DSP 会出现大量连接重建,延迟进一步变差。HTTP/2 在一条连接上用多个流承载请求,取消时只重置该流、连接保留,所需连接数约为 在途请求数 ÷ 单连接并发流数。对超时较多或 QPS 很高的 DSP,优先推动使用 HTTP/2。

F.4 预定向的位图是针对用户的吗?每个用户一个位图?

不是。位图由 DSP 的预定向配置生成,是与用户无关的静态索引,所有请求共享。

项 说明
构建时机 配置快照构建时生成一次
数量 每个"维度的取值"一个(如"省份=广东""系统=android""网络=4G"),总数 = 各维度取值数之和
长度 等于 (DSP, 代码位) 条目数,第 i 位表示条目 i 是否接受该取值
内存 例如 34 个省份 × 2000 个条目 ≈ 8.5 KB;不随用户数增长

每个请求按自己的属性值(广州、Android、4G……)取出对应维度的位图逐一按位与,得到一个临时的结果位图,为 1 的位就是可询价的 DSP 代码位,用完即丢弃。

唯一与用户数据有关的是人群维度:补全阶段从 KV 读出用户所属的人群包 ID 列表,把这些人群包对应的位图按位或,再参与按位与。人群包位图本身仍是按配置构建的静态数据。

F.5 流量屏蔽规则用在什么场景?如何在微秒级内按优先级匹配?

流量屏蔽规则由媒体、运营、合规方设置,决定"某类流量不出广告,或不出某些广告、某些 DSP"。它与预定向方向相反:预定向是买方声明想要什么,屏蔽规则是卖方声明不卖什么。

场景

场景 规则示例
应用商店送审 送审版本不出广告,或不出下载类广告,避免审核被拒
特殊时期 全国哀悼日全部屏蔽;重大活动、突发事件期间在指定城市屏蔽某类广告
地域合规 某些地区不出医疗、金融类广告
渠道合同 预装渠道、合作厂商渠道包不出第三方广告,或只允许指定 DSP
版本缺陷 某版本的某个样式渲染异常,修复前对该版本屏蔽该样式
用户体验 新安装用户首日不出开屏;未成年人模式屏蔽不适宜行业
竞品与商务 某些渠道或场景不出竞品广告主的广告

多条规则可能同时命中,例如全国性的"特殊时期全部屏蔽"与某 App 的"允许内部测试广告",由优先级决定哪条生效。

匹配方法:与预定向相同的位图思路,只是位图的每一位代表一条规则。

构建:规则按优先级从高到低编号(编号越小优先级越高)
      每个维度的每个取值建一个规则位图;规则在某维度"不限",该维度所有取值上都置 1
      版本区间按端点切段;时间段由后台每秒计算"当前生效规则位图"
匹配:生效位图 AND 城市位图 AND 渠道位图 AND App 位图 AND 版本段位图 AND ...
      结果中最低位的 1 = 优先级最高的命中规则(找到第一个非零 64 位字,再取尾部零的个数)

几千条规则时每个位图几十个 64 位字,全部按位与再加一次找最低位只需几百纳秒。进一步优化:按 App 预切分规则子集(规则数从几千降到几十);缓存"属性组合 → 动作"(key 带配置版本号);属性在网关阶段转为整数,匹配时只处理整数。