Pub/Subと並列処理を活用した疎結合高速アーキテクチャの構築計画

はじめに

はじめまして!求人ボックスの山田です。 業務では求人ボックスにおける広告出稿システムの開発を担当しています。

今回は、自分が取り組んでいる広告入稿システムの新アーキテクチャ化について、その内容をご紹介します。 このプロジェクトは現在開発・検証中の部分もあり、まだ完了したプロジェクトではありません。そのため、効果検証などはまだ実施できていません。

この記事では、アーキテクチャの改修において、どのような課題にどう対処したのかについて紹介できたらと思います。

求人ボックスの広告システム

求人ボックスは、多数の求人情報を集約するアグリゲーションサイトです。

サイトの検索結果画面では、求人ボックスのシステムが自社で収集した求人情報とともに、他社からの依頼に基づく求人情報も広告求人として表示されます。 これらの広告求人は求人ボックスの売上に大きく寄与しています。そのため、広告システムの保守性や耐障害性は極めて重要となります。

現行システムの課題

求人ボックスは2015年にサービスを開始し、その成長に伴い広告を出稿する企業数や広告求人数が増加しています。
サービスの規模が拡大するにつれて、保守性や運用面で課題を抱えるようになりました。 広告システムの設計面の課題として、以下の2点があると考えました。

単一障害点が存在する

広告の入稿処理は複数のサーバー上で並列に行なわれていますが、ローカルディレクトリのファイルを用いたバッチ間連携のため、企業ごとにサーバーを分けています。
この設計では、1つのサーバーがダウンすると、その企業の全求人の取り込みが停止してしまう問題があります。

広告求人の新規入稿・更新から、検索システムまでの反映時間が長い

現状では、数件程度の求人更新でも反映に数時間かかることがあります。また、その結果として障害からの復旧や日常的な運用対応にも時間がかかるようになっています。

上記のようなシステムの特徴により、運用作業や障害対応作業に追われ、日々の開発業務の効率が落ちる期間も出てきました。

今後、サービスの規模がさらに拡大し、取り扱うデータ量や処理内容が増えると予想されるため、これらの課題を解消することが必要です。
そのため、開発チームで広告システムの刷新を手がけることになりました。

広告入稿システムの概要

今回はシステム刷新のなかでも自分が主に担当した、広告求人の入稿部分について詳しく取り上げたいと思います。

以下は既存システムの概要図です。


広告求人の入稿処理では、企業が出稿した広告求人を求人ボックスのデータベースに格納します。 出稿企業との連携方法はいくつかありますが、そのなかでもメインの広告入稿方法は以下のような流れで実施されます。

  1. 広告出稿企業は、求人情報が格納されたファイルを指定のFTPサーバーなどに配置

  2. 配置されたファイルを求人ボックス側のシステムが読み取り、適切なフォーマットに整形した後、求人ボックスのデータベースに格納

ただし、1つのファイルには数百万件の求人が掲載されることもあり、各求人に対して求人ボックス内で定めたルールに基づいて掲載可否の判定や独自の加工処理を行ないます。 既存システムでは、ファイル(企業)ごとに取り込みを並列で実行できるため、件数の少ない企業の取り込みは迅速に行なわれますが、大量の求人数を持つ企業の取り込みには時間がかかるという課題がありました。

アーキテクチャの概要

上記の課題を解決するために、新しい広告システムではいくつかの改善を実施しました。 新システムは以下の図のような形で処理を実行します。

今回は3つの改善を取り上げます。

1.PubSubパターンを利用したアーキテクチャ

PubSub(Publisher-Subscriber)パターンとは、メッセージの送信者と受信者を分離することで、異なるシステム間の連携を容易にするアーキテクチャパターンです。 PubSubパターンにはいくつかのメリットがありますが、今回のシステムでは以下の目的から導入しました。

  • 可用性の実現: サーバーに依存しないPubSubメッセージを起点にしてバッチ処理を連携することで、1つのサーバーがダウンしても他のサーバーで求人の取り込みが可能な状態を保てます。これにより耐障害性を向上できます。

  • スケールアウトの容易さ: システムの負荷が増えた場合、新たなSubscriberを追加することで容易にスケールアウトが可能です。

  • システム間の連携の容易さ: メッセージの送信者と受信者が分離されているため、異なるシステム間でもメッセージのやり取りが容易になります。また、システムを疎結合コンポーネントに分離することで、保守性や可用性の向上を目指せます。

実装にあたっては、求人ボックスで導入実績のあるGoogle CloudのPub/Subを利用しました。

2.常時起動ワーカーによるキューメッセージ待機システム

求人情報が取り込まれるまでに、いくつかのバッチ処理を通過する必要があります。
これらのバッチ処理は、「20分おきに起動し、更新のあったものを検知して処理対象を取得する」といった形で動作していました。 バッチ内での処理を高速化できたとしても、バッチ間の連携による待機時間が発生し、結果として反映が遅延してしまいます。 (バッチAが10分おき起動、バッチBが15分おき起動の場合、最悪のケースで35分の遅延が発生することになります。)

