Communicatie tussen services ontwerpen voor microservices

Azure DevOps

De communicatie tussen microservices moet efficiënt en robuust zijn. Met veel kleine services die samenwerken om één zakelijke activiteit te voltooien, kan dit een uitdaging zijn. In dit artikel kijken we naar de afwegingen tussen asynchrone berichten versus synchrone API's. Vervolgens kijken we naar enkele van de uitdagingen bij het ontwerpen van tolerante communicatie tussen services.

Uitdagingen

Hier volgen enkele van de belangrijkste uitdagingen die voortvloeien uit service-naar-service-communicatie. Service-meshes, die verderop in dit artikel worden beschreven, zijn ontworpen om veel van deze uitdagingen aan te kunnen.

Flexibiliteit. Er kunnen tientallen of zelfs honderden exemplaren van een bepaalde microservice zijn. Een exemplaar kan om verschillende redenen mislukken. Er kan een fout op knooppuntniveau zijn, zoals een hardwarefout of een VM opnieuw opstarten. Een exemplaar kan vastlopen of worden overspoeld met aanvragen en geen nieuwe aanvragen kunnen verwerken. Elk van deze gebeurtenissen kan ertoe leiden dat een netwerkoproep mislukt. Er zijn twee ontwerppatronen die kunnen helpen om service-naar-service-netwerkoproepen toleranter te maken:

  • Probeer het opnieuw. Een netwerkoproep kan mislukken vanwege een tijdelijke fout die vanzelf verdwijnt. In plaats van helemaal uit te voeren, moet de aanroeper de bewerking meestal een bepaald aantal keer opnieuw proberen of totdat een geconfigureerde time-outperiode is verstreken. Als een bewerking echter niet idempotent is, kunnen nieuwe pogingen onbedoelde bijwerkingen veroorzaken. De oorspronkelijke oproep kan slagen, maar de beller krijgt nooit een antwoord. Als de aanroeper opnieuw probeert, kan de bewerking twee keer worden aangeroepen. Over het algemeen is het niet veilig om de POST- of PATCH-methoden opnieuw te proberen, omdat deze niet gegarandeerd idempotent zijn.

  • Circuitonderbreker. Te veel mislukte aanvragen kan een knelpunt veroorzaken, omdat aanvragen in behandeling zich opstapelen in de wachtrij. Deze geblokkeerde aanvragen kunnen kritische bedrijfsresources bevatten, zoals geheugen, threads, databaseverbindingen enzovoort, die tot een opeenstapeling van storingen kan leiden. Het circuitonderbrekerpatroon kan voorkomen dat een service herhaaldelijk een bewerking probeert uit te voeren die waarschijnlijk zal mislukken.

Taakverdeling. Wanneer service 'A' service 'B' aanroept, moet de aanvraag een actief exemplaar van service 'B' bereiken. In Kubernetes biedt het Service resourcetype een stabiel IP-adres voor een groep pods. Netwerkverkeer naar het IP-adres van de service wordt doorgestuurd naar een pod door middel van iptable-regels. Standaard wordt een willekeurige pod gekozen. Een service-mesh (zie hieronder) kan intelligentere taakverdelingsalgoritmen bieden op basis van waargenomen latentie of andere metrische gegevens.

Gedistribueerde tracering. Eén transactie kan meerdere services omvatten. Dat kan het lastig maken om de algehele prestaties en status van het systeem te bewaken. Zelfs als elke service logboeken en metrische gegevens genereert, zonder ze aan elkaar te koppelen, zijn ze van beperkt gebruik. In het artikel Logboekregistratie en bewaking wordt meer gesproken over gedistribueerde tracering, maar we noemen het hier als een uitdaging.

Serviceversiebeheer. Wanneer een team een nieuwe versie van een service implementeert, moeten ze voorkomen dat andere services of externe clients die ervan afhankelijk zijn, worden onderbroken. Daarnaast wilt u mogelijk meerdere versies van een service naast elkaar uitvoeren en aanvragen routeren naar een bepaalde versie. Zie API-versiebeheer voor meer informatie over dit probleem.

TLS-versleuteling en wederzijdse TLS-verificatie. Om veiligheidsredenen wilt u mogelijk verkeer tussen services versleutelen met TLS en wederzijdse TLS-verificatie gebruiken om bellers te verifiëren.

Synchrone versus asynchrone berichten

Er zijn twee basisberichtenpatronen die microservices kunnen gebruiken om te communiceren met andere microservices.

  1. Synchrone communicatie. In dit patroon roept een service een API aan die een andere service beschikbaar maakt, met behulp van een protocol zoals HTTP of gRPC. Deze optie is een synchroon berichtenpatroon omdat de beller wacht op een antwoord van de ontvanger.

  2. Asynchroon bericht doorgeven. In dit patroon verzendt een service een bericht zonder te wachten op een antwoord en verwerken een of meer services het bericht asynchroon.

Het is belangrijk om onderscheid te maken tussen asynchrone I/O en een asynchroon protocol. Asynchrone I/O betekent dat de aanroepende thread niet wordt geblokkeerd terwijl de I/O wordt voltooid. Dat is belangrijk voor de prestaties, maar het is een implementatiedetail in termen van de architectuur. Een asynchroon protocol betekent dat de afzender niet op een antwoord wacht. HTTP is een synchroon protocol, ook al kan een HTTP-client asynchrone I/O gebruiken bij het verzenden van een aanvraag.

Er zijn compromissen voor elk patroon. Aanvraag/antwoord is een goed begrepen paradigma, dus het ontwerpen van een API kan natuurlijker aanvoelen dan het ontwerpen van een berichtensysteem. Asynchrone berichten hebben echter enkele voordelen die nuttig kunnen zijn in een microservicearchitectuur:

  • Verminderde koppeling. De afzender van het bericht hoeft de consument niet te weten.

  • Meerdere abonnees. Met behulp van een pub/submodel kunnen meerdere consumenten zich abonneren om gebeurtenissen te ontvangen. Zie Gebeurtenisgestuurde architectuurstijl.

  • Foutisolatie. Als de consument uitvalt, kan de afzender nog steeds berichten verzenden. De berichten worden opgehaald wanneer de consument herstelt. Deze mogelijkheid is vooral handig in een microservicearchitectuur, omdat elke service zijn eigen levenscyclus heeft. Een service kan op elk gewenst moment niet meer beschikbaar zijn of worden vervangen door een nieuwere versie. Asynchrone berichten kunnen onregelmatige downtime verwerken. Synchrone API's daarentegen vereisen dat de downstreamservice beschikbaar is, anders mislukt de bewerking.

  • Reactiesnelheid. Een upstream-service kan sneller reageren als deze niet wacht op downstreamservices. Dit is met name handig in een microservicearchitectuur. Als er een keten van serviceafhankelijkheden is (service A roept B aan, die C aanroept, enzovoort), kan het wachten op synchrone aanroepen onaanvaardbare hoeveelheden latentie toevoegen.

  • Herverdeling van de belasting. Een wachtrij kan fungeren als een buffer om de werkbelasting te herverdeling, zodat ontvangers berichten op hun eigen snelheid kunnen verwerken.

  • Werkstromen. Wachtrijen kunnen worden gebruikt om een werkstroom te beheren door het bericht na elke stap in de werkstroom te controleren.

