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

テクノロジー

広告配信システムの統合とモダン化 〜10年分のレガシー脱却~

本記事は2022年11月に開催した「Tech-Verse 2022」で発表したセッションを要約したものです。アーカイブ動画を文末に掲載しています。質疑応答の様子も収録されていますのでぜひご覧ください。

ヤフーが提供する「Yahoo!広告 ディスプレイ広告」では、パソコンやスマートフォンなどヤフーのサービスをはじめとしたさまざまな広告配信枠から広告リクエストが送られてきます。そのリクエストは約数十万rpsに上るため、高い性能要件が求められます。また、広告のターゲティングや最適化といった処理も必要です。私たちのチームではこうした大量のリクエストをさばき、複雑な処理を行う広告配信システムを開発・運用しています。

従来の広告配信システムには課題がありました。それは「Yahoo!ディスプレイアドネットワーク」や「インタレストマッチ」というサービス名だった過去の時代から同じアーキテクチャで運用されてきたことです。これにより徐々にシステムやコードの品質に課題が発生していました。今回はそうした課題をどのように解決したかご紹介します。

広告配信システムの統合とモダン化

私たちは広告配信システムの統合とモダン化を行いました。具体的な取り組みは以下3つです。

1. コンポーネント統合

1つ目はコンポーネントの統合により、通信処理順の最適化を行いました。

今回、2つのコンポーネントに関して処理内容からその役割を改めて見直したところ、1つにまとめた方が処理を最適化でき、今後の改善にもつなげやすくなることから統合を決定しました。

従来の広告配信システムでは、広告配信サーバーと、広告最適化サーバーに分かれていました。広告リクエストが来ると、広告配信サーバーはサーバーAに通信を行い、広告最適化サーバーにリクエストを送信します。広告最適化サーバーはサーバーBとサーバーCに通信を行っていました。この時、サーバーAとBCは直列に情報を取得していることになります。

今回この2つのシステムを統合したことで、システム間の通信がなくなり、サーバーABCへの通信が並列で行えるようになりました。こうした通信の処理の最適化により、平均レイテンシーを10%改善できました。

2. CaaS環境への移行

2つ目は、これまで仮想環境で動いていたサーバーをコンテナベースのCaaS環境へ移行しました。 CaaS環境に移行したことでリリース時間が85%削減でき、運用コストを大幅に下げることができました。

3. C++98で動いていた既存システムをGo言語に刷新

3つ目は、C++98で動いていた既存システムをGo言語に書き換えることで、開発言語のモダン化を図りました。もともとC++98はブラックボックスのライブラリに依存し、やむを得ず使っていたという背景があったのですが、ライブラリ依存を徐々に脱却し、システム刷新できる状態になったタイミングでGo言語に書き換えました。

Go言語を選んだ理由は、同じ部門内のシステムを使用している部分が多いことに加え、部門内のライブラリなどの資産が使えて、社内のサポートも活用できるためです。また、刷新システムがさまざまなコンポーネントと通信するため並列処理が書きやすいことや、pprofなどの便利なツールが公式でサポートされていること、さらにJavaなどに比べてガベージコレクションによるレイテンシーへの影響が少ない、といったこともあります。もともと大規模なアプリケーションをGo言語に書き換えたことで、社内のGoリポジトリの中で上位のコード量となりました。

事前準備でクリアしたチームや全体設計の課題

次に実際にどのようにシステム刷新を進めていったのかを紹介します。今回のシステム刷新では、「事前準備」、「開発」、「試験」のそれぞれのフェーズで工夫を凝らしました。

まず、事前準備の段階では、4つの課題がありました。

1. 関係者の多さ

10年分のアーキテクチャ変更を行うため、部門内外含めて広範囲にビジョンの共有が必要でした。「コンポーネントのあるべき姿に向けて、どうシステム刷新していくのか」というビジョンを発信し、全員で共通認識を持てるようにしました。

2. 複雑かつ膨大な機能

現状は使われていない不要な機能を洗い出し、整理を行いました。機能の棚卸しをすることで、移行前の開発コスト削減につながりました。

3. 複数人での開発

複数人で開発するとき、どこから実装すればよいのか判断できません。開発を並列で進められるように、全体の設計とメイン処理の実装を行いました。具体的には、空のデータ構造や関数を用意しておくことで、アプリケーションの一連の流れを実装しました。こうすることで、開発者は「構造体に項目を追加する」、「関数の中身を実装する」ことに集中すればよく、複数人で機能別に並列に開発できるようになりました。

