Header Bidding 導入によるネットワーク広告改善の開発事情

こんにちは。メディアプロダクト開発部の我妻謙樹(id:kenju)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。

cookpad における広告開発

2015年11月に、"クックパッドの広告エンジニアは何をやっているのか" というタイトルで、広告開発部の開発内容について紹介する記事が公開されていますが、それから 2 年余り経過し、広告配信システム周りの状況も大きく変化しました。はじめに、現在の cookpad における広告開発の概要について、軽くご紹介します。

まず、私が所属しているメディアプロダクト開発部では、広告配信システムに加え、動画配信サービスの開発も担当しています。過去には同じチームから、動画配信周りの技術について以下のような投稿もありますので、そちらもご覧ください。

広告配信システムの開発で担当しているサービスの一覧は、以下の通りです:

  • 広告配信サーバーの開発 (Rails)
  • 社内向け広告入稿システムの開発 (Rails)
  • 広告ログ基盤の運用(Python, Kinesis Streams, DynamoDB, Lambda)
  • 広告配信用 SDK の開発(各プラットフォームに準拠。WEB 向けは JavaScript)

プロジェクト別で言えば、以下がスコープです:

  • 既存自社広告商品の改善
  • 新広告商品の新規開発
  • ネットワーク広告商品の開発や改善

例えば、昨年末から今年はじめにかけて、とある新広告商品の開発に携わっていたのですが、その時のプロジェクトの一部についてスライドが公開されているので、そちらもご覧ください。

今回は、その中でも「ネットワーク広告商品の開発や改善」における一プロジェクトについてご紹介します。

背景

cookpad では、以下の 2 種類の広告を配信しています。

  • 自社広告 1
  • ネットワーク広告 2

この内、ネットワーク広告においては、 Supply Side Platform (以下、SSP)各社と連携して複数のアドサーバー経由で広告を配信しているのですが、それらの広告は頭打ちになってきています。そのため、広告の収益改善に取り組む必要がありました。

安易に広告枠を増やすことはユーザー体験の低下やネットワーク負荷増加に繋がるため、避けなければなりません。したがって、現在配信されている広告の買付け額や配信フローを改善させる必要があります。

そこで、近年日本でも導入が進んできている Header Bidding と呼ばれる仕組みを導入することになりました。

About Header Bidding

Header Bidding とは、アドサーバーに広告のリクエストをする前に、SSP 各社に広告枠の最適な額を入札します。

仕組み的には、<head> タグ内(= Header)で事前に入札リクエスト(= Bidding)を行うことから、"Header Bidding" と呼ばれています。

Without Header Bidding

例えば、Header Bidding を経由しない、従来のパターンでの広告枠買付け方式を見てみましょう。ここで図の用語の定義は以下の通りとします:

  • Client ... 広告を表示させる側ここでは cookpad の本体サイト
  • Ad Server ... アドサーバー
  • SSP ... SSP各社
  • Floor ... フロアプライス 3
  • Bid ... 入札結果 4
  • Winning bid ... 買付けに成功した入札価格

f:id:itiskj:20180613203016p:plain

既存のアドサーバーの入札ロジックは、基本的にウォーターフォール方式(5)で買付けが行われます。したがって、上記図のケースの場合、

  • 広告枠に対してフロアプライスが $1.0
  • SSP α の入札結果が $0.8(フロアプライス以下)
  • SSP β の入札結果が $1.2(フロアプライス以上)

上から順番に問い合わせていった結果、最初にフロアプライス以上の価格で入札してきた「SSP β」の広告が表示されることになります。

ここで「SSP δ」の入札結果が、$2.0 であることに着目してください。もし、「SSP δ」入札結果を反映できていたら、その広告枠の価値は $2.0 になります。つまり、広告枠本来の価値は $2.0 といえます。しかし、ウォーターフォール形式の仕組み上の制約によって、$1.2 の入札結果が反映されてしまいました(差し引き $0.8 の機会損失ですね)。

これを解決するのが、Header Bidding です。

With Header Bidding

次に、入札サーバを挟む場合で見ていきましょう。なお、この場合は後述する Server-to-Server 方式で説明していきます。

f:id:itiskj:20180613203037p:plain

この場合、

  1. Client -> Bid Server ...Header Bidding を入札サーバにリクエスト
  2. Bid Server -> SSP ... 入札。このとき、各社への入札は 同じタイミングで入札 される
  3. Bid Server -> Client ... 入札結果を返す。ここでは、「SSP δ」が $2.0 で買付けをすレスポンスを返してきたので、Winning bid が $2.0 になる
  4. Client -> Ad Server ... 入札結果が返ってきてから広告をリクエストする。このとき、Winnig bid を伝えることで、「SSP δ」広告が返却されることになる 6

