fastapiによるプロセスのスレッド停止に関する原因究明のメモ

2025年3月21日

fastapiによるプロセスのスレッド停止に関する原因究明のメモ

1. 問題の背景と経緯

1.1. 問題の内容

  1. fastapiからC言語で作成したプログラムを起動すると、Cプログラムの中にあるスレッドが途中で停止するという問題が発生している。

  2. Cプログラム をfastapiプログラムから起動したの場合、udpマルチキャストの受信を⾏うスレッドが、数回〜10数回⽬で停⽌することが発覚。

1.2. 事前確認事項

  1. fastapiプログラムから Cプログラムを起動せず、コマンド(手入力)で起動した場合、この問題は発⽣しないことは確認済。

  2. また、1のCプログラム起動だけでなく、複数のCプログラムを起動した場合においても、この問題が発生しないことも確認済み。

  3. 以上より、この問題は、Cプログラム本体ではなくて、それを起動するfastapi(fastapiプログラム)側に問題があると推認された。

  4. この問題は、以下のfastapiプログラムのコードの差し替えによって解決することが判明している。

問題が発生するケースでのfastapiプログラムのコマンド発生プロセスのコード

(1)
pid = await run_command_and_get_pid(command)

(2)
task = asyncio.create_task(run_command_async(command))
process = await task
pid = process.pid

# なお、上記(1)(2)は、実質的には同じ処理である
問題が発生しないケースでのfastapiプログラムのコマンド発生プロセスのコード

with open(os.devnull, 'w') as devnull:
process = subprocess.Popen(shlex.split(command), stdout=devnull,
stderr=devnull)
pid = process.pid

2. 問題点の差分の明確化

この問題の発生/不発生は、以下の2点に集約できることが確認された。

  1. asyncio.create_task と await を併用した非同期処理
  2. 上記を使用しない、subprocess.Popen を使用したプロセス処理

3. fastapiプログラムの制御対象である、Cプログラムの特徴

Cプログラムは、映像転送を行うメインルーチンと、SRT転送の状況を常時把握するスレッド、自分と自分以外のCプログラムのプロセスをリアルタイムでカウントするスレッドと、稼動中のCプログラムに通信を行うためのスレッド(現在未使用)からなる、映像転送&プロトコル変換プログラムである。

alt text

1Mbps以上の映像転送を行いながら、映像転送環境を常に把握し続ける必要のある、リアルタイム性能が要求される高負荷の制御プログラムである。

4. 検討

問題の原因を分析するためには、いくつかの可能性を考慮する必要がある。特に、asyncio.create_task や await を使用した非同期処理と、subprocess.Popen を使用したプロセス管理の違いが影響している可能性がある。この点を中心に考察する。

4.1. asyncio.create_task や await を使用した非同期処理において、排除できる可能性

4.1.1. 非同期処理の影響の可能性

asyncio.create_task と await を使用した場合、非同期処理によってプロセスの状態が意図しない形で処理されている可能性があるが、今回はこれには該当していない。プロセス自体が稼動していることは確認している。したがって、Python のガベージコレクションやスケジューリングの問題によって、起動したプロセスが適切に管理されないケースは考慮しなくてよい。

4.1.2. タスクが解放された可能性

ログファイルを用いて確認した結果、asyncio.create_task を使用して作成されたタスクが適切に監視されず終了した形跡はないので、この件について考慮しなくてよい。

4.1.3. await によるブロックの影響の可能性

非同期関数内で await を使用してプロセスの終了を待っていると、FastAPI のリクエストが別の処理と競合し、プロセスの動作に影響を与える可能性はあるが、今回はCプログラムが1個の場合でも発生しているので、これも考慮する必要はない。

4.2. asyncio.create_task や await を使用した非同期処理において、排除できない可能性

4.2.1. イベントループのスケジューリングとリソースの競合

asyncio を使用すると、イベントループによってすべての非同期タスクがスケジュールされる。しかし、他のタスクやリクエスト処理が増えると、イベントループ全体の負荷が高まり、処理が遅延する可能性がある。

つまり、fastapiプログラムが、Cプログラムを含めた全体のタスク管理を行うため、Cプログラムの自由な実行が制限されるという問題が発生しうる。

この状況では、特定のタスク(今回の場合はsend_bitrate_or_count_processes のスレッド)への割り当てが遅れることで、スレッドが停止したかのような挙動が発生することがありえる。

4.2.2. プロセス管理の不安定さ

非同期タスクでは、プロセス管理がイベントループの中で行われるため、イベントループ自体の負荷やスケジュールの影響を受けやすくなる。

つまり、リアルタイム性の高い高タスクのCプログラムの実行が、fastapiプログラムの動作を遅らせ、その結果、fastapiプログラムの管理下にあるCプログラムの実行が制限されるという悪循環を発生させると考えうる。

4.2.3. プロセスのモニタリングの不安定さ

非同期処理では、プロセスの監視 (monitor_process_status) を含むさまざまなタスクが並行して実行される。この際、イベントループがプロセスの管理や状態確認の処理を後回しにすることで、プロセスの実行状態が適切に反映されず、スレッドが停止しているかのような挙動になったと考えうる。

4.3. subprocess.PopenによってCプログラムのスレッド問題が解決した理由理由