Er zijn echter ook enkele uitdagingen bij het effectief gebruik van asynchrone berichten.

  • Koppeling met de berichteninfrastructuur. Het gebruik van een bepaalde berichteninfrastructuur kan leiden tot een nauwe koppeling met die infrastructuur. Het is lastig om later over te schakelen naar een andere berichteninfrastructuur.

  • Latentie. End-to-end latentie voor een bewerking kan hoog worden als de berichtenwachtrijen vol raken.

  • Kosten. Bij hoge doorvoer kunnen de financiële kosten van de berichteninfrastructuur aanzienlijk zijn.

  • Complexiteit. Het verwerken van asynchrone berichten is geen triviale taak. U moet bijvoorbeeld gedupliceerde berichten afhandelen door te de-dupliceren of door bewerkingen idempotent te maken. Het is ook moeilijk om semantiek van aanvragen en antwoorden te implementeren met behulp van asynchrone berichten. Als u een antwoord wilt verzenden, hebt u een andere wachtrij nodig, plus een manier om aanvraag- en antwoordberichten te correleren.

  • Doorvoer. Als berichten wachtrijsemantiek vereisen, kan de wachtrij een knelpunt in het systeem worden. Voor elk bericht is ten minste één wachtrijbewerking en één dequeue-bewerking vereist. Bovendien vereist wachtrijsemantiek over het algemeen een soort vergrendeling binnen de berichteninfrastructuur. Als de wachtrij een beheerde service is, is er mogelijk extra latentie, omdat de wachtrij zich buiten het virtuele netwerk van het cluster bevindt. U kunt deze problemen oplossen door berichten in batches te plaatsen, maar dat bemoeilijkt de code. Als voor de berichten geen wachtrijsemantiek is vereist, kunt u mogelijk een gebeurtenisstroom gebruiken in plaats van een wachtrij. Zie Gebeurtenisgestuurde architectuurstijl voor meer informatie.

Drone Delivery: De berichtenpatronen kiezen

In deze oplossing wordt gebruikgemaakt van het voorbeeld van Drone Delivery. Het is ideaal voor de luchtvaart- en vliegtuigindustrie.

Met deze overwegingen in het achterhoofd heeft het ontwikkelteam de volgende ontwerpkeuzes gemaakt voor de Drone Delivery-toepassing:

  • De opnameservice maakt een openbare REST API beschikbaar die clienttoepassingen gebruiken om leveringen te plannen, bij te werken of te annuleren.

  • De opnameservice maakt gebruik van Event Hubs om asynchrone berichten te verzenden naar de Scheduler-service. Asynchrone berichten zijn nodig om de taakverdeling te implementeren die vereist is voor opname.

  • De services Account, Delivery, Package, Drone en Third-party Transport zijn allemaal beschikbaar voor interne REST API's. De Scheduler-service roept deze API's aan om een gebruikersaanvraag uit te voeren. Een reden om synchrone API's te gebruiken, is dat de Scheduler een antwoord moet krijgen van elk van de downstreamservices. Een fout in een van de downstreamservices betekent dat de hele bewerking is mislukt. Een mogelijk probleem is echter de hoeveelheid latentie die wordt geïntroduceerd door het aanroepen van de back-endservices.

  • Als een downstreamservice een niet-tijdelijke fout heeft, moet de hele transactie worden gemarkeerd als mislukt. Om dit geval af te handelen, stuurt de Scheduler-service een asynchroon bericht naar de Supervisor, zodat de Supervisor compenserende transacties kan plannen.

  • De Delivery-service maakt een openbare API beschikbaar die clients kunnen gebruiken om de status van een levering op te halen. In het artikel API-gateway bespreken we hoe een API-gateway de onderliggende services voor de client kan verbergen, zodat de client niet hoeft te weten welke services welke API's beschikbaar maken.

  • Terwijl een drone in vlucht is, verzendt de Drone-service gebeurtenissen die de huidige locatie en status van de drone bevatten. De Leveringsservice luistert naar deze gebeurtenissen om de status van een levering bij te houden.

  • Wanneer de status van een levering verandert, verzendt de bezorgingsservice een bezorgingsstatus-gebeurtenis, zoals DeliveryCreated of DeliveryCompleted. Elke service kan zich abonneren op deze gebeurtenissen. In het huidige ontwerp is de service Leveringsgeschiedenis de enige abonnee, maar er kunnen later nog andere abonnees zijn. De gebeurtenissen kunnen bijvoorbeeld naar een realtime analyseservice gaan. En omdat de Scheduler niet hoeft te wachten op een antwoord, heeft het toevoegen van meer abonnees geen invloed op het hoofdwerkstroompad.

