Communicatie tussen services ontwerpen voor microservices

Communicatie tussen microservices moet efficiënt en robuust zijn. Omdat er veel kleine services met elkaar samenwerken om één zakelijke activiteit te voltooien, kan dit een uitdaging zijn. In dit artikel kijken we naar de balans tussen asynchrone berichten en synchrone API's. Vervolgens kijken we naar een aantal uitdagingen bij het ontwerpen van flexibele communicatie tussen de service.

Uitdagingen

Hier zijn enkele van de belangrijkste uitdagingen die voortvloeien uit service-naar-service-communicatie. Service-meshes, die later 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 een aantal redenen mislukken. Er kan een fout op knooppuntniveau zijn, zoals een hardwarefout of het opnieuw opstarten van een VM. Een exemplaar kan vastgelopen zijn of worden overspoeld met aanvragen en kan geen nieuwe aanvragen verwerken. Elk van deze gebeurtenissen kan ertoe leiden dat een netwerkoproep mislukt. Er zijn twee ontwerppatronen die service-naar-service-netwerk-aanroepen robuuster kunnen maken:

  • Opnieuw proberen. Een netwerkoproep kan mislukken vanwege een tijdelijke fout die vanzelf wordt verwijderd. In plaats van direct te mislukken, moet de aanroeper de bewerking doorgaans een bepaald aantal keren opnieuw proberen of totdat een geconfigureerde time-outperiode is verstreken. Als een bewerking echter niet idempotent is, kunnen nieuwe proberen onbedoelde neveneffecten veroorzaken. De oorspronkelijke aanroep kan slagen, maar de aanroeper krijgt nooit een antwoord. Als de aanroeper nieuwe aanroepen onderroept, kan de bewerking tweemaal worden aangeroepen. Over het algemeen is het niet veilig om POST- of PATCH-methoden opnieuw uit te proberen, omdat deze niet gegarandeerd idempotent zijn.

  • Circuit breaker. Te veel mislukte aanvragen kunnen een knelpunt veroorzaken, omdat aanvragen in behandeling in de wachtrij worden verzameld. Deze geblokkeerde aanvragen kunnen kritische bedrijfsresources bevatten, zoals geheugen, threads, databaseverbindingen enzovoort, die tot een opeenstapeling van storingen kan leiden. Het patroon Circuit breaker kan verhinderen 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 Service het resourcetype een stabiel IP-adres voor een groep pods. Netwerkverkeer naar het IP-adres van de service wordt doorgestuurd naar een pod met 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 omvat. 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 een manier om ze aan elkaar te binden, zijn ze van beperkt gebruik. In het artikel Logboekregistratie en controle wordt meer gesproken over gedistribueerde tracering, maar we noemen dit hier als een uitdaging.

Versieversie van de service. Wanneer een team een nieuwe versie van een service implementeert, moeten ze voorkomen dat andere services of externe clients die ervan afhankelijk zijn, worden door het team worden breekt. Bovendien wilt u mogelijk meerdere versies van een service naast elkaar uitvoeren en aanvragen naar een bepaalde versie doorseen. Zie API Versioning (API-versieverversies) voor meer informatie over dit probleem.

TLS-versleuteling en wederzijdse TLS-verificatie. Uit veiligheidsoverwegingen wilt u mogelijk verkeer tussen services versleutelen met TLS en wederzijdse TLS-verificatie gebruiken om aanroepers te verifiëren.

Synchrone versus asynchrone berichten

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

  1. Synchrone communicatie. In dit patroon roept een service een API aan die door een andere service beschikbaar wordt gemaakt, met behulp van een protocol zoals HTTP of gRPC. Deze optie is een synchroon berichtpatroon omdat de aanroeper wacht op een reactie van de ontvanger.

  2. Asynchrone 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 is voltooid. Dat is belangrijk voor de prestaties, maar is een implementatiedetail in termen van de architectuur. Een asynchroon protocol betekent dat de afzender niet wacht op een antwoord. HTTP is een synchroon protocol, ook al kan een HTTP-client asynchrone I/O gebruiken wanneer deze een aanvraag verzendt.

