Будущее веб-сервисов Python, похоже, не зависит от GIL
25 октября 2025 г.
В начале месяца был выпущен Python 3.14. Этот релиз был особенно интересен для меня из-за улучшений в "free-threaded" варианте интерпретатора.
В частности, двумя основными изменениями по сравнению со свободнопоточным вариантом Python 3.13 являются:
Поддержка свободных потоков теперь достигла стадии II, что означает, что она больше не считается экспериментальной
Реализация теперь завершена, а это означает, что обходные пути, введенные в Python 3.13 для придания коду звучания без GIL, теперь отсутствуют, и в свободнопоточной реализации теперь используется адаптивный интерпретатор в качестве варианта с поддержкой GIL. Эти факты, а также дополнительные оптимизации значительно уменьшают снижение производительности, увеличивая разницу с 35% до 5-10%.
Мигель Гринберг написал замечательную статью о производительности Python 3.14, в которой есть разделы, посвященные свободнопоточному варианту по сравнению с GIL. Его результаты показывают значительное улучшение производительности Python 3.14t по сравнению с 3.13t, что не может не радовать!
В то время как его тесты сосредоточены на работе с процессором, вычислении последовательности Фибоначчи и использовании алгоритма пузырьковой сортировки, огромная часть моего опыта работы с Python сосредоточена на веб-разработке – в конце концов, основными проектами OSS, которые я поддерживаю, являются веб–фреймворк и веб-сервер для Python, - и поэтому я хотел провести правильное сравнение интерпретаторов Python со свободным потоком и GIL в веб-приложениях: даже если 99,9999% существующих веб–сервисов связаны с вводом-выводом - взаимодействием с базой данных или отправкой запросов к другим сервисам – параллелизм здесь является ключевым фактором, и мы потратили десятилетия на то, чтобы делать странные вещи с многопроцессорным модулем, чтобы выполнять больше работы параллельно. Может быть, сейчас наконец-то можно перестать тратить гигабайты памяти только на то, чтобы обслуживать более 1 запроса за раз?
Критерии - это сложно
Давайте посмотрим правде в глаза. Нам было нелегко разобраться в тестах, особенно когда они касались веб–технологий. Интернет полон дискуссий вокруг них, люди спорят о каждом их аспекте: методологии, коде, среде. Самые популярные реакции на тесты такие: "но почему вы не протестировали также X" или "мое приложение, использующее эту библиотеку, не масштабируется так, как это, вы лжете". Я уже слышал подобные комментарии к этой статье.
Но это потому, что мы склонны обобщать результаты тестов, а мы на самом деле не должны этого делать. С моей точки зрения, хороший тест - это очень самодостаточный тест, который тестирует очень маленькую вещь вне реального – и гораздо более широкого – контекста. И почему это так? Потому что хороший бенчмарк должен максимально снижать уровень шума. Меня определенно не интересует, работает ли фреймворк X быстрее, чем Y war – еще и потому, что в этих заявлениях обычно не указано, в какой части, – и меня не очень волнует наличие очень широкой матрицы тестовых примеров.
На самом деле я просто хочу посмотреть, сможем ли мы, используя одно веб-приложение ASGI и одно веб-приложение WSGI, выполняющее одно и то же действие, выявить различия между стандартным Python версии 3.14 и его многопоточным вариантом и принять решения, основанные на этих результатах. Пожалуйста, имейте это в виду, когда смотрите на приведенные ниже цифры.
Методология
Как упоминалось ранее, идея состоит в том, чтобы протестировать два основных прикладных протокола на Python – ASGI и WSGI – на Python 3.14 с включенным и отключенным GIL, сохранив все остальное исправленным: сервер, код, параллелизм, цикл обработки событий.
Таким образом, я создал ASGI–приложение, используя FastAPI, и WSGI-приложение, используя Flask - почему именно эти фреймворки? Просто потому, что они самые популярные – с двумя конечными точками: глупым генератором ответов в формате JSON и поддельной конечной точкой с привязкой ввода-вывода. Вот код для версии FastAPI:
import asyncio
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse, JSONResponse
app = FastAPI()
@app.get("/json")
async def json_data():
return JSONResponse({"message": "Hello, world!"})
@app.get("/io")
async def io_fake():
await asyncio.sleep(0.01)
return PlainTextResponse(b"Hello, waited 10ms")
а вот и код для версии Flask:
import json
import time
import flask
app = flask.Flask(__name__)
app.config["JSONIFY_PRETTYPRINT_REGULAR"] = False
@app.route("/json")
def json_data():
return flask.jsonify(message="Hello, world!")
@app.route("/io")
def io_fake():
time.sleep(0.01)
response = flask.make_response(b"Hello, waited 10ms")
response.content_type = "text/plain"
return response
ASGI-бенчмарки
Как видно, фиктивная I/O-точка ожидания имеет задержку в 10 мс - идея в том, чтобы смоделировать ожидание ответа от базы данных. Да, я понимаю, что здесь игнорируется сериализация/десериализация и прочие реальные детали взаимодействия, но это не цель данного теста.
Мы запускаем эти приложения с помощью Granian и создаём нагрузку с помощью rewrk. Почему Granian? Во-первых, я сам его поддерживаю. А во-вторых — это единственный известный мне сервер, который использует потоки вместо процессов для запуска воркеров в режиме free-threaded Python.
Конфигурация стенда
- ОС: Gentoo Linux 6.12.47
- CPU: AMD Ryzen 7 5700X
- Python: CPython 3.14 и 3.14t (установлены через uv)
Команды запуска
granian --interface asgi --loop asyncio --workers {N} impl_fastapi:app rewrk -d 30s -c {CONCURRENCY} --host http://127.0.0.1:8000/{ENDPOINT} JSON endpoint
| Python | workers | RPS | Средняя задержка | Макс. задержка | CPU | RAM |
|---|---|---|---|---|---|---|
| 3.14 | 1 | 30415 | 4.20ms | 45.29ms | 0.42 | 90MB |
| 3.14t | 1 | 24218 | 5.27ms | 59.25ms | 0.80 | 80MB |
| 3.14 | 2 | 59219 | 4.32ms | 70.71ms | 1.47 | 147MB |
| 3.14t | 2 | 48446 | 5.28ms | 68.17ms | 1.73 | 90MB |
Как видно из таблицы, free-threaded реализация примерно на 20% медленнее, но использует меньше памяти.
I/O endpoint
| Python | workers | RPS | Средняя задержка | Макс. задержка | CPU | RAM |
|---|---|---|---|---|---|---|
| 3.14 | 1 | 11333 | 11.28ms | 40.72ms | 0.41 | 90MB |
| 3.14t | 1 | 11351 | 11.26ms | 35.18ms | 0.38 | 81MB |
| 3.14 | 2 | 22775 | 11.22ms | 114.82ms | 0.69 | 148MB |
| 3.14t | 2 | 23473 | 10.89ms | 60.29ms | 1.10 | 91MB |
Здесь результаты почти одинаковы, с лёгким преимуществом у free-threaded реализации, при этом она снова потребляет меньше памяти.
WSGI-бенчмарки
Запуск WSGI-приложения, которое содержит как CPU-нагруженные, так и I/O-нагруженные эндпоинты, сложнее. Почему? Потому что в обычном (с GIL) Python при CPU-нагрузке нужно минимизировать количество потоков, чтобы избежать блокировок GIL, а при I/O-нагрузке, наоборот, увеличить число потоков, чтобы не простаивать во время ожидания операций ввода/вывода.
Влияние количества потоков (Python 3.14 с GIL)
| endpoint | threads | RPS | Средняя задержка | Макс. задержка |
|---|---|---|---|---|
| JSON | 1 | 19377 | 6.60ms | 28.35ms |
| JSON | 8 | 18704 | 6.76ms | 25.82ms |
| JSON | 32 | 18639 | 6.68ms | 33.91ms |
| JSON | 128 | 15547 | 8.17ms | 3949.40ms |
| I/O | 1 | 94 | 1263.59ms | 1357.80ms |
| I/O | 8 | 781 | 161.99ms | 197.73ms |
| I/O | 32 | 3115 | 40.82ms | 120.61ms |
| I/O | 128 | 11271 | 11.28ms | 59.58ms |
Чем больше потоков, тем лучше результаты у I/O, но хуже у JSON. Именно поэтому в WSGI всегда приходилось искать баланс между числом потоков и блокировками GIL.
В free-threaded Python мы можем об этом забыть - каждый поток может реально исполняться параллельно. Но теперь возникает другой вопрос: что масштабировать — workers или threads? В конце концов, воркеры — тоже потоки.
Эксперимент с количеством воркеров и потоков (JSON endpoint)
| workers | threads | RPS | Средняя задержка | Макс. задержка |
|---|---|---|---|---|
| 1 | 2 | 28898 | 4.42ms | 86.96ms |
| 2 | 1 | 28424 | 4.49ms | 75.80ms |
| 1 | 4 | 54669 | 2.33ms | 112.06ms |
| 4 | 1 | 53532 | 2.38ms | 121.91ms |
| 2 | 2 | 55426 | 2.30ms | 124.16ms |
Увеличение количества воркеров вносит некоторый оверхед, поэтому баланс нужно подбирать под конкретную нагрузку. Поскольку для I/O всё равно требуется высокий уровень параллелизма, этот фактор не столь критичен.
Команда запуска Flask
granian --interface wsgi --workers {N} --blocking-threads 64 impl_flask:app rewrk -d 30s -c {CONCURRENCY} --host http://127.0.0.1:8000/{ENDPOINT} JSON endpoint (Flask)
| Python | workers | RPS | Средняя задержка | Макс. задержка | CPU | RAM |
|---|---|---|---|---|---|---|
| 3.14 | 1 | 18773 | 6.11ms | 27446.19ms | 0.53 | 101MB |
| 3.14t | 1 | 70626 | 1.81ms | 311.76ms | 6.50 | 356MB |
| 3.14 | 2 | 36173 | 5.73ms | 27692.21ms | 1.31 | 188MB |
| 3.14t | 2 | 60138 | 4.25ms | 294.55ms | 6.56 | 413MB |
Для CPU-нагруженных сценариев преимущество free-threaded версии очевидно — она задействует больше CPU и показывает значительно выше RPS. Однако её использование памяти заметно больше — возможно, из-за особенностей сборщика мусора.
I/O endpoint (Flask)
| Python | workers | RPS | Средняя задержка | Макс. задержка | CPU | RAM |
|---|---|---|---|---|---|---|
| 3.14 | 1 | 6282 | 20.34ms | 62.28ms | 0.40 | 105MB |
| 3.14t | 1 | 6244 | 20.47ms | 164.59ms | 0.42 | 216MB |
| 3.14 | 2 | 12566 | 20.33ms | 88.34ms | 0.65 | 180MB |
| 3.14t | 2 | 12444 | 20.55ms | 124.06ms | 1.18 | 286MB |
При I/O-нагрузке результаты примерно одинаковые, но free-threaded версия опять же потребляет больше памяти.
Итоговые выводы
Хотя выполнение чисто Python-кода примерно на 20% медленнее на free-threaded Python 3.14, он демонстрирует целый ряд преимуществ:
- ASGI: отсутствие необходимости масштабировать процессы для использования всех ядер CPU - теперь это можно делать потоками с меньшими затратами памяти.
- WSGI: возможность работать без опасений по поводу блокировок GIL и сложных компромиссов между потоками и процессами.
- Упрощение архитектуры: можно перестать использовать monkeypatch (например, через gevent) или планировать миграцию на asyncio ради масштабирования.
Даже если free-threaded Python изначально не создавался с прицелом на веб-приложения, он уже сейчас показывает, что будущее Python-сервисов может быть без GIL - проще, эффективнее и чище в архитектурном плане.
Автор: инженер из компании Sentry.
Оригинал