Skip to content
Charles Shao
Go back

Bidder 定向过滤:竞价漏斗最前端的一票否决

Updated:
–views

本文是 买侧竞价链路 系列的第 4 篇(定向过滤)。 全系列 9 篇:

  1. Bidder 竞价服务架构
  2. OpenRTB 协议精讲
  3. Bidder 解析层
  4. Bidder 定向过滤
  5. RTA 竞价前过滤
  6. Bidder 频次控制:五维配置与身份降级
  7. Bidder 频次控制工程篇
  8. Bidder 预算 Pacing
  9. Pacing 目标曲线

一句话定位:利用极致轻量的布尔匹配与查表机制在请求与候选双层面上果断剔除无效流量,将宝贵算力精准留给后续阶段。

总览篇将 Bidder 拆解为解析、定向过滤、召回、打分、出价、排序与填充七级漏斗,且上一篇已深挖了第一级解析(Parse)的机制。本篇将继续探讨第二级——定向过滤(Targeting Filter)。作为漏斗最前端、成本最低且削减流量最多的一道闸门,它严格奉行「能便宜地砍掉的流量,就别让它往下走」的原则。建议读者先阅读总览与解析篇再回到这里。

解析环节将原始字节流转化为带有 deadline 的内部请求对象 BidContext。拿到该对象后,Bidder 执行的第一项「减法」操作便是定向过滤。这一步的核心目的是判断当前广告曝光(Impression)是否应该参与实时竞价(RTB),以及库存中是否有合适的广告能够投放。需要强调的是,定向过滤不负责打分或出价,它仅仅在布尔逻辑层面上执行一票否决;正因如此,系统能够用最低廉的算力尽早剔除不合格的流量与候选广告,从而确保后续昂贵的召回与打分环节只作用于「有希望」的少数标的。

Bidder 定向过滤子流水线示意图:解析产出的 BidContext 先进入请求级 gating(反作弊 / IVT、黑白名单、合规同意、底价校验)对整次曝光做一刀切,通过后再进入候选级布尔匹配(地域 / 时段 / 设备 / 媒体 / 人群包求交)逐条筛广告,随后做可本地布尔判断的频控粗筛(用户级全局频控、已达标 campaign 位图预排除),产出候选集交给召回精排;请求整体不合格或无任何存活候选时立即快速失败返回 no-bid,不再进入召回;黑白名单、定向规则、合规策略由 Nacos 热更新下发 定向过滤子流水线(自上而下):请求级 gating(对整次曝光一刀切)→ 候选级布尔匹配(对每条广告求交)→ 频控粗筛(可本地布尔的那部分),产出候选集交给召回。蓝色为两级过滤主步骤,橙色为可前置的频控粗筛,黄色为产出的候选集,紫色为配置面(Nacos)热更新,红色为快速失败路径。

TL;DR

Table of contents

Open Table of contents

一、定向过滤为什么值得单独一篇

在总览中,漏斗的排布遵循着唯一原则:处理成本越低、能过滤流量越多的环节就越靠前。解析层仅仅是完成数据翻译与清洗的第 0 到 1 级过程,而定向过滤则是整条链路中第一个真正意义上「做减法」的环节。它直接决定了后续高度耗费算力的召回与打分阶段需要承载多少并发压力。具体而言,定向过滤具备三个极为特殊的工程属性:

与「召回」的边界:本篇讲语义,召回篇讲数据结构

在全景漏斗架构中,定向过滤(第 2 级)与召回(第 3 级)紧密相连,并且两者都在处理「将曝光上下文与广告定向条件进行求交」的核心逻辑。它们之间的明确分工如下:

模块定位定向过滤(本篇内容)召回引擎(后续篇章)
核心目标定义该不该投、确认匹配资格(业务语义)实现百万候选集中的毫秒级求交(工程算法)
侧重点定向维度、布尔语义、请求 gating、缺省策略倒排索引、RoaringBitmap 运算、Bloom Filter 截断
职责差异明确并输出「匹配」规则集合基于数据结构高速执行「匹配」动作

简而言之,定向过滤负责在业务逻辑上画出严格的匹配边界,而召回组件则致力于在极限的延时预算内,通过极致优化的数据结构来完成海量比对。本篇将在第八节略微触及底层工程实现,但关于内存索引等深层次的技术细节将留待召回篇进行专门探讨。

二、两类过滤:先请求级 gating,再候选级匹配

定向过滤在内部严格被划分为上下游两层清晰的防线,且在任何情况下执行顺序都绝不可被颠倒:

