☰
广告RTA实时决策:链路架构、容量估算与降级实践
2026/10/29 15:45:18 网站建设 项目流程

1. RTA 在广告系统里到底站在哪个位置

先把结论摆到最前面:RTA(Real-Time API)是媒体侧在发起一次竞价之前,向广告主实时发起的一次“要不要这个流量”的问询。广告主在几十毫秒内返回一个参与或不参与的决策,媒体侧拿到这个决策之后,再决定这次流量是否进入后面的排序和竞价环节。它不是出价,不是创意,也不是人群包,它是竞价链路上游的一道实时闸门。

我第一次接触这套机制是在一个投放成本压不下来的项目里。当时广告主手里有一份高价值用户名单和一份已经成交过的老客名单,想把老客排除掉、把高价值用户捞出来加价,但这两份名单每天都在变,做成人群包推到媒体侧,最短也要几个小时才能生效,长的时候隔天。结果就是预算持续花在已经成交过的用户身上,重复曝光,ROI 一路往下掉。后来改成 RTA 接入,媒体侧每次竞价前带着脱敏的设备标识来问一句,我们这边查一次自己的库,返回“这个不要”“这个要,而且加价 1.2 倍”,当天就把无效消耗砍掉了将近三成。

所以这篇文章适合三类人看:一是做广告系统后端、要对接媒体侧 RTA 接口的研发;二是做投放策略、想知道 RTA 到底能解决什么、不能解决什么的优化同学;三是数据侧、需要评估 RTA 接入后效果怎么衡量的人。无论你基础如何,读完至少能搞清楚这条链路长什么样、参数怎么估、坑在哪里。

1.1 先说清楚它和 RTB、DMP、人群包的区别

这几个词经常被混着用,实际职责完全不同。我用一个生活化的类比:把广告投放想象成一场招聘会。

  • DMP 和人群包是提前做好的简历库,招聘方在开场前把候选人分好类,贴好标签,但标签是静态的,今天贴完明天人可能就变了。
  • RTB是现场竞价,多个招聘方同时对一个人出价,价高者得。
  • RTA是在竞价开始之前,招聘方先看一眼这个人的简历,说“这个人我不面了”或者“这个人我很想要,我愿意加预算”。

三者的顺序是:RTA 决策在前,RTB 竞价在后。RTA 说不要,后面的竞价根本不会发生,这就省下了大量的无效竞价成本。

维度RTARTBDMP/人群包DPA
触发时机竞价前竞价中提前离线竞价中
决策方广告主媒体侧撮合广告主/媒体媒体侧
延迟要求极严,通常几十毫秒严,百毫秒级无较严
数据流向媒体问、广告主答出价上报广告主上传商品库拉取
典型用途老客排除、高价值加价、风控价格竞争定向圈选动态创意

看懂这张表就明白了一个关键点:RTA 的价值不在于“更精准”,而在于“更实时 + 数据不出域”。人群包也能做精准,但它必须把名单交出去,而且更新慢;RTA 是名单留在自己手里,媒体只带着一个标识来问,问完就走,不留存。

1.2 为什么静态定向包顶不住现在的投放节奏

我在几个项目里反复验证过一个现象:一个跑得不错的人群包,生命周期通常在 7 到 14 天之间开始衰减。原因不复杂。

第一是数据滞后。人群包从广告主侧导出、上传、媒体侧审核、生效,链路走完少则几小时多则一天。对于电商大促、游戏开服、本地生活这类节奏极快的场景,一天的滞后意味着策略完全失效。

第二是粒度太粗。人群包本质上是布尔值——在或不在。但真实的业务判断往往是连续的:“这个用户是我的高价值用户,但今天已经买过了,先别推”“这个用户是沉睡用户,可以给一个高一点的出价试试”。这种带条件的、带权重的判断,静态包表达不了。

第三是数据合规压力。名单出域这件事本身就有风险,涉及的字段越多、越敏感,审核越难过。RTA 恰好绕开了这个问题:媒体侧只发一个哈希后的标识过来,广告主在本地完成匹配,原始数据一步都没出门。

注意:RTA 返回的是“要不要”和“加不加价”,它不参与最终出价计算。最终成交价仍然由媒体侧的竞价机制决定,RTA 只影响你能不能进这个场、以及进场时的心理价位。

1.3 三类最典型的接入场景

不是所有投放都需要 RTA。我梳理下来,真正值得投入研发资源接入的,基本是这三类。

