第一次听到 RTA 这个词是在一次投放复盘会上。某电商客户的投放负责人拍着桌子问我们自己的一批高价值会员、刚加购未付款的那批人、复购周期还有三天的老客媒体那边根本看不见凭什么让它用一个统一的出价替我决定把广告给谁看这句话其实点破了 RTA 存在的全部理由——媒体手里握着流量广告主手里握着一方数据两边谁也拿不走对方的东西那就只能在竞价发生的那一瞬间开一个几十毫秒的窗口让广告主自己喊一句“这个流量我要、不要、或者加价多少”。RTA全称 Realtime API中文一般叫实时广告接口干的就是这件事。这篇文章面向三类读者一是刚开始接触商业化系统、听到 RTA、RTB、DSP 一堆缩写就头大的工程师二是被业务方追问“我们能不能自己筛流量”的产品和运营同学三是已经上手写过 RTA 服务、但被超时和降级坑过的同行。我会从链路位置讲起一直讲到协议字段怎么定、服务端怎么扛峰值、上线后怎么验证效果中间穿插我自己踩过的坑。这一篇是整个广告系统设计系列的第一篇先把 RTA 这个“接口”讲透后面再展开讲竞价、计费、归因那些更靠后的环节。1. RTA 在竞价链路上究竟站在哪个位置很多刚接触的人会把 RTA 理解成“广告主自己出价”这个理解只对了一小半。RTA 的本质是一次带外询问媒体在决定要不要把这次曝光机会交给某个广告主之前先发一条请求过去问一句“这次你还投不投”。广告主回一个判断和一组系数媒体拿着这个结果继续走自己的排序和计费流程。所以 RTA 严格来说不是出价动作而是参竞决策加权重调整成交价的形成机制并没有因为它发生根本改变。把这个位置搞清楚后面所有的设计取舍才有依据。位置偏前能省下媒体侧大量无用的召回和排序算力位置偏后拿到的上下文更丰富判断更准但留给你的时间窗口会被前面的环节吃掉一大截。1.1 一次曝光请求从媒体侧走到广告主侧的时间轴一次典型的请求链路大概是这样的用户刷新信息流客户端发起广告请求媒体侧广告引擎收到请求后做召回从几万条广告里捞出几千条候选接着粗排把候选压到几百条精排算出每条的预估点击率和预估转化率然后进入竞价环节按出价和预估收益排序选出最终展示的那一两条最后渲染、曝光、回传。RTA 的调用点通常落在两个位置的其中一个。第一个是请求刚进来、召回之前好处是最省算力一次 RTA 判断可以一次性过滤掉整个广告主账号下的所有广告代价是此时媒体侧手里的信息还很粗糙只有设备、广告位、时段这类基础上下文。第二个是召回之后、排序之前好处是媒体能把已经算出来的粗排分数、预估价值档位一起带给你判断精度明显更高代价是这条链路已经被媒体侧的计算资源覆盖了一部分省不掉了。我自己做过的两个项目分别用了这两种。效果类的投放走的是靠前的位置因为广告主的核心诉求是拦掉明显不值的流量早拦早省品牌类投放走的是靠后的位置因为需要媒体侧的预估信息辅助判断而且量级本身不大省那点算力意义有限。选哪个位置本质是在算力成本和判断精度之间做权衡没有标准答案。1.2 RTA 与 RTB、DMP、oCPX 的职责边界这几个概念经常被混着用实际分工差别很大。我整理了一张对照表建议先记住这张表再往下看后面的内容。维度RTARTBDMP 人群包oCPX决策发起方媒体发起询问广告主应答媒体发起售卖买方出价广告主离线圈人媒体侧按转化目标优化时效性毫秒级逐请求判断毫秒级逐请求竞价天级或小时级更新天级模型迭代是否直接决定成交价否影响参竞与否和出价系数是出价直接决定成交否影响定向范围间接影响出价策略数据是否离开广告主否判断在自己机房完成部分交互是人群包要上传否典型时延预算30 到 100 毫秒100 毫秒上下无无看这张表最关键的一行是“数据是否离开广告主”。DMP 人群包的问题在于你必须把一批用户标识上传到媒体侧隐私约束越来越紧的环境下这条路越来越窄而且天级更新意味着今天上午刚流失的用户明天才能被圈进包。RTA 把判断留在自己机房只回一个结果天然绕开了这两个问题。RTB 和 RTA 的关系更容易弄混。RTB 是媒体把流量拿到公开市场上去卖买方是 DSP谁出价高谁拿走RTA 是媒体私下问广告主“这个流量你要不要”广告主没有议价权只有一个筛选权和权重调整权。同一条流量上两者可能同时存在先走 RTA 判断要不要参竞参竞之后再走竞价流程决定成交价。理解成“RTA 是入场券RTB 是拍卖槌”会更直观。2. 广告主为什么非要在链路中间插一手如果媒体侧的定向能力已经足够强RTA 就没有存在的必要。现实是媒体侧的画像再全也永远缺广告主手里那部分最值钱的信息用户的会员等级、历史订单金额、退换货记录、购物车状态、客服工单情绪、线下门店的到店记录。这些数据要么涉及商业机密要么涉及用户隐私不可能交给媒体只能在广告主自己的服务器上算。这就是 RTA 最核心的价值主张把“数据不出域”和“实时参与决策”这两件看起来矛盾的事同时做到。下面拆三个我在实际项目里最常遇到的场景。2.1 一方数据不出域判断只能在你自己机房做某电商客户做过一个很典型的尝试把高复购用户做成人群包上传投了两周效果不错然后想把“近 7 天浏览过某品类但未下单”这批人单独拿出来加权。结果发现人群包这条路走不通因为“近 7 天浏览但未下单”是一个每天都在变的动态集合今天圈出来的人明天就有一半已经下单或者已经失去兴趣天级更新根本追不上。改成 RTA 之后媒体每次请求过来我们拿设备标识哈希去查用户的实时行为库几十毫秒内判断这个人是处于“犹豫期”还是“已经买完”还是“根本不感兴趣”分别返回不同的系数。用户刚下单完成下一次请求立刻就能返回极低权重广告预算不会再浪费在已经成交的人身上。这个改动上线后重复曝光率降了一大截同样的预算带来的新客数明显上升。这里有个工程上的关键约束请求里带过来的用户标识一定是加密或哈希过的广告主这边必须用同样的哈希算法和盐值才能对上。哈希算法和盐值一旦变更整个匹配率会断崖式下跌一定要把版本号写进配置、做成可灰度切换的。2.2 差价值分层同一个出价买不到所有人群媒体侧的 oCPX 出价是一个相对统一的目标比如“按成交出价 80 元”。但真实世界里一个新客的终身价值和复购三次的老客完全不是一回事。媒体侧的模型只看得到转化行为看不到转化之后带来的长期价值所以它给出的出价会倾向于“好转化的人”而不是“高价值的人”。RTA 的系数机制正好补上这一层。我一般会做一个简单的分层把用户按历史贡献切成四五档最高档返回 1.5 到 2.0 的系数中间档 1.0犹豫期 0.7明确已流失的 0.3 甚至直接拒绝参竞。系数不是拍脑袋定的要有数据支撑把每一档人群在自然状态下的转化率和客单价拉出来算出期望价值再和整体平均值做比值这个比值就是理论上合理的系数。需要提醒一点很多媒体平台的系数是截断的。我遇到过平台允许返回 0.1 到 10.0 的系数但内部实际生效范围只有 0.5 到 2.0你返回 5.0 也没用会被静默截断。这个上限一定要在接入前跟平台确认清楚否则你会以为策略生效了实际上完全没起作用。2.3 预算保护与劣质流量拦截第三个场景是防守。某游戏客户在推广期遇到过一波异常点击特征是设备集中在少数几个机型段、点击时间间隔极其规律、后续留存几乎为零。媒体侧的无效流量识别有滞后等平台封掉这批设备预算已经烧掉一大截。我们用 RTA 做了一个实时的黑名单层把历史识别的异常设备特征提取出来做成规则每次请求过来先过一遍命中就返回不参竞。规则本身很轻几微秒就能算完但效果立竿见影——上线后一周内异常设备的曝光量占比从百分之十几降到个位数。防守型 RTA 还有一个隐含用途新客保护。大促期间某些广告主会刻意压低老客的权重把预算腾给新客拉新这时候 RTA 就是一个实时的预算阀门比在媒体后台调出价灵敏得多。3. 协议层怎么设计字段取舍决定系统上限RTA 的协议看着简单一问一答两个报文但字段设计上的每一个决定都会在后面的性能和稳定性上放大。我的经验是请求字段宁多勿少但要分层响应字段宁少勿多因为它是热路径上的最后一步。下面把这两块拆开讲。3.1 请求报文哪些字段必须有哪些字段给了也别乱用一个可用的请求报文至少要有这几类信息。第一类是链路标识包括请求 ID、时间戳、广告主账号 ID、广告位 ID。请求 ID 是后面排查问题的唯一线索必须原样回传绝对不能自己生成新的。第二类是流量上下文包括广告位类型、流量场景、设备型号、操作系统、网络类型、地域通常只到城市级别。第三类是用户标识一般是设备 ID 的哈希值或者媒体侧分配的统一 ID加密方式要提前约定好。第四类是媒体侧的预估信息比如预估点击率档位、预估曝光价值档位有些平台会给有些不会。下面是一个简化过的协议定义用 protobuf 写实际项目里我也会用类似的形状message RtaRequest { string request_id 1; // 必须原样回传 string account_id 2; string slot_id 3; int64 timestamp_ms 4; string device_hash 5; // 与广告主侧算法一致 string media_uid 6; string os 7; string model 8; int32 network_type 9; string city_code 10; int32 est_ctr_level 11; // 媒体侧预估档位0 表示未提供 int32 est_value_lvl 12; mapstring,string ext 15; }字段里有几个坑要提前说。timestamp_ms建议统一用毫秒时间戳并强制 UTC我见过因为时区处理不一致导致夜间策略全部错乱的案例。ext扩展字段看着方便但千万不要把大对象塞进去序列化开销会直接吃掉你的时延预算而且字段一多两侧版本很容易对不齐。还有个容易被忽略的点请求里带过来的用户标识必须是单向不可逆的。明文手机号、明文设备号绝对不能出现在报文里这是红线。哈希用的盐值要有版本管理变更前先做双写比对确认匹配率没掉再切。3.2 响应报文越短越好字段越少越安全响应报文的核心只有三个字段请求 ID、是否参竞、出价系数。能加的就再加一个可选的定向覆盖字段比如指定这次只能投哪几个创意但不要滥用字段越多解析越慢、出错概率越高。message RtaResponse { string request_id 1; int32 bid_flag 2; // 1 参竞0 不参竞 int32 bid_ratio 3; // 放大一万倍的整数如 15000 表示 1.5 }系数为什么用整数而不是浮点因为不同语言的浮点序列化精度处理不一致0.7 在某些实现里序列化出来是 0.6999999回传之后被截断成 0.69看起来只差一点点累积起来就是预算偏差。用整数放大一万倍全都用 int32 传这个问题彻底消失。响应报文一定要有严格的字段白名单校验。媒体侧的解析器通常很脆弱多一个未知字段可能导致整条响应被丢弃而响应被丢弃的默认行为往往是“按参竞处理”你的拦截策略就全废了。这一点在后面讲降级的时候还会提到。3.3 时延预算的拆解与分配平台给的超时通常是 100 毫秒但这 100 毫秒不是让你全部用掉的。真正可用的预算要扣掉网络往返、媒体侧的排队和重试一般能留给你自己服务的时间在 30 到 60 毫秒之间。我习惯按下面这个方式拆环节预算说明网络往返5 到 15 毫秒同城机房内网通常 1 到 3 毫秒跨区会到 10 毫秒以上网关与反序列化2 到 5 毫秒网关层越薄越好不要在网关做业务逻辑业务逻辑与规则3 到 8 毫秒纯内存计算超过 10 毫秒说明规则太复杂特征读取5 到 20 毫秒本地缓存可以压到 1 毫秒内远程调用是最大变量模型推理1 到 10 毫秒轻量模型够用复杂模型要么异步要么放弃序列化与回包1 到 3 毫秒响应字段越少这块越省抖动缓冲剩余全部至少留 30% 应对 GC 和网络抖动这张表的意义在于它把“快”这个模糊要求变成了可分配的预算。你能清楚地知道哪一段超了、超了多少、该从哪里挤时间。我见过太多团队把预算全花在模型上结果网络抖动一来就大面积超时。4. 服务端落地从跑通 Demo 到扛住峰值Demo 阶段随便写写就能跑真正难的是峰值。RTA 服务的流量特征和普通业务服务完全不同平时可能只有几百 QPS晚高峰某一刻突然冲到几万而且这种猛涨往往只持续十几分钟。下面讲三块实操性最强的内容。4.1 特征读取路径的选型特征读取是整个服务里最大的时延变量三种路径各有适用场景。本地内存缓存适合放变化不频繁、体量不大的数据比如黑名单、人群分层结果、配置项。读取耗时在微秒级缺点是更新要靠定时拉取或消息推送有一致性延迟。我一般用本地缓存兜底命中率通常在 70% 以上。分布式缓存适合放用户级别的特征比如最近一次行为时间、累计订单数、会员等级。单次读取在 1 到 3 毫秒可以接受。要特别注意连接池配置和超时设置连接池开小了会在峰值排队开大了会把缓存打爆一般按“单实例 QPS 乘以平均耗时”再留两倍余量来估。远程 RPC 调用不到万不得已不要放在主链路上。一次跨服务调用动辄十几毫秒还要叠加超时重试连锁故障的起点往往就在这里。如果确实需要一定要做成异步预热加本地缓存的组合。我的选择顺序是先把能在本地解决的都放本地本地解决不了的走分布式缓存只有在数据强实时且体量很小的情况下才考虑 RPC。4.2 缓存分层与热点隔离的具体做法分层缓存的核心思路是“越靠近进程越快越快容量越小”。典型的做法是两级进程内一个 LRU容量按实例内存的百分之十左右划进程外一个 Redis 集群按用户标识哈希分片。这里有个特别容易踩的坑叫缓存击穿。某次大促前我们给一批重点设备打上了临时标记结果这批设备恰好是当晚流量最集中的机型段缓存 key 全部集中在少数几个分片上Redis 单分片 CPU 瞬间打满请求排队超时整条链路无响应。事后复盘发现问题不在于缓存本身而在于 key 的分布没有做打散。解决办法有两个一是在 key 上加随机后缀做多副本二是对超高热的 key 做本地永久缓存并单独设一个短周期的刷新线程。另一个经验是给缓存加版本号。策略更新的时候新旧两版同时存在一段时间通过版本号路由切换瞬间不会出现大面积 miss。这个做法在灰度期间特别有用出问题回滚只需要改一个配置。4.3 容量估算与压测一个可以复算的例子容量估算不能拍脑袋。我给一个可以照着套的算法假设日曝光 1000 万次晚高峰集中度按 25% 算那就是 250 万次曝光压在 3 小时里约 10800 秒平均 QPS 大约 231 次。但 RTA 的请求次数不等于曝光次数因为召回阶段可能一个广告主被问多次这里按 3 倍算就是 700 次 QPS。峰值系数再乘 3 到 5得到 2100 到 3500 QPS 的设计目标。这还没算重试和压测水位最终按 8000 QPS 准备资源比较稳妥。单实例能力要实测。用 Go 或 Java 写一个只做内存计算加一次缓存读取的服务4 核 8G 的容器大概能跑到 2000 到 4000 QPS具体取决于序列化方式和 GC 表现。按 2500 QPS 算8000 QPS 需要 4 个实例考虑双机房部署和故障冗余实际开 8 到 10 个。压测的时候不要只压稳态一定要做突增测试从 500 QPS 在 10 秒内拉到 8000 QPS观察错误率和 P99 时延的变化曲线。真实的流量洪峰就是这样来的稳态压测再漂亮也没用。5. 超时与降级最容易被低估的设计部分大部分团队把精力花在策略和模型上对超时降级的处理却很随意结果一次抖动就能把整个月的预算烧完。这块我吃过的亏最多单独拿出来讲。5.1 超时到底该放行还是该拒绝这是个业务问题不是技术问题。判断标准只有一个RTA 在你的策略里承担的是“防守”职责还是“进攻”职责。如果是防守型比如拦截异常设备、拦截已成交用户超时就应该放行也就是返回参竞、系数 1.0。因为你漏掉一次拦截的损失远小于把所有正常流量都掐死的损失。我们早期有个配置写反了超时返回不参竞结果某次网络抖动把整个账号的曝光量打掉了八成业务方直接打电话过来问是不是投放停了。如果是进攻型比如严格控制预算消耗速度超时就应该保守返回低系数或者不参竞。因为这时候你的核心诉求是别超支宁可少买也不多买。最忌讳的是不同环境配置不一致。测试环境超时 500 毫秒、线上 50 毫秒测试全绿上线就炸这种事故我见过不止一次。所有超时参数必须走配置中心和代码一起做版本管理。5.2 多级降级开关的编排顺序降级不是只有一个开关而是一组要有明确的触发顺序。我一般编排成这样几级。第一级是关闭复杂策略只保留最基础的黑白名单这一级通常在整体超时率超过 5% 时自动触发。第二级是跳过远程特征读取全部走本地缓存超时率超过 10% 时触发。第三级是全量返回默认值也就是所有请求都返回参竞加系数 1.0这一级需要人工确认或由全局熔断器自动触发。每一级都要有可观测的指标触发次数、持续时长、影响请求数。没有指标监控的降级开关等于没有。我们现在的做法是每次降级触发都往告警群推一条消息附上当前的时延曲线截图人工介入判断要不要继续往上降。还有个小技巧给降级状态加一个“半开”模式。降级一段时间后抽出 1% 的流量走完整链路试探成功了就恢复失败就继续降级。这样避免了一直停在降级状态没人管的情况。6. 效果验证与踩坑记录策略做得再漂亮没有可靠的验证方法就是自嗨。RTA 的效果验证比一般的功能验证难得多因为它影响的是竞价结果而竞价结果又受大盘流量、竞争环境、季节因素影响干扰项太多。6.1 先空跑再灰度影子模式的正确用法上线第一步不是直接生效是空跑。空跑的意思是完整的接收请求、完整的跑完策略、把判断结果记下来但响应里统一返回参竞加系数 1.0对线上没有任何影响。空跑期的价值在于验证两件事一是链路的稳定性时延、错误率、超时率这些硬指标二是策略的合理性把“RTA 判断为高价值”的那批流量单独拉出来看它们的实际转化率是不是真的高于平均水平。如果拉出来发现没什么区别说明策略本身有问题这时候改策略比上线后再改代价小得多。空跑一般跑三到七天覆盖一个完整的周中加周末周期。我一般会看一个指标策略判定为高价值人群的转化率相比全量平均转化率的提升倍数。这个倍数低于 1.2 的话我基本会判定策略不成熟先回去优化再上线。灰度阶段按 5%、20%、50%、100% 逐步放量每一步观察至少一天。放量过程中最需要盯的是量级变化——如果 RTA 生效后曝光量骤降超过 30%先怀疑是不是策略过严把好流量也拦掉了。6.2 AB 实验里那些让结论失真的细节RTA 的 AB 实验有两个隐蔽的坑。第一个是分流时机。如果媒体侧的实验分流发生在 RTA 调用之后那你的对照组其实也受到了 RTA 影响两组没有本质区别实验结论必然是不显著。接入前一定要确认分流发生在链路的哪一步最好让平台侧提供官方的实验能力。第二个是出价不可比。对照组没有 RTA 加权实打实按基础出价跑实验组有了系数出价水平天然更高。这时候转化量上升可能只是因为出价高买到了更多流量而不是因为人群选得准。解决办法是分层看数据把流量按媒体侧的预估价值档位切分在同一档位内比较两组的表现这样才能剥离出价带来的干扰。还有一个归因窗口的问题。转化有延迟7 天归因窗口和 1 天归因窗口算出来的 ROI 能差一倍以上。做对比实验时两组的归因口径必须完全一致且要等归因窗口完全走完再下结论。6.3 我踩过的几个坑第一个坑是响应字段不一致。某次发版新增了一个可选字段测试环境没问题线上有一部分媒体侧的老版本解析器不认识这个字段直接把整条响应判为无效并走了默认放行。结果那一晚的无效流量拦截完全失效成本涨了三成。教训是响应报文只增不改新增字段必须在ext里加且要做好向后兼容。第二个坑是策略过拟合。模型用最近 30 天的转化数据训练跑了一个月发现新客占比从 40% 掉到 15%量级还在但增长停了。原因是模型越来越倾向于“历史上转化过的人”而这些人早晚会被消耗完。后来加了新客探索的强制比例保证每天都有一部分预算投给模型不看好但可能是新客的人群情况才稳住。第三个坑是时钟问题。策略里有一段逻辑按小时切换权重服务器时间同步出了一点偏差两台机器差了 2 秒在整点前后出现了结果不一致同一批用户在两次请求里拿到了不同系数。虽然影响不大但排查花了大半天。后来所有时间相关逻辑统一从请求时间戳取不再用服务器本地时间。第四个坑是缓存 key 设计。早期用设备 ID 加广告位 ID 做 key粒度太细导致命中率只有三成。改成只用设备 ID 之后命中率上到七成代价是同一设备在不同广告位拿到相同判断对效果类投放影响不大可以接受。第五个坑是压测数据太干净。压测用的是随机生成的均匀分布请求真实的流量分布是极度倾斜的少数热门设备贡献了大量请求。上线后缓存热点打满压测时完全没暴露。后来压测数据全部换成从线上采样回放才算是真实。7. 数据安全约束下的工程处理最后单独说一下数据安全这块因为它是 RTA 能长期跑下去的前提。RTA 协议本身不带明文用户信息这是底线但光做到这一点还不够。哈希算法要足够强并且要做加盐盐值定期轮换。轮换期间新旧两套哈希并行计算两边的匹配结果做比对确认匹配率没掉才能停掉旧盐。我见过用简单的 MD5 不加盐直接对手机号做哈希的这种在字典攻击面前基本等于明文。特征读取要遵循最小必要原则。判断一个用户要不要投其实只需要几个布尔标志没必要把完整的订单表拉出来。我们现在的做法是把用户状态预计算成几个枚举值写进缓存请求进来只读这几个值原始明细数据完全不参与线上链路。判断逻辑要留在自己的机房。这是 RTA 相对于人群包上传最大的优势千万别为了省事把明细数据同步到第三方环境去算那就把优势主动丢掉了。访问日志要做脱敏。请求 ID、时延、参竞结果这些可以留用户标识必须哈希后再落盘且设置合理的保留周期。日志一多这里最容易出疏漏。回头看一下RTA 这套东西技术含量其实不算高核心就是一个超低时延的决策服务。真正难的是那些细节位置选在哪、字段怎么定、预算怎么拆、超时怎么降级、效果怎么验证。这些细节没有标准答案全靠在具体项目里一次次试出来。我在实际项目里最深的体会是先把超时和降级这一层做扎实再去优化策略和模型顺序反了会浪费大量时间在排查“策略为什么不生效”上。
阅读完成 · 觉得有帮助?