请求级与候选级两层过滤示意图:一次 Bid Request 先进入第一层请求级 gating,对整次曝光做一刀切——依次经反作弊 / IVT 初筛、黑白名单(媒体 / 域名 bundle / 地域 / 设备 / IP 段)、合规与同意(TCF consent / COPPA / 敏感类目)、底价校验(bidfloor 归一后连底价都够不上);请求合格后才进入第二层候选级布尔匹配,从百万级广告库出发与曝光上下文求交(地域 / 时段 / 设备 / 媒体 / 人群包),再做频控粗筛(已达标位图预排除),产出候选集 Top-N 交给召回 / 打分;请求整体不合格或无广告匹配时直接返回 no-bid,省掉整轮召回 两层过滤:第一层对整次曝光一刀切(请求级 gating),任何一条不过就整请求 no-bid、省掉整轮召回;只有请求合格,才轮到第二层对每条广告逐条布尔匹配(候选级)。先做最省的一刀切,是漏斗「越便宜越靠前」原则在这一级内部的再一次体现。

之所以必须坚持「先请求级、再候选级」的设计规范,是因为请求级判定只需一次轻量级的布尔检查便能直接阻断整个无效请求,成本低廉而过滤收益巨大;相比之下,候选级必须对成千上万条参竞广告进行复杂的特征集遍历与匹配,计算开销极其高昂。将成本最低且能够大面积削减流量的操作强行顶在架构最前线,正是对于整个竞价漏斗核心设计原则的最深刻践行。

三、请求级资格判定:先看这波流量值不值得竞价

请求级 gating 主要用于处理那些「针对整个广告曝光时刻成立且与具体广告主无关」的全局防御性判断。在实际业务中,我们最常面临的是以下四道核心防线:

3.1 反作弊 / IVT 初筛

在程序化广告生态中,不可避免地充斥着海量的无效流量(IVT),例如恶意爬虫机器人、来自数据中心的批量请求、恶劣的点击农场以及各种伪造的 App bundle 等。若对这类劣质流量盲目出价,将给平台带来纯粹且无法挽回的预算浪费。因此,在定向过滤系统的最前端会前置部署一层轻量级的防御初筛机制:一旦识别到请求源自已知的作弊 IP 段、数据中心 ASN,或是带有异常 User-Agent 及伪装特征,便会立即将整个请求丢弃。

需要特别说明的是,这里的「初筛」仅局限于能够通过本地内存进行极速布尔查表的判定逻辑。那些需要深挖用户行为序列、进行跨请求追踪或复杂模型推演的深度作弊识别,通常会被交由后端的离线或近线系统去处理,其最终分析结果会以黑名单集合的形式异步回灌至前端的本地缓存中(这完全契合了总览篇中提出的「底层状态靠回灌」的数据面设计理念)。关于全链路的 GIVT/SIVT 分类以及结算扣量机制,读者可以参阅反作弊与 IVT一文。

3.2 黑白名单

作为最简单却也最高频的一票否决手段,黑白名单在底层系统中的本质形态就是驻留在本地内存里的海量集合或位图字典。当请求抵达时,引擎仅需执行极为轻量的 O(1)O(1) 查表操作,即可迅速定夺生死:

随着平台业务板块的不断扩张,黑白名单的规模必然会呈指数级膨胀(具体应对策略详见第九节);但无论数据体量如何庞大,其判定过程都必须死守本地内存查表的底线,绝不能允许因为一次极其普通的黑名单校验而去发起不可控的远程网络调用。

3.3 合规与同意

这一层触及的是极其严肃的全球数据安全与法律合规红线。如果在这一环节出现漏报,平台将面临的绝不是「投放效果不佳」,而是可能导致毁灭性罚款的严重合规事故:

综上,合规判断不仅要求实现高度的配置化以灵活应对各地区朝令夕改的媒体政策,更要求保证强悍的可审计追溯能力,确保每一笔因合规引发的拒投操作都有迹可循。尤其在面对解析异常或 consent 信息直接缺失的混沌请求时,系统应当秉持最保守的避险原则——一律按「未授权」来做兜底处理,宁可主动错失流量曝光,也绝不触碰任何合规底线。

3.4 底价校验(bidfloor)

Bid Request 数据包中的 bidfloor 字段,代表了媒体卖方(Supply Side)对本次广告曝光设定的最低心理防线(即最低成交底价)。如果我们根据 campaign 设定的出价基准以及动态 pacing 系数约束算出的极限出价上限,仍然难以触达这一底价门槛,那么无论后续的特征召回与深度学习模型估算得有多么精准,这场拍卖都注定是一场必败的僵局。因此,系统应当在此时直截了当地做出 no-bid 决策,从而彻底切断整条流水线在无意义流量上的算力消耗。

这里必须强调一个极易被忽略、且与解析篇深度耦合的业务细节:鉴于 bidfloor 往往携带独立宣告的币种信息(bidfloorcur),校验时必须严格拉取并使用解析层统一转换后的系统内部币种作为基准。一旦在这一点上产生错位,系统就会荒唐地拿美元底价去跟人民币出价做比对,导致大量的优质流量被错误抛弃或低价劣质流量被错误吸纳。

