🧯

Firebaseを卒業するに至った理由

2022/10/14に公開約3,700字10件のコメント

https://parque.io

株式会社パルケの悩めるCTO、みつるです。

この記事は先日ツイートしたものを加筆修正したものとなります。

https://twitter.com/MitsuruOkura/status/1577462184967540738?s=20&t=mpdaStbowHXC8HNEk_ccJg

株式会社パルケでは、最初のプロダクト開発でFirebaseを全面的に採用し、1年以上にわたって運用してきました。

一方でFirebaseの制限・制約がつらい、と思う理由も徐々に積み重なってきました。
結果、Firebaseから卒業するという大きな判断をしました。

今その判断は間違っていなかったと思います。

ここではFirebaseがつらかった理由をまとめておきたいと思います。

https://firebase.google.com/

第10位:Firebaseでは実現できない要件があった。

パルケのプロダクトでは、エンタープライズ向けにセキュリティ対応を強化したい、という要件がありましたが、以下要件の実現が難しかったです。

  • IPアドレス制限をできるようにしたかったが、Firestoreへアクセス毎にIPアドレスで制限をかける方法が見つからなかった。
  • Cloud Storage for Firebaseで、ダウンロードURLを取得する時に権限チェックをかける事はできたが、そのURLにアクセスした時に権限チェックをかけること方法が見つからなかった。

第9位:Cloud Functionsのデプロイに時間がかかる

Github Actionsから自動デプロイする際に、変更に影響があるFunctionだけを選択してデプロイする方法がわかりませんでした。

各Functionで共通で利用されている定数や処理を変更した時に、その変更の影響があるFunctionを機械的に選択してデプロイに投げる、というロジックを書くのが難しく断念しました。

結果、毎回全Functionを再デプロイするしかなく、所要時間が30分以上かかり、その間サービスを止めざるを得ない事がありました。

第8位:自動テストを書くのがつらい

Firebase SDKを呼び出している処理のテストを書くの事が困難でした。

  • SDKの処理自体のモックを用意していくのは難易度が高く手間もかかります。
  • SDKの呼び出しを部分を外出ししてWrapする場合、本来必要のないファイル分割とPropsの受け渡しが発生しました。

第7位:各種制限が意外ときつい

はじまったばかりのサービスでも、制限を意識しないといけないケースがあり、将来に不安を感じました。

Cloud Functionの数:1,000

初期のサービスも簡単に数は100を超えたので、気軽にFunctionを追加しないように注意していかないといけないな、と思いました。

処理の種類ごとにFunctionを作るのではなく、ある程度処理をまとめてパラメータで内部で動きを変えるようにした方が良かったのかもしれません。

Firestoreの複合インデックスの数:200

複数の条件でCollectionから検索する場合、複合インデックスを定義する必要がありますが、この制限の200がきついと思いました。

むやみに複合インデックスを追加せずに、クライアントでデータをとりあえず取得してフィルターする、という対応が必要になりました。

Firestoreのドキュメントの最大サイズ:1MB

リッチテキストの内容や、アクセス権のためにuidの配列を持たせようとすると1MBの壁はかなり厳しいです。

サイズ制限を超える可能性があるドキュメントは、サイズを超えた時にドキュメントを分割してリンクリストにするといった複雑な対応を入れて置く必要があります。

この制限は、Firestoreがデータの従量課金をデータサイズや転送バイト数ではなく、ドキュメント数にしている事が要因だと思います。

Authのカスタムクレームのサイズ:1KB

細かいロールの条件を入れておこうと思うと、1KBでは足りないので、他のドキュメントなどに格納しておく必要がありますが、そうするとセキュリティルールの記述がかなり難しくなってしまいます。

第6位:Storybookがつらい

FirebaseのSDKを使うコンポーネントをStorybookで表示する事が難しかったです。Storybookで表示するデータをFirebaseからではなく、外から渡せるようにしておく必要があります。

対策として、FirebaseにさわるコンテナコンポーネントとStorybookのための表示用コンポーネントに分ける必要がありました。

