Freigeben über


Gängige Webanwendungsarchitekturen

Tipp

Dieser Inhalt ist ein Auszug aus dem eBook, Architect Modern Web Applications mit ASP.NET Core und Azure, verfügbar auf .NET Docs oder als kostenloses herunterladbares PDF, das offline gelesen werden kann.

Miniaturansicht des E-Books „Architect Modern Web Applications with ASP.NET Core and Azure“.

"Wenn Sie denken, dass gute Architektur teuer ist, probieren Sie schlechte Architektur aus." - Brian Foote und Joseph Yoder

Die meisten herkömmlichen .NET-Anwendungen werden als einzelne Einheiten bereitgestellt, die einer ausführbaren Datei oder einer einzelnen Webanwendung entsprechen, die in einer einzelnen IIS-Appdomäne ausgeführt wird. Dieser Ansatz ist das einfachste Bereitstellungsmodell und dient vielen internen und kleineren öffentlichen Anwendungen sehr gut. Selbst bei dieser einzelnen Bereitstellungseinheit profitieren die meisten nicht trivialen Geschäftsanwendungen jedoch von einer logischen Trennung in mehrere Ebenen.

Was ist eine monolithische Anwendung?

Eine monolithische Anwendung ist eine, die in Bezug auf ihr Verhalten völlig eigenständig ist. Es kann mit anderen Diensten oder Datenspeichern interagieren, während der Betrieb ausgeführt wird, aber der Kern des Verhaltens wird innerhalb seines eigenen Prozesses ausgeführt, und die gesamte Anwendung wird in der Regel als einzelne Einheit bereitgestellt. Wenn eine solche Anwendung horizontal skaliert werden muss, wird die gesamte Anwendung in der Regel auf mehreren Servern oder virtuellen Computern dupliziert.

All-in-One-Anwendungen

Die kleinste mögliche Anzahl von Projekten für eine Anwendungsarchitektur ist eine. In dieser Architektur ist die gesamte Logik der Anwendung in einem einzelnen Projekt enthalten, zu einer einzigen Assembly kompiliert und als einzelne Einheit bereitgestellt.

Ein neues ASP.NET Core-Projekt, ob in Visual Studio oder über die Befehlszeile erstellt, beginnt als einfacher "All-in-One"-Monolith. Sie enthält das gesamte Verhalten der Anwendung, einschließlich Präsentations-, Geschäfts- und Datenzugriffslogik. Abbildung 5-1 zeigt die Dateistruktur einer Einzelprojekt-App.

Ein einzelnes Projekt ASP.NET Core-App

Abbildung 5-1. Ein einzelnes Projekt ASP.NET Core-App.

In einem einzigen Projektszenario wird die Trennung von Bedenken durch die Verwendung von Ordnern erreicht. Die Standardvorlage enthält separate Ordner für die Verantwortlichkeiten des MVC-Musters, nämlich Modelle, Ansichten und Controller, sowie zusätzliche Ordner für Daten und Services. In dieser Anordnung sollten Präsentationsdetails so weit wie möglich auf den Ordner "Ansichten" beschränkt sein, und Details zur Datenzugriffsimplementierung sollten auf Klassen beschränkt sein, die im Ordner "Daten" gespeichert sind. Geschäftslogik sollte sich in Diensten und Klassen im Ordner "Models" befinden.

Obwohl einfach, weist die monolithische Lösung mit einem einzigen Projekt einige Nachteile auf. Da die Größe und Komplexität des Projekts wächst, wird auch die Anzahl der Dateien und Ordner weiter wachsen. Bedenken der Benutzeroberfläche (Modelle, Ansichten, Controller) befinden sich in mehreren Ordnern, die nicht alphabetisch gruppiert sind. Dieses Problem wird nur schlimmer, wenn zusätzliche Konstrukte auf Benutzeroberflächesebene, z. B. Filter oder ModelBinders, in ihren eigenen Ordnern hinzugefügt werden. Geschäftslogik wird zwischen den Ordnern "Modelle" und "Dienste" verteilt, und es gibt keinen eindeutigen Hinweis darauf, welche Klassen in welchen Ordnern von welchen anderen Ordnern abhängig sein sollen. Dieser Mangel an Organisation auf Projektebene führt häufig zu Spaghetti-Code.

Um diese Probleme zu beheben, entwickeln sich Anwendungen häufig zu Multiprojektlösungen, bei denen jedes Projekt als in einer bestimmten Ebene der Anwendung betrachtet wird.

Was sind Ebenen?

