Was macht Akka.NET Best Practices?
Wann dieses Skill verwendet wird
Verwende dieses Skill, wenn:
- Entwurf von Actor-Kommunikationsmustern
- Entscheidung zwischen EventStream und DistributedPubSub
- Implementierung von Fehlerbehandlung in Actors
- Verständnis von Supervisionsstrategien
- Wahl zwischen Props-Mustern und DependencyResolver
- Entwurf von Arbeitsverteilung über Knoten hinweg
- Erstellung testbarer Actor-Systeme, die mit oder ohne Cluster-Infrastruktur ausgeführt werden können
- Abstraktion von Cluster Sharding für lokale Testszenarien
Referenzdateien
- work-distribution-patterns.md: Datenbank-Warteschlangen, Akka.Streams-Drosselung, Outbox-Muster
- cluster-local-abstractions.md: GenericChildPerEntityParent, IPubSubMediator, Verkabelung der Ausführungsmodi
- async-cancellation-patterns.md: Actor-begrenzter CancellationToken, verknüpfte CTS, Timeout-Behandlung
1. EventStream vs. DistributedPubSub
Wichtig: EventStream ist NUR LOKAL
Context.System.EventStream ist lokal auf einen einzelnen ActorSystem-Prozess beschränkt. Es funktioniert NICHT über Cluster-Knoten hinweg.
// SCHLECHT: Funktioniert nur auf einem einzelnen Server
// Wenn ein zweiter Server hinzugefügt wird, erhalten Abonnenten auf Server 2 keine Ereignisse von Server 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
Wann EventStream angemessen ist:
- Protokollierung und Diagnose innerhalb eines einzelnen Prozesses
- Lokaler Ereignisbus für echt Einprozessanwendungen
- Entwicklungs-/Testszenarien
DistributedPubSub für Multi-Node verwenden
Für Ereignisse, die Actors über mehrere Cluster-Knoten erreichen müssen, verwende Akka.Cluster.Tools.PublishSubscribe:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// Den DistributedPubSub-Mediator holen
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// In ein Thema veröffentlichen – erreicht alle Abonnenten auf allen Knoten
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
Akka.Hosting-Konfiguration für DistributedPubSub
builder.WithDistributedPubSub(role: null); // Verfügbar auf allen Rollen, oder eine Rolle angeben
Themen-Entwurfsmuster
| Muster | Themenformat | Anwendungsfall |
|---|---|---|
| Pro Benutzer | timeline:{userId} | Timeline-Updates, Benachrichtigungen |
| Pro Entität | post:{postId} | Aktualisierungen zu Beitragsinteraktionen |
| Broadcast | system:announcements | Systemweite Benachrichtigungen |
| Rollenbasiert | workers:rss-poller | Arbeitsverteilung |
2. Supervisionsstrategien
Wichtige Klarstellung: Supervision gilt für KINDER
Eine für einen Actor definierte Supervisionsstrategie legt fest, wie dieser Actor seine Kinder beaufsichtigt, NICHT wie der Actor selbst beaufsichtigt wird.
public class ParentActor : ReceiveActor
{
// Diese Strategie gilt für Kinder von ParentActor, NICHT für ParentActor selbst
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
Standard-Supervisionsstrategie
Die standardmäßige OneForOneStrategy beinhaltet bereits eine Ratenbegrenzung:
- 10 Neustarts innerhalb von 1 Sekunde = Actor wird dauerhaft gestoppt
- Dies verhindert Endlosschleifen von Neustarts
Du brauchst selten eine benutzerdefinierte Strategie, es sei denn, du hast spezifische Anforderungen.
Wann eine benutzerdefinierte Supervision definiert werden sollte
Gute Gründe:
- Ein Actor wirft Ausnahmen, die auf eine nicht behebbare Zustandskorruption hindeuten -> Neustart
- Ein Actor wirft Ausnahmen, die KEINEN Neustart verursachen sollten (erwartete Fehler) -> Fortsetzen
- Fehler bei einem Kind sollten Geschwister betreffen ->
AllForOneStrategyverwenden - Andere Wiederholungsgrenzen als die Standardeinstellung erforderlich
Schlechte Gründe:
- „Nur um sicherzugehen“ – die Standardeinstellung ist bereits sicher
- Du verstehst nicht, was der Actor tut – verstehe es zuerst
3. Fehlerbehandlung: Supervision vs. Try-Catch
Wann Try-Catch verwendet werden sollte (die meisten Fälle)
Try-Catch verwenden, wenn:
- Der Fehler ist erwartet (Netzwerk-Timeout, ungültige Eingabe, externer Dienst nicht verfügbar)
- Du weißt genau, warum die Ausnahme aufgetreten ist
- Du kannst sie ordentlich behandeln (Wiederholung, Fehlerantwort senden, protokollieren und fortfahren)
- Ein Neustart würde nicht helfen (derselbe Fehler würde erneut auftreten)
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// Items verarbeiten...
}
catch (HttpRequestException ex)
{
// Erwarteter Fehler – protokollieren und Wiederholung planen
_log.Warning("Feed {Url} nicht verfügbar: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// Ungültiges Feed-Format – protokollieren und als fehlerhaft markieren
_log.Error("Feed {Url} hat ein ungültiges Format: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
Wann die Supervision die Behandlung übernehmen sollte
Ausnahmen propagieren lassen (Supervision auslösen), wenn:
- Du keine Ahnung hast, warum die Ausnahme aufgetreten ist
- Der Zustand des Actors könnte korrupt sein
- Ein Neustart würde helfen (frischer Zustand, Ressourcen neu verbinden)
- Es handelt sich um einen Programmierfehler (NullReferenceException, InvalidOperationException durch fehlerhafte Logik)
Anti-Pattern: Unbekannte Ausnahmen verschlucken
// SCHLECHT: Ausnahmen verschlucken versteckt Probleme
catch (Exception ex)
{
_log.Error(ex, "Fehler bei der Verarbeitung");
// Actor fährt mit potenziell korruptem Zustand fort
}
// GUT: Bekannte Ausnahmen behandeln, unbekannte propagieren lassen
catch (HttpRequestException ex)
{
// Bekannter, erwarteter Fehler – ordentlich behandeln
_log.Warning("HTTP-Anfrage fehlgeschlagen: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// Unbekannte Ausnahmen propagieren zur Supervision
4. Props vs. DependencyResolver
Wann einfache Props verwendet werden sollten
Props.Create() verwenden, wenn:
- Der Actor benötigt weder
IServiceProvidernochIRequiredActor<T> - Alle Abhängigkeiten können über den Konstruktor übergeben werden
- Der Actor ist einfach und in sich geschlossen
// Einfacher Actor ohne DI-Bedarf
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
Wann DependencyResolver verwendet werden sollte
resolver.Props<T>() verwenden, wenn:
- Der Actor benötigt
IServiceProvider, um abgegrenzte Dienste zu erstellen - Der Actor verwendet
IRequiredActor<T>, um Referenzen zu anderen Actors zu erhalten - Der Actor hat viele Abhängigkeiten, die bereits im DI-Container sind
// Registrierung mit DI
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
Überlegungen zur Fernbereitstellung
Du brauchst fast nie eine Fernbereitstellung. Wenn du keine Fernbereitstellung durchführst (und das tust du wahrscheinlich nicht):
Props.Create(() => new Actor(...))mit Closures ist in Ordnung- Die „Warnung zum Serialisierungsproblem“ trifft nicht zu
Verwende für die meisten Anwendungen Cluster Sharding anstelle von Fernbereitstellung – es übernimmt die Verteilung automatisch.
5. Arbeitsverteilungsmuster
Wenn du viele Hintergrundjobs hast (RSS-Feeds, E-Mail-Versand usw.), verarbeite sie nicht alle auf einmal – dies verursacht Thundering-Herd-Probleme.
Drei Muster zur Lösung:
- Datenbankgesteuerte Arbeitswarteschlange – Verwende
FOR UPDATE SKIP LOCKEDfür eine natürliche knotenübergreifende Verteilung - Akka.Streams-Ratenbegrenzung – Drossle die Verarbeitung innerhalb eines einzelnen Knotens
- Durable Queue (Outbox-Muster) – Datenbankgestützte Outbox für zuverlässige Verarbeitung
Siehe work-distribution-patterns.md für vollständige Codebeispiele.
6. Zusammenfassung häufiger Fehler
| Fehler | Warum es falsch ist | Behebung |
|---|---|---|
| EventStream für knotenübergreifendes Pub/Sub verwenden | EventStream ist nur lokal | DistributedPubSub verwenden |
| Supervision definieren, um einen Actor zu „schützen“ | Supervision schützt Kinder | Die Hierarchie verstehen |
| Alle Ausnahmen abfangen | Versteckt Fehler, korrumpiert Zustand | Nur erwartete Fehler abfangen |
| Immer DependencyResolver verwenden | Fügt unnötige Komplexität hinzu | Einfache Props verwenden, wenn möglich |
| Alle Hintergrundjobs auf einmal verarbeiten | Thundering Herd, Ressourcenerschöpfung | Datenbankwarteschlange + Ratenbegrenzung verwenden |
| Ausnahmen für erwartete Fehler werfen | Löst unnötige Neustarts aus | Ergebnistypen zurückgeben, Messaging verwenden |
7. Kurzreferenz
Entscheidungsbaum für Kommunikationsmuster
Muss zwischen Actors kommuniziert werden?
├── Nur derselbe Prozess? -> EventStream ist in Ordnung
├── Über Cluster-Knoten hinweg?
│ ├── Punkt-zu-Punkt? -> ActorSelection oder bekannte IActorRef verwenden
│ └── Pub/Sub? -> DistributedPubSub verwenden
└── Fire-and-Forget an ein externes System? -> Outbox-Muster in Betracht ziehen
Entscheidungsbaum für Fehlerbehandlung
Ausnahme in einem Actor aufgetreten?
├── Erwarteter Fehler (HTTP-Timeout, ungültige Eingabe)?
│ └── Try-catch, ordentlich behandeln, fortfahren
├── Zustand könnte korrupt sein?
│ └── Supervision neu starten lassen
├── Unbekannte Ursache?
│ └── Supervision neu starten lassen
└── Programmierfehler (Null-Ref, fehlerhafte Logik)?
└── Supervision neu starten lassen, den Fehler beheben
Entscheidungsbaum für Props
Props für Actor erstellen?
├── Actor benötigt IServiceProvider?
│ └── resolver.Props<T>() verwenden
├── Actor benötigt IRequiredActor<T>?
│ └── resolver.Props<T>() verwenden
├── Einfacher Actor mit Konstruktorparametern?
│ └── Props.Create(() => new Actor(...)) verwenden
└── Fernbereitstellung erforderlich?
└── Wahrscheinlich nicht – stattdessen Cluster Sharding verwenden
8. Abstraktionen für Cluster/Lokal-Modus
Für Anwendungen, die sowohl in geclusterten Produktions- als auch in lokalen Testumgebungen laufen müssen, verwende Abstraktionsmuster, um zwischen Implementierungen umzuschalten:
AkkaExecutionMode-Enum – Steuert, welche Implementierungen verwendet werden (LocalTest vs. Clustered)GenericChildPerEntityParent– Ahmt Sharding-Verhalten lokal mit demselbenIMessageExtractornachIPubSubMediator– Abstrahiert DistributedPubSub für austauschbare lokale/Cluster-Implementierungen
Siehe cluster-local-abstractions.md für den vollständigen Implementierungscode.
9. Actor-Protokollierung
ILoggingAdapter verwenden, nicht ILogger
In Actors verwende ILoggingAdapter von Context.GetLogger() anstelle von DI-injiziertem ILogger<T>:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("Verarbeite Nachricht für Benutzer {UserId}", msg.UserId);
_log.Error(ex, "Fehler bei der Verarbeitung von {MessageType}", msg.GetType().Name);
});
}
}
Warum ILoggingAdapter:
- Integriert sich in Akkas Protokollierungspipeline und Supervision
- Unterstützt semantische/strukturierte Protokollierung ab Version 1.5.57
- Methodennamen:
Info(),Debug(),Warning(),Error()(nicht dieLog*-Varianten) - Kein DI erforderlich – wird direkt aus dem Actor-Kontext bezogen
Injiziere ILogger<T> nicht in Actors – es umgeht Akkas Protokollierungsinfrastruktur.
Semantische Protokollierung (v1.5.57+)
// Benannte Platzhalter für bessere Protokollaggregation und Abfragen
_log.Info("Bestellung {OrderId} für Kunden {CustomerId} verarbeitet", order.Id, order.CustomerId);
// Benannte Platzhalter gegenüber positionalen bevorzugen
// Gut: {OrderId}, {CustomerId}
// Vermeiden: {0}, {1}
10. Verwalten asynchroner Vorgänge mit CancellationToken
Wenn Actors asynchrone Vorgänge über PipeTo starten, können diese Vorgänge den Actor überleben, wenn sie nicht ordnungsgemäß verwaltet werden. Wichtige Praktiken:
- Actor-CTS in PostStop – Immer in
PostStop()abbrechen und entsorgen - Neuer CTS pro Vorgang – Vorherigen abbrechen, bevor neue Arbeit gestartet wird
- Token überall übergeben – EF Core-Abfragen, HTTP-Aufrufe usw.
- Verknüpfter CTS für Timeouts – Externe Aufrufe erhalten kurze Timeouts zum Verhindern von Hängenbleiben
- Ordentliche Behandlung – Timeout vs. Herunterfahren in catch-Blöcken unterscheiden
Siehe async-cancellation-patterns.md für den vollständigen Implementierungscode.