Ferris Talk #8: Wasm loves Rust – WebAssembly und Rust jenseits des Browsers

Seite 2: Wasm versus Container

Inhaltsverzeichnis

Klingt das nicht ähnlich wie das, was sich mit Container-Technologie erreichen lässt? In der Microservice-Architektur geht es darum, lose gekoppelte Komponenten bereitzustellen, die mit unterschiedlichen Sprachen programmiert sein können, sich unabhängig von einander aktualisieren lassen und aus ihrer Sandbox heraus nur eingeschränkt auf die Außenwelt zugreifen können.

Auch wenn Wasm technisch anders funktioniert, verfolgen Wasm-Komponenten zum Teil ähnliche Ziele: Wie es ein Container Image Format gibt, so bietet Wasm das zuvor erwähnte Binärformat für portablen Code. Für Container braucht es zum Ausführen eine Container-Runtime, bei Wasm gibt es Wasm-Runtimes. Anwendungen in Containern können über die API des Betriebssystems (unter anderem über die Linux-API) auf Dinge wie das Dateisystem und das Netzwerk zugreifen, bei Wasm hat WASI eine im weitesten Sinn vergleichbare Rolle.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Wenn Container und Wasm so viel gemeinsam haben, warum nicht bei Containern bleiben? Die Antwort liegt darin, dass Wasm-Module deutlich leichtgewichtiger sind als Container Images. Während beispielsweise eine Web-API in einem Container ihren eigenen Webserver mitbringen muss, lässt sich ein Wasm-Modul mit einem HTTP-Handler direkt in einen bestehenden Webserver einbetten. Wenn der Host mit der eingebetteten Wasm-Komponente kommunizieren muss, braucht es dafür kein aufwendiges Kommunikationsprotokoll wie gRPC, es ist nicht einmal zwingend eine Prozess-Grenze zu überwinden. Trotzdem will niemand die Isolation potenziell schädlicher Softwarekomponenten aufgeben. Wo Teams früher aus Performancegründen auf die Sicherheitsvorteile von Containern verzichteten und native Plug-ins einsetzten, können sie in Zukunft auf Wasm-Module zurückgreifen und bekommen dadurch das Beste aus beiden Welten.

Mit Wasm lassen sich also kleine, leichtgewichtige Komponenten bauen, die in größere Systeme wie Webserver, Laufzeitumgebungen (wie Node.js oder CPython), modulare Geschäftsanwendungen und elastisch skalierbare Cloud-Umgebungen effizient und sicher einbindbar sind.

Schluss mit der Theorie – es folgen zwei praktische Projekte, die zeigen, welches Potenzial in Wasm auf dem Server steckt. Es sei an dieser Stelle darauf hingewiesen, dass alle Projekte und APIs, zu denen hier Erklärungen und Demonstrationen folgen, experimentell sind, sich daher noch häufig und grundlegend ändern werden und noch ein Stück von der Produktionstauglichkeit entfernt sind. Jetzt ist die Zeit, die Technologie zu evaluieren, die Auswirkungen auf künftige Softwarearchitekturen einzuschätzen, Feedback zu geben und vielleicht sogar etwas zu den entsprechenden Open-Source-Projekten beizutragen.

Zum Einstieg soll es um das WebAssembly Gateway Interface-Projekt (WAGI) gehen. Wer schon länger in der Webentwicklung arbeitet, kennt vielleicht von früher das Common Gateway Interface (CGI). WAGI funktioniert wie CGI, nur basiert es eben auf Wasm. Die Grundprinzipien von WAGI sind schnell erklärt:

  • WAGI implementiert einen Webserver, bei dem sich über Konfigurationseinstellungen Routen mit Wasm-Modulen verbinden lassen.
  • Jeder eingehende Web-Request ruft eine Methode in einem Wasm-Modul auf.
  • Die HTTP-Header erhalten das Wasm-Modul über Umgebungsvariablen.
  • Die Query-Parameter werden als Kommandozeilenargumente übergeben.
  • stdin stellt den Request Body bereit.
  • Die HTTP-Response muss auf stdout ausgegeben werden. Dabei kommen als erstes die HTTP Response Header, danach eine Leerzeile und anschließend der HTTP Response Body.
  • Der Zugriff auf Dateien und Domänen (für ausgehende HTTP-Requests) lässt sich über eine Konfigurationsdatei steuern. Im Hintergrund arbeitet WASI.

WAGI ist eine besonders einfache Möglichkeit, HTTP-Handler in Form von Wasm-Modulen zu entwickeln. Durch die Verwendung von Kommandozeilenargumenten, Umgebungsvariablen, stdin und stdout ist es möglich, Handler in jeder beliebigen Sprache zu schreiben, die Wasm als Compile-Target unterstützt. Entwicklerinnen und Entwickler benötigen dafür keine gesonderten Frameworks. WAGI ist in Rust entwickelt, funktioniert auch ausgezeichnet mit dieser Sprache – ist aber eben nicht auf Rust beschränkt.