第一类是新老客分层。广告主的成交数据在自己手里,媒体侧不知道谁是老客。把老客排除掉,把预算集中投给真正的新用户,这是最刚需、收益也最直接场景。我见过最夸张的一个案子,接入前有 40% 的曝光打在了已成交用户身上,接入后这个数字降到 8% 以内。

第二类是价值分层加价。同一个广告位,对不同用户的期望价值差异可能有好几倍。RTA 支持返回一个出价系数,广告主可以对自己模型打分高的用户返回一个大于 1 的系数,对低价值用户返回小于 1 的系数,甚至直接拒绝。这相当于把自己的一手数据变成了竞价优势。

第三类是风控与反作弊。某些行业会遇到恶意点击、批量注册、羊毛党。这类流量从媒体侧的画像上看不出异常,但广告主自己的风控库里有记录。用 RTA 在竞价前拦掉,比事后申诉有效得多。

2. 一次 RTA 请求里到底发生了什么

理解这条链路,最好的方式是拿一次真实的曝光请求顺着走一遍。

用户在客户端刷新信息流,媒体侧的广告服务开始处理这个请求。它先做基础的定向过滤,把不符合条件(地域、时段、频次)的广告计划筛掉,剩下一批候选计划。这时候,凡是配置了 RTA 的计划,媒体侧会把这些计划的 RTA 地址和当前流量的信息打包,并发发出去。每个广告主各自的接口独立响应,互不影响。回来的结果被合并成一张“准入表”,只有返回“参与”的计划才进入后面的粗排、精排、竞价环节。

整个 RTA 环节的耗时预算通常被压在30 到 80 毫秒之间,部分媒体给的窗口更窄。这个数字很关键,它决定后面所有的架构选择——为什么必须做缓存、为什么必须做超时降级、为什么不能在里面跑复杂查询。

2.1 请求里都带了哪些字段

不同媒体侧的字段定义不一样,但核心结构高度一致。下面是一个典型的请求体,字段名我做了抽象,实际对接时以对方文档为准。

{ "request_id": "a1b2c3d4-20240612-0001", "timestamp": 1718150400123, "ad_slot_id": "feed_slot_03", "media_user_id": "8f14e45fceea167a5a36dedd4bea2543", "device": { "os_type": "android", "os_version": "13", "network": "wifi", "carrier": "unknown" }, "context": { "province": "310000", "city": "310100", "hour": 21, "content_category": "sports" }, "media_tags": ["high_active", "shopping_intent"], "plan_id": "plan_88213", "signature": "xxxxx" }

几个字段值得单独说。

media_user_id是媒体侧对用户的稳定标识,通常是经过哈希或加密的,广告主拿到后要用同样的算法做一次映射,才能和自己库里的用户对上。这里有个坑:不同媒体的 ID 体系完全不通,同一个用户在不同平台上的标识毫无关系。所以广告主侧一般要维护一张映射表,把各家的 ID 映射到自己的统一 ID 上。

request_id是排障的生命线。线上出问题时,拿着这个 ID 能一路追到日志,没有它基本没法定位。

media_tags是媒体侧附带的标签,通常是粗粒度的活跃度、兴趣分类。这些标签能用,但不要过度依赖,一是粒度粗,二是各家定义不统一。

2.2 返回值的语义比想象中更微妙

最简化的返回就是一个布尔值,参与或不参与。但实际对接中,返回值往往包含更多信息。

{ "request_id": "a1b2c3d4-20240612-0001", "code": 0, "participate": true, "bid_ratio": 1.25, "custom_creative_id": "creative_778", "ttl": 300 }

bid_ratio是出价系数,媒体侧会用这个系数去调整这个计划在本流量上的出价。系数怎么定,后面第 4 节会讲。

ttl是缓存有效期,单位秒。这个字段很多人忽略,但它对降低 QPS 峰值极其重要。返回 300 意味着同一个用户在这 5 分钟内的后续请求,媒体侧可以直接复用这次的结论,不必再打过来。合理设置 ttl,能把实际请求量压掉一大半。

提示:bid_ratio不是越大越好。我给的建议是新接入的广告主先只返回 0 或 1,跑一周把链路跑稳,再加系数逻辑。一开始就上复杂的系数策略,出问题的时候你分不清是链路问题还是策略问题。

2.3 超时了怎么办:降级策略决定成败

这是整个 RTA 设计里最容易被低估的部分。广告主的服务不可能永远健康,网络抖动、发布重启、下游存储慢查询,任何一环出问题都会导致超时。超时之后怎么处理,必须在接入前就和业务方定死。

两种降级方向,各有代价:

降级策略含义好处风险
默认参与超时当作“要”不丢量,投放不断可能大量无效消耗,成本失控
默认不参与超时当作“不要”成本可控丢量,量级断崖式下跌