という順番で処理が実行されます。Header Bidding を利用しないケースと違って、一番買付け額が高い SSP の入札結果が反映されたことがわかります。

ポイントは、

  • アドサーバーにリクエストする前に入札を行うこと
  • SSP 各社へのリクエストが並行に行われること

です。これによって、本来失われてしまっていた入札価格を最適化することができます。

Client vs S2S Header Bidding

Header Bidding には

  • Client Header Bidding
  • Server-to-Server Header Bidding

の2種類の方式が存在します。

Client Header Biddingは、クライアント側で入札を行う形式です。技術的には、<script> タグ内で、入札先の SSP 一覧を指定して、それぞれに入札リクエストを行います。それらの入札リクエストの結果を待って、一番 eCPM の優れた入札結果を選択する形式です。

Server-to-Server Header Bidding は、サーバー側で入札を行う形式です。Client Header Bidding との違いは、入札サーバーに 1 回だけリクエストを送信すれば良い点です。また、入札ロジック(例:SSP 各社からの入札結果の待ち合わせ、タイムアウト処理、入札結果の比較)を入札サーバーが担ってくれるので、クライアント側の責務が大幅に削減されることです。

現在は、Server-to-Server 方式が主流です。

Header Bidding Services

なお、Transparent Ad Marketplace(以下、TAM)という、Amazon が提供する Header Bidding の広告サービスを採用しています。

設計・実装

基本的には、TAMの提供するドキュメントに沿って、<head> タグ入札サーバーにリクエストするスクリプトを埋め込めば、導入は完了します。

しかし、弊社の場合

  • 独自の広告入稿・配信サーバーを介して自社広告・ネットワーク広告すべての広告を配信している
  • しかも、ページごとに配信される広告スロット(7)は静的ではなく動的に変化する

といった制約のため、スムーズな導入ができず改修が必要でした。

以下の図が、Header Bidding を行うまでの大幅な流れです。ここで、

  • ads ... 社内広告配信サーバ
  • display.js ... 広告表示用の JavaScript SDK
  • cookpad_ads-ruby ... display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
  • apstag ... TAM の提供する Header Bidding 用ライブラリ
  • googletag ... DFP の提供するアドネットワーク用ライブラリ

だとします。なお、googletag の公式ドキュメントは、https://developers.google.com/doubleclick-gpt/reference からご覧になれます。

f:id:itiskj:20180613203104p:plain

Header Bidding を実行するまでの大まかな流れは、以下の通りです:

  1. JavaScript SDK が、広告配信サーバーから表示すべき広告リクエストする
  2. 広告にネットワーク広告が含まれている場合、Header Biddingの一連の処理を開始する
  3. まずは、apstag, googletag それぞれの初期化を行う(例:デフォルトのタイムアウト設定)
  4. apstag を用いて TAM に Header Bidding リクエストを送る
  5. 入札結果をもとに、DFP に広告リクエストを送る
  6. DFP から広告リクエストが返却されたら、広告を表示する

ポイントは、Header Bidding をリクエストしている間、

  • googletag.pubads().disableInitialLoad() で DFP リクエストを中断し、Header Bidding を行う
  • 入札結果が返ってきたら、googletag.pubads().refresh([opt_slots...]) で広告のレンダリングフローを再開する

という点です。

結果

以上を持って、Header Bidding を導入するまでの一連の流れを説明してきました。具体的な数字はここでは伏せますが、今回の導入によってネットワーク広告の収益改善を実現することができました。

新たな課題

広告レンダリングフローのパフォーマンス悪化

しかし、ここで新たな課題も発生してしまいました。

それは、Header Bidding リクエストの分、ネットワーク広告が表示されるまでのレイテンシが増加してしまった、という点です。

広告が表示されるまでの一連のレンダリングプロセスを、以下の図に示しました。

Processing, DOMContentLoaded, load は、ブラウザが HTML/CSS をパースしてレンダリングするまで一連のフローの一般的用語です。気になる方は、Ilya Grigorik(8) による"Measuring the Critical Rendering Path" をご覧ください)

f:id:itiskj:20180613203121p:plain

ぱっと見て気づくのは、社内広告配信サーバの ads へのリクエストから、Header Bidding 、そして DFPへのリクエストまですべてがシリアルに実行されていることです。今回 Header Bidding を導入したことによって、その分レイテンシが増加したのです、大体 150 ~ 400 (ms) と、かなり致命的なパフォーマンス低下になってしまいました。

