Share via


Arbeta med Reliable Collections

Service Fabric erbjuder en tillståndskänslig programmeringsmodell som är tillgänglig för .NET-utvecklare via Reliable Collections. Mer specifikt tillhandahåller Service Fabric tillförlitliga ordlistor och tillförlitliga köklasser. När du använder dessa klasser partitioneras tillståndet (för skalbarhet), replikeras (för tillgänglighet) och utförs i en partition (för ACID-semantik). Nu ska vi titta på en typisk användning av ett tillförlitligt ordlisteobjekt och se vad det faktiskt gör.

try
{
   // Create a new Transaction object for this partition
   using (ITransaction tx = base.StateManager.CreateTransaction())
   {
      // AddAsync takes key's write lock; if >4 secs, TimeoutException
      // Key & value put in temp dictionary (read your own writes),
      // serialized, redo/undo record is logged & sent to secondary replicas
      await m_dic.AddAsync(tx, key, value, cancellationToken);

      // CommitAsync sends Commit record to log & secondary replicas
      // After quorum responds, all locks released
      await tx.CommitAsync();
   }
   // If CommitAsync isn't called, Dispose sends Abort
   // record to log & all locks released
}
catch (TimeoutException)
{
   // choose how to handle the situation where you couldn't get a lock on the file because it was 
   // already in use. You might delay and retry the operation
   await Task.Delay(100);
}

Alla åtgärder på tillförlitliga ordlisteobjekt (förutom ClearAsync, som inte kan ångras) kräver ett ITransaction-objekt. Det här objektet har associerats med alla ändringar som du försöker göra i en tillförlitlig ordlista och/eller tillförlitliga köobjekt i en enda partition. Du hämtar ett ITransaction-objekt genom att anropa partitionens StateManager-metod CreateTransaction.

I koden ovan skickas ITransaction-objektet till en tillförlitlig ordlistas AddAsync-metod. Internt tar ordlistemetoder som accepterar en nyckel ett läsar-/skrivlås som är associerat med nyckeln. Om metoden ändrar nyckelns värde, tar metoden ett skrivlås på nyckeln och om metoden bara läser från nyckelns värde, tas ett läslås på nyckeln. Eftersom AddAsync ändrar nyckelns värde till det nya, skickade värdet tas nyckelns skrivlås. Så om 2 (eller fler) trådar försöker lägga till värden med samma nyckel samtidigt, hämtar en tråd skrivlåset och de andra trådarna blockeras. Som standard blockerar metoderna i upp till 4 sekunder för att hämta låset. efter 4 sekunder genererar metoderna en TimeoutException. Metodöverlagringar finns så att du kan skicka ett explicit timeout-värde om du vill.

Vanligtvis skriver du din kod för att reagera på en TimeoutException genom att fånga den och försöka utföra hela åtgärden igen (som du ser i koden ovan). I den här enkla koden anropar vi bara Task.Delay som skickar 100 millisekunder varje gång. Men i verkligheten kan det vara bättre att använda någon form av exponentiell back-off-fördröjning istället.

När låset har hämtats lägger AddAsync till nyckel- och värdeobjektreferenserna till en intern tillfällig ordlista som är associerad med ITransaction-objektet. Detta görs för att ge dig read-your-own-writes-semantik. Det vill säga när du anropar AddAsync returnerar ett senare anrop till TryGetValueAsync med samma ITransaction-objekt värdet även om du ännu inte har checkat in transaktionen.

Kommentar

Om du anropar TryGetValueAsync med en ny transaktion returneras en referens till det senast bekräftade värdet. Ändra inte referensen direkt eftersom den kringgår mekanismen för att bevara och replikera ändringarna. Vi rekommenderar att du gör värdena skrivskyddade så att det enda sättet att ändra värdet för en nyckel är genom tillförlitliga ordliste-API:er.

Sedan serialiserar AddAsync dina nyckel- och värdeobjekt till bytematriser och lägger till dessa bytematriser i en loggfil på den lokala noden. Slutligen skickar AddAsync bytematriserna till alla sekundära repliker så att de har samma nyckel/värde-information. Även om nyckel-/värdeinformationen har skrivits till en loggfil anses informationen inte vara en del av ordlistan förrän transaktionen som de är associerade med har checkats in.

I koden ovan genomför anropet till CommitAsync alla transaktionens åtgärder. Mer specifikt lägger den till incheckningsinformation i loggfilen på den lokala noden och skickar även incheckningsposten till alla sekundära repliker. När ett kvorum (majoritet) av replikerna har svarat anses alla dataändringar vara permanenta och alla lås som är associerade med nycklar som har manipulerats via ITransaction-objektet frigörs så att andra trådar/transaktioner kan ändra samma nycklar och deras värden.

Om CommitAsync inte anropas (vanligtvis på grund av att ett undantag utlöses) tas ITransaction-objektet bort. När du tar bort ett oövervakat ITransaction-objekt avbryter Service Fabric informationen i den lokala nodens loggfil och inget behöver skickas till någon av de sekundära replikerna. Och sedan frigörs alla lås som är associerade med nycklar som har manipulerats via transaktionen.

Flyktiga tillförlitliga samlingar

I vissa arbetsbelastningar, till exempel en replikerad cache, kan enstaka dataförluster tolereras. Om du undviker beständighet för data till disk kan du få bättre svarstider och dataflöden när du skriver till Reliable Dictionaries. Kompromissen med bristande beständighet är att om kvorumförlust uppstår uppstår fullständig dataförlust. Eftersom kvorumförlust är en sällsynt händelse kan den ökade prestandan vara värd den sällsynta risken för dataförlust för dessa arbetsbelastningar.

För närvarande är flyktigt stöd endast tillgängligt för Reliable Dictionaries och Reliable Queues, och inte ReliableConcurrentQueues. Se listan över varningar för att informera ditt beslut om att använda flyktiga samlingar.

Om du vill aktivera flyktigt stöd i din tjänst anger du flaggan i deklarationen HasPersistedState för tjänsttyp till false, så här:

<StatefulServiceType ServiceTypeName="MyServiceType" HasPersistedState="false" />

Kommentar

Befintliga beständiga tjänster kan inte göras flyktiga och vice versa. Om du vill göra det måste du ta bort den befintliga tjänsten och sedan distribuera tjänsten med den uppdaterade flaggan. Det innebär att du måste vara villig att ådra dig fullständig dataförlust om du vill ändra HasPersistedState flaggan.

Vanliga fallgropar och hur du undviker dem

Nu när du förstår hur tillförlitliga samlingar fungerar internt ska vi ta en titt på några vanliga missbruk av dem. Se koden nedan:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // AddAsync serializes the name/user, logs the bytes,
   // & sends the bytes to the secondary replicas.
   await m_dic.AddAsync(tx, name, user);

   // The line below updates the property's value in memory only; the
   // new value is NOT serialized, logged, & sent to secondary replicas.
   user.LastLogin = DateTime.UtcNow;  // Corruption!

   await tx.CommitAsync();
}

När du arbetar med en vanlig .NET-ordlista kan du lägga till en nyckel/ett värde i ordlistan och sedan ändra värdet för en egenskap (till exempel LastLogin). Den här koden fungerar dock inte korrekt med en tillförlitlig ordlista. Kom ihåg att från den tidigare diskussionen serialiserar anropet till AddAsync nyckel-/värdeobjekten till bytematriser och sparar sedan matriserna i en lokal fil och skickar dem även till de sekundära replikerna. Om du senare ändrar en egenskap ändrar detta endast egenskapens värde i minnet. det påverkar inte den lokala filen eller data som skickas till replikerna. Om processen kraschar kastas det som finns i minnet bort. När en ny process startar eller om en annan replik blir primär är det gamla egenskapsvärdet det som är tillgängligt.

Jag kan inte nog betona hur lätt det är att göra den typ av misstag som visas ovan. Och du får bara lära dig om misstaget om/när processen går ner. Det rätta sättet att skriva koden är helt enkelt att vända de två raderna:

using (ITransaction tx = StateManager.CreateTransaction())
{
   user.LastLogin = DateTime.UtcNow;  // Do this BEFORE calling AddAsync
   await m_dic.AddAsync(tx, name, user);
   await tx.CommitAsync();
}

Här är ett annat exempel som visar ett vanligt misstag:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> user = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (user.HasValue)
   {
      // The line below updates the property's value in memory only; the
      // new value is NOT serialized, logged, & sent to secondary replicas.
      user.Value.LastLogin = DateTime.UtcNow; // Corruption!
      await tx.CommitAsync();
   }
}

Återigen, med vanliga .NET-ordlistor fungerar koden ovan bra och är ett vanligt mönster: utvecklaren använder en nyckel för att leta upp ett värde. Om värdet finns ändrar utvecklaren en egenskaps värde. Men med tillförlitliga samlingar uppvisar den här koden samma problem som redan diskuterats: du får inte ändra ett objekt när du har gett det till en tillförlitlig samling.

