Werken met betrouwbare verzamelingen

Service Fabric biedt een stateful programmeermodel dat beschikbaar is voor .NET-ontwikkelaars via Reliable Collections. Service Fabric biedt met name betrouwbare woordenlijst- en betrouwbare wachtrijklassen. Wanneer u deze klassen gebruikt, wordt uw status gepartitioneerd (voor schaalbaarheid), gerepliceerd (voor beschikbaarheid) en verwerkt binnen een partitie (voor ACID-semantiek). Laten we eens kijken naar een typisch gebruik van een betrouwbaar woordenlijstobject en kijken wat het eigenlijk doet.

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);
}

Voor alle bewerkingen op betrouwbare woordenlijstobjecten (met uitzondering van ClearAsync, dat niet ongedaan kan wordengemaakt), is een ITransaction-object vereist. Aan dit object zijn alle wijzigingen gekoppeld die u probeert aan te brengen in een betrouwbare woordenlijst en/of betrouwbare wachtrijobjecten binnen één partitie. U verkrijgt een ITransaction-object door de methode CreateTransaction van de partitie aan te roepen.

In de bovenstaande code wordt het ITransaction-object doorgegeven aan de AddAsync-methode van een betrouwbare woordenlijst. Intern gebruiken woordenlijstmethoden die een sleutel accepteren een lezer/schrijververgrendeling die aan de sleutel is gekoppeld. Als de methode de waarde van de sleutel wijzigt, neemt de methode een schrijfvergrendeling op de sleutel en als de methode alleen leest uit de waarde van de sleutel, wordt er een leesvergrendeling op de sleutel genomen. Omdat AddAsync de waarde van de sleutel wijzigt in de nieuwe, doorgegeven waarde, wordt de schrijfvergrendeling van de sleutel genomen. Dus als 2 (of meer) threads proberen waarden met dezelfde sleutel tegelijk toe te voegen, krijgt één thread de schrijfvergrendeling en worden de andere threads geblokkeerd. Methoden blokkeren standaard maximaal 4 seconden om de vergrendeling te verkrijgen; na 4 seconden gooien de methoden een TimeoutException. Er bestaan overbelastingen van methoden, zodat u desgewenst een expliciete time-outwaarde kunt doorgeven.

Meestal schrijft u uw code om te reageren op een TimeoutException door deze te vangen en de hele bewerking opnieuw uit te voeren (zoals wordt weergegeven in de bovenstaande code). In deze eenvoudige code roepen we task.delay elke keer 100 milliseconden aan. Maar in werkelijkheid kunt u beter een soort exponentieel uitstel gebruiken in plaats daarvan.

Zodra de vergrendeling is verkregen, voegt AddAsync de sleutel- en waardeobjectverwijzingen toe aan een interne tijdelijke woordenlijst die is gekoppeld aan het ITransaction-object. Dit wordt gedaan om u semantiek voor lezen-uw-eigen-schrijfbewerkingen te bieden. Nadat u AddAsync hebt aangeroepen, retourneert een latere aanroep naar TryGetValueAsync met hetzelfde ITransaction-object de waarde, zelfs als u de transactie nog niet hebt doorgevoerd.

Notitie

Als u TryGetValueAsync aanroept met een nieuwe transactie, wordt een verwijzing naar de laatst doorgevoerde waarde geretourneerd. Wijzig die verwijzing niet rechtstreeks, omdat hiermee het mechanisme voor het persistent maken en repliceren van de wijzigingen wordt overgeslagen. We raden u aan de waarden alleen-lezen te maken, zodat de enige manier om de waarde voor een sleutel te wijzigen, is via betrouwbare woordenlijst-API's.

Vervolgens serialiseert AddAsync uw sleutel- en waardeobjecten naar bytematrices en voegt u deze bytematrices toe aan een logboekbestand op het lokale knooppunt. Ten slotte verzendt AddAsync de bytematrices naar alle secundaire replica's, zodat ze dezelfde sleutel/waarde-informatie hebben. Hoewel de sleutel-/waardegegevens naar een logboekbestand zijn geschreven, wordt de informatie pas als onderdeel van de woordenlijst beschouwd als de transactie waaraan ze zijn gekoppeld.

