Kekeの日記

エンジニア、読書なんでも

アドテクコンペに参加!わずか0.1秒の過酷な広告の世界

はじめに

広告がどう決まるのかは説明しますが、お金の流れなどは説明しません。

今回はインフラやサーバーサイド、チーム開発のお話です。

参加したコンペ

今回は以下のアドテクコンペにサーバーサイドエンジニアとして参加してきました。

www.cyberagent.co.jp

コンペの内容

4時間の間で広告主からもらった予算を使って、なるべく広告を出し切り、利益を最大化することを競います。

目次

広告の世界を知る

広告が決まる仕組み

広告が決まる仕組みは一言で言うと

広告枠によって開かれるオークションの勝者

です。

たとえば以下のYahoo!Japanのサイトには(動画)広告が導入されています。

f:id:bobchan1915:20180924102739p:plain

これは見るたびに違うケースが多いと思います。

実は以下のような流れで決まっているのです。

1. ユーザーが広告のあるサイトに遷移しようとする

f:id:bobchan1915:20180924104605p:plain

2. 広告枠に対して広告をリクエストする

これはユーザーには、まだ見えない世界です。

f:id:bobchan1915:20180924104721p:plain

広告枠があるのでSSPに広告をくれっとリクエストします。

f:id:bobchan1915:20180924104732p:plain

ここでSSPを説明します。

Supply Side Platform(サプライサイドプラットフォーム)の略で、媒体の広告枠販売や広告収益最大化を支援するツールです。

有名なところだとGoogle, Yahooなどが運用しています。 僕は以下のような感じでSSPを捉えています。

広告枠に対して広告をオークションによって選んで、ユーザーにもいい広告を、広告主にもいい影響(コンバージョンなど)を目指しているサービス

本題に戻して、SSPにリクエストがくると、オークションがはじまります。

f:id:bobchan1915:20180924105318p:plain

するとDSPと呼ばれる、実世界でいうと広告代理店のようなものが広告を入札します。

DSPとは

Demand-Side Platformの略称で、広告主(広告配信を希望している側)のプラットフォーム。

です。広告主から予算をもらって、広告を出す責任があります。

世の中にはたくさんのDSPがあり、一気に入札します。

f:id:bobchan1915:20180924105540p:plain

そしてSSP側は、もっとも高い広告をユーザーに返します。

f:id:bobchan1915:20180924105638p:plain

すると、ユーザーの画面の広告枠に表示されて、はじめて目にするわけです。

f:id:bobchan1915:20180924105712p:plain

また、この中でいつまでもオークションが開かれるわけではなくてDSPは100ms以内に返さないと無効なリクエストだと判断されます。

f:id:bobchan1915:20180924112857p:plain

まとめると以下のようになっています。

https://www.innovation.co.jp/urumo/images/2017/07/dsp-1.png

0.1秒ってどのくらい

0.1秒でこんなことをすべてやらないといけないのですが、実際にどのくらいの速さか比較しています。

アクション およその時間(秒)
瞬き 0.3
人間の限界反射速度 0.2

ソリューション

アーキテクチャ

Google Cloud Platform(GCE)を使って以下のように構築しました。 もちろん実質2日間ともあって、もっと必要な機能が、改善点は大いにあります。

https://camo.githubusercontent.com/1965deed73f83fc4a8e31d2f523aa1506a727b87/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3135333332302f37303430623861612d376538302d336535392d656630362d3532333662383730623739382e706e67

それぞれのGKEクラスタの役割

以下のようにクラスタが分かれています。

クラスタ名 役割
Auction オークションの入札に関する責務をもつクラスタ
Win オークションで勝ったときの結果の通知を受けるクラスタ
Influx 時系列データベースinfluxDBのクラスタ
Collection ニア・リアルタイム分析のための集計データを受けるクラスタ

それぞれのGCEインスタンスの役割

以下のようにインスタンスを構築しました。

インスタンス名 役割
Redis 最新の予算をキャッシュしておくRedisインスタンス
Grafana 監視ツールであるGrafana用のインスタンス

Cloud Storageの役割

事前に配布された静的ファイルを共有空間に置くのが目的です。

広告主の予算が.jsonで保存されてあったりします。

Cloud Registryの役割

f:id:bobchan1915:20180924215124p:plain

開発したコンテナを格納し、GKEクラスタがpullして使います。

Cloud SQLの役割

f:id:bobchan1915:20180928142521p:plain

MySQL入札したオークションのトランザクションを格納しています。

Cloud Functionの役割

f:id:bobchan1915:20180924220404p:plain

最新の予算残高の復旧用APIを用意しています。

Node.jsランタイムを使用しています。 発表のときはgopythonしか使ってないようなことをいいましたが、ここではjavascriptを使っています。

Redisはインメモリで扱うために、Redisが落ちてしまったときなど、データが吹き飛ぶ可能性があります。

