celld: запуск Cloudflare Workers на своей инфраструктуре

Как это работает

Один узел — это один процесс celld, запущенный на одной машине. Каждый узел встраивает V8 и исполняет бандлы Wrangler. Узлы, работающие с одним общим бакетом, образуют флот; в этом бакете хранятся деплойменты, состояние ячеек и небольшие записи о владении. Условная запись в бакет передаёт узлу владение ячейкой, поэтому в любой момент у ячейки ровно один владелец — никакого протокола членства, никакого детектора сбоев, никакого сервиса консенсуса. Подписанный пиринговый HTTP обеспечивает маршрутизацию и транспорт реплицируемого лога.

celld фиксирует каждую подтверждённую запись SQLite как данные LTX — транзакционный формат, используемый для репликации. Одиночный узел подтверждает устойчивость записи, выгружая эти данные в бакет. Флот из двух и более узлов делает это быстрее: владелец отправляет данные одному или двум другим узлам, и запись считается устойчивой, как только они сохранили её на диск. После этого данные загружаются в бакет. У одиночного узла нет других узлов для отправки, поэтому каждая запись ожидает ответа от бакета и выполняется значительно медленнее. Прежде чем перехват восстановит ячейку, celld извлекает незакрытый лог у предыдущего владельца — так бакет хранит долгосрочное состояние, а узлы остаются взаимозаменяемыми. Полное описание протокола см. в разделе гарантии celld.

Что запускается на celld

celld исполняет программную платформу Workers: среду выполнения и каждую привязку, которую Cloudflare строит поверх Workers и Durable Objects. Пространство имён KV, очередь, база данных D1, Workflow и индекс R2 — каждый из них является ячейкой, а значит, получает те же лиз, ту же репликацию и тот же механизм восстановления после сбоя, что и Durable Object. В таблице ниже каждая строка ссылается на проект, разворачиваемый без каких-либо изменений:

Сервис Пример

Workers: обработчики fetch, сервисные привязки, JS RPC, совместимость с Node.js

hello

Durable Objects: хранилище SQLite, будильники, гибернирующие WebSocket

counter

KV: листинг, метаданные, истечение срока действия, массовый импорт

kv

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

документация

D1: SQL-базы данных, пакетные запросы, миграции

d1

R2: чтение, запись, листинг, составная загрузка

r2

Workflows: устойчивые шаги, паузы, события, приостановка и перезапуск

workflow

Cron Triggers: одно выполнение на каждое событие по всему флоту

cron

Статические ресурсы: только ресурсы или вместе с Worker, _headers, _redirects

документация

Продукты, которым требуется сеть Cloudflare, GPU или ферма браузеров, выходят за рамки возможностей celld. Страница совместимости с Cloudflare перечисляет все пробелы в перечисленных сервисах.

Установка

Установщик загружает бинарный файл celld (подлинность можно проверить командой gh attestation verify):

curl -fsSL https://celld.dev/install.sh | sh

Если установщик попросит, добавьте ~/.local/bin в переменную PATH.

Проекты Workers, разворачиваемые через celld deploy, требуют наличия esbuild в PATH; проектам, состоящим только из статических ресурсов, это не нужно.

Установщик сохраняет каждый релиз в ~/.local/lib/celld/releases и создаёт символическую ссылку на текущий. Чтобы удалить celld, удалите ссылку и папку с релизами:

rm `which celld` && rm -rf ~/.local/lib/celld

Контейнер

Образ релиза содержит бинарный файл celld и публикуется для Linux x86-64 и ARM64:

docker run --rm ghcr.io/denoland/celld --version

Чтобы сохранять локальное состояние среды выполнения и передавать стандартные переменные окружения с учётными данными AWS, выполните:

docker volume create celld-state
docker run --rm --network host \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_SESSION_TOKEN \
  -e CELLD_WATCH=/var/lib/celld/state \
  -v celld-state:/var/lib/celld \
  ghcr.io/denoland/celld \
  --bucket s3://my-cells-bucket \
  --endpoint https://ACCOUNT.r2.cloudflarestorage.com \
  --region auto \
  --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 \
  --advertise node-a.internal:8081

Для AWS S3 уберите --endpoint и --region. Откройте порт 8080 через балансировщик нагрузки, а порт 8081 оставьте в закрытой сети.

Запуск

