PythonのWeb frameworkのパフォーマンス比較 (Django, Flask, responder, FastAPI, japronto)

以下のPython Web frameworkを使って単純なAPIを立てて、負荷試験をしてみました。

- Django (2.X)
- Flask
- FastAPI
- responder
- japronto

結果的に、ざっくりと以下が分かりました！

- performanceは「japronto >>> FastAPI > responder >>> Flask ~ Django」だと言えそう
- **FastAPI**と**responder**はsingle workerだと**秒間100~1000程度のrequest**であればpython界で圧倒的なperformanceを誇る**japrontoとほとんど同水準**

（検証に使用したコードは[こちら](https://github.com/tokusumi/python-web-framework-benchmark-test)。結果のプロットが見られる```.ipynb```ファイルもあります。ただし、plotlyを使用したのでGithubからは見れませんので、cloneする必要があります）
# 目次
- 1. Python Web Frameworkの概要
- 2. 負荷試験の条件
- 3. 結果
- 4. まとめ

# 1. Python Web Frameworkの概要
## Django
恐らくPythonのweb frameworkといえば第一に名前があがるのがこのframeworkだと思います。
典型的な構成は、以下のようになります。
![Untitled Diagram-Django.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/ebde778f-38fa-db71-3c90-be252cd35f9a.png)

Djangoは色々入っている大きなframeworkですが、WSGI serverだけは別なので、比較の際はWSGI serverをそろえてあげる必要があります。
また、最近Django 3がでて、ASGIに対応したらしいです。（でたばかりで動作が怪しいみたいなので今回は見送ります）
## Flask
Pythonのmicroframeworkで最も知名度が高いと思われます。
以下のようなWerkzeugというlibraryのラッパーとしてはじまっています。

> Werkzeug is a comprehensive WSGI web application library. It began as a simple collection of various utilities for WSGI applications and has become one of the most advanced WSGI utility libraries.
https://www.palletsprojects.com/p/werkzeug/

典型的な構成は、以下のようになります。（RDSとの接続はSQLAlchemyというORMがよく使われるので書いていますが、他のORMも使えます）
![Untitled Diagram-flask (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/b9623e6f-33ab-e3e5-9f40-fdd57d2ddc47.png)

よって、Flaskと比較する場合は、WSGI serverを共通化する必要があることが分かります。また、DBが絡んだ比較をする場合は、基本的にはSQLAlchemyに揃える必要がありそうです。

## responder
割と新しいPythonのmicroframeworkです。(2020年1月現在)
web applicationとしてはStarletteというlibraryのラッパーです。
responderは (正確にはStarletteは) ASGIになっていて全体的に非同期で動かせて、一般的にはDjango, FlaskなどのWSGIよりは高performanceだと言われています。

典型的な構成は、以下のようになります。（RDSとの接続は(確認した限り) Documentsで言及されていません。ググるとtortoise ORMというものがよく紹介されていたので書きました。しかし、Issuesを見るとSQLAlchemyも動くみたいです）
![Untitled Diagram-responder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/f8b45f53-67d7-b042-2a4e-8351b64c79f6.png)


わかり易さのためにASGI applicationとASGI serverを分けて書いていますが、responderはuvicornもラップしているので、responderだけで完結しているかのように扱えます。

## FastAPI
responderよりも数ヶ月後にでた新しいPythonのmicroframeworkです。(2020年1月現在)
responderと同様にStarletteのラッパーです。ですが、RDSのサポートが充実していたり、自動でSwaggerを作ってくれたりなど、よりproduction向きだと思います。(何よりもdocumentが手厚いです)

典型的な構成は、以下のようになります。（RDSとの接続はSQLAlchemyというORMがdocumentで紹介されています。しかし、他も使えるようです）
また、uvicornをgunicorn経由で使ってmultiworkerで動かすことも推奨されています。
![Untitled Diagram-fastAPI (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/bd9f659d-1435-e79a-f953-b860315efe96.png)

## japronto
japrontoはほぼ自己完結しているweb serverです。
ほぼCで書かれており、異常に高パフォーマンスになっています。
officialのREADMEにはgolangやnode.jsを大きく上まっているというベンチマークが紹介されています。(wrkにより、1 thread, 100 connectionsで2400 requests/secの負荷を与えている)
![results.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/56373cf8-937e-fd19-c228-aa994c01edaa.png)
https://github.com/squeaky-pl/japronto#performance

ただし、productionでの利用は推奨されていません。開発も止まっているので、あくまで参考(最強の象徴)ということで紹介します。

---

今回はDBは扱わないこととします。
そこで、framework毎に、それぞれ使用する WSGI/ASGI serverは以下のようにまとめられます。

| | WSGI/ASGI | application |server | 
| --- | --- | --- | --- |
| Django | WSGI | (pure) | gunicorn |
| Flask | WSGI | Werkzeug | gunicorn |
| responder | ASGI | starlette | uvicorn |
| FastAPI | ASGI | starlette | uvicorn |
| japronto | ASGI | (pure) | (pure) |

この時点でパフォーマンスは、(左から順に高パフォーマンス)
japronto > FastAPI ~ responder > Flask ~ Django
であることが予想できます。実際にどの程度の差があるのか調べたいと思います。
## 補足
[Web Framework Benchmarks](https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=plaintext&l=zijmrj-f&f=1ekg-0-cn8-1kw-5jrvsw-hrbf28-1k6pu-18yle-4zua0-2)を参照すると、
japronto > FastAPI > Flask ~ Django > responder
になっていますが、細かい部分がよくわからないので、自分でもやってみます。

# 2. 負荷試験の条件
- 負荷試験ツール: wrk2
- 環境: 同一マシン上でweb serverと負荷試験ツールを同時に動かします (正確性を欠くので避けるべきですが、あくまでもframeworkの比較を行うのでそれぞれの条件が対等であれば十分だという考えでこの環境を採用します)
- 指標: latency

## wrk2
今回は各framework毎に簡単な処理に対するlatencyを出力させて、可視化ツールでまとめて可視化することにします。なので、シンプルな負荷試験ツールが適していそうです。
そこで、CUI上で簡単に使えるwrk2を使用します。

### install
macでは、homebrewを使えば簡単にinstallできます。

```
$ brew tap jabley/homebrew-wrk2
$ brew install --HEAD wrk2
```

## 使用環境
- Macbook pro: 
    - CPU: 2.9 GHz Intel Core i5
    - memory: 8GM

## latency
ここでは、applicationのperformanceの定義をlatencyの低さとします。
> レイテンシ(latency): あるシステムの遅延時間。ネットワークや機能単位の反応の遅さを評する際に用いられることが多い。小さいほど良い。

(参考:[負荷試験のためのノウハウと Webフレームワークの負荷試験 (Python,Node,Go,PHP)](https://qiita.com/tkngue/items/62101788c0f384a5b12e))

## WSGI/ASGI server
gunicornの設定は、uvicornにあわせてworker数1にします。


```
$ gunicorn \
  --workers 1 \
  --bind 0.0.0.0:8000 \
  hello.wsgi
```
uvicornはdefaultの設定を使用します。

```
$ uvicorn main:app --no-access-log
```

## Applications
get methodでたたき、query parameterを受け取って、
```f"hello world, {query}"```
をjsonで返すAPIをたてます。
各frameworkのコードをまとめて書きます。(Django以外は似たりよったりです)
### Django
(viewのみ)

```python
from django.http.response import JsonResponse
from django.views import View

class HelloQueryView(View):
    def get(self, request, *args, **kwargs):
        ids = request.GET.get('id', '')
        return JsonResponse({"text": f"Hello python, {ids}!"})
```
### Flask
```python
from flask import Flask, jsonify, request
app = Flask(__name__)

@app.route('/query')
def hello_query():
    ids = request.args.get('id')
    return jsonify({"text": f"hello world, {ids}!"}), 200

if __name__ == '__main__':
    app.run(debug=False)
```
### responder
```python
import responder
api = responder.API()

@api.route("/query")
async def hello_world_query(req, resp, *args, **kwargs):
    ids = req.params.get("id")
    resp.media = {"text": f"hello world, {ids}!"}

if __name__ == "__main__":
    api.run(debug=False, access_log=False)
```
### FastAPI
```python
from fastapi import FastAPI
app = FastAPI()

@app.get("/query")
async def hello_query(id: str = None):
    return {"text": f"hello world, {id}!"}
```
### japronto
```python
from japronto import Application
app = Application()

def hello_query(request):
    ids = request.query.get("id")
    return request.Response(json={"text": f"Hello python, {ids}!"})

app.router.add_route("/query", hello_query)

if __name__ == "__main__":
    app.run()
```
# 3. 結果
以下5条件で負荷試験を行う。
1. connection 10, requests/sec 100, duration 30s
2. connection 50, requests/sec 500, duration 30s
3. connection 100, requests/sec 1000, duration 30s
4. connection 500, requests/sec 5000, duration 30s
5. connection 1000, requests/sec 10000, duration 30s

縦軸をlatency[ms], 横軸をpercentileとして結果をプロットします。

## connection 10, requests/sec 100, duration 30s
![newplot (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/1eac0e54-9ac4-06db-a09d-c73469b5b8ce.png)

- DjangoとFlaskはもうきつそう
- ASGI系はどれも似たような感じ 
- japrontoの一人勝ちではない

## connection 50, requests/sec 500, duration 30s
![newplot (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/92a5fea8-9ac4-4c1e-36dd-b32caf0fa382.png)

- DjangoとFlaskは終了
- ASGI系はどれも似たような感じだが、responderが一歩出遅れている
- japrontoの一人勝ちではない

## connection 100, requests/sec 1000, duration 30s
![newplot (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/04e3d05c-d98a-d2c5-47e0-66f9161f2209.png)

- japronto > FastAPI > responderの順位がはっきりしてきた
- FastAPIとresponderはギリギリ耐えている

## connection ５00, requests/sec ５000, duration 30s
![newplot (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/42becadd-be15-9dcc-0364-a5b315cf7ec2.png)

- responderは終了
- FastAPIはほぼ終了

## connection 1000, requests/sec 10000, duration 30s
![newplot (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246552/544832e3-54af-9e57-4aa8-db48c885e6f7.png)

- responder, FastAPIは完全に終了
- japrontoは99.9パーセンタイル程度までは余裕

## アクセス規模別の適用可能性
1秒間のリクエスト数の大きさ別に各Web frameworkが**single workerで**現実的(timeoutしない程度)なlatencyを保てるかを大まかに表にまとめると、以下のようになりそうです。（ただし、$O(100)$は100から1000ぐらいの値を指す）☓である領域に対処するにはマシンスペックを上げて対処するしかなさそうです。

| | 秒間$O(100)$のrequest | 秒間$O(1000)$のrequest |
| --- | --- | --- |
| Django | ☓ | ☓ |
| Flask | ☓ | ☓ |
| responder | ○ | ☓ |
| FastAPI | ○ | △ |
| japronto | ○ | ○ |

# 4. まとめ
wrk2でPythonのweb frameworkのsingle workerでのlatencyを比較したところ、

- japronto >>> FastAPI > responder >>> Flask ~ Djangoだと言えそう
- **FastAPI**と**responder**はsingle workerだと**秒間$O(100)$のrequest**であればpython界で圧倒的なperformanceを誇る**japrontoとほとんど同水準**

だということがわかりました！
とはいえ、インフラに強く依存するはずなのであくまで参考程度ということで！

# 備考
これまでresponderを使っていましたが、FastAPIに乗り換えたいと思います:v:
-> [入門しました](https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9)

# Refs
- [負荷試験のためのノウハウと Webフレームワークの負荷試験 (Python,Node,Go,PHP)](https://qiita.com/tkngue/items/62101788c0f384a5b12e)
- [Python API Framework Benchmarks](https://fgimian.github.io/blog/2018/06/05/python-api-framework-benchmarks/)