Wer mehr über WAGI mit Rust erfahren möchte, kann einen Blick auf einen Vortrag von Stefan Baumgartner und mir beim Rust Linz Meetup werfen ("Introduction to WAGI – Simple HTTP-based WebAssembly Workloads") und sich die zugehörigen Beispiele auf GitHub ansehen.

Die Architektur von WAGI ist zwar einfach und elegant, hat jedoch auch Nachteile. Die wesentlichste Einschränkung ist, dass das Starten eines eigenen Prozesses für jeden Web-Request sich relativ aufwendig gestaltet. Da Wasm beim Starten schnell ist, lässt sich der Performancenachteil bei kleineren Projekten verschmerzen. Für den Produktivbetrieb von Anwendungen mit großer Last ist das Konzept aber nur bedingt geeignet. Tatsächlich gilt WAGI daher nur als Proof of Concept beziehungsweise als eine Brückentechnologie, die nötig ist, bis sich eine weitere, mit Wasm verbundene Technologie etabliert hat: das WebAssembly Component Model.

Beim Wasm Component Model steht nicht die Entwicklung eigenständiger Anwendungen im Vordergrund. Stattdessen geht es darum, einbettbare Wasm-basierende Komponenten zu ermöglichen. Dabei gehen Wasm-typische Eigenschaften wie die Plattform- und Sprachenunabhängigkeit, die sichere Ausführung von Komponenten in einer Sandbox, Geschwindigkeit und Effizienz beim Aufruf von Funktionen in Wasm-Komponenten nicht verloren.

Rust ist, was die Unterstützung des Wasm Component Models betrifft, ganz vorne mit dabei. Es ist jedoch ausdrückliches Ziel des Wasm Component Models, dass sich verschiedene Programmiersprachen mischen lassen. Was diesen Punkt betrifft, sind die unterschiedlichen Typsysteme von Programmiersprachen jenseits von Basisdatentypen wie i32 eine Herausforderung. Strings oder Listen in Rust sind schließlich etwas anderes als Strings oder Listen in C# oder Java. Das Wasm Interface Types-Projekt (WIT) versucht, das Problem in den Griff zu bekommen.

Wer mag, kann das Wasm Component Model schon heute ausprobieren. Eine kürzlich vorgestellte Plattform, die darauf aufbaut, ist Spin von Fermyon. Spin ist ein Framework zur Entwicklung von Event-getriebenen Microservices mit Wasm-Komponenten, das in Rust geschrieben ist, jedoch schon jetzt neben Rust weitere Programmiersprachen wie Go unterstützt. Spin kann sowohl mit WAGI-Handlern als auch mit Wasm Components umgehen. WAGI bietet die Kompatibilität zu Sprachen, die Wasm Components noch nicht unterstützen. In Rust empfiehlt es sich aber, auf die neueren und zukunftsträchtigeren Wasm Components zu setzen.

Der folgende Codeausschnitt soll veranschaulichen, wie Spin funktioniert. Gezeigt wird eine "Hello World"-Wasm-Komponente, die sich durch eingehende HTTP-Requests aktivieren lässt und die eine einfache HTTP-Response zurückgibt. Neben dem hier demonstrierten HTTP-Trigger enthält Spin bereits einen solchen für eingehende Nachrichten in Redis Channels. Es ist abzusehen, dass bald weitere Trigger folgen werden.

use anyhow::Result;
use spin_sdk::{
    http::{Request, Response},
    http_component,
};

#[http_component]
fn hello_world(req: Request) -> Result<Response> {
    println!("{:?}", req);
    Ok(http::Response::builder()
        .status(200)
        .body(Some("Hello, heise!".into()))?)
}

Listing: Spin "Hello World"

Eine Konfigurationsdatei (spin.toml) definiert das Routing. Außerdem lassen sich in der Datei Zugriffe auf gewisse Verzeichnisse oder Domänen freigeben (über WASI).

spin_version = "1"
name = "spin-hello-world"
version = "1.0.0"
trigger = { type = "http", base = "/" }

[[component]]
id = "hello"
source = "target/wasm32-wasi/release/spinhelloworld.wasm"
[component.trigger]
route = "/hello"

Listing: Spin-Konfiguration

Spin ist ein gutes Beispiel dafür, wie das Zusammenfügen leichtgewichtiger Wasm-Module zu Anwendungen in Zukunft funktionieren kann. Die Wasm-Module lassen sich wie Container unabhängig voneinander aktualisieren und sind in dem, was sie tun können, eingeschränkt. Die Performancenachteile, die es bei der Kommunikation zwischen Containern gibt, entfallen allerdings.