Edited at

Firestoreを本番で半年運用したアーキテクチャ:その① 〜Firestoreを中心に据えた全体設計のコンセプト〜


これは?

Firebase Meetup #10 で 「Firestore導入前に検討したかったベスト5」 というテーマで発表したのですが、

まさかのメインの話の部分を図2枚で済ますという荒業をしてしまったので、ちゃんと書き納めます。

(反省のツイート...→ https://twitter.com/pitown/status/1093141514186706944 )

そしてなんか、書きたいこと多くない?と気付いたので、3回くらいに分けました。

①Firestoreを中心に据えた全体設計のコンセプト ←今回

②PubSubとCloud Functionsを使って、FirestoreのCollectionをマイクロサービスに見立てた話

③パフォーマンスの劣化対策として、CacheのCollectionを作った話

ちなみに、書き終えてから、初期構成の話しかしていないな、と思ったのですが、それ以降の話はまた次回以降に書こうと思います。


Firestoreを選んだ理由


  • とにかくその時にこのサービス向けに手を動かせるのが自分しかいなかった

  • 自分はフロントエンドがつよい

  • 比較的リアルタイム性が求められるアプリケーション

  • とにかく急ぎ

という理由。

いま考えても、使わない理由は特になかったな、と思います。

そんな状況なわけだったので、Firestore以外の選定も、初期構成はとにかくリソースを少なく早く立ち上がることを考え、できる限りマネージドなものに乗っかっていきました。


Firestoreの周りを固めるアーキテクチャ

まずは、ざっくりとチャット部分だけを抜き出したうちのプロダクト仕様がこれです。

ユーザーとのLINEのやり取りを、LINE API越しにやり取りするというものです。

これの青い部分が自分たちでつくらなければならないところ。

そうした時に、


  • LINE APIからメッセージが送られてくるのを決して落としてはいけない

  • 双方リアルタイム(スタッフ→ユーザーは普通にリクエストするだけだけど)

ということを考えるわけですね。

それを元につくったのが、以下の初期バージョンのアーキテクチャです。

(以前 発表資料 で書いたもの)

image.png

(元のスライドと若干違うのは、ええ、そうです、今、見たら違ったなって気付いたからなんですよ..)


サーバサイドについて

まず、LINE APIのWebhookを受けるためのGAEが一台あります。

image.png

これは要は絶対に落ちてはいけないくんであって、とにかくログを吐きつつpubsubにpublishするだけ。

裏がどんなに失敗しようが、ここのログでデバッグできるようにしたかったんですね。

Nginxがいいかなと思ったものの、マネージドでまとめるのであれば、

すべてをマネージドでまとめなければ恩恵を受けにくいと考えてのGAEってかんじですね。

あと、PubSubが裏にいるので、

image.png

ここでデータを一時的に貯められる。

急にズドンと来ても、PubSubで吸収してくれるという、若干オーバーな設計になってます。

ただ、料金とか工数とか見ると、別にリッチに倒しておけばいいじゃん、というスタンスが通る程度のものだったので、

最初からPubSubでやってます。

それで、PubSubからPull型でデータをSubscribeしていく(Node.jsのSDKに乗っかりたかったのでPull!)、という流れ。

あとは、責務に応じて、Cloud FunctionsとGAEを分けてFirestoreに保存していました。

image.png

そして、もう一つこのあたりで、Cloud FunctionやPubSubを活用しやい理由がありました。

PubSubは、at least onceを保証していますよね。

なので、1回かもしれないけど、複数回実行されるかもしれない。

そして、Cloud Functionsも晴れて先日GAになったときに、at least onceを保証しました。

(ずっと、「今はGAじゃないから、at least onceは保証できないよ」という文字が煌々としていて気になってたんですよ…)

そうすると、PubSubとCloud Functionsのコンビは、1回以上の動作が起こります。

一方で、Firestoreは、原則的に、更新するものは、setで更新・追加するようにするのがスタンダードです(と思ってます)。(setは、新規追加も更新もできて、同一IDの場合上書きするものです)

ということは、Firestoreでsetで書き込んでいる限り、新規追加だろうが更新になってしまっただろうが、同じ値の複数回の書き込みが走っても、問題無いということになります。

これにより、

PubSub〜Cloud Functionsで1回以上の実行を保証、

Firestoreで1回だけの書き込み(にsetだと見える)を保証、

という形で、全体の整合性を取れるようにしました。

実際の思考順序は、↑の流れは試行錯誤中にこの流れになっただけであって、一番はじめに考えていたのが、このあたりの、「書き込み回数をコントロールする」ことでした。

これがFirestoreの設計の基本なのかな、と個人的には考えています。

また、書き込みに関しては、うちのユーザーはLINE越しでかならずHTTPリクエストが発生するので、Firestoreに書き込むだけ、ということだけでは完結しないので、

LINE APIにHTTPリクエストしつつ、Firestoreにも保存する、ということをしています。

ここを変に複雑にするとかえってメンテがしんどくなるので、Firestoreを拡張なんかはしないように意識しました。


クライアントについて

image.png

クライアントは、読み込みについてはFirestoreの超強力な特性を享受できるので、それを最大限活かしています。

最初にFirestoreをSDKで利用したときに、めちゃくちゃ強力だな、と実感していて、

できれば、それを最大限素直に表現してあげたいな、というのが願いでした。

特に、自分たちのビジネスは開発する段階では、ビジネスが死ぬかもしれないし、大きく変わるかもしれない、

というものだったので、

ちゃんとビジネスの成立のタイミングに合わせて堅くしていきたいなと。(もっと言うと資金調達とか採用とかと併せて)

そのときに、viewにはReactを用いようと決めていたのですが、イベントシステムをどうしようと。

既に、圧倒的にReactといえばRedux、という世論に加えエコシステムが出来上がっていたのですが、どうにもRedux最初重いな、と思ったわけです。

Fluxの世界に乗っかって、リアクティブにループ回して理想系を保って、適切にstore分割して、

そうやって作っていくスピード感だと、スクラップアンドビルドの回転そこまで上げられないよな、と思ってしまったわけです。

Reduxにすごく精通していれば話は別なのかも!ですが。

僕がReduxやるなら、もっとチームがいて、綺麗なAPIがあってやりたいなと。

僕が欲しかったのはもっとミニマルなもので、そのときの僕のイメージはこれです。

image.png

もう、とにかくシンプルに書き出してくれ!という思いです。

やりたい操作は簡単なデータの結合とFilter程度なんだ!という。

その希望を見事に叶えてくれたのが、MobXでした。

image.png

(ちなみに、システム化してすぐに突然数千人が入ってくるような珍しい状況でもあったんですが、このリアクティブな感じを眺めているのがめちゃくちゃに気持ち良かったです。)

MobXはとにかく自由なので、つまり指針がないと、際限なくてヤバいんですが笑、

ザ・シンプルに使っている分には問題ないだろうと。

このへんの意思決定としては、初期は速さを犠牲にしてしまうと、できるハズだった検証がいくつかできなくなるということになり、わりと感覚に頼って戦わざるを得ないことになりがちでアカン、という考えがありました。

ちゃんとやるぞ!というタイミングでしっかりとしたつくりにシフトしていくのが良いと思っていて、もしかしたらリファクタリングはある程度の覚悟しないといけないかもしれないけれど、利益がちゃんと出ていればそれなりのリソースをかけられるとも思っていて。

↓保守性については、こんなイメージを持ってやっています。最初にしっかりやりすぎてもドメインのフィールドを正確に定義できないのであれば、徐々にそれがねじれていってしまうことにも繋がるので、最初はとにかく素直に!の気持ちです。

image.png

なので、事業の形が定まって利益を上げ始めるときに、変に色に染まった設計とかがないようにしようという感じでつくっていきました。スケールしたときに後で来て(?)つくりかえてくれる達人が困らないように!!

ということで、ちょこちょこ各論をはさみつつも(言いたいことだけ書いた)、全体でやっていたコンセプトを書いてみました。

次は、もう少し、細かいFirestoreの設計の話なんかを書きます。