怎么选?我的经验是按投放目标分。以成本为第一目标的计划,用默认不参与,宁可少投也别乱投;以起量为第一目标的计划,用默认参与,先保住曝光,事后通过报表把无效流量找出来。两种策略可以按计划配置,不必全站一刀切。

另外还有一个更细的做法:按超时类型区分。如果是连接超时(网络层问题),大概率是整体故障,走默认不参与;如果是读超时(对方处理慢),说明服务活着只是慢,可以走默认参与。这个区分需要媒体侧支持,不是所有平台都给,但对成本控制很有价值。

3. 架构选型:自建、托管还是标准化方案

确定要接 RTA 之后,第一个决策是这套服务怎么落地。我见过三种做法,各有适用面。

3.1 三种部署形态的实际差异

自建独立服务:自己写接口、自己部署、自己扛容量。优点是完全可控,想加什么逻辑就加什么逻辑,用户数据完全在自己机房;缺点是从零搭高可用架构,人力投入不小,一台机器扛不住的时候扩容、限流、监控全都要自己做。

托管在云上的函数服务:把决策逻辑写成一个函数,流量来了自动弹性扩容。上手快,前期几乎没有运维负担;但冷启动延迟是个隐患,第一次调用可能要几百毫秒,这在 80 毫秒的窗口里是致命的。要么做预热,要么用预留实例,成本会上去。

用媒体侧提供的标准化接入工具:部分平台会提供一套标准化的对接组件,广告主只需要填充决策逻辑。接入成本最低,但灵活度受限,复杂的多条件判断、多数据源联查往往表达不了。

形态上手成本延迟可控性灵活性适合规模
自建服务高高高中大型,日均请求千万级以上
云函数低中(需预热)中中小型,波峰波谷差异大
标准化组件很低高低验证阶段、简单规则

我的建议是分两步走:先用标准化组件或最小自建版本跑通链路,验证业务价值,再决定要不要投入做完整的高可用架构。很多人一上来就设计一套复杂的微服务,结果业务侧发现收益不及预期,架构白做了。

3.2 容量怎么估:一步一步算给你看

这一步最容易被拍脑袋。我见过有人按“日活乘个系数”就报了机器数,上线当天直接被打挂。正确的做法是从曝光量往回推。

假设某广告主日均曝光 1 亿次,媒体侧的平均竞价倍率是 3(也就是一次曝光背后平均发生 3 次竞价请求),RTA 挂在计划层级,平均每个请求要问 1.5 个 RTA 接口,那么:

日均 RTA 请求数 = 1亿 × 3 × 1.5 = 4.5 亿次 平均 QPS = 4.5亿 / 86400 ≈ 5208

再看峰值。信息流类流量的峰谷比通常在 3 到 5 之间,晚间高峰集中。取 4:

峰值 QPS ≈ 5208 × 4 ≈ 20800

单机处理能力取决于你的实现。一次请求如果只是查一次本地缓存加一次 Redis,单核轻松跑到 2000 QPS 以上;如果每次都要查一次数据库,单机可能只有 300 到 500 QPS。按查 Redis 估算,单机 8 核保守取 8000 QPS:

所需机器数 = 20800 / 8000 ≈ 2.6 → 取 4 台(留 50% 余量)

这套算法比拍脑袋靠谱得多。但要注意两个修正项:一是 TTL 缓存会显著降低实际请求量,上面算的是理论上限;二是突发流量,比如某个内容突然爆了,瞬时 QPS 可能是均值的十倍以上,这时候靠限流和降级兜底,不能靠堆机器。

3.3 高可用设计:三个必须做的动作

第一,多可用区部署。单机房的风险不用多说,一次网络割接就能让投放停半天。至少两个可用区,媒体侧配置多个回调地址做容灾切换。

第二,限流要在入口做。不要指望下游扛住所有流量。在接口层设置令牌桶,超过阈值的请求直接走降级返回,把压力挡在业务逻辑之外。限流阈值按压测结果的 70% 设定,留出缓冲。

第三,监控要盯住延迟分位数而不是均值。均值 20 毫秒看着很健康,但 P99 可能是 300 毫秒,意味着每 100 次请求就有 1 次超时。RTA 这种场景,P99 才是真实体验。我一般会同时看 P50、P95、P99 和超时率四条线。

4. 从联调到稳定运行:完整的落地流程

链路设计和容量都定了,接下来是实操。这一段我按时间顺序讲,把每个阶段的动作和容易踩的坑都说清楚。

