Python Workers: Быстрый старт, любые пакеты и новый уровень разработки на Cloudflare

В прошлом году мы анонсировали базовую поддержку Python Workers, позволив разработчикам на Python развернуть код в регионе: Земля одной командой и воспользоваться преимуществами платформы Workers.

С тех пор мы упорно трудились, чтобы работа с Python на Workers стала отличной. Мы сосредоточились на добавлении поддержки пакетов на платформу, и теперь это реальность — с исключительно быстрыми холодными стартами и разработкой, ориентированной на Python.

Это означает изменение в способе включения пакетов в Python Worker. Вместо предоставления ограниченного набора встроенных пакетов мы теперь поддерживаем любой пакет, совместимый с Pyodide, средой выполнения WebAssembly, которая является основой Python Workers. Это включает все чистые Python-пакеты, а также многие пакеты, зависящие от динамических библиотек. Мы также создали инструменты вокруг uv, чтобы упростить установку пакетов.

Мы также реализовали специальные снимки памяти для сокращения времени холодного старта. Эти снимки обеспечивают значительное повышение скорости по сравнению с другими бессерверными платформами для Python. В тестах на холодный старт с использованием распространенных пакетов Cloudflare Workers запускаются более чем в 2,4 раза быстрее, чем AWS Lambda, и в 3 раза быстрее, чем Google Cloud Run.

В этом посте блога мы объясним, что делает Python Workers уникальными, и поделимся некоторыми техническими деталями о том, как мы достигли описанных выше успехов. Но сначала, для тех, кто, возможно, не знаком с Workers или бессерверными платформами — и особенно для тех, кто пришел из мира Python — позвольте рассказать, зачем вообще может понадобиться использовать Workers.

Глобальное развертывание Python за 2 минуты

Часть магии Workers — это простой код и легкие глобальные развертывания. Давайте начнем с демонстрации того, как можно развернуть приложение FastAPI по всему миру с быстрыми холодными стартами менее чем за две минуты.

Простой Worker с использованием FastAPI может быть реализован в несколько строк:

from fastapi import FastAPI
from workers import WorkerEntrypoint
import asgi

app = FastAPI()

@app.get("/")
async def root():
   return {"message": "Это FastAPI на Workers"}

class Default(WorkerEntrypoint):
   async def fetch(self, request):
       return await asgi.fetch(app, request.js_object, self.env)

Чтобы развернуть что-то подобное, просто убедитесь, что у вас установлены uv и npm, а затем выполните следующее:

$ uv tool install workers-py
$ pywrangler init --template 
    https://github.com/cloudflare/python-workers-examples/03-fastapi
$ pywrangler deploy

Всего с небольшим количеством кода и командой pywrangler deploy вы развернули свое приложение в глобальной сети Cloudflare, которая охватывает 330 локаций в 125 странах. Не нужно беспокоиться об инфраструктуре или масштабировании.

И для многих случаев использования Python Workers полностью бесплатны. Наш бесплатный тариф предлагает 100 000 запросов в день и 10 мс процессорного времени на вызов. Для получения дополнительной информации ознакомьтесь с страницей тарифов в нашей документации.

Больше примеров можно найти в репозитории на GitHub. А читайте дальше, чтобы узнать больше о Python Workers.

Так что же можно делать с Python Workers?

Теперь, когда у вас есть Worker, возможно практически все. Вы пишете код, поэтому вам и решать. Ваш Python Worker получает HTTP-запросы и может делать запросы к любому серверу в публичном интернете.

Вы можете настроить триггеры cron, чтобы ваш Worker запускался по регулярному расписанию. Кроме того, если у вас есть более сложные требования, вы можете использовать Workflows для Python Workers или даже долгоживущие WebSocket-серверы и клиенты с помощью Durable Objects.

