Выполнение фоновой задачи fastAPI занимает до 100 раз больше времени, чем прямой вызов функции

У меня есть простая конечная точка fastAPI, развернутая в Google Cloud Run. Я сам написал класс Workflow. Когда экземпляр Workflow выполняется, происходят некоторые шаги, например, файлы обрабатываются, и результат помещается в базу данных vectorstore.

Обычно это занимает несколько секунд для каждого файла, такого как этот:

from .workflow import Workflow
...

@app.post('/execute_workflow_directly')
async def execute_workflow_directly(request: Request)
    ...  # get files from request object
    workflow = Workflow.get_simple_workflow(files=files)
    workflow.execute()
    return JSONResponse(status_code=200, content = {'message': 'Successfully processed files'})

Теперь, если задействовано много файлов, это может занять некоторое время, и я не хочу, чтобы вызывающий абонент конечной точки ждал, поэтому я хочу запустить выполнение рабочего процесса в фоновом режиме следующим образом:

from .workflow import Workflow
from fastapi import BackgroundTasks
...

def run_workflow_in_background(workflow: Workflow):
    workflow.execute()

@app.post('/execute_workflow_in_background')
async def execute_workflow_in_background(request: Request, background_tasks: BackgroundTasks):
    ...  # get files from request object
    workflow = Workflow.get_simple_workflow(files=files)
    background_tasks.add_task(run_workflow_in_background, workflow)
    return JSONResponse(status_code=202, content = {'message': 'File processing started'})

Тестируя это только с одним файлом, я уже столкнулся с проблемой: локально он работает нормально, но когда я развертываю его в своей службе Google Cloud Run, время выполнения зашкаливает: в одном примере фоновое выполнение заняло почти ~ 500 секунд, пока я не увидел результат в базе данных, по сравнению с ~ 5 секундами при непосредственном выполнении рабочего процесса.

Я уже пытался увеличить количество ядер ЦП до 4, а впоследствии и количество рабочих стрелков до 4. Не уверен, что это имеет смысл, но время выполнения не уменьшилось.

Могу ли я решить эту проблему, выделив каким-то образом больше ресурсов для запуска Google Cloud, или мой подход ошибочен, и я делаю что-то не так, или уже должен переключиться на что-то более сложное, например, Celery?


Изменить (не имеет отношения к моей проблеме, см. принятый ответ):

Я прочитал принятый ответ на этот вопрос, и он помог прояснить некоторые вещи, но на самом деле не отвечает на мой вопрос, почему существует такая большая разница во времени выполнения между запуском напрямую и в качестве фоновой задачи. Обе версии вызывают интенсивный процессор workflow.execute() асинхронно, если я не ошибаюсь.

Я действительно не могу изменить определение конечной точки на def, потому что я ожидаю другого кода внутри.

Я попытался изменить фоновую функцию на

async def run_workflow_in_background(workflow: Workflow):
    await run_in_threadpool(workflow.execute)

и

async def run_workflow_in_background(workflow: Workflow):
    loop = asyncio.get_running_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        res = await loop.run_in_executor(pool, workflow.execute)

и

async def run_workflow_in_background(workflow: Workflow):
    res = await asyncio.to_thread(workflow.execute)

и

async def run_workflow_in_background(workflow: Workflow):
    loop = asyncio.get_running_loop()
    with concurrent.futures.ProcessPoolExecutor() as pool:
        res = await loop.run_in_executor(pool, workflow.execute)

как было предложено, и это не помогло.

Я попытался увеличить количество рабочих, как было предложено, и это не помогло.

Думаю, я рассмотрю возможность перехода на Celery, но все же хочу понять, почему он так медленно работает с фоновыми задачами fastAPI.

Отвечает ли это на ваш вопрос? FastAPI запускает API-вызовы последовательно, а не параллельно

Chris 19.04.2023 16:54

@Chris: Не совсем так, посмотри мои правки. Может быть, все еще мое отсутствие полного понимания.

Clang 19.04.2023 19:23

Как выглядят ваши настройки Cloud Run? Как описано в этого ответа, если вы не установите для минимального экземпляра значение не менее 1, а для ЦП всегда установлено значение true, Cloud Run будет ограничивать доступ вашего ЦП, как только завершится HTTP-запрос, что звучит точно так же, как вы повторное описание.

M.O. 19.04.2023 20:48
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
3
219
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В Cloud Function, как и в Cloud Run, ЦП выделяется (и оплачивается) только при обработке запроса.

Запрос считается обработанным между получением запроса и отправкой ответа.

В остальное время ЦП дросселируется (ниже 5%).


Как говорится, оглянитесь на свои функции.

  • Самый быстрый получает данные, обрабатывает данные и отправляет ответ. Процессор выделяется на все время обработки.
  • Самый медленный получает данные, запускает задачу в фоновом режиме (многопоточность, форк или что-то еще) и немедленно отправляет ответ. После отправки ответа ЦП отключается, и начинается обработка. Конечно, это очень медленно, вы выходите за пределы выделения ЦП.

Чтобы решить эту проблему, вы можете использовать Cloud Run с опцией ЦП всегда выделяется (или без дросселирования процессора с помощью командной строки GCLOUD). Нет варианта с облачными функциями

Спасибо, это была именно проблема. Хорошее объяснение, имеет много смысла. Конечно, это не идеально с точки зрения затрат, когда эти фоновые задачи выполняются всего несколько раз в день. Есть ли способ убедиться, что у фоновых задач достаточно ресурсов, не всегда выделяя их?

Clang 19.04.2023 21:26

Другие вопросы по теме