Storybookのために本来必要ではないコンポーネント分割をして、プロダクトの複雑さを増やすのは本意ではありませんでした。

第5位:セキュリティルールがつらい

スキーマに型がないため、ルールの定義は文字列の組み合わせとなり、型サポートのない辛いコーディングとなります。

ドキュメントのフィールドのuidをチェックする、ぐらいであればシンプルなのですが、それ以上の事をしようとすると途端に難易度が上がります。

パルケでは、チーム管理、そのチームの中でのロール、そしてゲストに応じた権限制御が必要でしたが、その実現には大変な苦労が伴いました。

ドキュメント毎にアクセスできるユーザーのuidを配列で持たせるのがセキュリティルール的には一番簡単です。
ただし、そのためにはチームのメンバー追加・削除・ロール変更に伴って大量のドキュメントの同期処理が必要となり、ボトルネックとなっていました。

第4位:Cloud Functionsのコールドスタート

サービススタート初期のユーザー数が少ない時期は、コールドスタートが多発します。

5秒から10秒ほどかかるため、ユーザーで処理待ちが発生する場合は致命的です。

minimum instanceを指定する回避策もありますが、これはコストへの影響が大きいと思います。

Cloud Functionsを利用するのは、Firestoreに直接参照したり更新できない場合の小さな処理が多いのですが、それを一つ一つコンテナ化して都度起動・停止するというのがあまり効率的ではない気がします。

大半のケースでは、Expressで各種エンドポイントを処理するAPIサーバーを一つたたて起動しっぱなしにしておく方が合理的だと思いました。

第3位:ドキュメントの件数単位の課金

Firestoreはドキュメント単位の読み出し、書き込み数で課金されます。

私のようなケチな人間は、なるべく一つのドキュメントに情報を詰めたり、再フェッチをなるべく抑制する対策をするなど、プロダクトの本質ではないことにリソースを使ってしまいました。

Realtime Databaseの方はデータ容量と転送量による課金のため、比較的余計な事を考える必要がなく、制約もシンプルで少なかったと思います。

第2位:クライアントコードが肥大化する

Firestoreから取得したデータは、画面に表示する目的で正規化されていないため、クライアント側で統合したり加工したりフィルタする処理が多くなります。

結果的に、クライアントのコードが肥大化して読みづらくメンテしづらいものとなっていきました。

第1位:クラウド破産の恐怖

Firestoreは並列処理に非常に強いため、下手なコードで無限ループが発生すると、請求もあっという間に膨れてます。

対策は予算アラートメールのみで、上限で自動停止はできません。
朝起きてアラートメールに気づいた時にはクラウド破産、、という恐怖に常に怯えていました。
無限に自動スケールは心臓に悪いので、個人的には今後も避けていきたいと思っています。

最後に

Firebaseはシンプルに使い始められる、非常にすぐれたサービスである事は間違いありません。
ただ、ある程度複雑なアプリケーションを動かす場合は、Firebaseのサービスに対する深い理解と高度なアーキテクチャや開発運用の設計が求められるようになると感じました。

次のようなプロジェクトでは、Firebaseの採用で享受できるメリットが大きく、よい選択肢となるでしょう。

  • シンプルで単純なアプリケーションのプロジェクト
  • またはFirebaseに熟練したエンジニアとFirebaseに適したアーキテクチャで運用できるプロジェクト

ここまで記載した辛かった事は、2021年末時点のもので、現状ではあてはまらいものがあるかもしれません。
また、上記は私の調査不足で事実と異なる部分があるかもしれません。
もしそういったお気づきの点がありましたらコメントで教えていただけますと幸いです。

Discussion

技術は適材適所で、そもそもFirestoreはRDBの代替として設計されていないと思います。例えば、チャットや通知などのリアルタイム性のある機能などに採用すると開発コストが抑えられる気がします。

コメントありがとうございます!
今回の経験で適材適所、という事を痛感できました。
確かにチャットや通知でリアルタイム機能を使いたい時にうまく活用すると開発コスト抑えられそうですね。

Firestoreの最大サイズの話など共感するところが多く、Firebaseの辛みがよくわかりました。
ちなみに脱Firebaseの移行先はどちらを選定されましたか?