总结来说,请求级 gating 所构建的这四道坚固防线(反作弊、黑名单过滤、合规审查以及底价核验)共同解答了一个决定系统生死的命题:面对汹涌而来的这波流量,我们究竟该不该下场参竞? 只要其中哪怕一条规则报警,系统便应以最快的速度启动 no-bid 逃生通道,漂亮地完成整个生态链路中最具性价比的一次流量抛弃。

四、定向维度分类:候选级匹配的输入

只有当那些优质的流量请求昂首挺胸地跨过了 gating 层的严酷检验,它们才有资格步入精密的候选级匹配环节。为了能在「曝光上下文」与「海量广告定向条件」之间展开严苛的求交运算,我们首先必须梳理清楚当前的定向体系究竟包含哪些关键维度。在商业化系统后台由广告主(Demand Side)自由配置的定向门槛,在落库后映射到 Bidder 的底层数据结构中,大致可以收敛为以下核心板块:

细分维度曝光侧特征(源自 BidContext 对象)广告侧门槛(campaign 定向配置)
地域 geo由底层 IP 或前端 GPS 定位实时解析出的国家、省份、城市体系允许进行投放的核心地域聚合圈(包含主动排除地域)
时段 dayparting当前请求进入网关时挂载的本地标准时间及对应星期特征限定允许进行预算释放的特定黄金时段(如强制工作日 9–18 点)
设备 / 物理环境OS 底层版本、具体机型、网络连接状态(WiFi/4G/5G)、运营商及系统语言面向目标用户群组精确筛选的设备体系、OS 版本及网络通道门槛
媒体 / 广告版位媒体应用分类矩阵、对应 App bundle 标识、广告位展示类型(banner/原生/激励视频)及尺寸明确要求放行或坚决实施排除的媒体及对应版位类型集合
人群包 / DMP 资产依托平台用户 ID 所实时关联召回的丰富人群标签组合库要求强匹配覆盖或者彻底剥离隔离的人群标签数据包
上下文内容特征用户所处页面或是应用端底层映射的细颗粒度内容分类及热搜关键词用于高度关联当前上下文语境的内容匹配参数设定

在理解上述各维度的匹配运算时,有两个贯穿系统始终的核心设计理念不可忽视:

与此同时,我们还必须警惕在这些日常维度上极易触发的口径隐患陷阱,并在全局配置层面建立不容挑战的一致性:

此外,尽管业务方通常习惯将频次控制视作广义上的「流量定向」手段之一,但鉴于其在系统内部的判定顺序有着极其独特的架构地位,我们将在第六节展开一场针对它的深度剖析。

五、布尔匹配的语义:最容易出错的地方

剥开技术封装的外壳,定向匹配在底层的真实面貌,就是一场紧张的高速布尔表达式求值战役:系统冷酷地将当前广告曝光捕捉到的各种上下文碎片特征代入到由万千广告主精心编制的定向条件树中,最终在树的顶端输出一个冷冰冰的布尔结果——要么是 true(准许下场),要么是 false(无情出局)。这套看似极其简易的防御逻辑,往往就潜伏着足以让整个商业化团队彻夜不眠、甚至导致巨额预算瞬间蒸发的神坑。

5.1 定向条件树:AND / OR / NOT 的组合艺术

单条广告的受众定向策略从来就不是单向的孤岛,它们往往是由错综复杂的多维条件彼此叠加交织而成。举个极具代表性的例子:「必须锁定(北京 OR 上海)的优质受众,AND(必须使用最新版 iOS 平台),AND(NOT 投放到任何已被标记排除的低俗媒体),AND(必须命中高价值人群包 X)」。在底层工程架构里,这套带有极强业务诉求的规则将被重构为一棵庞大且严密的布尔判定树,而随后的匹配动作便是对这棵树发起的穿透性求值。在这棵树上,存在着不可触碰的关键底线:

为了彻底撕开「不同维度协同 AND、同维参数横展 OR、黑名单严酷排除、大规模多值匹配运算以及关键节点熔断机制」这五重逻辑纠缠的技术外衣,我们不妨通过一个带着实战温度的样本案例来进行全景推演:

当前流量侧捕获的曝光上下文(由底层 BidContext 装载,已彻底通过全量归一化):
  geo      = CN-Shanghai
  os       = iOS
  media    = news.example.com
  segments = {high_value, sports_fan}     // 当前单一目标用户身上背负着多个重量级画像标签

针对广告标的 A 所建立的定向条件树矩阵:
  geo       ∈ {CN-Beijing, CN-Shanghai}       // 维度内的宽容 OR 放行机制
  AND os    ∈ {iOS}
  AND media ∉ {gambling.example.com}           // 针对高危违规媒体的 NOT 排除拦截令
  AND segments ∩ {high_value} ≠ ∅              // 针对标签资产大盘发起的硬核集合求交指令
  
