BackEnd

FastAPIのPath Operationをasync defにするときはブロッキングに気をつけよう!

投稿日:

はじめに

今回は、FastAPIのAPIのエンドポイントを定義する方法である Path Operation に渡す関数を async def にした時の挙動について調べました。この手の記事はすでにたくさんありますが、今回自分で調べて裏をとりたかったため、調べた内容をまとめてみました。

Path Operationと並行処理

FastAPIでAPIを定義するには、Path Operationデコレータを使います。以下がFastAPIの最小限のコードです。

@app.get("/") といったデコレータに関数を登録することで、指定されたパスとメソッドへのリクエストに対応する関数を定義できます。
この関数は async def にすることもできます。

どちらのコードでも、実際に動かしてみると一見挙動に違いは見られません。しかし、実はこの関数の呼び出し方が異なるのです。

通常の def は、APIリクエストがあった時に直接呼び出されるのではなく、FastAPIのスレッドプールによって呼び出され、 awaitされます。こうすることで、同期関数で重たい処理があっても、他のリクエストをブロックすることはなく、「並列」処理されます。

ところが、 async def はイベントループから呼び出されます。イベントループは「並行」処理なので、シングルスレッドでタスクを切り替えながら実行されます。そのため、同期で重たい処理やI/O待ちなどが発生してしまうと、制御が解放されず、他のタスクが実行待ちのままとなってしまいます。

結果として、他のAPIリクエストを受け付けられなくなってしまい、ブロッキングしてしまうというわけです。

挙動の確認

以下のコードを使って実験してみます。

このサンプルでは、asyncpgを使ってDBにSELECTしつつ、requestsで外部APIにリクエストしています。asyncpgは非同期なので、このAPIの関数も async def にせざるを得ません。

にもかかわらず、requestsは同期処理となっているため、外部APIのレスポンス待ちが発生し、I/Oブロックしてしまいます。

対応方法

この問題の対応方法を3パターン考えてみました。

asyncioを使って自力で非同期化する

組み込みモジュールであるasyncioを使い、イベントループ内で関数を実行することで、制御を解放します。

このコードを使うと、 GET /req_very_slow_task のレスポンスを待っている最中にも、 GET / はブロッキングされず、すぐレスポンスが返ってきます。

今回は、requestsを使いましたが、例えばboto3でも同じ手法で非同期化することができます。

async対応済みのパッケージを使う

先ほどの例ではrequestsを使いましたが、同じHTTPクライアントである httpx は初めから非同期をサポートしています。(ちなみに、requestsはメンテナンスが止まっているので、今現在では同期の場合でもhttpxがおすすめです。)

httpxを使って、次のようにコードを変更します。

httpx.AsyncClient を使うことで、自力でasyncioを使わなくても簡単に非同期化することができています。そのため、先ほどよりコードがすっきりしました。

その他の同期パッケージも、他の代替えがある場合にはそれを使用することで、自力でasyncioを使わなくとも非同期化することができます。

やりたいこと 同期 非同期
HTTPクライアント httpx or requestsなど httpxなど
AWS クライアント boto3 aioboto3
※全機能が使えるのはs3のみ
ファイルI/O 組み込みの open() 関数 aiofiles
PostgreSQL psycopgなど psycopg or asyncpg など
MySQL mysqlclient or PyMySQL など asyncmy or aiomysql
※開発は止まっている模様

async defをやめる

アプリケーション全体でasyncpgを使うと決めた場合、おそらくほとんどのエンドポイントが async def になると思いますが、それは至るとこでI/Oバウンドなどによるブロッキングが起きないか、気をつけなければならないことになります。

そこまでパフォーマンスにシビアでなければ、アプリケーション全体で使用するであろう処理を同期にすることで、コード全体をすっきりとさせ、やるべきことに集中することができるかもしれません。

冒頭で話した通り、同期の def もスレッドプール内のスレッドによって実行されるため、他のAPIをブロッキングすることはありません。もちろん、スレッドの切り替え(コンテキストスイッチ)が発生することから、asyncよりかはパフォーマンスが劣るみたいですが、パフォーマンスをどこまで切り詰めるかと、コードのわかりやすさのバランスが重要だと思います。

さいごに

非同期は奥が深いですね。CPUをどのように使うのか、並列と並行ではアプローチが異なることも含めて、改めて勉強になりました。

おすすめ書籍

エキスパートPythonプログラミング 改訂4版 (アスキードワンゴ) ハイパーモダンPython ―信頼性の高いワークフローを構築するモダンテクニック 動かして学ぶ!Python FastAPI開発入門

blog-page_footer_336




blog-page_footer_336




-BackEnd
-,

執筆者:

免責事項

このブログは、記事上部に記載のある投稿日時点の一般的な情報を提供するものであり、投資等の勧誘・法的・税務上の助言を提供するものではありません。仮想通貨の投資・損益計算は複雑であり、個々の取引状況や法律の変更によって異なる可能性があります。ブログに記載された情報は参考程度のものであり、特定の状況に基づいた行動の決定には専門家の助言を求めることをお勧めします。当ブログの情報に基づいた行動に関連して生じた損失やリスクについて、筆者は責任を負いかねます。最新の法律や税務情報を確認し、必要に応じて専門家に相談することをお勧めします。


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


関連記事

PHPerだけどKotlinを勉強したって良いよね その1〜クラス編〜

1 はじめに2 インターフェース2.1 基本2.2 デフォルト実装付メソッド2.3 同じメソッド名を持つ複数のインターフェース3 修飾子3.1 open3.2 abstract4 可視性修飾子5 さい ...

Go言語

GoのWeb Application Framework

1 はじめに2 代表的なGoのWAF2.1 軽量なWAF2.2 フルスタックなWAF3 Ginを使ってみる3.1 クエリパラメータ+ポストパラメータ3.2 ファイルアップロード3.3 URLのグループ ...

Stripe Connectを使って複合プランの継続課金を実装

1 はじめに1.1 想定する継続課金1.2 想定するシチュエーション2 継続課金商品の作成2.1 プランの継続課金商品の作成2.2 ID数の継続課金商品の作成3 実装3.1 プランAとID数を30契約 ...

Go言語

Go 1.16でのgo getとgo installの変更点

1 はじめに2 go getとgo installの役割2.1 go getの役割2.2 go installの役割3 この変更で何が改善されたのか3.1 go:generateと併用する4 その他の ...

rails

Railsでの非同期処理とDelayed Job

1 はじめに2 Active Job2.1 Active Jobの役割2.2 ジョブを作成する2.3 ジョブをキューに登録する2.4 コールバック2.5 例外3 Delayed Job3.1 設定3. ...

フォロー

blog-page_side_responsive

2024年9月
1234567
891011121314
15161718192021
22232425262728
2930  

アプリ情報

私たちは無料アプリもリリースしています、ぜひご覧ください。 下記のアイコンから無料でダウンロードできます。