AdTechSep 10, 2026読了10分

ピーク時に広告計測がイベントを失う ― 継ぎ目、重複キー、ビッダーとの突合

イベントパイプライン広告計測アトリビューションデータ整合性
画像を読み込めませんでした

ピーク時に計測パイプラインがイベントを失い、アトリビューションの数字がビッダーのログと決して合わない。これは一つの症状の裏にある二つの障害である。誰も数えていない継ぎ目で、そもそも届かなかったイベント。そして届いたが、別の規則で数えられたイベントだ。ここでは、継ぎ目、キー、遅延ウィンドウ、そして突合の橋をどう作るかを扱う。

ピーク時にイベントを失い、ビッダーのログと決して突き合わないイベントパイプラインは、一つの症状の裏にある二つの障害でありうる。一つはトランスポートだ。生成されたのに、誰も数えていない継ぎ目でどこにも着かなかったイベントである。もう一つは定義の問題だ。着地して数えられたが、オークションの記録とは別の規則で数えられたイベントである。

よくある枠組み ― パイプラインがイベントを落とすのだから、パイプラインを作り直せ ― で直るのは、多くても二つのうちの一つである。以下では両者を切り分け、突合が何を生むのかを述べる。それは一致ではない。以下で引く既定値は、トランスポート側が Kafka、ストレージ側が ClickHouse のものである。

短い答えは構造にある。オークションごとに一つの識別子を端から端まで運ぶこと、すべてのホップの両側にカウンタを置くこと、そしてすでに閉じたウィンドウに対して突合すること。amBrain が公に裏付けられるのは AdTech の仕事である。DSP 開発、リアルタイムビディングのプラットフォーム、アドエクスチェンジのエンジニアリングだ。どの RTB スタックでも、入札・落札・インプレッションの記録が出てくるのはその側である。以下のパイプラインは、私たちの事例からではなく、問題の仕組みから記述している。

失われたイベントと遅れたイベントは、一つのクエリで見分けられる

一つ目の障害は損失である。イベントは生成されたのに、特定のホップで、特定の理由で届かなかった。ページから出て行かなかったビーコン、デプロイの途中で再起動したエッジ、いっぱいになったプロデューサのバッファ、処理の前にオフセットをコミットしたコンシューマである。

二つ目は、そもそも障害ではない。計測側が数えるのはクライアント起点のイベントであり、ビッダーが記録するのはサーバー側のオークション結果である。一方は落札であり、もう一方はその落札に何が起きたかの観測である。MRC の計測ガイドラインは、プリフェッチ、プリレンダー、自動更新を、それぞれ検出して開示すべき別個の事象として扱う。これは数え方の規則であって、トランスポートの障害ではない。二つを見分けるのにかかるのは、クエリ一本といくらかの忍耐である。

  • 同じイベント時刻のウィンドウを、その対象時刻の1時間後、6時間後、そして丸1日後に流し直す
  • 実行のたびに縮む不足分は、イベントが失われたのではなく遅れて届いたことを意味し、トランスポートは問題ない
  • 行ではなく、重複排除キーの異なり数を数える。at-least-once のトランスポートは再配信を保証するので、行ベースの曲線は過大計上を覆い隠す
  • 横ばいのままの不足分は、イベントが消えたことを意味し、問いはどの継ぎ目かになる
  • この試験に必要なのは、イベント時刻のタイムスタンプ、リトライを越えて残る識別子、そしてウィンドウを流し直せるだけの長さの保持期間である
  • 収束曲線はイベント種別ごとにグラフとして、それが説明する数字の隣に掲げる

その曲線が無いあいだ、議論の双方は意見にすぎない。曲線ができれば、その形が、この記事のどちらの半分が当てはまるかを決める。そして二つの半分は排他ではない。

イベントは名前の付いた継ぎ目で失われる。数えていない継ぎ目に責任を負わせることはできない