そこで、新システムでは常時Pub/Subのメッセージの受信を待機し、受信したメッセージに基づいて処理を実施する方式にバッチ処理を改修しました。
メッセージングサービスを利用することで、シンプルなコードで常時起動型のバッチを実装でき、これは大きなメリットとなりました。
Pub/Subとの通信オーバーヘッドなどを除けば、バッチ間連携による待機時間をゼロにでき、求人情報の反映時間を大幅に短縮できる見込みです。

3.単一ファイルの取り込みを並列化

既存システムでは、求人情報が掲載されたファイルを1つずつ取り込む方式でした。このファイルには数百万件の求人が掲載されることもあり、取り込み時間がボトルネックとなっていました。 そこで、以下のような流れで、ファイルを分割し複数のワーカーで並列で取り込む方式に改修しました。

  1. まず、入稿ファイルを求人N件ずつM個のブロックに分割し、分割完了メッセージを送信する

  2. 次に、並列起動している常時起動ワーカーがメッセージを受信し、分割済みのファイルを読み取り、求人の掲載可否判定と加工処理を実施する

これにより、例えばA時間かかっていたファイルの入稿は、並列数Mで実行すればC+A/M時間に短縮されます。(Cは分割処理のオーバーヘッド)

この方式で並列化すると、処理求人数に応じてワーカーの起動数を制御することで、簡単にスケールアウトさせることができます。 また、サーバーの台数やスペックに応じて分割数を調整すれば、取り込み速度をチューニングしやすいというメリットもあります。

GoogleCloud Pub/Subを利用する上での考慮点

Google Cloud PubSubは便利なメッセージングシステムですが、注意点もあることがわかりました。活用する場合、設計・開発段階で考慮すべき点を2点お伝えします。

メッセージの先入先出しが保証されない

Google Cloud Pub/Subは、デフォルトの設定ではメッセージの先入先出しは保証されません。 そのため、メッセージそのものに更新内容を入れてしまうと、同じデータへの更新順序が逆転し、古い更新内容による上書き現象が発生することになります。

この問題に対しては、発行するメッセージには更新するデータのIDのみを入れ、IDに紐づくデータの内容(メタデータ)はRDBなど別のストレージに保存することで解決しています。 (メッセージを受信したバッチは必ずストレージに保存された最新データを見ることで、更新内容の逆転を発生させません。) メッセージに更新内容を入れないというのは、メッセージあたりのデータ量削減にもなります。Pub/Subは従量課金制のため、この方法は使用料金の節約にもなります。

サブスクリプションの設定にて上記の保証を入れることができますが、パフォーマンスに影響を与える懸念があるため、導入にあたっては検証が必要です。

受信メッセージの重複が発生する可能性がある

実は検証中に気付いたことなのですが、Google Cloud Pub/Subはごくたまに重複したメッセージをサブスクライブします。

したがって、重複したメッセージを受け取っても問題なくシステムが動く(べき等になる)ように作らないといけません。自分は開発時にこの点を見落としておりべき等性が担保できていなかったため、システムの修正が必要になりました。

アーキテクチャのデメリット・苦労した点

ストレージの消費量が多くなった

既存システムでは、1つのバッチ内で実施していたものを、PubSubのメッセージによって連携する複数のバッチに分割しました。 先述したとおり、どのデータを処理するかというデータのIDはメッセージに載せていますが、実際のデータは別の場所に保存する必要があります。

これによって、既存システムではメモリに載せていたデータを新システムではストレージに一度保存している形になっています。 耐障害性は向上する反面、一時的なデータを保存するために既存システム以上のストレージリソースが必要になりました。

バリア同期の実装が必要となった

求人ボックスの広告システムでは、仕様上「ある特定サイトの広告がすべて取り込まれた後に開始する処理」というものが存在します。 しかし、求人情報を並列に取り込むと、「特定のサイトの広告がすべて取り込まれたタイミング」を検知するのが難しくなります。

この問題を解決するために、各ワーカーが処理の完了を待機し、検知する仕組み(バリア同期)が必要となりました。

具体的には、MySQLに分割データの状態を管理するテーブルを作成し、自前のバリア同期システムを実装しました。 データの状態管理には整合性が必要なため、ここではRDBを選択しました。

並列分散処理の難しさとして、この同期処理が挙げられます。 今後、同じような設計をする際には、この点をあらかじめ意識する必要があると感じました。

まとめ

求人ボックスの広告システムを疎結合アーキテクチャと並列化による高速化で改善するプロジェクトに取り組みました。

カカクコムでは、ともにサービスをつくる仲間を募集しています!

カカクコムのエンジニアリングにご興味のある方は、ぜひこちらをご覧ください!

カカクコム採用サイト