Da Anwendungen komplexer werden, besteht eine Möglichkeit, diese Komplexität zu verwalten, darin, die Anwendung nach ihren Zuständigkeiten oder Bedenken aufzulösen. Dieser Ansatz folgt der Trennung von Bedenkenprinzip und kann dazu beitragen, eine wachsende Codebasis organisiert zu halten, damit Entwickler leicht finden können, wo bestimmte Funktionen implementiert werden. Layered Architecture bietet jedoch eine Reihe von Vorteilen, die nicht nur in der Codeorganisation enthalten sind.

Durch das Organisieren von Code in Ebenen können allgemeine Funktionen auf niedriger Ebene in der gesamten Anwendung wiederverwendet werden. Diese Wiederverwendung ist vorteilhaft, da weniger Code geschrieben werden muss und die Anwendung die Standardisierung auf eine einzige Implementierung ermöglichen kann, gemäß dem Prinzip "Don't Repeat Yourself" (DRY).

Mit einer mehrschichtigen Architektur können Anwendungen Beschränkungen erzwingen, welche Ebenen mit anderen Ebenen kommunizieren dürfen. Diese Architektur hilft bei der Kapselung. Wenn eine Ebene geändert oder ersetzt wird, sollten nur die Ebenen betroffen sein, die damit arbeiten. Durch das Einschränken, welche Ebenen von welchen anderen Ebenen abhängig sind, können die Auswirkungen von Änderungen verringert werden, sodass sich eine einzelne Änderung nicht auf die gesamte Anwendung auswirkt.

Ebenen (und Kapselung) erleichtern das Ersetzen von Funktionen innerhalb der Anwendung erheblich. Beispielsweise kann eine Anwendung zunächst eine eigene SQL Server-Datenbank für Persistenz verwenden, später aber eine cloudbasierte Persistenzstrategie oder eine hinter einer Web-API verwenden. Wenn die Anwendung ihre Persistenzimplementierung ordnungsgemäß innerhalb einer logischen Ebene gekapselt hat, könnte diese SQL Server-spezifische Ebene durch eine neue, die die gleiche öffentliche Schnittstelle implementiert, ersetzt werden.

Neben dem Potenzial, Implementierungen als Reaktion auf zukünftige Änderungen der Anforderungen auszutauschen, können Anwendungsebenen auch das Austauschen von Implementierungen für Testzwecke vereinfachen. Anstatt Tests zu schreiben, die mit der realen Datenschicht oder UI-Ebene der Anwendung arbeiten, können diese Ebenen zur Testzeit durch gefälschte Implementierungen ersetzt werden, die bekannte Antworten auf Anforderungen bereitstellen. Dieser Ansatz erleichtert in der Regel das Schreiben und viel schnelleren Ausführen von Tests im Vergleich zur Ausführung von Tests mit der tatsächlichen Infrastruktur der Anwendung.

Logische Layering ist eine gängige Technik zur Verbesserung der Organisation von Code in Unternehmenssoftwareanwendungen, und es gibt mehrere Möglichkeiten, wie Code in Ebenen organisiert werden kann.

Hinweis

Ebenen stellen die logische Trennung innerhalb der Anwendung dar. Wenn die Anwendungslogik physisch an separate Server oder Prozesse verteilt wird, werden diese separaten physischen Bereitstellungsziele als Ebenen bezeichnet. Es ist möglich und häufig eine N-Layer-Anwendung zu verwenden, die auf einer einzelnen Ebene bereitgestellt wird.

Herkömmliche "N-Layer"-Architekturanwendungen

Die gängigste Organisation der Anwendungslogik in Ebenen ist in Abbildung 5-2 dargestellt.

Typische Anwendungsebenen

Abbildung 5-2. Typische Anwendungsebenen.

Diese Ebenen werden häufig als Benutzeroberfläche, BLL (Geschäftslogikebene) und DAL (Data Access Layer) abgekürzt. Mithilfe dieser Architektur stellen Benutzer Anforderungen über die UI-Ebene, die nur mit der BLL interagiert. Die BLL kann wiederum dal für Datenzugriffsanforderungen aufrufen. Die UI-Ebene sollte keine Anforderungen direkt an die DAL stellen, und sie sollte auch nicht direkt mit Persistenz durch andere Mittel interagieren. Gleichzeitig sollte die BLL nur über die DAL mit der Persistenz interagieren. Auf diese Weise hat jede Ebene eine eigene bekannte Verantwortung.

