GCP - Firestoreについて

今まで、Google App Script + Spreadsheetで運用してきた勤怠管理ツールを、
Firestoreに乗せ換えてみることにしました。

調べたことなどを、まとめておこうと思います。


## Firestoreのデータ

### Firestoreのデータの持たせ方
Firestoreは、Key-Valueペアで構成されるフィールド、フィールドを複数もつドキュメント、ドキュメントをまとめたコレクションといった感じで構成されます。
また、コレクション-ドキュメント-サブコレクション-ドキュメントといった具合に、階層構造をもつことも出来ます。

- [Firestoreのデータモデル](https://cloud.google.com/firestore/docs/data-model?hl=ja)
- [サポートされるデータ型](https://cloud.google.com/firestore/docs/concepts/data-types?hl=ja)
- [割り当てと制限値](https://cloud.google.com/firestore/quotas?hl=ja)

![無題のプレゼンテーション (6).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525959/b9778d3d-57fe-a916-04eb-470ff3d5842f.jpeg)


### 今回採用したデータ構造
ユーザごとに日単位でドキュメントを作成し、月単位のサブコレクションで束ねる構造としました。
連続したドキュメントIDは[非推奨](https://cloud.google.com/firestore/docs/best-practices?hl=ja#document_ids)ですが、更新頻度的に問題なしと判断しました。

![無題のプレゼンテーション (7).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525959/5cace0cd-c6f5-01a4-c8e1-38eef11681fb.jpeg)


### 見積
費用を見積もってみます。
私のように小規模な使い方であれば、費用の心配は無さそうです。

#### ストレージサイズ

[こちら](https://cloud.google.com/firestore/docs/storage-size?hl=ja)の考え方に則り、計算します。
無料分として、1GBが提供されていますので、その範囲に収まるかを見ていきます。


- 一人あたり10年分のサイズ

 
    |項目|形式|サイズ|一人あたり1年分|一人あたり10年分|
    |:-:|:--|:--|--:|--:|
    |コレクション|users|6by(=5by+1by)|6by|6by|
    |ドキュメント|11桁のユーザID|34by(=6by+12by+16by)|34by|34by|
    |コレクション|yyyymm形式の年月|7by(=6by+1by)|84by(=7by*12)|840by|
    |ドキュメント|yyyymmdd形式の年月日|82by(=6by+12by+7by+9by+16by+32by)|30,012by|300,120by|
    |フィールド(出てきた時間）|フィールド名(start)の日時型|15by(=6by+1by+8by)|5,490by|54,900by|
    |フィールド(帰った時間）|フィールド名(end)の日時型|13by(=4by+1by+8by)|4,758by|47,580by|
    |合計|||53,194by|531,580by|


- 人数が増えた場合

    |人数|サイズ(KB)|費用|
    |--:|--:|--:|
    |1人|519KB|$0|
    |10人|5,191KB|$0|
    |100人|51,912KB|$0|
    |1,000人|519,121KB|$0|
    |10,000人|5,191,211KB|$0.427|


#### ドキュメントの読み取り

1日あたり、読み取り5万回の無料枠がありますので、その範囲に収まるかを見ていきます。

- 一人あたりの回数
    - 読み取り ・・・ 31 (出退勤時間の表示。月末が最大値となる。)

- 人数が増えた場合

    |人数|読み取りドキュメント数|費用(日単位)|費用(月単位)|
    |--:|--:|--:|--:|
    |1人|31ドキュメント|$0|$0|
    |10人|310ドキュメント|$0|$0|
    |100人|3,100ドキュメント|$0|$0|
    |1,000人|31,000ドキュメント|$0|$0|
    |10,000人|310,000ドキュメント|$0.094|$2.902|




#### ドキュメントの書き込み

1日あたり、書き込み2万回の無料枠がありますので、その範囲に収まるかを見ていきます。

- 一人あたりの回数
    - 書き込み ・・・ 3 (出勤時刻の記録＋退勤時刻の記録＋打ち間違いの修正)

- 人数が増えた場合

    |人数|書き込みドキュメント数|費用(日単位)|費用(月単位)|
    |--:|--:|--:|--:|
    |1人|3ドキュメント|$0|$0|
    |10人|30ドキュメント|$0|$0|
    |100人|300ドキュメント|$0|$0|
    |1,000人|3,000ドキュメント|$0|$0|
    |10,000人|30,000ドキュメント|$0.011|$0.335|



## 処理時間の計測

読み取りと書き込みの時間を確認しておきます。
今回の条件では、いずれも、数百ms程度かかる結果となりましたので、
ひとつのトランザクション内でのFirestoreの処理は極力少なくした方が良さそうです。

### 環境

- コンピューティング ： Cloud Function 128MB
- リージョン : us-east4 *Firestoreと同じリージョン
- ランタイム : Python3.7
- ライブラリ : google-cloud-firestore1.8.1
  - 読み取り : collection.stream()
  - 書き込み : document.set()


### 読み取り
約200回分の処理を集計しました。
1回の処理で複数のドキュメントを取得し、月末に近づくに連れて取得するドキュメント数が
増えていく特性を持ちますが、特に影響を受けてはいなそうです。

|項目|値|
|:--|--:|
|50パーセンタイル|220ms|
|90パーセンタイル|462ms|
|99パーセンタイル|779ms|
|最大|1,196ms|

![無題のプレゼンテーション (4).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525959/ecefdf20-1421-c935-efd2-805998a1677a.jpeg)

### 書き込み
約400回分の処理を集計しました。
1回の処理で単一のドキュメントを更新します。

|項目|値|
|:--|--:|
|50パーセンタイル|365ms|
|90パーセンタイル|765ms|
|99パーセンタイル|1,228ms|
|最大|1,470ms|

![無題のプレゼンテーション (5).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525959/b04c5c44-58f2-2b8d-904b-1be8ae2f6542.jpeg)


## 運用

### 監視
なるべく[Always Free](https://cloud.google.com/free/docs/gcp-free-tier?hl=ja)の無料枠で安心して使えるようにするための通知ポリシーを考えてみます。

1. Firestoreにおける無料枠
  Cloud Functionsには、日ごとにリセットされる無料枠があります(時間、分は筆者にて計算)。
  今回はこれを基準にして、通知ポリシーを考えています。

    ||日単位|時間単位|分単位|
    |:-:|:-:|:-:|:-:|
    |ドキュメントの読み取り|50,000回|2,083回|35回|
    |ドキュメントの書き込み|20,000回|833回|14回|
    |ドキュメントの削除|20,000回|833回|14回|


1. 通知ポリシーの条件

    |Resorce Type|Metric|Aggregator|Aligner|Period|Configuration
    |:-:|:-:|:-:|:-:|:-:|:-:|
    |Firestore Instance|Document Reads|none|sum|1minute|Any time series violates/is above/35/1min|
    |Firestore Instance|Document Writes|none|sum|1minute|Any time series violates/is above/14/1min|
    |Firestore Instance|Document Deletes|none|sum|1minute|Any time series violates/is above/14/1min|



### バックアップ
GCPのドキュメントには、[手動バックアップ](https://cloud.google.com/firestore/docs/manage-data/export-import?hl=ja)の記載しかありませんでしたので、
[Firebaseのドキュメント](https://firebase.google.com/docs/firestore/solutions/schedule-export?hl=ja#gcp-console_1)を参照し、毎晩バックアップを自動で取得するよう実装しました。

#### アーキテクチャ
![無題のプレゼンテーション (8).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525959/1101cf58-dc3c-a0fe-71a2-1f6b5523b2c9.jpeg)

#### Cloud Schedulerの設定

|項目|設定値|備考|
|:--|:--|:--|
|名前|scheduledFirestoreExport||
|頻度|0 5 * * *|毎朝5時に実行|
|タイムゾーン|日本標準時(JST)|〃|
|ターゲット|Pub/Sub||
|トピック|initiateFirestoreExport|後述のトピックを指定|
|ペイロード|start export||

#### Cloud Pub/Subの設定

|項目|設定値|備考|
|:--|:--|:--|
|トピックID|initiateFirestoreExport||

#### Cloud Functionsの設定

|項目|設定値|備考|
|:--|:--|:--|
|名前| firestoreExport ||
|リージョン|us-east4|Firestoreと合わせる|
|トリガーのタイプ|Cloud Pub/Sub||
|トリガー|initiateFirestoreExport|前述のトピックを指定|
|割り当てられるメモリ|128MB||
|関数インスタンスの最大数|1||

#### Cloud Storageの設定

|項目|設定値|備考|
|:--|:--|:--|
|名前|fs-daily-export||
|ロケーションタイプ|Multi-region|可用性を向上|
|場所|us|Firestoreと同一リージョンか、マルチリージョンである必要がある|
|ライフサイクル|オブジェクトが更新されてから3日で削除|最新世代があれば十分だが余裕をもって|


## その他
### データの移行
Firestoreには、最初に決めたロケーションから変更できない制約がありますので、
取得したバックアップを別のプロジェクトに移行する手順を確認しておきます。

#### 環境
- 移行元のリージョン : us-east4
- 移行先のリージョン : asia-northeast1

#### 手順

```shell:一旦ローカルにコピー
gcloud config set project 移行元のプロジェクト
gsutil cp -r gs://バックアップデータがあるバケット/対象のフォルダ ./.
```

```shell:移行先のバケットにコピー
gcloud config set project 移行先のプロジェクト
gsutil cp -r ./対象のフォルダ gs://バックアップデータを置くバケット/
```

```shell:インポート
gcloud beta firestore import ./対象のフォルダ gs://バックアップデータを置くバケット/対象のフォルダ
```