На скорость выполнения кода Fastapi на Python влияет развертывание с помощью uvicorn vs gunicorn

Я написал приложение fastapi. И теперь я подумываю о его развертывании, но, похоже, у меня возникают странные неожиданные проблемы с производительностью, которые, похоже, зависят от того, использую ли я uvicorn или gunicorn. В частности, весь код (даже код стандартной библиотеки на чистом Python), кажется, становится медленнее, если я использую gunicorn. Для отладки производительности я написал небольшое приложение, которое демонстрирует это:

import asyncio, time
from fastapi import FastAPI, Path
from datetime import datetime

app = FastAPI()

@app.get("/delay/{delay1}/{delay2}")
async def get_delay(
    delay1: float = Path(..., title="Nonblocking time taken to respond"),
    delay2: float = Path(..., title="Blocking time taken to respond"),
):
    total_start_time = datetime.now()
    times = []
    for i in range(100):
        start_time = datetime.now()
        await asyncio.sleep(delay1)
        time.sleep(delay2)
        times.append(str(datetime.now()-start_time))
    return {"delays":[delay1,delay2],"total_time_taken":str(datetime.now()-total_start_time),"times":times}

Запуск приложения fastapi с помощью:

gunicorn api.performance_test:app -b localhost:8001 -k uvicorn.workers.UvicornWorker --workers 1

Резонирующее тело перехода к http://localhost:8001/delay/0.0/0.0 постоянно выглядит примерно так:

{
  "delays": [
    0.0,
    0.0
  ],
  "total_time_taken": "0:00:00.057946",
  "times": [
    "0:00:00.000323",
    ...smilar values omitted for brevity...
    "0:00:00.000274"
  ]
}

Однако используя:

uvicorn api.performance_test:app --port 8001 

Я постоянно получаю такие сроки

{
  "delays": [
    0.0,
    0.0
  ],
  "total_time_taken": "0:00:00.002630",
  "times": [
    "0:00:00.000037",
    ...snip...
    "0:00:00.000020"
  ]
}

Разница становится еще более заметной, когда я раскомментирую оператор await asyncio.sleep(delay1).

Поэтому мне интересно, что делают gunicorn / uvicorn со средой выполнения python / fastapi, чтобы создать эту разницу в 10 раз в скорости выполнения кода.

Я выполнил эти тесты, используя Python 3.8.2 на OS X 11.2.3 с процессором Intel I7.

И это соответствующие части моего pip freeze вывода

fastapi==0.65.1
gunicorn==20.1.0
uvicorn==0.13.4

person M.D.    schedule 29.05.2021    source источник


Ответы (3)


Я не могу воспроизвести ваши результаты.

Моя среда: ubuntu на WSL2 в Windows 10

соответствующие части моего pip freeze вывода:

fastapi==0.65.1
gunicorn==20.1.0
uvicorn==0.14.0

Я немного изменил код:

import asyncio, time
from fastapi import FastAPI, Path
from datetime import datetime
import statistics

app = FastAPI()

@app.get("/delay/{delay1}/{delay2}")
async def get_delay(
    delay1: float = Path(..., title="Nonblocking time taken to respond"),
    delay2: float = Path(..., title="Blocking time taken to respond"),
):
    total_start_time = datetime.now()
    times = []
    for i in range(100):
        start_time = datetime.now()
        await asyncio.sleep(delay1)
        time.sleep(delay2)
        time_delta= (datetime.now()-start_time).microseconds
        times.append(time_delta)

    times_average = statistics.mean(times)

    return {"delays":[delay1,delay2],"total_time_taken":(datetime.now()-total_start_time).microseconds,"times_avarage":times_average,"times":times}

За исключением первой загрузки веб-сайта, мои результаты для обоих методов почти одинаковы.

В большинстве случаев для обоих методов времена находятся между 0:00:00.000530 и 0:00:00.000620.

Первая попытка для каждого занимает больше времени: около 0:00:00.003000. Однако после того, как я перезапустил Windows и снова попробовал эти тесты, я заметил, что у меня больше не увеличивается время выполнения первых запросов после запуска сервера (я думаю, это благодаря большому количеству свободной оперативной памяти после перезапуска)


Примеры не первых прогонов (3 попытки):

# `uvicorn performance_test:app --port 8083`