また、ビジネスとシステムのロジックを分離したかったため、設計にクリーンアーキテクチャを取り入れました。例えば、広告のターゲティング機能といったビジネスロジックの改善施策と、HTTP通信時の圧縮方式の変更といったシステムロジックの改善施策があったときに、独立してコード修正ができるような設計にしたいと考えました。こうしたクリーンアーキテクチャの設計方針は、実装から意図を読み取れるようにしました。具体的には依存性の注入をできるようにしたり、どのようなロジックをどこに実装すべきか分かるようにコメントを残したりしました。

4. 開発チームが分離していた

もともとシステム開発チームが複数に分離していたことや、比較的経験の浅いメンバーも多いことから、チーム間でお互いのシステムに対する理解が浅くなってしまう課題がありました。対策として、お互いのシステムについての勉強会を実施し、改めて理解と確認を促しました。

ナレッジ共有と効率化で開発環境を改善

次に開発のフェーズでは、主に2つの観点から工夫しました。

1. ナレッジの共有

最初は少人数で開発を進めていましたが、徐々に開発メンバーが増えていきました。実際に開発を進めていると、実装方針に迷う部分がでてきます。それぞれの知見を定期的なミーティングで共有し、適宜、ガイドライン化しました。

ナレッジの具体例は、「外部ライブラリを使う際にはインターフェースを使い依存性の注入をできるようにする」、「newやビルドといった関数名の付け方」、「数値は基本的にint64で扱う」など、実装時に判断に迷いやすい部分です。

こうした細かな部分をガイドライン化して共有することで、コードレビューで指摘しやすくなりました。ガイドライン化やナレッジ共有によって、開発途中で人数が増えてもコードの品質を維持できました。

2. 開発の効率化

アプリケーション機能の中には、よく使う典型的な処理が何度も登場します。並列で開発を進めていると重複した機能が実装されてしまうリスクもあります。こうした車輪の再発明が生じないように、よく使う機能は共通のライブラリとして利用できるようにしました。

例えば、Go言語にはJSONタグという機能があります。お互いの構造体に変換する際に用いるものです。広告配信システムではさまざまな種類のレスポンスを返すため、レスポンスに応じた項目の削除・追加の処理を何カ所にもしていく必要があります。そのため、Goのreflectパッケージを使ってJSONタグを拡張するライブラリ化を行い、似たような処理を簡単に書けるようになりした。

2つ目の例としては、クリーンアーキテクチャの採用により、開発を進めるに従いインターフェースの数が増えていました。結果、テスト用のモックの生成が遅くなり開発効率が落ちていました。実際には必要な数だけモックを作り、モック生成の高速化を行いました。

このような細かい対応によって、共通ライブラリ化やテストの効率化を図りました。

大規模なシステム刷新におけるさまざまな試験

大規模なシステムでは、データ連携や通信処理が発生するため、アプリケーションの一連の処理を単体テストだけでカバーすることは非常に困難です。刷新したシステムでは、さまざまな試験を行いました。関数単位のユニットテストはもちろん、スモーク試験、機能試験、性能試験という3つの試験を行いました。

スモーク試験による全体的な挙動への自動テスト

スモーク試験では、レスポンスやログのインターフェースが正しか、モックを使って網羅的に確認しました。機能試験では実際のデータを使って連携したり、サーバーを接続したりすることで機能要件を満たせるか確認しました。性能試験では大量リクエストを処理できるか確認しました。

大規模なアプリケーションにおいてインターフェースが期待する動作をしているかテストをするため、スモーク試験ではモックサーバーを使って網羅的にインターフェースのチェックを行いました。

下図は、実際のシステム構成図を示しています。まず左側のスモーク試験のスクリプトは、CI/CD上のジョブで自動実行できるようにしました。中央のAdサーバー(広告配信サーバー)は、今回刷新されたシステムではCaaS上で動作しています。またコンテナのPod内にはAdサーバーとの通信コンポーネントの代わりとなるモックサーバーがあります。AdサーバーにはgRPCとHTTPで通信するサーバーがあり、通信方式ごとにモックサーバーを用意しました。

スモーク試験の構成を表した図

まず、スモーク試験のスクリプトを実行した際に、モックサーバーに対してレスポンスの内容を登録します。レスポンスの内容は、テストケースごとの設定ファイルで定義されています。次にスモーク試験のスクリプトが、Adサーバーに対して設定した広告リクエストを送信します。広告リクエストを受けたAdサーバーは、途中でgRPCサーバーやHTTPサーバーと通信しながら処理を進め、最終的に広告レスポンスを返し、アクセスログをApache Kafka(以降、Kafka)に出力する流れです。