Запустить приложение локально без облачного бакета:

celld dev

Команда запускает один узел celld и использует локальное объектное хранилище. Docker и облачный бакет не нужны. Worker слушает на http://127.0.0.1:9876. Используйте celld dev --port PORT, чтобы выбрать другой порт, и celld dev --host IP — чтобы указать другой сетевой интерфейс. Не-петлевой IP открывает Worker-слушатель для сети, тогда как внутренний операторский слушатель остаётся на петлевом интерфейсе. Команда сохраняет состояние приложения в .celld/dev, поэтому при повторном запуске используются те же устойчивые данные.

Состояние также переживает изменение конфигурации, но celld не выполняет его миграцию — объект может сохранить значение, которое новая конфигурация отклоняет. Сбой при этом выглядит не связанным с изменением. Используйте celld dev --clean, чтобы удалить .celld/dev перед запуском сервера и начать с чистого локального состояния.

По умолчанию вывод выделяет URL приложения и скрывает предупреждения и информационные логи узла. Используйте celld dev --logs, чтобы показать их. Ошибки остаются видимыми и без этого флага. Установите NO_COLOR, чтобы отключить цвет, или FORCE_COLOR, чтобы включить его, когда вывод не является терминалом. NO_COLOR всегда имеет приоритет.

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

celld использует стандартную цепочку учётных данных AWS. На Amazon EKS celld читает учётные данные Pod Identity из внедрённых переменных окружения и файла токена авторизации. Разверните приложение в S3-совместимый бакет, затем запустите celld против того же бакета:

celld deploy . \
  --bucket s3://my-cells-bucket

celld \
  --bucket s3://my-cells-bucket \
  --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 \
  --advertise 10.0.0.12:8081

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

Если задержка записи для вас важна, запускайте два и более узлов. Второй узел не требует дополнительной настройки — узлы находят друг друга через бакет.

Насколько ускорится запись, зависит от расстояния до бакета и нагрузки на флот. Запись в расположенное рядом хранилище занимает около 90 мс. В одном нагруженном тестовом флоте против хранилища в другом регионе измерялось около 600 мс, а после подключения второго узла — около 25 мс. Подробности см. в разделе гарантии celld.

Для другого S3-совместимого сервиса используйте --endpoint; --region нужен, если регион не определяется автоматически. Бакет с префиксом gs:// переключает celld на Google Cloud Storage: тогда используется Cloud Storage XML API с предусловиями генерации, а аутентификация выполняется через Application Default Credentials. celld отклоняет --endpoint для S3 при использовании бакета gs:// и игнорирует регион хранилища:

celld deploy . --bucket gs://my-cells-bucket
celld --bucket gs://my-cells-bucket --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 --advertise 10.0.0.12:8081

Бакет с префиксом az:// переключает celld на Azure Blob Storage, где NAME — это контейнер, а переменная AZURE_STORAGE_ACCOUNT_NAME задаёт имя учётной записи хранилища. celld требует ровно одного семейства учётных данных: ключ учётной записи хранилища, управляемое удостоверение или удостоверение рабочей нагрузки. Удостоверение рабочей нагрузки AKS использует переменные AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_TENANT_ID и AZURE_FEDERATED_TOKEN_FILE; при этом узел полномочий должен указывать на публичное облако Azure. Удостоверению Microsoft Entra требуется разрешение на уровне плоскости данных для чтения, записи, листинга и удаления блобов; роль Storage Blob Data Contributor предоставляет эти права. celld отклоняет --endpoint для S3 при использовании бакета az:// и игнорирует регион хранилища. Ограничения см. в разделе гарантии celld:

export AZURE_STORAGE_ACCOUNT_NAME=myaccount
celld deploy . --bucket az://my-cells-container
celld --bucket az://my-cells-container --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 --advertise 10.0.0.12:8081

Флот запускает одно приложение, и каждый узел загружает его последний успешно зафиксированный деплоймент из deploy/current.json. celld deploy вызывает esbuild из PATH для кода Workers, принимает поддерживаемое подмножество конфигурации Wrangler — включая совместно разворачиваемые или только статические ресурсы — и напрямую записывает объекты деплоймента, используя задокументированные типы из crates/celld/protocol.rs. Каждый узел находит владельцев и пиров через лизы бакета — никаких учётных записей и сервисов регистрации. Полный список параметров командной строки: celld --help.

