こんにちは。データサイエンス部の石川です。
この記事は Gunosy Advent Calendar 2023 の 7 日目の記事です。昨日の記事は johnmanjiro さんの『tokio ベースの Rust 向け fluentd クライアントを作りました』でした。
先日、オンライン実験を速く試すための基盤を構築し、実際に A/B テストを開始しました。この記事では初めに弊社独自の A/B 管理基盤やサービス間の依存関係に由来する課題を整理したうえで、どのような解決策を実装したかについて紹介します。
背景
はじめに広告関連のサービスの概要と、それぞれのサービスの依存関係について説明します。 次に、施策の検証において求められていることについて整理します。
広告関連サービスの概要と依存関係
広告配信に関連するサービスはマイクロサービス化されており、中でも A/B テスト管理基盤、広告配信サーバ、および広告スコアリングサーバが広告配信の最適化の A/B テストに大きく関わっています。
A/B テスト管理基盤
A/B テスト管理基盤は、広告関連のサービスにおいて実施される A/B テストを効果的に管理するための基盤です。
基本的な機能としては yaml ファイルを使用して A/B テストを設定できるものがあります。バリデーション機能や共通部分が多い A/B テストの定義コードの自動生成のために、Go 言語を用いています。
このため、同じく Go 言語で書かれている、A/B テストを利用するサービス(広告配信サーバ)に対してプライベートパッケージとして A/B テストの定義コードを提供することができます。このアプローチにより、サービス側では yaml の読み込みに必要な構造体を再定義する必要がなく、コードを共通化することが可能となります。
詳細は以下のブログ記事で紹介されています。
広告配信サーバと広告スコアリングサーバ
広告配信サーバは、クライアントからの広告リクエストに対して広告を配信することに責務を持ったサービスです。
一方、広告スコアリングサーバは、広告配信を最適化するため、 ML モデルを活用して広告のクリック率 (CTR) やコンバージョン率 (CVR) の予測を行い、広告の入札単価やスコアの計算を行います。
これらのサーバ間の通信は gRPC が使用され、データのフォーマットは protobuf ファイルに定義されたスキーマに従います。
詳細については以下のブログ記事で詳しく解説されています。
つまり、広告配信サーバは A/B テスト管理基盤で定義されたパッケージと、広告スコアリングサーバの protobuf ファイルから生成されたコードに依存していることになります。
施策の検証への要求
施策の検証において、オフライン実験とオンライン用のコード開発工数が同程度である状況が多く見受けられます。具体的な施策の効果を確認するためには、実質プロダクションリリースに必要な工数がかかることが多く、そのためには手軽かつ迅速に情報を得る手段が求められます。ROI を最大化するためには、工数のかかる施策の効果の確度を向上させる必要があります。
こうした課題に対処するために、長期的な運用を考慮した工数が大きい実装に取り組む前に、手軽に情報を増やすための実験が有効です。オンラインでの実験を通じて、施策の効果を確かめることは、実際の運用において重要な意味を持ちます。また、実験の立案者による理想像の相違からくる課題にも、実施前に手軽な実験を行うことで迅速かつ効果的に対応できます。
課題
A/B テストの実施において、A/B テスト管理基盤、広告配信サーバ、広告スコアリングサーバがそれぞれ異なるリポジトリに分かれていることから、一つのロジックを適用する A/B テストを試す場合に、変更が必要なリポジトリが 3 つにも及び、手順が複雑かつ時間がかかるという問題が生じています。
具体的な手順
- A/B テスト管理基盤で A/B テストのパラメータの構造体を定義する
- 広告配信サーバで A/B テスト管理基盤のパッケージを読み込む
- 広告スコアリングサーバで広告配信サーバから受け取る A/B パラメータを protobuf ファイルに追加する
- 広告配信サーバで広告スコアリングサーバへのリクエストに A/B パラメータを追加する
- 広告スコアリングサーバでロジックを実装する
- A/B テスト管理基盤で A/B 設定に関する yaml を追加する
上記のような煩雑な手順を踏む必要があり、また仕組みが整備されてないために、実際に以下のような A/B テストを試す際にもいくつかの課題が生じました。
実施した A/B テストは以下の通りです。
- 特定のユーザー群に対して、特定のロジック(ある変数に対して単純な関数を適用する)を適用する A/B テストを実施した
- デプロイの実装コストを避けつつオンラインで検証するため、ユーザーリストをアプリケーションに内包させる形でデプロイすることにした
これによって、以下のような課題が生じました。
- 施策のリターンが実際にどれくらい見込めるか不明瞭だった
- データとコードが分離されていないため、ユーザーリストの更新にはアプリケーションのデプロイが必要だった
- 1 回の CI に結構な時間がかかる事情のため、ステージング環境とプロダクション環境への 2 回の CI が走ることで、ユーザーリストの更新をするだけの作業にも時間がかかった
解決策
上記のような課題に対して、以下のような解決策を考えました。
- A/B テストの「枠」をあらかじめ用意しておく
- データは S3 にアップロードされれば、広告スコアリングサーバが利用できるようにしておく
上図について説明します。
まず図の左側では、あらかじめ A/B テスト管理基盤で適当にいくつかのロジックを持たない空の A/B テストを用意しておきます。 合わせて A/B テストの定義コードを広告配信サーバにプライベートパッケージとして利用可能にしておきます。
また広告配信サーバでは、広告スコアリングサーバへの gRPC リクエストに A/B パラメータを追加する処理を実装しておきます。 このときに、パラメータの一つとしてロジックのオンオフを制御するフィールドを追加しておきます。 これによって A/B テストの実施における準備の工数を削減することができます。
続いて図の右側では、まず最初にユーザーリスト・広告リストを生成する SQL クエリをレビューしてもらいます。 SQL クエリでリストを生成するのは、広告ロジックチームだけでなく、SQL を主に利用する BI チームも容易にリストを生成できるためです。
問題なければ、Digdag を使ったバッチ処理で S3 の特定のパスに CSV 形式でユーザーリスト・広告リストがアップロードされます。 必要なユーザーリスト・広告リストが揃ったら、別の Digdag のワークフローで CSV ファイルを SQLite のデータベースファイルに書き込む処理を行います。
あとは 広告スコアリングサーバが書き込み済みの SQLite のデータベースファイルを定期的に読み込むようにするだけです。
これによって、以下のようなメリットを得ることができました。
- デプロイの実装コストを避けつつオンラインで検証することができた
- データとコードが分離されたため、ユーザーリスト・広告リストの更新にアプリケーションのデプロイが不要になり、更新にかかる時間が短縮された
ユーザーリスト・広告リストの詳細
ユーザーリスト・広告リストの詳細について、具体的にいくつかの議論事項を取り上げて、どのように実装することに決定したかを整理します。
全ユーザー・全広告を表現したいときどうするか?
ロジックを適用させたいユーザー・広告 ID を列挙する必要があるのですが、全てのユーザー・広告を表現したい場合にはメモリやファイルの容量が足りなくなる可能性があります。
そこで以下のような選択肢が考えました。
- 全 ID を列挙する
- 特殊な割り当て(e.g. ID 列に -1 を入れる)を作る
- ID を指定するファイルを作成しない(指定しないことで全件を対象にする)
それぞれの選択肢について、メリット・デメリットを整理します。
全 ID を列挙する
- Pros
- シンプル
- Cons
- ユーザーリストのメモリ消費が大きすぎる
- 時間経過で漏れが出てくる
- Pros
特殊な割り当て(e.g. ID 列に -1 を入れる)を作る
- Pros
- 複数のユーザー・広告の組み合わせを用意する場合、特定の組み合わせに対してのみ全件を対象にするといった表現が可能
- Cons
- 特殊な割り当てに関する知識を必要とする
- 実装が複雑になる
- Pros
ID を指定するファイルを作成しない(指定しないことで全件を対象にする)
- Pros
- シンプル
- Cons
- ファイルが置かれてないのが意図的なのかミスなのか判定できない
- ミスの場合想定外にロジック適用対象が増えてしまう
- 複数のユーザー・広告の組み合わせを用意する場合、特定の組み合わせに対してのみ全件を対象にするといった表現ができない
- Pros
以上から、複数ユーザー・広告の組み合わせを表現できる 2 番の特殊な割り当てを作ることにしました。
API はどのようにしてユーザーリスト・広告リストを持つのか?
API はユーザーリスト・広告リストを持つ必要があります。これは、ユーザーリスト・広告リストを持つことで、特定のユーザー・広告に対してのみロジックを適用することができるためです。
ユーザーリスト・広告リストを持つ方法としては以下のような選択肢が考えられます。
- 全部メモリに乗せる
- ファイル DB (SQLite etc.) を使う
- Redis Cluster に突っ込む
それぞれの選択肢について、メリット・デメリットを整理します。
全部メモリに乗せる
- Pros
- 実装はシンプルになる
- Cons
- レコード数は減らないのでカラム数による
- ランタイムのメモリリソースが多くかかる
- 組み合わせが増えた場合に対応が難しくなる
- Pros
ファイル DB (SQLite etc.) を使う
- Pros
- メモリの消費量は少ない
- 全件メモリに乗せるわけではない
- クエリで対象を簡単にフィルタリング出来る
- Cons
- スピードが問題になる可能性はある
- メモリ上の cache が必要になる可能性がある
- Pros
Redis Cluster に突っ込む
- Pros
- メモリの消費量は少ない
- 全件メモリに乗せるわけではない
- Cons
- ランニングコストが掛かる
- I/O がボトルネックになる可能性がある
- Pros
以上から、手軽だがメモリ消費量の少ない 2 番のファイル DB を使うことにしました。
まとめ
この記事では、オンライン実験を速く試すための基盤構築について紹介しました。
具体的にはオンライン実験を速く試すための基盤構築について、弊社独自の A/B 管理基盤やサービス間の依存関係に由来する課題を整理したうえで、どのような解決策を実装したかについて書きました。
今後は、実際に A/B テストを開始した結果や、その後の課題の対応について紹介できればと思います。
次回は koizumi さんの『インシデント発生時における初動対応の自動化』です。お楽しみに!