その後、スモーク試験のスクリプトは、「広告レスポンスが設定ファイルで意図した内容と一致しているか」、「Kafkaをコンシュームしてアクセスログが意図した内容か」、「モックサーバーから期待するリクエストと一致しているか」をチェックします。

これら全てのテストケースが無事に通れば、スモーク試験は成功です。スモーク試験を実施したことで、ユニットテストではカバーしにくい全体的な挙動に対して自動テストを行うことができました。機能追加や修正による意図しないレギュレーションを、CI/CD上で検知できるようにもなりました。

機能試験でスモーク試験の穴をカバー

次に機能試験です。ここでは実際にデータを連携したり、試験環境のサーバーと接続したりして試験を行います。スモーク試験ではカバーできないパターンを確認しました。スモーク試験では「テストケースの複雑化」や「実行速度の遅延」といった課題が生じますし、全てのパラメータで全ての値のパターンを確認することも困難です。

機能試験では、テスト環境における広告入稿や広告配信枠の設定など主導の設定を行いました。下図、左側の「Functional Test Tool」が機能試験のスクリプトですが、スクリプトを作成し、半自動でテストを行いました。

機能試験の構成を表した図

機能試験の処理内容を説明します。

まず、事前にツール用のテストケースの定義ファイル設定や、テスト用広告枠設定、入稿を行います。テスト用広告枠設定の例として、「このサイトにはどういったクリエイティブの広告が出せるのか」、「ここのサイトにはこうした広告は出さない」といったブロック機能を設定しました。入稿例では広告のターゲティング設定や、動画・画像といった各種広告フォーマットを設定しました。

次にローカル環境で実行する Functional Test Tool を実行し、旧システムと新システムに広告リクエストを送信します。Adサーバーは、モックではないバックエンドサーバーと通信して広告レスポンスを返し、テスト結果がそれぞれ出力されます。

最後にテスト結果から広告レスポンス、アクセスログなどの差分をチェックしました。

スモーク試験と流れは似ていますが、「試験用のバックエンドサーバーに接続する」、「テスト環境で設定したデータが連携されているかなどを確認する」という点で異なります。機能試験によって、スモーク試験でカバーできていなかったデータ連携やサーバー通信について動作確認できました。

性能試験で旧システムと新システムを比較

最後に性能試験です。性能試験では「大規模なリクエストを処理できるのか」、「旧システムと比べて性能が悪化していないか」を確認しました。性能試験はCI/CDジョブでスクリプトを動かして実施します。左から「aggregate cron script」は、性能試験用のクエリを収集します。「perf job」は実際にAdサーバーへ負荷をかけ、「perf result viewer」は性能試験結果が保存されます。

性能試験の構成を表した図

システムの構成を説明します。まず、旧システムとなるAdサーバーの本番環境のアクセスログがHDFS上に蓄積されています。このアクセスログにクエリが含まれるため、CI/CDジョブで定期的に収集しておき、HDFS上に保存。次にperf jobがクエリを取得しAdサーバーの古いシステムと新しいシステムにリクエストを送信して負荷をかけます。

AdサーバーはPrometheusでメトリクスを計測しており、Grafanaでそのメトリクスを描画しています。負荷をかけ終わるとperf result viewerに性能試験のサマリやログの内容を記録します。最後に、性能試験実施者がperf result viewerの結果やGrafanaのメトリクスを見て性能試験結果を比較します。性能試験によって同じ広告リクエストと同じ負荷で、新旧システムを比較できました。

この性能試験で判明し、今回改善策を講じた2つの例を紹介します。

  1. gRPCサーバーとの通信レイテンシーが遅いことがGrafanaのメトリクスから判明しました。これは、コネクションプールを利用することで改善できました。
  2. Goのプロファイリングツールであるpprofを利用して、構造体を生成している関数のメモリ使用量が大きくなっていた点も明らかになりました。これは関数の返り値を、値ではなくポインタにすることで改善しました。

これらは原因としては単純なものの、性能試験により問題を発見・解決できた例です。性能試験の仕組みは、性能のボトルネック調査に非常に役立ちました。

以上、ここまででヤフーの広告配信システムに関するシステム統合とモダン化に関して、事前準備、開発、試験までさまざまなフェーズでの工夫や課題などを紹介してきました。 ぜひ、皆様の組織でのシステム開発で参考にしていただければ幸いです。

アーカイブ動画


Apache®, Apache Kafka™, 及びKafkaのロゴは、米国および/またはその他の国におけるApache Software Foundationの商標または登録商標です。

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました


川崎 祥
バックエンドエンジニア
広告配信システムの開発と運用を担当しています。

このページの先頭へ