49
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FirestoreのリアルタイムリスナーonSnapshotを使うと無料枠を飛び越えていた

Posted at

はじめまして。
2次元ツリーを使ってゆるくチャットするサービスQ&Qを作っているあどにゃーです。
スクリーンショット 2021-05-30 15.56.03.png

#無料枠を超えて課金されていた
ユーザが増えずほぼ開発仲間とのチャットと化しているQ&Qですが、気づいたら無料枠を超えて課金されてました:sob:
ほぼ2人でチャットしているだけなのに、なぜfirestoreの1日の読み取り無料枠の5万回を超えるのかの調査が始まったのでした:tired_face:
#スクリーンショット 2021-05-30 15.46.46.png

#onSnapshotとは
onSnapshotはfirestoreのリアルタイムリスナーをはる機能です。Twitterや5chに代表されるような昔の掲示板は更新ボタンを押してリロードしないと新しい書き込みをGETすることはできません。しかしonSnapshotを使うとLINEのようにリロードなしにリアルタイムで新しい書き込みを読み込むことができます。
2次元チャット機能でもリアルタイムで周りの書き込みを見たいため、このonSnapshotを下記コードのように使っていました。

  this.listener = this.treeRef.collection('nodes')
      .onSnapshot(async nodes => {
        for (const node of nodes.docs) {
          // nodes array
          this.nodes[node.id] = node.data()
          // init for issue and questions
          let qData = node.data()
        }
      })

#onSnapshotの罠
onSnapshotは対象のcollectionに追加・変更・修正がある度にリアルタイムでデータを読み込みます。2次元チャットでいうと各付箋のnodeが変わる度に読み込まれるわけです。つまり、二人で50回チャットを繰り返すと、2人 x (1 + 2 + 3 + ... + 50)回の読み込みが走るわけです。この時の読み込み回数は
2人 * 50*(50+1)/2 = 2550回
おそろしい:innocent::innocent::innocent:
たかだか50回のやりとりをすると2550回読み込みが起きているのです。

###さらなる罠の4倍界王拳
それでも1日にたかだか数100しかQ&Qしてないので、5万回も行かないだろうと思ったのですが、もう一つの罠がありました。それはnodeのデータの中に時間をタイムスタンプを司るcreatedAt, updatedAt
といったプロパティを持っているケースです。

 nodes = {
  createdAt: timestamp,
  content: text,
  updatedAt: timestamp
 }

こういう場合、firesotreの仕様上、同時にすべてのデータが更新されるわけではなくプロパティの更新に時間差が生まれ、各プロパティの更新の度にonSnapshotは発火します。

  1. nodesのプロパティはまずcontentが更新されonSnapshotが発火
  2. createdAtの時間が更新されonSnapshotが発火
  3. updatedAtの時間が更新されonSnapshotが発火
    のように、2倍、3倍界王拳が起きてリード回数が何倍にも膨らむケースがあるようです。Q&Qのケースでは4倍界王拳になっていました。
    4 * 2550 = 10200回

50回の書き込みで1万回の読み込み

おお、なんという錬金術:innocent:
これなら1日5万回の書き込みをかんたんに超えていくのも納得です。

#docChangesで差分更新に変更
onSnapshotにはcollectionの追加・削除・変更にその変更差分だけを取得するdocChanges()があります。これを使って差分だけを取得し、Vue.js側でwatchプロパティでnodesの変化を監視して変更があれば2次元ツリーを更新してあげるだけ。

    // nodesのlistenrを作る
    this.listener = this.treeRef.collection('nodes')
      .onSnapshot((snapshot) => {
        snapshot.docChanges().forEach((change) => {
        // 追加時
          if (change.type === 'added') {
            this.nodes[change.doc.id] = change.doc.data()
          }
          // 修正(更新時)
          if (change.type === 'modified') {
            this.nodes[change.doc.id] = change.doc.data()
          }
          // 完全削除時
          if (change.type === 'removed') {
            delete this.nodes[change.doc.id]
          }
        })
        this.isListened = true
      })
  }

これで1回で読み込まれるのは、1ノードの変化になるので、つまり、二人で50回チャットを繰り返すても、2人 x (1 + 1 + 1 + ... +1)回の読み込みしか走りません。
つまり、
2人 * 50 = 100回
で100回の読み込みで済むことになります。nodesのプロパティ更新の時間差で重複更新がおきても、たかだか100~400回の範囲に収まるので気にする必要はありません:wink:

#まとめ
onSnapshotをそのまま使うと読み込み回数が爆発したので、docChanges()を使用して差分更新にする修正を行いました。
###これで多くの人が使っても無料枠で耐えられるはずなの、ぜひQ&Qを使ってみてください:sweat_smile:

###スクショした記事
https://qnqtree.com/tree/AtYVuJS5kdQPbaZfMvue

49
38
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?