前回の記事で、 Spacy のモデルがメモリリークすることを調べました。
fastAPI で Spacy を動かしたとき、メモリがどれぐらい増加するのか確認します。
コードは Github にあげています。設定を諸々変えたので現状プルリクのままマージしていません。
設定
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時間実行します。
試験の結果
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のページをまるごと使っているので、短い文だともう少し捌けるかもしれません。
参考: 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 やその他の設定できそうなので、詳しく調べようと思います。
参考資料
- fastapi
- docker
- locust