はじめに
こんにちは!バックエンドエンジニアの谷口(@ravineport)です。
以前、 大規模調査を支えるアンケートシステムのアーキテクチャという記事で弊社のアンケートシステムのアーキテクチャについて紹介しましたが、今回はこれをもう少し深堀りしてみようと思います。
必要なことは改めてこの記事でも取り上げるので、この記事を読むにあたって前述の記事は前提としていません(が、読んでいただけるととてもうれしいです!)。
EmotionTech では Google Cloud を採用しており、今回紹介するアンケートシステムも Google Cloud 上で構成されています。
アンケートシステムの概要
前述の記事の繰り返しになってしまいますが、今回改めて紹介するアンケートシステムの概要です。
このシステムは企業が利用することを想定しており、企業がアンケートを作成し回答を集めるためのシステムです。 例えば顧客のニーズを理解し、最適なサービスを提供をするためのユーザー向け調査などにご活用いただいています。他にも従業員の方に向けてのアンケートにもご活用いただいています。
集めた回答を集計・分析する機能も提供していますが、この記事ではそこのアーキテクチャについては触れません。 この記事ではアンケートの設定から回答を貯めるまでのアーキテクチャについて解説します。
アンケートシステムに求められる非機能要件
こちらも前述の記事の繰り返しになってしまいますが改めて弊社のアンケートシステムに求められる非機能要件についても触れておきます。
- 大規模調査における大量のリクエスト(具体的には秒間 1000〜回答を想定)を処理できるスケーラビリティ
- 回答サーバーは可用性が高いこと
- 停止すると、顧客の声を集める機会が失われてしまうだけでなく、アンケートを実施する企業のブランドイメージ毀損に繋がる恐れがあるため
- 回答者のユーザー体験を損ねないよう、アンケート設定内容取得・回答の送信は低レイテンシであることが望ましい
- 回答データの損失は許容されない
- 受け付けた回答は分析機能のユーザーがほぼリアルタイムで確認できるのが望ましい
アンケートシステム全体のアーキテクチャ
概観
図にするとざっくりこんな感じです。

大きく3つのパートに分けることができるのでそれぞれ解説します。
アンケート設定に関するところ
主にアンケート運用担当者が関わる部分です。アンケートの構成を設定したり、アンケートの公開・非公開などが行われます。公開・非公開というのは文字通り、アンケートをインターネットに公開したり公開中のものを非公開にしたりすると言ったアクションです。

アンケート設定サーバー
アンケートの作成・更新や公開・非公開など回答以外のリクエストを扱うのが主な役割です。
リクエストは Cloud Load Balancing → API Gateway のサービスを通ってアンケート設定に関係するAPIサーバー(図のアンケート設定サーバー)に渡ってきます。
Cloud Run
アンケート設定サーバーのアプリケーションは Cloud Run 上にデプロイされています。アンケート設定に更新が入ったりしたときには、回答サーバーが古い情報を参照しないようにキャッシュを削除する必要があるため Memorystore for Redis にもつながっています。
また、Cloud Run Jobs も利用しています。主にAPIサーバーへのリクエストのうち非同期に処理したいものに活用しています。図にあるのはアンケートの公開・非公開に関連して非同期に処理したいものです。理由については後ほど解説します。
EmotionTech ではアンケート以外にも様々なサービスがあり、それらでも Cloud Run を活用しています。
Rustで実装されたアプリケーション
Cloud Run 上で動いているアプリケーションは Rust で実装されています。後述する回答サーバーのアプリケーションも Rust で実装されています。 本システムが扱うアンケートはそこそこ複雑なドメインなため、それを表現するために一定の表現力がほしいこと、非機能要件を満たすための処理速度にも一役買ってくれるであろうことから Rust を採用しました。
AlloyDB
アンケート設定は AlloyDB (PostgreSQL)に永続化しています。 パフォーマンスを要求される回答サーバーからも参照されるため、当時の Cloud SQL よりも高パフォーマンス・高可用性の AlloyDB を採用しました。
必要に応じて読み取りプールを追加して読み取りに対してスケーラビリティを上げることも可能です。
本番環境は高可用性のためにマルチゾーン プライマリインスタンス構成にしておき、それ以外の開発環境等では単一ゾーン プライマリインスタンス構成にしておいてコストを抑えることができるのもうれしいポイントです。
開発当時にはなかったため検討されなかったのですが、今なら Cloud SQL Enterprise Plus エディションも採用の余地がありそうです。
アンケート回答に関するところ
アンケート回答者のためのアンケート回答画面用の設定情報を返したり、回答者が入力した回答を受け付けたりする部分です。
必要な情報は可能な限り Memorystore for Redis から取得し、回答送信は Cloud Pub/Sub にパブリッシュした時点で完了するようにすることで、低レイテンシのレスポンスを実現しています。

