Help us understand the problem. What is going on with this article?

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」だと言えそう
  • FastAPIresponderはsingle workerだと秒間100~1000程度のrequestであればpython界で圧倒的なperformanceを誇るjaprontoとほとんど同水準

(検証に使用したコードはこちら。結果のプロットが見られる.ipynbファイルもあります。ただし、plotlyを使用したのでGithubからは見れませんので、cloneする必要があります)

目次

  • 1. Python Web Frameworkの概要
  • 2. 負荷試験の条件
  • 3. 結果
  • 4. まとめ

1. Python Web Frameworkの概要

Django

恐らくPythonのweb frameworkといえば第一に名前があがるのがこのframeworkだと思います。
典型的な構成は、以下のようになります。
Untitled Diagram-Django.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

よって、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

わかり易さのために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

japronto

japrontoはほぼ自己完結しているweb serverです。
ほぼCで書かれており、異常に高パフォーマンスになっています。
officialのREADMEにはgolangやnode.jsを大きく上まっているというベンチマークが紹介されています。(wrkにより、1 thread, 100 connectionsで2400 requests/secの負荷を与えている)
results.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を参照すると、
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))

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のみ)

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

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

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

from fastapi import FastAPI
app = FastAPI()

@app.get("/query")
async def hello_query(id: str = None):
    return {"text": f"hello world, {id}!"}

japronto

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

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

connection 50, requests/sec 500, duration 30s

newplot (9).png

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

connection 100, requests/sec 1000, duration 30s

newplot (11).png

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

connection 500, requests/sec 5000, duration 30s

newplot (7).png

  • responderは終了
  • FastAPIはほぼ終了

connection 1000, requests/sec 10000, duration 30s

newplot (8).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だと言えそう
  • FastAPIresponderはsingle workerだと秒間$O(100)$のrequestであればpython界で圧倒的なperformanceを誇るjaprontoとほとんど同水準

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

備考

これまでresponderを使っていましたが、FastAPIに乗り換えたいと思います:v:
-> 入門しました

Refs

bee2
名古屋の会社でPythonで機械学習してます。 Julialangをかじってます。 Qiitaはエンジニアリングっぽいことを書き、はてなブログは機械学習関連のことを書いてます。
https://tksmml.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした