Alexander Bier

  • Journey
  • CV
  • Portfolio
  • Blog

Alexander Bier

  • Journey
  • CV
  • Portfolio
  • Blog

Hollywood: Go

Alexander
13. September 2024
Softwareentwicklung

Während eines meiner jüngsten Projekte, dem Flugsicherungssystem, das man auf meinem Portfolio einsehen kann, wurde ich mit dem Hollywood Actor Framework und dem Actor-Modell in Kombination mit Go konfrontiert. Es hat sich schnell als die perfekte Lösung für die hohen Anforderungen an Nebenläufigkeit und Skalierbarkeit erwiesen, die dieses Projekt stellte. Aber was macht diese Kombination eigentlich so leistungsfähig? Tauchen wir tiefer ein.

Das Hollywood Actor Framework im Überblick

Das Hollywood Actor Framework basiert auf dem Actor-Modell, einem Paradigma, bei dem „Akteure“ (oder Actors) autonome Einheiten sind, die Nachrichten empfangen und verarbeiten. Ein wichtiges Prinzip des Actor-Modells ist „Don’t call us, we’ll call you“ – Akteure agieren unabhängig und reagieren nur auf eingehende Nachrichten. Diese Architektur eignet sich hervorragend für stark parallele und verteilte Systeme, da sie viele der klassischen Probleme der Parallelverarbeitung vermeidet, wie etwa Race Conditions und Deadlocks.

Warum Go und das Actor-Modell so gut zusammenpassen

Go bietet eine starke Unterstützung für Parallelität und Nebenläufigkeit, hauptsächlich durch zwei zentrale Features: Goroutinen und Channels. Diese Mechanismen sind wie geschaffen für die Implementierung des Actor-Modells.

Goroutinen als Akteure

In Go sind Goroutinen leichtgewichtige Threads, die Akteuren sehr ähnlich sind. Sie laufen unabhängig voneinander und kommunizieren über Channels. Goroutinen lassen sich einfach erstellen und verbrauchen wenig Ressourcen, was sie perfekt für Systeme mit hoher Parallelität macht.

Ein Beispiel für eine einfache Goroutine, die als Akteur fungiert, könnte so aussehen:

func actor(messages chan string) {
    for msg := range messages {
        fmt.Println("Received:", msg)
    }
}

func main() {
    messages := make(chan string)
    go actor(messages)

    messages <- "Message 1"
    messages <- "Message 2"
    close(messages)
}

In diesem Beispiel ist die Funktion actor ein einfacher Akteur, der Nachrichten über den Channel empfängt und verarbeitet. Der Channel dient als Posteingang, in den Nachrichten geschickt werden. Genau wie im Actor-Modell bleibt der Zustand innerhalb der Goroutine und es gibt keine geteilten Daten, was zu einer sicheren und skalierbaren Parallelverarbeitung führt.

Channels als Nachrichten-Transport

Im Actor-Modell kommunizieren Akteure über Nachrichten. In Go wird dies durch Channels realisiert. Channels sind typsicher und bieten eine elegante Möglichkeit, Daten zwischen Goroutinen zu übertragen, ohne auf komplexe Synchronisationsmechanismen zurückgreifen zu müssen.

Hier ein etwas komplexeres Beispiel, bei dem mehrere Akteure über Channels miteinander kommunizieren:

In diesem Beispiel starten wir drei Akteure, die alle auf denselben Nachrichten-Channel hören. Jeder Akteur empfängt Nachrichten und verarbeitet sie unabhängig voneinander. Am Ende des Programms wird der Channel geschlossen, und jeder Akteur signalisiert über das done-Channel, dass er seine Arbeit beendet hat. Diese Art der Architektur eignet sich hervorragend für parallele Arbeitslasten, da die Akteure unabhängig voneinander arbeiten und sich nicht um die Synchronisation kümmern müssen.

