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


関連記事

Go言語

FSMを使った状態管理をGoで実装する

1 はじめに2 FSMとは3 基本的な実装3.1 実装する状態管理の概要3.2 状態管理を実装する4 structを定義した応用的な実装5 FSMの可視化6 さいごに7 おすすめ書籍 はじめに 一般的 ...

laravel logo

laravel-enumを使ってみたら快適だった

1 はじめに2 enumについて3 環境4 導入5 enumクラス5.1 生成5.2 enumクラス編集5.3 日本語化6 マイグレーション6.1 生成6.2 編集7 プロパティのキャスト8 さいごに ...

Rust入門してみた その5 ライフタイム

1 はじめに2 ライフタイムと借用チェッカー3 コンパイラにライフタイムを教える3.1 ライフタイムの指定が不要なケース4 ライフタイムの省略5 さいごに6 おすすめ書籍 はじめに Rustには、借用 ...

rails

Rails5.2+Pumaのデプロイ設定

1 はじめに1.1 環境2 Pumaとは?2.1 Unicornとの比較2.2 Pumaのスレッド3 Pumaの使い方4 デプロイ設定4.1 Gemを追加4.2 Capfileの設定4.3 deplo ...

laravel logo

Laravelのバッチ処理を作る

1 はじめに2 環境3 artisanコマンド作成4 artisanコマンドをバッチとして登録する5 さいごに6 おすすめ書籍 はじめに こんにちは。webアプリにつきもののバッチ処理ですが、もちろん ...

フォロー

blog-page_side_responsive

2024年9月
1234567
891011121314
15161718192021
22232425262728
2930  

アプリ情報

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