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


自己紹介

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

※本記事はZOZOテクノロジーズ#4の22日目です.

概要(3行)

  • Dockerを使ってFastAPIとMySQLの環境を構築
  • 負荷実験ツールとしてLocustをローカルにインストール
  • Locustを使ってコンテナへ向けて負荷実験

FastAPIとは?

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

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

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

Locustとは?

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

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

事前準備

  • Docker及びDocker Composeがインストール済み
  • Python3.6以上がインストール済み
  • 以下のファイル構成を用意(GitHubに完成版があります)
.
├── 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を構築します。

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で管理してみる

Locustのインストール

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

$ python -m pip install locustio

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

  • ユーザの情報を取得や登録する簡単なAPIを用意しました。
  • ORMであるSQLAlchemyでMySQLを操作しています。
  • 各エンドポイントの定義(def)の前にasyncとつけて非同期化しています。
./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倍実行されることを表しています。
./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)

実行

  • コンテナを立ち上げます。
$ docker-compose up -d --build
  • Locustを起動します。

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

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

LocustをWebで監視

  • Locustを起動すると以下のログが通知されます。
[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 へアクセスします。


初回起動時では、Number of users to simulateでどのぐらいのユーザ数でアクセスするかと
Hatch rateで毎秒何人ユーザを増やすかを設定できます。

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

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

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

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

おわりに

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

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

その他FastAPIの記事