Вот еще примеры того, что можно делать с использованием Python Workers:

  • Рендерить HTML-шаблоны на граничной сети с помощью библиотеки вроде Jinja, одновременно получая динамический контент напрямую с вашего сервера

  • Изменять ответ от вашего сервера — например, можно динамически внедрять теги OpenGraph в ваш HTML на основе запрашиваемого контента

  • Создать чат-комнату с использованием Durable Objects и WebSockets

  • Потреблять данные из WebSocket-соединений, например, из потока Bluesky

  • Генерировать изображения с помощью Python-пакета Pillow

  • Написать небольшой Python Worker, который предоставляет API Python-пакета, и затем обращаться к нему из вашего JavaScript Worker с помощью RPC

Более быстрые холодные старты с пакетами, чем у Lambda и Cloud Run

Бессерверные платформы, такие как Workers, экономят ваши деньги, запуская ваш код только тогда, когда это необходимо. Это означает, что если ваш Worker не получает запросов, он может быть остановлен и потребует перезапуска при поступлении нового запроса. Обычно это влечет за собой накладные расходы на ресурсы, которые мы называем «холодным стартом». Важно сводить их к минимуму, чтобы уменьшить задержки для конечных пользователей.

В стандартном Python запуск среды выполнения дорог, и наша первоначальная реализация Python Workers была сосредоточена на быстром запуске среды выполнения. Однако мы быстро поняли, что этого недостаточно. Даже если среда выполнения Python запускается быстро, в реальных сценариях начальная загрузка обычно включает загрузку модулей из пакетов, и, к сожалению, в Python многие популярные пакеты могут загружаться несколько секунд.

Мы поставили цель сделать холодные старты быстрыми, независимо от того, загружаются ли пакеты.

Чтобы измерить реалистичную производительность холодного старта, мы создали бенчмарк, который импортирует распространенные пакеты, а также бенчмарк, запускающий «hello world» на голой среде выполнения Python. В то время как Lambda способна быстро запустить только среду выполнения, как только требуется импортировать пакеты, время холодного старта резко возрастает.

Вот среднее время холодного старта при загрузке трех распространенных пакетов (httpx, fastapi и pydantic):

Платформа

Среднее время холодного старта (сек.)

Cloudflare Python Workers

1.027

AWS Lambda

2.502

Google Cloud Run

3.069

В этом случае у Cloudflare Python Workers холодные старты в 2,4 раза быстрее, чем у AWS Lambda, и в 3 раза быстрее, чем у Google Cloud Run. Мы достигли этих низких значений времени холодного старта с помощью снимков памяти, и в следующем разделе мы объясним, как мы это сделали.

Мы регулярно проводим эти бенчмарки. Перейдите сюда для получения актуальных данных и дополнительной информации о нашей методике тестирования.

Мы архитектурно отличаемся от этих других платформ — а именно, Workers основаны на изолятах. Поэтому наши цели высоки, и мы планируем будущее с нулевым временем холодного старта.

Инструменты для работы с пакетами, интегрированные с uv

Разнообразная экосистема пакетов — это большая часть того, что делает Python таким замечательным. Именно поэтому мы усердно работали над тем, чтобы использование пакетов в Workers было максимально простым.

Мы поняли, что работа с существующими инструментами Python — это лучший путь к отличному опыту разработки. Поэтому мы выбрали менеджер пакетов и проектов uv, так как он быстрый, зрелый и набирает обороты в экосистеме Python.

Мы создали собственные инструменты вокруг uv под названием pywrangler. Этот инструмент по сути выполняет следующие действия:

  • Читает файл pyproject.toml вашего Worker'а, чтобы определить указанные в нем зависимости

  • Включает ваши зависимости в папку python_modules, которая находится внутри вашего Worker'а

Pywrangler использует uv для установки зависимостей способом, совместимым с Python Workers, и вызывает wrangler при локальной разработке или развертывании Workers. 

Фактически это означает, что вам достаточно выполнить pywrangler dev и pywrangler deploy, чтобы протестировать ваш Worker локально и развернуть его. 