Er zijn afwegingen met elk patroon. Aanvraag/antwoord is een goed te begrijpen paradigma, dus het ontwerpen van een API kan natuurlijker zijn dan het ontwerpen van een berichtensysteem. Asynchrone berichten bieden echter enkele voordelen die nuttig kunnen zijn in een microservicesarchitectuur:

  • Beperkte koppeling. De afzender van het bericht hoeft niets te weten over de consument.

  • Meerdere abonnees. Met behulp van een pub/submodel kunnen meerdere consumenten zich abonneren op het ontvangen van gebeurtenissen. 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 nuttig in een microservicearchitectuur, omdat elke service een eigen levenscyclus heeft. Een service kan op elk moment niet meer beschikbaar zijn of worden vervangen door een nieuwere versie. Asynchrone berichten kunnen onregelmatige downtime verwerken. Synchrone API's vereisen daarentegen dat de downstreamservice beschikbaar is of dat de bewerking mislukt.

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

  • Load leveling. Een wachtrij kan fungeren als een buffer om de workload te levelen, zodat ontvangers berichten op hun eigen snelheid kunnen verwerken.

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

Er zijn echter ook enkele uitdagingen voor het effectief gebruiken van asynchrone berichten.

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

  • Latentie. End-to-endlatentie 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 dubbele berichten afhandelen, ofwel door ontdubbeling of door bewerkingen idempotent te maken. Het is ook moeilijk om semantiek voor aanvraag-antwoord 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 voor berichten wachtrijsemantiek is vereist, kan de wachtrij een knelpunt in het systeem worden. Voor elk bericht is ten minste één wachtrijbewerking en één uit de wachtrij verwijderde bewerking vereist. Bovendien is voor wachtrijsemantiek doorgaans een soort vergrendeling binnen de berichteninfrastructuur vereist. Als de wachtrij een beheerde service is, kan er extra latentie zijn, omdat de wachtrij extern is voor het virtuele netwerk van het cluster. U kunt deze problemen oplossen door berichten batchig te verzenden, maar dat maakt de code ingewikkelder. 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 berichtpatronen kiezen

Met deze overwegingen in gedachten heeft het ontwikkelteam de volgende ontwerpkeuzen gemaakt voor de Drone Delivery-toepassing

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

  • De opnameservice gebruikt Event Hubs om asynchrone berichten naar de Scheduler-service te verzenden. Asynchrone berichten zijn nodig om de vereiste load leveling voor opname te implementeren.

  • De services Account, Delivery, Package, Drone en Third-Party Transport bieden allemaal interne REST API's. De Scheduler-service roept deze API's aan om een gebruikersaanvraag uit te voeren. Een van de redenen voor het gebruik van synchrone API's is dat de Scheduler een reactie 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 als mislukt worden gemarkeerd. Om dit geval af te handelen, verzendt de Scheduler-service een asynchroon bericht naar de Supervisor, zodat de Supervisor compenserende transacties kan plannen.

  • De Leveringsservice maakt een openbare API beschikbaar die clients kunnen gebruiken om de status van een levering op te halen. In het artikel API-gatewaywordt besproken 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 op de 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 wordt gewijzigd, verzendt de leveringsservice een leveringsstatusgebeurtenis, zoals DeliveryCreated of DeliveryCompleted . Elke service kan zich abonneren op deze gebeurtenissen. In het huidige ontwerp is de Leveringsgeschiedenis-service de enige abonnee, maar mogelijk zijn er later nog andere abonnees. 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 leveringsstatusgebeurtenissen worden afgeleid van locatiegebeurtenissen van drones. Wanneer een drone bijvoorbeeld een bezorgingslocatie bereikt en een pakket aflevert, wordt dit door de leveringsservice omgezet in een DeliveryCompleted-gebeurtenis. Dit is een voorbeeld van het denken in termen van domeinmodellen. Zoals eerder beschreven, behoort Drone Management tot een afzonderlijke contextgrens. De dronegebeurtenissen geven de fysieke locatie van een drone aan. De leveringsgebeurtenissen vertegenwoordigen daarentegen wijzigingen in de status van een levering, wat een andere bedrijfsentiteit is.