広告イベントが生成されたあと、静かに存在しなくなる場所は七つある。加えて、保証に見えて保証ではない設定が一つある。

  • クライアント側の収集。ビーコンは発火するのにドキュメントのほうが先に破棄される。あるいはクリエイティブがキャッシュされ、プリフェッチされ、自動更新されて、オークションの記録には無いものを数える
  • エッジでの取り込み。接続数の上限、keep-alive の枯渇、デプロイ中の再起動、そして危険な変種 ― イベントが永続化される前に成功を返すことである
  • プロデューサのバッファ。クライアントは決められた時間だけブロックし、その後エラーを投げる。そのエラーを捕まえて何も数えないコードが、データの死に場所である
  • ブローカーの永続性。確認応答を要求しなければ、レコードが届いたことは何も保証されない。リーダー単独では、フォロワーが複製する前にそのリーダーが落ちればレコードは失われる
  • 全レプリカからの確認応答は永続性ではない。待つのは現在の in-sync 集合であり、その最小サイズの既定値は1だ。だからフォロワーを遅れさせるピークでは、リーダー単独でコミットされる
  • コンシューマ。処理の前にオフセットをコミットするのは at-most-once であり、しかもそれは決定ではなく既定値である ― 明示的に切らない限り、クライアントはタイマーでコミットする
  • 保持期間の超過。保持ウィンドウを越えて遅れたコンシューマは、次のオフセットが削除されていることに気づく。そして既定のリセット方針は、そのコンシューマをログの先端へ飛ばす
  • カラムナストアへのロード。fire-and-forget の挿入はバッファに入った時点で確認応答を返し、依存するマテリアライズドビューは別の設定で重複排除する ― 生テーブルとレポートが袂を分かつ場所である

この規則はエンジニアリングの規則ではなく、レポーティングの規則である。損失は名前の付いた継ぎ目に帰属させるか、まったく帰属させないかのどちらかだ。静かな継ぎ目を見えるようにしておく警報は二つ。保持期間に対して時間で測ったコンシューマラグと、飛ばす代わりに失敗するリセット方針である。

重複排除には、最初のリトライより前から存在するキーが要る

重複排除キーは、宛先側で都合よく選ぶ主キーではない。すべてのリトライより上流で、オークションの時点またはイベント生成の時点で割り当てられるものであり、受け手が勝手に作ることはない。到着タイムスタンプは含めない。リトライは新しい到着時刻を持ち、新しいキーになってしまうからだ。

キーはすべての試行で同一でなければならず、そこから構成要素が決まる。エクスチェンジまたはシート、オークション識別子、インプレッション識別子、そしてイベント種別である。トピックはそのキーでパーティション分割し、リトライが同じ場所に落ち、キーごとの順序が保たれるようにする。この指示はトランスポートに対してのみのものだ。同じ言葉をカラムナストアに当てはめると、イベントごとに一つのパーティションができ、ブロックあたりの上限で挿入が死ぬ。ストレージは時刻でパーティション分割し、キーで並べる。

  • 冪等プロデューサは、一つのセッション内でのプロデューサのリトライによる重複を取り除く。Kafka は 3.0 以降、全レプリカからの確認応答とともに、これを既定で有効にしている
  • その既定値には条件が付く。古い設定から来た矛盾する項目は、冪等性を黙って無効にする。だから、動いているプロセス自身に何が入っているかを尋ねる
  • アプリケーション層の重複は見えない。クラッシュしたプロセスの再送、二度発火したビーコン、取り込みジョブを流し直した運用者である
  • ClickHouse の挿入時重複排除はブロックの内容をハッシュする。だからリバランス後にバッチを組み直したコンシューマは、同じ行を別の形で送ることになり、ハッシュは一致しない
  • このウィンドウはブロック数と時間で区切られており、レプリケーションされていないテーブルでは既定値がゼロ、つまり無効である。挿入トークンを使えば、その依存から抜けられる
  • ログ内部の exactly-once が覆うのは consume-transform-produce であり、分析データベースへのホップは、トランスポートが何を約束していようと、その境界の外にある

だから機能する形は、冪等なキーを伴う at-least-once のトランスポートである。書き込み時の重複排除はストレージの請求を正気に保ち、読み取り時の重複排除が数字を正しくする。マージが異なるパーティションのパートを結合することはないので、次のパーティションに落ちた重複は、クエリが尋ねたときにだけ解決される。

損失が数字になるか噂になるかは、back pressure が決める

