Redigera

Dela via


Utforma kommunikation mellan tjänster för mikrotjänster

Azure DevOps

Kommunikationen mellan mikrotjänster måste vara effektiv och robust. Med många små tjänster som interagerar för att slutföra en enda affärsaktivitet kan detta vara en utmaning. I den här artikeln tittar vi på kompromisserna mellan asynkrona meddelanden och synkrona API:er. Sedan tittar vi på några av utmaningarna med att utforma elastisk kommunikation mellan tjänster.

Utmaningar

Här är några av de största utmaningarna som uppstår vid service-till-tjänst-kommunikation. Tjänstnät, som beskrivs senare i den här artikeln, är utformade för att hantera många av dessa utmaningar.

Återhämtning. Det kan finnas dussintals eller till och med hundratals instanser av en viss mikrotjänst. En instans kan misslyckas av valfritt antal orsaker. Det kan uppstå ett fel på nodnivå, till exempel ett maskinvarufel eller en omstart av den virtuella datorn. En instans kan krascha eller överbelastas med begäranden och kan inte bearbeta några nya begäranden. Alla dessa händelser kan orsaka att ett nätverksanrop misslyckas. Det finns två designmönster som kan hjälpa till att göra tjänst-till-tjänst-nätverksanrop mer motståndskraftiga:

  • Försöka igen. Ett nätverksanrop kan misslyckas på grund av ett tillfälligt fel som försvinner av sig självt. I stället för att misslyckas direkt bör anroparen vanligtvis försöka utföra åtgärden igen ett visst antal gånger, eller tills en konfigurerad tidsgräns förflutit. Men om en åtgärd inte är idempotent kan återförsök orsaka oavsiktliga biverkningar. Det ursprungliga anropet kan lyckas, men anroparen får aldrig något svar. Om anroparen försöker igen kan åtgärden anropas två gånger. I allmänhet är det inte säkert att försöka post- eller PATCH-metoder igen, eftersom dessa inte garanteras vara idempotenter.

  • Kretsbrytare. För många misslyckade begäranden kan orsaka en flaskhals, eftersom väntande begäranden ackumuleras i kön. Dessa blockerade förfrågningar kan använda kritiska systemresurser som minne, trådar, databasanslutningar och så vidare, vilket kan orsaka eskalerande fel. Kretsbrytarmönstret kan förhindra att en tjänst upprepade gånger försöker utföra en åtgärd som sannolikt kommer att misslyckas.

Belastningsutjämning. När tjänsten "A" anropar tjänsten "B" måste begäran nå en instans av tjänsten "B" som körs. I Kubernetes Service tillhandahåller resurstypen en stabil IP-adress för en grupp poddar. Nätverkstrafik till tjänstens IP-adress vidarebefordras till en podd med hjälp av iptable-regler. Som standard väljs en slumpmässig podd. Ett tjänstnät (se nedan) kan ge mer intelligenta belastningsutjämningsalgoritmer baserat på observerade svarstider eller andra mått.

Distribuerad spårning. En enskild transaktion kan omfatta flera tjänster. Det kan göra det svårt att övervaka systemets övergripande prestanda och hälsa. Även om varje tjänst genererar loggar och mått, utan något sätt att koppla ihop dem, är de av begränsad användning. I artikeln Loggning och övervakning beskrivs mer om distribuerad spårning, men vi nämner det här som en utmaning.

Versionshantering av tjänsten. När ett team distribuerar en ny version av en tjänst måste de undvika att bryta andra tjänster eller externa klienter som är beroende av den. Dessutom kanske du vill köra flera versioner av en tjänst sida vid sida och dirigera begäranden till en viss version. Mer information om det här problemet finns i API-versionshantering .

TLS-kryptering och ömsesidig TLS-autentisering. Av säkerhetsskäl kanske du vill kryptera trafik mellan tjänster med TLS och använda ömsesidig TLS-autentisering för att autentisera anropare.

Synkrona kontra asynkrona meddelanden

Det finns två grundläggande meddelandemönster som mikrotjänster kan använda för att kommunicera med andra mikrotjänster.

  1. Synkron kommunikation. I det här mönstret anropar en tjänst ett API som en annan tjänst exponerar med hjälp av ett protokoll som HTTP eller gRPC. Det här alternativet är ett synkront meddelandemönster eftersom anroparen väntar på ett svar från mottagaren.

  2. Asynkront meddelande skickas. I det här mönstret skickar en tjänst ett meddelande utan att vänta på ett svar, och en eller flera tjänster bearbetar meddelandet asynkront.