Diagram van dronecommunicatie

U ziet dat bezorgingsstatus-gebeurtenissen worden afgeleid van dronelocatie-gebeurtenissen. Wanneer een drone bijvoorbeeld een bezorglocatie bereikt en een pakket aflevert, vertaalt de Delivery-service dit in een DeliveryCompleted-gebeurtenis. Dit is een voorbeeld van denken in termen van domeinmodellen. Zoals eerder beschreven, hoort Drone Management in een afzonderlijke begrensde context. De drone-gebeurtenissen brengen de fysieke locatie van een drone over. De leveringsevenementen daarentegen vertegenwoordigen wijzigingen in de status van een levering, die een andere bedrijfsentiteit is.

Een service-mesh gebruiken

Een service-mesh is een softwarelaag die service-naar-service-communicatie afhandelt. Service-meshes zijn ontworpen om veel van de problemen in de vorige sectie aan te pakken en om de verantwoordelijkheid voor deze problemen weg te verplaatsen van de microservices zelf en naar een gedeelde laag. De service-mesh fungeert als een proxy die netwerkcommunicatie tussen microservices in het cluster onderschept. Op dit moment is het service mesh-concept voornamelijk van toepassing op containerorchestrators, in plaats van serverloze architecturen.

Notitie

Service-mesh is een voorbeeld van het Ambassador-patroon : een helperservice die netwerkaanvragen verzendt namens de toepassing.

Op dit moment zijn Linkerd en Istio de belangrijkste opties voor een service-mesh in Kubernetes. Beide technologieën ontwikkelen zich snel. Enkele functies die linkerd en Istio gemeen hebben, zijn echter:

  • Taakverdeling op sessieniveau, op basis van waargenomen latenties of het aantal openstaande aanvragen. Dit kan de prestaties verbeteren ten opzichte van de laag-4-taakverdeling die wordt geleverd door Kubernetes.

  • Laag 7-routering op basis van URL-pad, hostheader, API-versie of andere regels op toepassingsniveau.

  • Probeer mislukte aanvragen opnieuw. Een service-mesh begrijpt HTTP-foutcodes en kan mislukte aanvragen automatisch opnieuw proberen. U kunt dat maximum aantal nieuwe pogingen configureren, samen met een time-outperiode om de maximale latentie te beperken.

  • Circuitonderbreking. Als aanvragen voor een exemplaar consistent mislukken, wordt de service-mesh tijdelijk gemarkeerd als niet-beschikbaar. Na een uitstelperiode wordt het exemplaar opnieuw geprobeerd. U kunt de circuitonderbreker configureren op basis van verschillende criteria, zoals het aantal opeenvolgende fouten,

  • Service mesh legt metrische gegevens vast over aanroepen tussen services, zoals het aanvraagvolume, latentie, fout- en slagingspercentages en antwoordgrootten. De service-mesh maakt ook gedistribueerde tracering mogelijk door correlatie-informatie toe te voegen voor elke hop in een aanvraag.

  • Wederzijdse TLS-verificatie voor service-naar-service-aanroepen.

Hebt u een service mesh nodig? Dat hangt ervan af. Zonder service-mesh moet u rekening houden met elk van de uitdagingen die aan het begin van dit artikel worden vermeld. U kunt problemen zoals opnieuw proberen, circuitonderbrekers en gedistribueerde tracering oplossen zonder service mesh, maar een service-mesh verplaatst deze problemen uit de afzonderlijke services en naar een toegewezen laag. Een service-mesh voegt daarentegen complexiteit toe aan de installatie en configuratie van het cluster. Er kunnen gevolgen zijn voor de prestaties, omdat aanvragen nu worden doorgestuurd via de service mesh-proxy en omdat er nu extra services worden uitgevoerd op elk knooppunt in het cluster. U moet de prestaties en belasting grondig testen voordat u een service-mesh in productie implementeert.

