Mönstret för idempotenta konsumenter

Utforma meddelandekonsumenter så att bearbetning av samma meddelande mer än en gång har samma effekt som bearbetningen en gång. Meddelandesystem som garanterar leverans minst en gång kan leverera samma meddelande flera gånger. Utan motståndskraft mot dubbletter kan ombearbetning av ett meddelande skapa duplicerade poster, dubbeldebitera en kund eller ha andra oönskade effekter.

Kontext och problem

Distribuerade program utbyter ofta arbete via en meddelandekö i stället för direkta synkrona anrop. De flesta mäklare, inklusive Azure Service Bus, Azure Event Hubs, Apache Kafka och RabbitMQ, tillhandahåller leverans minst en gång. Den här garantin säkerställer att ett meddelande når en konsument även när fel inträffar, men det innebär också att mäklaren kan leverera samma meddelande mer än en gång.

Dubbletter uppstår från flera källor:

  • Producentens återförsök

    En producent skickar ett meddelande, får ingen bekräftelse på grund av ett tillfälligt nätverksfel eller tidsgräns och skickar meddelandet igen. Meddelandeförmedlaren har nu två kopior trots att sändningen lyckades första gången.

  • Ny leverans efter utebliven bekräftelse

    En konsument tar emot och bearbetar ett meddelande men misslyckas med att bekräfta det eftersom konsumenten kraschar, låset upphör att gälla eller bekräftelsen går förlorad. Meddelandeförmedlaren antar att meddelandet inte har bearbetats och levererar det igen.

  • Konsumentfel i mitten av bearbetningen

    En konsument slutför en databasskrivning men kraschar innan den bekräftar meddelandet. När en annan instans hämtar det återlevererade meddelandet upprepas skrivningen.

Leverans exakt en gång i ett distribuerat system är opraktiskt att garantera. Även meddelandeförmedlare som hävdar exactly-once-semantik garanterar endast de operationer som de direkt kontrollerar, till exempel att leverera meddelanden till konsumenter eller skriva tillbaka data till meddelandeförmedlaren. De kan inte garantera de externa bieffekter som konsumenter orsakar i andra system. Den hållbara lösningen är inte att eliminera duplicerad leverans. Det är för att få konsumenten att tolerera det. När du kombinerar leverans minst en gång med en konsument som ignorerar dubbletter uppnår du i praktiken exakt en gång-bearbetning.

Lösning

Gör konsumenten idempotent genom att föra ett register över bearbetade meddelanden och hoppa över alla meddelanden som den har sett tidigare. Konsumenten baserar detta beslut på en stabil identifierare som består även vid återsändning, kontrollerar en beständig lagring för att avgöra om identifieraren redan har behandlats och bearbetar antingen meddelandet eller kasserar det som en dubblett.

Följande steg beskriver kärnflödet:

  1. Läs meddelandet och extrahera dess dedupliceringsnyckel.
  2. Kontrollera dedupliceringslagret för den nyckeln.
  3. Om nyckeln finns behandlar du meddelandet som en dubblett. Bekräfta det och stoppa, och returnera eventuellt det tidigare registrerade utfallet.
  4. Om nyckeln inte finns bearbetar du meddelandet och registrerar nyckeln i en enda atomisk åtgärd och bekräftar sedan meddelandet.

Välj en stabil dedupliceringsnyckel

Nyckeln måste unikt och konsekvent identifiera det logiska meddelandet i varje omleverans. Använd en producenttilldelad meddelandeidentifierare eller en idempotensnyckel på affärsnivå som identifierar den specifika logiska åtgärden, inte en kontext för delad korrelation som flera meddelanden kan innehålla. I Azure Service Bus fyller egenskapen MessageId detta syfte eftersom den unikt identifierar meddelandet och dess nyttolast. Använd CorrelationId inte som nyckel eftersom den grupperar relaterade meddelanden, till exempel en begäran och dess svar. För händelser som följer CloudEvents-specifikationen identifierar kombinationen av attributen source och id en händelse unikt och förblir stabil mellan omleveranserna.

Basa dig inte på identifierare på transportnivå som förmedlaren återskapar vid omleverans eller på värden som härleds från leveransförsök, eftersom dessa värden ändras mellan dubblettmeddelanden och omöjliggör upptäckt av dubbletter. Undvik också att härleda nyckeln från flyktiga fält, till exempel mottagningstidsstämplar.