Det rätta sättet att uppdatera ett värde i en tillförlitlig samling är att hämta en referens till det befintliga värdet och betrakta det objekt som refereras till av den här referensen som oföränderligt. Skapa sedan ett nytt objekt som är en exakt kopia av det ursprungliga objektet. Nu kan du ändra tillståndet för det nya objektet och skriva det nya objektet till samlingen så att det serialiseras till bytematriser, läggs till i den lokala filen och skickas till replikerna. När du har checkat in ändringarna har objekten i minnet, den lokala filen och alla repliker samma exakta tillstånd. Allt är bra!

Koden nedan visar rätt sätt att uppdatera ett värde i en tillförlitlig samling:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> currentUser = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (currentUser.HasValue)
   {
      // Create new user object with the same state as the current user object.
      // NOTE: This must be a deep copy; not a shallow copy. Specifically, only
      // immutable state can be shared by currentUser & updatedUser object graphs.
      User updatedUser = new User(currentUser);

      // In the new object, modify any properties you desire
      updatedUser.LastLogin = DateTime.UtcNow;

      // Update the key's value to the updateUser info
      await m_dic.SetValue(tx, name, updatedUser);
      await tx.CommitAsync();
   }
}

Definiera oföränderliga datatyper för att förhindra programmerarfel

Vi rekommenderar att kompilatorn rapporterar fel när du oavsiktligt skapar kod som muterar tillståndet för ett objekt som du ska betrakta som oföränderligt. Men C#-kompilatorn har inte möjlighet att göra detta. För att undvika potentiella programmerarbuggar rekommenderar vi därför starkt att du definierar de typer som du använder med tillförlitliga samlingar som oföränderliga typer. Mer specifikt innebär det att du håller dig till kärnvärdestyper (till exempel tal [Int32, UInt64 osv.], DateTime, Guid, TimeSpan och liknande). Du kan också använda Sträng. Det är bäst att undvika samlingsegenskaper eftersom serialisering och deserialisering av dem ofta kan skada prestanda. Men om du vill använda samlingsegenskaper rekommenderar vi starkt att du använder . NET:s oföränderliga samlingsbibliotek (System.Collections.Immutable). Det här biblioteket är tillgängligt för nedladdning från https://nuget.org. Vi rekommenderar också att du förseglar dina klasser och gör fälten skrivskyddade när det är möjligt.

UserInfo-typen nedan visar hur du definierar en oföränderlig typ som utnyttjar ovan nämnda rekommendationer.

[DataContract]
// If you don't seal, you must ensure that any derived classes are also immutable
public sealed class UserInfo
{
   private static readonly IEnumerable<ItemId> NoBids = ImmutableList<ItemId>.Empty;

   public UserInfo(String email, IEnumerable<ItemId> itemsBidding = null) 
   {
      Email = email;
      ItemsBidding = (itemsBidding == null) ? NoBids : itemsBidding.ToImmutableList();
   }

   [OnDeserialized]
   private void OnDeserialized(StreamingContext context)
   {
      // Convert the deserialized collection to an immutable collection
      ItemsBidding = ItemsBidding.ToImmutableList();
   }

   [DataMember]
   public readonly String Email;

   // Ideally, this would be a readonly field but it can't be because OnDeserialized
   // has to set it. So instead, the getter is public and the setter is private.
   [DataMember]
   public IEnumerable<ItemId> ItemsBidding { get; private set; }

   // Since each UserInfo object is immutable, we add a new ItemId to the ItemsBidding
   // collection by creating a new immutable UserInfo object with the added ItemId.
   public UserInfo AddItemBidding(ItemId itemId)
   {
      return new UserInfo(Email, ((ImmutableList<ItemId>)ItemsBidding).Add(itemId));
   }
}

ItemId-typen är också en oföränderlig typ som visas här:

[DataContract]
public struct ItemId
{
   [DataMember] public readonly String Seller;
   [DataMember] public readonly String ItemName;
   public ItemId(String seller, String itemName)
   {
      Seller = seller;
      ItemName = itemName;
   }
}

Schemaversionshantering (uppgraderingar)

Internt serialiserar Reliable Collections dina objekt med hjälp av . NET:s DataContractSerializer. De serialiserade objekten sparas på den primära replikens lokala disk och överförs även till de sekundära replikerna. När tjänsten mognar är det troligt att du vill ändra den typ av data (schema) som tjänsten kräver. Hantera versionshantering av dina data med stor försiktighet. Först och främst måste du alltid kunna deserialisera gamla data. Mer specifikt innebär det att din deserialiseringskod måste vara oändligt bakåtkompatibel: Version 333 av tjänstkoden måste kunna användas på data som placerats i en tillförlitlig samling av version 1 av tjänstkoden för 5 år sedan.

