AdTechSep 10, 2026阅读需 10 分钟

广告测量在高峰丢事件:接缝、重复键与出价方联表

事件管道广告测量归因数据完整性
图片加载失败

你的测量管道在流量高峰丢事件,归因数字又从来对不上出价方日志。这是一个症状背后的两处故障:在没人计数的接缝上从未到达的事件,以及到达了但按另一套规则计数的事件。本文讲接缝、键、迟到窗口和对账桥是怎么搭起来的。

一条在流量高峰丢事件、又始终对不上出价方日志的事件管道,可能是一个症状背后的两处故障。一处是传输:事件被创建却从未落地,在一道没人计数的接缝上。另一处是定义:事件落了地,但计数规则与竞价记录不同。

通常的说法——管道在丢事件,所以把管道重建一遍——最多只能修好其中一个。下文把两者分开,并说明一次对账产出的是什么,它不是相等。下面引用的默认值,传输侧取自 Kafka,存储侧取自 ClickHouse。

简短的答案是结构性的:每次竞价一个端到端携带的标识符,每一跳的两侧各一个计数器,以及对一个已经关闭的窗口做的对账。amBrain 可以公开证实的内容是 AdTech 这块:DSP 开发、实时竞价平台与广告交易平台工程。在任何 RTB 技术栈里,出价、胜出和展示记录都来自这一侧。下面这条管道是从问题本身的机理讲起的,不是来自我们的某个案例。

一条查询就能把丢失的事件和迟到的事件分开

第一种故障是丢失:事件被创建却从未到达,在某一具体的跳上,出于某一具体的原因。从未离开页面的信标、发布中途被重启的边缘节点、被写满的生产者缓冲区、在处理之前就提交了偏移量的消费者。

第二种根本不是故障。测量侧数的是客户端发起的事件;出价方记录的是服务端的竞价结果。一个是授予,另一个是对这次授予后来怎么样的观察。MRC 的测量指南把预取、预渲染和自动刷新当作需要分别检测并披露的事项:这是计数规则,不是传输故障。把两者分开只需要一条查询和一点耐心。

  • 在同一个事件时间窗口所覆盖的时间之后一小时、六小时和整整一天,各重跑一次这个窗口
  • 每跑一次都在变小的缺口,说明事件是迟到而不是丢失,传输没问题
  • 统计不同的去重键,而不是行数:at-least-once 传输保证会有重投,基于行数的曲线会掩盖多计
  • 始终不变的缺口说明事件确实没了,问题只剩是哪一道接缝
  • 这个测试需要一个事件时间戳、一个能扛过重试的标识符,以及长到足以重跑该窗口的保留期
  • 把收敛曲线按事件类型做成图表公布,就放在它所解释的那个数字旁边

在那条曲线出现之前,争论的双方都只是观点。之后,它的形状决定本文哪一半适用,而这两半并不互斥。

事件是在能叫出名字的接缝上消失的,而没有计数的接缝无法被追责

广告事件被创建之后又悄悄不复存在的地方有七处,另外还有一个看起来像保证、其实不是的配置项。

  • 客户端采集:信标发出了,但文档先卸载了;或者创意被缓存、预取或自动刷新,于是计入了竞价记录里没有的东西
  • 边缘接入:连接数上限、keep-alive 耗尽、发布期间的重启,以及危险的那一种变体——事件尚未持久化就返回了成功
  • 生产者缓冲区:客户端阻塞一段有界的时间,然后抛错,而捕获了这个错误却什么都不计数的代码,就是数据死掉的地方
  • Broker 持久性:不要求任何确认时,没有任何东西保证记录到达;仅由 leader 确认时,如果该 leader 在 follower 完成复制之前故障,记录就丢了
  • 全部副本确认不等于持久性:它等的是当前的同步副本集,而这个集合的最小规模默认是一,所以当高峰把 follower 拖到落后时,提交就只发生在 leader 上
  • 消费者:在处理之前提交偏移量就是 at-most-once,而这是默认行为而不是一个决定——除非把它关掉,客户端会按定时器提交
  • 超出保留期:落后到保留窗口之外的消费者,会发现自己的下一个偏移量已被删除,而默认的重置策略会把它跳到日志的最新处
  • 写入列式存储:fire-and-forget 的插入一进缓冲就返回确认,而依赖它的物化视图要靠另一个单独的设置才去重——原始表与报表就是在这里分道扬镳的

这条规则是报表规则,不是工程规则:一次丢失要么归到一道叫得出名字的接缝上,要么根本没有归属。两个告警能让安静的接缝保持可见——以时间度量、并与保留期对照的消费者 lag,以及一条宁可报错也不跳转的重置策略。

去重需要一个在第一次重试之前就存在的键

去重键不是在目的端顺手挑的主键。它在所有重试的上游就已经分配好——在竞价发生时,或在事件被创建时——接收端从不自己造一个。到达时间戳不进入键:重试会带来新的到达时间,也就带来新的键。

这个键在每一次尝试中都必须完全相同,这决定了它的组成:广告交易平台或席位、竞价标识符、展示标识符和事件类型。按这个键给 topic 分区,让重试落在一起,按键的顺序也能在重试后保留。这条指示只针对传输层:同一个词用到列式存储上,会变成每个事件一个分区,插入会因为每个块的分区数上限而失败。存储按时间分区,按这个键排序。

  • 幂等生产者消除的是一个会话内生产者重试产生的重复,Kafka 从 3.0 起默认启用它,同时默认要求全部副本确认
  • 这个默认值是有条件的:旧配置里一个相互冲突的设置会静默关掉幂等性,所以要去问正在运行的进程它实际是什么
  • 它看不见应用层的重复:崩溃后重发的进程、发了两次的信标、把接入任务又跑了一遍的运维
  • ClickHouse 的插入去重对数据块内容做哈希,所以在重平衡之后重新分批的消费者,会以新的形状发送同样的行,哈希就对不上了
  • 这个窗口在块数和时间上都有界,在非复制表上默认为零,也就是关闭;用插入令牌可以摆脱这种依赖
  • 日志内部的 exactly-once 覆盖的是消费-转换-生产,而进入分析型数据库的那一跳在这个边界之外,无论传输层承诺了什么

所以可行的形态是 at-least-once 传输加幂等键。写入时去重让存储账单保持理智;读取时去重才让数字正确。合并从不把不同分区的 part 合到一起,所以落到下一个分区里的重复,只有在查询发问时才会被解决。

back pressure 决定一次丢失是一个数字还是一个传闻

过载之下系统有三个选择:让生产者慢下来、带计数地降载,或者悄悄地丢。只有第三个不可接受,而它正是从没被问过这个问题的代码的默认行为。高峰期的丢失,要么是一条一直涨到内存耗尽的队列,要么是一次在持久化之前就发出的确认。

  • 每一跳都用有界队列,用明确的拒绝代替增长。无界队列只是把丢失搬到内存压力和一次重启里
  • 生产者的阻塞时间和缓冲区大小是容量决策:按你实测的峰值来定,并对阻塞耗时设置告警
  • 按类别降载,而不是随机丢弃:展示事件和可计费事件保留,诊断事件先走,并且每一个被降载的事件都让一个带标签的计数器加一
  • 消费者 lag 就是被显性化的 back pressure。对最老未处理事件的年龄、以及 lag 变化的速度设置告警

带标签计数器的丢弃是一个已知量,之后还能对账。没有计数器的丢弃,丢掉的不是数据,而是一个数字。

迟到是结构性的,对不上的差额有一半是日历问题

OpenRTB 的实施指南说得很直接:从广告请求经过竞价、再到渲染和计费的这条序列,本质上不是事务性的。两个计数之间站着太多方。

延迟是预期之内的,而不是例外。竞价请求可以携带展示的过期时间,出价可以携带出价方能容忍的延迟,同一份指南给出的经验值,从网页端的分钟量级,一直到应用内缓存格式和拼接视频的远远更长。

这两个字段都不是契约。指南说得很直白:比出价方声明的过期时间更晚到达的计费通知,仍然可能是可计费的——这是出价方与广告交易平台之间的政策讨论,而不是协议强加的东西。

  • 每个事件有三个时间戳,而驱动窗口的只有一个:设备时钟,不可信;边缘接收时间,偏晚;竞价时间,权威
  • 在广告交易平台提供的情况下还存在第四个:携带展示达成时刻的那个宏;在它缺失时,规范假定通知在其后数秒到达
  • watermark 声明事件时间已经推进到某个点、不再期待更早的元素,因此允许迟到多久是你自己选的参数
  • 按事件类型公布迟到窗口,连同重报策略:窗口开着时数字会变动,之后冻结,每次变动都记录在案
  • 把迟到事件计入并标记为迟到,因为丢掉它就是丢掉已经向你计费的花费

窗口关闭之后才到达的事件不是管道的缺陷,而是这个媒介的属性。唯一真正的选择是:窗口开着时让数字在公开处变动,还是之后在私下变动。

对账要用协议本来就携带的标识符来联表

广告交易平台替换进通知和跟踪 URL 的标识符是:来自竞价请求的竞价标识符、展示标识符,以及在出价方生成过的情况下的出价标识符。这三个单独拿出来都不能当键。

规范说竞价标识符只在广告交易平台内唯一,而不是全局唯一:两个广告交易平台可以在同一天给你同一个字符串。展示标识符只在它自己的竞价请求内唯一,经常就是字面量 1。出价标识符是可选的。

站得住的键是组合键:你经由的广告交易平台或席位,加上竞价标识符,加上展示标识符。在竞价发生时于出价方一侧生成它,任何比它短的东西都只当作前缀,而不是键。

如果信标里没有这些宏,事件级别的对账就不可能,剩下的只有按时间、广告位和创意做匹配。你能改为搭起来的,是一座由六个计数组成的对账桥,每一个都说明自己与上一步不同的原因。

  • 竞价胜出,来自出价方日志——唯一完全属于你的计数
  • 广告交易平台收到的胜出通知——差额来自通知丢失和超时,而按规范,一条胜出通知并不必然意味着已经投放
  • 你的边缘接收到的信标——差额来自客户端采集以及它之上的每一道接缝
  • 去重之后的事件——差额来自重试,并且应当逐周稳定
  • 无效流量过滤之后的事件——差额是一个由你公布、而不是事后才发现的过滤率
  • 可计费事件——差额来自计费规则,而通知属于服务端,广告交易平台在那里入账收入

目标是各步骤之间的比值稳定,警报是无法解释的变动。在同一个系统内部,各跳的比值应当等于一,任何偏离都是信号。而跨越竞价到测量的边界时,等于一才是可疑读数。

同样这些计数器,按比值来读,问题就变成了算术:接收数比发送数、生产数比接收数、消费数比生产数、插入数比消费数。一张图上的四个比值,能在任何人打开日志之前说清事件去哪了。再按来源和分区加上生产者侧的序号,一个空洞就从怀疑变成了证据。

重建管道给不了你什么

有一个问题对账桥回答不了、而合同能回答:你按其中哪个数字付钱。卖方按自己的可计费事件入账收入,买方按自己的那个控速,而指南把长期存在的差距当作双方之间的支持沟通来处理。

提前决定哪个计数是花费的权威记录,以及差距达到多少时,报表里的一条备注要变成向广告交易平台提的工单。上面这些工作换来的是:丢失可归因、重复被如实呈现、对账能逐行解释。它换不来下面这些。

  • 它不会让两边的计数相等:两边本来就有意在数不同的事件,差额只能被解释,永远不会被消除
  • 它不会找回埋点存在之前就被丢掉的事件——收敛曲线从计数器上线那天才开始
  • 它不会消除数据重报:迟到窗口开着的时候,昨天的数字仍会变动;容忍不了这一点的业务,需要把结账时间推后
  • 宏缺失时它撑不住:信标里没有竞价标识符,任何存储设计都做不出事件级别的联表
  • 它不会让采样过的数据事后变得可联表,因为采样在写下这一行之前就决定了哪些问题还能回答
  • 它不能替代 MRC 式审计所期待的披露清单:采集点、日志频率、延迟估计、不一致的处理规则

能回答那些披露问题的管道,有一套完整性说法。回答不了的只有一个观点,而观点就是季度末拿来吵架的东西。

值得设防的故障,不是那种缺了一小时数据、引发调查的故障。而是安静的那一种:一道不计数就丢弃的接缝、一次在持久化之前就被确认的插入,以及一次针对仍然开着的窗口做的对账。

amBrain 可以公开证实的内容:amBrain 是一家位于亚美尼亚埃里温的软件工程公司,用 Rust 构建低延迟交易平台、matching engine 和实时竞价系统。amBrain 自 2019 年起做软件开发。重建测量管道不是这里描述的工作。如果数字是在出价方和广告交易平台一侧对不上——也就是出价、胜出和展示记录本身——那才是值得谈的那场对话,而它从收敛曲线开始,不是从重建开始。

手头有类似的设计?

带上您当前的架构和让您担心的故障场景,我们用半小时一起梳理。

相关文章

图片加载失败
AdTech
Sep 10, 2026阅读需 10 分钟

RTB 出价方中的 Go GC 停顿:mark assist、时限与 Rust 决策

阅读全文
图片加载失败
AdTech
Mar 5, 2026阅读时长7分钟

AI如何重塑2026年的程序化广告

阅读全文
图片加载失败
AdTech
Feb 14, 2026阅读时长6分钟

隐私优先的定向:不依赖第三方Cookie构建广告技术

阅读全文