質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.95%

FastAPIでスレッドエラーとなってしまう

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 80

score 2

前提・実現したいこと

FastAPIにてREST APIを作成し、サーバ(Conoha Wing)に乗せてlocalhostで動かし、
.htaccessにてリバースプロキシさせて稼働しています。(フロントはReact)
FastAPIの起動には下記設定でgunicornを利用しています。
■ gunicorn設定ファイル

name = 'gunicorn'

worker_class = 'uvicorn.workers.UvicornWorker'
workers = 5
threads = 5
keepalive = 2


起動後しばらくは良いのですが、
何度かアクセスを行うとスレッド作成ができないエラーが発生してしまいます。

実際にps -fLu [ユーザID]でプロセスを確認してみると、
起動直後はプロセスは6つ(親 + worker 5個)ですが、
アクセス毎にこのプロセスが増えていきどこかのタイミングで上限に達してしまっているようです。

このプロセスは本来はアクセス毎に生成され、通信が終了した時点で消えるものなのではないのでしょうか?
また、このエラーを解決する方法はありますでしょうか?

発生している問題・エラーメッセージ

  File ".../api/venv/lib64/python3.6/site-packages/fastapi/dependencies/utils.py", line 550, in solve_dependencies
    solved = await run_in_threadpool(call, **sub_values)
  File ".../api/venv/lib64/python3.6/site-packages/starlette/concurrency.py", line 40, in run_in_threadpool
    return await loop.run_in_executor(None, func, *args)
  File "uvloop/loop.pyx", line 2658, in uvloop.loop.Loop.run_in_executor
  File "/opt/alt/python36/lib64/python3.6/concurrent/futures/thread.py", line 123, in submit
    self._adjust_thread_count()
  File "/opt/alt/python36/lib64/python3.6/concurrent/futures/thread.py", line 142, in _adjust_thread_count
    t.start()
  File "/opt/alt/python36/lib64/python3.6/threading.py", line 846, in start
    _start_new_thread(self._bootstrap, ())
RuntimeError: can't start new thread

試したこと

通信を受けるメソッドをasyncで定義すると解決するとあったのですがそれでもだめでした。

@router.get("/", response_model=List[schemas.User])
async def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud_user.get_users(db, skip=skip, limit=limit)
    return users

下記のように構成を小さくして再度試してみました。
結果的にはやはりプロセスが残っており、上限?の70プロセスで毎回エラーとなります。
ただし、DBアクセスを行わない'/'へのアクセスではプロセスの残りはなかったので、
DB周りが怪しいということが分かりました。closeはちゃんとしているんですが。。。
■ FastAPI側

# main.py
from fastapi import Depends, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List

from database import models, schemas
from database.database import SessionLocal, engine
from database.cruds import user as crud_user
from dependencies import get_db

models.Base.metadata.create_all(bind=engine)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.middleware('http')
async def db_session_middleware(request: Request, call_next):
    response = Response('Internal server error', status_code=500)
    try:
        request.state.db = SessionLocal()
        response = await call_next(request)
    finally:
        request.state.db.close()

    return response


@app.get("/")
async def root():
    return {"message": "Hello World!"}


@app.get("/users", response_model=List[schemas.User])
async def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud_user.get_users(db, skip=skip, limit=limit)
    return users
# dependencies.py
from fastapi import Request


def get_db(request: Request):
    return request.state.db
# database/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

import json

with open('config.json', mode='r') as f:
    config = json.load(f)
    database_info = config['database']
    DATABASE_URL = "mysql+pymysql://%s:%s@%s/%s?charset=utf8mb4" % (
        database_info['user'],
        database_info['password'],
        database_info['host'],
        database_info['db'],
    )

engine = create_engine(DATABASE_URL, echo=False, pool_recycle=60)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
# database/cruds/user.py
from sqlalchemy.orm import Session

from database import models

from typing import List


def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[models.User]:
    return db.query(models.User).offset(skip).limit(limit).all()

■ API実行用
テスト用に作ったもので実際はReactからaxiosで実行しています。

import subprocess


def main():
    for i in range(0, 100):
        print(f'{i+1}回目')
        subprocess.call('curl http://localhost:8001/users', shell=True)


if __name__ == '__main__':
    main()

補足情報(FW/ツールのバージョンなど)

・python => 3.6
・FastAPI => 0.65.1
・uvicorn => 0.13.4
・gunicorn => 20.1.0

サーバ
・Conoha Wing

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

check解決した方法

+1

自己解決しました。
やはりDB周りだったようです。

# dependencies.py
from fastapi import Request

# def get_db(request: Request):      <- old
async def get_db(request: Request): #<- new
    return request.state.db

FastAPIのチュートリアルにはoldで書かれているので注意が必要そうですね。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/06/11 18:57

    解決できて良かったです!

    キャンセル

0

fastapi側の処理としてどのような形になっていますでしょうか?
レスポンスを返さず保持した状態ですとthreadがどんどん増えていきまして、スレッドエラーになるかと思います。
お手数でなければ該当のurlに対しての処理が書かれたコードをご提示頂くことは可能でしょうか?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/06/11 11:10

    コメントありがとうございます。

    構成を小さくして再度テストしてみたものを追記いたしました。
    urlに対しての処理はmain.pyのread_usersになります。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.95%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る