执行严酷的逐维求值演算(一旦触碰 false 雷区即刻全线短路跳出):
  geo       CN-Shanghai ∈ {北京, 上海}          → true 通关
  os        iOS ∈ {iOS}                        → true 通关
  media     news… ∉ {gambling…}                → true 通关(完美避开排除禁区)
  segment   {high_value,sports_fan} ∩ {high_value} ≠ ∅ → true 通关
  ───────────────────────────────────────────
  系统裁定整体求值 = true  → 恭喜广告 A 成功挤入存活候选集!

现在我们将这个精巧的沙盘稍微做个逆向改动。如果在上述演练中,我们把 os 的特征字段强行更改为 Android,那么当探针在第 2 个维度进行求值判定时就会直接炸出 false 的火花,整个防御系统会随之触发强力短路,后面那两轮繁重的判定逻辑会被系统无情地直接丢进垃圾桶。借由这波精妙的短路阻断所硬省下来的算力,恰恰是整条候选级匹配链路中最为烧钱、最耗成本的那一部分(比如对海量人群数据包实施巨量的集合求交操作,亦或是启动极度消耗内存的深度内容语义比对模块)。这也极其生动地说明了为何要如此刻意地调配排布顺序:我们千方百计地将那些判定成本最为低廉、且一刀下去就能削掉海量无效废弃流量的维度关卡(如 geo 或 os 模块)极其霸道地钉在条件树的最前排卡位,其根本用意就是为了让这一波短路拦截机制能够尽早发威、抢占先机。

5.2 正向 vs 排除:警惕 NOT 逻辑的悄悄反转

在众多纠缠的判定规则中,排除定向(NOT / exclude)由于其天生带有强烈的业务反差感,历来是技术人员眼中最为恐惧的 bug 爆发重灾区。从业务运营的真实视角出发,「在全国除上海以外的所有地区进行疯狂轰炸」和「仅局限于在上海这一孤城内实施单点爆破」,这完全是两套南辕北辙的运营策略。然而,当这些策略转化为底层冷冰冰的代码架构时,它们之间往往仅仅是一行代码乃至一个简单的逻辑取反符之差。为了彻底锁死这只随时可能引发逻辑反转的恶魔,工程研发上必须采纳并死守以下极为苛刻的铁律:

为了把这条语义刻画得深入骨髓,我们将它摊开并彻底翻译成一张清晰见底的极简真值表(其中变量 P 代表正向集合,变量 E 代表负向排除集合,变量 v 代表当前汹涌而来的曝光流量在该特定维度上所携裹的具体特征取值):

正向集合 P负向排除集合 E达成命中的判定条件核心业务诉求含义
呈现非空状态呈现空白状态v ∈ P目不斜视,仅仅针对 P 进行死磕投放
呈现非空状态同样呈现非空状态v ∈ P 并且严格遵守 v ∉ E先大网捞鱼筛选入局,随后立即实施定点剔除
呈现空白状态呈现非空状态只要满足 v ∉ E向全流量池敞开大门进行无差别投放,唯独死守不让 E 沾边(此时正向设定为空白即代表不设任何限制)
呈现空白状态同样呈现空白状态结果恒为 true该维度的业务考核处于完全不设防状态(关于此类缺省语义所暗藏的致命隐患,务必详查 5.3 及 5.5 节的核心剖析)

在这套严酷的体系支撑下,正向集合呈现空白状态仅仅象征着「对流量敞开怀抱、不设任何限制」,而绝不能被系统曲解为「全盘封杀、道路不通」;而关于排除操作的处理准则,它在系统生命周期内永远只能是一次极其明确且伴随强烈目的性的「坚决减法」。只要我们的防线死死守住这两条神圣不可侵犯的底线,NOT 拦截逻辑就不会因为某处隐蔽分支上一个程序员漫不经心的手滑取反操作,而悄无声息地酝酿出一场灾难性的业务崩盘。

5.3 缺省语义:本篇中最昂贵的一个坑

「当业务运营同学在某个核心维度上压根就没有去设定任何定向条件时,这套庞大的自动竞价系统究竟该如何去应对并处理?」这个看似极其微小、微不足道的缺省语义(default semantics)设定,往往就是整个广告变现商业化定向过滤模块里,最最容易引爆大规模连锁血崩事故、且造成实际损失数额最为骇人的那个深坑:

从纯粹的代码设计角度来看,这两种截然不同的约定在自身的闭环逻辑内似乎都能做到无懈可击的「自洽」;但这有一个极其严苛且容不得半点沙子的先决条件:整个庞大而复杂的系统上下游链路必须随时随地保持着极度统一的口径步伐,并且必须要跟前端投放控制台那套复杂的交互语义实现严丝合缝的绝对对齐映射。那些在真实商业战场中造成真金白银巨额损失的线上灾难性事故,往往就如同隐形毒蛇一般潜伏在那些无人问津的系统边界接合处:当某一个业务维度在前端后台系统清晰地展示为「不限」状态时,这一指令在传递给深水区 Bidder 组件的过程中却意外地被解析成了一个空荡荡的「空集」;于是乎,原本被寄予厚望的「不限流量池畅游」特权,便在一连串阴差阳错间被系统的主脑死板地误判成了一道冷冰冰的「全线封杀、全面不匹配」的斩立决。这种深入骨髓的底层逻辑 bug 在事发时绝不会触发任何常规的系统异常报错机制,也不会在监控大盘上拉响任何关于超时拥堵的警报声;它只是宛如一个毫无存在感的幽灵,在黑夜中悄无声息地把公司的巨款投错了方向,或者干脆把预算死死地锁在金库里。这种级别的隐患极难通过常规粗犷的监控大盘在第一时间被捕获并察觉。为了死死守住这条攸关公司生死的商业底线,全体底层开发团队务必要在上线前夕疯狂地依靠真实的海量线上流量进行高压回放测试,同时辅以针对各类缺省语义组合的极端严密单测网络,来为其保驾护航。

