Mönstret för idempotenta konsumenter

Utforma meddelandekonsumenter så att bearbetning av samma meddelande mer än en gång har samma effekter som bearbetning av det en gång. Meddelandesystem som garanterar leverans minst en gång kan leverera samma meddelande flera gånger. Motståndskraft mot dubbletter säkerställer att ombearbetning av ett meddelande inte skapar duplicerade poster, dubbeldebiterar en kund eller har andra oönskade effekter.

Kontext och problem

Distribuerade program utbyter ofta arbete via en meddelandekö i stället för att använda 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 innebär också att mäklaren kan leverera samma meddelande mer än en gång.

Det är opraktiskt att garantera leverans exakt en gång i ett distribuerat system. Även mäklare som använder exakt en gång-semantik kan bara garantera åtgärder som de direkt kontrollerar, till exempel att leverera meddelanden till konsumenter eller skriva data tillbaka till mäklaren. De kan inte kontrollera de biverkningar som konsumenterna implementerar i externa system. Den hållbara lösningen är inte att eliminera duplicerad leverans utan att göra så att konsumenten bearbetar den korrekt. När du kombinerar leverans minst en gång med en konsument som ignorerar dubbletter uppnår du effektiv bearbetning en gång .

Dubbletter kan uppstå 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 under bearbetningen. En konsument slutför en databasskrivning men kraschar innan meddelandet bekräftas. En annan instans hämtar meddelandet och upprepar skrivningen.

Lösning

Skapa en idempotent konsument genom att låta konsumenten föra register över meddelanden som den har bearbetat framgångsrikt och hoppa över alla meddelanden som den redan har bearbetat, baserat på en stabil identifierare som överlever återleverans. Konsumenten kontrollerar lagret för beständiga identifierare för att avgöra om den redan har behandlat identifieraren och behandlar sedan antingen meddelandet eller kasserar det som en dubblett.

Kärnflödet består av följande steg:

  1. Läs meddelandet och extrahera dess dedupliceringsnyckel.
  2. Kontrollera dedupliceringslagret för den nyckeln.
  3. Om nyckeln redan finns i lagringen behandlas meddelandet som dubblett. Bekräfta meddelandet och sluta bearbeta det, om du vill returnera det tidigare registrerade resultatet.
  4. Om nyckeln ännu inte finns i lagringen, bearbeta meddelandet och registrera nyckeln i en enda atomisk operation, och kvittera sedan meddelandet.

Följande avsnitt ger vägledning om hur du gör konsumenter idempotenta:

Välj en stabil dedupliceringsnyckel

Dedupliceringsnyckeln måste unikt och konsekvent identifiera det logiska meddelandet för varje omleverans. Använd antingen 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.

Ange till exempel egenskapen Service Bus MessageId till ett värde som unikt identifierar det logiska meddelandet. Använd CorrelationId inte som nyckel eftersom den refererar till grupper av relaterade meddelanden, till exempel en begäran och dess svar. För händelser som följer CloudEvents-specifikationen identifierar attributkombinationen source och id unikt en händelse och förblir stabil mellan omleveranserna.

Basera dig inte på identifierare på transportnivå som brokern återskapar vid omleverans, eller på värden som härleds från leveransförsök. Dessa värden ändras mellan leveranser och försvårar identifiering 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, får varje konsument sin egen kopia av ett meddelande och måste oberoende spåra slutförandet av bearbetningen. Om konsumenterna delar ett dedupliceringslager ska du nyckla posterna med en sammansatt nyckel baserad på konsumentens identitet och meddelandets identitet. En lagring som baseras enbart på meddelandeidentiteten skulle göra det möjligt för den första konsumenten att förhindra bearbetning för alla andra.

Bestämma var bearbetade nycklar ska lagras

Följande lagringsalternativ är vanliga för bearbetade nycklar:

  • 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 om många meddelandetyper delar samma 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 affärsdatatypen.

Spara den bearbetade nyckeln och sideffekterna atomärt

Ett kontrollera-sedan-bearbeta-flöde har ett riskfönster för fel. Om en konsument bearbetar ett meddelande och sedan registrerar nyckeln i ett separat steg, kan en krasch mellan de två åtgärderna innebära att effekterna redan har genomförts men att nyckeln inte har registrerats, vilket gör att konsumenten bearbetar meddelandet igen vid nästa omleverans.

Undvik det här felfönstret genom att skriva dedupliceringsmarkören och effekterna på affärssidan i samma transaktion. Om en konsument genomför båda åtgärderna i samma transaktion eller inte alls, hittar den vid omleverans antingen markören och hoppar över meddelandet, eller så hittar den ingen markör eftersom transaktionen inte slutfördes och kan då säkert bearbeta om meddelandet. Den här transaktionsvarianten är Inbox-mönstret och är motsvarigheten på konsumentsidan till producentmönstret transaktionell utkorg.

