Обновить

Комментарии 17

1) Я думал, что писать вычислительный код, как в примере, на самом Питоне (без библиотек) - это моветон. Питон же медленный. Прав ли я?

2) Есть ли библиотеки (Polars?), которые внутри себя организуют параллельные вычисления, не прибегая к каким-либо механизмам самого Питона? Если есть, почему ничего о них не написано?

3) Есть ли сравнения производительности (по максимальному rps, напрмер) для типичного web-сервиса на Python с Java или C#, где надо обратиться к REST, к БД, что-то совсем простенькое посчитать и записать или отдать результат в БД или по REST? Я к тому, что верно ли утверждение, что писать что-то для high load на Питоне - это плохая идея?

Ответила вам. Коммент ниже

Приветствую.

1. Для тяжёлых вычислений Python довольно медленный и ограничен GIL. Для простых расчётов, I/O-задач, бизнес-логики и «склейки» компонентов он используется повсеместно. И на практике Python редко считает сам — тяжёлую работу за него делают библиотеки на C или Rust.

2. Да, конечно есть. NumPy, Polars, PyTorch и другие считают в нативном коде, параллельно и без GIL. Python там обеспечивает только интерфейс. В статье это не подчеркнуто, потому что для экосистемы Python это уже стандартная модель.

3. Здесь распишу подробнее. По rps:

·        Java / C# в среднем дают больше RPS на одном процессе

·        Python даёт меньше RPS на процесс, но разница обычно не в разы, а в процентах

Потому что в таком сервисе:

·        70–90% времени уходит на ожидание сети и БД

·        Python в это время просто ждёт, а не «медленно считает»

Например, запрос в БД — 5–20 мс, HTTP-вызов — 10–50 мс, Python-логика — микросекунды. То есть, даже если Python в 2 раза медленнее Java на вычислениях —
на фоне сетевых задержек это почти незаметно.

Python используют под high load и весьма успешно. Так как асинхронные серверы (FastAPI, aiohttp) держат десятки тысяч соединений, сервисы масштабируют горизонтально (добавляют инстансы), а тяжёлые вычисления выносятся в отдельные воркеры или нативные библиотеки. В целом, high load решается архитектурой, а не языком.

Но, если у вас в каждом запросе много CPU-вычислений, нельзя масштабироваться горизонтально и нужно максимум RPS с одного процесса любой ценой, то лучше выбрать Java, C# или Go.

Какую нагрузку ваш сервис на Python будет держать, зависит как минимум от реализации сервера: одно и то же FastAPI приложение можно "поднять" на uvicorn (дефолт) или granian (написан на rust) - rps и latency будут сильно отличаться.

Далее, если Вы используйте классическую связку FastAPI + Pydantic, то будьте готовы, что Вам придется "платить" за сериализацию и валидацию (да, Pydantic был переписан на rust в свое время, но ему это не особо помогло). Как альтернатива пайдентику - msgspec (написан на С, очень быстрый)

И так, по тихой грусти, можно придти к тому, что библиотеки на других ЯП под Python - это не только про математику и тяжеловесные CPU-bound вычисления (numpy, polars и т.д.) - это вообще про все, что "вокруг" среднестатистического Python веб-сервиса.
И тогда логичный вопрос: а что у нас от самого пайтона то осталось, кроме синтаксических конструкций?

Вы абсолютно правы: современный высокопроизводительный Python всё больше напоминает "клей" для эффективных библиотек, написанных на C, Rust или C++. Использование Granian вместо Uvicorn или msgspec вместо Pydantic — это отличные примеры того, как разработчики стараются обойти врожденную медлительность интерпретатора.

Что же остается от самого Python? Пожалуй, самое ценное — скорость разработки и экосистема. Python стал универсальным интерфейсом: он позволяет строить сложную логику на простом и читаемом языке, делегируя "черную работу" низкоуровневым движкам. В итоге мы получаем лучшее из двух миров: комфорт написания кода и производительность, близкую к нативной. Да, от чистого Python остаются в основном "синтаксические конструкции", но именно они делают разработку доступной и быстрой