Пиринговый HTTP и операторский API используют внутренний слушатель. Размещайте все объявленные адреса в доверенной закрытой сети или в зашифрованной оверлейной сети, например WireGuard или Tailscale, и не открывайте внутренний порт публично. celld отклоняет явный публичный IP, если не указан флаг --unsafe-public-advertise. Явный объявленный адрес требует явного адреса внутреннего слушателя; вы должны маршрутизировать объявленный адрес на внутренний слушатель — celld не может проверить имя хоста или транслированный порт. Первый активный узел создаёт fleet/peer-auth.json в бакете. Запросы fetch и RPC ячейки несут версию протокола и опираются на доверенную закрытую сеть. Запросы управления пирами и зарезервированными ячейками используют HMAC флота. Этот HMAC привязан к каждому телу запроса и обеспечивает ограничение по времени и защиту от повторных атак. Относитесь к доступу к бакету и его учётным данным как к доступу администратора флота.

Управление флотом

celld diagnose по умолчанию перечисляет все лизы узлов, затем выполняет подписанное прямое зондирование каждого живого пира:

celld diagnose --bucket s3://my-cells-bucket

Отчёт продолжает проверку после отдельных сбоев и различает устаревшие записи, неверные или небезопасные объявленные адреса, недоступных пиров и несовместимые протоколы. Также выводятся грубые показатели каждого узла: количество owned-ячеек, resident-ячеек, WebSocket-соединений, RSS, CPU, файловых дескрипторов, давления памяти и образцы сброса. Передайте один или несколько параметров --peer NODE_ID, чтобы ограничить проверку.

celld cell list выводит список экземпляров Durable Object в бакете флота:

celld cell list --bucket s3://my-cells-bucket

Команда печатает по одной области видимости ячейки Class:ID на строку. Укажите имя класса, чтобы вывести только экземпляры этого класса; передайте --json для вывода по одному JSON-объекту на строку. Экземпляр появляется в списке после того, как до него доходит первое событие, — тогда владелец записывает запись о владении в бакет. Идентификатор, который приложение только вычисляет, не появляется.

Листинг ограничен: один запрос к хранилищу возвращает не более 1000 экземпляров, поэтому команда выводит не более 1000 и сообщает в stderr, что есть ещё. Передайте --after SCOPE, чтобы продолжить с последнего выведенного экземпляра, или --all, чтобы прочитать весь список.

celld d1 выполняет SQL-запросы и миграции для развёрнутой базы данных D1. Он находит узел через те же лизы и отправляет работу на узел, которому принадлежит база данных:

celld d1 migrations apply ledger --bucket s3://my-cells-bucket

Расширение файла миграции нечувствительно к регистру ASCII, поэтому файлы .sql и .SQL считаются миграциями. Файлы с другим расширением команда игнорирует.

celld kv читает и записывает данные в развёрнутое пространство имён KV. Команды массовых операций используют формат файлов Wrangler, поэтому экспорт из Wrangler можно напрямую импортировать в celld:

celld kv bulk put sessions wrangler-export.json \
  --bucket s3://my-cells-bucket

celld queue инспектирует развёрнутую очередь и управляет ею. Очередь может продолжать принимать сообщения, пока доставка приостановлена:

celld queue info jobs --bucket s3://my-cells-bucket
celld queue pause jobs --bucket s3://my-cells-bucket
celld queue resume jobs --bucket s3://my-cells-bucket

Установить жёсткий лимит resident-ячеек на каждом загруженном узле:

CELLD_MAX_RESIDENT_CELLS=1000 \
celld --bucket s3://my-cells-bucket --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 --advertise node-a.internal:8081

celld балансирует владение ячейками по всему флоту. Каждый узел читает общий образец флота каждые пять секунд. Один узел обновляет образец из лизов узлов, поэтому каждое обновление читает каждый лиз по одному разу для всего флота. Узел с наибольшим числом owned-ячеек на единицу веса передаёт не более 32 гибернированных ячеек за образец пиру, у которого меньше всего ячеек относительно его доли. Гибернированная ячейка перемещается одной записью, а её припаркованные гибернируемые WebSocket закрываются с кодом 1012 — клиенты переподключаются к новому владельцу. Резидентная ячейка перемещается только после того, как её гибернирует вытеснение по простою (CELLD_IDLE_EVICT_S). Установите CELLD_PLACEMENT_WEIGHT, чтобы дать узлу долю больше или меньше числа его CPU, и установите CELLD_REBALANCE_INTERVAL_MS=0, чтобы отключить балансировку. POST /rebalance/pause на внутреннем слушателе любого узла приостанавливает флот.