過負荷のとき、システムの選択肢は三つある。送り手を遅くするか、カウンタを付けて捨てるか、静かに失うかだ。受け入れられないのは三つ目だけであり、それは、この問いを一度も投げられなかったコードの既定の振る舞いである。ピーク時の損失とは、メモリが尽きるまで伸びたキューか、永続化の前に返した確認応答のことだ。

  • すべてのホップで有界キューを使い、伸ばす代わりに明示的に拒否する。非有界キューは、損失をメモリ逼迫と再起動へ移し替えるだけである
  • プロデューサのブロッキング時間とバッファサイズは、容量の決定である。実測したピークから寸法を決め、ブロックされた時間に警報を張る
  • 捨てるときは無作為にではなく、種別ごとに捨てる。インプレッションと課金対象イベントは残し、診断用が先に落ちる。そして捨てたイベントは必ず、ラベル付きカウンタを一つ進める
  • コンシューマラグは、back pressure を目に見えるようにしたものである。最も古い未処理イベントの経過時間と、ラグの変化の速さに警報を張る

ラベル付きカウンタを伴う破棄は、あとで突き合わせられる既知の量である。カウンタのない破棄は、失われたデータではなく、失われた数字である。

遅れて届くことは構造的であり、食い違いの半分はカレンダーの問題である

OpenRTB の実装ガイダンスはそれを直接述べている。広告リクエストからオークションを経て描画と課金に至る一連の流れは、本質的にトランザクショナルではない。二つの数のあいだには、当事者が多すぎるのだ。

遅延は例外ではなく想定内である。入札リクエストはインプレッションの有効期限を、入札はビッダーが許容する遅延を運べる。同じガイダンスは目安も示しており、web では1分程度から、キャッシュされたアプリ内フォーマットやスティッチされた動画ではそれよりはるかに長い。

どちらのフィールドも契約ではない。ガイダンスははっきり述べている。ビッダーが宣言した有効期限より遅れて届いた課金通知でも、なお課金対象になりうる ― プロトコルが課すことではなく、ビッダーとエクスチェンジのあいだの方針の話である。

  • イベントごとにタイムスタンプは三つあり、ウィンドウを動かすのはちょうど一つである。端末の時計は信用できない。エッジでの受信時刻は遅い。オークション時刻が正本である
  • 4つ目は、エクスチェンジが提供している場合に存在する。インプレッションが充足された時刻を運ぶマクロだ。それが無い場合、仕様は通知がその数秒後に続いたものと想定する
  • watermark は、イベント時刻がある地点に達し、それより前の要素はもう来ないと宣言する。したがって、許容する遅延はあなたが選ぶパラメータである
  • 遅延ウィンドウは、イベント種別ごとに遡及修正の方針とともに公表する。開いているあいだ数字は動き、その後は凍結され、動いた分は記録される
  • 遅れたイベントは数えたうえで、遅延の印を付ける。捨てることは、請求された消化額を捨てることだからだ

ウィンドウの後に届くイベントは、パイプラインの欠陥ではなく、媒体の性質である。実際の選択肢は一つだけだ。ウィンドウが開いているあいだに数字を公の場で動かすか、それとも後から人知れず動かすかである。

突合は、プロトコルがすでに運んでいる識別子で結合する

エクスチェンジが通知や計測用の URL に埋め込む識別子は、入札リクエストのオークション識別子、インプレッション識別子、そしてビッダーが発行していれば入札識別子である。三つとも、それ単独ではキーにならない。

仕様はオークション識別子を、グローバルに一意ではなく、エクスチェンジ内で一意だとしている。二つのエクスチェンジが同じ日に同じ文字列を渡してくることはありうる。インプレッション識別子が一意なのは自分の入札リクエストの内側だけであり、多くの場合は文字どおり 1 である。入札識別子は任意である。

持ちこたえるキーは複合キーである。取引したエクスチェンジまたはシート、オークション識別子、そしてインプレッション識別子だ。オークションの時点でビッダー側が発行し、それより短いものはキーではなく接頭辞として扱う。

ビーコンにそれらのマクロが無ければ、イベント単位の突合は不可能であり、残るのは時刻・配置面・クリエイティブによる照合だけになる。代わりに作れるのは、六つの数からなる橋である。各段は、一つ上の段と食い違う理由を名指しする。

  • ビッダーのログから取る落札数 ― 完全に自分のものである唯一の数
  • エクスチェンジが受け取った落札通知 ― 差分は通知の損失とタイムアウトであり、仕様上、落札通知は必ずしも配信を意味しない
  • 自社のエッジで受け取ったビーコン ― 差分はクライアント側の収集と、その上にあるすべての継ぎ目である
  • 重複排除後のイベント数 ― 差分はリトライであり、週ごとに安定しているはずである
  • 無効トラフィックのフィルタリング後のイベント数 ― 差分はフィルタリング率であり、あとから発見するものではなく、公表するものである
  • 課金対象イベント ― 差分は課金規則であり、通知はエクスチェンジが収益を計上するサーバー側にある