Een service-mesh gebruiken

Een service-mesh is een softwarelaag die service-naar-servicecommunicatie verwerkt. 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 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 container-orchestrators, in plaats van serverloze architecturen.

Notitie

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

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

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

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

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

  • Circuitdebreking. Als aanvragen door een instantie consistent worden mislukt, wordt deze door de service-mesh tijdelijk als niet-beschikbaar markeren. Na een back-offperiode wordt het exemplaar opnieuw geprobeerd. U kunt de circuit breaker configureren op basis van verschillende criteria, zoals het aantal opeenvolgende fouten,

  • Service mesh legt metrische gegevens vast over interservice-aanroepen, zoals het aanvraagvolume, latentie, fout- en succespercentages en antwoordgrootten. De service-mesh maakt ook gedistribueerde tracering mogelijk door correlatiegegevens 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 een service-mesh moet u rekening houden met elk van de uitdagingen die aan het begin van dit artikel worden genoemd. U kunt problemen zoals opnieuw proberen, circuit breaker en gedistribueerde tracering oplossen zonder een service-mesh, maar een service-mesh verplaatst deze problemen van de afzonderlijke services naar een toegewezen laag. Aan de andere kant voegt een service-mesh complexiteit toe aan de installatie en configuratie van het cluster. Dit kan gevolgen hebben voor de prestaties, omdat aanvragen nu worden gerouteerd via de service-mesh-proxy en omdat er nu extra services worden uitgevoerd op elk knooppunt in het cluster. U moet grondige prestatie- en belastingstests uitvoeren voordat u een service-mesh in productie implementeert.

Gedistribueerde transacties

Een veelvoorkomende uitdaging in microservices is het correct verwerken van transacties die meerdere services bespannen. In dit scenario is het succes van een transactie vaak alles of niets als een van de deelnemende services uitvalt. De hele — transactie moet mislukken.

Er zijn twee gevallen om rekening mee te houden:

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

  • Een niet-transiente fout is een fout die op zichzelf waarschijnlijk niet zal verdwijnen. Niet-tijdelijke fouten omvatten normale foutvoorwaarden, zoals ongeldige invoer. Ze omvatten ook onverhandelde uitzonderingen in toepassingscode of een proces dat vast loopt. Als dit type fout optreedt, moet de hele bedrijfstransactie worden gemarkeerd als een fout. Het kan nodig zijn om andere stappen in dezelfde transactie die al is geslaagd ongedaan te maken.

Na een niet-tijdelijke fout heeft de huidige transactie mogelijk een gedeeltelijk mislukte status, 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 geslaagd 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 compenseren. Dit is een variant van het Scheduler Agent Supervisor-patroon. De Supervisor-service kan ook andere acties uitvoeren, zoals de gebruiker via sms of e-mail op de hoogte stellen of een waarschuwing verzenden naar een dashboard voor bewerkingen.

Diagram met de Supervisor-microservice

De Scheduler-service zelf kan mislukken (bijvoorbeeld omdat een knooppunt vast loopt). In dat geval kan een nieuwe instantie worden aangenomen en overnemen. Alle transacties die al worden uitgevoerd, moeten echter worden hervat.

Een van de benaderingen is het opslaan van een controlepunt in een duurzame opslag nadat elke stap in de werkstroom is voltooid. Als een exemplaar van de Scheduler-service midden in een transactie vast loopt, kan een nieuw exemplaar het controlepunt gebruiken om te hervatten waar het vorige exemplaar was uitgeschakeld. Het schrijven van controlepunten kan echter leiden tot prestatieoverhead.

Een andere optie is om alle bewerkingen zo te ontwerpen dat ze idempotent zijn. Een bewerking is idempotent als deze meerdere keren kan worden aangeroepen zonder extra neveneffecten 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.