In de bovenstaande code voert de aanroep van CommitAsync alle bewerkingen van de transactie door. In het bijzonder worden doorvoergegevens toegevoegd aan het logboekbestand op het lokale knooppunt en wordt de doorvoerrecord ook naar alle secundaire replica's verzonden. Zodra een quorum (meerderheid) van de replica's heeft gereageerd, worden alle gegevenswijzigingen beschouwd als permanent en eventuele vergrendelingen die zijn gekoppeld aan sleutels die zijn gemanipuleerd via het object ITransaction, worden vrijgegeven, zodat andere threads/transacties dezelfde sleutels en hun waarden kunnen manipuleren.

Als CommitAsync niet wordt aangeroepen (meestal vanwege een uitzondering die wordt gegenereerd), wordt het object ITransaction verwijderd. Bij het verwijderen van een niet-verzonden ITransaction-object voegt Service Fabric informatie toe aan het logboekbestand van het lokale knooppunt en hoeft er niets naar een van de secundaire replica's te worden verzonden. En vervolgens worden eventuele vergrendelingen die zijn gekoppeld aan sleutels die via de transactie zijn gemanipuleerd, vrijgegeven.

Vluchtige betrouwbare verzamelingen

In sommige workloads, zoals een gerepliceerde cache, kan incidenteel gegevensverlies worden getolereerd. Het voorkomen van persistentie van de gegevens op schijf kan betere latentie en doorvoer mogelijk maken bij het schrijven naar betrouwbare woordenlijsten. Het nadeel van een gebrek aan persistentie is dat als quorumverlies optreedt, volledig gegevensverlies zal optreden. Omdat quorumverlies zelden voorkomt, kunnen de verbeterde prestaties de zeldzame kans op gegevensverlies voor deze workloads waard zijn.

Op dit moment is vluchtige ondersteuning alleen beschikbaar voor betrouwbare woordenlijsten en betrouwbare wachtrijen, en niet voor ReliableConcurrentQueues. Raadpleeg de lijst met opmerkingen om uw beslissing te informeren over het gebruik van vluchtige verzamelingen .

Als u vluchtige ondersteuning in uw service wilt inschakelen, stelt u de HasPersistedState vlag in de declaratie van het servicetype in op false, bijvoorbeeld:

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

Notitie

Bestaande persistente services kunnen niet vluchtig worden gemaakt en omgekeerd. Als u dit wilt doen, moet u de bestaande service verwijderen en vervolgens de service implementeren met de bijgewerkte vlag. Dit betekent dat u bereid moet zijn om volledig gegevensverlies te veroorzaken als u de HasPersistedState vlag wilt wijzigen.

Veelvoorkomende valkuilen en hoe ze te vermijden

Nu u begrijpt hoe de betrouwbare verzamelingen intern werken, gaan we eens kijken naar enkele veelvoorkomende misbruiken van deze verzamelingen. Zie de onderstaande code:

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();
}

Wanneer u met een gewone .NET-woordenlijst werkt, kunt u een sleutel/waarde toevoegen aan de woordenlijst en vervolgens de waarde van een eigenschap wijzigen (zoals LastLogin). Deze code werkt echter niet correct met een betrouwbare woordenlijst. Vergeet niet dat de aanroep van AddAsync de sleutel-/waardeobjecten serialiseert naar bytematrices en de matrices vervolgens opslaat in een lokaal bestand en deze ook naar de secundaire replica's verzendt. Als u later een eigenschap wijzigt, wordt hiermee alleen de waarde van de eigenschap in het geheugen gewijzigd; dit heeft geen invloed op het lokale bestand of de gegevens die naar de replica's worden verzonden. Als het proces vastloopt, wordt wat er in het geheugen zit weggegooid. Wanneer een nieuw proces wordt gestart of als een andere replica primair wordt, is de oude eigenschapswaarde beschikbaar.

Ik kan niet genoeg stress geven hoe gemakkelijk het is om het soort fout te maken dat hierboven wordt weergegeven. En u leert alleen over de fout als/wanneer het proces uitvalt. De juiste manier om de code te schrijven, is door de twee regels om te draaien:

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

Hier volgt een ander voorbeeld met een veelvoorkomende fout:

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();
   }
}

Nogmaals, met reguliere .NET-woordenlijsten werkt de bovenstaande code prima en is het een gemeenschappelijk patroon: de ontwikkelaar gebruikt een sleutel om een waarde op te zoeken. Als de waarde bestaat, wijzigt de ontwikkelaar de waarde van een eigenschap. Bij betrouwbare verzamelingen vertoont deze code echter hetzelfde probleem als al besproken: u moet een object niet wijzigen nadat u het hebt gegeven aan een betrouwbare verzameling.

