Mönster för beteende

Låt varje komponent i systemet delta i beslutsprocessen om arbetsflödet för en affärstransaktion, i stället för att förlita sig på en central kontrollpunkt.

Kontext och problem

I arkitekturen för mikrotjänster är det ofta så att ett molnbaserat program är indelat i flera små tjänster som fungerar tillsammans för att bearbeta en affärstransaktion från end-to-end. För att minska kopplingen mellan tjänster ansvarar varje tjänst för en enda affärsåtgärd. Några fördelar är snabbare utveckling, mindre kodbas och skalbarhet. Att designa ett effektivt och skalbart arbetsflöde är dock en utmaning och kräver ofta komplex kommunikation mellan tjänster.

Tjänsterna kommunicerar med varandra med hjälp av väldefinierade API:er. Även en enda affärsåtgärd kan resultera i flera punkt-till-punkt-anrop mellan alla tjänster. Ett vanligt kommunikationsmönster är att använda en centraliserad tjänst som fungerar som initierare. Den bekräftar alla inkommande begäranden och delegerar åtgärder till respektive tjänster. När du gör det hanteras även arbetsflödet för hela affärstransaktionen. Varje tjänst slutför bara en åtgärd och är inte medveten om det övergripande arbetsflödet.

Orkestreringsmönstret minskar punkt-till-punkt-kommunikationen mellan tjänster men har vissa nackdelar på grund av den nära kopplingen mellan orkestreraren och andra tjänster som deltar i bearbetningen av affärstransaktionen. För att kunna köra uppgifter i en sekvens måste orkestreraren ha viss domänkunskap om dessa tjänsters ansvarsområden. Om du vill lägga till eller ta bort tjänster bryts befintlig logik och du måste omkoda delar av kommunikationsvägen. Du kan konfigurera arbetsflödet, lägga till eller ta bort tjänster enkelt med en väl utformad initierare, men en sådan implementering är komplex och svår att underhålla.

Bearbeta en begäran med hjälp av en central initierare

Lösning

Låt varje tjänst bestämma när och hur en affärsåtgärd bearbetas i stället för att vara beroende av en central initierare.

Ett sätt att implementera en bra implementering är att använda det asynkrona meddelandemönstret för att samordna verksamheten.

Bearbeta en begäran med hjälp av en förfrågningsman

En klientbegäran publicerar meddelanden till en meddelandekö. När meddelanden tas emot skickas de till prenumeranter eller tjänster som är intresserade av meddelandet. Varje prenumererande tjänst gör sin åtgärd enligt meddelandet och svarar på meddelandekön med åtgärden lyckad eller misslyckad. Om det lyckas kan tjänsten skicka tillbaka ett meddelande till samma kö eller en annan meddelandekö så att en annan tjänst kan fortsätta arbetsflödet om det behövs. Om en åtgärd misslyckas kan meddelandebussen försöka utföra åtgärden igen.

På så sätt kan tjänsterna samordna arbetsflödet sinsemellan utan att vara beroende av en orkestrerare eller ha direkt kommunikation mellan dem.

Eftersom det inte finns någon punkt-till-punkt-kommunikation hjälper det här mönstret till att minska kopplingen mellan tjänster. Dessutom kan den ta bort prestandaflaskhalsen som orsakas av initieraren när den måste hantera alla transaktioner.

När du ska använda det här mönstret

Använd det återkommande mönstret om du förväntar dig att uppdatera, ta bort eller lägga till nya tjänster ofta. Hela appen kan ändras med mindre arbete och minimalt avbrott i befintliga tjänster.

Överväg det här mönstret om du upplever prestandaflaskhalsar i den centrala initieraren.

Det här mönstret är en naturlig modell för den serverlösa arkitekturen där alla tjänster kan vara kortlivade eller händelsedrivna. Tjänster kan tas bort på grund av en händelse, utföra sin uppgift och tas bort när uppgiften har slutförts.

Problem och överväganden

Decentralisering av orchestrator kan orsaka problem vid hantering av arbetsflödet.

Om en tjänst inte kan slutföra en affärsåtgärd kan det vara svårt att återställa från det felet. Ett sätt är att få tjänsten att indikera fel genom att en händelse utlyssas. En annan tjänst prenumererar på dessa misslyckade händelser vidtar nödvändiga åtgärder, till exempel att tillämpa kompenserande transaktioner för att ångra lyckade åtgärder i en begäran. Den misslyckade tjänsten kan också misslyckas med att utlysa en händelse för felet. I så fall bör du överväga att använda ett nytt försök och, eller en time out-mekanism, för att identifiera åtgärden som ett fel. Ett exempel finns i avsnittet Exempel.

Det är enkelt att implementera ett arbetsflöde när du vill bearbeta oberoende affärsåtgärder parallellt. Du kan använda en enda meddelandebuss. Arbetsflödet kan dock bli komplicerat när det är svårt att göra något i en sekvens. Tjänst C kan till exempel bara starta åtgärden efter att tjänst A och tjänst B har slutfört sina åtgärder. En metod är att ha flera meddelandebussar som hämtar meddelanden i den ordning som krävs. Mer information finns i avsnittet Exempel.

Det lättade mönstret blir en utmaning om antalet tjänster växer snabbt. Med tanke på det stora antalet oberoende rörliga delar tenderar arbetsflödet mellan tjänster att bli komplext. Distribuerad spårning blir också svårt.

Orkestratorn hanterar arbetsflödets återhämtning centralt och kan bli en enskild felpunkt. Å andra sidan distribueras rollen mellan alla tjänster för att inte bli lika robust.