När fler än en oberoende konsument bearbetar samma kanal, till exempel flera prenumeranter i en publiceringsprenumerationsdesign, bearbetar varje konsument legitimt sin egen kopia av ett meddelande och måste oberoende spåra slutförandet av meddelandebearbetningen. Om dessa konsumenter delar ett dedupliceringslager ska du basera postens nyckel på en kombination av konsumentidentiteten och meddelandeidentiteten. En lagring som enbart baseras på meddelandeidentiteten gör att den första konsumenten kan undertrycka bearbetningen för alla andra.

Bestämma var bearbetade nycklar ska lagras

Du har två vanliga alternativ:

  • En dedikerad tabell för deduplicering. Konsumenten har en separat tabell, ibland kallad inkorg, som innehåller en rad per bearbetad nyckel. Den här metoden håller dedupliceringsproblem åtskilda från affärsdata och fungerar bra när många meddelandetyper delar en mekanism.

  • Själva affärsentiteten. Konsumenten lagrar nyckeln på posten som meddelandet skapar eller uppdaterar. Den här metoden undviker en separat tabell men kopplar deduplicering till formen på affärsdata.

Checka in markören och bieffekterna atomiskt

Flödet kontrollera-sedan-bearbeta har ett tidsfönster där fel kan uppstå. Om konsumenten bearbetar meddelandet och sedan registrerar nyckeln i ett separat steg, innebär ett avbrott mellan de två åtgärderna att bieffekterna redan har utförts medan nyckeln fortfarande är oregistrerad, så meddelandet bearbetas igen vid nästa leverans.

Åtgärda det här felfönstret genom att skriva dedupliceringsmarkören och effekterna på affärssidan i samma transaktion. När båda genomförs tillsammans eller inte alls hittar en omleverans antingen markören och hoppar över den, eller också hittar den ingen markör eftersom transaktionen har rullats tillbaka och då kan bearbetas om på ett säkert sätt. Den här transaktionsvarianten är inkorgsmönstret, och det är motsvarigheten på konsumentsidan till mönstret Transactional Outbox på producentsidan.

Skydda mot samtidiga dubbletter

Under leverans minst en gång med flera konkurrerande konsumenter kan två instanser ta emot kopior av samma meddelande samtidigt. Båda kan klara existenskontrollen innan någon av incheckningarna, så enbart kontrollen förhindrar inte dubbel bearbetning.

Framtvinga korrekthet i datalagret i stället för i programlogik:

  • Använd en unik begränsning för dedupliceringsnyckeln. Båda transaktionerna försöker infoga nyckeln, men bara en lyckas. Den andra misslyckas med villkoret och behandlar meddelandet som en dubblett. Den här metoden gör databasen till den enda skiljedomaren för loppet.

  • Undvik kontrollera-och-sätt-kapplöpningar i cachar. Ett mönster som först kontrollerar en nyckel och sedan sätter den i två separata operationer innebär ett tidsfönster där samtidiga återförsök båda kan lägga beslag på nyckeln. Använd en atomisk villkorsstyrd skrivning, till exempel en infogning som misslyckas vid konflikt eller en set-if-absent-åtgärd, så att anspråk på nyckeln är ett enda atomiskt steg.

Hantera sidoeffekter som inte kan ingå i transaktionen

Vissa processer kan inte ingå i konsumentens databastransaktion, till exempel att anropa ett tredjeparts-API eller skriva till en extern lagringsplats. För dessa processer använder du en tvåfasmetod:

  1. Registrera nyckeln med statusen pågående innan du utför den externa åtgärden.
  2. Utför processen.
  3. Uppdatera posten till slutförd och lagra utfallet.

Vid omleverans kan en slutförd post göra att du kan hoppa över att göra om anropet. En pågående post indikerar att ett tidigare försök kan ha slutförts delvis eller bearbetas av en annan konsument.

Problem och överväganden

