Monolith oder Microservice? Beides mit Go Service Weaver!

Monolith oder Microservice? Beides mit Go Service Weaver!

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.

Microservice oder Monolith?

Microservice oder Monolith?

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…

Service Weaver für Go

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:

  • Bibliotheken und Tools, mit denen man modular aufgebaute Anwendungen erstellen kann.
  • Sogenannte Deployer, die für die Konfiguration der Ausführungsumgebungen zuständig sind.
Service Weaver

Service Weaver

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.

Service Weaver einrichten

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 erstellen

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 einen error als Rückgabewert besitzen!

Wie erstelle ich eine Anwendung?

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

Dependency Injection

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.

Listener für Remoting

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”…

Wie erhalte ich den Status der Anwendung?

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
Service Weaver Dashboard

Service Weaver Dashboard

Deployment mit mehreren Instanzen

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.

Deployment in die Wolken

Mit den entsprechenden Plugins lässt sich die Anwendung jetzt auch in eine Cloud-Umgebung bringen. Dabei wird sie automatisch:

  • Anwendung in einen Container verpackt
  • Container-Images wird hochgeladen
  • Kubernetes Cluster wird erstellt und konfiguriert
  • Load-Balancer und Netzwerk-Infrastruktur wird eingerichtet
  • Anwendung wird verteilt in Kubernetes ausgeführt

    weaver gke deploy weaver.toml
    

Das Beispiel stammt übrigens von der Projekt-Seite

Was gibt es noch??

Service Weaver unterstützt bei allem noch weitere Aspekte einer Anwendungsentwicklung:

  • Integration in die Logging-Infrastruktur der Cloud-Provider
  • Erzeugen von Metriken und entsprechende Integration
  • Tracing
  • Profiling

Wo kann ich mehr erfahren?

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

Kennen Sie schon das Buch zum Thema?

Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.

  • Von den Sprachgrundlagen bis zur Qualitätssicherung
  • Architekturstil verstehen und direkt anwenden
  • Idiomatic Go, gRPC, Go Cloud Development Kit
  • Cloud-native Anwendungen erstellen
Microservices mit Go Buch

zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)

Kontakt

Source Fellows GmbH

Source Fellows GmbH Logo

Lerchenstraße 31

72762 Reutlingen

Telefon: (0049) 07121 6969 802

E-Mail: info@source-fellows.com