CloudFunctionとFirestoreで作るサーバーレスダッシュボード開発5つの勘所
はじめに
こんにちは、Voicyデータストラテジストの小山内です。Voicyではプロダクトの大改修フェイズを迎えており、殆どのサービスを作り直す勢いで、開発チームが日夜開発に勤しんでいます!
↓例えば、バックエンドは、ぱんでぃ(Yoshimasa Hamada)さんが担当しています。
大改修にも含まれる新機能の1つとして、”放送の聴取ログをパーソナリティさんに提供するダッシュボード”を開発する事となりました。
複数の実装方法を考えた中で、今回はGCPの各種プロダクトを活用した、サーバーレスなダッシュボードを作る方向性で検討を進めています。
本記事では、実際に技術検証を進めるに当たって、ポイントとなった点をサンプルコードと共に共有します。
同じ様なダッシュボード開発ではなくても、GCP製品をお使いの方で、参考になりそうな箇所があったら、嬉しい限りです!✏️
対象読者と記事から得られるナレッジ
対象読者:
- ダッシュボード機能の様な、データを活用したプロダクトを開発している方
- Firestoreを使っている方
記事から得られるナレッジ:
・記事内では以下5つのポイントを説明しています!
1: CloudFucntion上でクエリ実行&集計結果をテーブルに保存するHow
2: Pub/SubをFunction内で呼び出して、ジョブフローを作るHow
3: Firestoreの一括書き込みで 大量のドキュメントを短期間で更新するHow
4:サブコレクションを使って、データモデリングを行うHow
5:クライアント側からFirestoreにクエリを発行するHow
“素敵なダッシュボード”に求められる条件ってなんだっけ?
いきなり実装の詳細に入る前に、アーキテクチャを設計するに当たって考えた事を簡単に紹介します!
今回、機能を開発するに当たり、利用者 / 開発者の両方の視点から、”素敵なダッシュボード”に求められる要件を整理しました。サマリと、今回のアーキテクチャでケアした部分を併記すると以下になります。
データを見る人に提供したい体験 👨👩👧
- ノンストレスでサクサク/待ち時間を感じる事なくデータを閲覧できる
事前集計の可能なメトリクスは、一定間隔でバッチ実行し、 データストアに格納
- 深掘り分析に役立つ、詳細なメトリクスを閲覧できる
BigQueryに保存するローデータをデータソースにする事で、standardSQLで表現可能な範囲において、いかなる指標も追加できる
開発チームに提供したい体験 👨💻
・指標の追加/削除など仕様に変更があった場合にも、簡単に対応できる
スキーマレスなFirestore&カラム指定無しのコーディングで、0開発でスキーマを事後変更できる
・将来的にデータ量がスケールした場合にも、追加対応不要で安定稼働できる
大規模データの格納にも十分なパフォーマンスを発揮するFirestoreを使用する
・シンプルなデータ設計で、データエンジニア/実装担当の学習コストを低く抑える事ができる
Firestoreのサブコレクションを用いた直感的に理解しやすいDB構造と、クエリ
・安い💰
10万書き込みでも$0.18のスタートアップにも優しいお値段
Cloud Firestoreの料金
実装上のポイント
先に述べた実装要件を満たす様に、設計したアーキテクチャが以下になります。
もの凄くザックリと説明すると、”BigQuery での集計/Firestoreへのデータ格納の2つをCloudFuncitonに時系列に沿って実行する”構成となっています。
STEP1: BigQueryで集計する
実装上のポイント1:CloudFucntion上でクエリ実行&集計結果をテーブルに保存する方法
BigQuery クライアント ライブラリを使用する事で、CloudFunction上でBigQueryに関する様々な処理を実行する事ができます。
Voicyですと、実際に最終的にFirestoreに投入するデータは、BigQueryに集計・保存してあるので、BQにクエリを発行し、新規のテーブルを保存する処理が必要になります。
CloudFunctionの最大実行時間は540秒ですが、BigQueryのクエリ実行は非常に高速なので、今回の要件だと実行時間が問題になる事は無さそうでした。
コード1: CloudFunction内でBigQueryでクエリ実行&テーブル保存 — Python
# ライブラリのインポート
from google.cloud import bigquery# 初期化
project_id = 'voicy-xxxxxx'
client = bigquery.Client(project = project_id)# ジョブ設定の記述
dataset_id = 'dashboard_soure_data'
path = 'daily_uu'
job_config = bigquery.QueryJobConfig()
table_ref = client.dataset(dataset_id).table(path)
job_config.destination = table_ref
job_config.write_disposition='WRITE_TRUNCATE'# クエリの発行&テーブル保存
query_job = client.query(
query,
location='US',
job_config=job_config)# APIリクエストの発行 --> クエリの実行&テーブルへ保存
query_job.result() # Waits for the query to finish
print('Query results loaded to table {}'.format(table_ref.path))
実装上のポイント2:Pub/SubをFunction内で呼び出して、ジョブフローを作る
Pub/Subを間に挟む事で、関数の実行順を担保しながら、マイクロサービス的に処理を繋げる事ができます。具体例を挙げると、以下のような流れで、データを処理しています。
1: Function A内で、BigQueryで計算実行&DAUをテーブルに格納
2: Function Aの末尾で、Pub/Subの”dashboard_report_daily”にイベントをパブリッシュする
3: Function Bを、Pub/Subの”dashboard_report_daily”をトリガーとして監視するように設定。2が終了した瞬間にFirestoreへの、DAU格納処理を実行する
このように処理を細分化しておく事で、今後別の機能でDAUの集計値を使いたくなった場合でも、処理を使い回す事が出来ると考えています。
BigQuery同様に、Pub/Subもクライアントライブラリが用意されているので、こちらを使用します。(とってもシンプル!💡)
コード2: CloudFunction内でPub/SubにイベントをPublishする— Python
# ライブラリのインポート
from google.cloud import pubsub# 初期化
publisher = pubsub.PublisherClient()# イベントをパブリッシュ!
publisher.publish("projects/voicy_xxxx/topics/xxxx",b'Query finished!')
CloudFunctionは、デフォルトでは各プロジェクトにデフォルトのサービスアカウントの権限で実行されます。同一プロジェクト内のリソースは認証無しでアクセスできますが、異なるプロジェクトのリソースを操作する際には、追加の権限が必要になるので、都度対応しつつ進める必要があります。
参考記事
STEP2: Firestoreのバッチ書き込み実行
さて、ここまでで、①CloudFucntion内でBigQueryを操作し、②Pub/Subを使って複数のFucntionでフローを組む 事が出来るようになりました。
続いて、実際にFirestoreにデータを書き込む部分を見ていきたいと思います。
実装上のポイント3:Firestoreの一括書き込みで 大量のドキュメントを短時間で更新する
今回のダッシュボード機能の開発、実は当初AWSのDynamoDBをクライアントから呼び出すデータストアとして使用する予定でしたが、書き込み性能で難しいところがあり、断念した経緯があります。
Firestoreは、一括書き込みの仕組みを持っており、こちらを使用する事で大量データの書き込みを個別で処理するよりも高速で捌く事ができます。
コード上では、batchオブジェクトにset()でドキュメントをセット→ commit()で書き込み処理実行というフローになります。
Firestoreの書き込み性能については、以下の制約があるので実利用の際には、検討が必要です。
1: 1秒間の最大ドキュメント書き込み回数は最大10,000回
2: 一括書き込みで同時にcommit()できるドキュメント数は最大500個
具体的なコードは後ほど紹介しますが、実際に書き込みのパフォーマンスを見てみましょう。
約50,000件の書き込みで2分30秒かかるパフォーマンスでした🎉
今回の案件では必要十分な性能なので問題ないですが、より多くのデータを書き込み処理を行う場合は、CloudFunction上では捌き切れなくなるため、別の実行環境を用意する必要がありそうです。
実際に、バッチを生成し、Firestoreにドキュメントの書き込みを行うコードは以下になります。先に述べた制約があるため、500回単位でbatchオブジェクトを初期化しながら、commit()をループしている所がポイントです。
def fs_batch_write(dataset): # 書き込み先のコレクションパスを設定
records_collection = self.db.collection('speakers')
# 初期化
index = 0
batch = self.db.batch() # 書き込み処理のループ開始 (今回は、PandasのDataFrameを使用)
for idx,record in dataset.iterrows():
# Python dictionary型に変換
document = record.to_dict() # 500レコード毎にバッチをコミット
if index % 500 == 0:
if index > 0:
# バッチをコミット!
batch.commit()
# 次のイテレーションに備えて、バッチを初期化
batch = self.db.batch()
index += 1 # ドキュメントの書き込み先参照を作成
record_ref = records_collection.document(str(document["DocumentID"]) # バッチに書き込み先参照とドキュメントを渡して、処理をセット
batch.set(record_ref, document) # ループを抜ける前に、コミットを実行
if idx % 500 != 0:
batch.commit()
実装上のポイント4:サブコレクションを駆使して、直感的に理解しやすいDB構造にする
記事冒頭、実装時に気をつけるポイントとして、以下の点を挙げていました。
・シンプルなデータ設計で、データエンジニア/実装担当の学習コストを低く抑える事ができる
Firestoreのサブコレクションを用いた直感的に理解しやすいDB構造と、クエリ
Firestoreでは、 サブコレクションを活用する事で、人間が直感的に把握しやすいデータモデルを設計する事ができます。
実際に、データモデリングを行う際には、セキュリティ要件や、発行される可能性のあるクエリの種類(ビジネスロジック)も考慮し、以下のような構成にしました。
Voicyの場合だと、個別のパーソナリティが複数の放送を保持し、さらに個別の放送は複数のチャプターによって構成されているという関係性があります。DAUなどの時系列データもまた親子関係として、それぞれのドキュメントに紐づいています。
こうした関係性をサブコレクションで表現する事で、直感的に理解しやすい構成を意識しました。
一方で、こちらの構成だと、”各パーソナリティ/放送を跨いだデータを取得するクエリ” は実行できないため、例えば、”Voicy内全ての放送の中で1.000UU以上の放送の一覧”などは取得できません。
しかし、もし実際に今後そのようなクエリ(設計時には想定していなかった新規のビジネスロジック) が必要になった際には、データの複製や、非正規化を行い柔軟に設計も変更する予定です。
ダッシュボードの様に、ユーザーの使用状況に合わせて、データモデル自体が変化する可能性が高い機能は、NoSQLのスキーマレスな性質と大変相性が良いものだと感じます。
STEP3: FirestoreClientライブラリからデータ読み込み
実装上のポイント5:クライアント側からFirestoreにクエリを発行する
ここまでの説明で、サブコレクションを用いてモデリングされたFirestoreに、大量のデータを短時間で格納する事が出来ました。
最後の仕上げに、クライアント側から、Firestore側にクエリを発行してみます。 以下の様なクエリを投げる事で、SQLの様なデータ取得処理を記述できます。具体的なクエリの書き方は、公式ドキュメントをご参照下さい。
db.collection(u'Speakers').where('SpeakerName','==','Voicy社内報')
非常にシンプルな書式なので、クエリをデバッグしながら組み立てて行けば、詰まる所も殆ど無くデータを取得できるかと思います!
サーバーレスなダッシュボードが完成しました!
長くなりましたが、色々とつまづきそうなポイントを越え、実装を終えることが出来ました。最後まで読んで頂きありがとうございました🍵
部分的にでも、参考になる箇所があれば、Voicyではこうやって作っているよという事で、ナレッジとして使って頂ければと思います🔥
Voicyでは、共に働く仲間を募集しています!
Voicyでは、共に”音声×テクノロジーでワクワクする社会を作る”仲間を募集しています。全方位絶賛募集中ですので、気になった方はぜひオフィスに遊びに来てください〜!
https://www.wantedly.com/companies/voicy
もくもく会もほぼ毎週開催しています!
↓その他データ関連のテックブログ記事へのリンク(Voicyのデータ活用が丸裸!👀)