LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


【インターンレポート】テスト広告の配信を配信先の端末を登録して配信する仕組みにした話

自己紹介

こんにちは。東京大学大学院情報理工学系研究科修士1年の古田悟です。8月22日から6週間、LINE の就業型インターンに参加させていただきました。

この記事では、今回のインターン期間中に私が実装した機能の開発背景と過程についてお話ししていきます。

広告ネットワークについて

私が配属された LINE広告ネットワークチームは、LINE BLOGなどの LINE のファミリーアプリや、その他のサードパーティーアプリに広告を配信するためのプラットフォームを開発・運用しているチームになります。

インターネット広告業界には、広告枠を提供する業者 (サプライ) と広告を出稿する業者 (デマンド)、そしてその間をつなぐ業者の3者が存在します。サプライ側とデマンド側を繋ぐ業者は、サプライ側を束ねる SSP (Supply Side Platform) とデマンド側を束ねる DSP (Demand Side Platform) に大きく分けることができます

私が配属された広告ネットワークチームは、このサプライ側の業者を束ね、広告を配信する仕組みを提供しているチームになります。
サプライ側を束ね各アプリに広告を配信すると言っても、それらのアプリの開発に直接このチームのメンバーが携わっている訳ではありません。代わりに SDK (Software Development Kit) と呼ばれる、ある機能を開発するために必要なツールが集まったパッケージを各パブリッシャーに配布しています。SDK には機能を呼び出すインターフェースやプログラム例、ドキュメントなどが含まれています。アプリ開発者の方々は SDK を利用することで、アプリの中の指定した枠に表示する広告を、LINE広告ネットワークの広告配信サーバから取得するという機能をアプリに組み込むことができるようになります。
こうして LINEアプリ以外のアプリにも LINE の広告ネットワークを通した広告を配信することが可能になっています。

テスト広告配信について

今回のインターンではテスト広告配信の仕様変更を担当しました。

アプリ提供者はアプリを実行可能な状態にした (= ビルドした) 後、アプリを公開する前に動作のテストをする必要があります。テストをしないとアプリに対する様々な操作に対して想定した動きをするかが確認できないからです。
広告の表示などもテスト対象ですが、通常の広告はターゲット属性や表示回数などの影響で安定して表示されないこともあるため、こうした確認はテストモード専用の広告を使って行います。
私たちの広告配信サーバはアプリから送られてきた広告リクエストがテスト用のものかどうかを判定し、テストモードのリクエストに対してはテスト用の広告を、そうでない場合には通常の広告を返すような動作をしなければなりません。

広告リクエストがテストモードかどうかの制御は、現在 SDK の is_test というパラメータの true / false を切り替えることによって行なっています。この切り替えは SDK を組み込むアプリ側がソースコード上で指定して行なっていることが主です。
このパラメータが true でビルドされたアプリの広告リクエストはテストフラグが ON の状態になるため、そのアプリからのリクエストにはテスト広告を返すことができる仕組みです。

このテスト広告配信の仕組みでは is_test パラメータが true のままビルドされたアプリを誤って配信してしまった場合、一般のユーザーの方にもテスト広告が配信されてしまうというリスクが存在します。その場合、テスト用の広告では広告収益が入らないため、パブリッシャーの利益損失にもなってしまいます。こうしたミスはサーバサイドでリカバーできるようになっているべきですが、is_test パラメータはアプリのソースコード上で指定されている場合が主なので、こちら側での対策が難しくなってしまいます。テスト配信自体の仕組みをサーバサイドでコントロールできる仕組みに寄せていくことが求められていました。
その手段の一つとして、広告リクエストがテストモードかどうかをアプリに組み込まれる SDK のパラメータではなく、そのリクエストを送ってきた端末が検証用端末かどうかによって判定する方法が考えられます。具体的には、事前にパブリッシャーがテストに用いる端末の広告IDを登録しておき、広告リクエストが送られてきた際に送ってきた端末の広告IDがテスト用広告IDとして登録されているか否かを判定する仕組みです。
この機能はソースコード上での指定に比べて利便性も高いので、パブリッシャー側からも実装が求められていた機能でした。

実装について

設計

今回のインターンでは、リクエストを送ってきた端末がテスト端末かをサーバ内で判定する部分の実装を行いました。作成するものは下の図のようにまとめられます。
(登録用WebUI と MySQL 部分は未実装なので括弧で括っています)

パブリッシャーによって WebUI から登録されたデータは MySQL に保存され、そのデータの中からサーバでの判定に必要なデータのみが Redis に同期されます。これらのデータベースは所属チームの他の機能に合わせて選定しました。
広告配信サーバはトラフィックが非常に多く、応答時間も短く保つことが求められます。端末の広告ID判定は広告リクエストが来るたびに行われるため、その判定は素早く行われなければなりません。判定のたびに Redis にデータを読み込みにいくのではこうした条件を満たすことができないため、サーバにキャッシュを持たせることとしました。
今回のキャッシュは端末の広告ID判定の目的で利用するため、登録されている全ての広告IDをサーバが持っていないとキャッシュの意味がなくなってしまいます。これまでのテストログから、全てのパブリッシャーの検証用広告IDをサーバに持たせていてもデータ量的に大きな問題がないことも確認できたので、Redis 内の全てのデータをサーバにも持たせておく仕様としました。

広告リクエストが来るたびにリクエストを送ってきた端末の広告IDがサーバにある登録済み広告IDに含まれているかを判定します。従来用いられていた is_test パラメータについては、自動テストなどを組んでいるパブリッシャーもいることが考えられたため廃止はしないこととして開発を進めました。従来の is_test フラグと端末判定の結果が合わさってテストモードかを判定しています。