Аннотации типов

Вы можете создавать подсказки типов для всех привязок, определённых в вашем конфиге wrangler, с помощью pywrangler types. Эти подсказки типов будут работать с Pylance или с последними версиями mypy.

Для генерации типов мы используем wrangler types, чтобы создать подсказки типов TypeScript, затем используем компилятор TypeScript для создания абстрактного синтаксического дерева типов. Наконец, мы используем подсказки TypeScript — например, имеет ли JS-объект поле-итератор — чтобы сгенерировать подсказки типов mypy, которые работают с интерфейсом внешних функций Pyodide.

Сокращение времени холодного старта с использованием снапшотов

Запуск Python, как правило, довольно медленный, и импорт модуля Python может вызвать большой объём работы. Мы избегаем запуска Python во время холодного старта, используя снапшоты памяти.

Когда Воркер развёртывается, мы выполняем код верхнего уровня Воркера, затем делаем снапшот памяти и сохраняем его вместе с вашим Воркером. Каждый раз, когда мы запускаем новый изолят для Воркера, мы восстанавливаем снапшот памяти, и Воркер готов обрабатывать запросы без необходимости выполнять какой-либо Python-код для подготовки. Это значительно сокращает время холодного старта. Например, запуск Воркера, который импортирует fastapi, httpx и pydantic без снапшотов, занимает около 10 секунд. Со снапшотами — 1 секунду.

Тот факт, что Pyodide построен на WebAssembly, делает это возможным. Мы можем легко захватить всю линейную память рантайма и восстановить её.

Снапшоты памяти и энтропия

Для безопасности рантаймам WebAssembly не требуются такие функции, как рандомизация расположения адресного пространства (ASLR), поэтому большинство сложностей со снапшотами памяти в современной операционной системе не возникает. Как и со снапшотами нативной памяти, нам всё равно нужно тщательно обрабатывать энтропию при запуске, чтобы избежать использования генератора случайных чисел от XKCD (мы очень серьёзно относимся к настоящей случайности).

Python Workers redux: fast cold starts, packages, and a uv-first workflow

Создавая снапшот памяти, мы можем по неосторожности зафиксировать начальное значение (seed) для генерации случайных чисел. В этом случае последующие вызовы для получения «случайных» чисел будут постоянно возвращать одну и ту же последовательность значений для множества запросов.

Избежать этого особенно сложно, потому что Python использует много энтропии при запуске. Это включает функции libc getentropy() и getrandom(), а также чтение из /dev/random и /dev/urandom. Все эти функции используют одну и ту же реализацию через JavaScript-функцию crypto.getRandomValues().

В Cloudflare Workers функция crypto.getRandomValues() всегда была отключена при запуске, чтобы позволить нам в будущем перейти на использование снапшотов памяти. К сожалению, интерпретатор Python не может загрузиться, не вызывая эту функцию. И многим пакетам также требуется энтропия во время запуска. Энтропия используется, по сути, для двух целей:

  • Семена хеширования для рандомизации хешей

  • Семена для генераторов псевдослучайных чисел

Рандомизацию хешей мы выполняем во время запуска и принимаем тот факт, что у каждого конкретного Воркера фиксированное семя хеширования. В Python нет механизма для замены семени хеширования после запуска.

Для генераторов псевдослучайных чисел (ГПСЧ) мы применяем следующий подход:

Во время развёртывания:

  1. Инициализируем ГПСЧ фиксированным «отравленным семенем», затем записываем состояние ГПСЧ.

  2. Заменяем все API, которые обращаются к ГПСЧ, на обёртку, которая приводит к ошибке пользователя при развёртывании.

  3. Выполняем код верхнего уровня пользователя.

  4. Захватываем снапшот.