回答サーバー
回答サーバーは回答画面用のアンケート設定情報を返したり、回答者が入力した回答が有効なものか(主にリクエストの改ざん:存在しないはずの質問への回答がないか、存在しない選択肢を送っていないか等)を検証するのが主な役割です。
Cloud Run
アンケート設定サーバーと同様、回答サーバーのアプリケーションも Cloud Run 上にデプロイされています。 Cloud Run は負荷に応じてインスタンスを自動でスケールアウト・インするためアンケートシステムの要件にぴったりです。ただし、スケールアウト・インの判断の要因、例えば1インスタンスあたりのリクエストの最大同時実行数等の設定には注意が必要です。このあたりの値はアプリケーションの特性や性能試験を通じて決めています。 アンケート設定サーバーとは負荷特性が異なり、こちらの方が圧倒的に負荷がかかりやすいため別の Cloud Run サービスにしています。また、前段に API Gateway となるサービスはなく、直接 Cloud Load Balancing のバックエンドサービスにしています。
Rust で実装されたアプリケーション
前述しましたが、このアプリケーションもRustで実装されています。不正なリクエストの検証などCPUバウンドな処理を高速に処理したり、複雑なドメインに対する表現力、静的型付けによる堅牢な実装が可能などなどがうれしいポイントです。 また、選定時にはあまり意識していませんでしたがコンテナ起動が速いため(現状1秒程度)、リクエスト急増時の迅速なスケールに一役買ってくれています。
Memorystore for Redis
回答サーバーは低レイテンシ、高スループットが求められるため、アンケート設定情報のキャッシュが必要不可欠です。また、同じ回答者の回答を複数回受け付けないようにする機能もあるため、回答送信時にそれを瞬時に判断する必要もあります。それらの情報を保存、高速な読み出しができる場所として Memorystore for Redis を採用しています。
Memorystore for Redis にはティアがあり用途に応じて選択できます。本番環境以外ではベーシックティア、本番環境ではより高可用性が提供されるスタンダードティアを採用しています。 スタンダードティアでは必要に応じてリードレプリカを追加することも可能です。
手動でフェイルオーバーを起こすこともできるため、大量のリクエストが来たときにフェイルオーバーが起こった場合の影響をテストすることも可能です。
Pub/Sub
回答サーバーに送られてきた回答データは検証された後、Pub/Sub の特定のトピックにパブリッシュされます。
回答送信リクエストはパブリッシュが完了した時点でレスポンスを返すようにしています。パブリッシュのレイテンシは基本的に数十ms程度であるため、回答送信リクエストの低レイテンシ化を実現できています。
また、回答送信リクエストと後述する回答データの永続化を Pub/Sub を介することで非同期化しているため、回答サーバーの責務を小さくしつつ後続の処理で回答データが損失しないようなストリーム処理が可能です。
パブリッシュするときは、メッセージのペイロードに回答データの JSON、メタ情報としてカスタム属性をいくつか追加しています。 カスタム属性には回答データを後述する BigQuery のテーブルに振り分けるために必要な情報を追加しています。
回答データをBigQueryまで流すところ
検証された回答データを Pub/Sub からサブスクライブして最終的に BigQuery に貯める部分です。

Dataflow
Pub/Sub から回答データのメッセージをサブスクライブして、その内容に応じてアンケートごとの BigQuery のテーブルに追加していくETLです。
Dataflow でストリーム処理を行うために Pub/Sub にはそれ用の Pull 型サブスクリプションを追加してそこからサブスクライブするようにしています。
処理自体は Apache Beam を使って書くのですが、Dataflow と組み合わせることで大規模なストリーム処理をする際に考慮しなければならないところをよしなにサポートしてくれます。
このあたりのよしな感などは過去にもこのブログで紹介しているでぜひご覧ください。
Dataflow を Pub/Sub と統合するとサブスクライブはもちろん、メッセージの重複排除、Pub/Sub への Ack もよしなに行ってくれるため、ETL の処理自体に集中することができます。
Apache Beam の実装では、Pub/Sub のメッセージペイロード(回答データの JSON )の内容には関与していないところがポイントです。メッセージのカスタム属性に応じて、適切な BigQuery のテーブルに回答データの JSON 型のデータを書き込んでいます。理由等の詳細は後述します。
BigQuery
回答データを貯めるところです。
Dataflow から JSON 型のデータを書き込むときには Storage Write API (at least once) を使っています。Storage Write API のスループット上限はマルチリージョンでは 3 GB/秒、リージョンで 300 MB/秒(参考)で本アンケートシステムでは十分な性能です。
どのようなテーブル構造をしているのか、それらの作成タイミング等は次の章で解説します。
BigQuery まわりのアーキテクチャ
概観
アンケートシステム全体のアーキテクチャのうち、BigQuery まわりを詳細化したのが以下の図です。