5.4 匹配深度依赖于解析层的归一化质量

有一条不可逾越的底层红线必须在这里被再次大书特书:所有关于布尔匹配运算的最终正确性与精准度,均无条件、毫无保留地建立在系统所接收到的各个流量特征字段已被处于上游的解析层给完美剥离并成功完成了归一化(详见解析篇 2.2 节)这块最为坚固的基石之上。无论它是繁琐的全球地域编码字典、五花八门的移动设备与各类奇葩 OS 的版本枚举集合、还是错综复杂的全球法币换算单位,亦或者是必须收敛统一的结构化 User-Agent 字符串,都必须在传递之前做到近乎偏执的整齐划一。否则,对于业内最为常见的同一个「iOS」操作系统的判定,如果它在 A 家流量采买方(ADX)传输协议中被随意地标记为首字母小写的 iOS,而在 B 家大厂却被生硬地大写为 IOS,甚至到了 C 家又被别出心裁地拆解嵌套在 os=ios 这样的子结构里;那么,整个负责比对求交的匹配层就会在面对这堆烂摊子时直接精神分裂,导致最终的判定结果时而精准无误、时而荒谬绝伦。既然定向过滤模块从诞生之日起就不负责承担任何关于底层数据清洗与字段归一化的繁杂工作,那么一旦上游的解析层在这关键的一环上出现了哪怕一丝的松懈与渎职,整个处于下游的二级布尔匹配系统就会如同多米诺骨牌一般面临全线崩溃与彻底失真的毁灭性风险。

5.5 把「缺省」与「缺失」收敛进一张决策表

在错综复杂的实际运行环境中,我们在 5.3 节详细提及的「由于广告主疏忽而未设定条件」(即广告侧的逻辑缺省)与即将在 7.3 节隆重登场探讨的「底层曝光字段因故发生断档缺失」(即流量侧的客观数据缺失),完全是隶属于两个完全独立、互不干涉的正交维度概念。然而,命运就是这么喜欢开玩笑,这两者往往偏偏会在同一处关键的底层求值逻辑路口上发生迎面相撞,从而极易被那些疲惫不堪的程序员给稀里糊涂地混为一谈;进而在某些极其极端的特定场景组合之下,悄然无息地诱发一场令人防不胜防的「悄悄把钱投错」的大型灾难故障。唯有顶着巨大的痛苦,将这二者的各种排列组合强行平摊进一张宛如九宫格般严密的终极决策表之中,我们才能针对每一个单独的细分网格进行滴水不漏的严密规制约定:

情形分类剖析核心归属侧系统兜底核心约定法则该维度的系统求值终局结果
广告侧针对某维未下发定向限制归属于广告系统侧强行约定:未设即代表不设限敞开放行(这是强烈推荐的做法,但必须死死咬住并跟后台大盘对齐)系统裁定结果恒为 true
广告侧针对某维未下发定向限制归属于广告系统侧强行约定:未设即代表系统全盘封杀拒绝通行系统裁定结果恒为 false(这种做法极其容易引发整个系统层面严重的毁灭性欠投,需要极其慎重地酌情使用)
曝光流量某核心参数字段发生缺失断档归属于曝光流量侧极度保守退让策略:既然缺失即视同为高度不匹配结果判定为 false(此策略极度适用于那些对全球隐私合规法案以及顶级品牌安全要求极其苛刻、敏感到神经质的交易场景)
曝光流量某核心参数字段发生缺失断档归属于曝光流量侧激进出击吞量策略:哪怕缺失也视作无限制大胆放行直接按「不限制」逻辑强行一路大开绿灯放行(此策略极度适用于那些身背沉重 KPI、以极致抢量与扩大消耗规模为第一乃至唯一首要目标的野蛮竞价场景)

