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

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

Utmaningar

Här är några av de största utmaningarna vid kommunikation mellan tjänster. 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 olika orsaker. Det kan finnas 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 nya begäranden. Alla dessa händelser kan orsaka att ett nätverkssamtal misslyckas. Det finns två designmönster som kan hjälpa dig att göra tjänst-till-tjänst-nätverkssamtal mer motståndskraftiga:

  • Försök igen. Ett nätverkssamtal 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 ett visst antal gånger eller tills en konfigurerad time out-period har gått ut. Men om en åtgärd inte är idempotent kan återförsök orsaka oönskade sidoeffekter. 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 använda POST- eller PATCH-metoder igen, eftersom dessa inte garanterat är idempotenta.

  • Kretsbrytaren. 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 provar en åtgärd som troligen 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 tillhandahåller Service resurstypen en stabil IP-adress för en grupp med 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 algoritmer för belastningsutjämning baserat på observerad svarstid eller andra mått.

Distribuerad spårning. En enda transaktion kan sträcka sig över 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. Artikeln Loggning och övervakning handlar mer om distribuerad spårning, men vi nämner det här som en utmaning.

Tjänstversionshantering. När ett team distribuerar en ny version av en tjänst måste de undvika att andra tjänster eller externa klienter som är beroende av den bryts. 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 trafiken 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 som 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 asynkrona I/O och ett asynkront protokoll. Asynkrona I/O innebär att anropstråden inte blockeras medan I/O slutförs. Det är viktigt för prestanda, men är en implementeringsdetaljer när det gäller arkitekturen. Ett asynkront protokoll innebär att avsändaren inte väntar på svar. HTTP är ett synkront protokoll, även om en HTTP-klient kan använda asynkrona I/O när den skickar en begäran.

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

  • Minskad koppling. Avsändaren behöver inte känna till konsumenten.

  • Flera prenumeranter. Med hjälp av en pub/sub-modell kan flera konsumenter prenumerera på att ta emot händelser. Se Händelsedriven arkitekturstil.

  • 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 när som helst. Asynkrona meddelanden kan hantera tillfälliga avbrott. Synkrona API:er kräver å andra sidan att nedströmstjä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 en väntan på synkrona anrop leda till oacceptabla svarstider.

  • Belastningsutjämning. En kö kan fungera som en buffert för att jämna ut 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 på ett effektivt sätt.

  • Koppling med meddelandeinfrastrukturen. Användning av en viss meddelandeinfrastruktur kan orsaka nära koppling till infrastrukturen. Det blir svårt att växla till en annan meddelandeinfrastruktur senare.

  • Svarstid . Svarstiden från slutet till slut 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. Hantering av asynkrona meddelanden är inte en trivial uppgift. Du måste till exempel hantera duplicerade meddelanden, antingen genom att dedlicera eller genom att göra åtgärderna idempotenta. Det är också svårt att implementera semantik för begäran-svar med hjälp av asynkrona meddelanden. Om du vill skicka ett svar behöver du en annan kö, plus ett sätt att korrelera begäran- och svarsmeddelanden.

  • Dataflöde. Om meddelanden kräver kösemantikkan kön bli en flaskhals i systemet. Varje meddelande kräver minst en köåtgärd och en åtgärd för att ta bort kön. Dessutom kräver kösemantik vanligtvis någon typ av låsning i meddelandeinfrastrukturen. Om kön är en hanterad tjänst kan det uppstå ytterligare fördröjningar eftersom kön är extern till klustrets virtuella nätverk. Du kan åtgärda dessa problem genom att batchbetala 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

Med dessa överväganden i åtanke gjorde utvecklingsteamet följande designval för drönarleveransprogrammet

  • Inmatningstjänsten exponerar en offentlig 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 är nödvändiga för att implementera den belastningsutjämning som krävs för inmatning.

  • Alla tjänster för konto, leverans, paket, drönare och transporttjänster från tredje part exponerar interna REST-API:er. Scheduler-tjänsten anropar dessa API:er för att utföra en användarbegäran. En anledning att använda synkrona API:er är att Scheduler behöver 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 den fördröjning som introduceras genom att anropa backend-tjänsterna.

  • Om en underordnad tjänst har ett icke-överande fel ska 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-gatewaydiskuterar 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.

  • När en drönare är i drift 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 statusen 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 tjänst för realtidsanalys. Och eftersom Scheduler inte behöver vänta på ett svar påverkar inte tillägg av fler prenumeranter den huvudsakliga arbetsflödesvägen.

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. Det här är ett exempel på att tänka i termer av domänmodeller. Som vi beskrev tidigare tillhör Drönarhantering 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ärsenhet.