Dessutom uppgraderas tjänstkoden en uppgraderingsdomän i taget. Under en uppgradering har du därför två olika versioner av tjänstkoden som körs samtidigt. Du måste undvika att den nya versionen av tjänstkoden använder det nya schemat eftersom gamla versioner av tjänstkoden kanske inte kan hantera det nya schemat. När det är möjligt bör du utforma varje version av tjänsten så att den är kompatibel med en version. Mer specifikt innebär det att V1 i tjänstkoden ska kunna ignorera alla schemaelement som den inte uttryckligen hanterar. Den måste dock kunna spara alla data som den inte uttryckligen känner till och skriva tillbaka dem när du uppdaterar en ordlistenyckel eller ett värde.

Varning

Du kan ändra schemat för en nyckel, men du måste se till att nyckelns likhets- och jämförelsealgoritmer är stabila. Beteendet för tillförlitliga samlingar efter en ändring i någon av dessa algoritmer är odefinierat och kan leda till skadade data, förlust och tjänstkrascher. .NET-strängar kan användas som en nyckel men använd själva strängen som nyckel – använd inte resultatet av String.GetHashCode som nyckel.

Du kan också utföra en uppgradering med flera faser.

  1. Uppgradera tjänsten till en ny version som
    • har både den ursprungliga V1 och den nya V2-versionen av datakontrakten som ingår i tjänstkodpaketet;
    • registrerar anpassade V2-tillståndsserialiserare, om det behövs;
    • utför alla åtgärder på den ursprungliga V1-samlingen med hjälp av V1-datakontrakten.
  2. Uppgradera tjänsten till en ny version som
    • skapar en ny V2-samling.
    • utför varje tilläggs-, uppdaterings- och borttagningsåtgärd på de första V1- och V2-samlingarna i en enda transaktion.
    • utför endast läsåtgärder på V1-samlingen.
  3. Kopiera alla data från V1-samlingen till V2-samlingen.
    • Detta kan göras i en bakgrundsprocess av tjänstversionen som distribuerades i steg 2.
    • Hämta alla nycklar från V1-samlingen igen. Uppräkning utförs med IsolationLevel.Snapshot som standard för att undvika att samlingen låses under hela åtgärden.
    • För varje nyckel använder du en separat transaktion för att
      • TryGetValueAsync från V1-samlingen.
      • Om värdet redan har tagits bort från V1-samlingen sedan kopieringsprocessen startade bör nyckeln hoppas över och inte återuppstå i V2-samlingen.
      • TryAddAsync värdet till V2-samlingen.
      • Om värdet redan har lagts till i V2-samlingen sedan kopieringsprocessen startade bör nyckeln hoppas över.
      • Transaktionen ska endast checkas in om TryAddAsync returnerar true.
      • API:er för värdeåtkomst använder IsolationLevel.ReadRepeatable som standard och förlitar sig på låsning för att garantera att värdena inte ändras av en annan anropare förrän transaktionen har checkats in eller avbrutits.
  4. Uppgradera tjänsten till en ny version som
    • utför endast läsåtgärder på V2-samlingen.
    • utför fortfarande varje tilläggs-, uppdaterings- och borttagningsåtgärd på de första V1- och V2-samlingarna för att behålla alternativet att återställa till V1.
  5. Testa tjänsten på ett omfattande sätt och bekräfta att den fungerar som förväntat.
    • Om du missade någon värdeåtkomståtgärd som inte uppdaterades för att fungera på både V1- och V2-samlingen kanske du märker att data saknas.
    • Om några data saknas återställs till steg 1 tar du bort V2-samlingen och upprepar processen.
  6. Uppgradera tjänsten till en ny version som
    • utför endast alla åtgärder på V2-samlingen.
    • Att gå tillbaka till V1 är inte längre möjligt med en tjänståterställning och skulle kräva att du rullar framåt med omvända steg 2–4.
  7. Uppgradera tjänsten en ny version som
  8. Vänta på loggtrunkering.
    • Som standard sker detta var 50 MB av skrivningar (lägger till, uppdaterar och tar bort) till tillförlitliga samlingar.
  9. Uppgradera tjänsten till en ny version som
    • har inte längre V1-datakontrakten inkluderade i tjänstkodpaketet.

Nästa steg

Mer information om hur du skapar framåtkompatibla datakontrakt finns i Framåtkompatibla datakontrakt

Information om metodtips för versionshantering av datakontrakt finns i Versionshantering av datakontrakt

Information om hur du implementerar versionstoleranta datakontrakt finns i Versionstoleranta serialiseringsåteranrop

Information om hur du tillhandahåller en datastruktur som kan samverka mellan flera versioner finns i IExtensibleDataObject

Information om hur du konfigurerar tillförlitliga samlingar finns i Replikeringskonfiguration