Das Thema Microservices-Architektur ist in den letzten Jahren immer populärer geworden. Der Grund dafür liegt in den zahlreichen Vorteilen, die der modulare Architekturstil mit sich bringt, vor allem wenn es um die Entwicklung komplexer Anwendungen geht.
Die Microservices-Architektur eignet sich gut für Anwendungen, die Transaktionen schnell verarbeiten und ein hohes Verkehrsaufkommen sowie eine große Anzahl gleichzeitiger Benutzer bewältigen müssen. Aus diesem Grund setzen Unternehmen, die SaaS-Anwendungen entwickeln, sei es eine E-Commerce-Plattform, eine Social-Media-App, ein Streaming-Dienst oder eine Essensliefer-App, auf Microservices, um ihre Lösungen skalierbar und ausfallsicher zu machen.
Wir haben hier bereits die Vor- und Nachteile der Implementierung einer Microservices-Architektur behandelt und Microservices mit monolithischen Architekturen verglichen.
Dieser Artikel wurde für .NET-Entwickler und Lösungsarchitekten geschrieben, die ihre Microservices-basierte Anwendung verbessern und den richtigen Ansatz für die Implementierung der Kommunikation zwischen Microservices finden möchten.
Können Microservices völlig unabhängig sein?
Die auf Microservices basierende Anwendungsarchitektur ist so konzipiert, dass kleine Teile eines großen Systems unabhängig voneinander arbeiten können. Jeder Microservice ist für eine bestimmte Funktion des Systems zuständig, und jeder Service kann mit einem anderen Datenspeicher verbunden sein. Ein Dienst kann beispielsweise Daten in einer Microsoft SQL-Datenbank speichern, ein anderer kann NoSQL-Speicher wie MongoDB usw. verwenden.
Auf der anderen Seite gibt es viele Fälle, in denen wir keine völlig unabhängigen Teile erstellen können. Zum Beispiel erstellen wir einen Security-Microservice, der für die Sicherheit und Autorisierung der Benutzer zuständig ist. Außerdem benötigen wir den Microservice Communication, um Nachrichten, Benachrichtigungen und andere Instanzen der Interaktion mit dem Benutzer zu verfolgen. Beide Microservices hängen von der gleichen Benutzerbasis ab, sind aber unabhängig in der Art der gespeicherten Benutzerinformationen und bewahren nur die Daten auf, die für ihren Betrieb wichtig sind. In einem solchen Szenario besteht die Hauptidee darin, die Abhängigkeit eines jeden Dienstes von einem anderen Dienst zu minimieren. Manchmal müssen wir dienstübergreifende Abfragen durchführen oder sogar einige ähnliche Daten an verschiedenen Orten speichern. An diesem Punkt sollten wir den Microservices die Möglichkeit geben, miteinander zu kommunizieren.
Warum müssen wir die Kommunikation zwischen den Diensten aufbauen?
Wie wir bereits erwähnt haben, kann es sehr schwierig sein, eine völlig unabhängige Architektur zu erstellen, da Ihre Microservices in einigen Fällen miteinander kommunizieren müssen. Übrigens kann eine Kommunikation auch dann erforderlich sein, wenn Sie ähnliche Daten in verschiedenen Speichern speichern. Als Beispiel nehmen wir eines unserer bestehenden Projekte, bei dem wir die Kommunikation zwischen den Services implementieren mussten.
Wir haben eine Anwendung, die aus einer Liste von Microservices besteht. Für unser Beispiel werden wir nur einige von ihnen betrachten – Autorisierung, Analytik, Bestellungen, Gateway und E-Mail-Service. Jeder Dienst verfügt über eine eigene Datenbank, um Daten unabhängig zu speichern.
Schauen wir uns kurz den Zweck jedes Microservices an:
- Gateway. Verteilt Anfragen an andere Microservices auf der Grundlage ihres Ziels. Andere Microservices akzeptieren nur Anfragen vom Gateway-Microservice. Gateway ist der einzige Microservice, der für externe Anfragen offen ist.
- Autorisierung. Bietet die wichtigsten Sicherheitsfunktionen für die gesamte Anwendung – Benutzeranmeldung, Abmeldung, Registrierung, Passwortwiederherstellung usw.
- E-Mail. Bietet die Möglichkeit, verschiedene E-Mail-Nachrichten an Benutzer zu senden, z. B. Erinnerungen, Newsletter, E-Mails zur Passwortwiederherstellung usw.
- Bestellungen. Dies ist der Microservice, der für die Ausführung aller wichtigen Bestellvorgänge verantwortlich ist, wie z. B. Erstellen, Aktualisieren, Löschen, Anzeigen, Berechnen, Preisgestaltung, Verfolgen von Bestellungen und vieles mehr. Dieser Dienst enthält alle Informationen über Bestellungen im System und ist der wichtigste Datenspeicher der Anwendung.
- Analytik. Bietet einige Funktionen zur Anzeige von Statistiken und Analysen, z. B. die Anzahl der offenen Aufträge im System, wie lange sie offen waren, und andere Daten, die für eine effektive Entscheidungsfindung erforderlich sind. Dieser Dienst speichert zwar nicht jedes Detail der Bestellung, ermöglicht aber einen schnellen und bequemen Zugriff auf wichtige Daten, die in einem optisch ansprechenden Format dargestellt werden.
Wie Sie sehen können, ist jeder Microservice für unterschiedliche Vorgänge zuständig und benötigt meist nicht denselben Datenspeicher, da die Daten für jeden Microservice recht unterschiedlich sind. Die Ausnahme ist der Microservice Analytik. Er sollte die gleichen Informationen über Bestellungen im System haben. Sollten wir jedoch denselben Datenspeicher verwenden?
Warum ist es besser, für jeden Microservice eine andere Datenhaltung zu verwenden?
Wenn Sie eine auf Microservices basierende Architektur für Ihre Anwendung aufbauen, sollten Sie auf die Skalierbarkeit Ihrer Anwendung achten. Wenn Sie denselben Datenspeicher für verschiedene Microservices verwenden, werden Sie Probleme mit der Skalierbarkeit bekommen. Wenn einer Ihrer Dienste überlastet ist, kann sich dies auch auf die Datenspeicherung auswirken. Sie sollten also mehr Elemente skalieren, um die Leistung in einem Ihrer Dienste zu verbessern. In diesem Fall sind die Anwendungsinstanzen viel größer. Beachten Sie, dass die in einem Speicher abgelegten Daten die Leistung beeinträchtigen können. Außerdem kann dies zu einer hohen Last führen.
Um diese Probleme zu vermeiden, können Sie für jeden Microservice einen anderen Datenspeicher verwenden. Wenn Sie den Speicher so gestalten, dass er den Anforderungen eines bestimmten Dienstes entspricht, können Sie die Leistung von Operationen mit Datenverarbeitung verbessern. Außerdem kann es die Entwicklung und die Arbeit mit Daten vereinfachen, da die Daten in einem bequemen, gebrauchsfertigen Format gespeichert werden und eine Datenkonvertierung vermieden wird.
Jegliche Änderungen an Ihrem Microservice führen nicht zu versteckten Fehlern in anderen Microservices, da diese unabhängig sind. Wenn Sie denselben Speicher verwenden und eine Entscheidung zur Änderung eines Entitätsmodells treffen, müssen Sie diese Änderungen nicht in jedem anderen Dienst wiederholen, der denselben Datenspeicher verwendet.
Für unseren Fall mit den Microservices Analytics und Orders haben wir einige zusätzliche Details. Der Dienst „Orders“ kann die Bestellungen jederzeit ändern und hat eine hohe Last. Der Analysedienst sollte nur einige Daten für Diagramme und Dashboards berechnen. Es besteht keine Notwendigkeit, alle Änderungen sofort zu erhalten, so dass wir diese Operationen nach Zeitplan (stündlich, täglich usw.) durchführen können.
Zusammenfassend lässt sich sagen, dass wir unsere Daten in diesen beiden Diensten synchronisieren und sie neu berechnen müssen, wenn Dateneinheiten geändert werden. Dies lässt sich mit Hilfe der Kommunikation zwischen den Diensten unter Verwendung von Nachrichtenwarteschlangen bewerkstelligen. Wir haben zu diesem Zweck einen Azure Service Bus Message Broker verwendet.
Was ist Azure Service Bus?
Azure Service Bus ist ein vollständig verwalteter Nachrichtenbroker für die Unternehmensintegration. Azure Service Bus kann Anwendungen und Dienste entkoppeln und bietet eine zuverlässige und sichere Plattform für die asynchrone Übertragung von Daten und Zuständen. Die Hauptidee besteht darin, beliebige Daten zwischen den Diensten mithilfe von Nachrichten zu senden. Diese Nachrichten liegen im Binärformat vor und können JSON, XML oder sogar einfachen Text enthalten.
Die Hauptmerkmale von Azure Service Bus sind wie folgt:
- Senden von Nachrichten über Warteschlangen. Der Empfänger sollte eine spezielle Warteschlange abhören, und wenn eine Nachricht an diese Warteschlange gesendet wird, empfängt der Hörer sie. Die Nachrichten werden an die Warteschlange gesendet und warten auf jeden Hörer, der diese Nachrichten empfangen möchte.
- Themen und Abonnements. Sie können Verleger und Abonnenten erstellen, die bestimmte Themen abhören. So kann ein Verleger eine Nachricht an verschiedene Empfänger weitergeben (1:n-Beziehung).
- Zeitplanung. Sie können eine Nachricht für eine bestimmte Zeit planen, und die Abonnenten erhalten sie zu einem bestimmten Zeitpunkt.
Warteschlangen
Alle Nachrichten werden an Warteschlangen gesendet und von diesen empfangen. Sie senden lediglich eine Nachricht an eine bestimmte Warteschlange, und die Nachrichten warten auf einen Empfänger, der diese Nachricht empfängt und verarbeitet. Am Ende der Nachrichtenverarbeitung sollte der Empfänger eine Nachricht abschließen, um sie aus der Warteschlange zu entfernen.
Sie können Ihre Warteschlange auch so konfigurieren, dass Ihre Nachricht beim ersten Empfang automatisch abgeschlossen wird. In diesem Fall wird die Nachricht gelöscht, sobald der Zuhörer sie erhalten hat.
Themen
Es gibt noch eine weitere Möglichkeit, Nachrichten zu verwenden, und zwar auf der Grundlage von Themen und Abonnements. Sie können einige Filter, Lebenszeiten und sogar Bedingungen konfigurieren, um Nachrichten in verschiedenen Instanzen zu empfangen. Darüber hinaus können verschiedene Empfänger Nachrichten von einem Absender bearbeiten. Ein Abonnent kann eine Kopie jeder an das Thema gesendeten Nachricht erhalten, aber die Nachrichten können ablaufen oder automatisch gelöscht werden.
Wir zogen die Verwendung von Warteschlangen vor, da wir nur einen Dienst (Analytics) haben, der auf alle Nachrichten über Auftragsaktualisierungen hören sollte.
Implementierung von Azure Service Bus
Azure Service Bus ist wirklich einfach zu bedienen. Wir können diese Funktionalität in ein paar Zeilen implementieren. Aber zuerst müssen wir zum Azure-Portal gehen und eine Service-Bus-Ressource erstellen.
Der nächste Schritt ist das Ausfüllen der Hauptfelder – Name, Ressourcengruppe, Standort. Wenn alles erfolgreich ist, erhalten Sie eine Benachrichtigung darüber und können Ihren Service-Bus-Namensraum sehen. Um mit dem Service Bus arbeiten zu können, müssen wir eine Verbindungszeichenfolge erhalten. Dazu klicken Sie auf „Gemeinsame Zugriffsrichtlinien“. Sie müssen diese Daten in Ihre Anwendungskonfigurationsdatei eingeben.
Der nächste Schritt ist die Erstellung einer Warteschlange. Dies kann über das Azure-Portal erfolgen.
In diesem Stadium können Sie Ihre Warteschlange konfigurieren. Sie müssen den Namen Ihrer Warteschlange festlegen, und Sie können ohne besondere Einstellungen fortfahren.
Es gibt jedoch einige wichtige Dinge, die Sie über die Konfigurationswerte wissen sollten:
- Lebenszeit der Nachricht. Zeigt an, wie lange Ihre Nachricht noch empfangen und verarbeitet werden kann. Wenn Ihre Nachricht abgelaufen ist, wird sie in die Warteschlange für tote Briefe verschoben.
- Dauer der Sperre. Wenn ein Hörer eine Nachricht empfängt, hat er eine gewisse Zeit, um sie zu verarbeiten. Während dieser Zeit wird die Nachricht gesperrt, und niemand sonst kann diese Nachricht empfangen. Wenn diese Zeitspanne verstrichen ist, kann die Nachricht von jeder Instanz, die diese Warteschlange abhört, empfangen werden.
- Dead Letter Warteschlange. Eine weitere Sache, die Sie wissen sollten, ist, dass, wenn eine Nachricht zehnmal empfangen und nicht abgeschlossen wird, diese Nachricht an die Dead Letter Warteschlange gesendet wird. Wenn Ihre Anwendung während der Nachrichtenverarbeitung einen unbehandelten Fehler auslöst, wird diese Nachricht an die Warteschlange zurückgegeben.
Der erste Schritt ist die Installation des Microsoft.Azure.ServiceBus-Pakets. Dies kann über den NuGet Package Manager durchgeführt werden.
Um Ihre erste Nachricht an die Warteschlange zu senden, müssen Sie sich mit der Azure Service Bus-Warteschlange verbinden. Dies kann auf die folgende Weise geschehen:
const string ServiceBusConnectionString = "your connection string";
const string QueueName = "your queue name";
static IQueueClient queueClient;
static async Task MainAsync() {
queueClient = new QueueClient(ServiceBusConnectionString, QueueName);
// Register QueueClient's MessageHandler and receive messages in a loop
RegisterOnMessageHandlerAndReceiveMessages();
Console.ReadKey();
await queueClient.CloseAsync();
}
Nun müssen wir einen Message-Handler registrieren, der die Warteschlange abhört.
static void RegisterOnMessageHandlerAndReceiveMessages() {
// Configure the MessageHandler Options in terms of exception handling, number of concurrent messages to deliver etc.
var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler) {
// Maximum number of Concurrent calls to the callback `ProcessMessagesAsync`, set to 1 for simplicity.
// Set it according to how many messages the application wants to process in parallel.
MaxConcurrentCalls = 1,
// Indicates whether MessagePump should automatically complete the messages after returning from User Callback.
// False below indicates the Complete will be handled by the User Callback as in `ProcessMessagesAsync` below.
AutoComplete = false
};
// Register the function that will process messages
queueClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
}
Bevor wir unsere erste Nachricht an die Warteschlange senden, müssen wir eine Methode vorbereiten, um die von der Warteschlange empfangenen Nachrichten zu verarbeiten. Im obigen Code haben wir die Handler-Methode mit dem Namen „ProcessMessagesAsync“ registriert. Implementieren wir diese Methode auf folgende Weise:
static async Task ProcessMessagesAsync(Message message, CancellationToken token) {
// Process the message
Console.WriteLine($"Received message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");
// Complete the message so that it is not received again.
await queueClient.CompleteAsync(message.SystemProperties.LockToken);
}
Beachten Sie, dass Sie Ihre Nachricht erneut erhalten, wenn Sie sie nicht mit der CompleteAsync-Methode abschließen oder sie auf automatische Vervollständigung setzen.
Der letzte Schritt ist das Senden unserer ersten Nachricht an die Warteschlange.
// Create a new message to send to the queue
var message = new Message(Encoding.UTF8.GetBytes(messageBody));
// Send the message to the queue
await queueClient.SendAsync(message);
Nach diesem Schritt können Sie zum Azure-Portal gehen und sehen, dass der Nachrichtenzähler jetzt höher als Null ist. Wenn Sie Ihre Konsole überprüfen, sehen Sie, dass Sie eine neue Nachricht aus der Warteschlange erhalten haben.
Wie Sie sehen, ist es wirklich einfach zu bedienen. Sie brauchen nicht viel Code zu schreiben, um das Interservice-Messaging mit Azure Service Bus zu implementieren. Außerdem müssen Sie die Verarbeitung Ihrer Nachrichten nicht kontrollieren. Wenn etwas schief geht und Sie eine Ausnahme erhalten, wird diese Nachricht automatisch in die Warteschlange zurückgeschickt. Darüber hinaus müssen Sie sich nicht um einen endlosen Nachrichtenempfang (Schleife) kümmern, denn wenn die Nachricht mehrmals einen Fehler verursacht, wird sie in die Dead Letter-Warteschlange gestellt. Danach wird die Nachricht nicht mehr empfangen.
Nach der Aktualisierung der Bestellung ergibt sich der folgende Ablauf:
- Der Order-Microservice sendet eine Nachricht mit der aktualisierten Bestellung an die Order-Warteschlange;
- Der Analytics-Microservice, der diese Warteschlange abhört, empfängt diese Nachricht;
- Der Analytics-Microservice verarbeitet diese Nachricht, aktualisiert die Entitäten in Bezug auf das neue Auftragsmodell und löst eine Neuberechnung der Analysen aus;
- Der Analytics-Microservice berechnet einige Analysedaten neu und schließt diese Nachricht ab.
In diesem Fall haben wir die aktuellen Analysedaten direkt nach der Aktualisierung der Bestellung. Wir sollten den gleichen Ablauf für die Erstellung und Löschung von Bestellungen implementieren.
Vorteile dieses Ansatzes:
- Wir können die Daten zwischen den Diensten leicht synchronisieren;
- Es müssen nicht beide Dienste online sein, und wir können den Analytics-Microservice einfach nach einem Zeitplan laufen lassen, um Nachrichten zu empfangen und zu verarbeiten, die erforderlichen Daten zu berechnen und den Microservice in den Ruhezustand zu versetzen;
- Wenn die Verarbeitung einer Nachricht fehlschlägt, wird sie automatisch erneut gesendet. Es müssen keine speziellen Funktionen oder sogar try-catch-Anweisungen erstellt werden;
- Die Nachricht kann nicht verloren gehen. Selbst wenn sie nicht verarbeitet werden kann, geht sie in die Dead Letter-Warteschlange, und Sie können Ihre fehlgeschlagenen Nachrichten leicht kontrollieren.
Wichtige Hinweise:
- Die Daten werden nicht sofort synchronisiert. Sie werden zunächst in die Warteschlange gestellt, dann empfangen und schließlich verarbeitet. Wenn Sie eine sofortige Datensynchronisierung benötigen, sollten Sie direkte Anfragen verwenden, um eine Entität zu aktualisieren, aber dies kann die Leistung beeinträchtigen und zusätzliche Probleme verursachen;
- Sie sollten Instanzen in der Anwendung kontrollieren, die auf die Warteschlange hören. Verwenden Sie Singleton-Klassen, um die Warteschlange abzuhören, sonst kann es zu einem Fehler kommen, wenn die gleiche Nachricht mehrmals von der gleichen Anwendung empfangen wird.
Dead Letter-Warteschlange in Azure Service Bus
Wenn bei der Verarbeitung von Nachrichten mehrmals Fehler oder Misserfolge auftreten oder die Nachricht abgelaufen ist, kann sie in die Dead Letter-Warteschlange verschoben werden. Diese Nachrichten werden in dieser Warteschlange gespeichert, bis Sie sie löschen oder verarbeiten. Die Dead Letter-Warteschlange kann Nachrichten an den Empfänger senden, und Sie können mit diesen Nachrichten wie in jeder anderen Warteschlange arbeiten. Der einzige Unterschied zwischen diesen Warteschlangen ist der Name. Wenn Sie die Dead-Letter-Warteschlange abhören wollen, sollten Sie die folgende Anweisung verwenden:
const string ServiceBusConnectionString = "your connection string";
const string QueueName = "your queue name";
static IQueueClient deadLetterQueueClient;
static async Task MainAsync() {
deadLetterQueueClient = new QueueClient(ServiceBusConnectionString, EntityNameHelper.FormatDeadLetterPath(QueueName));
RegisterOnMessageHandlerAndReceiveMessages();
Console.ReadKey();
await queueClient.CloseAsync();
}
Sie sollten einfach den Namen Ihrer Warteschlange an die MethodeFormatDeadLetterPath von EntityNameHelper übergeben und das Ergebnis als Warteschlangennamen verwenden. Für jede Warteschlange, die Sie in Azure Service Bus erstellt haben, gibt es eine zusätzliche Dead Letter-Warteschlange.
Außerdem können Sie Dead Letter im Azure Portal ohne Code-Implementierung anzeigen. Sie können jedoch dieses Codebeispiel verwenden, um z. B. eine Dead Letter-Verarbeitungsfunktion zu erstellen, die Sie über Fehler in Ihrer Anwendung informiert oder diese sogar behebt.
Zusammenfassung
In manchen Fällen können wir keine völlig unabhängigen Microservices erstellen. An diesem Punkt müssen wir möglicherweise die Kommunikation zwischen den Diensten implementieren, was durch Interservice-Messaging geschehen kann. Wir empfehlen Ihnen, Azure Service Bus zu verwenden, um Daten in der Anwendung zu übertragen, Interservice-Messaging zu erstellen und Daten zwischen Microservices in Ihrer Anwendung zu synchronisieren.
Diese Funktion ist wirklich einfach in jede bestehende Anwendung zu implementieren. Mit Hilfe dieses Dienstes können Sie die Systemlast verringern, da Ihre Anwendung in der Lage ist, Aufgaben nacheinander zu verarbeiten und nicht gleichzeitig, wie bei direkten Anfragen.
In einer auf Microservices basierenden Architektur spielt die Kommunikation zwischen den Microservices eine wichtige Rolle, wenn es um die Leistung geht. Je nach Ihren Anforderungen müssen Sie also den richtigen Ansatz für die Kommunikation zwischen den Diensten wählen.