Varje tjänst ansvarar inte bara för driftens återhämtning utan även för arbetsflödet. Det här ansvaret kan vara besvärligt för tjänsten och svårt att implementera. Varje tjänst måste försöka igen vid tillfälliga, icke-övergående och timeout-fel, så att begäran avslutas på ett smidigt sätt, om det behövs. Dessutom måste tjänsten vara noggrann med att kommunicera om åtgärden lyckas eller misslyckas så att andra tjänster kan agera därefter.

Exempel

Det här exemplet visar det första mönstret med drone delivery-appen. När en klient begär upphämtning tilldelar appen en drönare och meddelar klienten.

GitHub finns ett exempel på det här mönstret på GitHub.

Närbilden av en mappningsbeskrivning genereras automatiskt

En enda klientaffärstransaktion kräver tre olika affärsåtgärder: skapa eller uppdatera ett paket, tilldela en drönare för att leverera paketet och kontrollera leveransstatusen. Dessa åtgärder utförs av tre mikrotjänster: Paket, Drone Scheduler och leveranstjänster. I stället för en central initierare använder tjänsterna meddelanden för att samarbeta och samordna begäran sinsemellan.

Design

Affärstransaktionen bearbetas i en sekvens via flera hopp. Varje hopp har en meddelandebuss och respektive affärstjänst.

När en klient skickar en leveransbegäran via en HTTP-slutpunkt tar inmatningstjänsten emot den, höja en åtgärdshändelse och skickar den till en meddelandebuss. Buss anropar den prenumererade affärstjänsten och skickar händelsen i en POST-begäran. När affärstjänsten tar emot händelsen kan den slutföra åtgärden med lyckat, misslyckat eller så kan begäran ta för lång tid. Om åtgärden lyckas svarar tjänsten på buss med statuskoden OK, höjer en ny åtgärdshändelse och skickar den till meddelandebussen för nästa hopp. Vid ett fel eller en time out rapporterar tjänsten fel genom att skicka BadRequest-koden till meddelandebussen som skickade den ursprungliga POST-begäran. Meddelandebussen försöker utföra åtgärden igen baserat på en återförsöksprincip. När perioden har löpt ut flaggar meddelandebussen den misslyckade åtgärden och ytterligare bearbetning av hela begäran stoppas.

Det här arbetsflödet fortsätter tills hela begäran har bearbetats.

Designen använder flera meddelandebussar för att bearbeta hela affärstransaktionen. Microsoft Azure Event Grid tillhandahåller meddelandetjänsten. Appen distribueras i ett Azure Kubernetes Service (AKS)-kluster med två containrar i samma podd. En container kör ambassadören som interagerar med Event Grid medan den andra kör en affärstjänst. Metoden med två containrar i samma podd förbättrar prestanda och skalbarhet. Ambassadören och affärstjänsten delar samma nätverk, vilket ger korta svarstider och högt dataflöde.

För att undvika sammanhängande återförsöksåtgärder som kan leda till flera försök kan Event Grid bara att försöka utföra en åtgärd på nytt i stället för affärstjänsten. Den flaggar en misslyckad begäran genom att skicka ett meddelande till en kö för dead letter (DLQ).

Affärstjänsterna är idempotenta för att se till att återförsöksåtgärder inte resulterar i dubblettresurser. Pakettjänsten använder till exempel upsert-åtgärder för att lägga till data i datalagret.

Exemplet implementerar en anpassad lösning för att korrelera anrop mellan alla tjänster Event Grid hopp.

Här är ett kodexempel som visar det oönstrade mönstret mellan alla företagstjänster. Den visar arbetsflödet för apptransaktionerna för Drönarleverans. Kod för undantagshantering och loggning har tagits bort av utrymmessysken.

[HttpPost]
[Route("/api/[controller]/operation")]
[ProducesResponseType(typeof(void), 200)]
[ProducesResponseType(typeof(void), 400)]
[ProducesResponseType(typeof(void), 500)]

public async Task<IActionResult> Post([FromBody] EventGridEvent[] events)
{

   if (events == null)
   {
       return BadRequest("No Event for Choreography");
   }

   foreach(var e in events)
   {

        List<EventGridEvent> listEvents = new List<EventGridEvent>();
        e.Topic = eventRepository.GetTopic();
        e.EventTime = DateTime.Now;
        switch (e.EventType)
        {
            case Operations.ChoreographyOperation.ScheduleDelivery:
            {
                var packageGen = await packageServiceCaller.UpsertPackageAsync(delivery.PackageInfo).ConfigureAwait(false);
                if (packageGen is null)
                {
                    //BadRequest allows the event to be reprocessed by Event Grid
                    return BadRequest("Package creation failed.");
                }

                //we set the event type to the next choreography step
                e.EventType = Operations.ChoreographyOperation.CreatePackage;
                listEvents.Add(e);
                await eventRepository.SendEventAsync(listEvents);
                return Ok("Created Package Completed");
            }
            case Operations.ChoreographyOperation.CreatePackage:
            {
                var droneId = await droneSchedulerServiceCaller.GetDroneIdAsync(delivery).ConfigureAwait(false);
                if (droneId is null)
                {
                    //BadRequest allows the event to be reprocessed by Event Grid
                    return BadRequest("could not get a drone id");
                }
                e.Subject = droneId;
                e.EventType = Operations.ChoreographyOperation.GetDrone;
                listEvents.Add(e);
                await eventRepository.SendEventAsync(listEvents);
                return Ok("Drone Completed");
            }
            case Operations.ChoreographyOperation.GetDrone:
            {
                var deliverySchedule = await deliveryServiceCaller.ScheduleDeliveryAsync(delivery, e.Subject);
                return Ok("Delivery Completed");
            }
            return BadRequest();
    }
}

Överväg de här mönstren i din design för att vara bra.