Ein Nachteil dieses herkömmlichen Layering-Ansatzes besteht darin, dass Kompilierungszeitabhängigkeiten von oben nach unten ausgeführt werden. Das heißt, die UI-Ebene hängt von der BLL ab, die vom DAL abhängt. Dies bedeutet, dass die BLL, die in der Regel die wichtigste Logik in der Anwendung enthält, von Datenzugriffsimplementierungsdetails (und häufig vom Vorhandensein einer Datenbank) abhängig ist. Das Testen von Geschäftslogik in einer solchen Architektur ist häufig schwierig, was eine Testdatenbank erfordert. Das Abhängigkeitsinversionsprinzip kann verwendet werden, um dieses Problem zu beheben, wie im nächsten Abschnitt zu sehen ist.

Abbildung 5-3 zeigt eine Beispiellösung, die die Anwendung in drei Projekte nach Verantwortung (oder Ebene) unterbricht.

Eine einfache monolithische Anwendung mit drei Projekten

Abbildung 5-3. Eine einfache monolithische Anwendung mit drei Projekten.

Obwohl diese Anwendung mehrere Projekte für Organisationszwecke verwendet, wird sie weiterhin als einzelne Einheit bereitgestellt, und ihre Clients interagieren als einzelne Web-App. Dies ermöglicht einen sehr einfachen Bereitstellungsprozess. Abbildung 5-4 zeigt, wie eine solche App mit Azure gehostet werden kann.

Einfache Bereitstellung von Azure Web App

Abbildung 5-4. Einfache Bereitstellung von Azure Web App

Da die Anwendung wachsen muss, sind möglicherweise komplexere und robustere Bereitstellungslösungen erforderlich. Abbildung 5-5 zeigt ein Beispiel für einen komplexeren Bereitstellungsplan, der zusätzliche Funktionen unterstützt.

Bereitstellen einer Web-App in einem Azure App Service

Abbildung 5-5. Bereitstellen einer Web-App in einem Azure App Service

Intern verbessert die Organisation dieses Projekts in mehreren Projekten, die auf der Verantwortung basieren, die Verhaltbarkeit der Anwendung.

Diese Einheit kann skaliert oder verkleinert werden, um cloudbasierte Skalierbarkeit nach Bedarf zu nutzen. Die Skalierung bedeutet, dass zusätzliche CPU, Arbeitsspeicher, Speicherplatz oder andere Ressourcen zu den Servern hinzugefügt werden, die Ihre App hosten. Das Skalieren bedeutet, dass zusätzliche Instanzen solcher Server hinzugefügt werden, unabhängig davon, ob es sich um physische Server, virtuelle Computer oder Container handelt. Wenn Ihre App in mehreren Instanzen gehostet wird, wird ein Lastenausgleichsmodul verwendet, um Anforderungen einzelnen App-Instanzen zuzuweisen.

Der einfachste Ansatz zum Skalieren einer Webanwendung in Azure besteht darin, die Skalierung manuell im App Service Plan der Anwendung zu konfigurieren. Abbildung 5-6 zeigt den entsprechenden Azure-Dashboardbildschirm, um zu konfigurieren, wie viele Instanzen eine App bereitstellen.

Skalierung des App-Serviceplans in Azure

Abbildung 5-6. Skalierung des App-Serviceplans in Azure.

Saubere Architektur

Anwendungen, die dem Dependency Inversion-Prinzip folgen, sowie den Domain-Driven Design (DDD)-Prinzipien kommen in der Regel zu einer ähnlichen Architektur. Diese Architektur war im Laufe der Jahre unter vielen Namen bekannt. Zuerst wurde diese Architektur als „Hexagonal Architecture“ bezeichnet. Darauf folgte der Begriff „Ports-and-Adapters“. In letzter Zeit wird sie als Onion Architecture oder Clean Architecture zitiert. Letzterer Name, Clean Architecture, wird als Name für diese Architektur in diesem E-Book verwendet.

Die eShopOnWeb-Referenzanwendung verwendet den Ansatz "Clean Architecture", um den Code in Projekte zu organisieren. Sie finden eine Lösungsvorlage, die Sie als Ausgangspunkt für Ihre eigenen ASP.NET Core-Lösungen im GitHub-Repository "ardalis/cleanarchitecture " verwenden können, oder indem Sie die Vorlage aus NuGet installieren.

Mit sauberer Architektur wird die Geschäftslogik und das Anwendungsmodell im Mittelpunkt der Anwendung platziert. Anstatt dass die Geschäftslogik vom Datenzugriff oder anderen Infrastrukturproblemen abhängig ist, wird diese Abhängigkeit umgekehrt: Infrastruktur- und Implementierungsdetails hängen vom Application Core ab. Diese Funktionalität wird durch Definieren von Abstraktionen oder Schnittstellen im Application Core erreicht, die dann durch Typen implementiert werden, die in der Infrastrukturebene definiert sind. Eine gängige Methode zur Visualisierung dieser Architektur ist die Verwendung einer Reihe konzentrischer Kreise, ähnlich einer Zwiebel. Abbildung 5-7 zeigt ein Beispiel für diese Architekturdarstellung.

