выбирать Wasm вместо контейнеров для edge-логики с жёсткой задержкой
WebAssembly на edge: когда Wasm выгоднее контейнеров для serverless и CDN
13 минут
Контейнеры остаются основой долгоживущих сервисов, но узлам CDN нужны быстрый старт и маленькие бинарники. Рантаймы Wasm в Cloudflare Workers, Fastly Compute и Spin дают изолированные запросы — для auth, маршрутизации и трансформаций, а не для баз данных.
Почему контейнеры тяжелы для узлов CDN
Edge-платформы ставят логику в точки присутствия ближе к пользователю. Ответ часто должен уложиться в десятки миллисекунд. Холодный старт контейнера или классического FaaS в сотни миллисекунд или секунды ломает персонализацию, A/B, проверки доступа и маршрутизацию API на краю сети. Минимальные образы Node или Python всё равно резервируют десятки мегабайт; тысячи одновременных изолятов по глобальной сети PoP зря тратят память. Контейнеры делят ядро хоста — даже с gVisor или Kata изоляция тяжелее песочницы Wasm, рассчитанной на мультитенантных гостей. Тащить слой ОС ради правки заголовка или проверки JWT — это большие образы и медленный выкат. Wasm не заменяет Kubernetes для stateful-бэкендов; он дополняет контейнеры там, где единица работы — короткий изолированный запрос.
Как Wasm работает на edge: рантаймы, WASI и компоненты
WebAssembly начинался в браузере. WASI расширяет его для серверов явными правами вместо полного доступа к ОС. WASI Preview 1 ещё распространён для простых модулей; Preview 2 и Component Model стабилизируют типизированные интерфейсы через WIT, чтобы хост и гости собирались как конструктор. На узлах edge обычно крутится Wasm-рантайм — изоляты V8 в Cloudflare Workers, движки класса Wasmtime у Fastly, Spin и Wasmtime для self-hosted и похожих CDN. Модули стартуют за микросекунды или единицы миллисекунд: гостевое ядро не поднимается. Память линейная и ограниченная. В Wasm компилируют Rust, C, C++, Zig и часть языков выше уровнем; у Go часто раздутый бинарник из‑за рантайма. Переносимость между хостами растёт, но не абсолютна: Rust на Cloudflare Workers идёт в wasm32-unknown-unknown через workers-rs, а Spin и многие WASI-инструменты — в wasm32-wasip1 или component-toolchain.
Когда выбирать Wasm, а когда контейнеры
Берите Wasm, если нагрузка без локального состояния или хранит его только в KV, Durable Objects, Redis или на origin; если решают холодный старт и размер бинарника; если нужна жёсткая песочница для чужих или мультитенантных плагинов; если логика — мидлварь на запрос: доступ, маршрутизация, rewrite, feature flags, бот-гейты. Оставайтесь на контейнерах, если нужен полный Linux ABI, локальные диски, базы, экосистемы, которые плохо собираются в Wasm, долгоживущие воркеры или GPU. Скорость оптимизированного Wasm близка к нативной на узких циклах; главный выигрыш — плотность и старт. Считайте Wasm отдельным рантаймом рядом с контейнерной платформой, а не полной заменой.
Пример: JWT-гейт на Cloudflare Workers на Rust
Документация Cloudflare ведёт Rust Workers через crate workers-rs и цель wasm32-unknown-unknown — не через самодельный C ABI и переменные окружения. Создайте проект через cargo generate cloudflare/workers-rs и напишите обработчик fetch: отклоняйте запросы без Bearer, при необходимости декодируйте claims для обогащения, при ошибке разбора закрывайте доступ. В продакшене проверяйте подпись нормальной JWT-библиотекой и ключами из Secrets или KV; ниже декодирование только иллюстративное. worker-build и Wrangler собирают JS-обёртку и оптимизированный Wasm-бандл.
#!/usr/bin/env bash
set -euo pipefail
rustup target add wasm32-unknown-unknown
cargo install cargo-generate worker-build
cargo generate cloudflare/workers-rs --name edge-auth-worker
cd edge-auth-workeruse base64::{
engine::general_purpose::URL_SAFE_NO_PAD,
Engine,
};
use serde::Deserialize;
use worker::*;
#[derive(Deserialize)]
struct JwtClaims {
sub: String,
exp: Option<u64>,
scope: Option<String>,
}
#[event(fetch)]
async fn main(req: Request, _env: Env, _ctx: Context) -> Result<Response> {
let auth = match req.headers().get("Authorization")? {
Some(value) => value,
None => return Response::error("Missing Authorization", 401),
};
let token = match auth.strip_prefix("Bearer ") {
Some(value) if !value.is_empty() => value,
_ => return Response::error("Invalid Authorization", 401),
};
let claims = match decode_claims(token) {
Ok(claims) => claims,
Err(_) => return Response::error("Invalid or expired token", 403),
};
// Production: verify signature with a JWT library, then proxy to origin.
Response::from_json(&serde_json::json!({
"ok": true,
"user": claims.sub,
"scope": claims.scope,
}))
}
fn decode_claims(token: &str) -> Result<JwtClaims> {
let mut parts = token.split('.');
let (_header, payload, _sig) = match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(h), Some(p), Some(s), None) if !h.is_empty() && !p.is_empty() && !s.is_empty() => {
(h, p, s)
}
_ => return Err(Error::RustError("malformed jwt".into())),
};
let bytes = URL_SAFE_NO_PAD
.decode(payload)
.map_err(|_| Error::RustError("jwt payload decode".into()))?;
let claims: JwtClaims =
serde_json::from_slice(&bytes).map_err(|_| Error::RustError("jwt json".into()))?;
if let Some(exp) = claims.exp {
let now = Date::now().as_millis() as u64 / 1000;
if exp < now {
return Err(Error::RustError("expired".into()));
}
}
Ok(claims)
}[package]
name = "edge-auth-worker"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
worker = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
[profile.release]
lto = true
strip = true
codegen-units = 1name = "edge-auth-worker"
main = "build/worker/shim.mjs"
compatibility_date = "2026-01-01"
[build]
command = "cargo install -q worker-build && worker-build --release"
[[routes]]
pattern = "api.example.com/*"
zone_name = "example.com"Переносимые WASI-сборки для Spin и своих рантаймов
Для Spin, Wasmtime и других WASI-хостов используйте актуальную цель Rust wasm32-wasip1 (старое имя wasm32-wasi переименовано). Собирайте release, затем срезайте отладку и оптимизируйте через wasm-opt из Binaryen. Fastly Compute и Spin опираются на WASI HTTP worlds и компоненты; держите платформенный клей тонким, чтобы бизнес-логика оставалась переносимой. Возможности хоста различаются — ФС, сокеты и часы выдаются явно. Тестируйте модуль на том рантайме, куда выкатываете, а не только unit-тестами на хостовой архитектуре.
#!/usr/bin/env bash
set -euo pipefail
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
wasm-opt -O3 --strip-debug --strip-producers \
target/wasm32-wasip1/release/edge_auth_worker.wasm \
-o dist/edge-auth-worker.wasm
ls -lh dist/edge-auth-worker.wasmПрактика: размер, состояние, наблюдаемость, безопасность
Держите граф зависимостей узким; включайте LTO и strip в release; прогоняйте wasm-opt перед загрузкой, чтобы простые гейты оставались в десятках или низких сотнях килобайт. Для горячих путей берите Rust или C; Go в Wasm часто тащит мегабайты рантайма. Сессии и кеш — в Cloudflare KV, Durable Objects, Redis или API origin: гостевой Wasm не замена базе. Пишите структурированные логи и метрики платформы (Workers Analytics Engine, логи Fastly, OpenTelemetry где есть) — иначе сбои на edge незаметны. Избегайте unsafe в Rust-гостях; аудируйте C/C++; проверяйте каждый заголовок и тело; задавайте лимиты памяти и CPU на запрос. При ошибках для auth закрывайте доступ; открытый фоллбэк на origin — только если так решили продуктово.
Выкатывайте постепенно и оставляйте контейнеры в архитектуре
Сначала перенесите одну мидлварь — доступ, фильтрацию ботов или geo-маршрутизацию — на долю трафика или отдельный хост. Сравните p99, долю ошибок и стоимость на миллион запросов с прежним путём на контейнерах или Lambda. Расширяйте только после зелёных дашбордов. Подписывайте и учитывайте Wasm-артефакты как любой supply chain. Контейнеры и VM остаются домом для API с тяжёлыми фреймворками, data plane и пакетных задач; стандартизируйте эти рантаймы отдельно, а Wasm отдайте сверхплотному краю сети. Операционная привычка избирательна: по умолчанию контейнеры для полных сервисов, Wasm — когда решают холодный старт, плотность и песочница.
Поэтапный выкат Wasm сочетается с progressive delivery из гайда по canary и feature flags в Kubernetes.
Контейнеры по-прежнему стандарт для полных приложений — как их унифицировать, см. в гайде по стандартизации операций с контейнерами и VM.