4.3.1. subprocess.Popen との違い

subprocess.Popen を使用して起動する場合は、Python の非同期処理機構に依存されることなく(fastapiプログラムの管理に組込まれないため)、独立してプロセスが管理される。このため、プロセスの状態が安定して保たれたと考える。

これは事実上、手入力でコマンドを押下する処理と同じことをやっており、前述の「コマンド(手入力)で起動した場合、この問題は発生しないことが確認済」という内容と整合が取れている。

5. 結論

今回のプログラムにおける問題の原因は、非同期タスクがイベントループのスケジューリングに依存している点に起因していると考えられる。

具体的には、非同期処理 (asyncio.create_task + await) のリソース競合やスケジューリングのタイミングによって、プロセスの管理や監視が適切に行われず、スレッドが途中で停止しているように見えたと考えられる。

以上より、subprocess.Popenの利用によってCプログラムのスレッド問題が解決したことは、合理的に説明ができる

以上

6. 付録

6.1. 「非同期タスクでのコマンド実行 (asyncio.create_task + await)」の意義(なんのために、このような仕組みが用意されているのか)

非同期タスク (asyncio.create_task + await) の意義は、主に以下の3つの観点に集約される

6.1.1. 高いスループットと効率的なリソース使用

非同期タスクは、待機中のリソース(たとえば、ネットワークI/OやファイルI/Oなど)を効率的に使用しながら、他のタスクの実行を並行して進めることを可能にする。これにより、CPUの使用率を高め、多くのリクエストやタスクを同時に処理することができる。例えば、クライアントからのHTTPリクエストに対する応答待ちの間に、他のタスクの処理を進めることができるため、高いスループットが実現される

6.1.2. ブロッキングの回避とレスポンスの向上

従来の同期的な実装では、一つの処理が完了するまで他の処理を待たせる必要がある(ブロッキング)。これに対して、非同期処理では、あるタスクがI/O待機などで停止している間にも、他のタスクが進行するため、アプリケーション全体のレスポンスが向上する。具体的には、ネットワークのレスポンスを待っている間に他の処理を進行させることで、CPUのアイドル時間を減らし、待機時間のロスを最小化する。

6.1.3. 大量の同時接続やリクエストの処理に強い

非同期タスクの仕組みは、大量の接続やリクエストを効率的に処理するために設計されている。特に、Webアプリケーションやマイクロサービスでは、多くのクライアントからの接続を管理し、レスポンスを返す必要がある。例えば、FastAPI などの非同期対応Webフレームワークでは、同時に多数のリクエストを処理し、レスポンスを返すことが求められる。このとき、asyncio.create_task と await を利用することで、各リクエストを非同期タスクとして並列に処理できる。

6.2. Cプログラムは、映像転送(中継)とプロトコル変換を行う典型的な制御プログラムで、リアルタイムのパフォーマンスが要求されるプログラムである。このようなプログラムの起動にfastapiを用いるのは正しい選択だったか?

FastAPIを使用してリアルタイムパフォーマンスが要求される制御プログラム(Cプログラム)を起動することは、設計上の選択としていくつかの利点と課題が存在すると考えた。

6.2.1. FastAPIを使用する利点

6.2.1.1. 軽量で高速な非同期フレームワーク:

FastAPIはPythonの非同期処理を活かして、高速かつ効率的にリクエストを処理できる。これにより、複数のリクエストを並列に処理することが可能であり、APIベースのコントロールが求められる場面では有効である。

6.2.1.2. シンプルで明快なAPI構築:

RESTful APIを簡単に構築できるため、外部からのコントロールやモニタリングが要求される制御プログラムにとっては適切である。また、外部から簡単にステータス確認やプロセスの制御が可能になる

6.2.1.3. スケーラビリティの確保:

FastAPIは、UvicornやGunicornといったASGIサーバーと組み合わせることで、スケーラブルな構成をとることができる。リクエストの多い状況や複数の制御要求が同時に発生するような環境で柔軟に対応できる。

6.2.2. FastAPIを制御プログラムに用いる際の課題と考慮点

一方で、リアルタイムのパフォーマンスを要求される制御プログラムにおいて、FastAPIの非同期タスクやイベントループの特性が以下のような問題を引き起こす可能性がある。

6.2.2.1. イベントループの負荷と競合:

FastAPIは非同期フレームワークであるため、複数のタスクが同時に実行されることを前提としている。しかし、リアルタイム制御を要するプログラム(映像転送やプロトコル変換など)の場合、イベントループの負荷が高まると、タスクの遅延やリソース競合が発生しやすくなる。

6.2.2.2. リアルタイム性能の不安定さ:

映像の中継やプロトコル変換を行うプログラムでは、タイミングやデータ転送の精度が重要である。FastAPIを介したプロセスの管理やコマンドの実行は、イベントループに依存するため、Pythonのインタープリタの遅延やスレッド管理の問題がリアルタイム性能を損なうリスクがある。

6.3. (付録の)結論

プロセスの制御や起動部分をFastAPIから分離する本件の方式は、fastapi + 制御プログラム の併用方式としては最適である、と考える。

2025年3月21日2024,江端さんの技術メモ

Posted by ebata