Clean Architecture: „Zwiebelansicht“

Abbildung 5-7. Clean Architecture: „Zwiebelansicht“

In diesem Diagramm beziehen sich alle Abhängigkeiten auf den inneren Kreisring, also auf den Anwendungskern. Der Anwendungskern nimmt seinen Namen von seiner Position am Kern dieses Diagramms an. Und Sie können im Diagramm sehen, dass der Application Core keine Abhängigkeiten von anderen Anwendungsebenen hat. Die Entitäten und Schnittstellen der Anwendung befinden sich im Mittelpunkt. Knapp außerhalb, aber immer noch im Application Core, befinden sich Domänen-Services, die üblicherweise Schnittstellen implementieren, die in der inneren Schicht definiert sind. Außerhalb des Anwendungskerns hängen sowohl die UI- als auch die Infrastrukturebenen vom Application Core ab, aber nicht voneinander (notwendigerweise).

Abbildung 5-8 zeigt ein herkömmliches horizontales Layerdiagramm, das die Abhängigkeit zwischen der Benutzeroberfläche und anderen Ebenen besser widerspiegelt.

Saubere Architektur; Horizontale Layeransicht

Abbildung 5-8. Saubere Architektur; Horizontale Layeransicht

Beachten Sie, dass die Pfeile mit durchgezogener Linie Abhängigkeiten zur Kompilierzeit darstellen. Die Pfeile mit gestrichelten Linien stellen Abhängigkeiten dar, die nur zur Laufzeit bestehen. Mit der sauberen Architektur arbeitet die UI-Ebene mit Schnittstellen, die zur Kompilierungszeit im Application Core definiert sind, und im Idealfall sollten sie nicht über die in der Infrastrukturebene definierten Implementierungstypen wissen. Zur Laufzeit sind diese Implementierungstypen jedoch erforderlich, damit die App ausgeführt werden kann. Daher müssen sie über das Einfügen von Abhängigkeiten vorhanden und mit den Application Core-Schnittstellen verkabelt werden.

Abbildung 5-9 zeigt eine detailliertere Ansicht der Architektur einer ASP.NET Core-Anwendung, wenn sie gemäß diesen Empfehlungen erstellt wird.

ASP.NET Kernarchitekturdiagramm nach sauberer Architektur

Abbildung 5-9. ASP.NET Kernarchitekturdiagramm nach sauberer Architektur.

Da der Application Core nicht von der Infrastruktur abhängt, ist es sehr einfach, automatisierte Komponententests für diese Ebene zu schreiben. Abbildungen 5-10 und 5-11 zeigen, wie Tests in diese Architektur passen.

UnitTestCore

Abbildung 5-10. Isolierter Komponententest des Anwendungskerns

Integrationstests

Abbildung 5-11. Integrationstest von Infrastrukturimplementierungen mit externen Abhängigkeiten

Da die UI-Ebene keine direkte Abhängigkeit von typen hat, die im Infrastrukturprojekt definiert sind, ist es ebenfalls sehr einfach, Implementierungen auszutauschen, um Tests zu vereinfachen oder auf sich ändernde Anwendungsanforderungen zu reagieren. ASP.NET Cores integrierte Verwendung und Unterstützung für Abhängigkeitsinjektion macht diese Architektur die am besten geeignete Möglichkeit, nicht triviale monolithische Anwendungen zu strukturieren.

Bei monolithischen Anwendungen werden alle Anwendungskern-, Infrastruktur- und UI-Projekte als einzelne Anwendung ausgeführt. Die Laufzeitanwendungsarchitektur könnte etwa wie in Abbildung 5-12 aussehen.

ASP.NET Kernarchitektur 2

Abbildung 5-12. Ein Beispiel ASP.NET Core-App-Laufzeitarchitektur.

Organisieren von Code in sauberer Architektur

In einer Clean Architecture-Lösung hat jedes Projekt klare Verantwortlichkeiten. Daher gehören bestimmte Typen zu jedem Projekt, und Sie finden häufig Ordner, die diesen Typen im entsprechenden Projekt entsprechen.

Anwendungskern

Der Application Core enthält das Geschäftsmodell, das Entitäten, Dienste und Schnittstellen umfasst. Diese Schnittstellen umfassen Abstraktionen für Vorgänge, die mithilfe der Infrastruktur ausgeführt werden, z. B. Datenzugriff, Dateisystemzugriff, Netzwerkaufrufe usw. Manchmal müssen Dienste oder Schnittstellen, die auf dieser Ebene definiert sind, mit Nicht-Entitätstypen arbeiten, die keine Abhängigkeiten von der Benutzeroberfläche oder Infrastruktur aufweisen. Diese können als einfache Datenübertragungsobjekte (DATA Transfer Objects, DTOs) definiert werden.

Application Core-Typen
  • Entitäten (geschäftsmodellklassen, die beibehalten werden)
  • Aggregate (Gruppen von Entitäten)
  • Schnittstellen
  • Domänendienste
  • Spezifikationen
  • Benutzerdefinierte Ausnahmen und Guard-Klauseln
  • Domänenereignisse und Handler

Infrastruktur

Das Infrastrukturprojekt umfasst in der Regel Datenzugriffsimplementierungen. In einer typischen ASP.NET Core-Webanwendung umfassen diese Implementierungen das Entity Framework (EF) DbContext, alle EF Core-Objekte Migration , die definiert wurden, und Datenzugriffsimplementierungsklassen. Die am häufigsten verwendete Methode zum Abstrahieren von Datenzugriffsimplementierungscode ist die Verwendung des Repository-Entwurfsmusters.

Zusätzlich zu Implementierungen des Datenzugriffs sollte das Infrastrukturprojekt Implementierungen von Diensten enthalten, die mit Infrastrukturbedenken interagieren müssen. Diese Dienste sollten Schnittstellen implementieren, die im Application Core definiert sind. Daher sollte infrastruktur über einen Verweis auf das Application Core-Projekt verfügen.

Infrastrukturtypen
  • EF Core-Datentypen (DbContext, Migration)
  • Implementierungstypen für den Datenzugriff (Repositorys)
  • Infrastrukturspezifische Dienste (z. B. FileLogger oder SmtpNotifier)

UI-Ebene

Die Benutzeroberflächenebene in einer ASP.NET Core MVC-Anwendung ist der Einstiegspunkt für die Anwendung. Dieses Projekt sollte auf das Application Core-Projekt verweisen, und seine Typen sollten ausschließlich über Schnittstellen interagieren, die in Application Core definiert sind. In der UI-Schicht sollten keine direkte Instanziierung oder statische Aufrufe von Typen von Infrastrukturebenen zugelassen werden.

Benutzeroberflächenebenentypen
  • Steuergeräte
  • Benutzerdefinierte Filter
  • Benutzerdefinierte Middleware
  • Ansichten
  • ViewModels
  • Unternehmensgründung

Die Startup Klasse oder Program.cs Datei ist für die Konfiguration der Anwendung und für die Verkabelung von Implementierungstypen mit Schnittstellen verantwortlich. Die Stelle, an der diese Logik ausgeführt wird, wird als Kompositionsstamm der App bezeichnet und ermöglicht die ordnungsgemäße Ausführung der Abhängigkeitsinjektion zur Laufzeit.

Hinweis

Um die Abhängigkeitsinjektion während des App-Starts zu verknüpfen, muss das Projekt der UI-Ebene möglicherweise auf das Infrastrukturprojekt verweisen. Diese Abhängigkeit kann am einfachsten mithilfe eines benutzerdefinierten DI-Containers beseitigt werden, der integrierte Unterstützung für das Laden von Typen aus Assemblys bietet. Im Rahmen dieses Beispiels besteht der einfachste Ansatz darin, dem Benutzeroberflächenprojekt das Verweisen auf das Infrastrukturprojekt zu ermöglichen (Entwickler sollten jedoch tatsächliche Verweise auf Typen im Infrastrukturprojekt auf den Kompositionsstamm der App beschränken).

Monolithische Anwendungen und Container

Sie können eine einzelne und monolithische Bereitstellungsbasierte Webanwendung oder einen Dienst erstellen und als Container bereitstellen. Innerhalb der Anwendung ist es möglicherweise nicht monolithisch, sondern in mehreren Bibliotheken, Komponenten oder Ebenen organisiert. Extern ist es ein einzelner Container mit einem einzelnen Prozess, einer einzelnen Webanwendung oder einem einzelnen Dienst.

Zum Verwalten dieses Modells stellen Sie einen einzelnen Container bereit, der die Anwendung darstellt. Zum Skalieren fügen Sie einfach zusätzliche Kopien mit einem Lastenausgleich vorne hinzu. Die Einfachheit kommt aus der Verwaltung einer einzelnen Bereitstellung in einem einzelnen Container oder virtuellen Computer.

Abbildung 5-13

Sie können mehrere Komponenten/Bibliotheken oder interne Ebenen innerhalb jedes Containers einschließen, wie in Abbildung 5-13 dargestellt. Nach dem Containerprinzip "ein Container erledigt eine Aufgabe und führt sie in einem Prozess aus", könnte der monolithische Ansatz ein Konflikt darstellen.