開発過程

データベース整備

「設計」の項にある通り、パブリッシャーによって登録されたデータは MySQL から Redis、サーバ内にキャッシュされます。MySQL に保存するデータは登録用WebUI の仕様に応じて必要な項目が変わってきますが、この仕様策定にあたっては事業・企画側との合意が必要になってしまいます。
そのため、まずはこのデータ層のうち Redis とサーバキャッシュ部分の実装を行いました。

具体的なサーバの更新メソッドは

a. Redis データの追加/更新があったときに、追加/更新されたレコードのデータのみを取りに行く
b. サーバの再起動や Redis テーブルの一括更新の時のために、全レコードを取りに行く

という2種類の実装を用意しました。これらの更新メソッドは複数同時に呼び出される可能性があり、その処理には注意が必要でした。
広告配信サーバでは高トラフィック低レイテンシが求められるため、データの入出力中に他の処理をブロックしない non-blocking I/O を利用した非同期な並列制御を行っています。そのため複数の更新メソッドがほぼ同時に呼び出された場合には並列処理によって操作の上書きなどが行われてしまう可能性があります。それを避けるためにデータの更新を同時に一つしか走らせないロック制御を途中に組み込んでいます。

今回のキャッシュの特殊な点は、更新メソッドが呼ばれる頻度はそこまで高くない一方で、キャッシュ読み込みのリクエスト頻度は極めて高いという点です。
ロックをかける際、データ操作中はデータ自体にロックをかけ、取得/更新に関わらず一つのデータアクセスしか許さない実装が行われることがあります。今回の条件で更新中にデータ取得ができなくなってしまうと、頻繁に発生する広告リクエストを処理することができなくなってしまうという問題が発生します。
そのためロックは更新系の操作に対してかけるような実装にし、取得はこれと独立して行えるようにしています。キャッシュ更新の操作を単一に保つことで整合性を担保しつつ、広告リクエストは止まらずに捌くことができるようになっています。

Redis の読み込みにはサーバ内の処理に比べると時間がかかります。更新系操作のロックをかけるにあたっては、Redis 読み込み部分から一つの操作しか走らないようなロックをかけてしまうと複数の処理に時間がかかってしまうことが考えられました。そのため Redis から読み込んだデータをサーバに同期する部分にロックを入れています。
この実装の下で単純な読み込みデータの同期を実装してしまうと、2つの更新メソッドの内 b の更新の直後に a の更新が発生した場合に Redis のデータ読み込み量が少ない a の同期の方が先に走ってしまい、後から走った b の更新によって a の更新が上書きされてしまう可能性が考えられました (非常に稀なケースですが)。Redis・キャッシュに保存するデータに更新時間のタイムスタンプを持たせておき、更新の際には同じデータのタイムスタンプを比較する処理を入れることで、こうした上書きを防いでいます。

テスト端末判定

判定については単純に、リクエストを送ってきた端末の広告IDがサーバにキャッシュしてある広告IDに含まれているかで判定します。
当初実装が楽という理由でアプリからの広告リクエストの中身をサーバ内で使う表現に変換する部分 (parser) に端末判定を組み込んでいたのですが、検証判定のようなロジックと parser は分離すべきという指摘をいただき、リクエストのパラメータがサーバ内のロジックと合わさる部分に組み込む形に変更しました。こうしたコードの書き方に留まらないレビューもいただけたのも勉強になったポイントだと感じています。

テストモードか否かでの制御

これまでリクエストがテストモードか否かの判定は、parser によって変換されたリクエストの is_test パラメータを直接参照することによって行なっていました。
前段の変更によりこの参照先を変える必要が出たため、サーバ内の該当部分とそれに対応するテストの修正を行う必要がありました。この作業は技術的に難しいポイントは全くなかったのですが、大規模開発ならではの一部の変更が各所に及ぼす影響の大きさについて実感を持つことができました。

テスト動作

以上のような実装の後に動作検証用の環境でサーバの動きをテストしました。Redis に直接検証端末の広告ID情報を書き込むバッチ処理を走らせ、検証用アプリで動作確認を行いました。結果として無事に is_test パラメータが false の時にもテスト広告が表示されるようになりました。(とてもわかりづらいですが笑)

WebUI の仕様策定や作成は今後のタスクとして残ったものになります。

インターンの感想

チームで主に開発に用いられていた Scala はそれまで触ったことのない言語で、Redis を始めとするデータベース周りの知識もほとんど持っておらず、さらにはインターネット広告の仕組みなどもキャッチアップが必要であったため、インターン開始当初は「自分は何もできないかもしれない...」と非常に焦っていました。それでも、メンター・上長のお二方のみならず、チームの皆さんが質問に対してすぐに答えてくださる体制になっていたおかげで、必要な知識をすぐに補うことができました。また、そんなレベルが足りていない状況にも関わらず、無謀にも「影響の大きいサーバのロジックをいじってみたい」と希望した私の意向を最大限に尊重してくださり、要望に合うタスクを用意し、最後までサポートしてくださったことは本当に感謝しています。

LINE の広告ネットワークという大規模な基盤に自分の開発を組み込めたことは、非常にやりがいのある経験でした。一方で、初めての大規模なシステムに対して、関連コードの把握に手間取ったり、自分の変更が影響する範囲の広さに慎重になりすぎたりした結果、開発が思っていたスピードでは進まず、目標にしていた WebUI の作成まで終わらなかったことについては若干の後悔が残っています。ただそうした反省も含め、大きく成長につながった6週間でした。この経験を今後の活動に繋げていきたいと思います。