De juiste manier om een waarde in een betrouwbare verzameling bij te werken, is door een verwijzing naar de bestaande waarde op te halen en het object te beschouwen waarnaar wordt verwezen door deze verwijzing onveranderbaar. Maak vervolgens een nieuw object dat een exacte kopie is van het oorspronkelijke object. U kunt nu de status van dit nieuwe object wijzigen en het nieuwe object naar de verzameling schrijven, zodat het wordt geserialiseerd naar bytematrices, toegevoegd aan het lokale bestand en naar de replica's wordt verzonden. Nadat de wijzigingen zijn doorgevoerd, hebben de in-memory objecten, het lokale bestand en alle replica's dezelfde exacte status. Alles is goed!

De onderstaande code toont de juiste manier om een waarde in een betrouwbare verzameling bij te werken:

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();
   }
}

Onveranderbare gegevenstypen definiëren om programmeursfouten te voorkomen

In het ideale gevallen willen we dat de compiler fouten rapporteert wanneer u per ongeluk code produceert die de status van een object muteert dat u onveranderbaar moet beschouwen. Maar de C#-compiler heeft niet de mogelijkheid om dit te doen. Om potentiële programmeursfouten te voorkomen, raden we u ten zeerste aan de typen te definiëren die u met betrouwbare verzamelingen gebruikt om onveranderbare typen te zijn. Dit betekent dat u zich houdt aan kernwaardetypen (zoals getallen [Int32, UInt64, enzovoort], DateTime, Guid, TimeSpan en dergelijke). U kunt ook Tekenreeks gebruiken. Het is raadzaam om verzamelingseigenschappen te voorkomen omdat ze worden geserialiseerd en gedeserialiseerd, de prestaties vaak kunnen schaden. Als u echter verzamelingseigenschappen wilt gebruiken, raden we u ten zeerste aan het gebruik van . De onveranderbare verzamelingenbibliotheek van NET (System.Collections.Immutable). Deze bibliotheek is beschikbaar om te downloaden van https://nuget.org. We raden u ook aan uw klassen af te sluiten en waar mogelijk velden alleen-lezen te maken.

Het type UserInfo hieronder laat zien hoe u een onveranderbaar type definieert dat gebruikmaakt van bovengenoemde aanbevelingen.

[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));
   }
}

Het type ItemId is ook een onveranderbaar type, zoals hier wordt weergegeven:

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

Schemaversiebeheer (upgrades)

Intern serialiseren Betrouwbare verzamelingen uw objecten met behulp van . DataContractSerializer van NET. De geserialiseerde objecten worden bewaard op de lokale schijf van de primaire replica en worden ook verzonden naar de secundaire replica's. Naarmate uw service verder wordt ontwikkeld, wilt u waarschijnlijk het soort gegevens (schema) wijzigen dat uw service nodig heeft. Benader versiebeheer van uw gegevens met grote zorg. In de eerste plaats moet u altijd oude gegevens kunnen deserialiseren. Dit betekent dat uw deserialisatiecode oneindig achterwaarts compatibel moet zijn: versie 333 van uw servicecode moet 5 jaar geleden kunnen werken op gegevens die in een betrouwbare verzameling zijn geplaatst in versie 1 van uw servicecode.

Bovendien wordt servicecode één upgradedomein tegelijk bijgewerkt. Tijdens een upgrade hebt u dus twee verschillende versies van uw servicecode die tegelijkertijd wordt uitgevoerd. U moet voorkomen dat de nieuwe versie van uw servicecode het nieuwe schema gebruikt omdat oude versies van uw servicecode het nieuwe schema mogelijk niet kunnen verwerken. Indien mogelijk moet u elke versie van uw service ontwerpen om compatibel te zijn met één versie. Dit betekent dat V1 van uw servicecode alle schema-elementen die niet expliciet worden verwerkt, moet kunnen negeren. Het moet echter wel in staat zijn om gegevens op te slaan die niet expliciet bekend zijn en deze terugschrijven bij het bijwerken van een woordenlijstsleutel of -waarde.

Waarschuwing

Hoewel u het schema van een sleutel kunt wijzigen, moet u ervoor zorgen dat de gelijkheids- en vergelijkingsalgoritmen van uw sleutel stabiel zijn. Gedrag van betrouwbare verzamelingen na een wijziging in een van deze algoritmen is niet gedefinieerd en kan leiden tot beschadiging van gegevens, verlies en servicecrashes. .NET-tekenreeksen kunnen als sleutel worden gebruikt, maar gebruik de tekenreeks zelf als sleutel. Gebruik het resultaat van String.GetHashCode niet als de sleutel.

