FastAPIの負荷実験環境を作ってみる

## 自己紹介
こんにちは，ZOZOテクノロジーズの内定者さっとです．
APIフレームワークの中ではFastAPIは結構好きで、個人で使うならほぼほぼこれを使っています。
FastAPIについては、以前に何個か記事を書います！
興味ある方は最後にリンクを貼っているので、そちらも見ていただければ思います！

※本記事は[ZOZOテクノロジーズ#4](https://qiita.com/advent-calendar/2019/zozo_tech4)の22日目です．

- [ZOZOテクノロジーズ#1](https://qiita.com/advent-calendar/2019/zozo_tech)
- [ZOZOテクノロジーズ#2](https://qiita.com/advent-calendar/2019/zozo_tech2)
- [ZOZOテクノロジーズ#3](https://qiita.com/advent-calendar/2019/zozo_tech3)
- [ZOZOテクノロジーズ#5](https://qiita.com/advent-calendar/2019/zozo_tech5)

## 概要(3行)
- Dockerを使ってFastAPIとMySQLの環境を構築
- 負荷実験ツールとしてLocustをローカルにインストール
- Locustを使ってコンテナへ向けて負荷実験

## FastAPIとは？
Python3.6以上で動作する高速なAPIフレームワークです。
https://fastapi.tiangolo.com/
2019年初頭頃、少し話題になったと思います。

特徴としては、**Node.js**や**Go**と同等のパフォーマンス性能でかつ**Flask**ライクな書き方で簡単にAPIを生やすことができます。ハッカソンなど時間が限られた開発で有効なフレームワークだと思います。

また、**Swagger**が自動で生成されるため、動作チェックやドキュメント作成の時間が大幅に短縮されます。

## Locustとは？
Pythonで書かれた、負荷テストツールです。
https://locust.io/

負荷実験のシナリオはPythonで記述し、簡単に実装することができます。また、実行結果はWebで確認することができ、攻撃対象へアクセスするユーザ数の設定や実験の停止などテストツールを制御することができます。

## 事前準備
- Docker及びDocker Composeがインストール済み
- Python3.6以上がインストール済み
- 以下のファイル構成を用意([GitHub](https://github.com/sattosan/stress_fastapi)に完成版があります)

```bash
.
├── api
│   ├── db.py
│   ├── main.py
│   └── model.py
├── docker
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── conf.d
│   │   │   └── my.cnf
│   │   └── initdb.d
│   │       └── schema.sql
│   └── uvicorn
│       ├── Dockerfile
│       └── requirements.txt
├── docker-compose.yml
└── locustfile.py
```

## APIサーバとMySQLの用意
docker-composeを使って必要なAPIサーバとDBを構築します。

```docker
version: '3'
services:
  # MySQL
  db:
    container_name: "db"
    # path配下のDockerfile読み込み
    build: ./docker/mysql
    # コンテナが落ちたら再起動する
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: sample_db
      MYSQL_USER: user
      MYSQL_PASSWORD: password # ユーザのパスワード
      MYSQL_ROOT_PASSWORD: password # ルートパスワード
    ports:
      - "3306:3306"
    volumes:
      - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d # 定義通りテーブルを作成
      - ./docker/mysql/conf.d:/etc/mysql/conf.d # MySQLの基本設定(文字化け対策)
    networks:
      - local-net

  # FastAPI
  api:
    # db起動後に立ち上げる
    links:
      - db
    container_name: "api"
    build: ./docker/uvicorn
    restart: always
    tty: true
    ports:
      - 8000:8000
    volumes:
      - ./api:/usr/src/api
    networks:
      - local-net

# コンテナ間で通信を行うためのネットワークブリッジ
networks:
  local-net:
    driver: bridge
```
詳しい内容については、以下の記事で書いているので参考にしてください。
[FastAPIをMySQLと接続してDockerで管理してみる](https://qiita.com/satto_sann/items/4fbc1a4e2b33fa2237d2)


## Locustのインストール
以下のコマンドで、ローカルにLocustをインストールします。

```bash
$ python -m pip install locustio
```

## 負荷を受けるAPIサーバのメインコード

- ユーザの情報を取得や登録する簡単なAPIを用意しました。
- ORMであるSQLAlchemyでMySQLを操作しています。
- 各エンドポイントの定義(def)の前に**async**とつけて非同期化しています。

```python:./api/main.py
# -*- coding: utf-8 -*-
from fastapi import FastAPI
from db import session  # DBと接続するためのセッション
from model import UserTable  # Userテーブルのモデル


# fastapiでエンドポイントを定義するために必要
app = FastAPI()

# ユーザ情報を返す GET
@app.get("/users/{user_id}")
async def read_user(user_id: int):
    # DBからuser_idを元にユーザを検索
    user = session.query(UserTable).\
        filter(UserTable.id == user_id).first()
    # ユーザが見つからなかった場合
    if (user is None):
        return {"code": 404, "message": "User Not Found"}
    return user

# ユーザ情報を登録 POST
@app.post("/users")
# クエリでnameとageを受け取る
async def create_user(name: str, age: int):
    user = UserTable()
    user.name = name
    user.age = age
    session.add(user)
    session.commit()
```

## 負荷実験シナリオ

- UserTaskSetクラスに実験シナリオを定義していきます。
- on_startメソッドに初期設定を定義します。
- taskに書かれた数字は重みで、```@task(2)```は```@task(1)```の2倍実行されることを表しています。
 
```python:./locustfile.py
from locust import HttpLocust, TaskSet, task, between
import random
import string


class UserTaskSet(TaskSet):
    def on_start(self):
        self.client.headers = {'Content-Type': 'application/json;'}

    @task(1)
    def fetch_user(self):
        self.client.get("/users/{}".format(random.randint(0, 100)))

    @task(2)
    def create_user(self):
        # ランダムに適当に名前を生成
        name = ''.join(random.choices(string.ascii_letters, k=10))
        age = random.randint(0, 80)
        self.client.post("/users?name={}&age={}".format(name, age))


class UserLocust(HttpLocust):
    task_set = UserTaskSet

    wait_time = between(0.100, 1.500)
```

## 実行

- コンテナを立ち上げます。

```bash
$ docker-compose up -d --build
```

- Locustを起動します。

APIサーバはlocalhost:8000で待ち受けているので、これに向けて負荷をかけるように指定してあげます。

```bash
$ locust -f locustfile.py --host=http://localhost:8000
```

## LocustをWebで監視
- Locustを起動すると以下のログが通知されます。

```bash
[2019-12-02 02:13:58,632] hoge/INFO/locust.main: Starting web monitor at *:8089
[2019-12-02 02:13:58,633] hoge/INFO/locust.main: Starting Locust 0.13.2
```

- ログでは、*:8089でリッスンしているみたいなので、http://localhost:8089 へアクセスします。

![Screenshot from 2019-12-02 02-17-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/b2b3909f-4ba1-9d91-a597-8f9463b41279.png)
初回起動時では、**Number of users to simulate**でどのぐらいのユーザ数でアクセスするかと
**Hatch rate**で毎秒何人ユーザを増やすかを設定できます。

- テストが実行されると、**Statistics**ページではログが流れます。

ログでは、リクエスト数やレスポンスタイム、失敗した数などが見れます。

![Screenshot from 2019-12-02 02-25-58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/28fb0d57-715d-7820-7bb7-15b2bd5c4451.png)

- **Charts**ページでは、一行ごとのリクエスト数やレスポンスタイム、現在のユーザ数がチャートで表示されます。

![Screenshot from 2019-12-02 02-29-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/30c7527f-17d6-2b31-7d78-f4d8a1a2add3.png)

- 最後に、上部の**STOP**で簡単に負荷を停止することができます。

## おわりに
今回は、Locustを使った負荷実験環境の構築を行いました。
今後は、FastAPIって本当に早いの？を検証するために、この環境を使い様々なAPIフレームワークと比較したいと思います。

今回使ったコードは、GitHubにあげているので、興味ある方はぜひ触ってみてくださいね！
https://github.com/sattosan/stress_fastapi

## その他FastAPIの記事
- [FastAPIをMySQLと接続してDockerで管理してみる](https://qiita.com/satto_sann/items/4fbc1a4e2b33fa2237d2)
- [FastAPIでCORSを回避](https://qiita.com/satto_sann/items/0e1f5dbbe62efc612a78)
- [FastAPIでPOSTされたJSONのレスポンスbodyを受け取る](https://qiita.com/satto_sann/items/8a458a8952f50b73f420)