Berichten in een servicegerichte architectuur

(Joan Zapata) (8 december 2020)

Service-georiënteerde architectuur (SOA) heeft bewezen zijn sterke punten in vrijwel alle industrieën om schaalbare en evolutieve systemen te produceren. SOA heeft veel betekenissen, maar het kernidee om een ​​complexe applicatie op te splitsen in kleinere, herbruikbare en losjes gekoppelde services heeft een lange weg afgelegd sinds eind jaren negentig. We hebben het concept vanaf dag één omarmd en na drie jaar hebben we nu ongeveer twintig domeingestuurde ontkoppelde services die onze backend-applicaties structureren. Hierdoor konden we de talen gebruiken die we leuk vonden – zoals Kotlin , Elixir – en de tools we wilden – zoals Event Sourcing – voor de taak die voorhanden was.

Maar het hebben van meerdere services brengt zijn eigen uitdagingen met zich mee, de eerste is hoe services met elkaar laten communiceren ? Er zijn veel strategieën beschikbaar, en dit artikel is een terugkeer van de ervaring met degene die we gebruiken bij Memo Bank.

Laten we beginnen met twee services en dit eenvoudige gebruik als voorbeeld:

RUST over HTTPS

Net als veel andere backend-applicaties zijn we begonnen met REST oproepen via HTTPS.

Cyclische afhankelijkheden tussen services moeten ten koste van alles worden vermeden, dus contracten kan klanten niet rechtstreeks vertellen om de nieuwe klant te activeren zodra het contract is ondertekend. In plaats daarvan moeten klanten regelmatig verzoeken naar contracten sturen om te weten wanneer de taak is voltooid.

HTTPS is een goed begin, en zeker iets om in de gereedschapskist te houden: het is eenvoudig in te stellen, wordt breed ondersteund en ook gebruikt om APIs bloot te stellen aan frontend-applicaties. Maar er is één groot probleem met dit schema, het is synchroon (hoewel het niet noodzakelijk blokkeert). Het betekent dat terwijl contracten werkt, klanten wachten met een actieve verbinding. Bij grootschalig gebruik betekent dit dat de langzaamste service het hele systeem kan vertragen. Het betekent ook dat wanneer de service contracten niet beschikbaar is, de service klanten dat ook niet is, wat cascading failure wordt genoemd.

Hoe hebben we dit aangepakt deze kwestie? We gebruikten twee verschillende patronen: discrete en continue berichtenuitwisseling.

Discrete berichtenverkeer

Laten we de eerste use case nemen: klanten willen contracten om een ​​contract te sturen.

Hiervoor kunnen we het probleem oplossen met behulp van een discreet berichtmechanisme: wachtrijen . U kunt een wachtrij zien als een postbus van derden met hoge beschikbaarheid. Zelfs als de hoofddienst niet beschikbaar is, accepteert de mailbox berichten. Meerdere exemplaren van dezelfde service kunnen dezelfde wachtrij gebruiken, in welk geval de wachtrij fungeert als een load balancer en ervoor zorgt dat elk bericht minstens één keer wordt afgehandeld. Zodra een bericht is afgehandeld, kan de mailbox het vergeten.

We gebruiken wachtrijen om -opdrachten (bijv. “SendContract”) te verzenden van de ene dienst naar de andere. Het heeft verschillende voordelen:

  • De beller-service vertrouwt niet op de beschikbaarheid van de ontvangende service, en kan zijn werk hervatten zodra het commando in de wachtrij staat;
  • De ontvanger kan de commandos in zijn eigen tempo afhandelen , en we kunnen de ontvangerservice gemakkelijk omhoog of omlaag schalen, afhankelijk van de belasting van de wachtrijen;
  • Als bonus storing kan gemakkelijk worden geïsoleerd en handmatig worden afgehandeld (we hopen de wachtrijen met dode letters in een ander artikel te behandelen).

Continuous messaging

Laten we nu eens kijken naar de tweede use case: wanneer het contract is ondertekend, moeten contracten klanten dit laten weten gebeurde zodat het de klant kan activeren.

Het is verleidelijk om hier een andere wachtrij te gebruiken, maar zoals we al eerder zeiden, willen we geen cyclische afhankelijkheid, dus contracten weten het niet iets over klanten . Het kan dus geen “ActivateCustomer” -commando verzenden, en het kan niet weten wanneer of waar het dat commando naartoe moet sturen.

We hebben dit probleem opgelost met behulp van een continu berichtmechanisme: streams .U kunt een stream zien als een geordende reeks gebeurtenissen die in realtime kunnen worden gebruikt, maar ook in de loop van de tijd beschikbaar worden gemaakt. In tegenstelling tot wachtrijen die niet-gerelateerde kortstondige opdrachten zijn, vertellen streams een persistent verhaal .

Elke service bij Memo Bank zendt een stream van events die de levenscyclus van de bronnen beschrijven. Het bouwen en onderhouden van deze evenementen is een integraal onderdeel van elke ontwikkeling , ongeacht of dit evenement onmiddellijk nodig is of niet. Het maakt deel uit van de routine, net als geautomatiseerde tests en meetgegevens.

