Uvicorn + Gunicorn + Starlette зависает при подаче, не может перезапустить службу без sigkill

Обслуживаю модель на ВМ через gunicorn + uvicorn.

Он автоматически запускается супервизором, выполняющим api.sh.

api.sh содержит:

source /home/asd/.virtual_envs/myproject/bin/activate

/home/asd/.virtual_envs/myproject/bin/gunicorn --max-requests-jitter 30 -w 6 -b 0.0.0.0:4080 api:app -k uvicorn.workers.UvicornWorker

Не вдаваясь в подробности api.py, он содержит следующие основные части:

from starlette.applications import Starlette
from models import SomeModelClass


app = Starlette(debug=False)
model = SomeModelClass()


@app.route('/do_things', methods=['GET', 'POST', 'HEAD'])
async def add_styles(request):
    if request.method == 'GET':
        params = request.query_params
    elif request.method == 'POST':
        params = await request.json()
    elif request.method == 'HEAD':
        return UJSONResponse([])

    # Doing things
    result = model(params)
    return UJSONResponse(result)

Что происходит, так это то, что я начинаю получать эти ошибки после того, как api работает в течение нескольких дней:

[INFO] Starting gunicorn 20.0.3
[ERROR] Connection in use: ('0.0.0.0', 4080)
[ERROR] Retrying in 1 second.
[ERROR] Connection in use: ('0.0.0.0', 4080)
[ERROR] Retrying in 1 second.
[ERROR] Connection in use: ('0.0.0.0', 4080)
[ERROR] Retrying in 1 second.
[ERROR] Connection in use: ('0.0.0.0', 4080)
[ERROR] Retrying in 1 second.
...

Перезапуск api в supervisord ничего не делает, я получаю те же сообщения, что и выше. Единственный способ, который я обнаружил, это работает:

  1. Остановить api в супервизоре
  2. Посмотрите, какой pid работает на порту 4080 (процесс python3.8): sudo netstat -tulpn | grep LISTEN
  3. Убить его бегом kill -9 [PID]
  4. Повторите шаги 2-3 1-2 раза, пока ничего не займет порт 4080.
  5. Запустить api в супервизоре

У вас есть идеи, как это решить?


person Matas Minelga    schedule 30.11.2020    source источник


Ответы (1)


Код фактически использовал Pool из multiprocessing, и это, скорее всего, вызвало эту проблему.

Пример:

from starlette.applications import Starlette
from models import SomeModelClass
from multiprocessing import Pool
from utils import myfun


app = Starlette(debug=False)
model = SomeModelClass()


@app.route('/do_things', methods=['GET', 'POST', 'HEAD'])
async def add_styles(request):
    if request.method == 'GET':
        params = request.query_params
    elif request.method == 'POST':
        params = await request.json()
    elif request.method == 'HEAD':
        return UJSONResponse([])

    # Doing things
    result = model(params)
    # Start of the offending code
    pool = Pool(4)
    result = pool.map(myfun, result, chunksize=1)
    # End of the offending code
    return UJSONResponse(result)

Решение - заменить multiprocessing на concurrency:

from starlette.applications import Starlette
from models import SomeModelClass
import concurrent.futures
from utils import myfun


app = Starlette(debug=False)
model = SomeModelClass()


@app.route('/do_things', methods=['GET', 'POST', 'HEAD'])
async def add_styles(request):
    if request.method == 'GET':
        params = request.query_params
    elif request.method == 'POST':
        params = await request.json()
    elif request.method == 'HEAD':
        return UJSONResponse([])

    # Doing things
    result = model(params)
    # Start of the fix
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        result = executor.map(myfun, result)
    result = list(result)
    # End of the fix
    return UJSONResponse(result)
person Matas Minelga    schedule 19.01.2021