Verfolgte man in den letzten Jahren Architekturdiskussionen entstand oft der Eindruck, dass nur Microservice basierte Architekturen zielführend und das Maß aller Dinge sind. Die Aufteilung einer Anwendung in separate Services und deren getrennte Ausführung war angesagt.
Klar kann dieser Ansatz Vorteile bringen, aber auf der anderen Seite darf man die damit entstehenden Herausforderungen nicht klein reden. Der Aufwand, der z. B. in die Bereitstellung der Infrastruktur gesteckt wird, kann den angestrebten Nutzer der Architektur zunichte machen. Oft verdient der Cloud-Anbieter und nicht die Anwendung an Qualität oder Funktionalität.
In Microservice basierten Architekturen verschwimmen gerne die logischen mit den physischen Grenzen der Anwendung bzw. deren enthaltenen Module. Jedes Modul wird als separater Service betrachtet und dementsprechend als separate Einheit umgesetzt und zur Verfügung gestellt. Logische Grenzen werden den physischen Grenzen gleich gesetzt.
In Diskussionen über die Vor- oder Nachteile von Monolithen versus Microservices, werden interessanterweise oftmals diese verschwommenen Grenzen als Argumentation verwendet. Monolithen werden z. B. in Artikeln als schlecht dargestellt, da beim Ausfall eines Moduls, die gesamte Anwendung ausfallen könnte, oder dass Microservices besser entkoppelt sind, da sie physikalisch getrennt sind.
Martin Fowler war 2015 nicht alleine mit seinem Vorschlag mit einer monolithisch aufgebauten Anwendung zu starten und diese später in einzelne Service aufzuteilen.
you shouldn’t start a new project with microservices, even if you’re sure your application will be big enough to make it worthwhile. - Martin Fowler
Eigentlich sollte man sich doch auf eine fachliche Umsetzung konzentrieren und nicht wieder in technischen Details abtauchen…
Das Service Weaver Projekt möchte für Go-Anwendungen die logische Aufteilung von der physikalischen Trennung entkoppeln. Module können als solche implementiert werden und müssen nicht zwangsläufig separat deployt werden. Diese Möglichkeit besteht allerdings weiterhin. Auch zu einem späteren Zeitpunkt.
Write your application as a modular binary. Deploy it as a set of microservices. - Service Weaver
Das Projekt wurde bei Google intern entwickelt und ist als Open Source bei Github verfügbar. Es besteht aus zwei Bestandteilen:
Mit Service Weaver erstellte Anwendungen können über die verschiedenen Deployer, lokal oder in Cloud-Umgebungen ausgeführt werden. Entscheidend ist eine Konfiguration, die zu jeder Zeit angepasst werden kann. Welche und wieviele Instanzen eines Moduls ausgeführt werden sollen ist ebenfalls eine Konfiguration. Nicht jedes Modul muss separat ausgeführt werden, auch das ist eine entsprechende Konfiguration.
Das Entwicklungsmodell für Service Weaver-Komponenten basierte, wie oft bei Go,
auf Codegenerierung. Hierzu wird das Kommandozeilenwerkzeug weaver
benötigt,
das mit folgendem Kommando installiert werden kann:
go install github.com/ServiceWeaver/weaver/cmd/weaver@latest
Zusätzlich kann man die entsprechenden Deployer als Plugins für das
weaver
-Tool installieren. Für die Google Cloud z. B. über:
go install github.com/ServiceWeaver/weaver-gke/cmd/weaver-gke-local@latest
Und dann kann es schon los gehen…
Service Weaver Komponenten bzw. Module werden über Go structs implementiert. Sie
müssen, wie im folgendem Beispiel des reverser
-Struct ein Interface
(Reverser
) besitzen und einen speziellen embedded Type (weaver.Implements
)
nutzen. Dieser ist mittels Generics typisiert auf den entsprechenden
Interface-Typ.
package main
import (
"context"
"github.com/ServiceWeaver/weaver"
)
// Reverser component.
type Reverser interface {
Reverse(context.Context, string) (string, error)
}
// Implementation of the Reverser component.
type reverser struct {
weaver.Implements[Reverser]
}
func (r *reverser) Reverse(_ context.Context, s string) (string, error) {
runes := []rune(s)
n := len(runes)
for i := 0; i < n/2; i++ {
runes[i], runes[n-i-1] = runes[n-i-1], runes[i]
}
return string(runes), nil
}
Jede Methode im Interface muss als ersten Parameter einen
context.Context
als Parameter definieren und einenerror
als Rückgabewert besitzen!
Für jede Anwendung wird eine “Haupt-Komponente” benötigt, die als Einstiegspunkt, ausgeführt wird:
weaver.Run(<CONTEXT>, <MAIN-COMPONENT-INIT-FUNCTION>)
Diese Haupt-Komponente, im Beispiel app
, muss wieder als struct implementiert
werden und den embedded Type weaver.Implements[weaver.Main]
nutzen.
Den Start bzw. die Instanziierung der Haupt-Komponente übernimmt die Service
Weaver Bibliothek. Benötigt wird nur noch die als Parameter der
weaver.Run
-Function übergebene Funktion (im Beispiel serve
), die als
Parameter bereits eine Instanz der Haupt-Komponente erhält.
Diese serve
-Funktion ist quasi unser Einstiegspunkt in die Anwendung.
package main
import (
"context"
"fmt"
"log"
"github.com/ServiceWeaver/weaver"
)
func main() {
if err := weaver.Run(context.Background(), serve); err != nil {
log.Fatal(err)
}
}
type app struct{
weaver.Implements[weaver.Main]
}
func serve(ctx context.Context, app *app) error {
fmt.Println("Hello World")
return nil
}
Soll die Anwendung ohne weitere Abhängigkeit und im ersten Moment ohne die
Reverser
-Komponente von oben ausgeführt werden, generiert man den
entsprechenden, benötigten Code für Service Weaver mit dem weaver
-Kommando und
startet die Anwendung lokal als normale Go-Anwendung:
weaver generate
go run .
Die Anwendung läuft und liefert folgende Ausgabe:
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : 53a1911f-ff78-40a9-b9db-bc4f5da9811d │
╰───────────────────────────────────────────────────╯
Hello World
Soll jetzt noch die Reverser
-Komponente verwendet werden, kann man eine
Referenz in die Main-Komponente mittels reverser weaver.Ref[Reverser]
aufnehmen und die Komponente nutzen. Nichts anderes als Dependency Injection, da
die Bibliothek das Füllen des Attributes erledigt.
type app struct {
weaver.Implements[weaver.Main]
reverser weaver.Ref[Reverser]
}
Die Nutzung in der serve
-Funlktion ist einfach:
func serve(ctx context.Context, app *app) error {
reverse, err := app.reverser.Get().Reverse(ctx, "!dlroW olleH")
if err != nil {
return err
}
fmt.Println(reverse)
return nil
}
Nach einer Generierung und Ausführung erscheint folgende Ausgabe:
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : 2ce66efa-6da1-47a6-b0c8-f945f32e14ff │
╰───────────────────────────────────────────────────╯
Hello World!
Der Anwendung läuft komplett lokal und auch der Aufruf zur Komponente wird als lokaler Methodenaufruf durchgeführt.
Durch die Einbindung eines weaver.Listener
kann die Anwendung um eine
Remote-Schnittstelle erweitert werden. Ein Listener findet dynamisch einen
freien Port auf der Zielmaschine und erstellt dafür entsprechenden
net.Listener
der Standardbibliothek.
Auch hier ist der Einsatz einfach. Die Haupt-Komponente wird mit einer neuen
Referenz (hello weaver.Listener
)…
type app struct {
weaver.Implements[weaver.Main]
reverser weaver.Ref[Reverser]
hello weaver.Listener
}
… und in der Anwendung kann dieser Listener referenziert werden:
func serve(ctx context.Context, app *app) error {
// The hello listener will listen on a random port chosen by the operating
// system. This behavior can be changed in the config file.
fmt.Printf("hello listener available on %v\n", app.hello)
// Serve the /hello endpoint.
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
reversed, err := app.reverser.Get().Reverse(ctx, name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Hello, %s!\n", reversed)
})
return http.Serve(app.hello, nil)
}
Neu generieren und ausführen und die Ausgabe sieht wie folgt aus:
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : f26c5fda-acad-4f03-a68d-f61557381ec2 │
╰───────────────────────────────────────────────────╯
hello listener available on [::]:44093
Der Service steht über HTTP zur Verfügung und kann Anfragen beantworten. So weit so gut. Hierzu bräuchten wir noch keine große “Maschine”…
Mit einem weaver
-Kommando können nun auch Informationen zum Deployment
abgefragt werden:
$ weaver single status
╭──────────────────────────────────────────────────────╮
│ DEPLOYMENTS │
├───────┬──────────────────────────────────────┬───────┤
│ APP │ DEPLOYMENT │ AGE │
├───────┼──────────────────────────────────────┼───────┤
│ hello │ f26c5fda-acad-4f03-a68d-f61557381ec2 │ 2m53s │
╰───────┴──────────────────────────────────────┴───────╯
╭────────────────────────────────────────────────────╮
│ COMPONENTS │
├───────┬────────────┬────────────────┬──────────────┤
│ APP │ DEPLOYMENT │ COMPONENT │ REPLICA PIDS │
├───────┼────────────┼────────────────┼──────────────┤
│ hello │ f26c5fda │ weaver.Main │ 139310 │
│ hello │ f26c5fda │ hello.Reverser │ 139310 │
╰───────┴────────────┴────────────────┴──────────────╯
╭────────────────────────────────────────────╮
│ LISTENERS │
├───────┬────────────┬──────────┬────────────┤
│ APP │ DEPLOYMENT │ LISTENER │ ADDRESS │
├───────┼────────────┼──────────┼────────────┤
│ hello │ f26c5fda │ hello │ [::]:44093 │
╰───────┴────────────┴──────────┴────────────╯
In der Ausgabe sieht man eine Angabe zu REPLICA PIDS
. Bei diesen Werten sieht
man, dass die Anwendung aktuell mit einer einzigen Process ID verknüpft ist.
Also nur eine Instanz im Einsatz.
Wer es etwas grafischer möchte, kann das Dashboard zum Service in einem Browser öffnen:
weaver single dashboard
Bisher läuft die Anwendung als “klassischer” Monolith. Entscheidet man sich nun
für ein “Microservice-Deployment”, kann dies über den weaver multi
Befehl
erreicht werden.
Zuerst wird eine kleine Konfigurationsdatei (weaver.toml
) erstellt. Hier wird
der Name des Anwendungs-Binaries angegeben:
[serviceweaver]
binary = "./myapp"
Danach lässt sich die Anwendung übersetzen und deployen:
$ go build -o myapp .
$ weaver multi deploy weaver.toml
╭───────────────────────────────────────────────────╮
│ app : myapp │
│ deployment : f4c34c4f-303d-45ab-9368-d1e5dc358d34 │
╰───────────────────────────────────────────────────╯
S0101 01:00:00.000000 stdout cf3e874f │ hello listener available on [::]:37251
S0101 01:00:00.000000 stdout a2b2216d │ hello listener available on [::]:37251
Diesmal kann man mit weaver multi dashboard
das Dashboard oder mit weaver
multi status
den Status anzeigen.
╭──────────────────────────────────────────────────────╮
│ DEPLOYMENTS │
├───────┬──────────────────────────────────────┬───────┤
│ APP │ DEPLOYMENT │ AGE │
├───────┼──────────────────────────────────────┼───────┤
│ myapp │ f4c34c4f-303d-45ab-9368-d1e5dc358d34 │ 2m11s │
╰───────┴──────────────────────────────────────┴───────╯
╭──────────────────────────────────────────────────────╮
│ COMPONENTS │
├───────┬────────────┬────────────────┬────────────────┤
│ APP │ DEPLOYMENT │ COMPONENT │ REPLICA PIDS │
├───────┼────────────┼────────────────┼────────────────┤
│ myapp │ f4c34c4f │ weaver.Main │ 148637, 148645 │
│ myapp │ f4c34c4f │ hello.Reverser │ 148659, 148670 │
╰───────┴────────────┴────────────────┴────────────────╯
╭────────────────────────────────────────────╮
│ LISTENERS │
├───────┬────────────┬──────────┬────────────┤
│ APP │ DEPLOYMENT │ LISTENER │ ADDRESS │
├───────┼────────────┼──────────┼────────────┤
│ myapp │ f4c34c4f │ hello │ [::]:37251 │
╰───────┴────────────┴──────────┴────────────╯
… und siehe da, es gibt mehrere Process IDs bei den REPLICA PIDS
für die
einzelnen Komponenten. Die Anwendung wird nun “verteilt” ausgeführt. Natürlich
hier nur lokal, aber immerhin bereits 4 Prozessse. Jede Komponente mit zwei
Instanzen.
Mit den entsprechenden Plugins lässt sich die Anwendung jetzt auch in eine Cloud-Umgebung bringen. Dabei wird sie automatisch:
Anwendung wird verteilt in Kubernetes ausgeführt
weaver gke deploy weaver.toml
Das Beispiel stammt übrigens von der Projekt-Seite
Service Weaver unterstützt bei allem noch weitere Aspekte einer Anwendungsentwicklung:
Natürlich ist der Artikel nicht vollumfänglich und kann die Dokumentation ersetzen. Er soll in erster Linie Lust machen, sich mal Service Weaver anzuschauen.
Auch wenn mich das Programmiermodell nicht ganz überzeugt, da die eigene Anwendung plötzlich Abhängigkeiten zu einer externen Bibliothek für Dependency-Injection und Deployment erhält, ist der Ansatz auf jeden Fall interessant.
Vielleicht kann man mit kleineren “Wrappern” das Abhängigkeitsproblem abmildern oder neuere Versionen liefern hier noch bessere Ansätze. Entsprechende Github Issues bei Golang sind erstellt und die Problematik ist bekannt.
Was auf jeden Fall erreicht wird ist eine Trennung von Anwendungsmodularisierung und Deployment-Modularisierung.
18.03.2024
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com