⛄⛄ この記事は Classi developers Advent Calendar 2021 12/21の記事です ⛄⛄
こんにちは、いりすです。
最近、バーチャルの肉体を得てYoutubeで配信したりVTuberっぽいことをしてます。
腰と足をトラッキングする用にkat loco sというデバイスを買ったりして、VRの感動を覚えてます。

さて、今回は、以前作成したGoogle CloudのFirestoreのCRUD APIに負荷テストをするアプリケーションについて解説し、負荷テスト結果をみていきます(VR関係ない笑)
source:
モチベーション
なぜこのアプリケーションを作ったか、です
最近Cloud SQLだけでなく、Documentを保存する場所として Firestore を利用する機会が多く、
使っていく上でFirestoreの性能をどの程度発揮できるか気になることが多く、いっそのこと様々なテストをできるようにアプリケーション一式実装して試してみようと思ったからです。
Firestoreってなに?
Firestore は Google CloudのNoSQL Documentデータベースです。
基本的にはjsonのような形式でデータを保存するDocument, Documentの集まりであるCollectionという構造があり、collection/document の階層構造にデータを保存していきます。
(documentにもサブコレクションが作れ collection/document/collection/document/... といった階層構造が可能です)
https://cloud.google.com/firestore/docs/data-model
事前準備
Google Cloudで新しいプロジェクトを作成し、firestoreを有効します。
Google Cloudの検索からfirestoreを検索し、firestoreのコンソールへ飛びます。

最初firestoreを有効にしていない場合、以下の初期化画面が現れます。

今回はネイティブモードを選択します。
firestoreのモードを選択すると下記のロケーション選択画面が出てくるのでasia-northeast1を選択します。

以上で初期設定は終わりました。
アーキテクチャ
今回考えたアーキテクチャはFirestoreにCRUDを送るAPIをFlask+uWSGI+Nginxで作成し、負荷テストサービスとしてLocustを利用します。

実装
下記にどのソースがどの実装に対応しているか記載します。
.
├── README.md
├── api # uWSGI+Flaskのソースやconfig
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── src # Flaskのソース
│ │ ├── app.py
│ │ └── firestore_client.py
│ └── uwsgi.ini
├── docker-compose.yaml
├── locust # locustのソース
│ ├── Dockerfile
│ ├── requirements.txt
│ └── src
│ ├── __pycache__
│ │ └── locustfile.cpython-38.pyc
│ └── locustfile.py
└── nginx # nginxのコンフィグとdockerfile
├── Dockerfile
└── nginx.conf
Flask
FlaskでFirestoreに単純なCRUDをするAPIを作ります。
APIのエンドポイントとしては、URIにFirestoreのルートコレクションとドキュメントIDを指定してCRUDを行うようにします。
pythonのfirestore clientとして公式の google-cloud-firestoreを用います。
下記に一例としてGet methodを例として上げます。
@app.route("/fs/<string:collection_id>/<string:doc_id>", methods=['GET'])
def fs_get(collection_id, doc_id):
response = {'success': True}
doc_ref = fs.collection(collection_id).document(doc_id)
doc = doc_ref.get()
if doc.exists:
response = doc.to_dict()
else:
response = {}
return json.dumps(response)
fs.collection(collection_id).document(doc_id)で参照したいドキュメントの場所を指定し、doc_ref.get()で指定参照からドキュメントを取得します。
Firestore Client
firestore clientはlocalの時はデバッグ用のmock, そうでなければ実際のfirestoreにつなぐようにします。 mockには mock-firestore を利用します。 これで、ローカル実行を容易にします。
def get_fs_client(config: str):
if config == 'local':
fs = MockFirestore()
else:
fs = firestore.Client()
return fs
config = os.getenv('APP_CONFIG')
fs = get_fs_client(config)
Locust
locustで下記の負荷をかけるシナリオを書きます。
- 指定の
collection_id/doc_idに document を Createする - 作成したdocumentをReadする
- 作成したdocumentをUpdateする
- 作成したdocumentをDeleteする
class QuickstartUser(FastHttpUser):
wait_time = between(1, 2.5)
def initialize_task(self):
self.collection_id = uuid4()
self.doc_id = uuid4()
def get_fs(self):
uri = f'/fs/{self.collection_id}/{self.doc_id}'
self.client.get(uri, name='get_fs')
time.sleep(0.01)
def post_fs(self):
uri = f'/fs/{self.collection_id}/{self.doc_id}'
headers = {'content-type': 'application/json'}
payload = {'key': 'value'}
self.client.post(uri, json=payload, headers=headers, name='post_fs')
time.sleep(0.01)
def put_fs(self):
uri = f'/fs/{self.collection_id}/{self.doc_id}'
headers = {'content-type': 'application/json'}
payload = {'key': 'updated'}
self.client.put(uri, json=payload, headers=headers, name='put_fs')
time.sleep(0.01)
def delete_fs(self):
uri = f'/fs/{self.collection_id}/{self.doc_id}'
self.client.delete(uri, name='delete_fs')
time.sleep(0.01)
@task
def normal_scenario(self):
self.initialize_task()
self.post_fs()
self.get_fs()
self.put_fs()
self.delete_fs()
Userを継承することで負荷シナリオを実施するユーザーのクラスを定義できます。
そして、@taskでLocustで実行するタスクを定義できます。
今回はnormal_scenarioに負荷シナリオ実施前の初期化(collection_idとdoc_idの初期化)してからCRUDを順に実行するシナリオを書きました。
このように、LocustではAPIにリクエストを送るシナリオをpythonで記載することができ、負荷テストを楽に実施できます。
実行
今回は問題設定を簡単にするためlocalのdocker-composeを使います
- mockで実行
$ export DIR=${pwd}
$ docker-compose build
$ docker-compose up
- mock以外で実行
GOOGLE_APPLICATION_CREDENTIALSはgcloud auth loginした際に生成される認証情報を指定します。(この例だと~/.config/gcloud/application_default_credentials.json)PROJECT_IDはfirestoreのcrudしたいprojectを指定します(新しくプロジェクトを作成し、firestoreを有効にすることをお勧めします。)
$ export APP_CONFIG=develop
$ export PROJECT_ID=[Your_PROJECT_ID]
$ export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json
$ export DIR=${pwd}
$ docker-compose build
$ docker-compose up
locustにアクセスしてテスト実行するにはhttp://localhost:9081にアクセスし、下記のように設定し、runすれば可能です。