上述这两大核心维度的判定约定不仅需要各自保持绝对的独立性互不干扰,更要在系统的架构层面做到全面、显式的可视化配置下发能力。更为生死攸关的一点是,关于缺省语义的边界定义,必须要在「前端运营后台界面的无障碍视觉展示 = 后端编译定型时期的底层数据格式咬合 = 最终在线高并发运行时的毫秒级实时匹配」这三大决定生死的环节之中做到宛如铁桶一般的绝对无缝对齐(这也就是我们在配置落地篇的 3.2 节中反复强调、甚至于在编译期就要将「未设」这个混沌概念死死钉在具体的集合快照里的深层设计初衷所在)。一旦在运转时出现了令人抓狂的偏差:比如某维度在光鲜亮丽的后台管理系统上明明清晰地显示为「不限」绿灯,但在底层数据编译流转时却被错误地映射成了一个毫无生气的空集,最终在最底层的在线运行时判定中又把这个该死的空集给离谱地误判成为了一道「全线封锁拦截、全流量不通」的死亡通牒;那么,这条看似完美的广告链路就会在毫无征兆的情况下遭遇诡异的零投放困局,且令人绝望的是,在这个过程中整套系统将不会留下一行任何有价值的报错或者崩溃日志;这,恰恰也就是我们在前文 5.3 节中不厌其烦地描述过的、那个最为昂贵且最让顶级排障专家都感到头疼欲裂的灾难级事故类型。

六、频次控制的位置探讨:为何不能全放最前端

在总览体系的 3.2 节中,我们曾颇有心计地埋下了一个意味深长的伏笔:频次控制(Frequency Capping,其在业务上的核心诉求主要用于限制「单一独立用户针对某一条特定广告物料或者巨型 campaign 所遭受的总曝光洗礼次数」)从表面上粗略地看过去,似乎也能够被归为一种纯粹意义上的定向过滤筛查手段;基于这个逻辑,它理应与那些冷冰冰的黑白名单一道被极其自然地放置在整个宏大竞价系统的最前沿防御端。然而,在这个充满妥协的现实技术世界里,真正深入到极致精细化层面的频控校验逻辑却由于底层物理架构的掣肘,根本无法实现所谓的全面彻底前置。本节将毫不留情地彻底讲透这一妥协式架构设计背后所隐藏的那些令人着迷的顺序考量。

频次控制的位置示意图:左侧一组是能前置到定向过滤的频控(纯本地布尔、不依赖具体候选)——用户级全局频控(该用户今日已达总曝光上限则整请求丢弃)、已达标 campaign 的位图预排除(本地位图标记已封顶广告,求交时直接剔除);请求依次经这两步后进入召回(倒排 + 位图求交)产出存活候选;右侧一组是必须在召回之后才能做的精细频控——per-user×per-campaign 逐条封顶(查该用户看过这条广告几次,常需带超时的 Redis 点查)、随排序闸门与预算 / pacing 闸门并列作为付费前最后一道;一条注释说明为何不能全放最前:精细频控需要具体候选加用户历史,召回前无候选、也不宜为每次请求都远程点查,最后胜者进入填充响应 频控架构的两半分野:左半部分(如用户级全局频控与已达标位图预排除)因不依赖具体候选,可依托纯本地布尔判断进行前置;右半部分(即 per-user × per-campaign 的精细封顶)则必须依赖「具体候选 + 用户历史」,因此只能置于召回之后随排序闸门一并执行,且常需发起带超时的 Redis 远程点查。

6.1 能前置的部分:不依赖具体候选的粗筛

以下两类频控机制由于只需进行简单的本地内存布尔状态判断,因此完全可以理直气壮地置于定向过滤体系的最前端阵地:

6.2 必须后置的部分:per-user × per-campaign 精细封顶

那些真刀真枪、触及用户核心体验底线的精细频控逻辑,其唯一核心旨在精确地解答「这一个被锁定标识的特定用户,对于这一条独立投放的具体广告或者是这一个细分的 campaign,到底已经在他的屏幕前被迫或者主动地看了多少次」。这种细到了骨头里的微观粒度判定之所以在物理与逻辑层面上均无法实现前置拦截,主要是因为其在架构层面上受制于两个根本无法跨越的硬性物理约束:

6.3 与预算闸门的架构共性类比

其实,如果我们能够跳出代码细节的桎梏,去宏观俯瞰,就会惊奇地发现精细频控体系与主宰着生死大权的预算消耗闸门,在系统庞大的定位版图中竟然属于如假包换的同构双生组件:它们二者皆是「在系统真正下达最终的正式付费扣款决策之前,针对那些在重重漏斗中幸存下来的极少数头部候选者进行逐一、逐条残酷施压,并且高度乃至完全依赖于外部极度不稳定、随时发生可变状态跳跃的最后一道终极核查关卡」。这也极其精辟地解释了,为什么在本文所构建的总览全局宏大架构之中,我们会如此顺理成章、毫不违和地将它们这两大金刚一起死死地捆绑合并放置在「排序 + 预算 / 频控联合制约闸门」那至高无上的最后一级战壕里(即总览篇 3.6 节)。与此同时,这两大在战壕里共同作战的超级组件也都无可避免、无一例外地面临着分布式系统环境下状态一致性这一令人绝望的技术梦魇;因为它们的每一次微小的计数增加与扣减操作,都被无情地打散并分散在那上千个各自为战的计算节点实例之中,且来自于用户客户端最真实的实际曝光打点回传数据总是不可避免地夹带着令人抓狂的天然网络延迟。针对这类折磨了无数顶级架构师的终极难题,业界所给出的通用妥协解法在其底层的哲学思辨上也是高度趋同、如出一辙的(即无一例外地采用了依靠本地节点进行近似模糊计数累加 + 依赖后台异步队列进行远程数据批量回灌 + 并在商业逻辑上容忍极少部分超出预算上限损失的保底止损策略)。关于这套复杂且精密的分布式频次控制核心数据在一致性层面如何展开绝地深潜的技术细节,本系列文章将会在后续精心规划的专门篇章中进行单独且深入地立项与深度探讨。