4.1 接口契约必须白纸黑字写下来

对接之前,一定要和媒体侧把这几件事写成文档,口头约定不作数。

超时时间:明确写死,比如 60 毫秒。不要用“尽快”这种描述。

返回码约定:0 表示成功,非 0 的每一种都对应什么含义,媒体侧收到非 0 时怎么处理,全部列清楚。我遇到过返回码没定义全,广告主返回了 1,媒体侧当成“不参与”,导致全天零曝光的事故。

幂等性:同一个request_id重复请求,返回值必须一致。这个在媒体侧重试时很关键。

鉴权方式:签名算法、密钥轮换周期。密钥一定要支持双活,否则轮换那一刻全部请求失败。

字段兼容:新增字段不能影响老逻辑,缺失字段要有默认值。这条特别重要,媒体侧随时可能加字段。

# 一个极简的决策函数骨架,重点看异常处理 def decide(req): try: uid = map_to_internal_id(req.get("media_user_id")) if not uid: return {"code": 0, "participate": True, "bid_ratio": 1.0} profile = cache.get(uid) if profile is None: profile = load_profile(uid) # 兜底查库,需限流 cache.set(uid, profile, ttl=300) if profile.is_purchased_recently: return {"code": 0, "participate": False} if profile.value_score > 0.8: return {"code": 0, "participate": True, "bid_ratio": 1.2} return {"code": 0, "participate": True, "bid_ratio": 1.0} except Exception: # 任何异常都不能抛出去,必须返回一个合法结构 return {"code": 0, "participate": True, "bid_ratio": 1.0}

这段代码里有两个细节值得强调。map_to_internal_id查不到时返回参与而不是拒绝,因为 ID 映射覆盖率很难做到 100%,全拒会丢大量正常流量。最外层的except必须兜住,任何未捕获的异常导致返回体不合法,媒体侧可能直接判定整批请求失败。

4.2 联调阶段要做的四件事

造流量。媒体侧一般会提供测试流量或模拟请求工具。如果没有,就自己按字段定义拼请求,覆盖正常用户、未知用户、缺失字段、超长字段、非法编码这几种情况。

压测。按估算峰值的 1.5 倍压,观察 P99 和错误率。压测时特别注意下游依赖,缓存和数据库的连接池很容易先被打满。

对账。把媒体侧记录的请求量和自己侧记录的请求量做比对,差异超过 1% 就要查。常见原因是重试和缓存命中导致两边计数口径不一致。

灰度。这一条单独展开说。

4.3 灰度节奏:宁可慢一点

我给的建议节奏是 1%、5%、20%、50%、全量,每一档至少观察 24 小时,看四个指标:请求量是否符合预期、超时率、参与率、以及投放侧的消耗和转化变化。

任何一档出现异常就回退,不要硬扛。有一回我们在 5% 那档发现参与率高达 96%,看着像是好事,仔细一查是 ID 映射覆盖率只有 4%,剩下 96% 全都走到了“查不到就放行”的默认分支,等于 RTA 根本没生效。如果不是分档灰度,这个问题会一直藏着。

注意:灰度期间不要同时改投放策略和 RTA 逻辑。两个变量一起动,出问题无法归因。这是我在项目里吃过亏的地方,一次改动引入两个变化,排查花了整整两天。

5. 效果怎么衡量:指标体系和常见误判

RTA 上线之后,最难的不是技术,是回答“它到底有没有用”。这一节讲指标怎么设,以及几个很容易踩的统计陷阱。

5.1 该盯的核心指标

技术侧和业务侧的指标要分开看。

技术侧四个:请求量、P99 延迟、超时率、参与率。参与率的绝对值没有意义,要看它的变化趋势和分群差异。如果某个用户群参与率突然从 80% 掉到 20%,那一定是数据源出了问题。

业务侧三个:无效曝光占比、转化成本、增量 ROI。前两个直接看报表,第三个需要做实验。

增量 ROI 的计算一定要用对照组。方法是在同一批计划里随机切出 5% 到 10% 的流量不接 RTA,作为对照,跑够统计显著所需的天数再比。我见过太多人直接拿接入前后的数据对比,得出一个漂亮但完全错误的结论。

5.2 三个典型的统计陷阱

陷阱一:前后对比代替 AB。大促期间接入 RTA,前后数据变化里混着大盘流量质量的变化、竞品出价的变化。不做对照,无法分离出 RTA 的真实贡献。

陷阱二:过滤带来的幸存者偏差。RTA 把一部分流量挡掉了,剩下的流量本身质量就更高,转化率自然上升。但这不代表整体收益上升,因为被挡掉的那部分流量本来也可能有转化。所以一定要看总转化数和总成本,而不是只看转化率。