Det är viktigt att skilja mellan asynkron I/O och ett asynkront protokoll. Asynkron I/O innebär att den anropande tråden inte blockeras medan I/O slutförs. Det är viktigt för prestanda, men är en implementeringsdetalj när det gäller arkitekturen. Ett asynkront protokoll innebär att avsändaren inte väntar på ett svar. HTTP är ett synkront protokoll, även om en HTTP-klient kan använda asynkron I/O när den skickar en begäran.

Det finns kompromisser med varje mönster. Begäran/svar är ett väl förstått paradigm, så det kan kännas mer naturligt att utforma ett API än att utforma ett meddelandesystem. Asynkrona meddelanden har dock vissa fördelar som kan vara användbara i en arkitektur för mikrotjänster:

  • Reducerad koppling. Meddelandesändaren behöver inte känna till konsumenten.

  • Flera prenumeranter. Med hjälp av en pub/undermodell kan flera konsumenter prenumerera på att ta emot händelser. Se Format för händelsedriven arkitektur.

  • Felisolering. Om konsumenten misslyckas kan avsändaren fortfarande skicka meddelanden. Meddelandena hämtas när konsumenten återställs. Den här möjligheten är särskilt användbar i en mikrotjänstarkitektur, eftersom varje tjänst har sin egen livscykel. En tjänst kan bli otillgänglig eller ersättas med en nyare version vid en viss tidpunkt. Asynkrona meddelanden kan hantera tillfälliga driftstopp. Synkrona API:er kräver å andra sidan att den underordnade tjänsten är tillgänglig eller att åtgärden misslyckas.

  • Svarstider. En överordnad tjänst kan svara snabbare om den inte väntar på underordnade tjänster. Detta är särskilt användbart i en mikrotjänstarkitektur. Om det finns en kedja av tjänstberoenden (tjänst A anropar B, som anropar C och så vidare) kan väntan på synkrona anrop lägga till oacceptabla svarstider.

  • Belastningsutjämning. En kö kan fungera som en buffert för att utjämna arbetsbelastningen, så att mottagarna kan bearbeta meddelanden i sin egen takt.

  • Arbetsflöden. Köer kan användas för att hantera ett arbetsflöde genom att markera meddelandet efter varje steg i arbetsflödet.

Det finns dock även vissa utmaningar med att använda asynkrona meddelanden effektivt.

  • Koppling till meddelandeinfrastrukturen. Om du använder en viss meddelandeinfrastruktur kan det leda till en nära koppling till infrastrukturen. Det blir svårt att växla till en annan meddelandeinfrastruktur senare.

  • Svarstid. Svarstiden från slutpunkt till slutpunkt för en åtgärd kan bli hög om meddelandeköerna fylls.

  • Kostnad. Vid höga dataflöden kan den ekonomiska kostnaden för meddelandeinfrastrukturen vara betydande.

  • Komplexitet. Att hantera asynkrona meddelanden är inte en trivial uppgift. Du måste till exempel hantera duplicerade meddelanden, antingen genom att deduplicera eller genom att göra åtgärder idempotent. Det är också svårt att implementera semantik för begärandesvar med asynkrona meddelanden. För att skicka ett svar behöver du en annan kö, plus ett sätt att korrelera begärande- och svarsmeddelanden.

  • Dataflöde. Om meddelanden kräver kösemantik kan kön bli en flaskhals i systemet. Varje meddelande kräver minst en köåtgärd och en dequeue-åtgärd. Dessutom kräver kösemantik vanligtvis någon form av låsning i meddelandeinfrastrukturen. Om kön är en hanterad tjänst kan det finnas ytterligare svarstider eftersom kön är extern för klustrets virtuella nätverk. Du kan åtgärda dessa problem genom att batcha meddelanden, men det komplicerar koden. Om meddelandena inte kräver kösemantik kan du kanske använda en händelseström i stället för en kö. Mer information finns i Händelsedriven arkitekturstil.

Drönarleverans: Välja meddelandemönster

Den här lösningen använder exemplet drönarleverans. Det är idealiskt för flyg- och flygplansindustrin.

