Перейти к содержимому
Hogin Hogin
Назад

SpinKube: запускаем WebAssembly-нагрузки в Kubernetes без своего рантайма

8 мин чтения

Под с обычным контейнером тратит на старт от сотен миллисекунд до нескольких секунд — pull образа, boot процесса, прогрев рантайма приложения. Для fan-out на edge, коротких HTTP-обработчиков и всплесков нагрузки это накладные расходы, которые накапливаются на каждом масштабировании HPA. SpinKube решает эту задачу иначе: WebAssembly-модуль планируется тем же kubectl apply, что и обычный под, но стартует как WASM-инстанс — без отдельной прослойки, без своего API, без нового способа деплоя. Разбираем, как это работает на уровне containerd и почему это не «контейнеры, но быстрее», а инструмент для конкретного профиля нагрузок.

Содержание

Открыть содержание

Почему об этом стоит знать в 2026

WebAssembly на сервере несколько лет был нишевой темой — сначала proof-of-concept, потом нишевые edge-платформы со своим закрытым рантаймом. Ситуация изменилась с двух сторон одновременно. Fermyon Spin — фреймворк и CLI для сборки WASM-микросервисов — после покупки Akamai обрабатывает на edge порядка 75 млн RPS, то есть это не игрушечный бенчмарк, а продовая нагрузка (источник). Параллельно проект SpinKube попал в CNCF Sandbox, а WASM-модули теперь планируются напрямую через containerd shim — то есть встроены в тот же путь, по которому Kubernetes уже планирует обычные контейнеры, а не через отдельный сайдкар или внешний контроллер.

Это меняет вопрос с «стоит ли вообще пробовать WASM на сервере» на «для какого процента моих сервисов это уже имеет смысл сегодня». Ответ — не «для всех», и дальше по тексту честно разберём, где граница.

Архитектура: один kubelet, два пути исполнения

Ключевая идея SpinKube в том, что WASM не заменяет контейнерный рантайм узла, а встраивается рядом с ним на уровне containerd shim. У containerd уже есть abstraction для этого — shim v2 API, через который планируется, какой процесс фактически исполняет workload. Обычный путь — containerd-shim-runc-v2, который запускает Linux-процесс в namespace из OCI-образа. containerd-shim-spin (построен на библиотеке runwasi) — второй, параллельный путь: он берёт .wasm-компонент и создаёт под него инстанс Wasmtime вместо процесса ОС.

Один kubelet, два пути исполнения пода — runc и containerd-shim-spin

Выбор пути делает не отдельный планировщик и не аннотация — это стандартное поле Kubernetes runtimeClassName в спеке пода. RuntimeClass с handler: spin говорит kubelet: для этого пода использовать не runc, а shim для Spin. Дальше весь остальной Kubernetes — Service, HPA, NetworkPolicy, зонды готовности — работает без изменений, потому что для контроллеров это по-прежнему обычный Pod с обычным статусом Running.

SpinKube-оператор и SpinApp CRD

Руками ставить RuntimeClass и следить за низкоуровневыми деталями shim на каждом узле неудобно — для этого в SpinKube есть оператор, который берёт на себя provisioning узлов и превращает декларативное описание Spin-приложения в обычные Kubernetes-объекты.

Как SpinApp превращается в под: SpinApp CR → spin-operator → Deployment/Service

Установка — три Helm-релиза: kwasm-operator ставит бинарник shim на узлы (аннотацией kwasm.sh/node=true включается на нужных нодах, без пересборки образа узла), cert-manager — стандартная зависимость для вебхуков оператора, и сам spin-operator:

helm install kwasm-operator kwasm-operator \
  --repo https://kwasm.sh/kwasm-operator/ \
  --namespace kwasm --create-namespace \
  --set kwasmOperator.installerImage=ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.21.0

kubectl annotate node --all kwasm.sh/node=true

helm install cert-manager cert-manager \
  --repo https://charts.jetstack.io \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true

helm install spin-operator \
  --namespace spin-operator --create-namespace --wait \
  oci://ghcr.io/spinkube/charts/spin-operator

kubectl apply -f - <<'EOF'
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmtime-spin-v2
handler: spin
EOF

После этого приложение собирается CLI spin, пакуется как OCI-артефакт и описывается CRD SpinApp:

spin new -t http-rust hello-spin
cd hello-spin
spin build
spin registry push ttl.sh/hello-spin:1h
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-spin
spec:
  image: "ttl.sh/hello-spin:1h"
  executor: containerd-shim-spin
  replicas: 3
  resources:
    limits:
      cpu: "100m"
      memory: "128Mi"
    requests:
      cpu: "50m"
      memory: "32Mi"

kubectl apply -f spinapp.yaml — и оператор реконсилирует это в Deployment с runtimeClassName: wasmtime-spin-v2 и Service перед ним. Для остального кластера — kubectl get pods, kubectl logs, HPA по CPU или кастомным метрикам — разницы с обычным контейнерным сервисом нет.

Честные ограничения WASI 0.2