Der Nachteil dieses Ansatzes ist, wenn/wenn die Anwendung wächst, und sie muss skaliert werden. Wenn die gesamte Anwendung skaliert wird, ist es nicht wirklich ein Problem. In den meisten Fällen sind jedoch einige Teile der Anwendung die Drosselpunkte, die eine Skalierung erfordern, während andere Komponenten weniger verwendet werden.

Das typische Beispiel aus dem Bereich E-Commerce zeigt, dass die Produktinformationskomponente wahrscheinlich skalieren muss. Viele mehr Kunden durchsuchen Produkte, als sie zu kaufen. Mehr Kunden verwenden ihren Korb als die Zahlungspipeline. Weniger Kunden fügen Kommentare hinzu und sehen ihren Kaufverlauf an. Und Sie haben wahrscheinlich nur eine Handvoll Mitarbeiter in einer einzigen Region, die die Inhalte und Marketingkampagnen verwalten müssen. Durch die Skalierung des monolithischen Designs wird der gesamte Code mehrmals bereitgestellt.

Zusätzlich zum Problem "Alles skalieren" müssen Änderungen an einer einzelnen Komponente eine vollständige Neutestung der gesamten Anwendung und eine vollständige erneute Bereitstellung aller Instanzen erfordern.

Der monolithische Ansatz ist üblich, und viele Organisationen entwickeln sich mit diesem architektonischen Ansatz. Viele haben ausreichend gute Ergebnisse, während andere an Grenzen stoßen. Viele entwickelten ihre Anwendungen in diesem Modell, da die Tools und Infrastruktur zu schwierig waren, dienstorientierte Architekturen (SOA) zu erstellen, und sie sahen die Notwendigkeit erst, wenn die App gewachsen ist. Wenn Sie an die Grenzen des monolithischen Ansatzes stoßen, ist der nächste logische Schritt das Aufteilen der App, damit diese Container und Microservices besser nutzen kann.

Abbildung 5-14

Die Bereitstellung von monolithischen Anwendungen in Microsoft Azure kann mithilfe dedizierter VMs für jede Instanz erreicht werden. Mithilfe von Azure Virtual Machine Scale Sets können Sie die virtuellen Computer ganz einfach skalieren. Azure App Services kann monolithische Anwendungen ausführen und Instanzen problemlos skalieren, ohne die virtuellen Computer verwalten zu müssen. Azure App Services kann auch einzelne Instanzen von Docker-Containern ausführen und die Bereitstellung vereinfachen. Mithilfe von Docker können Sie einen einzelnen virtuellen Computer als Docker-Host bereitstellen und mehrere Instanzen ausführen. Mithilfe des Azure-Balancers, wie in Abbildung 5-14 dargestellt, können Sie die Skalierung verwalten.

Die Bereitstellung auf den verschiedenen Hosts kann mit herkömmlichen Bereitstellungstechniken verwaltet werden. Die Docker-Hosts können mit Befehlen wie docker run verwaltet werden, entweder manuell oder über Automatisierungen wie Continuous Delivery (CD)-Pipelines.

Monolithische Anwendung, die als Container bereitgestellt wird

Es gibt Vorteile der Verwendung von Containern zum Verwalten von monolithischen Anwendungsbereitstellungen. Die Skalierung der Containerinstanzen ist viel schneller und einfacher als die Bereitstellung zusätzlicher VMs. Auch wenn VM-Skalierungsgruppen zum Skalieren von VMs verwendet werden, nimmt deren Erstellung Zeit in Anspruch. Wenn sie als App-Instanzen bereitgestellt wird, wird die Konfiguration der App als Teil der VM verwaltet.

Das Bereitstellen von Updates als Docker-Images ist viel schneller und netzwerkeffizient. Docker-Images beginnen in der Regel in Sekunden und beschleunigen rollouts. Das Abreißen einer Docker-Instanz ist so einfach wie das Ausstellen eines docker stop Befehls, in der Regel in weniger als einer Sekunde abgeschlossen.

Da Container von Natur aus unveränderlich sind, müssen Sie sich nie um beschädigte virtuelle Computer kümmern, während Updateskripts möglicherweise vergessen, bestimmte Konfigurationen oder Dateien auf dem Datenträger zu berücksichtigen.

Sie können Docker-Container für eine monolithische Bereitstellung einfacherer Webanwendungen verwenden. Dieser Ansatz verbessert die CI/DC-Pipelines (Continuous Integration und Continuous Deployment) und unterstützt Sie bei der erfolgreichen Bereitstellung in der Produktion. Nicht mehr "Es funktioniert auf meinem Computer, warum funktioniert es nicht in der Produktion?"