综合所有线索来看,针对整个频控战略大盘的落地策略必须做到界限分明:对于其中那些「与单一候选实体彻底解绑无关、且能够完全依赖本地极速布尔运算进行实时判定」的轻量级部分,理应不惜一切代价尽最大可能将其推向最前线去狂野地削减宏观流量大盘的冲击压力;而对于那些「深度耦合了 per-user × per-campaign 颗粒度且必须要去深挖溯源历史账本」的沉重且精细的查算部分,则必须在架构上保持绝对的克制,坚定不移地将其向后撤退、稳稳地安置并伴随最终的计费结算闸门同步执行。若是任何一个不懂敬畏的架构新手试图强行将后者这头怪兽塞入防线的最前端,其下场要么就是会在逻辑运算链条上陷入因为根本捞不到候选物料而面临无靶可打的死胡同僵局,要么便会以极其惨烈的代价提前且毫无防备地引爆那足以摧毁整个 Redis 集群根基的点查风暴雷场。

七、快速失败、缺省策略与系统热更新

7.1 快速失败原则:一旦无候选立即 no-bid

在这套层层剥茧、刀光剑影的定向过滤修罗场中,系统每向前迈出一个极为微小的细分裁决步伐,那个原本庞大的候选广告池都有可能在瞬间遭受血洗并被彻底清空为零。一旦在漫长过滤链条中的某一个残酷环节正式结束哨音吹响之后,系统探针赫然发现原本的池子里已经不存在任何存活的竞价候选目标,整个系统的主控大脑必须在第一时间、立刻且毫不迟疑地向上游强行返回 no-bid 放弃出价指令;它绝不应该、也绝对不能够再顶着巨大的资源浪费继续向前推进并驶入极其消耗内存与 CPU 的召回或是打分算力黑洞。这一理念,其实与前一篇章中所重点剖析的解析层(Parse)所死死捍卫并奉行的「只要非法格式即刻强行拒收斩杀」的防御理念可谓是遥相呼应、一脉相承:系统如果能够在错误的道路上失败得越早、斩断得越快,那么处于整个庞大漏斗下游各个关节节点上那些被无谓挥霍掉、原本可以用来赚取超额利润的宝贵算力资源就将被节省得越多。如果说我们在最前沿的请求级 gating 防线本质上就是在手起刀落地执行着干脆利落的「整盘请求无差别 no-bid 大扫除」,那么在度过了候选级匹配战役后若是发现战壕里空无一人、再无幸存者,那么其最终的操作结果同样也就是殊途同归的「整盘请求全军覆没式 no-bid」。为了能够在事后进行精准无比的数据归因与商业化转化率回溯复盘工作,在代码架构的出口端,这两处用于承担快速失败保底机制的泄洪通道出口均需要被开发人员极其严谨地打上清晰、明确且毫无歧义的专有无出价原因识别状态码(NBR 标识序列)。

7.2 过滤组件自身必须保持极致轻量

在这个追求极致性能的竞价核心地带,经常会像幽灵一般盘旋着一个极易被粗心的开发者所直接无视的致命工程悖论:整个过滤机制之所以被创造出来的最核心初衷,就是为了替公司省下巨额的后续复杂算力成本,然而,过滤操作这件事情的本身,同样是一只会不断吞噬并消耗着机器性能成本的凶猛怪兽。如果在系统的执行过程中,我们仅仅是为了去完成一次极为普通的布尔真伪判断,而去盲目且不可控地向外部模块疯狂发起带有网络损耗的远程 RPC 调用风暴、在极其宝贵的内存堆栈中肆无忌惮地构建那些体量惊人的臃肿临时对象实体、亦或是脑洞大开地引入了某种极其复杂且耗费 CPU 周期的非线性计算依赖图,那么这种做法就等同于彻底、完全地背叛并违背了整个漏斗架构那条至高无上的「处理成本能够降得越低就一定要排在越靠前位置」的原始架构设计哲学与初衷底线。为此,所有参与该核心模块研发的开发人员,都必须如同信仰教条一般死死坚守并捍卫以下的三大铁血原则底线:

7.3 面对数据缺失时的博弈:保守还是激进?

