Трендовые github проекты в нашем телеграм канале. Подпишись → Почему серверному WASM нужен настоящий async
WebAssembly давно перестал быть только браузерной технологией. Формат байткода, изоляция, предсказуемое исполнение и поддержка разных языков делают WASM привлекательным кандидатом для плагинов, edge-функций, встраиваемой бизнес-логики и экспериментальных серверных рантаймов. Но как только WASM выходит за пределы CPU-bound задач, он упирается в классическую проблему backend-разработки: почти любой полезный сервис ждёт сеть, диск, таймеры и внешние API.
В обычном приложении ожидание I/O стараются не превращать в простой потока. В серверном WASM это особенно важно, потому что сам компонент обычно изолирован и не выполняет системные операции напрямую. HTTP-запросы, файловый ввод-вывод, таймеры и другие эффекты обслуживает host-рантайм, например Wasmtime. Компонент вызывает импорт, управление уходит наружу, а результат приходит позже. Если такая граница работает синхронно, инстанс фактически замирает до ответа хоста.
Для одиночной функции это может выглядеть приемлемо. Для сервера, который должен держать тысячи одновременных запросов, модель быстро становится дорогой: вместо кооперативного ожидания приходится плодить инстансы или строить обходные event loop внутри гостевого кода. WASI 0.3 как раз меняет этот слой — асинхронность становится частью Component Model и ABI, а не частной договорённостью конкретного языка или рантайма.
Ограничения подхода WASI 0.2
В WASI 0.2 асинхронность строилась вокруг низкоуровневых примитивов вроде pollable, input-stream, output-stream, poll() и subscribe(). Это позволяло моделировать неблокирующее ожидание, но не давало универсального async-контракта на уровне интерфейса. Экспорт компонента оставался по природе синхронным: хост вошёл в функцию и не может повторно использовать тот же инстанс для другой работы, пока вызов не вернётся.
Конкурентность в таком мире переносится внутрь guest-кода. Компонент должен сам крутить event loop, опрашивать pollable-объекты и согласовывать ожидания. Если компонент написан на Rust, Go, Java или Python, под капотом оказываются разные модели исполнения: stackless state machine, goroutine, virtual threads, callbacks. Host-стороне приходится учитывать особенности языка, из которого собрали WASM, вместо того чтобы работать с единым ABI.
Это особенно неприятно при композиции. Component Model интересна тем, что компоненты можно собирать друг с другом, связывать интерфейсы и заменять реализации. Но если каждый компонент приносит свой event loop и собственное представление ожидания, композиция превращается в набор адаптеров. Один компонент ожидает через polling, другой — через рантайм языка, третий — через стримы с отдельной семантикой ошибок.
Есть и эксплуатационная проблема: readiness-модель сообщает, что объект готов к чтению, но не всегда удобно описывает завершение операции. Если ошибка становится видна только при следующем read, вызывающая сторона может не узнать корректный статус, если перестала читать поток раньше. Для серверного кода это важная деталь: корректное завершение, отмена и backpressure должны быть частью протокола, а не побочным эффектом чтения.
Что приносит WASI 0.3
Главная идея WASI 0.3 — поднять async на уровень Component Model. В WIT можно выразить асинхронный контракт напрямую:
interface processor {
process: async func(input: string) -> string;
ready: func() -> bool;
}
Ключевое здесь не в том, что функция обязательно реализована через определённый механизм языка. async означает: вызов может приостановиться до возврата значения. Это effect type — часть сигнатуры, описывающая наблюдаемое поведение функции. Если функция не помечена как async, но пытается приостановиться, рантайм имеет право считать это нарушением контракта.
За счёт этого async перестаёт быть внутренней привычкой Rust, Go или Java. Он становится свойством ABI. Генераторы bindings могут превращать WIT-интерфейсы в идиоматичный код целевого языка: futures в Rust, promises в JavaScript, каналы или другие конструкции в Go. Host-рантайм при этом видит общий протокол ожидания и может планировать задачи централизованно.
Важное следствие — единый event loop на уровне runtime. В WASI 0.2 каждый компонент мог требовать собственную логику ожидания. В WASI 0.3 планирование может оставаться обязанностью хоста: компонент приостанавливается, отдаёт управление, рантайм продолжает выполнять другие задачи и возвращается к первой, когда её waitable-объект готов.
future<T> и stream<T> как базовые строительные блоки
WASI 0.3 добавляет first-class конструкции future<T> и stream<T>. Семантически future<T> — это канал на ноль или одно значение, а stream<T> — канал на последовательность значений. Они могут появляться в параметрах и результатах функций, в том числе во вложенных типах:
async func(
input: stream<future<string>>
) -> result<stream<string>, stream<error>>
На уровне ABI такие значения представляются дескрипторами — индексами в таблицах текущего инстанса. Через границу компонента передаётся читающий конец, а пишущий конец остаётся у той стороны, которая будет производить данные. Это позволяет рантайму отслеживать владение, готовность и завершение без передачи реальных указателей на структуры конкретного языка.
Для разработчика backend-сервиса важен практический эффект: поток данных, результат удалённого вызова и завершение операции становятся явными типами интерфейса. Не нужно договариваться, что «вот этот ресурс надо poll-ить, а ошибка появится при следующем read». Интерфейс сразу показывает, где одиночный результат, где поток, а где функция может приостановиться.
Кроме того, у stream появляется более понятная модель завершения. Поток может сопровождаться future со статусом операции, который резолвится независимо от того, сколько данных уже прочитано. Это помогает отличать нормальное закрытие от ошибки даже в сценариях, где потребитель не вычитывает весь поток до конца.
Stackful и stackless без раскраски всей системы
Разные языки по-разному реализуют корутины. Stackful-модель даёт задаче собственный стек и похожа на lightweight thread: при ожидании рантайм сохраняет состояние и возвращается позже. Stackless-модель превращает async-функцию в конечный автомат: состояние хранится явно, а продолжение вызывается через callback или future-планировщик.
Component Model учитывает оба подхода. Для async-экспортов предусмотрены разные варианты ABI-сигнатур, чтобы языки со stackful-корутинами и языки со stackless-корутинами могли использовать естественную модель исполнения. При этом внешний контракт остаётся единым: функция может приостановиться и позже вернуть результат через механизм task return.
Это снижает риск «раскраски функций», когда async заражает всю цепочку вызовов и заставляет переписывать соседние слои. Компоненты могут сочетать синхронные и асинхронные функции, а рантайм решает, как именно планировать выполнение. Для систем, где WASM используется как plugin layer, это особенно ценно: не каждый плагин обязан быть асинхронным, но те, кто работает с I/O, получают нормальный способ отдавать управление.
Что это значит для homelab и backend-практики
Пока серверный WASM остаётся более экспериментальной зоной, чем классические контейнеры, но направление уже полезно отслеживать. Если вы запускаете плагины, edge-обработчики или изолированную бизнес-логику внутри Wasmtime, async в Component Model делает архитектуру менее хрупкой. Компонент может ждать HTTP-ответ, читать поток или возвращать future, не блокируя весь инстанс в синхронной точке ABI.
Практический чек-лист для оценки WASM-рантайма становится шире:
- поддерживает ли он актуальную Component Model и WASI 0.3;
- умеют ли нужные вам языковые bindings генерировать идиоматичный async-код;
- как реализованы cancellation и backpressure;
- можно ли наблюдать состояние задач, waitable-объектов и зависших операций;
- что происходит с линейной памятью, если async-вызов удерживает буфер до завершения;
- как компоненты компонуются друг с другом при потоковой передаче данных.
Последний пункт важен для production-подхода. Асинхронность — это не только скорость. Это ещё и корректная отмена, защита от переполнения очередей, контроль владения ресурсами и понятные границы ответственности между host и guest. Если эти свойства не описаны на уровне ABI, они неизбежно расползаются по адаптерам и соглашениям.
Итог
WASI 0.3 делает серверный WebAssembly ближе к реальным backend-нагрузкам. Вместо имитации async через polling и ручные event loop появляется единый контракт: async func, future<T>, stream<T> и планирование на уровне runtime. Компонент может приостановиться, хост может продолжить выполнять другую работу, а интерфейс сохраняет переносимость между языками.
Для разработчиков это означает меньше runtime-specific магии и больше явных типов на границе компонентов. Для операторов — потенциально более предсказуемую модель исполнения, где ожидание I/O, потоки данных, завершение и отмена становятся частью платформы. Именно такая основа нужна, чтобы WASM-компоненты перестали быть только безопасными вычислительными песочницами и стали удобным строительным блоком серверной инфраструктуры.