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


関連記事

Rust入門してみた (基本構文編)

1 はじめに2 Rustとは?3 Rustの特徴的な基本構文3.1 変数と定数3.2 所有権3.3 所有権の借用3.4 関数3.5 エラーハンドリング3.5.1 回復不能なエラー(panic!)3.5 ...

Stripe Connectを使ってCheckoutを利用した継続課金を実装

1 はじめに1.1 Checkoutを使う場合の動線2 決済画面への遷移2.1 マイグレーション2.2 Checkout Sessionの作成3 決済完了後の制御4 おまけ4.1 ローカルでWebho ...

js

TypeScriptでJavaScriptのライブラリを使用するには?

1 はじめに2 対応方法2.1 npmで@typesからインストールする2.2 自分で型定義ファイルを作る3 Declaration Space3.1 Type Declaration Space3. ...

rails

form_withでフォームの送信前に処理を行う方法

1 はじめに2 form_with3 サンプル4 さいごに5 参考 はじめに フォームを送信する前に処理を行いたいケース(Google Analyticsのイベントのトラッキングなど)があると思います ...

laravel logo

Server-Side Eventsを触ってみた

1 はじめに2 Server-Side Events(SSE) とは3 実装してみる3.1 動作の流れ3.2 Laravel側3.3 React側4 Chat GPTのレスポンス5 さいごに6 おすす ...

フォロー

blog-page_side_responsive

2024年9月
1234567
891011121314
15161718192021
22232425262728
2930  

アプリ情報

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