在那些每秒钟都席卷着海量金钱与数据的真实线上流量洪流冲刷之中,承载着核心数据的 BidContext 对象里面的某些关键核心字段在流转过程中发生各种诡异的断档与缺失,早已经是家常便饭般的实属常态操作(引发这些断档的元凶可能是上游流量发送方在传输通道中发生了物理丢包,也有极大的概率是因为我们的解析层在面对某些不规范畸形数据包时彻底宣告罢工、未能成功解出数据)。在面对这种突如其来的数据真空危机时,当前的这波匹配判定究竟应当被系统的大脑直接判处死刑宣告为未命中,还是应该被网开一面判定为成功命中?这,绝不是一个可以依靠程序员在写代码时的直觉去拍脑袋决定的微末细节,而是必须要在一个庞大系统被设计的最初期,就必须以决不妥协的姿态去明确定义并彻底死死固化下来的防御策略准则。开发团队绝不能把这种关乎公司存亡的商业判定权,草率地交给底层代码在面对 null 指针时所做出的那种充满偶然性的瞎猫碰死耗子般的灾难性巧合行为去随意决定:

面对这种灵魂深处的业务拷问,行业里压根就不存在那种放之四海而皆准的万能标准答案。但所有系统架构的底线与共识却异常明确且刺眼:在同一个系统架构内部,必须要采取全盘统一、白纸黑字显式的契约约定法则,不仅要能够做到在系统全局视角下的高度可控与可视化配置能力;并且必须要与前文我们曾在第五节中耗费大量笔墨探讨过的广告主缺省语义体系,去彻底地划清不可逾越的楚河汉界。「广告主在投放系统上由于失误而未设定向条件」这完全是属于广告管理系统这一侧的锅,而「底层曝光请求流量包在传输过程中发生了核心字段信息的脱落缺失」则完完全全是属于曝光流量对接链路这一侧的技术事故,如果有人敢把这两者给混淆揉捏在一起,那无疑是在给系统埋下一颗足以炸穿整个交易平台的超级核弹。

7.4 规则的热更新机制:全面接入 Nacos 下发

在充满硝烟与变数的实际商业变现业务运营战线之上,那些关乎生死的黑白拦截名单、极其复杂的定向逻辑门槛规则、犹如走钢丝一般的隐私合规审查策略,乃至能够左右收益大盘的频控阈值水位线,往往全都处于一种令人窒息的高频变动与不断调整的状态之中。面对这种随时可能发生颠覆性修改的业务常态,我们哪怕是用大脚趾头去想,也绝不可能容忍因为一次业务上极其微小的简单规则参数微调,就逼着开发人员去兴师动众地执行一次惊天动地的代码重启,或者是去走那漫长且充满风险的重新发版上线部署服务进程流程。正如我们在此前剖析过的处于上游的解析层组件深度依赖于配置中心来下发那些灵动的适配解析规则一般,上述我们所罗列过的所有的这套庞杂过滤防御策略集群大礼包,均必须、且毫无商量余地地通过业界成熟的分布式统一配置中心(诸如 Nacos 这样的超级利器)来进行毫秒级的实时热更新全网推送下发;并且必须从架构的底层就强力确保在单一计算进程的内部隔离区里,能够如同教科书般完美地实现原子化级别的一键式无缝切换。这等同于又一次在另一个维度上完美地呼应了我们在那篇宏伟的系统总览篇中所重点强调、不厌其烦去宣导的那条「一切关于缓存核心数据的更新替换都必须强制保障做到绝对的原子化切换」的神圣设计原则底线。因为在我们的眼中,一旦在高速运转的生产环境系统中出现了那种极其可怕的、犹如怪胎般处于半新半旧、割裂交替状态的混合规则大杂烩体系,那势必会在瞬间引发整个线上大盘在面对同类流量过滤行为决策时的集体精神错乱,导致前后判决规则逻辑发生极其惨烈的不一致坍塌灾难。

八、工程实现要点(点到即止,深入留给召回篇)

既然本篇聚焦于业务语义,对于工程实现层面我们仅列出几条不可妥协的关键约束。更硬核的数据结构与算法深挖,我们将留待后续的召回篇进行详细解读:

九、生产取舍与常见事故盘点


行业黑话总结:定向过滤(Targeting Filter);请求级 gating;候选级布尔匹配;正向与排除定向;缺省语义(default semantics);时段定向(dayparting);人群包定向;频次控制(Frequency Capping);位图预排除;快速失败机制;无效流量(IVT)。

定向过滤定义了「什么叫匹配」,而如何在大规模数据集中以毫秒级延迟实现这一匹配逻辑,则需要依赖极速的倒排索引与位图运算体系。这也正是召回引擎架构解析所要为您解答的核心命题。

延伸阅读

规范与一手资料:


–views
Share this post on:

Previous Post
Bidder 频次控制:五维配置与身份降级
Next Post
Bidder 解析层:将原始竞价请求转为内部可算对象