Deze gebeurtenissen worden dus een betrouwbaar logboek met tijdstempel van onveranderlijk feiten . En aangezien ze deel uitmaken van de API van de emitting-service, kunnen ze door elke andere service worden gebruikt, zowel in realtime (één voor één) als in de toekomst. Deze gebeurtenissen hebben een enorme waarde voor traceerbaarheid en data-analyse .

Samenvattend:

  • Wachtrijen worden gebruikt om opdrachten naar een specifieke service;
  • Streams worden gebruikt om feiten bloot te leggen van een specifieke service.

Alles over afhankelijkheden

Het bovenstaande diagram doet je misschien afvragen, hoewel het naar de pijlen kijkt, introduceert het niet een cyclische afhankelijkheid tussen klanten en contracten?

Nee dat doet het niet. Een afhankelijkheid wordt niet bepaald door de richting van de data, maar door de kennisdiensten van elkaar. Hier zijn klanten op de hoogte van contracten, het vertelt wat te doen en het luistert naar zijn verhaal. Maar contracten weet niets over klanten, het hoeft niet te weten wie de opdrachten verstuurt, noch wie naar zijn verhaal luistert.

Beide de wachtrij en de stream maken deel uit van de contracten API en klanten zijn afhankelijk van deze API.

Met een naamgeving conventie voor commandos en feiten is erg belangrijk om dit idee over te brengen. We gebruiken altijd een basisformulier voor opdrachten, zoals “SendContract”, en een voltooid deelwoordformulier voor feiten, zoals “ContractSent”.

Merk op dat het gemakkelijk overeenkomt met onze Core Banking System-architectuur gebaseerd op CQRS / ES . In deze terminologie zijn commandos hetzelfde, en events zijn feiten.

Hoe de richting van de afhankelijkheid te kiezen

Gezien de principes die eerder zijn uitgelegd, zou deze oplossing net zo geldig zijn:

Maar als beide oplossingen geldig zijn, hoe kies je dan de ene boven de andere? Nou, het is aan jou.

Het komt allemaal neer op de richting van de afhankelijkheid die je wilt in te stellen.

A: klanten is afhankelijk van contracten . B: contracten hangt af van klanten .

Hier zijn enkele vragen die we ons gewoonlijk stellen:

  • Kan de ene service gemakkelijk agnostisch zijn van de andere?
    Hier bijvoorbeeld, zolang we de inhoud aan contracten geven, contracten kunnen totaal agnostisch zijn van welke service er gebruik van maakt. Het is moeilijker voor te stellen dat klanten agnostisch zijn over het feit dat er een contract voor nodig is. Dat is in het voordeel van A.
  • Wat als een service een derde partij was?
    Voor Zo zou het voor Memo Bank geen zin hebben om klanten uit te besteden, maar wel voor contracten . Dit is dus ook in het voordeel van A.
  • Orchestreert één service andere services?
    Impliciete orkestratie is slecht, je kunt er meer over vinden in deze lezing door Bernd Ruecker. Het aanmaken van een klant is een complexe workflow waarbij veel diensten betrokken zijn (e-mails versturen, notificaties, een bankrekening aanmaken, etc.), dus klanten is hier waarschijnlijk een orkestrator.Door de orchestrator afhankelijk te maken van andere services – en niet andersom – wordt de code een stuk gemakkelijker te begrijpen, omdat de volledige workflow op één plek te vinden is. Dat is ook in het voordeel van A.
  • Creëert het een cyclus in de algehele architectuur?
    Zelfs als er geen verband is tussen de twee services, zijn ze allebei afhankelijk van andere services. Stel dat klanten afhankelijk is van gebruikers en gebruikers al afhankelijk is van contracten . Als we oplossing B zouden kiezen, zou er een cyclus ontstaan ​​met de drie services. Dat is ook in het voordeel van A.

Conclusie

Berichten zijn een van de eerste vragen die we moeten beantwoorden bij het creëren van een servicegerichte architectuur. Het gebruik van HTTPS en REST lijkt in eerste instantie de meest eenvoudige oplossing, maar heeft zijn beperkingen. We hebben ons arsenaal voltooid met wachtrijen en streams , en we hebben hoofdzakelijk twee richtlijnen opgesteld.

Ten eerste moet elke service gebeurtenissen wanneer er feiten gebeuren binnen deze service, zelfs als we deze gebeurtenissen nog niet nodig hebben. Deze gebeurtenissen zouden een verhaal moeten vertellen over wat er in de service gebeurt, zoals “ContractSent”, “ContractSigned”. Dit is geweldig voor traceerbaarheid – wat vereist is als bank -, maar ook om de API van elke service te consolideren en om het systeem gemakkelijker te maken om mee te werken voor alle teams.

Ten tweede draait het allemaal om de afhankelijkheden . Afhankelijkheden vormen het systeem en cyclische afhankelijkheden zijn de grootste vijand. Zodra de afhankelijkheden correct zijn ingesteld, zijn de berichtentools er alleen om de gegevens in elke richting te laten stromen.

Oorspronkelijk gepubliceerd op https://memo.bank/en/magazine .

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *