エイエイレトリック

なぐりがき

# Spacy + fastAPI に locust で負荷試験を実行する

前回の記事で、 Spacy のモデルがメモリリークすることを調べました。

fastAPI で Spacy を動かしたとき、メモリがどれぐらい増加するのか確認します。

コードは Github にあげています。設定を諸々変えたので現状プルリクのままマージしていません。

github.com

設定

fastAPI + Docker

fastAPI のコードは cookiecutter-spacy-fastapi をベースにしています。 Spacy の結果から固有表現 (Named entity) を返却するAPIです。

Spacyのモデルには日本語のなかで一番軽量な "ja_core_news_sm" を指定しています。

クラウド上にデプロイして動かすことを想定し、利用可能なメモリに制限をかけます。 今回はDocker コンテナの設定を利用します。

AWS で t3.small 相当の1G (mem_limit:1g) を設定しました。 mem_limit は version 3 で対応していないのでバージョンを下げています。

// docker-compose.yml
version: "2"
services:
  app:
    container_name: spacy_fastapi
    build: .
    volumes:
      - ./:/usr/src/
    ports:
      - "8080:8080"
    command: uvicorn main:app --reload --host 0.0.0.0 --port 8080
    mem_limit: 1g

コンテナごとのメモリの使用量は docker stats でわかるので、ログとして残しておきます。

% docker stats spacy_fastapi  --no-stream
CONTAINER ID   NAME            CPU %     MEM USAGE / LIMIT   MEM %     NET I/O     BLOCK I/O     PIDS
6be01b84084b   spacy_fastapi   0.49%     170MiB / 1GiB       16.60%    876B / 0B   8.16MB / 0B   12

ファイルの出力は こちらの記事 を参考にしました。 扱いやすいようにjson で出力します。

 while true; do docker stats --no-stream --format "{{ json . }}" |tee -a stats.txt; sleep 10; done

locust

負荷試験にはPython製の locust を使います。 固有表現一覧を返すエンドポイント /entities に対してリクエストするコードを作成しました。

locustのドキュメントにある負荷試験と異なり、API に POST で渡す文書データが必要です。

前回の記事で Spacy は Vocab をキャッシュすることがわかっており、同じデータを繰り返し渡すだけでは正しく検証できません。

大量のユニークデータで検証するために、Wikipediaの記事を使います。

今回は Wikipedia日英京都関連文書対訳コーパス を使います。 14,111ファイル と数時間のテストには十分な量です。 本来の用途とは異なりますが、コーパスの日本語部分のみ利用させてもらいました。

(Wikidump でも問題ないのですが、ストレージを圧迫しそうだったので……)

負荷試験

  • spacy_fastapi を動かす docker-compose up
  • メモリの使用量をログ出力する docker stats --no-stream --format "{{ json . }}"
  • locust を起動する poetry run locust -f locustfile.py

http://localhost:8089 で UI を起動し、実行します。

ユーザー数を5とし、1時間実行します。

locustのUI

試験の結果

1時間の結果は以下の通りです。1.71%のリクエストに失敗していることがわかります。

Type     Name         # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|-----------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST     /entities      7437   127(1.71%) |    411      18   10101    120 |    2.07        0.04

エラーとしては ReadTimeout (タイムアウト) と RemoteDisconnected の2つです。

5    POST    /entities   RemoteDisconnected('Remote end closed connection without response')
122 POST    /entities   ReadTimeout(ReadTimeoutError("HTTPConnectionPool(host='localhost', port=8080): Read timed out. (read timeout=10)"))

失敗しているタイミングをチャートで確認してみると、後半ずっと失敗していることがわかります。

また、 RPS は成功している時間帯で 2 程度になりました。 Wikipediaのページをまるごと使っているので、短い文だともう少し捌けるかもしれません。

locust report

参考: Docker のエラーログ

docker logs では FastAPI のログを遡れるのですが、 500 Internal Server Error 以外の表示がないため、どの部分でのエラーかわかりませんでした。

コード部分で例外時のログを設定する必要がありそうです。

INFO:     192.168.65.1:63904 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:63914 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:63915 - "POST /entities HTTP/1.1" 500 Internal Server Error
INFO:     192.168.65.1:63904 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:64213 - "POST /entities HTTP/1.1" 200 OK

メモリ使用率

docker stats から使用率だけ抽出します。

出力が 171MiB / 1GiB と分母の数値を含むので、簡単にフォーマットを調整しました。

import json

mem_usage_list = []
with open("stats.txt") as f:
    for line in f:
        data = json.loads(line.strip())
        if data["Name"] == "spacy_fastapi":
            mem_usage = data["MemUsage"].split("/")[0].strip()[:-3]
            mem_usage_list.append(mem_usage)

with open("stats_memory.txt", "w")as f:
    f.writelines("\n".join(mem_usage_list))

スプレッドシートでグラフにしたものが以下の通りです。 時間が経つごとにメモリ使用量が増えていき、 メモリ制限の 1GiB 相当の 1000MiB のまま動きません。

メモリ使用量

本番運用する場合はメモリを解放するため Spacy モデルか API自体を定期的にリロードしたいです。

リロードに関しては Gunicorn やその他の設定できそうなので、詳しく調べようと思います。

参考資料

前回 eieito.hatenablog.com