func actor(id int, messages chan string, done chan bool) {    
    for msg := range messages {
        fmt.Printf("Actor %d received: %s\n", id, msg)
    }
    done <- true
}
func main() {
    messages := make(chan string)
    done := make(chan bool)
    for i := 1; i <= 3; i++ {
        go actor(i, messages, done)
    }
    for _, msg := range []string{"Start", "Process", "End"} {
        messages <- msg
    }
    close(messages)
    for i := 1; i <= 3; i++ {
        <-done
    }
}

Technische Herausforderungen und Lösungen

Obwohl Go und das Actor-Modell hervorragend zusammenpassen, gibt es dennoch Herausforderungen, insbesondere bei der Nachrichtenverarbeitung, Fehlertoleranz und Zustandsverwaltung.

1. Skalierbare Nachrichtenverarbeitung

Eine der größten Herausforderungen bei verteilten Systemen ist die Verarbeitung von Nachrichten unter hoher Last. Ein Channel in Go kann durch Pufferung helfen, indem er Nachrichten speichert, bis der Akteur bereit ist, sie zu verarbeiten:

messages := make(chan string, 100)  // Puffer für 100 Nachrichten

Durch Pufferung lässt sich die Last gleichmäßig verteilen und vermeiden, dass der Sender blockiert, während der Akteur noch beschäftigt ist.

2. Fehlerisolierung und Wiederherstellung

In verteilten Systemen ist die Isolation von Fehlern entscheidend. Go bietet die Möglichkeit, Panic– und Recover-Mechanismen zu nutzen, um unerwartete Fehler abzufangen und die Goroutine neu zu starten:

func actorWithRecovery(messages chan string) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    for msg := range messages {
        if msg == "error" {
            panic("something went wrong")
        }
        fmt.Println("Processed:", msg)
    }
}

Dieser Mechanismus stellt sicher, dass eine Goroutine, die abstürzt, den Rest des Systems nicht beeinträchtigt. Im Actor-Modell kann man so sicherstellen, dass einzelne Akteure fehlerfrei weiterlaufen, auch wenn ein anderer Akteur abstürzt.

3. Zustandsverwaltung

Die Zustandsverwaltung ist im Actor-Modell ebenfalls wichtig. Jeder Akteur sollte seinen eigenen Zustand haben, um sicherzustellen, dass keine geteilten Zustände zu Race Conditions führen. In Go kann dies einfach durch den Zustand innerhalb der Goroutine realisiert werden. Externe Zustandsänderungen passieren nur durch Nachrichten:

func statefulActor(messages chan int) {
    count := 0
    for msg := range messages {
        count += msg
        fmt.Println("Current count:", count)
    }
}

Hier agiert der Akteur als Zustandsmaschine, die den Wert der empfangenen Nachrichten akkumuliert und sicherstellt, dass der Zustand nur von dieser einen Goroutine verwaltet wird.

Praktische Anwendung: Ein einfaches Beispiel

Der Einstieg in Hollywood ist unkompliziert. Hier ein einfaches Beispiel für die Erstellung und Nutzung eines Akteurs:

package main

import (
    "fmt"
    "github.com/anthdm/hollywood/actor"
)

type helloer struct{}

func newHelloer() actor.Receiver {
    return &helloer{}
}

func (h *helloer) Receive(ctx *actor.Context) {
    switch msg := ctx.Message().(type) {
    case actor.Initialized:
        fmt.Println("helloer has initialized")
    case actor.Started:
        fmt.Println("helloer has started")
    case actor.Stopped:
        fmt.Println("helloer has stopped")
    case string:
        fmt.Println("Received message:", msg)
    }
}

func main() {
    engine, err := actor.NewEngine(actor.NewEngineConfig())
    if err != nil {
        panic(err)
    }

    pid := engine.Spawn(newHelloer, "hello")
    engine.Send(pid, "Hello World!")
}

In diesem Beispiel erstellen wir eine helloer-Struktur, die als Akteur fungiert und auf Nachrichten reagiert. Die Receive-Methode verarbeitet unterschiedliche Nachrichtentypen und Lebenszyklusereignisse des Akteurs.