Tänk på följande när du bestämmer hur du ska implementera det här mönstret:

  • Föredra naturligt idempotenta operationer. Vissa åtgärder är i sig idempotenta och behöver ingen administration för deduplicering. En upsert med en verksamhetsidentifierare som nyckel, en skrivåtgärd som anger ett absolut värde i stället för en ökning, eller en HTTP PUT till en resursidentifierare ger samma resultat oavsett om den körs en eller flera gånger.

    Ibland kan du göra en åtgärd naturligt idempotent genom händelseburen tillståndsöverföring, där meddelandet innehåller det resulterande absoluta tillståndet, till exempel en beställnings nya status, så att konsumenten tillämpar det som en upsert i stället för en relativ ändring.

    Tips/Råd

    Utgå från naturlig idempotens i första hand, och lägg bara till dedupliceringstekniker för operationer som inte kan göras naturligt idempotenta.

  • Hantera livscykeln för dedupliceringsposter. Dedupliceringsposter ackumuleras om du inte låter dem upphöra att gälla. Behåll varje post åtminstone så länge som meddelandeförmedlaren kan leverera det ursprungliga meddelandet på nytt. Detta intervall beror på meddelandeförmedlarens maximala antal leveransförsök, dess tidsgräns för lås eller synlighet samt meddelandets livstid. Ange ett TTL-värde för dedupliceringsposter som är längre än det här fönstret så att en sen återleverans fortfarande hittar sin markör. Att ta bort poster för tidigt öppnar upp för dubbletter igen. Ta hänsyn till meddelanden som en operatör återsänder från en dead-letter-kö, eftersom en omsändning kan ske långt efter fönstret för normal återleverans.

  • Använd ett meddelanderamverk i stället för handrullande deduplicering. Att implementera dedupliceringslagret, den atomiska commiten och rensning av poster korrekt är felbenäget. Meddelandebaserade ramverk tillhandahåller det här mönstret som en inbyggd funktion.

    Till exempel deduplicerar NServiceBus inkommande meddelanden med deras meddelandeidentifierare och ger konfigurerbar kvarhållning och rensning för dedupliceringsdata. MassTransits konsumentinkorg spårar mottagna meddelanden utifrån deras meddelande-ID för att säkerställa att varje meddelande bara behandlas en gång.

  • Broker-deduplicering minskar men tar inte bort behovet av idempotent konsumentlogik. Vissa plattformar filtrerar dubbletter på transportlagret. Azure Service Bus dubblettidentifiering tar bort meddelanden som upprepas MessageId inom ett konfigurerat tidsfönster, vilket undertrycker dubbletter som orsakas av att producenten skickar återförsök. Den här funktionen fungerar på sändningssidan och i ett avgränsat fönster. Det hindrar inte en konsument från att bearbeta samma meddelande två gånger efter en omleverans, så du behöver fortfarande idempotent konsumentlogik. Behandla plattformsfunktioner som ett första lager av försvar som minskar mängden dubbletter, inte som en ersättning för mönstret.

  • Konto för meddelandebeställning. Deduplicering tar bort dubbletter men garanterar inte ordningen. Om konsumenten är beroende av bearbetningsordning kombinerar du det här mönstret med en ordningsmekanism, till exempel Azure Service Bus meddelandesessioner, eller inkluderar sekvens- eller versionsdata som gör att konsumenten kan avvisa inaktuella meddelanden.

  • Instrument för observerbarhet. Generera dedupliceringsnyckeln och en korrelationsidentifierare i strukturerade loggar och spåra ett mått för identifierade dubbletter. En stigande dubblettfrekvens kan tyda på felkonfiguration hos producenten, ett för litet bekräftelse- eller låsfönster eller konsumenter som inte fungerar korrekt. Använd spårning och korrelation från slutpunkt till slutpunkt för att följa ett meddelande mellan tjänster.

  • Vidareför idempotens till efterföljande anrop. Att göra en konsument idempotent skyddar inte de tjänster den anropar. När en konsument anropar underordnade tjänster som en del av bearbetningen sprider du idempotensnyckeln så att varje nivå kan deduplicera sitt eget arbete.

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

Använd det här mönstret i sådana här scenarier:

  • Du konsumerar meddelanden från en meddelandeförmedlare som tillhandahåller leveransgaranti om minst en gång, vilket är standard för de flesta meddelandeförmedlare.

  • Ombearbetning av ett meddelande ger felaktiga resultat, till exempel duplicerade ekonomiska transaktioner, dubbletter av resursskapande eller upprepade meddelanden.

  • Flera konkurrerande konsumenter bearbetar samma kanal, vilket gör samtidig duplicerad leverans sannolik.

Det här mönstret kanske inte är lämpligt när:

  • Varje åtgärd som konsumenten utför är redan naturligt idempotent, så upparbetning är ofarligt och dedupliceringsbokföring lägger till kostnader utan förmån.

  • Arbetslasten kan tolerera effekterna av enstaka dubbelbearbetning, och kostnaden för en dedupliceringsdatabas är större än konsekvenserna av en dubblett.

Idempotent behandling utöver meddelandehantering

Det här mönstret tillämpar idempotens på meddelandekonsumenter, men idempotent bearbetning är en mer övergripande princip för tillförlitlighet. Alla operationer som kan köras mer än en gång på en identisk uppgift drar nytta av detta. Den här principen omfattar ETL-transformeringar (extract, transform, load) som bearbetar omspelade data, dataströmbearbetning som återupptas från en kontrollpunkt, schemalagda jobb som överlappar eller startar om samt webhook- eller HTTP-slutpunkter som tar emot dubbletter av leveranser.