Med dessa överväganden i åtanke gjorde utvecklingsteamet följande designval för drone delivery-programmet:

  • Inmatningstjänsten exponerar ett offentligt REST-API som klientprogram använder för att schemalägga, uppdatera eller avbryta leveranser.

  • Inmatningstjänsten använder Event Hubs för att skicka asynkrona meddelanden till Scheduler-tjänsten. Asynkrona meddelanden krävs för att implementera den belastningsnivå som krävs för inmatning.

  • Tjänsterna Account, Delivery, Package, Drone och Third-Party Transport exponerar alla interna REST-API:er. Scheduler-tjänsten anropar dessa API:er för att utföra en användarbegäran. En anledning till att använda synkrona API:er är att Scheduler måste få ett svar från var och en av de underordnade tjänsterna. Ett fel i någon av de underordnade tjänsterna innebär att hela åtgärden misslyckades. Ett potentiellt problem är dock hur lång svarstid som introduceras genom att anropa serverdelstjänsterna.

  • Om någon underordnad tjänst har ett icke-övergående fel bör hela transaktionen markeras som misslyckad. För att hantera det här fallet skickar Scheduler-tjänsten ett asynkront meddelande till övervakaren, så att övervakaren kan schemalägga kompenserande transaktioner.

  • Leveranstjänsten exponerar ett offentligt API som klienter kan använda för att hämta status för en leverans. I artikeln API-gateway diskuterar vi hur en API-gateway kan dölja de underliggande tjänsterna från klienten, så att klienten inte behöver veta vilka tjänster som exponerar vilka API:er.

  • Medan en drönare är under flygning skickar Drönartjänsten händelser som innehåller drönarens aktuella plats och status. Leveranstjänsten lyssnar på dessa händelser för att spåra status för en leverans.

  • När statusen för en leverans ändras skickar leveranstjänsten en leveransstatushändelse, till exempel DeliveryCreated eller DeliveryCompleted. Alla tjänster kan prenumerera på dessa händelser. I den aktuella designen är tjänsten Leveranshistorik den enda prenumeranten, men det kan finnas andra prenumeranter senare. Händelserna kan till exempel gå till en realtidsanalystjänst. Och eftersom Scheduler inte behöver vänta på ett svar påverkar det inte huvudarbetsflödessökvägen att lägga till fler prenumeranter.

Diagram över drönarkommunikation

Observera att leveransstatushändelser härleds från drönarplatshändelser. När en drönare till exempel når en leveransplats och lämnar ett paket översätter leveranstjänsten detta till en DeliveryCompleted-händelse. Detta är ett exempel på tänkande när det gäller domänmodeller. Som vi beskrev tidigare hör Drone Management till i en separat avgränsad kontext. Drönarhändelserna förmedlar den fysiska platsen för en drönare. Leveranshändelserna representerar å andra sidan ändringar i statusen för en leverans, som är en annan affärsentitet.

Använda ett tjänstnät

Ett tjänstnät är ett programvarulager som hanterar kommunikation från tjänst till tjänst. Servicenät är utformade för att hantera många av de problem som anges i föregående avsnitt och för att flytta ansvaret för dessa problem bort från mikrotjänsterna själva och till ett delat lager. Service Mesh fungerar som en proxy som fångar upp nätverkskommunikation mellan mikrotjänster i klustret. För närvarande gäller service mesh-konceptet främst för containerorkestrerare snarare än serverlösa arkitekturer.

Kommentar

Service mesh är ett exempel på ambassadörsmönstret – en hjälptjänst som skickar nätverksbegäranden för programmets räkning.

Just nu är de viktigaste alternativen för ett tjänstnät i Kubernetes Linkerd och Istio. Båda dessa tekniker utvecklas snabbt. Några funktioner som både Linkerd och Istio har gemensamt är dock:

  • Belastningsutjämning på sessionsnivå, baserat på observerade svarstider eller antal utestående begäranden. Detta kan förbättra prestanda jämfört med layer-4-belastningsutjämningen som tillhandahålls av Kubernetes.

  • Layer-7-routning baserat på URL-sökväg, värdhuvud, API-version eller andra regler på programnivå.

  • Försök igen av misslyckade begäranden. Ett servicenät förstår HTTP-felkoder och kan automatiskt försöka igen misslyckade begäranden. Du kan konfigurera det maximala antalet återförsök, tillsammans med en tidsgränsperiod för att begränsa den maximala svarstiden.

  • Kretsbrytning. Om en instans konsekvent misslyckas med begäranden markeras tjänstens nät tillfälligt som otillgängligt. Efter en backoff-period kommer den att försöka instansen igen. Du kan konfigurera kretsbrytaren baserat på olika kriterier, till exempel antalet efterföljande fel,

  • Service Mesh samlar in mått om mellantjänstanrop, till exempel begärandevolym, svarstid, fel- och framgångsfrekvenser och svarsstorlekar. Service Mesh möjliggör även distribuerad spårning genom att lägga till korrelationsinformation för varje hopp i en begäran.

  • Ömsesidig TLS-autentisering för tjänst-till-tjänst-anrop.