実行結果
1秒間に1user増えていき(spawn rate = 1)、100userが同時にAPIへリクエストする想定で、locustで負荷をかけます。
firestore mockの場合
まず、local上のfirestore mockでの実行結果です。


100user p99が1msという結果になりました
firestore-mockはlocal computer内のメモリ上にデータを入れており、外のネットワークにリクエストを送ってないので、この結果は妥当に思えます。
実firestoreにつないだ場合
続いてlocal上に立てたAPIで実際にfirestoreにつないだ場合の負荷です。
実際に、負荷をかける前にAPIが正しくfirestoreと接続されているか確かめるにはTotal users = 1, Spawn rate = 1で負荷をかけてfirestoreコンソールでデータが作成できていることを確認できれば良いです。
mockと同様に100userで同時にAPIへリクエストする想定で負荷をかけます。


結果としてCRUDのすべてのAPIにおいて100user p99がおよそ70msという結果になりました。
mockに比べるとネットワークを経由しているのでレイテンシは上がりますが、それでも70msと高いレイテンシとなってます。
ただ、今回は local API+firestoreという単純な構成なため、今回考えられなかった負荷テストで考慮する点を列挙します。
負荷テストする上で考慮する点
- 負荷をかけてレイテンシが高い、レイテンシが低い、負荷テストのOK/NGを判断する基準をきめる
- 無限にユーザーを増やせば無限にレイテンシを上がるので、どの程度の負荷に耐えうるかの基準やテスト設計が必要です。
- 現在のユーザー数, RPSをログから見て通常時/ピーク時にどの程度か、今後ユーザーが増えた場合どの程度まで許容できるレイテンシとなるか
- アプリケーションを構成するインフラストラクチャとネットワークを考慮する
- 今回動かした環境はlocalですが、実際にアプリケーションを動かすのはCompute EngineやCloud Run, App Engine, Kubernetes Engineなど様々な選択肢を取れます
- resourceのmachine spec, location
- スケーリングしている場合のworker数
- kubernetesを使っている場合サービス間通信のレイテンシ
- 負荷を与える基盤もlocalなのか、computing resourceなのか、様々な選択を取れます。
- Load Balancingを使う場合、ロードバランサーの設定は正しいか、負荷を分散できているかも考慮対象です
- 実現したい機能に対して適切なStorageを選択、Storageの設計、Storageの接続なども性能に影響されます
- 今回動かした環境はlocalですが、実際にアプリケーションを動かすのはCompute EngineやCloud Run, App Engine, Kubernetes Engineなど様々な選択肢を取れます
- 負荷をかけて問題個所を特定できるか
ざっとあげると以上のようになります。
大体の観点は Google Cloudの資格である Professional Cloud Architect において、アプリケーションを設計する上で考慮する項目となっております。
自分は資格取得を通して上記観点を考えるきっかけとなりましたが、具体的な学習プロセスは以下の記事が参考になると思います:
まとめ
- firestore のCRUDをするAPIを作りました
- Locustでmockの場合と実際にfirestoreにつなぐ場合に負荷をかけました
- 今回は単純化した負荷テストなので、実際の負荷テスト観点を列挙しました
今回作ったAPIとLocustのシナリオは単純にCRUDするだけでしたが、今後は様々なコレクション階層、シナリオパターン、インフラまで含めて拡充していきたいです。
明日のClassi developers Advent Calendar 2021 はnakaearthさんです!
ばいばいー

冬服ver, kawaii