Комментарии 51
При этом выбран один процесс Uvicorn, без Gunicorn и множества воркеров, чтобы условия для обоих серверов были сопоставимыми.
А сколько был GOMAXPROCS? Какую нагрузку на CPU давал каждый сервер?
Как и обычно, в таких "тестах", ничего общего с практикой у бенчмарков. В этом нет никакого смысла — статья, ради статьи. Тем более, что уже давно есть ресурс https://www.techempower.com/benchmarks/, где бенчмарки не пустые.
протестировать базовую производительность HTTP-стека и асинхронности, без влияния базы данных, кэшей или бизнес-логики.
Это упрощенный эксперимент для иллюстрации разницы в производительности чистого HTTP стека go и python. Цель показать цифры под контролируемой нагрузкой, а не соревноваться с комплексными бенчмарками вроде techempower, где учитываются базы данных, кэши, разные фреймворки и реальные сценарии
если эксперимент упрощённый, то почему вы делаете в нем какие-то выводы? и какие были гипотезы у эксперимента?
В go здесь чистый http стек, а в python фреймворк + asgi + uvicorn. Чистый стек был бы, если бы вы использовали библиотеку http.
Опять же, ничего не имею против синтетических тестов, но непонятна логика, по которой исходя из результатов синтетического бенчмарка сделаны выводы о том, что подходит для "маленьких и средних проектов", а что для "больших" (кстати, где граница между средними и большими?), ведь результаты в реальных условиях могут оказаться совсем другими и будут зависеть от специфики конкретного проекта, вне зависимости от его "размера".
Бенчмарк с помощью инструмента, написанного на чистом Python? Вы там лол совсем?
кто-то сомневалмя что хомячка съедят
Технически Locust на Python может упираться в CPU и память, но при 1000 пользователей он работает эффективно, и тормоза одинаково сказываются на тестах Go и Python, так что сравнение остается корректным.
Очевидно же Locust здесь источник идентичной нагрузки для обеих систем. Это как критиковать весы за то, что они сделаны из пластика при измерении веса гири и пера. лол здесь только @andreymal
Почему выбран го 1.25.5 (выпущен 3 недели назад), но питон двухлетней давности?
результаты теста отражают реальные различия Go vs Python, а не версий, хотя небольшие улучшения производительности в последнем Go-релизе могут слегка увеличить отрыв.
В смысле, вы предлагаете взять го ещё посвежее? Не бывает различий языков, бывают различия реализаций - собственно, вы и компилятор питона не указали. Чистый питон на pypi, например, был бы на порядок шустрее cpython. А в (с)питоне 3.14 навалили гору оптимизаций, которую вы почему-то решили проигнорировать.
Было бы интересно сравнить с производительностью чистого nginx на той же машине
Немного странно, что питон так несправедливо занизили. Было бы интересно увидеть: последнюю версию без gil + стандартная библиотека + async + возвращать также байты. А ещё можно добавить компиляцию или оптимизированную сборку питона.
Без gil было бы медленнее, это же однопоточный код, из-за асинка
Такой тест дал бы более «максимальный» результат для Python, но тогда уже сравнивать чистый язык с Go стало бы сложнее
Что странно? Даже если взять последнюю версию Пайтон, включить оптимизированную сборку, использовать асинк с возвратом байтов, то это помогло бы лишь частично догнать го по RPS. Тут очевидно, что чистая конкурентность и использование всех ядер всё равно будут сильнее в пользу гошки, потому что его рантайм изначально спроектирован под высокую многопоточность
Небось Go расползся по всем ядрам, а Питон грел только одно...
Наверняка. Но тогда непонятно почему разница такая маленькая.
Вот этот Locust, как он, интересно, оценивает предел нагрузочной способности? Раз отказов не было, он как-то понимает, в какой момент надо остановиться и перестать повышать нагрузку. Интересно, по каким критериям он это делает?
И другой вопрос. Посылать и принимать HTTP-запросы - работа сравнимая. Locust ведь тоже мог упереться в пределы своей производительности. Благо что он написан на Питоне, исполняется на той же машине и тоже, вероятно, в одно ядро. Этот вариант в процессе тестирования не исключен.
В общем, к проведённому исследованию возникает много методологических вопросов...
Locust просто шлет запросы согласно заданным параметрам и собирает статистику. Если отказов нет, он не останавливается, а просто фиксирует, сколько запросов удалось обработать за время теста. Да, Locust может упереться в свои пределы (CPU/IO), особенно на Python и одном ядре, и это влияет на результаты, но в нашем сценарии мы использовали одинаковые условия для обоих серверов, так что сравнение, очевидно, остается корректным для относительной производительности.
суть - показать различия в runtime и модели конкурентности.
Очень странное сравнение:
Почему-то в качестве тестируемой OS выбран Windows. Я очень давно не видел Windows на серверах, а от операционной системы зависит очень много;
Тестирующий софт запущен на той же машине. При этом он может требовать ресурсов больше, чем тестируемое приложение;
Go-приложение по-умолчанию будет пытаться утилизировать все ядра, про Python я такого сказать не могу.
Да, на Windows результат будет немного отличаться, особенно для Go, но и цель статьи показать относительную разницу в чистом бенчмарке, а не дать цифры для продакшен-сервера.
При Locust на той же машине часть ресурсов CPU и памяти уходит на генерацию трафика, а не только на обработку запросов сервером, но зато условия одинаковые для обоих серверов, так что сравнительная оценка корректна.
Выбор
net/httpвместо популярных фреймворков вроде Gin или Echo обусловлен именно этим: эксперимент должен показать возможности языка, а не фреймворка.
Для Python был выбран FastAPI вместе с Uvicorn. FastAPI - современный web-фреймворк
Ну справедливое сравнение, что уж :) Тогда и для Go брали бы какой-нибудь fasthttp, что ли.
Для реального теста трафик лучше слать не loopback (localhost), а реально в другой машины. Так, у этого теста не очень много с реальностью. Ну да, Go в целом быстрее, чем питон.
Мы с коллегами однажды nodejs и Go сравнивали - примерно также, как в этой статье. На loopback Go был значительно быстрее. Но с реальным трафиком разница не была такой высокой и большинство выгод от Go терялись с сети.
Справедливое замечание, и я с ним в целом согласен.
Loopback-тест действительно убирает сетевые задержки и смещает фокус на производительность runtime и HTTP-стека. В этом и была цель эксперимента - посмотреть поведение серверов под высокой конкурентной нагрузкой, без влияния сети.
При тестировании между машинами часть разницы действительно съедается сетью, особенно для простых эндпоинтов, и относительный выигрыш Go становится менее заметным. Но это уже другой класс эксперимента.
В реальных системах обычно присутствуют оба фактора: и сетевые задержки, и нагрузка на сервер. Поэтому loopback тест не претендует на полную модель, но хорошо показывает пределы и особенности runtime под нагрузкой.
Отдельно стоит отметить, что даже если сеть нивелирует часть разницы, стабильность на верхних процентилях (98–99%) всё равно остаётся важной, особенно при росте нагрузки.
Другая машина мастхэв, да. Во-первых это реальность, как реализация отрабатывает полноценные сетевые соединения, во-вторых, кол-во открытых файловых дескрипторов на 1 машине не бесконечно.
Можно ли докрутить такие тесты , чтобы процесс зашел в OOM? Интересно было бы посмотреть , кто будет первый .
В описанном случае GIL никак не влияет на скорость ответа, так как запускается один поток, а вот Go может их плодить. Теперь вопрос: насколько справедливо сравнение?
сравнение справедливое в рамках заявленных условий. Go плодит потоки не потому что мы ему помогли, а потому что так устроен runtime.Python требует явного масштабирования по процессам и это уже другая архитектура, потребление памяти, операционные компромиссы.
Вопрос: Почему верхние percentiles у FastAPI такие высокие (например, 98-й — 2400 мс)?Ответ: В Python есть GIL
Вот это можно поправить в статье, в данном контексте неверно.
И все таки интересно было бы посмотреть на более равные условия при runtime.GOMAXPROCS(1)
Эластичность не проверили, циклическая нагрузка нужна.
Ну и 30 сек экстремально мало. Я провожу нагрузочные тесты 5..6..7 часов и часто проседание ловится только по истечении 3..4 часов. А тут получился этакий тест дидос фильтра, который должен сработать при обнаружении атаки на уровне приложения и дать команду прокси серверу начать защищаться.
По результатам тестов какие-то запросы выполнялись аж 21 секунду. В обоих случаях. И весьма интересно, что именно их настолько задержало. Причём нельзя исключить, что проблемы производительности стали возникать на уровне клиента.
Так что даже сложно сказать, что тестировалось - ПО сервера, клиента или самой ОС по распределению нагрузки между ними.
Кто то мне разжует о каких практичечких выводах может идти реч в тестах hello world?
Всё хорошо, но чтобы результаты были статистически значимыми, лучше было бы провести этот тест несколько раз, чтобы исключить выбросы или аномалии.
Фраза FastAPI достаточно быстр для большинства задач теперь имеет цифры. Было интересно узнать, для каких задач FastAPI справится с запасом, а для чего - однозначно Go.
Почему никто не заметил, что сервера возвращают разные по длине ответы?
Go-Server 9 байт
Python-Server 13 байт, на 44% больше!
А как себя поведут такие тесты если эти сервера будут находиться за реверс прокси? (npm/caddy/traefik / etc) . Ведь именно так будет работать реальный сервис. А так тест ради теста. Без бизнес логики и мидлвари. Тест нагрузки хеловорд...
Даёшь тест с uvloop ;) ? думаю должно быть получше и да возвращать лучше типа ok обоим серверам. И fastapi это фрамворк. Надо тестить Starlette. Мне кажется даже разница будет если туда же aiohttp вкорчить. А лучше вообще выкинуть все и тестить только asyncio.
import asyncio
async def handle_client(reader, writer):
# Читаем заголовки запроса (не парсим глубоко для простоты)
await reader.read(1024)
# Минимально необходимый HTTP-ответ
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 2\r\n"
"Connection: close\r\n"
"\r\n"
"OK"
)
writer.write(response.encode('utf-8'))
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle_client, '127.0.0.1', 8080)
print("Сервер запущен на http://127.0.0.1:8080")
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Go (...) Эффективнее расходует серверные ресурсы
Но ведь из таблиц этот вывод нельзя никак сделать! Мы не знаем ничего о использовании памяти и ядер процессора.

Python (FastAPI) vs Go: нагрузочный тест и анализ производительности