ここでは

  • Cloud Storageにある広告主の予算
  • Cloud SQLにある広告主ごとのトランザクション

を元に

(予算) - (トランザクションの総和)

を計算して、最新の残高を復旧しています。

設計ポイント

ゴール

コンペやハッカソンで「動けばいい」というようなスタンスの方は多くいます。

もちろん価値観でもあるし、ゴールの違いもあるかと思いますが

プロダクションとしての生命を考える。

ことが僕のゴールでした。

人員的にもコンピューティングコスト的にもスケールするような、機能は増やしやすいような、やりたいこともトレンドも押し寄せてくるけど取り入れやすいような、柔軟なプロダクトにしたかったのです。

マイクロサービス化

特にGKEクラスタの場合は、以下のように単一のインスタンスに置き換えても全く問題はありませんでした。

f:id:bobchan1915:20180924114059p:plain

そして、エンドポイントをパスで分けて全知全能なZeus APIと用意してもよかったでしょう。

しかし、チームでこれから開発していく、コンペは終わってもプロダクトだったら開発は数年にも続くかもしれないと思ったときに、マイクロサービス化することにしました。

そして

  • APIの責務を分離する
  • ソースコードも分離する
  • 個々のエンジニアのタスク分野を明瞭にする
  • コンピューティングコスト的な分離、管理のしやすさを徹底する

のような恩恵も、短い期間でしたが感じることができました。

ここで特に意識したことがはじめからユビキタス言語を使うことです。

機械学習の部分はml APIと呼ぶことにし、

  • 機械学習エンジン
  • AI エンジン
  • 予測API

などとチーム内で他の言い方にしないように徹底しました。

マイクロサービスだからこそ、双方の言っていることが別のこと指さないように注意したところがよかったです。

クリーンなMVC

特にどのAPIもMVC設計しました。 HTTPリクエストであることや、マイクロサービスなので比較的機能が少なくなりがちで、その程度ならMVCがもっともわかりやすいだろうと思って採用しました。

例として、Auction APIではmain.goが以下のようになっています。

func main() {
    host, port := helpers.GetEnv()

    c := controllers.NewController(host, port)

    e := echo.New()
    e.POST("/v1/auction", c.GetAd())
    e.GET("/healthy", c.HealthCheck())
    e.POST("/v1/alpha", c.SetAlpha())

    e.Debug = true

    e.Logger.Info(e.Start(":5050"))
}

もちろんドキュメントも書きますが、コードを見ただけでも何をするべきかわかります。 コントローラーは/controllers/controllers.goにあって、

func (c Controller) HealthCheck() echo.HandlerFunc {
    return func(ctx echo.Context) error {
        ctx.Echo().Logger.Info("Status 200")
        return ctx.String(http.StatusOK, "Haha, I am very healthy!")
    }
}

のようにメソッドを定義しています。非常に何が、どこにあるのかがわかりやすいかなと思います。

Pod間通信

GKEに使われているKubernetesはあくまでもDockerなどのコンテナオーケストレーションソフトウェアです。

言いたいのは、基本的にはDockerコンテナを組み合わせているので、仮想コンテナ内で通信が済むなら、外部ネットワークに出すよりもかなり高速です。

たとえばAuctionクラスタでは以下のように使っています。

f:id:bobchan1915:20180924115625p:plain

特にここでは100ms以内に返さないといけないので、意識しました。

特にKubernetesではDNSラウンドロビンによってService名でDNSに問い合わせるたびに異なるPodのIPアドレスが返ってくるためL4のロードバランシングをすることができます。

ニア・リアルタイムモニタリング

今回は時系列データベースinfluxDBとGrafanaを使いました。

f:id:bobchan1915:20180924123326p:plain

特に(実際の)チーム開発では、マーケッター、エンジニア、ビジネスマン、デザイサーなどいろんなロールの人がいます。

同じデータでも知りたい情報は異なるし、 可視化の方法も違うでしょう。

今回は時間の関係上、レスポンスタイムしか取得することができませんでした。

f:id:bobchan1915:20180924123508p:plain

最新の予算残高分は、APIもDB設計もしましたが収集部分が間に合わなかったです。

反省点

メッセージセマンティクスと予算の整合性のアプローチ

HTTPリクエストは、そのもの自体はExactly onceなので特にメッセージセマンティクスを意識することはないかもしれません。

しかしながら、今回は広告主の予算(=お金)を扱っていたのでオークションを通して、整合性が取れるかを気にしていました。

実際では、オークション後の広告が表示(インプ)されたりすると、必ずしもプロセス時間、取得時間などが一致するとは限りません。

特にそのようなケースでは概算値で計算したり、正確な残高はバッジ処理をしていく必要があるかなと思います。

広告は直接的なお金の取引ではないので、整合性は企業のアプローチでどうにかなると思いました。

徹底的な時間計測と可視化

私たちは入札をできるようにはできたのですが、100msに間に合わないケースがかなりあって、広告消費スピードは遅かったです。