BigQuery
本アンケートシステムには組織という概念があります。ざっくり言うと企業を表す概念で、それごとにデータセットを分けています。アンケートが作成されるとそれぞれのデータセット内にテーブルが作られます。
アンケートごとにテーブルが2つあり、それぞれ _raw と _analysis というサフィックスをつけています。
_rawテーブル
アンケートの設定に依らず固定された構造のテーブルです。Dataflow はこのテーブルに書き込んでおり、回答データの JSON は回答 JSON カラムという JSON 型のカラムにそのまま入ります。 他にもカラムはあるのですが図では省略しています。
ポイントはアンケートの設定に依らない構造をしているというところです。分析を考えると質問ごとにカラムが分かれているテーブルが望ましいのですが、負荷試験をしたところ Dataflow で回答 JSON をパースして各列に分ける処理が原因で要求されるパフォーマンスを出せなかったためこのようにしました。 回答データの JSON は回答サーバーで検証済みなため Dataflow ではここには関与せず、Pub/Sub のメッセージのカスタム属性を見て、適切なテーブルに振り分ける処理のみにすることで期待したパフォーマンスを出すことができています。また、パフォーマンスのために Storage Write API の at least once で書き込んでいるため重複した回答データが入っている可能性があります。
_analysis テーブル
前述したとおり、分析を行う上ではテーブルは質問ごとにカラムが分かれている必要があります。そのためのテーブルが _analysis テーブルです。回答データ一覧の確認でもこのテーブルを参照しています。
_analysis テーブルは _raw テーブルを参照するマテリアライズド ビューです。BigQuery のマテリアライズド ビューは、ベーステーブルのすべての増分データを自動的に反映してくれるため(参考)、最新データの反映をこちらでコントロールする必要がないのがうれしいポイントです。
テーブルの作成・更新タイミング
_raw テーブルの作成はアンケート作成APIへのリクエストのタイミングで同期的に行う一方で、_analysis テーブル(マテリアライズド ビュー)の作成・更新はアンケートの公開APIへのリクエストで発火しますが非同期で作成・更新しています。
もともとは_raw テーブルと同様に同期的に作成していたのですが、マテリアライズド・ビューの作成・更新が場合によっては想定より時間がかかってしまい、APIとしてのレスポンスタイムが無視できないレベルになることがあったため Cloud Run Jobs を使って非同期で行うようにしました。また、一時的な問題によってマテリアライズド・ビューが作成できないときがあってもCloud Run Jobs のリトライ機能によって結果整合性を保つことができています。
負荷試験
簡単にですが負荷試験についても触れておこうと思います。 どの程度のリクエストをさばくことができるのかを検証するために負荷試験を行っています。
実案件のアンケートを想定したアンケートを作成し、そこへの回答リクエストを送りどの程度までリクエストをさばくことができるのかを検証しました。 正常にさばくことができる秒間リクエスト数の限界やレイテンシ等の他にも、BigQuery で回答データが参照できるまでの時間についても検証しています。
大量の回答リクエストを送るにあたって、k6を使ってテストシナリオを記述し、Compute Engine のインスタンス上で実行しました。
Memorystore for Redis を使ったキャッシュの読み書きが正常に動いているケースの他にも、Memorystore for Redis がダウンしたときにさばける秒間リクエスト数、影響についても検証しています。Memorystore for Redis には手動フェイルオーバーの機能があるため、このあたりの検証がとてもやりやすかったです。
おわりに
大規模調査を支えるアンケートシステムのアーキテクチャについて改めてご紹介しました。今回ご紹介したこと以外に、ネットワーク構成や運用まわり(どんなメトリクスを監視しているのかなど)等、触れられなかったこともたくさんあるのですが、それはまた別の機会にご紹介できればと思います。
今後なにかシステム構築をするうえで少しでも参考になれば幸いです。
エモーションテックでは、顧客体験・従業員体験の改善を支えるプロダクト開発を一緒に進めてくれる仲間を募集しています。ご興味のある方はぜひ採用ページからご応募ください!