Во время выполнения:

  1. Проверяем, что состояние ГПСЧ не изменилось. Если оно изменилось, значит, мы забыли добавить обёртку для какого-то метода. Завершаем развёртывание с внутренней ошибкой.

  2. После восстановления снапшота повторно инициализируем генератор случайных чисел перед выполнением любых обработчиков.

Это гарантирует, что ГПСЧ можно использовать во время работы Воркера, но предотвращает их использование во время инициализации и до создания снапшота.

Снапшоты памяти и состояние WebAssembly

При создании снапшотов памяти в WebAssembly возникает дополнительная сложность: сохраняемый нами снапшот памяти состоит только из линейной памяти WebAssembly, но полное состояние экземпляра WebAssembly Pyodide не содержится в линейной памяти.

Есть две таблицы за пределами этой памяти.

Одна таблица хранит значения указателей на функции. Традиционные компьютеры используют архитектуру «фон Неймана», что означает, что код существует в том же адресном пространстве, что и данные, поэтому вызов указателя функции — это переход по некоторому адресу памяти. WebAssembly имеет «гарвардскую архитектуру», где код находится в отдельном адресном пространстве. Это ключ к большинству гарантий безопасности WebAssembly и, в частности, причина, по которой WebAssembly не нуждается в ASLR. Указатель на функцию в WebAssembly — это индекс в таблице указателей функций.

Вторая таблица хранит все объекты JavaScript, на которые есть ссылки из Python. Объекты JavaScript нельзя напрямую хранить в памяти, потому что виртуальная машина JavaScript запрещает прямое получение указателя на объект JavaScript. Вместо этого они помещаются в таблицу и представляются в WebAssembly как индекс в этой таблице.

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

Таблица указателей функций всегда находится в одном и том же состоянии при инициализации экземпляра WebAssembly и обновляется динамическим загрузчиком при загрузке динамических библиотек — нативных пакетов Python, таких как numpy.

Чтобы обработать динамическую загрузку:

  1. При создании снапшота мы модифицируем загрузчик, чтобы записывать порядок загрузки динамических библиотек, адрес в памяти, по которому размещены метаданные каждой библиотеки, и базовый адрес таблицы указателей функций для релокаций.

  2. При восстановлении снапшота мы перезагружаем динамические библиотеки в том же порядке и используем модифицированный аллокатор памяти, чтобы разместить метаданные по тем же адресам. Мы проверяем, что текущий размер таблицы указателей функций совпадает с записанным базовым адресом таблицы для динамической библиотеки.

Всё это гарантирует, что каждый указатель функции имеет то же значение после восстановления снапшота, что и во время его создания.

Для обработки ссылок на JavaScript мы реализовали довольно ограниченную систему. Если к объекту JavaScript можно получить доступ из globalThis через серию обращений к свойствам, мы записываем эти обращения и воспроизводим их при восстановлении снапшота. Если существует любая ссылка на объект JavaScript, недоступная таким образом, развёртывание Воркера завершается ошибкой. Этого достаточно для работы со всеми существующими пакетами Python с поддержкой Pyodide, которые делают импорт на верхнем уровне, например:

from js import fetch

Снижение частоты холодных стартов с помощью шардирования

Ещё одна важная особенность нашей стратегии производительности для Python Workers — это шардирование. Очень подробное описание процесса его реализации можно найти здесь. Вкратце: теперь мы направляем запросы на существующие экземпляры Воркеров, тогда как раньше могли бы запустить новый экземпляр.

Шардирование было фактически сначала включено для Python Workers и стало отличной испытательной площадкой для него. Холодный старт в Python намного дороже, чем в JavaScript, поэтому особенно важно гарантировать, что запросы направляются на уже работающий изолят.

Что дальше?

Это только начало. У нас много планов по улучшению Python Workers:

  • Более удобные для разработчиков инструменты

  • Ещё более быстрые холодные старты за счёт использования нашей архитектуры изолятов

  • Поддержка большего количества пакетов

  • Поддержка нативных TCP-сокетов, нативных WebSockets и больше привязок