目指すのは段階間の比率が安定していることであり、説明のつかない動きが警報である。一つのシステムの内側では、ホップごとの比率は1であるべきで、そこからの逸脱が信号になる。オークションから計測へまたぐ境界では、1のほうが疑わしい値である。

同じカウンタを比として読むと、問いは算術になる。送信に対する受理、受理に対する生成、生成に対する消費、消費に対する挿入である。一枚のグラフに四つの比を並べれば、誰かがログを開く前にイベントの行き先がわかる。さらに送信元とパーティションごとにプロデューサ側のシーケンス番号を付ければ、欠落は疑いではなく証拠になる。

パイプラインを作り直しても手に入らないもの

橋が答えず、契約が答える問いが一つある。これらの数のどれで支払うのか、である。売り手は自分の課金対象イベントで収益を計上し、買い手は自分の側で配信ペースを決める。ガイダンスは、乖離が続く場合を当事者間のサポート上の協議として扱う。

どの数を消化額の正本とするか、そしてどれだけの乖離でレポートの注記がエクスチェンジへのチケットに変わるかを、あらかじめ決めておく。ここまでの作業で手に入るのは、損失の帰属、正直な重複、そして一行ずつ説明できる突合である。手に入らないものは次のとおりだ。

  • 二つの数が等しくなるわけではない。双方は意図して別のイベントを数えており、その差は説明されるものであって、決して消せるものではない
  • 計装が存在する前に落ちたイベントは戻らない ― 収束曲線が始まるのは、カウンタが始まった日である
  • 遡及修正が無くなるわけではない。遅延ウィンドウが開いているあいだ、昨日の数字は動く。それに耐えられない事業には、より遅い締めが必要である
  • マクロの欠落には耐えられない。ビーコンにオークション識別子が無ければ、どんなストレージ設計でもイベント単位の結合は作れない
  • サンプリングされたデータが、あとから結合できるようになるわけではない。どの問いに答えられるままかは、行が書かれる前にサンプリングが決めてしまうからだ
  • MRC 型の監査が求める開示項目の代わりにはならない。取得地点、ログ記録の頻度、latency の見積もり、不整合の扱い規則である

それらの開示項目に答えられるパイプラインには、整合性の裏付けがある。答えられないパイプラインにあるのは意見であり、四半期の終わりに揉めるのは意見のほうだ。

設計で備える価値がある障害は、調査の口火を切る「消えた1時間」ではない。静かなほうである。カウンタ無しで捨てる継ぎ目、永続化される前に確認応答を返した挿入、そしてまだ開いているウィンドウに対する突合だ。

amBrain が公に裏付けられること:amBrain はアルメニア・エレバンのソフトウェアエンジニアリング企業であり、低 latency のトレーディングプラットフォーム、matching engine、リアルタイムビディングのシステムを Rust で作っている。amBrain は2019年からソフトウェアを作ってきた。計測パイプラインの作り直しは、ここで説明している仕事ではない。数字が合わなくなっているのがビッダーとエクスチェンジの側 ― 入札・落札・インプレッションの記録そのもの ― であれば、それが価値のある話であり、作り直しではなく収束曲線から始まる。

手元に似た設計はありますか?

現在のアーキテクチャと気になっている障害シナリオをお持ちください。30 分で一緒に確認します。

関連記事

画像を読み込めませんでした
AdTech
Sep 10, 2026読了10分

RTB ビッダーにおける Go の GC 停止 ― mark assist、デッドライン、そして Rust という判断

記事を読む
画像を読み込めませんでした
AdTech
Mar 5, 2026読了7分

2026年、AIはプログラマティック広告をどう変えるか

記事を読む
画像を読み込めませんでした
AdTech
Feb 14, 2026読了6分

プライバシーファーストのターゲティング:サードパーティCookieに依存しないアドテク構築

記事を読む