Wenn Sie jemals versucht haben, verschiedene Prozesse auf einem Linux-System effizient miteinander kommunizieren zu lassen, wissen Sie, dass die Landschaft … sagen wir mal, fragmentiert ist. Wir haben D-Bus, Unix-Sockets mit benutzerdefinierten Protokollen, REST-APIs über localhost, gRPC und unzählige andere Ansätze. Jeder bringt seine eigene Komplexität, Werkzeuganforderungen und Lernkurve mit sich.
Kürzlich stolperte ich über Varlink (und dessen neueren Rust‑zentrierten Verwandten Zlink), und es hat sofort zu dem gepasst, was wir bei DownToZero erreichen wollen. Wir bauen Infrastruktur, die zuverlässige, latenzarme Interprozesskommunikation benötigt – von Container‑Orchestrierung bis zur Verwaltung von Maschineneinstellungen. Je mehr ich mich mit Varlink beschäftigte, desto mehr wurde mir klar, dass das genau das sein könnte, was wir brauchen.
In diesem Beitrag führe ich Sie durch meine Experimente mit Varlink. Wir bauen gemeinsam Schritt für Schritt einen einfachen Hello‑World‑Dienst, und ich teile meine Gedanken dazu, warum mich diese Technologie für die Zukunft unserer Plattform begeistert.
Varlink ist ein Schnittstellendefinitionsformat und Protokoll, das dafür entworfen wurde, Service‑Schnittstellen zu definieren und zu implementieren. Man kann es als eine einfachere, modernere Alternative zu D‑Bus oder als leichtere Alternative zu gRPC für lokale Kommunikation betrachten.
Wenn Sie bereits mit D‑Bus gearbeitet haben, kennen Sie wahrscheinlich den Schmerz: komplexe Typsysteme, Introspektion, die spezielle Werkzeuge erfordert, XML‑Konfigurationsdateien, die zu verwirren scheinen. D‑Bus ist mächtig, aber es stammt auch aus einer Ära, in der „einfach“ keine Designpriorität war. Varlink geht einen anderen Weg – so könnte D‑Bus aussehen, wenn es heute mit modernen Gesichtspunkten zur Developer Experience entworfen würde.
Das macht Varlink interessant:
Selbstbeschreibende Schnittstellen: Jeder Varlink‑Dienst kann seine eigene API beschreiben. Sie können sich mit einem Dienst verbinden und fragen „Was kannst du?“ und eine maschinenlesbare (und menschenlesbare) Antwort erhalten.
Sprachagnostisch: Das Protokoll ist so einfach, dass Implementierungen in Rust, Go, Python, C und mehr existieren. Wichtig ist jedoch, dass die Schnittstellen selbst sprachneutral sind.
Socket‑basiert: Die Kommunikation erfolgt über Unix‑Sockets (oder TCP für entfernte Verbindungen), was bedeutet, dass es gut mit dem Linux‑Ökosystem, Containern und systemd zusammenarbeitet.
JSON‑basiertes Protokoll: Das Wire‑Format ist JSON, was das Debugging trivial macht. Sie können buchstäblich netcat verwenden, um mit einem Varlink‑Dienst zu sprechen, wenn Sie wollen.
systemd‑Integration: Das ist entscheidend. systemd verwendet bereits Varlink für einige seiner internen Dienste, was bedeutet, dass das Protokoll in der Praxis erprobt ist und erste Parteiuntersützung für Socket‑Aktivierung und Service‑Management existiert.
Bei DTZ beschäftigen wir uns ständig mit Maschinenkonfiguration und Orchestrierung. Unsere Infrastruktur umfasst physische Hardware (vielleicht erinnern Sie sich an unsere solarbetriebenen Knoten), Container und verschiedene Systemdienste, die miteinander koordiniert werden müssen.
Derzeit verwenden wir eine Mischung aus Ansätzen für Interprozesskommunikation:
Was uns fehlt, ist ein einheitlicher Weg, damit unsere System‑Level‑Dienste miteinander kommunizieren. Betrachten Sie diese Szenarien:
Für all das bietet Varlink eine überzeugende Lösung. Es ist lokal‑orientiert (gut für Latenz), selbstdokumentierend (gut für Debugging) und hat native systemd‑Unterstützung (gut für Zuverlässigkeit).
Die Tatsache, dass systemd selbst Varlink für Dienste wie systemd-resolved und systemd-hostnamed verwendet, bedeutet, dass wir potenziell direkt mit Systemdiensten über dasselbe Protokoll interagieren können, das wir auch für unsere eigenen Dienste nutzen. Das ist mächtig.
Genug Theorie – jetzt wird praktisch. Ich habe eine einfache Hello‑World‑Implementierung erstellt, um das Ganze auszuprobieren, und führe Sie durch den Aufbau von Grund auf.
Der komplette Quellcode ist verfügbar unter https://github.com/DownToZero-Cloud/varlink-helloworld.
Zuerst erstellen Sie ein neues Rust‑Projekt:
cargo new varlink-helloworld
cd varlink-helloworld
Nun müssen wir unsere Abhängigkeiten hinzufügen. Öffnen Sie Cargo.toml und fügen Sie hinzu:
[package]
name = "varlink-helloworld"
version = "0.1.0"
edition = "2024"
[dependencies]
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
zlink = { version = "0.2" }
Wir verwenden zlink, die moderne Rust‑Implementierung von Varlink. Sie ist asynchron‑first und basiert auf tokio, was perfekt zu unserer Art passt, Dienste bei DTZ zu bauen. Wir benötigen außerdem serde für die JSON‑Serialisierung (Varlinks Wire‑Format) und futures-util für Stream‑Handling.
Jetzt zum spaßigen Teil – die Implementierung unseres Dienstes von Grund auf. Die Schönheit von zlink ist, dass wir keine Code‑Generierung oder separate Schnittstellendefinitionsdateien benötigen. Wir definieren alles direkt in Rust, was volle IDE‑Unterstützung, Typprüfung und keinen Build‑Zauber bedeutet.
Erstellen Sie src/main.rs:
use serde::{Deserialize, Serialize};
use zlink::{
self, Call, Connection, ReplyError, Server, Service,
connection::Socket, service::MethodReply,
unix, varlink_service::Info,
};
const SOCKET_PATH: &str = "/tmp/hello.varlink";
#[tokio::main]
async fn main() {
println!("starting varlink hello world server");
run_server().await;
}
pub async fn run_server() {
// Clean up any existing socket file
let _ = tokio::fs::remove_file(SOCKET_PATH).await;
// Bind to the Unix socket
let listener = unix::bind(SOCKET_PATH).unwrap();
// Create our service and server
let service = HelloWorld {};
let server = Server::new(listener, service);
match server.run().await {
Ok(_) => println!("server done."),
Err(e) => println!("server error: {:?}", e),
}
}
Das ist unser Einstiegspunkt – simpel und sauber. Wir binden an einen Unix‑Socket unter /tmp/hello.varlink, erstellen unseren Dienst und lassen den Server eingehende Verbindungen verarbeiten.
Hier zeigt sich Varlinks Eleganz. Wir definieren unser Protokoll vollständig mit Rust‑Typen und serde‑Annotationen. Werfen wir einen Blick auf die einzelnen Teile:
Method Calls (eingehende Anfragen)
#[derive(Debug, Deserialize)]
#[serde(tag = "method")]
enum HelloWorldMethod {
#[serde(rename = "rocks.dtz.HelloWorld.Hello")]
Hello,
#[serde(rename = "rocks.dtz.HelloWorld.NamedHello")]
NamedHello {
#[serde(default)]
parameters: NamedHelloParameters,
},
#[serde(rename = "org.varlink.service.GetInfo")]
VarlinkGetInfo,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NamedHelloParameters {
name: String,
}
Das HelloWorldMethod‑Enum repräsentiert alle Methoden, die unser Dienst verarbeiten kann. Das Attribut #[serde(tag = "method")] sagt serde, dass das JSON‑Feld method verwendet wird, um zu bestimmen, in welchen Variantentyp des Enums deserialisiert werden soll. Die #[serde(rename = "...")]‑Attribute mappen unsere Rust‑Enum‑Varianten auf die tatsächlichen Varlink‑Methodennamen.
Beachten Sie, wie NamedHello ein verschachteltes parameters‑Feld hat – das entspricht dem Varlink‑Protokoll, in dem Methodenparameter im JSON in einem parameters‑Objekt verpackt sind.
Replies (ausgehende Antworten)
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum HelloWorldReply {
Hello(HelloResponse),
VarlinkInfo(Info<'static>),
}
#[derive(Debug, Serialize)]
pub struct HelloResponse {
message: String,
}
Das Reply‑Enum verwendet #[serde(untagged)], weil Varlink‑Antworten keinen Typ‑Discriminator enthalten – der Antworttyp ist implizit anhand der aufgerufenen Methode. HelloResponse ist unser einfacher Antwort‑Struct, der nur ein message‑Feld enthält.
Fehlerbehandlung
#[derive(Debug, ReplyError)]
#[zlink(interface = "rocks.dtz.HelloWorld")]
enum HelloWorldError {
Error { message: String },
}
Das #[derive(ReplyError)]‑Macro von zlink erzeugt den notwendigen Code, um unsere Fehler gemäß dem Varlink‑Fehlerformat zu serialisieren. Das Attribut #[zlink(interface = "...")] gibt an, zu welcher Schnittstelle diese Fehler gehören.
Nun verbinden wir alles, indem wir das Service‑Trait implementieren:
struct HelloWorld {}
impl Service for HelloWorld {
type MethodCall<'de> = HelloWorldMethod;
type ReplyParams<'ser> = HelloWorldReply;
type ReplyStreamParams = ();
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
type ReplyError<'ser> = HelloWorldError;
async fn handle<'ser, 'de: 'ser, Sock: Socket>(
&'ser mut self,
call: Call<Self::MethodCall<'de>>,
_conn: &mut Connection<Sock>,
) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
println!("handling call: {:?}", call.method());
match call.method() {
HelloWorldMethod::Hello => {
MethodReply::Single(Some(HelloWorldReply::Hello(HelloResponse {
message: "Hello, World!".to_string(),
})))
}
HelloWorldMethod::NamedHello { parameters } => {
MethodReply::Single(Some(HelloWorldReply::Hello(HelloResponse {
message: format!("Hello, {}!", parameters.name),
})))
}
HelloWorldMethod::VarlinkGetInfo => {
MethodReply::Single(Some(HelloWorldReply::VarlinkInfo(Info::<'static> {
vendor: "DownToZero",
product: "hello-world",
url: "https://github.com/DownToZero-Cloud/varlink-helloworld",
interfaces: vec!["rocks.dtz.HelloWorld", "org.varlink.service"],
version: "1.0.0",
})))
}
}
}
}
Das Service‑Trait ist das Herzstück von zlink. Lassen Sie uns aufschlüsseln, was passiert:
Associated Types: Wir geben an, welche Typen unser Dienst für Method‑Calls, Antworten, Streaming‑Antworten und Fehler verwendet. Das gibt uns vollständige Typensicherheit.
Die handle‑Methode: Hier werden alle eingehenden Aufrufe geroutet. Wir pattern matchen auf den deserialisierten Methodenaufruf und geben die passende Antwort zurück.
MethodReply::Single: Für nicht‑streamende Antworten verpacken wir unsere Antwort in MethodReply::Single. Varlink unterstützt auch Streaming‑Antworten (nützlich für Monitoring oder Subscriptions), aber hier bleiben wir einfach.
VarlinkGetInfo: Jeder Varlink‑Dienst sollte die Methode org.varlink.service.GetInfo implementieren. Diese gibt Metadaten über unseren Dienst zurück – Vendor, Produktname, Version, URL und die Liste der Schnittstellen, die wir implementieren.
Starten Sie den Server:
cargo run
You should see:
starting varlink hello world server
Now, in another terminal, we can test it using varlinkctl, which is part of systemd. First, let’s see what the service exposes:
varlinkctl info /tmp/hello.varlink
Output:
Vendor: DownToZero
Product: hello-world
Version: 1.0.0
URL: https://github.com/DownToZero-Cloud/varlink-helloworld
Interfaces: org.varlink.service
rocks.dtz.HelloWorld
Das ist die selbstbeschreibende Natur von Varlink in Aktion. Der Client kann genau entdecken, was dieser Dienst anbietet.
Rufen wir nun unsere Methoden auf:
varlinkctl call /tmp/hello.varlink rocks.dtz.HelloWorld.Hello {}
Output:
{
"message" : "Hello, World!"
}
Und mit einem Parameter:
varlinkctl call /tmp/hello.varlink rocks.dtz.HelloWorld.NamedHello '{"name":"jens"}'
Output:
{
"message" : "Hello, jens!"
}
Es funktioniert! Wir haben einen voll funktionsfähigen Varlink‑Dienst.
Eine Sache, die ich an Varlink liebe, ist, wie einfach es zu erkunden und zu debuggen ist. Da das Protokoll JSON‑basiert ist, können Sie sogar grundlegende Werkzeuge wie socat oder netcat für manuelle Tests verwenden:
echo '{"method":"rocks.dtz.HelloWorld.Hello","parameters":{}}' | \
socat - UNIX-CONNECT:/tmp/hello.varlink
Sie erhalten eine JSON‑Antwort, die Sie durch jq pipen oder einfach direkt lesen können. Keine speziellen Debugging‑Tools erforderlich, keine binären Protokolle zu dekodieren. Wenn Sie um 2 Uhr morgens debuggen und etwas nicht funktioniert, ist diese Einfachheit unbezahlbar.
Sie können auch die Schnittstellendefinition selbst introspektieren:
varlinkctl introspect /tmp/hello.varlink rocks.dtz.HelloWorld
Das gibt die exakte Schnittstellendefinition zurück, die wir zuvor geschrieben haben. In Kombination mit dem info‑Befehl haben Sie vollständige Sichtbarkeit darüber, was jeder Varlink‑Dienst tun kann – sogar Dienste, die Sie noch nie zuvor gesehen haben.
Eine der mächtigsten Eigenschaften von Varlink ist seine Integration mit systemd. Sie können socket‑aktivierte Dienste erstellen, die nur starten, wenn sich jemand verbindet, und systemd verwaltet den Lebenszyklus.
Erstellen Sie eine systemd‑Socket‑Unit (hello-varlink.socket):
[Unit]
Description=Hello World Varlink Socket
[Socket]
ListenStream=/run/hello.varlink
[Install]
WantedBy=sockets.target
Und eine entsprechende Service‑Unit (hello-varlink.service):
[Unit]
Description=Hello World Varlink Service
[Service]
ExecStart=/usr/local/bin/varlink-helloworld
Mit Socket‑Aktivierung hört systemd auf dem Socket und startet beim Eintreffen einer Verbindung Ihren Dienst und übergibt den Socket. Das bedeutet null Ressourcenverbrauch, bis jemand den Dienst tatsächlich benötigt – perfekt für unsere Scale‑to‑Zero‑Philosophie bei DTZ.
Aber die systemd‑Geschichte geht noch weiter. Mehrere systemd‑Komponenten stellen bereits Varlink‑Schnittstellen bereit:
Das bedeutet, dass wir genau die gleichen Varlink‑Muster verwenden können, die wir für unsere Dienste entwickeln, um mit dem Hostsystem zu interagieren. Möchten Sie den DNS‑Cache abfragen? varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name":"example.com"}'. Dasselbe Protokoll, dieselben Werkzeuge, dasselbe mentale Modell.
Für DTZ ist das besonders spannend, weil unsere Orchestrierungsschicht einen einheitlichen Ansatz sowohl für Anwendungs‑IPC als auch für System‑Level‑Management verwenden kann. Kein ständiges Wechseln zwischen unterschiedlichen APIs und Protokollen mehr.
Dieses Hello‑World‑Experiment hat mich wirklich für Varlinks Potenzial bei DTZ begeistert. Hier sind einige Richtungen, die ich in Betracht ziehe:
Maschinenkonfigurationsdienst: Ein Varlink‑Dienst, der Maschinen‑Einstellungen (Netzwerkkonfiguration, Ressourcenlimits, etc.) mit geeigneter Zugriffskontrolle bereitstellt.
Container‑Orchestrierungs‑IPC: Verwendung von Varlink für die Kommunikation zwischen unserem Container‑Runtime und Management‑Diensten.
Observability‑Aggregation: Ein lokaler Varlink‑Dienst, der Metriken aus verschiedenen Systemkomponenten aggregiert.
Systemd‑Integration: Direkte Abfrage der Varlink‑Schnittstellen von systemd für Service‑Status und Management.
Health‑Check‑Aggregation: Ein zentraler Varlink‑Dienst, der den Gesundheitsstatus aller laufenden Dienste sammelt und einen einheitlichen Health‑Endpoint bereitstellt.
Die Tatsache, dass wir dasselbe Protokoll verwenden können, um mit unseren eigenen Diensten UND Systemdiensten wie systemd-resolved zu sprechen, ist ein großer Gewinn für Konsistenz und verminderte Komplexität.
Ich bin auch neugierig auf die Performance‑Eigenschaften. JSON ist zwar nicht das kompakteste Wire‑Format, aber für lokale IPC ist der Parsing‑Overhead typischerweise vernachlässigbar gegenüber den Vorteilen der Menschenlesbarkeit. Dennoch plane ich, in einem Folgeexperiment Benchmarks durchzuführen, um echte Zahlen zu Latenz und Durchsatz für unsere Anwendungsfälle zu erhalten.
Varlink trifft einen Sweetspot zwischen Einfachheit und Fähigkeit. Es versucht nicht, jedes verteilte Systems‑Problem zu lösen – es fokussiert sich darauf, lokale IPC wirklich gut zu machen, mit gerade genug Features für Discoverability und Typsicherheit.
Für DownToZero, wo wir ständig auf Effizienz und Einfachheit optimieren, passt dieser Ansatz sehr gut. Wir brauchen nicht die Komplexität von gRPC für lokale Kommunikation. Wir wollen nicht die Overhead‑Kosten von HTTP für maschineninterne Aufrufe. Varlink gibt uns ein sauberes, wohlüberlegtes Protokoll, das gut mit dem Linux‑Ökosystem harmoniert, auf dem wir bauen.
Wenn Sie selbst experimentieren möchten, holen Sie sich den Code von GitHub, starten Sie Ihren Editor und probieren Sie es aus. Die Lernkurve ist sanft, und es ist sehr befriedigend, wenn der erste varlinkctl call Ihre Nachricht zurückgibt.
Viel Spaß beim Experimentieren!