gunicorn

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 40 posts
  1. Zenn · · @kamuidash

    CPU使用量は低いのに応答が遅い。Gunicornの送信待ちを調べる この記事の主な対象は、Gunicornの同期ワーカーを2個使い、それぞれが1件ずつ応答を書き込む構成です。 フレームワークを使わずに書いた小さなPythonアプリで、受信の遅い要求があるとき、別の要求への応答がどう変わるかを測りました。 同期ワーカーは、担当する応答の書き込みが終わるまで次の要求を扱わないプログラムです。「ワーカー」は、このような要求の処理を担当するプログラムを指します。受信側がゆっくり読み、2個とも書き込みを終えられないと、別の要求への影響はどうなるのでしょうか。 この条件では、ゆっくり進むダウンロードを2件同時に動かすと、別のアクセスへの応答時間が、約1.6msから約5.9秒に伸びました。ここで比べたのは、応答時間を短い順に並べたときに95%が収まる値、p95です。以下の代表値は、同じ条件を3回測ったp95の中央値です。 ダウンロードが1件だけなら、別のアクセスへのp95は約1.7msでした。また、間にNGINXというWebサーバーを置き、応答を先に受け取ってためる設定にすると、同じ同期ワーカー2個でも約2.3msでした。ダウンロード自体の完了時間は、受信プログラムが設定した約8秒です。 ただし、これはWebアプリ全般の性質や、フレームワークの性能順位を示す結果ではありません。 同じアプリを4スレッドで動かした補助条件では、NGINXがなくてもp95は約1.5msでした。非同期サーバーの測定は行っていません。 先に、今回測った条件を確認する 項目 今回の前提 アプリ フレームワークなしのPythonアプリ。WSGIというアプリとサーバーの呼び出し方式を使用。Flask・Django・FastAPI等は使わない 主な実行構成 Gunicorn 23.0.0のsyncワーカー2プロセス。各プロセスが1件ずつ応答する 主に比較するもの 同じアプリ・ワーカー構成で、直接アクセス/NGINXの応答バッファリングoff/onを比較 大きな応答 同じデータを繰り返す16MiB。受信プログラムが約2MiB/sで読み、約8秒で完了するように制御。受信バッファの設定値は8KiB(Linuxの返値16KiB) 別の要求 12バイトのJSONを返す窓口へ毎秒8件、9秒間要求。毎回新しいTCP接続を使う 通信 同じLinuxホスト内のHTTP/1.1。TLS・圧縮・キャッシュ・CDNなし。実インターネット回線やPaaSの公開経路を測らない 補助条件 同じアプリをGunicorn gthreadの1プロセス・4スレッドで動かす。方式と同時対応数の両方が変わる比較 応答時間は、別の要求のTCP接続を開始してから、12バイトの本文を読み終えるまでです。1条件あたり72件を3回測りました。CPU・メモリにも実験用の上限を設けており、値は末尾の設定欄に記載しています。 この実験から、別のフレームワークや非同期サーバーでも同じ件数で遅くなる、あるいはNGINXが必ず必要だとは判断できません。 以下では、この表の主構成で起きた違いと、その説明に使える仕組みを見ます。 ダウンロードと、すぐ返せる応答を同時に動かす 実験では、同じアプリに2種類のアクセス先を用意しました。 アクセス先 返すデータ 受け取る方法 /large 16MiBの大きなデータ 1秒あたり2MiBずつ読み、約8秒かけてダウンロードする /fast 12バイトの小さなJSONデータ 待たずに読み取り、応答が返るまでの時間を測る /fastは、プログラムから呼び出して結果を受け取る窓口、APIです。本文では、このアクセスを「別のアクセス」と呼びます。データベースへの問い合わせや重い計算を入れず、すぐに返せる内容にしました。MiBはデータ量の単位で、1MiBは1,048,576バイトです。 サーバーへ要求を送り、その応答を受け取る側のプログラムがクライアントです。ブラウザもその一種です。今回はブラウザの代わりに実験用プログラムを使い、ダウンロードする人と、別の要求を送る人の動きを再現しました。 主構成のGunicorn syncワーカーは1件ずつ要求を扱います。使用した版の公式設計資料にも、この動作が記載されています。同じフレームワークを使っていても、こうした実行方式やワーカー数の設定で、同時に進められる要求の数は変わります。今回はフレームワーク固有の機能を含めず、サーバー側の動きを観察するために、短い実験用アプリを使いました。 ダウンロードを0件・1件・2件と変えながら、/fastへの要求を毎秒8件、9秒間送りました。前の応答が遅くても、次の要求は125ms後に送ります。毎回72件の応答時間を測り、同じ条件を3回ずつ試しました。 さらに、NGINXで応答を中継する場合も測りました。このようにアプリと受信側の間で要求・応答を中継する役割を、リバースプロキシと呼びます。 NGINXには、受信側へ送るよりも先にアプリの応答を読み進め、メモリや一時ファイルにためる設定があります。これがバッファリングです。以降の表や図では、この設定を「先読みあり/なし」と表記します。先読みありでも、応答全体がたまるまで利用者への送信を待つわけではありません。 主構成(Gunicorn syncワーカー2個)の概念図です。各列上部の箱はアプリ側のワーカーをまとめて表しています。OSの通信バッファは、Linuxなどが送受信データを一時的に置くメモリです。箱の大きさは容量を表しません。 ダウンロードが2件になると、応答の担当が両方ふさがった この節の表とグラフは、すべてGunicornのsyncワーカー2個を固定した比較です。 別のアクセスのp95、つまり応答時間の95%が収まる値を、3回の測定の中央値で示します。1msは1,000分の1秒です。 アクセスの経路 ダウンロードなし 同時1件 同時2件 アプリへ直接アクセス 1.57 1.72 5,945.11 NGINX・先読みなし 2.66 2.33 1,071.47 NGINX・先読みあり 1.93 2.94 2.34 単位はmsです。例えば、5,945msは約5.9秒です。 実測値。主構成はGunicorn syncワーカー2個、同一ホスト内のHTTP/1.1です。点は3回の中央値、縦線は最小〜最大。縦軸は1→10→100→1,000msと10倍ずつ増える目盛り(対数軸)です。 アプリへ直接アクセスした場合、ダウンロードが1件なら、もう1つのワーカーが/fastの要求を処理できました。2件になると、両ワーカーが大きなデータを送っているため、/fastの要求は順番を待つようになります。 仕組みは、同期ワーカーの実装から説明できます。受け取る側が少しずつしか読まず、送受信用メモリの空きがなくなると、ソケットへの書き込みは空きができるまで待ちます。同期ワーカーは、そこで担当中の応答を書き終えられなければ、次の要求に進めません。 Gunicornの同期ワーカーの実装でも、応答データを少しずつ順に書き込んでいます。書き込み処理が呼ぶソケットは、プログラムがネットワークでデータをやり取りするための窓口です。この書き込みが終わらなければ、ワーカーは次の要求へ進めません。 今回観測したのは、アプリの書き込み処理にかかった時間と、別の要求への応答時間です。個々のTCPバッファの使用量や、ワーカーの停止位置をスタック解析で調べたわけではありません。結果は上記の動作と整合しますが、どのバッファに何バイトたまっていたかまでは確定していません。 このとき、アプリのCPU使用量は、各測定の平均で最大でも1コア分の約0.8%でした。計算する余力があっても、応答を担当する2個のワーカーが送信待ちでふさがることはあります。 実測値。syncワーカー2個、同時ダウンロード2件。1点が/fastへの1要求です。3回のうちp95が中央の測定を表示しました。横軸は要求を送った時刻、縦軸はその応答にかかった時間です。 NGINXの先読みを切っても、OSはデータをためる NGINXを挟み、先読みを無効にした場合、ダウンロード2件のときのp95は約1.1秒でした。アプリへの直接アクセスより短いものの、ダウンロードがないときの約2〜3msには戻りませんでした。 応答サイズを決める前に、8MiBのデータを1MiB/sで受け取る予備測定も行いました。そのときは、NGINXの先読みを切っていても、別のアクセスのp95は約2msでした。そこで応答を16MiB、受け取り速度を2MiB/sへ変え、ダウンロード時間を約8秒に保ったまま、送る量を増やして本測定を行いました。予備測定は表の値に混ぜていません。 予備測定と本測定では、応答サイズと読み取り速度の両方が変わっています。 この違いを、応答サイズだけの効果として切り分けた実験ではありません。以降の結論は16MiB・2MiB/sの本測定の条件に限って読みます。 先読みを無効にする設定はproxy_buffering offです。この設定を切っても、アプリとNGINXの間、NGINXと受信プログラムの間には、OSの送受信用メモリが残ります。NGINX自身も、アプリからデータを受け取るための小さなメモリを使います。NGINXの仕様では、offでも一度に受け取れる最大量をproxy_buffer_sizeで設定すると説明されています。 こうしたメモリに応答データをすべて渡し終えられれば、受信側が読み終わる前にアプリが次の要求へ進める場合があります。各ソケットの使用量は測っていないため、8MiBと16MiBの間に共通の境界があるとはいえません。先読みoffでも保持場所が残ることは仕様に基づく説明であり、その容量や今回の差への寄与を個別に実測したものではありません。 アプリが書き終えても、ダウンロードは終わらない 次に、ダウンロード2件の条件で、2つの時間を分けて測りました。 アプリの書き込みが終わるまで:16MiBの応答を少しずつ返し始めてから、最後のデータをOSの送信用メモリへ渡すまで。 受信側の完了時間:ダウンロードの要求を送ってから、16MiBを受信し、設定した読み取りペースに合わせる待ちも終えるまで。最後の読み取り後の速度調整の待ちも含みます。 アクセスの経路 アプリの書き込み(秒) 受信側の完了時間(秒) アプリへ直接アクセス 6.562 8.016 NGINX・先読みなし 1.970 8.009 NGINX・先読みあり 0.055 8.005 表は、それぞれの測定内の2応答の中央値を取り、さらに3回の測定の中央値を取った値です。 実測値。syncワーカー2個、ダウンロード2件×3回、計6応答。棒は6応答の中央値、黒点は各応答の書き込み時間。受信側の約8秒は読み取り速度の制御を含み、実回線上の純粋な転送時間ではありません。 先読みありでは、NGINXがアプリの応答を早く読み進めます。NGINXのメモリに収まらない分は、一時ファイルにも保存できます。NGINXの公式仕様にあるこの動作を、今回の測定でも確認しました。 この設定では、アプリはNGINXへ向けた書き込みを早く終え、次の要求に進めます。NGINXは受信側への送信を続けられるため、アプリの書き込み完了と受信側の完了を分けられます。今回の短いAPI応答時間は、この動作と整合しています。応答バッファリングを有効にすれば、どのアプリでも同じ改善量になるという結果ではありません。 実測値。主構成で同時ダウンロード2件、前の時系列図と同じ測定。約200msごとに使用中の一時ファイルを合計しました。論理サイズを示し、物理ディスクへの書き込み量や速度ではありません。 先読みありでは、使用中の一時ファイルが2件のダウンロードで合計最大約25.2MiBになりました。アプリのワーカーが送信待ちをする時間を減らす代わりに、NGINXがデータを保存するためのメモリやファイルを使っています。 ファイルの内容はOSのメモリにも載り得るため、保存先をファイルにすればメモリ消費がなくなるわけではありません。 同時に対応できる数を増やした場合 別の確認として、Gunicornを1プロセス・4スレッドのgthread構成に変更しました。スレッドは、1つのプロセス内で複数の処理を進める仕組みです。ここでは応答を担当できる数を4つにしています。 この構成でアプリへ直接アクセスし、ダウンロードを同時に2件行うと、別のアクセスへのp95は約1.5msでした。2件の送信を待っていても、ほかの要求を扱える担当が残っています。 アプリへの直接アクセス・同時ダウンロード2件 別の要求のp95(ms、3回中央値) 主構成:sync 2プロセス 5,945.11 補助構成:gthread 1プロセス・4スレッド 1.55 この補助条件では、ワーカーの方式と、同時に対応できる数の両方を変えています。 方式だけの効果、担当数だけの効果は切り分けていません。同じアプリでも実行構成を変えれば結果が変わった例であり、スレッド方式の性能上の優位や、最適なスレッド数を決める比較には使えません。 この測定で言えること、測っていないこと 今回の主構成では、受信を遅くした要求が同時に2件あると、アプリの書き込みと別の要求への応答が長くなりました。Gunicorn syncの仕様と合わせると、同期ワーカーが担当中の応答を書き終えるまで次の要求へ進めないことが、結果を理解する手がかりになります。NGINXの応答バッファリングを変えた主比較は、この同じワーカー構成に対して行ったものです。 Flask・Django・FastAPI等のフレームワーク、UvicornやNode.js等の別サーバー・実行環境、非同期方式を比較したわけではありません。この実験から、それらの環境で同じ現象や同じ秒数になるとは判断できません。また、補助構成ではNGINXなしでも約1.5msだったため、NGINXがWebアプリ一般に必須だという結論にもなりません。 自分のアプリで確かめるなら まず、アプリが応答データを作り終えるまでの時間と、そのデータを書き込み終えるまでの時間を分けて見ます。作る処理がすぐ終わっているのに書き込みに長くかかるなら、受信側の読み取りや、その途中の送受信を調べる手がかりになります。 次に、送信を待っているワーカーが、その間に別の要求を扱える構成かを確認します。今回の同期ワーカーのように1件ずつ対応する構成なら、送信待ちの件数とワーカー数を一緒に見ます。 PaaSなどでアプリの前に中継サーバーがある場合は、そのサーバーが応答をどこまで先に受け取る設定なのかも確認します。設定を変えた後は、別のアクセスの応答時間に加えて、中継サーバーのメモリや一時ファイルの使用量も比べます。 先読みの有効化は、少しずつデータをすぐ届けたい応答では、届くタイミングに影響する可能性もあります。今回の結果は、大きさが決まった通常のHTTP応答についてのものです。画面へ少しずつ文字を表示するSSEや、接続を維持して双方向に通信するWebSocketの設定は、それぞれ別に確かめる必要があります。 今回の結果を自分のアプリへ当てはめる前に、使っているサーバーの方式、ワーカーやスレッドの数、中継サーバーの設定が同じかを確認してください。フレームワーク名だけで結果を当てはめず、送信を待つ間に別の要求を扱える構成かを調べるのが、この実験から持ち帰れる確認方法です。 使用したバージョン・設定・測定方法 実行環境と受信速度 Gunicorn 23.0.0、NGINX 1.22.1(Debianパッケージ1.22.1-9+deb12u10)、CPython 3.11.2、Linux 6.12、aarch64です。大きな応答は、あらかじめ用意した64KiBのデータを256回返す16MiBです。 すべて同じホストの127.0.0.1を使う通信(ループバック)です。HTTP/1.1を使い、応答後に接続を閉じます。TLSによる暗号化、圧縮、キャッシュは使っていません。受信プログラムの読み取り間隔を調節して約2MiB/sにしています。実際のインターネット回線を低速にした実験ではありません。 受信側ソケットのSO_RCVBUF設定は8KiB、Linuxが返した値は16KiBです。Linuxの仕様にある管理用の領域も含む値で、実際のTCP受信窓そのものの値ではありません。asyncioのStreamReaderのlimitは16KiBです。ダウンロードの接続開始から約250ms後に、/fastの要求を送り始めました。 NGINXの設定と一時ファイルの計測 「先読みあり/なし」はproxy_buffering on / offに対応します。それ以外の設定は同じです。proxy_buffer_size 16k、proxy_buffers 4 16k、proxy_busy_buffers_size 32k、proxy_max_temp_file_size 32m、proxy_temp_file_write_size 64k。一時ファイルの32MiB上限は、1要求あたりの上限です。 一時ファイルは、名前を削除してもプログラムが開いたまま使う場合があります。そのため、ディレクトリの一覧ではなく/proc/PID/fdから、NGINXが開いているファイルを調べました。同じファイルを複数回数えないよう、デバイス番号とinode(ファイルの識別番号)で重複を除いています。Linuxのファイル参照の説明も参照できます。 数値の集計とばらつき 各測定の72件を応答時間の短い順に並べ、95%の位置を切り上げた69番目をp95としました。条件別の表は、そのp95の3回分の中央値です。ダウンロード2件のときの範囲は次のとおりです。 アクセスの経路 3回のp95の範囲(ms) アプリへ直接アクセス 5,928.68〜6,590.04 NGINX・先読みなし 1,052.72〜1,826.44 NGINX・先読みあり 1.96〜3.03 本測定は30回、/fastの計2,160件はすべてHTTP 200で成功しました。大きな応答33件もすべて16MiBを受信しています。要求を送る予定時刻から、実際に送り始めるまでの遅れは最大約7.0msでした。 CPU上限はアプリ0.25コア、NGINX0.20コア、制御・負荷生成0.20コアで、制限周期は10ms。メモリ上限はそれぞれ128MiB、96MiB、128MiBです。CPU時間・制限による待ち・メモリ使用量も保存しました。本測定はホスト全体のCPU閾値超過で一度安全停止し、未完了の測定を除外して、完了済み17回の後の13回だけを再開しています。閾値とアプリの条件は変えていません。 最大処理能力や長時間の耐久性を測ったものではありません。各TCPソケットのバッファ量、HTTP/2・HTTP/3、SSE、WebSocket、低速アップロード、多数接続、メモリやディスクが不足する状況も対象外です。 測定値は条件別集計、1回ずつの集計、要求ごとの応答時間、大きな応答の送受信時間、メモリなどの時系列にまとめました。データのREADMEに、集計方法と再現用コードの案内があります。 KamuiDashは、GitHubからWebアプリをデプロイできるPaaSです。

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

Live mentions badge

Show how many posts mention your project in the last 30 days.

Live mentions · last 30 days
benoitc/gunicorn mentions in the last 30 days