Gedistribueerde transacties

Een veelvoorkomende uitdaging in microservices is het correct verwerken van transacties die meerdere services omvatten. In dit scenario is het succes van een transactie vaak alles of niets. Als een van de deelnemende services mislukt, moet de hele transactie mislukken.

Er zijn twee zaken die u moet overwegen:

  • Een service kan een tijdelijke fout ondervinden, zoals een netwerktime-out. Deze fouten kunnen vaak worden opgelost door de aanroep opnieuw te proberen. Als de bewerking na een bepaald aantal pogingen nog steeds mislukt, wordt deze beschouwd als een niet-tijdelijke fout.

  • Een niet-tijdelijke fout is een fout die waarschijnlijk niet vanzelf verdwijnt. Niet-tijdelijke fouten omvatten normale foutvoorwaarden, zoals ongeldige invoer. Ze omvatten ook onverwerkte uitzonderingen in toepassingscode of een proces dat vastloopt. Als dit type fout optreedt, moet de hele zakelijke transactie worden gemarkeerd als een fout. Het kan nodig zijn om andere stappen ongedaan te maken in dezelfde transactie die al zijn geslaagd.

Na een niet-tijdelijke fout kan de huidige transactie een gedeeltelijk mislukte status hebben, waarbij een of meer stappen al zijn voltooid. Als de Drone-service bijvoorbeeld al een drone heeft gepland, moet de drone worden geannuleerd. In dat geval moet de toepassing de stappen die zijn voltooid ongedaan maken met behulp van een compenserende transactie. In sommige gevallen moet dit worden gedaan door een extern systeem of zelfs door een handmatig proces.

Als de logica voor compenserende transacties complex is, kunt u overwegen om een afzonderlijke service te maken die verantwoordelijk is voor dit proces. In de Drone Delivery-toepassing plaatst de Scheduler-service mislukte bewerkingen in een toegewezen wachtrij. Een afzonderlijke microservice, de Supervisor genaamd, leest uit deze wachtrij en roept een annulerings-API aan voor de services die moeten worden gecompenseerd. Dit is een variatie op het patroon Scheduler Agent Supervisor. De Supervisor-service kan ook andere acties uitvoeren, zoals de gebruiker per sms of e-mail op de hoogte stellen of een waarschuwing verzenden naar een bewerkingsdashboard.

Diagram met de microservice Supervisor

De Scheduler-service zelf kan mislukken (bijvoorbeeld omdat een knooppunt vastloopt). In dat geval kan een nieuw exemplaar worden ingesteld en het overnemen. Alle transacties die al werden uitgevoerd, moeten echter worden hervat.

Een van de benaderingen is om een controlepunt op te slaan in een duurzaam archief nadat elke stap in de werkstroom is voltooid. Als een exemplaar van de Scheduler-service vastloopt tijdens een transactie, kan een nieuw exemplaar het controlepunt gebruiken om te hervatten waar het vorige exemplaar was gebleven. Het schrijven van controlepunten kan echter zorgen voor prestatieoverhead.

Een andere optie is om alle bewerkingen idempotent te maken. Een bewerking is idempotent als deze meerdere keren kan worden aangeroepen zonder extra bijwerkingen te produceren na de eerste aanroep. In wezen moet de downstreamservice dubbele aanroepen negeren, wat betekent dat de service dubbele aanroepen moet kunnen detecteren. Het is niet altijd eenvoudig om idempotente methoden te implementeren. Zie Idempotente bewerkingen voor meer informatie.

Volgende stappen

Voor microservices die rechtstreeks met elkaar communiceren, is het belangrijk om goed ontworpen API's te maken.