マイクロサービスにしたからこそ、ネットワークがボトルネックになる可能性があるので計測をもって検証する必要があったと思います。

また、本質的な通信部分以外にも、負荷に耐えられずリクエストをブロックしているのではないかと思ってクラスタの可視化も必要だなと思いました。

特にこの規模の開発だと、Istioなどを使うとサービスメッシュ間のレイテンシ取得もできるなどより原因解明に繋がると思いました。

istio.io

また、ネットワーク関連のパフォーマンスをあげる策は以下のようなものがあるのではないでしょうか。

  • 内部ネットワークをHTTP1.1から変更する
  • PrivateクラスタにしてVPSでマルチクラスタをかぶせる
  • Podのスケジューリングを見直す

特にAffinityを管理してPodの高度スケジューリングをすればよかったのですが、指定したことがなかったのでそれに及びませんでした。

コンペ後にPod高度スケジューリング機能を検証したので、記しました。(追記日: 2018/09/26)

www.1915keke.com

データエンジニアの責務

私は特にデータエンジニアに興味があります。

具体的には

  • データサイエンティスト
  • インフラエンジニア
  • SRE
  • サーバーサイド

をまとめたような役割だと自分は思っています。ソフトウェア2.0と呼ばれていてAIというものがサーバーサイドなどの道具として広がるなかで、特定の属性だけに依存するのは、サービスを作る観点から好ましくはないのかなと思いました。

特にサービスを全く知らないデータサイエンティストもいれば、機械学習を全く知らないサーバーサイドエンジニアもいるわけで、「データエンジニア」とまとめて、連絡路的なエンジニアが必要なのではないかと感じました。

コンペ後に原因を調査し、改善したので追記します。(追記日: 2018/09/27)

レスポンスが100msに間に合っていなかったのは以下のようなレイテンシも一因としてありました。

clf=joblib.load('/deploy/lr_twofreature.pkl') 

これを毎回のレスポンス中で行なっていたために、標準入力のコストがかかってしまいパフォーマンス的にはよくありませんでした。

ファイルの標準出力がある状態では以下のようにレスポンス時間はかかっています。たとえば1000回リクエストをさばいたとします。以下のような結果になりました。

なお、ローカル環境です。Pod起動時にだけモデルを読み込むと以下のような結果です。

  • 平均値: 4.37 ms
  • 最大値: 13.1 ms

https://cdn-ak.f.st-hatena.com/images/fotolife/b/bobchan1915/20180928/20180928213417.png

元はというと毎回モデルを読み込んでいたのでレイテンシがありました。

  • 平均値: 5.61 ms
  • 最大値: 18.3 ms

f:id:bobchan1915:20180928215301p:plain

このようにローカル環境でも約1.4msくらいのパファーマンス改善がありました。 たった一回のファイル読み込みですが、要求されている100msの100分の1.5くらいは改善できました。

1.4秒が大きいというわけではなく、今回はたまたま一回だったものが別の機会に複数回あると大きなボトルネックになるよという反省でした。

データサイエンティストだけだと、このようなことが発生しえるので両方の知見があるデータエンジニアが橋渡しをする必要がありました。

エンジニアとしてのポリシーと信頼

発表スライドで他社製品ロゴ等を使用する場合は、その相手方に迷惑をかけないように、また、エンジニアとしても信頼を高めるためにポリシーを守らないといけません。

今回はGCPを使っていましたが、GCPが提供する使用ポリシーを守ってプレゼンを構築することができ、エンジニアとしては大事なことではないかと思っています。

たとえば、インフラやネットワークの色は以下のようにするべきと決まっています。

f:id:bobchan1915:20180924205520p:plain

また今回は使いませんでしたが、slackのロゴ使用は以下のようなブランドガイドラインが決まっていて、ロゴ等を使用する場合は守る必要があります。

f:id:bobchan1915:20180924205633p:plain

このように一つ一つお世話になっているものに対して相手側のものを使うならエンジニアならば良好な関係を築き、信頼度を高め流必要があります。

また、副産物としてスライドや資料が見やすくなると思っています。

そのような面で、発表スライドを短期間で作らないといけなかったたため、完全にポリシーを確認しきれていない部分があります。

まとめ

アドテクとは

広告とは、経済の循環を感じる大きなシステムで、また、人間的なクラシックなシステムだと思います。

僕自身も広告から知ったサービス、アプリ、イベントで有意義なものが多く、またそれを提供している方もそうでしょう。

価値を届ける、受け取るという面では素晴らしいシステムだと感じました。

その広告を、テクノロジーと掛け合わせて、よりここの人間に合わせていく、世界をよくしていくのでは?と実際に入札までしてみて、強く思いました。

インフラ自体はよく2日でやり切れたなと思います。

まだまだチームビルディング、エンジニアの能力、スタンス、日々の態度、アプローチなどなど、、、まだまだ大いに勉強することはあると感じたコンペでした。