猫でもわかるWebプログラミング

試行錯誤しながらエンジニア(プログラマー)として働く猫のブログ。技術的な話や、働き方の話、読書録とか、試行錯誤している日常の話。

Django の runserver より gunicorn のほうが早くなるのか ApacheBench で検証

Django の runserver は遅い

皆さん開発時に Django の runserver というコマンドを使っていると思いますが、この runserver は本番環境では使えません。

DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests.

runserver は、パフォーマンスのテストや、セキュリティの検証をしていないからです。とあります。

https://docs.djangoproject.com/ja/3.1/ref/django-admin/#django-admin-runserver

検証用のアプリケーションを用意する

検証用のアプリケーションを用意しました

github.com

このアプリケーションの / に対して、 ApacheBench で大量にリクエストを送り、パフォーマンスを検証します。

主に1秒間にどれだけのリクエストをさばけたかを見ることにします。

アプリケーションの中身は、一定時間 sleep した後に、Hello World を返すだけです。

from django.contrib import admin
from django.urls import path

from django.http import HttpResponse
from time import sleep

def index(request):
    sleep(0.2)
    return HttpResponse("Hello, world!")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index),
]

git clone して docker-compose up -d するだけで、アプリケーションが立ち上がります。

$ git clone git@github.com:yoshikyoto/django-bench.git
$ cd django-bench
$ docker-compose up -d

http://localhost:8000 でアプリケーションが起動しています。

runserver に対して ApacheBench を実行

まずは、 runserver に対して ApacheBench を実行して、どれくらいのパフォーマンスが出るかを測定します。

ApacheBench は、アプリケーションに対して大量のリクエストを送り、パフォーマンスを測定するツールで、Mac であればデフォルトでインストールされています。

runserver でアプリケーションが動いていることを確認します

$ docker-compose ps
 Name               Command               State           Ports
------------------------------------------------------------------------
django   bash -c ./manage.py runser ...   Up      0.0.0.0:8000->8000/tcp

今回は ApacheBench のコマンド ab で、1000 リクエストを 100 並列で送ってみます。

$ ab -n 1000 -c 100 http://127.0.0.1:8000/

3回実行した結果、秒間リクエスト数は、 57.26, 35.31, 18.46 となった。

実行結果の詳細を見たい人はこちらを確認してください

scrapbox.io

gunicorn に対して ApacheBench を実行

今度は、 gunicorn に変更して試してみます。

まずは Docker を終了させます。

$ docker-compose down

docker-compose.yml で、 runserver の command をコメントアウトし、 gunicorn の command をコメントインします。

gunicorn のワーカーの数なんですが、最初1にして ApacheBench でリクエストを送ってみたところ、CPU使用率が2%くらいしかなかったので、ワーカー数 50 になるようにコマンドを修正します。

gunicorn -w 50 -b 0.0.0.0:8000 testproject.wsgi:application

その後、 Docker を起動します。

$ docker-compose up -d

gunicorn が起動していることを確認します。

$ docker-compose ps
 Name               Command               State           Ports
------------------------------------------------------------------------
django   bash -c gunicorn -w 50 -b  ...   Up      0.0.0.0:8000->8000/tcp

ApacheBench を3回実行すると、秒間リクエスト数は、 125.97, 137.03, 111.55 となった。

runserver の時よりも倍以上のパフォーマンスになっている。

今回は sleep するだけで、CPU 負荷も、メモリも使わないので、worker を増やせば増やしただけパフォーマンスが向上する可能性が高い。

実際、 sleep するだけのプログラムは無いが、例えば、 DB にクエリを投げて結果を待っている時は、 sleep しているのと揺動、アプリケーションサーバーは CPU を使っていない。つまり、 woker の数を増やせばパフォーマンスが向上することになる。

nginx を使うことについて

実際にはここに nginx を挟んで

nginx -> gunicorn -> Django

といった構成になることが多い。

この場合、 nginx があるため、 gunicorn に直接リクエストを飛ばすよりも、パフォーマンスは劣化する。

ただし、nginx には、

  • アクセスログを書く機能
  • 特定の ip をブロックする機能
  • 秒間リクエスト数を制限する機能
  • レスポンスを gzip で圧縮する機能

など、 Web アプリケーションによって必要な機能が揃っているため、導入するメリットが大きい。

今回はやらないが、同様に nginx を挟んだ場合のパフォーマンスも計測してみると良い。

まとめ

ApacheBench を使って調べた結果、 Django の runserver コマンドを使うより、 gunicorn を使って並列数を上げたほうが、処理できるリクエスト数は多くなった。

f:id:yoshiki_utakata:20210215232758p:plain