Använda ett tjänstnät

Ett tjänstnät är ett programvarulager som hanterar tjänst-till-tjänst-kommunikation. Tjänstnä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 från mikrotjänster själva och till ett delat lager. Tjänstnätet fungerar som en proxy som fångar upp nätverkskommunikationen mellan mikrotjänster i klustret. För närvarande gäller begreppet tjänstnät främst containerorkestrerare i stället för serverlösa arkitekturer.

Anteckning

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

För tillfället är huvudalternativen för ett tjänstnät i Kubernetes linkerd och Istio. Båda dessa tekniker utvecklas snabbt. Vissa funktioner som både linkerd och Istio har är dock:

  • Belastningsutjämning på sessionsnivå, baserat på observerade svarstider eller antalet utestående begäranden. Detta kan förbättra prestandan över 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 med misslyckade begäranden. Ett tjänstnät förstår HTTP-felkoder och kan automatiskt försöka utföra misslyckade begäranden igen. Du kan konfigurera det maximala antalet återförsök, tillsammans med en tidsgräns för att begränsa den maximala svarstiden.

  • Kretsbrytning. Om en instans konsekvent misslyckas med begäranden markerar tjänstnätet det tillfälligt som otillgängligt. Efter en backoff-period försöker den instansen igen. Du kan konfigurera kretsbrytaren baserat på olika kriterier, till exempel antalet på varandra följande fel,

  • Service Mesh samlar in mått om anrop mellan tjänster, till exempel volym för begäran, svarstid, fel och lyckade anrop samt svarsstorlekar. Tjänstnätet 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 tjänstnät? Det beror på. Utan ett tjänstnät måste du ta hänsyn till 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 ett tjänstnät, men ett tjänstnät flyttar dessa problem från de enskilda tjänsterna och till ett dedikerat lager. Å andra sidan lägger ett tjänstnät till komplexitet i klustrets konfiguration och konfiguration. Prestanda kan påverkas, eftersom begäranden nu dirigeras via tjänstnätsproxyn och eftersom extra tjänster nu körs på varje nod i klustret. Du bör göra omfattande prestanda- och belastningstestning innan du distribuerar ett tjänstnät i produktion.

Distribuerade transaktioner

En vanlig utmaning i mikrotjänster är korrekt hantering av transaktioner som sträcker sig över flera tjänster. I det här scenariot är ofta framgången för en transaktion allt eller inget – om någon av de deltagande tjänsterna misslyckas måste hela transaktionen misslyckas.

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

  • En tjänst kan uppleva ett tillfälligt fel, till exempel en tidsgräns för nätverket. De här felen kan ofta lösas genom att försöka igen. Om åtgärden fortfarande misslyckas efter ett visst antal försök betraktas den som ett icke-översatt fel.

  • Ett icke-överande fel är ett fel som sannolikt inte försvinner på egen hand. Icke-transaktionsmässiga fel omfattar normala feltillstånd, till exempel ogiltiga indata. De omfattar även 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 lyckats.

Efter ett icke-transientt 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 har lyckats med hjälp av en kompenserande transaktion. I vissa fall måste detta göras av ett externt system eller till och med av en manuell process.

Om logiken för kompenserande transaktioner är komplex bör 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 övervakaren, 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 mönstret för Scheduler-agentövervakaren. Övervakartjänsten kan även vidta andra åtgärder, till exempel meddela användaren via sms eller e-post eller skicka en avisering till en instrumentpanel för åtgärder.

Diagram som visar övervakarens mikrotjänst

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

En metod är att spara en kontrollpunkt i ett beständigt 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. Men att skriva kontrollpunkter kan skapa prestandakostnader.

Ett annat alternativ är att utforma alla åtgärder så att de är idempotenta. En åtgärd är idempotent om den kan anropas flera gånger utan att ytterligare sidoeffekter produceras 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 idempotenta metoder. Mer information finns i Idempotenta åtgärder.

Nästa steg

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