Здесь важно не поддаться маркетинговой рамке «WASM — это контейнеры, только быстрее». Component model и WASI 0.2 (Preview 2), на которых построен Spin, — это песочница с намеренно урезанной поверхностью системных вызовов, а не полноценная замена Linux-процесса:

Иными словами: если сервис — это обвязка вокруг блокирующего С-драйвера или долгоживущий stateful процесс, WASM на Kubernetes ему не поможет и не должен рассматриваться как замена.

Где WASM реально выигрывает

Профиль нагрузок, для которых холодный старт в единицы миллисекунд и лёгкая песочница дают измеримый эффект:

Здесь же — важное практическое свойство: WASM-поды и обычные контейнерные Deployment спокойно сосуществуют в одном кластере и даже в одном namespace. RuntimeClass — это поле уровня пода, а не свойство кластера или namespace, поэтому миграция не требует «всё или ничего»: можно перевести на Spin ровно те сервисы, которые попадают в профиль выше, оставив остальное на runc.

Что нужно, чтобы сравнить старт и память на практике

Простой способ увидеть разницу — задеплоить SpinApp рядом с обычным Deployment, отдающим тот же HTTP-ответ, и прогнать оба через одинаковую нагрузку.

Холодный старт: контейнер тратит сотни мс — секунды, WASM-инстанс — единицы мс

kubectl scale deployment hello-container --replicas=0
kubectl scale spinapp hello-spin --replicas=0

kubectl scale deployment hello-container --replicas=1
kubectl get pod -l app=hello-container -o wide --watch
# ContainerCreating -> Running: обычно сотни миллисекунд —
# секунды, зависит от того, есть ли образ в кэше узла

kubectl scale spinapp hello-spin --replicas=1
kubectl get pod -l core.spinkube.dev/app-name=hello-spin -o wide --watch
# ContainerCreating -> Running: единицы миллисекунд после первого запроса

Нагрузочный прогон — hey или k6 на оба сервиса с одинаковыми параметрами:

hey -z 30s -c 50 http://hello-container.default.svc.cluster.local/
hey -z 30s -c 50 http://hello-spin.default.svc.cluster.local/

kubectl top pod -l app=hello-container
kubectl top pod -l core.spinkube.dev/app-name=hello-spin

Таблица сравнения

ПараметрОбычный контейнер (runc)WASM-приложение (SpinKube)
Холодный стартсотни мс — секундыединицы мс
Потребление памяти в простоедесятки — сотни МБ на процессединицы — десятки МБ на инстанс
Потоки / блокирующий I/Oполная поддержка ОСограничено WASI 0.2, без полноценных потоков
Сетевой доступпроизвольный socket()капабилити из манифеста приложения
Совместимость с существующим кодомлюбой Linux-бинарниктолько то, что собирается в wasm32-wasi
Планирование в Kubernetesстандартный путьтот же kubectl apply, RuntimeClass
Сосуществование в кластереда, рядом с обычными подами в том же namespace
Подходящий профильдолгоживущие stateful-сервисыкороткоживущие stateless обработчики

Как проверить, что всё работает

kubectl get spinapps
# NAME         READY   IMAGE
# hello-spin   True    ttl.sh/hello-spin:1h

kubectl get pods -l core.spinkube.dev/app-name=hello-spin
# NAME                          READY   STATUS    RESTARTS   AGE
# hello-spin-6d8f9c7b6f-x2k9p   1/1     Running   0          4s

kubectl describe pod -l core.spinkube.dev/app-name=hello-spin | grep "Runtime Class"
# Runtime Class Name:  wasmtime-spin-v2

curl -s http://hello-spin.default.svc.cluster.local/ -w '\n%{time_total}s\n'

Если под завис в ContainerCreating дольше пары секунд — почти всегда это значит, что shim не установлен на узле (проверить kwasm.sh/node аннотацию и логи kwasm-operator) или RuntimeClass не применился до создания пода.

Итог

SpinKube не заменяет контейнеры — он добавляет второй путь исполнения рядом с уже привычным runc, доступный через то же поле runtimeClassName, тот же kubectl apply, тот же HPA. Выигрыш реален и измерим для короткоживущих, stateless, I/O-light обработчиков: edge-функций, роутинга, лёгкой бизнес-логики, — там, где холодный старт в единицы миллисекунд и меньшая поверхность атаки WASI-песочницы окупают ограничения Preview 2. Для тяжёлого stateful backend, кода с нативными зависимостями или сервисов, которым нужны полноценные потоки, WASM пока не альтернатива — и не должен ей навязываться. Правильная стратегия на 2026 год — не мигрировать кластер целиком, а выбрать сервисы, которые уже попадают в этот профиль, и запустить их рядом с остальными подами без единого лишнего компонента в архитектуре.


Поделиться:

Предыдущая статья
SPIFFE/SPIRE: криптографическая identity для подов вместо статичных секретов
Следующая статья
ValidatingAdmissionPolicy: переезжаем с вебхуков на CEL прямо в API-сервере