{"delays":[0.0,0.0],"total_time_taken":553,"times_avarage":4.4,"times":[15,7,5,4,4,4,4,5,5,4,4,5,4,4,5,4,4,5,4,4,5,4,4,5,4,4,4,5,4,4,5,4,4,5,4,4,4,4,4,5,4,5,5,4,4,4,4,4,4,5,4,4,4,5,4,4,4,4,4,4,5,4,4,5,4,4,4,4,5,4,4,5,4,4,4,4,4,5,4,4,5,4,4,5,4,4,5,4,4,4,4,4,4,4,5,4,4,4,5,4]}
{"delays":[0.0,0.0],"total_time_taken":575,"times_avarage":4.61,"times":[15,6,5,5,5,5,5,5,5,5,5,4,5,5,5,5,4,4,4,4,4,5,5,5,4,5,4,4,4,5,5,5,4,5,5,4,4,4,4,5,5,5,5,4,4,4,4,5,5,4,4,4,4,4,4,4,4,5,5,4,4,4,4,5,5,5,5,5,5,5,4,4,4,4,5,5,4,5,5,4,4,4,4,4,4,5,5,5,4,4,4,4,5,5,5,5,4,4,4,4]}
{"delays":[0.0,0.0],"total_time_taken":548,"times_avarage":4.31,"times":[14,6,5,4,4,4,4,4,4,4,5,4,4,4,4,4,4,5,4,4,5,4,4,4,4,4,4,4,5,4,4,4,5,4,4,4,4,4,4,4,4,5,4,4,4,4,4,4,5,4,4,4,4,4,5,5,4,4,4,4,4,4,4,5,4,4,4,4,4,5,4,4,5,4,4,5,4,4,5,4,4,4,4,4,4,4,5,4,4,5,4,4,5,4,4,5,4,4,4,4]}


# `gunicorn performance_test:app -b localhost:8084 -k uvicorn.workers.UvicornWorker --workers 1`

{"delays":[0.0,0.0],"total_time_taken":551,"times_avarage":4.34,"times":[13,6,5,5,5,5,5,4,4,4,5,4,4,4,4,4,5,4,4,5,4,4,5,4,4,4,4,4,5,4,4,4,4,4,5,4,4,4,4,4,4,4,5,4,4,5,4,4,4,4,4,4,4,4,5,4,4,4,4,4,4,4,5,4,4,4,4,4,4,4,4,4,5,4,4,5,4,5,4,4,5,4,4,4,4,5,4,4,5,4,4,4,4,4,4,4,5,4,4,5]}
{"delays":[0.0,0.0],"total_time_taken":558,"times_avarage":4.48,"times":[14,7,5,5,5,5,5,5,4,4,4,4,4,4,5,5,4,4,4,4,5,4,4,4,5,5,4,4,4,5,5,4,4,4,5,4,4,4,5,5,4,4,4,4,5,5,4,4,5,5,4,4,5,5,4,4,4,5,4,4,5,4,4,5,5,4,4,4,5,4,4,4,5,4,4,4,5,4,5,4,4,4,5,4,4,4,5,4,4,4,5,4,4,4,5,4,4,4,5,4]}
{"delays":[0.0,0.0],"total_time_taken":550,"times_avarage":4.34,"times":[15,6,5,4,4,4,4,4,4,5,4,4,4,4,4,5,4,4,5,4,4,5,4,4,4,4,4,5,4,4,4,4,5,5,4,4,4,4,5,4,4,4,4,4,5,4,4,5,4,4,5,4,4,5,4,4,5,4,4,5,4,4,4,4,4,4,5,4,4,5,4,4,4,4,4,4,4,4,4,5,4,4,5,4,4,4,4,4,4,4,4,5,4,4,5,4,4,4,4,4]}

Примеры непервых запусков с комментариями await asyncio.sleep(delay1) (3 попытки):

# `uvicorn performance_test:app --port 8083`

{"delays":[0.0,0.0],"total_time_taken":159,"times_avarage":0.6,"times":[3,1,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0]}
{"delays":[0.0,0.0],"total_time_taken":162,"times_avarage":0.49,"times":[3,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,1,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1]}
{"delays":[0.0,0.0],"total_time_taken":156,"times_avarage":0.61,"times":[3,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1]}


# `gunicorn performance_test:app -b localhost:8084 -k uvicorn.workers.UvicornWorker --workers 1`

{"delays":[0.0,0.0],"total_time_taken":159,"times_avarage":0.59,"times":[2,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0]}
{"delays":[0.0,0.0],"total_time_taken":165,"times_avarage":0.62,"times":[3,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1]}
{"delays":[0.0,0.0],"total_time_taken":164,"times_avarage":0.54,"times":[2,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1]}

Я сделал скрипт Python, чтобы более точно измерить те времена:

import statistics
import requests
from time import sleep

number_of_tests=1000

sites_to_test=[
    {
        'name':'only uvicorn    ',
        'url':'http://127.0.0.1:8083/delay/0.0/0.0'
    },
    {
        'name':'gunicorn+uvicorn',
        'url':'http://127.0.0.1:8084/delay/0.0/0.0'
    }]