Fortgeschrittene Features und Nutzung

Neben den grundlegenden Funktionen bietet Hollywood auch erweiterte Features:

Fehlerbehandlung und Wiederherstellung

Hollywood garantiert die Zustellung von Nachrichten auch bei Akteur-Ausfällen. Nachrichten, die nicht zugestellt werden können, landen in der Dead Letter Queue, die durch das Eventstream-Feature überwacht wird.

Verteilte Systeme und Cluster

Hollywood unterstützt die Kommunikation zwischen Akteuren in einem Cluster und ermöglicht die Erstellung von verteilten, selbstentdeckenden Akteuren. Die Konfiguration erfolgt über das Remote-Paket und Protobuf für die Serialisierung von Nachrichten.

Eventstream für Monitoring und Fehlerbehandlung

Das Eventstream-Feature erlaubt es, auf Systemereignisse wie Akteur-Abstürze und Netzwerkausfälle zu reagieren. Diese Ereignisse können verwendet werden, um systemweite Monitoring- und Fehlerbehandlungsstrategien zu implementieren.

Hier ein Beispiel für die Nutzung des Eventstreams:

func main() {
    engine, err := actor.NewEngine(actor.NewEngineConfig())
    if err != nil {
        panic(err)
    }

    // Actor for monitoring events
    monitor := func(c *actor.Context) {
        switch msg := c.Message().(type) {
        case actor.DeadLetterEvent:
            fmt.Println("Dead Letter Event:", msg)
        }
    }

    engine.Spawn(monitor, "monitor")

    // Simulate failure
    pid := engine.Spawn(newHelloer, "hello")
    engine.Send(pid, "Failing message causing dead letter")
}

Integration und Anpassung

Hollywood bietet umfassende Anpassungsmöglichkeiten, darunter:

  • Middleware: Ermöglicht das Hinzufügen benutzerdefinierter Middleware für das Logging oder die Verarbeitung von Nachrichten.
  • Logging: Hollywood nutzt den log/slog-Paketstandard, um Protokolle zu verwalten. Benutzer können den Logger anpassen, um spezifische Anforderungen zu erfüllen.
4/5

Schreibe einen Kommentar Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

  • Willkommen auf meinem Blog!

    Willkommen auf meinem Blog!

    Allgemein
  • Docker für Einsteiger

    Docker für Einsteiger

    Softwareentwicklung
  • Vue 3: Composition API vs. Options API

    Vue 3: Composition API vs. Options API

    Webentwicklung
  • Der Cost-Average-Effekt

    Der Cost-Average-Effekt

    Begriffe, Finanzen
  • The Big Five for Life

    The Big Five for Life

    Inspiration
  • Das Wasserfallmodell

    Das Wasserfallmodell

    Projektmanagement

Agile Aktien Barrierefreiheit Buchvorstellung Bücher Cost-Average-Effekt DFS Die Siedler online Digitalisierung Digitalisierungstrategie Docker Einstieg Entscheidungsfindung Erklärung ETF Finanzen Gaming GCS GENVELO Go Index Indizes Kanban Kassensystem Kosten-Nutzen-Analyse Messe MFN Berlin Museum für Naturkunde Persönliche Weiterentwicklung Portfolio Projektmanagement Prozessautomatisierung Scrum Softwareentwicklung Spreads SWOT-Analyse Szenarioanalyse Toto Guillaume Volksbank vue Wasserfallmodell ZDE Zweck der Existenz

© 2023

Alexander Bier

  • Startseite
  • Datenschutz
  • Impressum
Zustimmung verwalten
Um dir ein optimales Erlebnis zu bieten, verwenden wir Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wenn du diesen Technologien zustimmst, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn du deine Zustimmung nicht erteilst oder zurückziehst, können bestimmte Merkmale und Funktionen beeinträchtigt werden.
Funktional Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt. Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
Optionen verwalten Dienste verwalten Verwalten von {vendor_count}-Lieferanten Lese mehr über diese Zwecke
Einstellungen ansehen
{title} {title} {title}