U kunt ook een upgrade met meerdere fasen uitvoeren.

  1. Service upgraden naar een nieuwe versie die
    • heeft zowel de oorspronkelijke V1 als de nieuwe V2-versie van de gegevenscontracten die zijn opgenomen in het servicecodepakket;
    • registreert aangepaste V2-statusserialisaties, indien nodig;
    • voert alle bewerkingen uit op de oorspronkelijke V1-verzameling met behulp van de V1-gegevenscontracten.
  2. Service upgraden naar een nieuwe versie die
    • maakt een nieuwe V2-verzameling;
    • voert elke bewerking voor toevoegen, bijwerken en verwijderen uit op eerste V1 en vervolgens V2-verzamelingen in één transactie;
    • voert alleen leesbewerkingen uit op de V1-verzameling.
  3. Kopieer alle gegevens uit de V1-verzameling naar de V2-verzameling.
    • Dit kan worden gedaan in een achtergrondproces door de serviceversie die in stap 2 is geïmplementeerd.
    • Alle sleutels uit de V1-verzameling opnieuw ophalen. Opsomming wordt standaard uitgevoerd met de IsolationLevel.Snapshot om te voorkomen dat de verzameling wordt vergrendeld voor de duur van de bewerking.
    • Gebruik voor elke sleutel een afzonderlijke transactie om
      • TryGetValueAsync uit de V1-verzameling.
      • Als de waarde al uit de V1-verzameling is verwijderd sinds het kopieerproces is gestart, moet de sleutel worden overgeslagen en niet opnieuw worden beveiligd in de V2-verzameling.
      • TryAddAsync de waarde naar de V2-verzameling.
      • Als de waarde al is toegevoegd aan de V2-verzameling sinds het kopieerproces is gestart, moet de sleutel worden overgeslagen.
      • De transactie mag alleen worden doorgevoerd als de TryAddAsync retourneert true.
      • Api's voor waardetoegang maken standaard gebruik van de IsolationLevel.ReadRepeatable en vertrouwen op vergrendeling om te garanderen dat de waarden niet door een andere beller worden gewijzigd totdat de transactie is doorgevoerd of afgebroken.
  4. Service upgraden naar een nieuwe versie die
    • voert alleen leesbewerkingen uit op de V2-verzameling;
    • voert nog steeds elke bewerking voor toevoegen, bijwerken en verwijderen uit op eerste V1 en vervolgens op V2-verzamelingen om de optie voor terugdraaien naar V1 te behouden.
  5. Test de service uitgebreid en controleer of deze werkt zoals verwacht.
    • Als u een waardetoegangsbewerking hebt gemist die niet is bijgewerkt voor zowel V1- als V2-verzameling, ziet u mogelijk ontbrekende gegevens.
    • Als er gegevens ontbreken, keert u terug naar stap 1, verwijdert u de V2-verzameling en herhaalt u het proces.
  6. Service upgraden naar een nieuwe versie die
    • voert alleen alle bewerkingen uit op de V2-verzameling;
    • teruggaan naar V1 is niet meer mogelijk met een serviceback en zou moeten worden teruggedraaid met omgekeerde stappen 2-4.
  7. Een nieuwe versie upgraden van de service die
  8. Wacht op afkapping van logboeken.
    • Dit gebeurt standaard elke 50 MB aan schrijfbewerkingen (wordt toegevoegd, bijgewerkt en verwijderd) aan betrouwbare verzamelingen.
  9. Service upgraden naar een nieuwe versie die
    • bevat niet langer de V1-gegevenscontracten die zijn opgenomen in het servicecodepakket.

Volgende stappen

Zie Forward-Compatible Data Contracts (Forward-Compatible Data Contracts) voor meer informatie over het maken van compatibele gegevenscontracten

Zie Data Contract Versioning voor meer informatie over best practices voor versiebeheer van gegevenscontracten

Zie Callbacks voor versietolerante serialisatie voor meer informatie over het implementeren van versietolerante gegevenscontracten

Zie IExtensibleDataObject voor meer informatie over het bieden van een gegevensstructuur die kan samenwerken tussen meerdere versies

Zie Replicator-configuratie voor meer informatie over het configureren van betrouwbare verzamelingen