Skydda mot samtidiga dubbletter

Under leverans minst en gång med samtidiga konkurrerande konsumenter kan två instanser ta emot kopior av samma meddelande samtidigt. Båda instanserna kan passera existenskontrollen innan endera instansen genomför transaktionen, så kontrollen i sig förhindrar inte dubbel bearbetning.

Framtvinga korrekthet i datalagret i stället för i programlogik genom att utföra följande steg:

  • Använd en unikhetsbegränsning för dedupliceringsnyckeln så att två transaktioner kan försöka infoga en nyckel, men bara en kan lyckas. Den andra transaktionen misslyckas med villkoret och behandlar meddelandet som en dubblett. Den här metoden gör databasen till den enda skiljedomaren för konflikten.

  • Undvik kontrollera-och-sätt-kapplöpningar i cachar. Ett mönster som kontrollerar en nyckel och sedan anger den i två separata åtgärder har ett fönster som gör att samtidiga återförsök kan göra anspråk på nyckeln. Använd en atomisk villkorsstyrd skrivning, till exempel en infogning som misslyckas vid konflikt eller en set-if-absent-åtgärd, för att göra anspråk på nyckeln till 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. Använd följande tvåfasmetod för dessa processer:

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

Vid omleveransen uppmanar en slutförd post konsumenten att hoppa över att upprepa anropet. En post med statusen pågående indikerar att ett tidigare försök kan ha delvis slutförts eller bearbetas av en annan konsument. Konsumenten bör stämma av inaktuella poster eller dirigera olösta ärenden för intervention innan de bekräftar omleveransen.

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 ett inkrement, eller en HTTP-begäran PUT till en resursidentifierare ger samma resultat oavsett om de körs en gång eller många gånger.

    Du kan ibland göra en operation naturligt idempotent genom att använda tillståndsöverföring via händelser. Meddelandet innehåller det resulterande absoluta tillståndet, till exempel en orders nya status, så att konsumenten behandlar det som en upsert i stället för som en relativ ändring.

    Tips/Råd

    Eftersträva naturlig idempotens om möjligt, och använd dedupliceringstekniker endast för operationer som inte kan utformas så att de blir naturligt idempotenta.

  • Använd ett meddelanderamverk i stället för att konfigurera deduplicering. Att korrekt implementera lagring för dubbletteliminering, commit och rensning är felbenäget. Meddelandebaserade ramverk tillhandahåller det här mönstret som en inbyggd funktion.

    Till exempel deduplicerar NServiceBus inkommande meddelanden med sina meddelandeidentifierare och ger konfigurerbar kvarhållning och rensning för dedupliceringsdata. MassTransit Consumer Outbox spårar mottagna meddelanden utifrån deras meddelandeidentifierare för att säkerställa exakt-en-gång-beteende för konsumenter.

  • Hantera livscykeln för dedupliceringsposter. Dedupliceringsposter ansamlas om du inte ställer in dem så att de upphör att gälla. Behåll varje post åtminstone så länge som brokern fortfarande kan leverera om det ursprungliga meddelandet. Storleken på det här fönstret beror på meddelandeförmedlarens maximala antal leveransförsök, dess lås- eller synlighetstimeout samt meddelandets livslängd.

    Ange en livslängd för dedupliceringsposter som är längre än det här intervallet, så att en sen återleverans fortfarande kan hitta sin markör. Att ta bort poster för tidigt öppnar upp för dubbletter igen. Ta hänsyn till meddelanden som operatörer skickar om från dead-letter-köer, eftersom dessa omskick kan ske långt efter att det normala fönstret för återleverans har stängts.

  • Ersätt inte idempotent logik hos konsumenten med mäklarens deduplicering. Vissa plattformar filtrerar dubbletter på transportlagret. Till exempel tar Service Bus dubblettidentifiering bort meddelanden som upprepar en MessageId inom ett konfigurerat tidsfönster, vilket förhindrar att duplicerade producent skickar återförsök.

    Den här funktionen fungerar på sändningssidan och i ett begränsat fönster, så det hindrar inte en konsument från att bearbeta samma meddelande två gånger efter en omleverans. Du behöver fortfarande idempotent konsumentlogik. Använd plattformsfunktioner för att minska duplicerade volymer, inte som ersättning för Idempotent Consumer-mönstret.

  • Konto för meddelandebeställning. Deduplicering tar bort dubbletter, men garanterar inte ordningen. Om konsumenten är beroende av bearbetningsordningen bör du kombinera det här mönstret med en ordningsmekanism, till exempel Service Bus meddelandesessioner, eller inkludera sekvens- eller versionsdata så att konsumenten kan avvisa föråldrade 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 distribuerad spårning och korrelation för att följa ett meddelande mellan tjänster.

  • Vidareför idempotens till efterföljande anrop. Att göra meddelandekonsumenten idempotent skyddar inte de tjänster som den anropar. När en konsument anropar underordnade tjänster som en del av bearbetningen sprider du idempotensnyckeln så att varje tjänstnivå 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.

  • Att bearbeta om ett meddelande kan ge upphov till felaktiga resultat, till exempel dubbla finansiella transaktioner, att resurser skapas dubbelt eller upprepade aviseringar.

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

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

  • De åtgärder som konsumenten utför är redan naturligt idempotenta, så ombearbetning är ofarlig och administration för deduplicering medför kostnader utan nytta.

  • Arbetslasten kan tolerera konsekvenserna av enstaka dubbelbearbetning, och kostnaden för en dedupliceringsdatabas överstiger konsekvenserna av en dubblett.

Idempotent behandling utöver meddelandehantering

Det här mönstret tillämpar idempotens på meddelandekonsumenter, men idempotent behandling är en bredare tillförlitlighetsprincip som kan vara till nytta för alla operationer som körs mer än en gång på samma uppgift. 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.

Samma kärnteknik gäller i varje enskilt fall.

  1. Använd en stabil nyckel för att identifiera arbetsenheten.
  2. Registrera det du bearbetar.
  3. Hoppa över eller absorbera duplicerade körningar så att upprepande av arbetet inte ändrar resultatet.

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

För HTTP-slutpunkter, se HTTP-metodsemantik och idempotens för vägledning om metodspecifikt beteende och dokumentation av säkra återförsök.

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
- 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 beskrivs en idempotent konsument som bearbetar beställningar från Service Bus och bevarar tillståndet i Azure Cosmos DB för NoSQL.

  1. En producent anger Service Bus MessageId som en orderidentifierare på affärsnivå.
  2. Konsumenten får meddelandet i PeekLock-läge , vilket gör meddelandet tillgängligt för omleverans om konsumenten inte kvittar det innan låset upphör att gälla.
  3. Konsumentens Azure Cosmos DB-container är partitionerad efter /orderId order-ID:t och sätter dokumentets id till samma order-ID, så att varje kopia av en viss order mappas till samma logiska partition och själva ordern id fungerar som markör för deduplicering.

Konsumenten utför följande steg för att bearbeta varje meddelande:

  1. Läs meddelandet och använd det MessageId som dedupliceringsnyckel.
  2. Försök att 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.
  5. Om det lagrade hashvärdet för begäran eller de oföränderliga affärsfälten matchar ska du behandla meddelandet som en dubblett, markera det som slutfört och hoppa över vidare bearbetning.
  6. Om den lagrade hashsumman för begäran eller de immutabla affärsfälten inte stämmer överens, skicka meddelandet till en dead-letter-kö och utlös en varning i stället för att tyst kassera meddelandet. Producenten kan ha återanvändt identifieraren för annat innehåll, eller så kan meddelandeinformationen ha ändrats sedan beställningen först bearbetades.
  7. 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 kan ta emot det.

Skapandeåtgärden är atomisk, så den fungerar som både dedupliceringskontrollen och skrivåtgärden. Två konsumenter som tar emot kopior av samma meddelande kan inte båda skapa ordern. Ett skapandeförsök lyckas och det andra försöket 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 dedupliceringsnyckeln och affärsdokumenten i samma partitionsnyckel. Eftersom en transaktionsbatch körs inom en enda logisk partition väljer du en partitionsnyckel som alla dokument för ett meddelande delar. Batchen genomför alla dokument tillsammans eller inga alls, så en krasch mellan bearbetning och bekräftelse kan inte göra att dedupliceringsmarkören och affärsdatan hamnar i otakt. En batch som försöker skapa ett dokument som redan finns returnerar statuskoden 409 (Conflict), vilket identifierar dubbletten.

För att göra den här idempotenta konsumenten även motståndskraftig mot dubbla återförsök vid sändning aktiverar du dubblettidentifiering för kön. I en Standard- eller Premium-kö undertrycker dubblettidentifiering upprepade sändningar i dess historikfönster. Den idempotenta konsumenten hanterar fortfarande eventuella dubbletter som hamnar utanför det fönstret eller som uppstår till följd av återleverans.

Nästa steg

  • Asynkrona meddelandealternativ i Azure beskriver de alternativ för meddelandeinfrastruktur som avgör dina leveransgarantier och krav på duplicerad hantering.

  • Transactional Outbox-mönstret, som på ett tillförlitligt sätt publicerar meddelanden genom att skriva dem i samma transaktion som affärsdata, är publicerarsidan av Idempotent Consumer-mönstret.

  • Med återförsöksmönstret kan program hantera tillfälliga fel genom att försöka utföra åtgärder igen, vilket gör idempotent bearbetning nödvändig eftersom återförsök kan orsaka duplicerad leverans.

  • Elastisk Azure Event Hubs och Azure Functions design tillämpar det här mönstret på funktioner som Event Hubs utlöser, inklusive dedupliceringstekniker för händelseströmmar.