FirebaseのTriggerアーキテクチャ
株式会社CauchyE CEO, CTOの 木村優 です。
今回はみなさんご存じのFirebaseに関する話を書きます。
目的として、Firebaseを使ったアプリの設計のお話を書こうとして本記事の執筆をはじめたのですが、長くなりそうだったので本記事では、Firebaseの構成要素であるFirestoreの特性と、それに起因する、アーキテクチャのパラダイムシフトの話をし、次回記事にFirebaseを使ったアプリの設計のお話を書きます。
というわけで、Firebaseの構成要素であるFirestoreの特性と、それに起因する、アーキテクチャのパラダイムシフトの話をします。
昔、その話を書いてあった優良記事が下記リンクの記事として存在していたのですが、なんやら記事は著者の都合により消えてしまっているようです。
Twitterやら、
はてブやら、
いろいろ形跡はあります。
削除するという著者の意図に反するかもしれませんが、どうしても読みたい方はウェブ魚拓など調べれば見つかるかもしれません。
さて、この記事(以後、「当該記事」と呼び、「本記事」と区別します)の概要をご説明します。結論は独自に要約すると以下です。
- RESTのようなフロントエンドからバックエンドへの「一方的待機」の仕組みではなく、バックエンドからフロントエンドへのデータフローも含む双方向のリアクティブな仕組みにより、RESTでやる「一回の呼び出しですべての業務を完了させる」という技術的負債の根元(Fat controller)とおさらばできる
- オートスケーリング、強整合性クエリと結果整合性クエリ、トランザクション、オートスケーリングなどを全て手に入れるということは、これまでのアーキテクチャではできなかった
順を追って説明します。
問題意識
REST APIが提供されている状況において、クライアント側では基本的に、「複数のAPIを決められた順番で叩かないといけない」というよりも、「自身(API)さえ叩いてくれたら、事前に決めた順番通りに処理を進めておく」となるように実装するほうが自然ですよね?
クライアント側が順番に関する責任を持たなくていいので、自然な発想なはずです。
しかしながら、「自身(API)さえ叩いてくれたら、事前に決めた順番通りに処理を進めておく」というようなAPIを作ったとき、このAPIは密結合の結合点となることが多いですよね?
「事前に決めた順番通りに処理」する処理が複数あった場合、その複数の処理が一つのモジュール的分割の領域に収まっていなければならないという制約はどこにもないどころか、むしろそういう一つのモジュール的分割の領域に収まっていない一連の処理を行うことのほうがよくあることだと思います。
この、「REST APIが密結合の結合点となること」を、当該記事は問題意識として持っているというわけです。
当該記事では以下のような比喩が使われていました。
このようなシステムを人間の世界に例えれば、1人のすべての業務を知り尽くした超優秀なリーダーがたくさんの指示待ち人間を引っ張って業務を遂行するのと似ています
このような超優秀なリーダーは、言い換えるならばFat controllerです。システムにいないほうが技術的負債がないですね。
組織としてそのまま見ても、超優秀なリーダーという単一点への依存は望ましくないと思います。
歴史的には、
「クライアント側が複数のAPIを決められた順番で叩かないといけない」
というやり方を採ることによって、
「REST APIが密結合の結合点となる」ことを回避する手法も産まれてきた、と当該記事は説明します。
それがマイクロサービスアーキテクチャです。
オンプレミスなインフラでモノリシックなアプリケーションが動いていた時代から、
モバイルアプリ、シングルページアプリケーション(SPA)のためにクラウド上でAPIが動く時代へと移り変わるにつれ、
クラウドのスケーリング能力を活かすことのできるマイクロサービスアーキテクチャが日の目を浴びたわけです。
結果として、「複数のAPIを決められた順番で叩かないといけない」という制約がクライアント側の実装の重荷になり
(それを解消するためのBackend For Frontendというさらなる階層を設けることになり)、また混乱を生むに至ったという観点で、
マイクロサービスアーキテクチャは人類にははやすぎた、と当該記事は喝破します。
さて、クライアント側が処理の順番に関する責任を持つというやり方でもなく、「REST APIが密結合の結合点となる」でもない、
それら両方を克服した新アーキテクチャが待たれますね?
当該記事では、「それこそFirebaseにより実現ができる」と主張されているのです。
Triggerアーキテクチャ
本節ではそれがどのようにFirebaseにより実現されるのかを説明しましょう。
※本節以降は当該記事へのリスペクトよりも本記事のオリジナリティのほうがウェイトが高くなります。
そもそも大前提として、Firebaseは以下の要素など(など。他にもありますが省略します)により成り立っていることを確認しておきます。
- Firebase Auth
- Firebase Functions
- Firebase Firestore
- Firebase Storage
語感でだいたい「あーAuthは認証ね」「あーFunctionsはサーバーレス関数ね」「あーFirestoreデータベースね」「あーStorageはストレージね」
とわかると思いますが、
ここで断言しておきたいのは、
「Functionsによりサーバーレス関数としてREST APIをつくる」
では一切ないということです。むしろこれこそが当該記事や本記事のキーポイントでもあります。
詳しく説明しましょう。
Firebase Functionsでは、たしかに「HTTPS関数」という(Functions内の概念上の)名前で、REST APIを生やすことはできます。
また、「呼び出し可能関数」という(Functions内の概念上の)名前で、SPA/モバイルアプリから、Firebase Authの認証情報つきで叩くことのできるREST APIを生やすこともできます。
しかしそれだけではありません。
Google Cloud Platform上にて構築された出版-購読型モデルサービス「Pub/Sub」のイベント発火を検知して発火する関数や、
Google Cloud Platform上のTime Schedulerと連携し、定期的に発火する関数、
そして本題に関係するのはここからですが、
Firestoreのデータ内容が作成・更新・削除のいずれかがなされたときに発火する関数「Cloud Firestore Trigger」、
Storageのデータ内容が作成・更新・削除のいずれかがなされたときに発火する関数「Cloud Storage Trigger」
があるのです。
イメージ、湧いてきたでしょうか?
FirestoreやStorageは、クライアント側から直接(書き込み含む)アクセスができるという特性があるのですが、
いわばクライアント側はFirestoreやStorageにデータを書き込むだけなんです。
どういう順番に処理をしないといけないとかはクライアント側は気にしなくて良いです。
サーバー側はどうでしょうか?
「Cloud Firestore Trigger」を、複数のモジュール的分割領域をまたがった、密結合な結合点にせざるを得ないでしょうか?
しなくていいですね?
ここが非常に重要なポイントなのです。
データベース内の、1つのドキュメントに関する作成・更新・削除イベントを購読する購読者は複数あってなんら問題ありませんから、
購読者たる「Cloud Firestore Trigger」「Cloud Storage Trigger」は超優秀な上司・スーパーマン・Fat controllerである必要がないのです。
先ほど、「Cloud Pub/Sub」はGoogle Cloud Platform上で出版-購読型モデルを実現するためのサービスだと言いましたが、それと同じです。
いわば「Cloud Firestore」「Cloud Storage」のデータの内容一つ一つが、出版-購読型モデルを実現するためのメッセージの一つなわけです。
出版側は購読者に関する情報を持たず、ブロードキャストします。出版側はクライアント側のことです。
これはつまり、クライアント側は処理の順番の責任がないことに相当します。
購読者は複数併存できます。購読者はここではTriggerのことです。
これはREST APIのように一か所に密結合な結合点として存在する必要はないことを意味します。
イメージ、湧いてきたでしょうか?
まとめると、いままで
- クライアント側のREST APIを叩く箇所の開発
- サーバー側のREST API(密結合な結合点)の開発
だったものが、
- クライアント側のFirestore/Storageに書き込みを行う箇所の開発
- サーバー側(サーバーレスだが…)のTriggerの、疎結合な形での複数の開発
になります。
このように、REST APIの積年の課題を見事に解決するこのアーキテクチャを、
本記事の(当該記事ではない)筆者は、個人的に「Triggerアーキテクチャ」と名付けています(もし他の名前があるよとかあれば教えてください)。
イベント駆動×マイクロサービスアーキテクチャについて
当該記事はマイクロサービスアーキテクチャが人類にははやすぎた、と書かれていましたが、問題はクライアント/BFF側が順番に責任を持つことであって、マイクロサービスアーキテクチャ自体に内在する問題ではないとも思われます。
正確に言うと、RPC型マイクロサービスアーキテクチャの問題と言えそうです。イベント駆動×マイクロサービスアーキテクチャはこの問題を解決しています。
本記事の「Triggerアーキテクチャ」は、解決したい問題の対象(=密結合な結合点)を解消したという点でイベント駆動×マイクロサービスアーキテクチャと非常に近いですが、Triggerアーキテクチャは一切マイクロサービスであるとは言っていないので、そこが異なります。
また、Triggerアーキテクチャは「イベント駆動×マイクロサービスアーキテクチャ」ではないイベント駆動アーキテクチャとも異なります。正確に言うと、背反ではありませんが、Triggerアーキテクチャのほうが部分集合です。Triggerアーキテクチャは、イベント情報がDB/ストレージの差分を持つことにより、購読者が複数のモジュール的分割領域をまたがった密結合な結合点になる心配がありません。イベント駆動アーキテクチャは、そのあたりは言及をしていない広範な概念であると思われます。
リアルタイムアップデート
先ほど、「購読者は『ここでは』Trigger」なる表現を使ったのは、
SPA/モバイルアプリ側も購読者になれるためです。
具体的に言うと、SPA/モバイルアプリ側はWebSocketを使って、Firestoreの内容更新をリアルタイムに受け取ることが可能です。
これを使うと、
- SPA/モバイルアプリ側からFirestoreへの書き込み命令を送る
- Firestoreは書き込み命令を受け、ブロードキャストの形でイベントを発行する
- Functionsはイベントを購読し、イベント内容に応じて設定されたTriggerを発火させる
- Trigger内からFirestoreに対してさらに別の書き込み命令を送る
- SPA/モバイルアプリ側は「自身が送った書き込み命令」「Triggerが送った書き込み命令」それぞれの結果を反映したFirestoreの内容をリアルタイムに受け取る
このような流れとなります。
このあたりは本記事の要点ではありませんが、firebaseは、リアルタイムアップデートだけではなく、状況に応じて強整合性クエリと結果整合性クエリをサポートしており、これだけ広くサポートしてくれるのはfirebaseの強みだ、というのが当該記事の二つ目の要点でした。
Firebaseのつかいにくさ
さて、ここまでさんざんfirebaseの良さを力説してきましたが、一方で
「Firebaseは使いにくい」「Firebaseはクセがある」
ともよく聞きます。
ごもっともだと思います。
なぜごもっともかと思うかというと、「REST API中心の構成のアプリケーションを作る」という(従来の)やり方にFirebaseを適用させようとするのは、
そりゃ向いてないだろうなと思うからです(向いてないんだけど、HTTPS関数や呼び出し可能関数によって実現はできるというのがさらに誤解を広めている感じも)。
Firebaseは、本記事の言う「Triggerアーキテクチャ」のアプリ開発に向いているのです。
「REST API中心の構成のアプリケーションを作る」には向いていないので、それは別の手段を採ったほうが良いです。
とはいえ、「Triggerアーキテクチャ」をやるにしても、
- NoSQLのスキーマレスはしんどい
- DBのスキーマ変更(マイグレーション)がしんどい
という声もよく聞きます。
ごもっともだと思います。 ぐうの音も出ません。
一方で、
NoSQLのスキーマレスはしんどい
に付随して、
「TriggerアーキテクチャのよさはわかったけどRDBでよくない?」
という問いはあると思いますが、これに関しては「Firebaseのつかいにくさにはそれなりのわけがある」ことの説明をしたいと思います。
まず、RDBを採用したい場合の代替案として、実際にFirebaseの代替を自称するSupabaseというプラットフォームが存在します。
SupabaseはPostgreSQLを採用しています。バリバリよく使われる一般的なRDBの一つです。
先ほど、FirestoreはWebSocketによってリアルタイムに内容を受け取ることができると説明しました。
この機能はSupabaseもサポートしています。
そしてAuthと連携し、クライアント側からの直接の書き込みもできます。Firestoreと同様ですね。
ただしこのSupabase、Triggerアーキテクチャに肝心なFunctionsのTriggerに相当する要素がありません。
SupabaseはFunctionsがComing soonだと書かれてあるので今後SupabaseのFunctionsの中にTriggerが含まれる可能性は否定できませんが、
現状、RDBでTriggerアーキテクチャをやる手段は存在していません。
なぜRDBでTriggerアーキテクチャをやる手段が存在していないか?
当然ながらSupabaseがこの問いを無意味化してくれる可能性は残っているのですが、一つ仮説として上げられるのは
ACID特性よりもBASE特性を採ったほうがTriggerアーキテクチャに向いているというものです。世の中には
- Atomicity
- Consistency
- Isolation
- Durability
というACID特性を持つデータベースと、
- Basically Available
- Soft-State
- Eventually Consistent
というBASE特性を持つデータベースがあります。
CAP定理というものにより、文字通り「万能な」データベースは世の中に存在しないことがわかっているので、世の中のデータベースは、
ACID特性かBASE特性のどっちかを採っています。両方は採れません。
※詳しくはググってください。
ここで、RDBのPostgresは、ACID特性を持ちます。
一方でFirestoreは、BASE特性を持ちます。
Firestoreに限らず、一般的にNoSQLはBASE特性を持ちます。
そもそものNoSQLが登場した目的意識というのは、このACID特性からBASE特性にシフトしてスケーラビリティを向上させることにありました。
ACID特性のデータベースは、基本的には一つのマシンで動作するのが自然な設計になっているので、スケールさせることは不可能ではないですが、
スケールするように複数のマシンに分散化して運用するにはそれなりの機構が必要で、かつ縮退(一度拡張したものを元に戻す)も難しいです。
Firebase Functionsはサーバーレスの構成になっていて、スケールがしやすい構成になっています。
このスケールしやすい構成になっているFunctionsに対して、Firestoreを、スケールが(不可能ではないが)比較的難しいACID特性のDBにした場合、
Firestore側がボトルネックになるという考えがあったのではないでしょうか?
あくまで仮説ですが、仮説としては成り立っていると思います。
また、この経緯を紐解くとわかるのですが、SQLからNoSQLへの過程は、「スキーマ定義から開放する」ことを念頭に置いたものではなく、
「BASE特性へのシフト」を念頭に置いたものだったと言えます。
「スキーマ定義の消失」は、「BASE特性へのシフト」を念頭に置いたSQLからNoSQLへの過程の副産物だ、ということができます。
(実際にAWS Dynamo DBはスキーマ定義っぽいものがあり、NoSQL=スキーマ定義がないDBだと誤解していた昔の筆者はNoSQLの意味ないじゃんと思ったりもしました)
「『スキーマ定義をなくす』という目的を持ち、却ってスキーマ定義があるよりも保守管理の観点で使いにくくなった自爆設計」のNoSQLなFirestoreを使うのはしんどい、というのは思って欲しくなく、
「BASE特性にシフトし、サーバーレスアーキテクチャと組み合わせることで強力な『Triggerアーキテクチャ』を生み出すための副産物」として、
スキーマレスと付き合っていくと思っていただけたらな、と個人的には思っています。
※繰り返しにはなりますがスキーマレスに起因する保守管理のペインはそれはそれでごもっともです。
まとめ
- Triggerアーキテクチャは、マイクロサービスアーキテクチャの二の舞にならず、そしてREST APIの積年の課題を解決した。
- Firestoreは、Triggerアーキテクチャに向いている。従来のRESTアーキテクチャにはあんまり向いてない(できるけどより良い手段がある)。
- FirestoreのNoSQL特有のつらみはたしかにあるが、それはスキーマレスを目的として自爆したものではなく、強力な革新(Triggerアーキテクチャ)の代償と思って付き合ってみてもらいたい。
CauchyE は一緒に働いてくれる人を待ってます!
ブロックチェーンやデータサイエンスに興味のあるエンジニアを積極的に採用中です!
以下のページから応募お待ちしております。
Discussion