はじめに
Firestore、便利ですよね。大好きなGoogle Cloudプロダクトのひとつです。
ネイティブモードで利用して、Web or モバイルアプリからmBaaS的に使うのが鉄板だと思います。
ですが例えばバッチジョブでデータを洗い替えたりする場合等、サーバから直接触りたくなる状況もちょくちょくあるかと思います。
直近、サーバからFirestoreを触る機会がそこそこあったので、その中で得た備忘録的なものをまとめました。
サンプルコードはGoですが、他の言語でも参考になる部分も多いかと思います。
思いつき次第随時更新いたします👨💻
Tips一覧
1. ローカル環境からFirestoreを利用する
Firestoreを組み込んだサーバアプリケーションをローカルで開発する場合、直接開発環境のFirestoreに繋ぎ込む方法と、Firestoreエミュレータを利用する方法があります。
開発環境のFirestoreに直接接続する
手元の環境から直接Firestoreに繋ぎ込む際のクレデンシャルは、サービスアカウントキーを利用する方法よりも、gcloud auth application-default login
を利用するのがセキュリティ上健全かなと思います。
以前書いた下記の記事が参考になると思います。
② Firestoreエミュレータと接続する
Firebaseプロダクトにはローカルで実行できるエミュレータがあり、Firestoreも例に漏れず、サーバアプリケーションから利用可能です。
エミュレータのデータは全てメモリ上に載っており、立ち上げるたびに初期化されてしまいます(永続化する方法もアリ)。
そのためFirestoreとの繋ぎ込みをモックする用途等で利用できます。
繋ぎ方は簡単で、環境変数FIRESTORE_EMULATOR_HOST
で指定したホストにエミュレータを立ち上げれば、SDKが環境変数を勝手に読み込み接続してくれます。
export FIRESTORE_EMULATOR_HOST="localhost:8000"
以前作ったWeb APIのサンプルコードが下記です。
DBとしてFirestoreエミュレータを利用しています。
Firestoreエミュレータを8000番に立ち上げて、8080番に立ち上げたGoのアプリケーションからFIRESTORE_EMULATOR_HOST
越しに接続するようになっています。
参考までに、同リポジトリからDockerfile
とdocker-compose.yml
の一部を転記します。
FROM node:alpine
RUN apk add openjdk11
RUN npm install -g firebase-tools
WORKDIR /app
CMD [ "firebase", "emulators:start", "--only", "firestore" ]
FirestoreエミュレータのプロジェクトIDは何でもOKです。
適当な任意の値(hogeでもfooでも)を入れておけば動きます。
Goのアプリケーションには、environment
としてPROJECT_ID
とFIRESTORE_EMULATOR_HOST
を渡しています。
version: "3.8"
services:
firestore:
build:
context: .
dockerfile: Dockerfile.firestore
ports:
- 8000:8000
volumes:
- .:/app
app:
build:
context: .
target: dev
dockerfile: Dockerfile
ports:
- 8080:8080
environment:
PROJECT_ID: foo
FIRESTORE_EMULATOR_HOST: firestore:8000
volumes:
- .:/go/src/app
2. 指数バックオフを組み込む
サーバアプリケーションではエラーが生じた際のリトライが重要です。
Google CloudのSDKでは透過的にリトライしてくれるエラーもありますが、例えばAPI上限に達した際のResourceExhausted
等は自前でリトライを組み込む必要があったりします。
リトライの戦略としては指数バックオフが一般的です。
指数バックオフは、失敗したリクエストをクライアントが再試行する際、失敗するごとに次の再試行までの待ち時間を増やしていく処理です。これは、ネットワークアプリケーションに使われる標準的なエラー処理方法です。
Cloud TasksやCloud Monitoring等のクライアントライブラリの場合、指数バックオフに関するオプションを引数で設定できます。
ListTasks(ctx, req, gax.WithRetry(DefaultRetryOption))
一方、Firestoreのクライアントライブラリには指数バックオフのオプションがありません。
そのためFirestoreからのレスポンスを検証し任意のエラー時にリトライするような処理を実装する必要があります。
頑張って自前で実装するか、先人が作った指数バックオフのパッケージを利用すると良いかと思います。
Goの場合、github.com/cenkalti/backoff
等があります。
指数バックオフを組み込む方法はググればいくらでも出てくるのでサンプルコードは割愛します。
実装の基本方針は、
- Firestoreから返ってきたエラーをgRPC status codeに変換する(
code := status.Code(err)
) - リトライ対象のコードだった場合リトライ(
if code == codes.ResourceExhausted
)
といった感じです。
Firestoreのリトライすべきエラーは公式ドキュメントに載っているので、実装の際、参考になるかと思います。
3. 大量データを高速で書き込む
Firestoreはシリアルにデータを書き込むよりBatch
で書き込んだ方が早いです。
更に速度を目指す場合、並列で書き込む必要があります。
シリアル書き込み < Batch書き込み <<< 並列Batch書き込みです。
データを一括入力するには、並列化された個別の書き込みでサーバー クライアント ライブラリを使用します。バッチ書き込みは、シリアル化された書き込みより優れたパフォーマンスを発揮しますが、並列書き込みほど優れてはいません。一括データ オペレーションには、モバイル / ウェブ SDK ではなく、サーバー クライアント ライブラリを使用する必要があります。
下記の記事にあるように、API制限に気を付けつつ、指数バックオフで適切にエラーを再試行しながら並列で書き込むのが正攻法です。
Goであれば、goroutine
の並列数をチャネル等で制御しつつ大量投入を行うことになるかと思います。
4. 接続周りのオプションを設定する
FirestoreのgRPC接続オプションはfirebase.NewApp()
のコンストラクタ時に引数で渡せます。
自分は下記の記事を参考に、コネクションを同期的に張るためにgrpc.WithBlock()
を渡す等しています。
その他KeepAlive
に関する設定等も渡すことができます。
必要に応じて設定すると良さそうです。
5. パフォーマンスを測定する
FirestoreのパフォーマンスはKey Visualizerで分析できます。
手前味噌ですが、Key Visualizerに関する記事を以前書きましたので参考になるかもです。
6. 監査ログを取得する
Firestoreに誰がアクセスしたかは、監査ログを追うことで確認できます。
こちらも手前味噌ですが、以前の記事です。よろしければ!
7. デフォルトのタイムアウトを回避する(Go限定)
Firestoreに限らずなのですが、Google CloudのGo SDKにはデフォルトのタイムアウトが設定されています。
下記にFirestoreのCommit
メソッドについて抜粋しますが、60,000ミリ秒のタイムアウトが設定されています。
func (c *gRPCClient) Commit(ctx context.Context, req *firestorepb.CommitRequest, opts ...gax.CallOption) (*firestorepb.CommitResponse, error) {
if _, ok := ctx.Deadline(); !ok && !c.disableDeadlines {
cctx, cancel := context.WithTimeout(ctx, 60000*time.Millisecond)
defer cancel()
ctx = cctx
}
...
}
大量書き込み等時間のかかるリクエストを行なった場合、稀にタイムアウト時間に達する場合があります。
回避の方法は公式ドキュメントにも記載されており、自分でタイムアウトを設定するか(自分で設定したタイムアウトがデフォルトタイムアウトを上書く)、環境変数GOOGLE_API_GO_EXPERIMENTAL_DISABLE_DEFAULT_DEADLINE
を設定する必要があります。
os.Setenv("GOOGLE_API_GO_EXPERIMENTAL_DISABLE_DEFAULT_DEADLINE", "true")
最後に
Key Visualizerがリリースされたり監査ログが充実する等、運用面の機能も充実してきたので、今後もしっかりキャッチアップしながらFirestoreと付き合っていこうと思います!