Een microservicegeoriënteerde toepassing ontwerpen
Tip
Deze inhoud is een fragment uit het eBook, .NET Microservices Architecture for Containerized .NET Applications, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.
Deze sectie is gericht op het ontwikkelen van een hypothetische bedrijfstoepassing aan de serverzijde.
Toepassingsspecificaties
De hypothetische toepassing verwerkt aanvragen door bedrijfslogica uit te voeren, toegang te krijgen tot databases en vervolgens HTML-, JSON- of XML-antwoorden te retourneren. We zullen zeggen dat de toepassing ondersteuning moet bieden voor verschillende clients, waaronder desktopbrowsers met SPA's (Single Page Applications), traditionele web-apps, mobiele web-apps en systeemeigen mobiele apps. De toepassing kan ook een API beschikbaar maken die derden kunnen gebruiken. Het moet ook asynchroon microservices of externe toepassingen kunnen integreren, zodat de benadering de tolerantie van de microservices kan helpen in het geval van gedeeltelijke storingen.
De toepassing bestaat uit deze typen onderdelen:
Presentatieonderdelen. Deze onderdelen zijn verantwoordelijk voor het verwerken van de gebruikersinterface en het verbruiken van externe services.
Domein- of bedrijfslogica. Dit onderdeel is de domeinlogica van de toepassing.
Logica voor databasetoegang. Dit onderdeel bestaat uit onderdelen voor gegevenstoegang die verantwoordelijk zijn voor toegang tot databases (SQL of NoSQL).
Integratielogica voor toepassingen. Dit onderdeel bevat een berichtenkanaal op basis van berichtbrokers.
Voor de toepassing is een hoge schaalbaarheid vereist, terwijl de verticale subsystemen autonoom kunnen worden uitgeschaald, omdat bepaalde subsystemen meer schaalbaarheid vereisen dan andere.
De toepassing moet kunnen worden geïmplementeerd in meerdere infrastructuuromgevingen (meerdere openbare clouds en on-premises) en moet in het ideale geval platformoverschrijdend zijn, kunnen overstappen van Linux naar Windows (of omgekeerd).
Context van ontwikkelingsteam
We gaan ook uit van het volgende over het ontwikkelingsproces voor de toepassing:
U hebt meerdere ontwikkelteams die zich richten op verschillende bedrijfsgebieden van de toepassing.
Nieuwe teamleden moeten snel productief worden en de toepassing moet gemakkelijk te begrijpen en wijzigen zijn.
De toepassing heeft een langetermijnontwikkeling en steeds veranderende bedrijfsregels.
U hebt een goede onderhoudbaarheid op de lange termijn nodig, wat betekent dat u flexibiliteit hebt bij het implementeren van nieuwe wijzigingen in de toekomst, terwijl u meerdere subsystemen kunt bijwerken met minimale gevolgen voor de andere subsystemen.
U wilt continue integratie en continue implementatie van de toepassing oefenen.
U wilt profiteren van opkomende technologieën (frameworks, programmeertalen, enzovoort) tijdens het ontwikkelen van de toepassing. U wilt geen volledige migraties van de toepassing uitvoeren wanneer u overstapt op nieuwe technologieën, omdat dit tot hoge kosten zou leiden en de voorspelbaarheid en stabiliteit van de toepassing zou beïnvloeden.
Een architectuur kiezen
Wat moet de implementatiearchitectuur van de toepassing zijn? De specificaties voor de toepassing, samen met de ontwikkelingscontext, suggereren sterk dat u de toepassing moet ontwerpen door deze op te vouwen in autonome subsystemen in de vorm van samenwerkende microservices en containers, waarbij een microservice een container is.
In deze benadering implementeert elke service (container) een set samenhangende en smalle gerelateerde functies. Een toepassing kan bijvoorbeeld bestaan uit services zoals de catalogusservice, het bestellen van service, basketservice, service voor gebruikersprofielen, enzovoort.
Microservices communiceren met behulp van protocollen zoals HTTP (REST), maar ook asynchroon (bijvoorbeeld met AMQP) waar mogelijk, met name wanneer updates worden doorgegeven met integratie-gebeurtenissen.
Microservices worden onafhankelijk van elkaar ontwikkeld en geïmplementeerd als containers. Deze aanpak betekent dat een ontwikkelteam een bepaalde microservice kan ontwikkelen en implementeren zonder dat dit van invloed is op andere subsystemen.
Elke microservice heeft een eigen database, zodat deze volledig kan worden losgekoppeld van andere microservices. Indien nodig wordt consistentie tussen databases van verschillende microservices bereikt met behulp van integratiegebeurtenissen op toepassingsniveau (via een logische gebeurtenisbus), zoals verwerkt in CQRS (Command and Query Responsibility Segregation). Daarom moeten de bedrijfsbeperkingen uiteindelijke consistentie tussen de meerdere microservices en gerelateerde databases omvatten.
eShopOnContainers: een referentietoepassing voor .NET en microservices die zijn geïmplementeerd met behulp van containers
Zodat u zich kunt richten op de architectuur en technologieën in plaats van na te denken over een hypothetisch bedrijfsdomein dat u misschien niet kent, hebben we een bekend bedrijfsdomein geselecteerd, namelijk een vereenvoudigde e-commercetoepassing (e-shop) die een catalogus met producten presenteert, bestellingen van klanten afneemt, inventaris controleert en andere bedrijfsfuncties uitvoert. Deze op containers gebaseerde toepassingsbroncode is beschikbaar in de GitHub-opslagplaats eShopOnContainers .
De toepassing bestaat uit meerdere subsystemen, waaronder verschillende front-ends voor de gebruikersinterface van de store (een webtoepassing en een systeemeigen mobiele app), samen met de back-end microservices en containers voor alle vereiste bewerkingen aan de serverzijde met verschillende API-gateways als geconsolideerde toegangspunten voor de interne microservices. Afbeelding 6-1 toont de architectuur van de referentietoepassing.
Afbeelding 6-1. De eShopOnContainers-referentietoepassingsarchitectuur voor de ontwikkelomgeving
In het bovenstaande diagram ziet u dat mobile- en beveiligd-WACHTWOORDVERIFICATIE-clients communiceren met eindpunten van één API-gateway, die vervolgens communiceren met microservices. Traditionele webclients communiceren met MVC-microservice, die communiceert met microservices via de API-gateway.
Hostingomgeving. In afbeelding 6-1 ziet u verschillende containers die zijn geïmplementeerd binnen één Docker-host. Dat zou het geval zijn bij het implementeren naar één Docker-host met de opdracht docker-compose up. Als u echter een orchestrator of containercluster gebruikt, kan elke container worden uitgevoerd op een andere host (knooppunt) en kan elk knooppunt een willekeurig aantal containers uitvoeren, zoals eerder in de sectie Architectuur is uitgelegd.
Communicatiearchitectuur. De eShopOnContainers-toepassing maakt gebruik van twee communicatietypen, afhankelijk van het soort functionele actie (query's versus updates en transacties):
Http-client-naar-microservice-communicatie via API-gateways. Deze methode wordt gebruikt voor query's en bij het accepteren van update- of transactionele opdrachten van de client-apps. De methode voor het gebruik van API Gateways wordt in latere secties uitgebreid beschreven.
Asynchrone communicatie op basis van gebeurtenissen. Deze communicatie vindt plaats via een gebeurtenisbus om updates door te geven aan microservices of om te integreren met externe toepassingen. De gebeurtenisbus kan worden geïmplementeerd met elke messaging-broker-infrastructuurtechnologie zoals RabbitMQ of met servicebussen op een hoger niveau (abstractieniveau), zoals Azure Service Bus, NServiceBus, MassTransit of Brighter.
De toepassing wordt geïmplementeerd als een set microservices in de vorm van containers. Client-apps kunnen communiceren met die microservices die als containers worden uitgevoerd via de openbare URL's die zijn gepubliceerd door de API-gateways.
Gegevenssoevereine per microservice
In de voorbeeldtoepassing is elke microservice eigenaar van een eigen database of gegevensbron, hoewel alle SQL Server-databases worden geïmplementeerd als één container. Deze ontwerpbeslissing is alleen gemaakt om een ontwikkelaar gemakkelijk de code te laten ophalen uit GitHub, deze te klonen en te openen in Visual Studio of Visual Studio Code. U kunt ook eenvoudig de aangepaste Docker-installatiekopieën compileren met behulp van de .NET CLI en de Docker CLI, en deze vervolgens implementeren en uitvoeren in een Docker-ontwikkelomgeving. In beide gevallen kunnen ontwikkelaars met behulp van containers voor gegevensbronnen binnen enkele minuten bouwen en implementeren zonder dat ze een externe database of een andere gegevensbron hoeven in te richten met harde afhankelijkheden van de infrastructuur (cloud of on-premises).
In een echte productieomgeving, voor hoge beschikbaarheid en schaalbaarheid, moeten de databases zijn gebaseerd op databaseservers in de cloud of on-premises, maar niet in containers.
Daarom zijn de implementatie-eenheden voor microservices (en zelfs voor databases in deze toepassing) Docker-containers en is de referentietoepassing een toepassing met meerdere containers die microservicesprincipes omvat.
Aanvullende bronnen
- eShopOnContainers GitHub-opslagplaats. Broncode voor de referentietoepassing
https://aka.ms/eShopOnContainers/
Voordelen van een op microservice gebaseerde oplossing
Een op microservice gebaseerde oplossing zoals deze heeft veel voordelen:
Elke microservice is relatief klein, eenvoudig te beheren en te ontwikkelen. Specifiek:
Het is eenvoudig voor een ontwikkelaar om snel te begrijpen en aan de slag te gaan met een goede productiviteit.
Containers starten snel, waardoor ontwikkelaars productiever worden.
Een IDE zoals Visual Studio kan kleinere projecten snel laden, waardoor ontwikkelaars productief zijn.
Elke microservice kan onafhankelijk van andere microservices worden ontworpen, ontwikkeld en geïmplementeerd, wat flexibiliteit biedt omdat het eenvoudiger is om nieuwe versies van microservices regelmatig te implementeren.
Het is mogelijk om afzonderlijke gebieden van de toepassing uit te schalen. De catalogusservice of de winkelwagenservice moet bijvoorbeeld worden uitgeschaald, maar niet het bestelproces. Een microservicesinfrastructuur is veel efficiënter met betrekking tot de resources die worden gebruikt bij het uitschalen dan een monolithische architectuur zou zijn.
U kunt het ontwikkelwerk verdelen tussen meerdere teams. Elke service kan eigendom zijn van één ontwikkelteam. Elk team kan hun service onafhankelijk van de rest van de teams beheren, ontwikkelen, implementeren en schalen.
Problemen zijn geïsoleerder. Als er zich een probleem voordoet in één service, wordt alleen die service in eerste instantie beïnvloed (behalve wanneer het verkeerde ontwerp wordt gebruikt, met directe afhankelijkheden tussen microservices) en kunnen andere services aanvragen blijven verwerken. Een defect onderdeel in een monolithische implementatiearchitectuur kan daarentegen het hele systeem uitschakelen, met name wanneer dit resources omvat, zoals een geheugenlek. Bovendien kunt u, wanneer een probleem in een microservice is opgelost, alleen de betrokken microservice implementeren zonder dat dit van invloed is op de rest van de toepassing.
U kunt de nieuwste technologieën gebruiken. Omdat u services onafhankelijk van elkaar kunt gaan ontwikkelen en ze naast elkaar kunt uitvoeren (dankzij containers en .NET), kunt u de nieuwste technologieën en frameworks gebruiken in plaats van op een oudere stack of framework voor de hele toepassing te blijven hangen.
Nadeel van een op microservice gebaseerde oplossing
Een op microservice gebaseerde oplossing zoals deze heeft ook enkele nadelen:
Gedistribueerde toepassing. Het distribueren van de toepassing voegt complexiteit toe voor ontwikkelaars bij het ontwerpen en bouwen van de services. Ontwikkelaars moeten bijvoorbeeld communicatie tussen services implementeren met behulp van protocollen zoals HTTP of AMQP, waardoor het testen en het verwerken van uitzonderingen complexer wordt. Het voegt ook latentie toe aan het systeem.
Implementatiecomplexiteit. Een toepassing met tientallen microservicestypen en een hoge schaalbaarheid nodig heeft (het moet veel exemplaren per service kunnen maken en deze services op veel hosts kunnen verdelen) betekent een hoge mate van implementatiecomplexiteit voor IT-bewerkingen en -beheer. Als u geen microservicegeoriënteerde infrastructuur gebruikt (zoals een orchestrator en scheduler), kan extra complexiteit veel meer ontwikkelingsinspanningen vereisen dan de bedrijfstoepassing zelf.
Atomische transacties. Atomische transacties tussen meerdere microservices zijn meestal niet mogelijk. De bedrijfsvereisten moeten uiteindelijk consistentie tussen meerdere microservices omvatten. Zie de uitdagingen van het verwerken van idempotent berichten voor meer informatie.
Toegenomen wereldwijde resourcebehoeften (totaal geheugen, stations en netwerkresources voor alle servers of hosts). Wanneer u in veel gevallen een monolithische toepassing vervangt door een microservicesbenadering, is de hoeveelheid initiële globale resources die nodig zijn voor de nieuwe microservicetoepassing groter dan de infrastructuurbehoeften van de oorspronkelijke monolithische toepassing. Deze aanpak komt doordat voor de hogere mate van granulariteit en gedistribueerde services meer globale resources nodig zijn. Gezien de lage kosten van resources in het algemeen en het voordeel van het uitschalen van bepaalde gebieden van de toepassing in vergelijking met de kosten op de lange termijn bij het ontwikkelen van monolithische toepassingen, is het toegenomen gebruik van resources meestal een goede afweging voor grote, langdurige toepassingen.
Problemen met directe client-naar-microservicecommunicatie. Wanneer de toepassing groot is, met tientallen microservices, zijn er uitdagingen en beperkingen als voor de toepassing directe client-naar-microservice-communicatie is vereist. Een probleem is een mogelijke mismatch tussen de behoeften van de client en de API's die door elk van de microservices worden weergegeven. In bepaalde gevallen moet de clienttoepassing mogelijk veel afzonderlijke aanvragen indienen om de gebruikersinterface op te stellen, wat inefficiënt kan zijn via internet en onpraktisch zou zijn via een mobiel netwerk. Daarom moeten aanvragen van de clienttoepassing naar het back-endsysteem worden geminimaliseerd.
Een ander probleem met directe client-naar-microservice-communicatie is dat sommige microservices mogelijk protocollen gebruiken die niet webvriendelijk zijn. Een service kan een binair protocol gebruiken, terwijl een andere service AMQP-berichten kan gebruiken. Deze protocollen zijn niet firewallvriendelijk en kunnen het beste intern worden gebruikt. Normaal gesproken moet een toepassing protocollen zoals HTTP en WebSockets gebruiken voor communicatie buiten de firewall.
Een ander nadeel van deze directe client-to-service-benadering is dat het moeilijk maakt om de contracten voor deze microservices te herstructureren. In de loop van de tijd willen ontwikkelaars mogelijk wijzigen hoe het systeem wordt gepartitioneerd in services. Ze kunnen bijvoorbeeld twee services samenvoegen of een service splitsen in twee of meer services. Als clients echter rechtstreeks met de services communiceren, kan het uitvoeren van dit type herstructurering de compatibiliteit met client-apps verbreken.
Zoals vermeld in de architectuursectie, kunt u bij het ontwerpen en bouwen van een complexe toepassing op basis van microservices overwegen om meerdere fijnmazige API-gateways te gebruiken in plaats van de eenvoudigere directe communicatie tussen clients en microservices.
Partitionering van de microservices. Ten slotte, ongeacht welke benadering u kiest voor uw microservicearchitectuur, is een andere uitdaging het bepalen hoe u een end-to-end-toepassing in meerdere microservices partitioneert. Zoals vermeld in de architectuursectie van de handleiding, zijn er verschillende technieken en benaderingen die u kunt gebruiken. In principe moet u gebieden van de toepassing identificeren die zijn losgekoppeld van de andere gebieden en die een laag aantal vaste afhankelijkheden hebben. In veel gevallen is deze benadering afgestemd op partitioneringsservices per use-case. In onze e-shoptoepassing hebben we bijvoorbeeld een bestelservice die verantwoordelijk is voor alle bedrijfslogica die betrekking heeft op het orderproces. We hebben ook de catalogusservice en de mandservice die andere mogelijkheden implementeert. In het ideale geval mag elke service slechts een kleine set verantwoordelijkheden hebben. Deze benadering is vergelijkbaar met het SRP (Single Responsibility Principle) dat wordt toegepast op klassen, die aangeeft dat een klasse slechts één reden moet hebben om te wijzigen. Maar in dit geval gaat het om microservices, dus het bereik is groter dan één klasse. Meestal moet een microservice autonoom zijn, end-to-end, inclusief verantwoordelijkheid voor zijn eigen gegevensbronnen.
Externe versus interne architectuur en ontwerppatronen
De externe architectuur is de microservicearchitectuur die is samengesteld door meerdere services, volgens de principes die worden beschreven in de architectuursectie van deze handleiding. Afhankelijk van de aard van elke microservice en onafhankelijk van de architectuur van een microservice op hoog niveau die u kiest, is het echter gebruikelijk en soms raadzaam om verschillende interne architecturen te hebben, elk op basis van verschillende patronen, voor verschillende microservices. De microservices kunnen zelfs verschillende technologieën en programmeertalen gebruiken. Afbeelding 6-2 illustreert deze diversiteit.
Afbeelding 6-2. Externe versus interne architectuur en ontwerp
In ons voorbeeld eShopOnContainers zijn de microservices voor catalogus, mand en gebruikersprofiel bijvoorbeeld eenvoudig (in principe CRUD-subsystemen). Daarom is hun interne architectuur en ontwerp eenvoudig. Mogelijk hebt u echter andere microservices, zoals de bestellende microservice, die complexer is en steeds veranderende bedrijfsregels vertegenwoordigt met een hoge mate van domeincomplexiteit. In dergelijke gevallen wilt u mogelijk meer geavanceerde patronen implementeren binnen een bepaalde microservice, zoals de patronen die zijn gedefinieerd met DDD-benaderingen (Domain-Driven Design), zoals we doen in de eShopOnContainers-bestelling microservice. (We bekijken deze DDD-patronen later in de sectie waarin de implementatie van de microservice eShopOnContainers wordt uitgelegd.)
Een andere reden voor een andere technologie per microservice is mogelijk de aard van elke microservice. Het is bijvoorbeeld beter om een functionele programmeertaal zoals F# te gebruiken of zelfs een taal zoals R als u AI- en machine learning-domeinen wilt gebruiken in plaats van een meer objectgeoriënteerde programmeertaal zoals C#.
De bottom line is dat elke microservice een andere interne architectuur kan hebben op basis van verschillende ontwerppatronen. Niet alle microservices moeten worden geïmplementeerd met behulp van geavanceerde DDD-patronen, omdat dat over-engineering zou zijn. Op dezelfde manier moeten complexe microservices met steeds veranderende bedrijfslogica niet worden geïmplementeerd als CRUD-onderdelen of kunt u uiteindelijk code van lage kwaliteit gebruiken.
De nieuwe wereld: meerdere architectuurpatronen en polyglot microservices
Er zijn veel architectuurpatronen die worden gebruikt door softwarearchitecten en ontwikkelaars. Hier volgen enkele (waarbij architectuurstijlen en architectuurpatronen worden vermengd):
Eenvoudige CRUD, één laag, één laag.
Schone architectuur (zoals gebruikt met eShopOnWeb)
U kunt ook microservices bouwen met veel technologieën en talen, zoals ASP.NET Core Web-API's, NancyFx, ASP.NET Core SignalR (beschikbaar met .NET Core 2 of hoger), F#, Node.js, Python, Java, C++, GoLang en meer.
Het belangrijkste punt is dat geen bepaald architectuurpatroon of -stijl, noch een bepaalde technologie, geschikt is voor alle situaties. Afbeelding 6-3 toont enkele benaderingen en technologieën (hoewel niet in een bepaalde volgorde) die in verschillende microservices kunnen worden gebruikt.
Afbeelding 6-3. Multi-architecturale patronen en de polyglot microservices wereld
Multi-architectonisch patroon en polyglot microservices betekent dat u talen en technologieën kunt combineren en afstemmen op de behoeften van elke microservice en ze nog steeds met elkaar kunnen communiceren. Zoals wordt weergegeven in afbeelding 6-3, in toepassingen die bestaan uit veel microservices (gebonden contexten in domeingestuurde ontwerpterminologie of gewoon 'subsystemen' als autonome microservices), kunt u elke microservice op een andere manier implementeren. Elk kan een ander architectuurpatroon hebben en verschillende talen en databases gebruiken, afhankelijk van de aard, bedrijfsvereisten en prioriteiten van de toepassing. In sommige gevallen zijn de microservices mogelijk vergelijkbaar. Maar dat is meestal niet het geval, omdat de contextgrens en vereisten van elk subsysteem meestal anders zijn.
Voor een eenvoudige CRUD-onderhoudstoepassing is het bijvoorbeeld niet zinvol om DDD-patronen te ontwerpen en te implementeren. Maar voor uw kerndomein of kernactiviteiten moet u mogelijk geavanceerdere patronen toepassen om de complexiteit van het bedrijf aan te pakken met steeds veranderende bedrijfsregels.
Vooral wanneer u te maken hebt met grote toepassingen die zijn samengesteld door meerdere subsystemen, moet u niet één architectuur op het hoogste niveau toepassen op basis van één architectuurpatroon. CQRS mag bijvoorbeeld niet worden toegepast als architectuur op het hoogste niveau voor een hele toepassing, maar kan nuttig zijn voor een specifieke set services.
Er is geen zilveren opsommingsteken of een correct architectuurpatroon voor elke case. U kunt niet 'één architectuurpatroon hebben om ze allemaal te beheren'. Afhankelijk van de prioriteiten van elke microservice, moet u voor elke microservice een andere benadering kiezen, zoals wordt uitgelegd in de volgende secties.