I varje enskilt fall gäller samma kärnteknik:

  1. Identifiera arbetsenheten med en stabil nyckel.
  2. Registrera det du redan har bearbetat.
  3. Hoppa över eller absorbera dubbletter så att upprepande av arbetet inte ändrar resultatet.

Mekanismerna i den här artikeln, såsom stabila nycklar, atomiska markörer och unika begränsningar, kan överföras till dessa sammanhang även när ingen meddelandeförmedlare är inblandad.

Design av arbetsbelastning

Utvärdera hur Idempotent Consumer-mönstret kan användas i utformningen av en arbetsbelastning för att uppfylla de mål och principer som beskrivs i pelarna i Azure Well-Architected Framework. Följande tabell innehåller vägledning om hur det här mönstret stöder målen för varje pelare.

Grundpelare Så här stöder det här mönstret pelarmål
Tillförlitlighets designbeslut hjälper din arbetsbelastning att bli motståndskraftig mot funktionsfel och säkerställer att den återställer sig till ett fullständigt fungerande tillstånd när ett fel uppstår. Med det här mönstret kan en arbetsbelastning använda leverans minst en gång och säkra återförsök utan att skada data, vilket omvandlar duplicerad leverans från en korrekthetsrisk till ett tolererat villkor.

- RE:07 Självbevarande
- Hantera tillfälliga fel

Om detta mönster inför kompromisser inom en pelare bör du överväga dem mot målen för de andra pelarna.

Example

I följande exempel visas en idempotent konsument som bearbetar beställningar från Azure Service Bus och bevarar tillståndet i Azure Cosmos DB för NoSQL.

En producent anger Service Bus MessageId som en orderidentifierare på affärsnivå. Mottagaren tar emot meddelanden i PeekLock-läge, vilket gör att ett meddelande levereras på nytt om mottagaren inte slutför det inom låstiden. Konsumentens Azure Cosmos DB-container partitioneras utifrån orderidentifieraren (/orderId) och dokumentets id sätts till samma orderidentifierare, så att varje kopia av en viss order hamnar i samma logiska partition och själva orderposten fungerar som dedupliceringsmarkör.

Konsumenten bearbetar varje meddelande enligt följande:

  1. Läs meddelandet och använd det MessageId som dedupliceringsnyckel.
  2. Skapa orderdokumentet med både id och partitionsnyckeln inställda på orderidentifieraren.
  3. Om skapandet lyckas slutför du meddelandet så att Service Bus tar bort det från kön.
  4. Om det inte går att skapa med statusen HTTP 409 (konflikt) eftersom ett dokument med det id redan finns läser du det befintliga dokumentet och jämför det med det aktuella meddelandet. Om en lagrad begäranshash eller oföränderliga affärsfält matchar behandlar du meddelandet som en dubblett, slutför det och hoppar över bearbetningen. Om de inte matchar kan producenten ha återanvändt identifieraren för annat innehåll, eller så kan beställningsinformationen ha ändrats sedan den först bearbetades. Därför kan det hända att meddelandet skickas i obevakat läge eller att en avisering genereras i stället för att den ignoreras i tysthet.
  5. Om bearbetningen misslyckas av en tillfällig anledning, frisläpp meddelandet så att Service Bus levererar det på nytt, eller låt låset löpa ut så att en annan konsument tar emot det.

Åtgärden create är atomisk, så den fungerar både som dedupliceringskontroll och skrivning. Två konsumenter som tar emot kopior av samma meddelande kan inte båda skapa ordern. Den ena skapande vinner och den andra returnerar en konflikt och tar bort dess dubblett på ett säkert sätt.

När bearbetningen måste skriva mer än ett dokument använder du en transaktionsbatch som innehåller både dedupliceringsdokumentet och affärsdokumenten i samma partitionsnyckel. Eftersom en transaktionsbatch körs inom en enda logisk partition, välj en partitionsnyckel som alla dokument för ett meddelande delar. Batchen genomför antingen alla dokument tillsammans eller inget av dem, så en krasch mellan bearbetning och bekräftelse kan inte leda till att dedupliceringsmarkören och affärsdata inte stämmer överens. En batch som försöker skapa ett dokument som redan finns returnerar statuskoden 409 (Conflict), vilket identifierar dubbletten.

Om du även vill göra den här konsumenten motståndskraftig mot dubbla sändningsförsök kan du aktivera dubblettidentifiering på kön. Dubblettdetektering förhindrar upprepade sändningar inom sitt historikfönster, och den idempotenta konsumenten hanterar eventuella dubbletter som hamnar utanför det fönstret eller som uppstår vid återleverans.

Nästa steg