for test in sites_to_test:

    total_time_taken_list=[]
    times_avarage_list=[]

    requests.get(test['url']) # first request may be slower, so better to not measure it

    for a in range(number_of_tests):
        r = requests.get(test['url'])
        json= r.json()

        total_time_taken_list.append(json['total_time_taken'])
        times_avarage_list.append(json['times_avarage'])
        # sleep(1) # results are slightly different with sleep between requests

    total_time_taken_avarage=statistics.mean(total_time_taken_list)
    times_avarage_avarage=statistics.mean(times_avarage_list)

    print({'name':test['name'], 'number_of_tests':number_of_tests, 'total_time_taken_avarage':total_time_taken_avarage, 'times_avarage_avarage':times_avarage_avarage})

Полученные результаты:

{'name': 'only uvicorn    ', 'number_of_tests': 2000, 'total_time_taken_avarage': 586.5985, 'times_avarage_avarage': 4.820865}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 2000, 'total_time_taken_avarage': 571.8415, 'times_avarage_avarage': 4.719035}

Результаты с комментариями await asyncio.sleep(delay1)

{'name': 'only uvicorn    ', 'number_of_tests': 2000, 'total_time_taken_avarage': 151.301, 'times_avarage_avarage': 0.602495}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 2000, 'total_time_taken_avarage': 144.4655, 'times_avarage_avarage': 0.59196}

Я также сделал другую версию вышеупомянутого скрипта, который меняет URL-адреса каждый 1 запрос (это дает немного большее время):

import statistics
import requests
from time import sleep

number_of_tests=1000

sites_to_test=[
    {
        'name':'only uvicorn    ',
        'url':'http://127.0.0.1:8083/delay/0.0/0.0',
        'total_time_taken_list':[],
        'times_avarage_list':[]
    },
    {
        'name':'gunicorn+uvicorn',
        'url':'http://127.0.0.1:8084/delay/0.0/0.0',
        'total_time_taken_list':[],
        'times_avarage_list':[]
    }]


for test in sites_to_test:
    requests.get(test['url']) # first request may be slower, so better to not measure it

for a in range(number_of_tests):

    for test in sites_to_test:
        r = requests.get(test['url'])
        json= r.json()

        test['total_time_taken_list'].append(json['total_time_taken'])
        test['times_avarage_list'].append(json['times_avarage'])
        # sleep(1) # results are slightly different with sleep between requests


for test in sites_to_test:
    total_time_taken_avarage=statistics.mean(test['total_time_taken_list'])
    times_avarage_avarage=statistics.mean(test['times_avarage_list'])

    print({'name':test['name'], 'number_of_tests':number_of_tests, 'total_time_taken_avarage':total_time_taken_avarage, 'times_avarage_avarage':times_avarage_avarage})

Полученные результаты:

{'name': 'only uvicorn    ', 'number_of_tests': 2000, 'total_time_taken_avarage': 589.4315, 'times_avarage_avarage': 4.789385}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 2000, 'total_time_taken_avarage': 589.0915, 'times_avarage_avarage': 4.761095}

Результаты с прокомментированными await asyncio.sleep(delay1)

{'name': 'only uvicorn    ', 'number_of_tests': 2000, 'total_time_taken_avarage': 152.8365, 'times_avarage_avarage': 0.59173}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 2000, 'total_time_taken_avarage': 154.4525, 'times_avarage_avarage': 0.59768}

Этот ответ должен помочь вам лучше отладить ваши результаты.

Я думаю, что будет полезно изучить ваши результаты, если вы поделитесь более подробной информацией о своей ОС / машине.

Также перезагрузите компьютер / сервер, это может повлиять.


Обновление 1:

Я вижу, что я использовал более новую версию uvicorn 0.14.0, чем указано в вопросе 0.13.4. Я также тестировал более старую версию 0.13.4, но результаты аналогичны, я все еще не могу воспроизвести ваши результаты.


Обновление 2:

Я провел еще несколько тестов и заметил кое-что интересное:

с uvloop в файле requirements.txt:

весь файл requirements.txt:

uvicorn==0.14.0
fastapi==0.65.1
gunicorn==20.1.0
uvloop==0.15.2

Полученные результаты:

{'name': 'only uvicorn    ', 'number_of_tests': 500, 'total_time_taken_avarage': 362.038, 'times_avarage_avarage': 2.54142}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 500, 'total_time_taken_avarage': 366.814, 'times_avarage_avarage': 2.56766}

без uvloop в файле requirements.txt:

весь файл requirements.txt:

uvicorn==0.14.0
fastapi==0.65.1
gunicorn==20.1.0

Полученные результаты:

{'name': 'only uvicorn    ', 'number_of_tests': 500, 'total_time_taken_avarage': 595.578, 'times_avarage_avarage': 4.83828}
{'name': 'gunicorn+uvicorn', 'number_of_tests': 500, 'total_time_taken_avarage': 584.64, 'times_avarage_avarage': 4.7155}

Обновление 3:

В этом ответе я использовал только Python 3.9.5.

person Karol Zlot    schedule 08.06.2021
comment
Спасибо за всестороннее тестирование! Моя ОС / Машина уже была спрятана где-то в моем длинном вопросе. Я выполнил эти тесты, используя Python 3.8.2 в OS X 11.2.3 с процессором Intel I7. Я посмотрю, смогу ли я также запустить несколько тестов на простой машине Ubuntu. Также спасибо, что указали, что простая установка uvloop дает значительный прирост производительности! - person M.D.; 08.06.2021
comment
@ M.D. Хорошо, я это пропустил. В этом ответе я использовал только Python 3.9.5, так что это также была другая версия, чем ваша. Мой процессор - Ryzen 3700x. - person Karol Zlot; 08.06.2021

Разница связана с используемым вами веб-сервером.

Аналогия может быть такой: two cars, same brand, same options, just a different engine, what's the difference?

Веб-серверы не совсем похожи на машину, но я думаю, вы уловили суть, которую я пытаюсь донести.

По сути, gunicorn - это synchronous веб-сервер, а uvicorn - это asynchronous веб-сервер. Поскольку вы используете ключевые слова fastapi и await, я думаю, вы уже знаете, что такое _8 _ / _ 9_.

Я не знаю различий в коде, поэтому отнеситесь к моему ответу с недоверием, но uvicorn более эффективен из-за asynchronous части. Я предполагаю, что разница во времени заключается в том, что если вы используете async веб-сервер, он уже настроен при запуске для обработки async функций, тогда как если вы используете sync веб-сервер, это не так, и есть какие-то накладные расходы в порядке чтобы абстрагироваться от этой части.

Это неправильный ответ, но он дает понять, в чем может заключаться разница.

person lsabi    schedule 29.05.2021
comment
Спасибо за ответ. Я ценю то, что дал мне немного контекста. И я бы понял, откуда исходит разница во времени, если бы делал тайминг вне вызова функции, например, во внешнем инструменте тестирования stres. Однако весь код синхронизации находится внутри кода get_delay. И даже если я помещу тело функции get_delay в отдельную синхронную функцию (конечно, без asyncio.sleep, потому что теперь она находится в функции, где ожидание недопустимо) и просто async def get_delay(delay1, delay2): return sync_function_call(delay1, delay2), я получаю аналогичные различия во времени. - person M.D.; 29.05.2021
comment
Так что по какой-то причине кажется, что при работе под guvicorn все, что связано с кодом Python, связанным с процессором, будет работать медленнее. То же самое касается кода, привязанного к процессору, в импортированных пакетах python. Единственное объяснение, которое я могу придумать, это то, что, возможно, Gunicorn устанавливает некоторые хуки, которые git запускаются каким-то очень распространенным событием при выполнении чистого кода Python. - person M.D.; 29.05.2021
comment
Это два движка, которые оптимизированы для разных задач. gunicorn был создан с учетом synchronous кода, а uvicorn был создан с учетом asynchronous кода. Кроме того, существует отдаленная вероятность того, что uvicorn выставит uvloop цикл событий вместо встроенного asyncio цикла событий, где первый намного быстрее, чем второй. Хотя я в этом не уверен, но тесты дают хорошие результаты github.com/MagicStack/uvloop - person lsabi; 30.05.2021
comment
Я предлагаю вам не слишком беспокоиться о выступлениях, если они не являются жестким ограничением для вашего проекта. Если доступны ASGI серверы, используйте один из них (это имеет смысл, поскольку вы используете ASGI фреймворк), в противном случае используйте WGSI, например gunicorn. Первый оптимизирован для выполнения asynchronous функций в fastapi, второй - нет. - person lsabi; 30.05.2021

Поскольку fastapi является ASGI фреймворком, он обеспечит лучшую производительность с ASGI сервером, например uvicorn или hypercorn. WSGI такой сервер, как gunicorn, не сможет обеспечить производительность, подобную uvicorn. ASGI серверы оптимизированы для asynchronous функций. Официальные документы fastapi также рекомендуют использовать ASGI серверы, такие как uvicorn или hypercorn.

https://fastapi.tiangolo.com/#installation

person Hasan Khan    schedule 08.06.2021
comment
Учтите, что gunicorn можно использовать с uvicorn, чтобы использовать преимущества нескольких ядер / ЦП. - person Karol Zlot; 08.06.2021