ADX 系统设计与技术方案
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:
-
-
-
-
-
response:
-
-
-
-
-
- 支持:路径映射、常量、枚举映射、单位转换、条件、数组展开、简单模板字符串
- 不支持任意脚本:映射规则必须可静态校验、执行时间可预测
- 规则在快照构建时预编译为字段访问函数表,运行时不解析字符串路径
- 平台提供"映射调试器":输入一条内部请求样例,实时显示映射后的 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 动态底价
动态底价按广告位 × 流量分段(系统、地域、时段、用户价值分层)设定。
方法:
- 离线统计各分段中最高有效出价的分布
- 以候选底价 f 模拟:
期望收入(f) = E[最高有效出价 × 1(最高有效出价 ≥ f)],同时考虑底价过高导致的填充率下降和用户体验成本 - DSP 会对底价做出反应,模拟结果只能作为初值,最终以 A/B 实验结果为准
- 按天更新,变动幅度设上限(如单日 ±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:
encoding: protobuf # json | protobuf
compression: gzip
endpoints:
-
-
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:
require_device_id: true
exclude_provinces:
ramp_stage: 100 # 放量档位(百分比)
circuit_breaker:
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 带配置版本号);属性在网关阶段转为整数,匹配时只处理整数。
暂无评论,欢迎留下第一条评论。