По умолчанию celld включает порог давления памяти на уровне 80% доступной памяти. Установите CELLD_MAX_RSS_MB, чтобы изменить порог, или задайте значение 0, чтобы отключить сброс при давлении памяти. В Linux cgroup порог использует наибольшее из: скорректированного аллокатором RSS и активного рабочего набора cgroup. celld вычисляет рабочий набор как memory.current минус inactive_file из memory.stat, затем вычитает измеренный резерв аллокатора. Этот расчёт включает активные заряды ядра, не отражённые в RSS процесса, и исключает файловые страницы и страницы аллокатора, которые celld не может освободить путём сброса ячейки. Маршрут /state выводит все четыре входных измерения.

Отдельный абсолютный предел применяется ко всему заряду cgroup на уровне 95% доступной памяти. Если celld не может прочитать заряд cgroup, используется RSS процесса. Предел защищает узел, когда сброс не может освободить заряд ядра. Узел записывает предупреждение в лог, когда предел применяется. Предел — это доля доступной памяти, а не доля порога. Поэтому значение CELLD_MAX_RSS_MB на уровне 95% или выше делает предел эффективным ограничением, и celld сообщает об этом решении при запуске. CELLD_MAX_RSS_MB=0 отключает и порог, и предел одновременно. Если celld не может определить размер доступной памяти, он применяет предел в 125% от явно заданного порога.

Под давлением celld устойчиво реплицирует и ограждает наименее недавно использованные простаивающие ячейки, публикует их как бесхозные без сброса эпох и отказывается повторно захватывать новые бесхозные ячейки. Ячейка с активной работой или живым хост-WebSocket сброшена не будет. Свободный узел не получает назначений; он захватывает освобождённую ячейку через тот же протокол бакета, когда к ней поступает обычный трафик. Каждый лимит снимается отдельно: порог снимается при 80% своего значения, предел — при 80% своего значения. Поэтому пересечение одного лимита не удерживает узел против другого.

Каждый изолят (isolate) также имеет лимит кучи V8, отдельный от памяти узла. По умолчанию это 128 МБ — значение, соответствующее лимиту Durable Object на Cloudflare; установите CELLD_V8_HEAP_LIMIT_MB, чтобы изменить его. Каждый гибернируемый клиент WebSocket хранит состояние в куче, поэтому лимит определяет, сколько клиентов может обслуживать ячейка: около 50 000 при значении по умолчанию и около 512 МБ для 100 000 клиентов.

Изолят, у которого занято более 90% этого лимита, отклоняет новый гибернируемый WebSocket и снова начинает их принимать, когда использование кучи падает ниже 90%. Изолят, достигший лимита, также прекращает материализацию результирующего набора SQL — обе ошибки называют кучу. celld измеряет кучу перед каждым событием, и изолят снова обслуживает запросы, когда использование падает ниже 75% лимита. Простаивающий изолят держит мёртвую кучу до первого выделения памяти, поэтому celld принудительно запускает сборку мусора, когда измеренное значение превышает эту долю. Перезапуск процесса при этом не требуется.

Участие в разработке

Pull request-ы отключены. Агенты для написания кода слишком легко отправляют объёмные изменения с малым контекстом, на изучение которых у мейнтейнеров уходит больше времени, чем они экономят. Вдумчивые вклады приветствуются: пожалуйста, разберитесь в коде, держите патч сфокусированным и уважайте время, которое вы просите на ревью.

Отправляйте вложение git format-patch на ry@deno.com.

Лицензионное соглашение участника: отправляя патч по электронной почте, вы подтверждаете, что имеете право его представить, и передаёте Deno Land Inc. все права на патч, которые вы можете передать. Там, где право не может быть передано, вы предоставляете Deno Land Inc. бессрочную, безотзывную, всемирную, безвозмездную, передаваемую, сублицензируемую лицензию на использование, изменение, объединение, повторное лицензирование, распространение или публикацию патча полностью или частично, с указанием авторства или без.

Лицензия

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

© 2026 meganuke