В нашей команде давеча написали веб-сервис на пайтоне, а потом еще месяца 2-3 его оптимизировали: затащили polars вместо pandas, переписали «драйверы» с использованием cython, перешли с uvicorn на granian, а после и вовсе - с http на gRPC, добавили dramatiq, чтобы эффективно утилизировать все ядра cpu на подах… А сейчас этот сервис в стадии активного переписывания на go :D

А что лучше для планировщика, который бы гарантированно "тикал" каждую минуту, и при совпадении времени выполнял воспроизведение аудио файла? Т. Е. Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика.

Вам подойдет asyncio. Он позволяет реализовать «кооперативную многозадачность»: вычислять точное время до следующего тика, пока звук проигрывается в фоне, не блокируя основной цикл.

Спасибо, к сожалению не имею возможность поставить + из-за кармы.

Для точных кроновых задач («точно каждую минуту 0 секунд») asyncio плохо подходит, да и Python в целом

Согласна, Python — это не система реального времени. Если процессор «ляжет», опоздает любой скрипт. Но для аудио это не критично. Если нужен именно Python, я бы выбрала asyncio. Он не «плывет», так как мы считаем время до :00 на каждом шаге, а не просто ждет по 60 секунд. Так, например:

while True:

now = datetime.datetime.now()

wait = 60 - now.second - now.microsecond/1e6

await asyncio.sleep(wait)

asyncio.create_task(play_audio())


в приведенном Вами примере кода нигде не гарантируется, что play_audio будет выполняться строго по таймеру:

  • asyncio.create_task создает задачу и помещает ее в ready_to_run очередь event loop'а, но не запускает ее непосредственно (на самой 1-й итерации sleep будет "холостой")

  • так как у нас (в целом) 1 поток + кооперативная многозадачность, play_audio (по каким либо причинам) может банально не отдавать управление назад в event loop какое-то длительное время (скажем, wait*2), и фьюча, порожденная вызовом asyncio.sleep, в этом случае не будет опрошена ивент-лупом «вовремя» (через wait секунд от момента добавления ее в очередь)

Тут банально нет какого-то механизма прерывания (как в выталкивающей модели многозадачности), и это, в глобальном смысле, проблема асинхронной модели в Python: кооперативная многозадачность на 1-м потоке.
И когда при помощи Python + asyncio нужно реализовать что-то чуть более сложное, чем сделать N запросов по сети конкурентно - начинаются танцы с бубнами, придумывание воркэраундов и т.д.

Если asyncio не подходит из-за рисков блокировки событийного цикла, можно же попробовать multiprocessing. Вынос воспроизведения в отдельный независимый процесс гарантирует, что планировщик не будет зависеть от длительности или ошибок выполнения задачи. Достаточно реализовать цикл, который динамически рассчитывает время до начала следующей минуты и запускает новый процесс ровно в «ноль секунд».

А multiprocessing - это overhead на переключение процессов...

В общем, это уже немного в сторону, но хотелось бы видеть (в обозримом будущем) какую-то более совершенную реализацию асинхронной модели в Python, тем более что в 3.14 GIL выпилили, и это открывает возможность для условного "asyncio V2": с M:N моделью, планировщиком, который будет скедъюдить тысячи async задач на ограниченном (количеством ядер CPU) множестве OS потоков. Эх, прекрасное далёко

asyncio.to_thread не заблокирует событийный цикл. Выкинули в отдельный поток воспроизведение и дальше считаем время. Нет оверхеда на создание процесса

а вообще, посмотрите на код, который привела автор(ка) статьи выше.
to_thread с create_task в принципе не "дружит", а c run_coroutine_threadsafe результат будет такой же, как и без него.

В любом случае, длительность выполнения play_audio влияет на реальное время выполнения sleep -> на время 1 итерации while цикла, т.е. задача, поставленная корневом комментарии - "Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика" - не решена

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации