エンジニア(プログラマー?)として仕事に就いて早8ヶ月、いつもお世話になってるQiitaへの初投稿です。
SNSアプリケーションを作成するときにもはや必須とも言えるフォローしている人の投稿を表示する、タイムライン機能をFirestoreで実装する方法について書いていきます。コストが1/100
以下になったかもって結論の記事です。
(計算式間違ってんぞ! 意味わからん! こういう要望どうすんじゃ! とかあったら教えてください )
はじめに
Firestoreは書き込み、読み込みなどの通信回数によって課金されるため、如何に通信回数を少なくするか頭を悩ませながら開発されているかと思います。
タイムライン機能における通信回数は、(自分が調べた限りでは)フォロワーの数が多くなるにつれて一投稿あたりのコストが大きくなってしまいます。
(コストについて、Ninoさんによるこちらの投稿が非常にわかりやすい説明を掲載してくれています。)
https://note.com/deerboy/n/n6fb4e57d30c6
以下のようなフォロー・フォロワー構造を取り、投稿を行うたびにフォローしている人たち各々のタイムラインにドキュメントを作成するといった事を行なっております。
つまり、100万人のフォロワーがいる人物が投稿すると、100万ものドキュメントの作成が必要になってしまうということです。
users: [ // user collection
userId: { // doc
follow: [ userId: { created_at } ],// follow collection
followed: [ userId: { created_at } ],// followed collection
timeline: [ id: { userId: id, postDatas: {} },// timeline collection
}
]
掲載していただいていた試算を紹介しますと、
[平均300人のフォロワー]
[ユーザー4500万人]
[3ツイート(/日)]
[タイムライン表示10回(/日)]
以上の条件での一日のコストは730万円
を超えてしまうようです。
この安くないコストを、なんとか小さくできないかと考えたものが以下に記す内容です。
目的
ローコストなタイムライン機能の実装
実装
データモデル
まず、フォローフォロワーモデルを以下のように設定しました。
users: [ // user collection
userId: { // doc
follow: [ userId: { created_at } ],
followed: [ userId: { created_at } ],
listsForTimeline: [
id: { userlist: ['userId', ......] } // 配列
],
timeline: [
id {
userId: id,
postData: {},
allowed_users: ['userId', ......], // 配列
created_at: time
}
]
}
]
先述と異なる主な点はlistsForTimeline
と、timeline.allowes_users
を追加した点です。
ユーザーのフォローが成立した場合など、タイムライン配信させたい場合に、listsForTimeline
にarrayUnion
で配列の要素として追加していきます
投稿
ユーザーによる投稿アクションが発生したとき、listsForTimeline
から配列をコピーし、timeline
に新しいドキュメントを作成します。
listsForTimeline: [
id: { userlist: ['myUserId', 'user2', 'user3'] } // timelineにコピー (自分にも表示したいのでmyUserIdいれます)
]
/* ↓投稿↓ (タイムライン以外の使用するなら、別途postを作成したり) */
timeline: [
id {
userId: id, // 投稿したUserのID
postData: {hoges: fugas}
allowed_users: ['myUserId', 'user2', 'user3'], // listsForTimelineからコピー
created_at: time
}
]
このようにする事で、次に説明するクエリでタイムラインを取得できるようになりました。
タイムラインの取得
ユーザーが閲覧可能なタイムラインを取得します。(collectionGroup
へのindex作成は適宜必要です)
const posts = []
db.collectionGroup('timeline')
.where('allowed_users', 'array-contains', userId)
.orderBy('created_at', 'desc')
.get()
.then(snapshot => {
snapshot.forEach(postRef => {
posts.push(postRef.data().postData)
})
})
これでタイムライン内の投稿を取得することができました。
効果
今回、投稿に伴うタイムライン作成の際にタイムライン用のリストを利用することにより通信回数の削減を試みました。
実際どの程度の効果が生じるのか概算してみました。
先述したサイトと同じ以下の条件で考えてみます。
[平均300人のフォロワー]
[ユーザー4500万人]
[3ツイート(/日)]
[タイムライン表示10回(/日)]
読み取りアクションの数は、
// 投稿時[`listForTimeline`からの読み取り] + タイムライン表示時[`timeline`の読み取り]
// (リストの数 * ツイート数 * ユーザー数) + (クエリ発行数 * 読み込み回数 * ユーザー数)
(ceil(300/10000) * 3 * 45,000,000) + (1 * 10 * 45,000,000) = 585000000
書き込みアクションの数は、
// 投稿時の書き込み
// リストの数 * ツイート数 * ユーザー数
(ceil(300/10000) * 3 * 45,000,000) = 135000000
すなわちかかるコストは
// 読み取り回数 * $0.06(/10万回) + 書き込み回数 * $0.18(/10万回)
(585000000 * 0.06*10^(-5)) + (135000000 * 0.18*10^(-5)) => $594 => 64650円
731万円 => 6.5万円と 1/100 以上に節約できました!!
とっても満足。
補足
実際は以下のような理由によりもう少しコストはかかってしまいます。
- リストの登録のために1クエリ必要
- クライアントに最大1万要素の配列なんか送りたくないんでfunctoins使います
-
rule
の設定もしなきゃいけないんじゃあ
それでもだいぶ安くなるんじゃないかな。きっと。多分。
おわりに
はじめにも言いましたが...
(計算式間違ってんぞ! 意味わからん! こういう要望どうすんじゃ! とかあったら教えてください )
あと、もっといいやり方あるぞ! っていうのも教えてください。 何卒。
ありがとうございました。