Eine mikroservicesbasierte Architektur hat viele Vorteile, aber diese Vorteile ergeben sich zu einem Preis von erhöhter Komplexität. In einigen Fällen überwiegen die Kosten die Vorteile, daher ist eine monolithische Bereitstellungsanwendung, die in einem einzigen Container oder nur in nur wenigen Containern ausgeführt wird, eine bessere Option.

Eine monolithische Anwendung kann möglicherweise nicht einfach in gut getrennte Microservices dekompiliert werden. Microservices sollten unabhängig voneinander arbeiten, um eine stabilere Anwendung bereitzustellen. Wenn Sie keine unabhängigen Featuresegmente der Anwendung bereitstellen können, wird durch die Trennung nur die Komplexität erhöht.

Eine Anwendung benötigt möglicherweise noch nicht die Unabhängigkeit der Skalierung von Funktionen. Viele Anwendungen, wenn sie über eine einzelne Instanz hinaus skalieren müssen, können dies durch den relativ einfachen Prozess des Klonens dieser gesamten Instanz tun. Die zusätzliche Arbeit, die Anwendung in eigenständige Dienste aufzuteilen, bietet nur dann einen minimalen Vorteil, wenn das Skalieren vollständiger Instanzen der Anwendung einfach und kostengünstig ist.

Früh bei der Entwicklung einer Anwendung haben Sie möglicherweise keine klare Vorstellung davon, wo sich die natürlichen funktionalen Grenzen befinden. Wenn Sie ein Minimal überlebensfähiges Produkt entwickeln, ist die natürliche Trennung möglicherweise noch nicht erfolgt. Einige dieser Bedingungen können vorübergehend sein. Sie können beginnen, eine monolithische Anwendung zu erstellen, und später trennen Sie einige Features, die als Microservices entwickelt und bereitgestellt werden sollen. Andere Bedingungen können für den Problemraum der Anwendung wesentlich sein, was bedeutet, dass die Anwendung möglicherweise nie in mehrere Microservices unterteilt wird.

Das Trennen einer Anwendung in viele diskrete Prozesse führt auch zu Mehraufwand. Es gibt mehr Komplexität beim Trennen von Features in verschiedene Prozesse. Die Kommunikationsprotokolle werden komplexer. Anstelle von Methodenaufrufen müssen Sie eine asynchrone Kommunikation zwischen Diensten verwenden. Wenn Sie zu einer Microservices-Architektur wechseln, müssen Sie viele der in der Microservices-Version der eShopOnContainers-Anwendung implementierten Bausteine hinzufügen: Ereignisbusbehandlung, Nachrichtenresilienz und Wiederholungen, letztendliche Konsistenz und vieles mehr.

Die viel einfachere eShopOnWeb-Referenzanwendung unterstützt die Verwendung eines monolithischen Ein-Container-Systems. Die Anwendung enthält eine Webanwendung, die herkömmliche MVC-Ansichten, Web-APIs und Razor Pages enthält. Optional können Sie die Blazor-basierte Administratorkomponente der Anwendung ausführen, die auch ein separates API-Projekt erfordert.

Die Anwendung kann über den Lösungsstamm mit den Befehlen docker-compose build und docker-compose up gestartet werden. Mit diesem Befehl wird ein Container für die Webinstanz konfiguriert, wobei der Dockerfile Stamm des Webprojekts verwendet wird und der Container auf einem angegebenen Port ausgeführt wird. Sie können die Quelle für diese Anwendung von GitHub herunterladen und lokal ausführen. Selbst diese monolithische Anwendung profitiert von der Bereitstellung in einer Containerumgebung.

Zum einen bedeutet die containerisierte Bereitstellung, dass jede Instanz der Anwendung in derselben Umgebung ausgeführt wird. Dieser Ansatz umfasst die Entwicklerumgebung, in der frühe Tests und Entwicklung stattfinden. Das Entwicklungsteam kann die Anwendung in einer containerisierten Umgebung ausführen, die der Produktionsumgebung entspricht.

Darüber hinaus skalieren containerisierte Anwendungen zu niedrigeren Kosten. Die Verwendung einer Containerumgebung ermöglicht eine größere Ressourcenfreigabe als herkömmliche VM-Umgebungen.

Schließlich erzwingt die Containerisierung der Anwendung eine Trennung zwischen der Geschäftslogik und dem Speicherserver. Wenn die Anwendung skaliert wird, basieren die mehreren Container alle auf einem einzigen physischen Speichermedium. Dieses Speichermedium wäre in der Regel ein Hochverfügbarkeitsserver, auf dem eine SQL Server-Datenbank ausgeführt wird.

Docker-Unterstützung

Das eShopOnWeb Projekt wird auf .NET ausgeführt. Daher kann sie entweder in Linux-basierten oder Windows-basierten Containern ausgeführt werden. Beachten Sie, dass Sie für die Docker-Bereitstellung denselben Hosttyp für SQL Server verwenden möchten. Linux-basierte Container ermöglichen einen kleineren Speicherbedarf und werden bevorzugt.

Sie können Visual Studio 2017 oder höher verwenden, um einer vorhandenen Anwendung Docker-Unterstützung hinzuzufügen, indem Sie im Projektmappen-Explorer mit der rechten Maustaste auf ein Projekt klicken und "Docker-Support> auswählen. In diesem Schritt werden die erforderlichen Dateien hinzugefügt und das Projekt so geändert, dass sie verwendet werden. Im aktuellen eShopOnWeb Beispiel sind diese Dateien bereits vorhanden.

Die Datei auf Lösungsebene docker-compose.yml enthält Informationen dazu, welche Images erstellt werden sollen und welche Container gestartet werden sollen. Mit der Datei können Sie den docker-compose Befehl verwenden, um mehrere Anwendungen gleichzeitig zu starten. In diesem Fall wird nur das Webprojekt gestartet. Sie können sie auch verwenden, um Abhängigkeiten zu konfigurieren, z. B. einen separaten Datenbankcontainer.

version: '3'

services:
  eshopwebmvc:
    image: eshopwebmvc
    build:
      context: .
      dockerfile: src/Web/Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "5106:5106"

networks:
  default:
    external:
      name: nat

Die Datei docker-compose.yml verweist im Projekt Dockerfile auf Web. Dies Dockerfile wird verwendet, um anzugeben, welcher Basiscontainer verwendet wird und wie die Anwendung darauf konfiguriert wird. Die Web-Datei von Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app

COPY *.sln .
COPY . .
WORKDIR /app/src/Web
RUN dotnet restore

RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/src/Web/out ./

ENTRYPOINT ["dotnet", "Web.dll"]

Problemlösung bei Docker-Problemen

Nachdem Sie die containerisierte Anwendung ausgeführt haben, wird sie weiter ausgeführt, bis Sie sie beenden. Sie können anzeigen, welche Container mit dem docker ps Befehl ausgeführt werden. Sie können einen ausgeführten Container mithilfe des docker stop Befehls beenden und die Container-ID angeben.

Beachten Sie, dass das Ausführen von Docker-Containern möglicherweise an Ports gebunden ist, die Sie andernfalls in Ihrer Entwicklungsumgebung verwenden möchten. Wenn Sie versuchen, eine Anwendung mit demselben Port wie ein ausgeführter Docker-Container auszuführen oder zu debuggen, wird eine Fehlermeldung angezeigt, die besagt, dass der Server nicht an diesen Port gebunden werden kann. Erneut sollte das Stoppen des Containers das Problem beheben.

Wenn Sie Ihrer Anwendung Docker-Unterstützung mithilfe von Visual Studio hinzufügen möchten, stellen Sie sicher, dass Docker Desktop ausgeführt wird, wenn Sie dies tun. Der Assistent wird nicht ordnungsgemäß ausgeführt, wenn Docker Desktop beim Starten des Assistenten nicht ausgeführt wird. Darüber hinaus überprüft der Assistent Ihre aktuelle Containerauswahl, um die richtige Docker-Unterstützung hinzuzufügen. Wenn Sie die Unterstützung für Windows-Container hinzufügen möchten, müssen Sie den Assistenten ausführen, während Docker Desktop mit Windows-Containern konfiguriert ist. Wenn Sie die Unterstützung für Linux-Container hinzufügen möchten, führen Sie den Assistenten aus, während Docker mit Linux-Containern konfiguriert ist.

Andere Architekturstile für Webanwendungen

  • Web-Queue-Worker: Die Kernkomponenten dieser Architektur sind ein Web-Front-End, das Clientanforderungen bedient, und ein Worker, der ressourcenintensive Aufgaben, lange ausgeführte Workflows oder Batchaufträge ausführt. Das Web-Front-End kommuniziert mit dem Worker über eine Nachrichtenwarteschlange.
  • N-Ebene: Eine N-Tier-Architektur teilt eine Anwendung in logische Ebenen und physische Ebenen auf.
  • Microservice: Eine Microservices-Architektur besteht aus einer Sammlung kleiner, autonomer Dienste. Jeder Dienst ist eigenständig und sollte eine einzige Geschäftsfunktion in einem begrenzten Kontext implementieren.

Referenzen – Allgemeine Webarchitekturen