広告レンダリングフローの可視化がされていない

筆者の肌感で「150 ~ 400ms」と説明しましたが、実はクライアント環境で実行されるまでの広告レンダリングフローは、今まで計測・可視化されていませんでした。

上記で挙げた広告レンダリングフローのパフォーマンスを改善したいものの、ボトルネックが正確にはどこになるのか、計測するまでわかりません。計測できないものは改善できないと言われるように、まずは計測・可視化のフローを導入しました。

ここで幸いなことに、Fluentd にログを流し、分析可能なデータウェアハウス(cookpad の場合は、現状 Redshift)にテーブルを構築するまでの仕組みはすでに存在していました。したがってクライアント側と多少のテーブル定義を書くだけで実現できました。

(補足:cookpad におけるデータ活用基盤については、"クックパッドのデータ活用基盤" をご覧ください。)

以下は、取得したログデータをもとに可視化してみた様子です。ログを先日から取得し始めたばかりなので、可視化のフローはまだ未着手です。社内で推奨されている BI ツールにダッシュボードを作り、定点観測できるところが直近のゴールです。

f:id:itiskj:20180613203133p:plain

広告レンダリングのクリティカルパスは任意のタイミングでロガーを仕込むだけですが、DFP の場合、googletag.events.SlotRenderEndedEventを利用すると、広告枠が表示されたタイミング、広告が "Viewable"(9)になったタイミングでイベントを取得できます。

対策と今後の展望

以上が、Header Bidding の導入から、新たに浮上した課題への対策の説明でした。直近ですと、以下に取り組んでいく予定です。

  • 広告レンダリングフローの可視化フェーズ
  • 広告レンダリングフローの最適化

広告レンダリングフローの最適化

「広告レンダリングフローの最適化」では、自社の広告配信サーバーへのリクエストのタイミングを、今より前倒しにする方針で設計及び PoC の実装を行っている段階です。

具体的に言うと、現在 HTML ファイルの <body> 下部で広告レンダリングフローを開始しているのですが、それを <head> タグの可能な限り早い段階で開始するように改善をする必要があります(過去の設計の都合上、広告配信サーバーへのリクエストは、各広告スロットの HTMLElement 要素が レンダーツリー10 に挿入され、実際に描画されるタイミングにブロックされている)。

f:id:itiskj:20180614112206p:plain

パフォーマンスの可視化及びレンダリングフローの最適化についても、また別の機会にご紹介したいと思います。

まとめ

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。技術的にチャレンジングな課題も多く、非常に面白い領域です。ぜひ、興味を持っていただけたら、Twitter などからご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。


  1. 自社広告 … 自社独自の営業チームが、直接広告主と契約を結び配信している広告。自社で配信されるクリエイティブを運用できるため、意図しない広告が配信されることがない。

  2. ネットワーク広告 … 他社の広告配信会社が提供している広告配信サーバーを経由して、広告の買付け・配信を行う広告。各社が提供する <script> タグを HTML に埋め込み、返却された広告を <iframe> にレンダリングする形が一般的。

  3. フロアプライス … 最低落札価格のこと。例えば、「フロアプライス $0.8」広告をリクエストしたとき、$0.8 以下の広告枠の買付けは行わない。

  4. 入札結果 …SSP 各社が、広告枠をいくらで買い付けるかを示す価格。これがフロアプライスより低い場合、広告枠に広告が表示されることはない。

  5. ウォーターフォール方式 … SSP 各社定義した順番で一つ一入札していく方式。「滝」語義が表すとおり、上から順に問い合わせ得ていく様子からこの名前で呼ばれる。

  6. アドサーバー側にどのように入札結果を伝えるかは、各アドサーバー側の仕様や実装に依存する。例えば TAM が DFP に対して Header Bidding を行う場合、Key/Value Targeting の仕組みを使っている。

  7. 広告スロット … 広告枠が表示される枠のこと。

  8. Ilya Grigorik … Google のエンジニアで、“High Performance Browser Networking” の著者、と言えばわかる方も多いかもしれません。完全に蛇足ですが、尊敬しているエンジニアの1人で、彼の著作をきっかけ Web の裏側に興味を持ちました。

  9. Viewable Impression … 広告が「ユーザーに見える状態」になったかどうかでインプレッションを測定している。詳細については、“Viewabiliity and Action View”を参考のこと。

  10. Render Tree … https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/