Behöver du ett servicenät? Det beror på. Utan ett servicenät måste du överväga var och en av de utmaningar som nämns i början av den här artikeln. Du kan lösa problem som återförsök, kretsbrytare och distribuerad spårning utan servicenät, men ett servicenät flyttar dessa problem från de enskilda tjänsterna och till ett dedikerat lager. Å andra sidan ökar ett tjänstnät komplexiteten i konfigurationen och konfigurationen av klustret. Det kan uppstå prestandakonsekvenser eftersom begäranden nu dirigeras via service mesh-proxyn och eftersom extra tjänster nu körs på varje nod i klustret. Du bör utföra noggranna prestanda- och belastningstester innan du distribuerar ett tjänstnät i produktion.

Distribuerade transaktioner

En vanlig utmaning inom mikrotjänster är korrekt hantering av transaktioner som omfattar flera tjänster. Ofta i det här scenariot är en transaktions framgång allt eller inget – om en av de deltagande tjänsterna misslyckas måste hela transaktionen misslyckas.

Det finns två fall att tänka på:

  • En tjänst kan drabbas av ett tillfälligt fel, till exempel en tidsgräns för nätverket. Dessa fel kan ofta lösas bara genom att försöka anropa igen. Om åtgärden fortfarande misslyckas efter ett visst antal försök anses den vara ett icke-tillfälligt fel.

  • Ett icke-tillfälligt fel är ett fel som sannolikt inte kommer att försvinna av sig självt. Icke-övergående fel omfattar normala feltillstånd, till exempel ogiltiga indata. De innehåller också ohanterade undantag i programkoden eller en process som kraschar. Om den här typen av fel inträffar måste hela affärstransaktionen markeras som ett fel. Det kan vara nödvändigt att ångra andra steg i samma transaktion som redan har slutförts.

Efter ett icke-tillfälligt fel kan den aktuella transaktionen vara i ett delvis misslyckat tillstånd, där ett eller flera steg redan har slutförts. Om drönartjänsten till exempel redan har schemalagt en drönare måste drönaren avbrytas. I så fall måste programmet ångra de steg som lyckades med hjälp av en kompenserande transaktion. I vissa fall måste den här åtgärden utföras av ett externt system eller till och med av en manuell process. Kom ihåg att kompenserande mått också kan misslyckas i designen.

Om logiken för kompenserande transaktioner är komplex kan du överväga att skapa en separat tjänst som ansvarar för den här processen. I programmet Drone Delivery placerar Scheduler-tjänsten misslyckade åtgärder i en dedikerad kö. En separat mikrotjänst, som kallas övervakare, läser från den här kön och anropar ett annullerings-API för de tjänster som behöver kompenseras. Det här är en variant av Scheduler Agent Supervisor-mönstret. Övervakningstjänsten kan också vidta andra åtgärder, till exempel att meddela användaren via sms eller e-post, eller skicka en avisering till en åtgärdsinstrumentpanel.

Diagram som visar övervakarens mikrotjänst

Själva Scheduler-tjänsten kan misslyckas (till exempel på grund av att en nod kraschar). I så fall kan en ny instans startas och ta över. Alla transaktioner som redan pågår måste dock återupptas.

En metod är att spara en kontrollpunkt i ett varaktigt lager när varje steg i arbetsflödet har slutförts. Om en instans av Scheduler-tjänsten kraschar mitt i en transaktion kan en ny instans använda kontrollpunkten för att återuppta den plats där den tidigare instansen slutade. Att skriva kontrollpunkter kan dock skapa prestandakostnader.

Ett annat alternativ är att utforma alla åtgärder som idempotent. En åtgärd är idempotent om den kan anropas flera gånger utan att orsaka ytterligare biverkningar efter det första anropet. I princip bör den underordnade tjänsten ignorera duplicerade anrop, vilket innebär att tjänsten måste kunna identifiera duplicerade anrop. Det är inte alltid enkelt att implementera idempotentmetoder. Mer information finns i Idempotent-åtgärder.

Nästa steg

För mikrotjänster som kommunicerar direkt med varandra är det viktigt att skapa väldesignade API:er.