コメントありがとうございます!
移行先としましては、以下のような構成となります。

サーバー:GraphQLサーバー(GraphQL Yoga + Express)
DB:PostgreSQL
Realtime データ:Redis(GraphQLのSubscriptionを利用)
ORマッパー : Prisma

これらを全てRender.comで運用しています。
Render.comは環境構築・運用がFirebaseと同じぐらい簡単なのですごく気に入っています。

ご返信ありがとうございます!
Rendor.comなんですね
HerokuやAWSのAppRunnerなど選定に迷いますが、参考にしてみます!

ここに書いてあることは大体克服できると思いますねー。

ホットスタンバイ問題は本番環境で、メモリを適切にすれば全く問題ない。テスト環境だと、接続ユーザーが少ないので遅い場面が多く見られる。本番環境だとある程度ユーザーが接続していて問題ない。正しくチューニングすればコストもそんなにかからないはず。昔は結構悩んだこともあったが慣れたら気にならなくなった。

クライアントのコードはむしろ、バックエンドを別にした方が肥大化します。stream使えないケース多いので、graphQL入れてサブスクリプション入れてなど余計なメンテと実装コストがかかる。

functionsが100超えるというのはあまりケースとしてなくて無駄なものが多そうな。
会計ソフトとか集計が多いなどめちゃめちゃ複雑なものを作ろうとしているならわからなくもないが
アプリを見た感じその気配はなかった。

クライアントサイドジョイントおおいとかですかね?

セキュリティルールは使い方さえわかればテストもかけるし不満はないですねー。

例えばrails環境でのデプロイの方が時間かかると思います。ローカルのエミュレーションとか使ってないとか?サーバーのデプロイは確認程度しか使わないのであまり不便に感じない。

無限ループに関しては、作り方次第としか言えないので、firestore 以前の話に思えます。

コメントありがとうございます!
Firebaseを使いこなされている方のご意見、大変参考になります。

正しくチューニングすればコストもそんなにかからないはず。昔は結構悩んだこともあったが慣れたら気にならなくなった。

適切なチューニングのやり方を理解して設定していけば気にならなくなってくるんですね。

クライアントのコードはむしろ、バックエンドを別にした方が肥大化します。stream使えないケース多いので、graphQL入れてサブスクリプション入れてなど余計なメンテと実装コストがかかる。

トータルで考えるとコードと実装コストは増えていますね。
特にGraphQLのサブスクリプションは確かに苦労しました。
リアルタイムにListenする部分を楽できるのはFirestoreが優れているところですね。

functionsが100超えるというのはあまりケースとしてなくて無駄なものが多そうな。

少しFunctionsを細かく作りすぎてしまっていた気がします。
今思えば、Functionの種類を減らしてパラメータで処理を分けるようにした方が良かった気がします。

無限ループに関しては、作り方次第としか言えないので、firestore 以前の話に思えます。

作り方次第、、耳が痛いところです。
万が一発生してしまった時に、サービスは止まらない代わりに請求も青天井になってしまうリスクを取るか、サービスが止まっても請求は一定で止まる方がいいか。
身銭を切ってサービスを作成している身としては難しい判断でした。

コメントを踏まえて改善してみたって記事お願いします!

追加で気が付きました!

リッチテキストの内容や、アクセス権のためにuidの配列を持たせようとする

なるべく一つのドキュメントに情報を詰めたり

これは致命的に設計が間違っているので追記しておきます。
後に大きな負債となります。

collectionで適宜定義するのが定石で uidを配列でもたせたら大変なことになります。肥大化したときに著しくパフォーマンスは落ちアプリケーションが成長したときに必ず困ることになります。

Firestoreは会社のサービスとしていきなりリリースする前に、2,3個練習でアプリを作ったほうが良いです。そのうえで必ず失敗する、その後設計できるようになるのでいきなり自社サービスで初投入すると大変なことになります。

コメントありがとうございます!

アプリケーションが成長したときに必ず困ることになります。

まさにここで困ったところでした。いい経験になりました。

ログインするとコメントできます