Ausgangspunkt: das Protokoll-Experiment
In der Vorweihnachtszeit hatte ich ein Experiment beschrieben, das aus einer eher banalen, aber wiederkehrenden Situation entstand: In Gesprächen und beim Troubleshooting tauchte immer wieder die Frage auf, was bei Pulsar eigentlich wirklich auf der Leitung passiert.
Im Alltag verlässt man sich auf Client-Libraries, APIs und Konfigurationen. Das funktioniert gut – blendet aber genau den Teil aus, in dem sich viele Annahmen, Zustände und implizite Regeln verbergen. Auch bei mir war ein Teil dieses Wissens eher gefühlt als verstanden.
Der naheliegendste Weg, das zu ändern, war ein radikaler: einen minimalen Broker zu bauen, der gerade so viel vom Pulsar-Binary-Protokoll implementiert, dass echte Clients damit sprechen können. Nicht als Produkt, nicht als Ersatz für Apache Pulsar, sondern als Analysewerkzeug.
Dieses Ziel wurde erreicht. Der Broker nahm Verbindungen an, verstand die relevanten Kommandos und verhielt sich aus Sicht realer Pulsar-Clients korrekt. Vor allem aber machte er das Protokoll greifbar: Zustände, Reihenfolgen und Erwartungen waren nicht mehr theoretisch, sondern konkret implementiert und beobachtbar.
Eigentlich war damit alles erledigt.
Vom Experiment zum gezielten Prototyp
Der Broker blieb in meinem Werkzeugkasten bestehen, weil er eine Eigenschaft hat, die das Original nicht besitzt: Er ist klein, stabil und vollständig überschaubar. Damit stellte sich eine neue, praktische Frage:
Lässt sich aus einem reinen Protokoll-Experiment gezielt ein Broker entwickeln, der dort sinnvoll einsetzbar ist, wo ein vollständiger Apache-Pulsar-Stack technisch oder betrieblich nicht passt?
Aus meiner täglichen Arbeit mit Apache Pulsar habe ich ein recht klares Gefühl dafür entwickelt, welche Eigenschaften in Unternehmensszenarien tatsächlich kritisch sind – und welche vor allem Komplexität erzeugen, ohne für bestimmte Einsatzbereiche einen proportionalen Mehrwert zu liefern.
Diese Erfahrung ermöglichte eine bewusste Entscheidung: minipulsar sollte kein reduzierter Apache Pulsar sein, sondern ein eigenständiger Broker-Prototyp, der sich konsequent auf einen kleinen, klar definierten Funktionskern konzentriert – und dafür auf einen Großteil der verteilten Infrastruktur verzichtet.
Ein zentrales Motiv für diese Weiterentwicklung war die Plattformunabhängigkeit. Durch die konsequente Nutzung von nativem Go entsteht ein einzelnes statisches Binary ohne Laufzeitabhängigkeiten.
Der Broker läuft bereits heute unverändert auf:
- Linux
- NetBSD
- FreeBSD
- macOS
- Windows
Gerade auf langlebigen Systemen, in isolierten Netzen oder auf Edge-Knoten sind diese Eigenschaften oft entscheidender als horizontale Skalierung oder Feature-Vollständigkeit.
minipulsar ist kein Spielzeug und kein vollständiger Ersatz für Apache Pulsar, sondern ein gezielt entwickelter Broker-Prototyp mit klaren Einsatzgrenzen.
Der Fokus liegt auf:
- Protokolltreue
- Portabilität
- expliziten Zustandsmodellen
- minimaler operativer Komplexität
Vergleich: Apache Pulsar vs. minipulsar
| Eigenschaft | Apache Pulsar | Kommentar | minipulsar | Kommentar |
|---|---|---|---|---|
| 🎯 Zielsetzung | +++ | Cloud, zentrale Systeme | +++ | Edge, Spezialszenarien |
| ⚙️ Laufzeitmodell | — | JVM, mehrere Prozesse | +++ | einzelnes Go-Binary |
| 📦 Binary / Image | — | ~ 1 GB Container Image | +++ | ~ 20 MB Standalone Binary |
| 🧠 RAM-Verbrauch | — | mehrere GB | +++ | wenige MB |
| 🔗 Abhängigkeiten | — | ZooKeeper, BookKeeper | +++ | keine externen |
| 🛠 Konfiguration | ++- | mächtig, komplex | +++ | deklarativ, überschaubar |
| 💻 Plattformen | +– | Linux | +++ | Linux, NetBSD, FreeBSD, MacOS, Windows |
| 🚀 Installation | — | komplex, mehrere Schritte | +++ | einfach, ein Binary |
| ⏱ Startup-Zeit | — | ~ 30 s | +++ | instant (< 1 s) |
| 💾 Persistenz | +++ | verteilt (Ledger) | +– | lokal (SQLite) |
| 📈 Skalierung | +++ | horizontal | +– | lokal (Node) |
| 🌍 Edge-Eignung | — | Overkill | +++ | explizit dafür |
Architektur und Laufzeitmodell
minipulsar besteht aus einem einzelnen statischen Go-Binary. Die zentralen Bausteine sind:
- TCP-Listener für Pulsar (
:6650) und optional TLS (:6651) - Protokoll-Layer für Framing, Checksummen und Protobuf-Parsing
- Broker-Core für Zustandsverwaltung und Delivery
- Storage-Layer auf Basis von SQLite
- eine kleine Control Plane über deklarative HCL-Konfiguration
Netzwerk und TLS
Der Broker kann gleichzeitig Plain TCP und TLS bedienen. TLS ist als separater Listener umgesetzt und verändert weder das Protokollverhalten noch die internen Zustandsmodelle.
LOOKUP-Antworten lassen sich explizit konfigurieren, sodass Clients konsistente Broker-Adressen erhalten – unabhängig vom verwendeten Transport.
Unterstützter Pulsar-Protokollumfang
minipulsar implementiert die für klassische Publish/Subscribe-Szenarien notwendigen Protokollpfade:
CONNECT/CONNECTEDLOOKUP/LOOKUP_RESPONSEPARTITIONED_METADATA(0 Partitionen)PRODUCER/PRODUCER_SUCCESSSEND/SEND_RECEIPTSUBSCRIBE/SUCCESSFLOW/MESSAGEACK(individual)PING/PONG
Standard-Pulsar-Clients funktionieren damit ohne Anpassung.
Schnellstart
Der Schnellstart zeigt zwei bewusst einfache Varianten: einen Minimalbetrieb ohne Konfigurationsdatei und einen Start mit expliziter HCL-Konfiguration.
Minimaler Start (ohne Konfiguration)
./minipulsar -addr :6650 -db ./minipulsar.db
Der Broker akzeptiert anschließend Pulsar-Clients unter:
pulsar://localhost:6650
Start mit HCL-Konfiguration
./minipulsar -addr :6650 -db ./minipulsar.db -messaging-config ./minipulsar.hcl
Die Struktur und Semantik der HCL-Konfiguration wird in den folgenden Abschnitten erläutert (Namespaces, Policies, Functions und Bindings).
Topics und Persistenzmodell
minipulsar unterscheidet zwischen persistenten und nicht-persistenten Topics.
Persistente Topics verwenden:
- ein append-only SQLite-Log
- einen Cursor pro Subscription
- eine Pending-Tabelle für ausgelieferte, noch nicht bestätigte Nachrichten
Nicht-persistente Topics speichern keine Nachrichten und liefern nur bei aktiven Consumern aus.
Delivery und Backpressure
Die Auslieferung ist vollständig clientgetrieben:
- Consumer steuern Permits über
FLOW - der Broker liefert nur bei verfügbaren Permits
- es existiert keine brokerseitige Priorisierung
Shared Subscriptions werden per Round-Robin bedient. ACKs entfernen Pending-Einträge, ohne den Cursor zu verändern.
Retention und Cleanup
Retention und Subscription-Lebenszyklen werden über die Control Plane definiert:
namespace "persistent://public/default" {
retention_seconds = 10
subscription_timeout_seconds = 30
}
Dieses Verhalten begrenzt Speicherbedarf und Zustandsmenge, insbesondere auf ressourcenarmen Systemen.
Security und Zugriffskontrolle
Security besteht aus zwei Ebenen:
- Verbindungsannahme (
security.mode) - Namespace-basierte Produce-/Consume-Regeln
Fehlende Regeln führen deterministisch zu Zugriff verweigert.
Betriebliche Aspekte und Beobachtbarkeit
Neben dem Protokoll- und Laufzeitmodell adressiert minipulsar auch grundlegende betriebliche Anforderungen, ohne zusätzliche Infrastruktur vorauszusetzen.
Metriken
Der Broker stellt unter http://127.0.0.1:8080/metrics Prometheus-kompatible Metriken bereit.
Diese werden topicfein erfasst und erlauben Einblicke in Verbindungen, Nachrichtenfluss und Auslieferung – ohne externen Agent oder Sidecar.
Logging
Logging erfolgt konsequent 12-Factor-konform über stdout.
Es gibt keine impliziten Log-Dateien, keine versteckten Pfade und keine Logger-Hierarchien mit Seiteneffekten.
Damit lässt sich minipulsar direkt in bestehende Log-Pipelines integrieren oder einfach per Shell beobachten.
Optionale TUI (Text User Interface)
Optional kann minipulsar mit einer interaktiven TUI gestartet werden.
Diese dient explizit nicht dem produktiven Betrieb, sondern als Werkzeug für Entwicklungs- und Testszenarien.
Die TUI zeigt auf einen Blick:
- aktive Client-Verbindungen
- Producer- und Consumer-Aktivität
- Topic-Zugriffe
- aktuelle Broker-Ereignisse
Gerade in lokalen Entwickler-Setups oder bei Protokolltests ist dies hilfreich, um unmittelbar zu sehen, wie sich Clients verbinden und wie sich der Broker verhält.
Lokale Functions und Bindings
Ein wiederkehrendes Problem in Messaging-Systemen ist, dass selbst sehr einfache Transformationen oder Weiterleitungen einen vollständigen Consumer- und Producer-Prozess erfordern. Für triviale Datenpfade entsteht dadurch unnötige Komplexität: zusätzlicher Code, zusätzliche Deployments und zusätzliche Betriebszustände.
minipulsar adressiert dieses Problem mit lokalen Funktionen und Bindings, die es erlauben, einfache Verarbeitungsschritte direkt im Broker zu definieren und explizit an Topics zu binden.
Das Konzept lokaler Nachrichtenverarbeitung existiert in dieser Form bereits in Apache Pulsar. Pulsar Functions erlauben es, eingehende Nachrichten direkt im Kontext des Brokers oder in einer eng angebundenen Laufzeit zu verarbeiten, zu transformieren oder weiterzuleiten – ohne dass dafür ein externer Consumer-Prozess notwendig ist.
Dieses Modell ist konzeptionell stark, weil es Datenpfade explizit beschreibt: Eingang, Verarbeitung und Ziel sind klar definiert und bleiben eng am Messaging-System gekoppelt. Für einfache Transformationen, Filter oder Weiterleitungen reduziert das sowohl Latenz als auch operative Komplexität.
Die Functions in minipulsar sind bewusst daran angelehnt. Ziel war es, eine ähnliche Möglichkeit zu schaffen, jedoch in einer deutlich leichtergewichtigen Form. Statt einer separaten Runtime oder eines komplexen Deployment-Modells werden kleine, deterministische Funktionen lokal im Broker ausgeführt.
Die Wahl von Lua ist dabei bewusst pragmatisch: Die Sprache ist leichtgewichtig, schnell einbettbar und gut geeignet für kurze, klar begrenzte Verarbeitungsschritte – ohne den Broker-Prozess unnötig zu belasten.
Vollständiges Beispiel: Messaging-Konfiguration
security {
mode = "strict"
}
namespace "persistent://public/default" {
produce = ["tester"]
consume = ["tester"]
subscription_timeout_seconds = 30
retention_seconds = 10
}
function "transform" {
path = "transform.lua"
max_runtime = "250ms"
}
binding {
source = "persistent://public/default/temperature.f"
function = "transform"
target = "persistent://public/default/temperature.c"
}
temperature.f publizieren.
Jede eingehende Nachricht wird lokal durch die Funktion transform verarbeitet
und das Ergebnis nach temperature.c weitergeleitet.
Die Lua-Funktion: Temperaturumrechnung
function handle(payload, ctx)
local value = tonumber(payload)
if value == nil then
return payload
end
local celsius = (value - 32) * 5 / 9
return string.format("%.2f", celsius)
end
Die Funktion arbeitet ausschließlich auf dem Payload und bleibt vollständig vom Pulsar-Wire-Format entkoppelt.
Bindings als lokale Datenpfade
Ablauf:
- Nachricht trifft auf
temperature.fein - Broker ruft die Funktion
transformauf - Rückgabewert wird neu publiziert
- Consumer lesen aus
temperature.c
Implementierungsansatz
Wie bereits beim ursprünglichen Protokoll-Experiment wurde auch dieser Prototyp iterativ entwickelt. Wesentliche Teile der Codes wurden durch ein LLM unterstützt implementiert; Architekturentscheidungen, Protokollvalidierung und Tests erfolgten bewusst manuell.
Fazit
minipulsar verbindet Pulsar-Protokolltreue mit einem bewusst kleinen, nachvollziehbaren Laufzeitmodell. Als Prototyp zeigt er, dass sich zentrale Eigenschaften von Pulsar – Zustandsmodelle, Flow-Control und Client-Interaktion – auch ohne verteilte Infrastruktur sauber abbilden lassen.
Damit positioniert sich minipulsar nicht als Konkurrenz, sondern als Ergänzung: als Werkzeug für Edge-Szenarien, für isolierte Umgebungen und für alle Fälle, in denen Verständnis, Portabilität und Klarheit wichtiger sind als maximale Skalierung.
Für mich ist minipulsar damit kein abgeschlossenes Projekt, sondern ein laufendes Denk- und Arbeitsobjekt. Solange sich daran neue Fragen zum Protokoll, zu Zuständen oder zu sinnvollen Vereinfachungen ergeben, werde ich weiter daran arbeiten, Dinge ausprobieren und schrittweise schärfen – nicht mit dem Ziel, „mehr“ zu bauen, sondern um besser zu verstehen, was wirklich nötig ist.