陷阱三:归因窗口不一致。RTA 影响的是曝光,转化可能在几天后才发生。如果实验只跑三天就下结论,会严重低估效果。窗口至少要覆盖完整归因周期。

指标观察周期判断标准
超时率实时超过 1% 立即排查
参与率按日环比波动超过 20% 要查原因
无效曝光占比按周下降即为正收益
增量 ROI完整归因周期实验组显著高于对照组

6. 线上问题排查实录

真正上线之后,问题五花八门。我把遇到过的、以及同行反馈过的典型情况整理成一张速查表,后面再展开讲两个最有代表性的。

6.1 常见问题速查表

现象最可能原因排查动作
曝光量断崖下跌参与率异常低,或返回结构不合法查返回码分布、抽样看返回体
成本不降反升bid_ratio 设置过高,或降级走了默认参与查系数分布、查超时率
超时率飙升下游慢查询、缓存击穿、发布重启看 P99、看缓存命中率、看发布时间点
参与率接近 100%ID 映射失效,全走默认分支统计映射命中率
不同媒体效果差异大各媒体 ID 覆盖率不同,或流量结构不同分媒体拆指标
请求量与预估差十倍TTL 缓存生效,或计划未全量开启核对 ttl 设置、核对计划配置

6.2 两个真实排查过程

第一个:曝光量莫名其妙掉了四成。

早上九点开始,投放侧的曝光量曲线直接砍掉一半,没有任何代码发布。先看 RTA 侧:请求量正常,超时率 0.3% 正常,但参与率从 78% 掉到了 21%。这就锁定了问题在决策逻辑。

再往下查,发现参与率下跌集中在某几个城市。继续拆,发现是 ID 映射服务的一个分片挂了,那个分片覆盖的用户全部映射失败,走到了我前面代码里那个“查不到就放行”的分支。但等等,放行应该是参与率上升啊?

再一看,不是。这里有个细节:映射失败之后会去查兜底存储,而兜底存储在故障时返回的是一个空对象而不是 None,代码里判断profile is None不成立,继续往下走,读到了一个所有字段都是默认值的 profile,is_purchased_recently默认是 True,于是全部拒绝。这就是默认值设计不当的典型事故。修法很简单,把默认值改成明确的“未知”状态,未知就走放行。

第二个:成本控制住了,但转化数跌得比预期多。

接入 RTA 两周,成本降了 35%,但总转化数降了 28%,增量 ROI 反而是负的。这个结果说明过滤得太狠了。

拆开看,被拒绝的流量里,有一批是“近 30 天有浏览但未购买”的用户,我们当时的规则是只要有浏览记录就拒绝,理由是“已经看过广告了”。但数据显示这批人里有一部分是在做决策比较,属于高意向人群,直接拒掉是浪费。

调整方案是把布尔判断改成系数判断:有过浏览但未购买的,返回 0.8 的系数而不是直接拒绝;已经购买的,才返回拒绝。改完之后成本回升了 8%,但总转化数回升了 22%,整体 ROI 转正。

这个教训我一直记着:RTA 的拒绝是最重的手段,能不用就不用,能用系数解决的优先用系数。布尔值适合表达“绝对不要”,而绝大多数业务判断其实是程度问题,不是是非问题。

6.3 几个实操心得

接口里千万别放同步的数据库查询。哪怕加了索引,一次慢查询就能把整个接口的 P99 拖崩。所有需要查库的判断,提前算好放进缓存。

返回体一定要做长度控制。有的媒体侧对响应体大小有限制,超了直接判失败。自定义创意 ID 这类字段能省就省。

日志要打全,但要注意脱敏。request_id、参与结果、命中规则三条必打,用户标识做哈希处理。出问题的时候这三条能覆盖八成排查场景。

压测要用真实流量结构。拿一批均匀分布的假请求压出来的数据毫无意义,真实流量的特征是极度不均匀,少数用户贡献了大部分请求,缓存命中率天差地别。

我个人在几个项目里反复验证的一点是:RTA 上线初期最容易出问题的不是决策逻辑本身,而是那些没被明确定义的边界情况——ID 查不到、字段缺失、下游超时、返回体格式不对。把这些边界一个一个列出来,逐个定义好行为,比优化决策算法本身带来的收益大得多。后面再扩展的话,可以考虑把决策逻辑做成可配置的规则引擎,让投放同学自己能调,不用每次改规则都排一次研发排期,这个在节奏快的行业里价值很高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询