Práce s Reliable Collections

Service Fabric nabízí stavový programovací model dostupný vývojářům .NET prostřednictvím spolehlivých kolekcí. Service Fabric konkrétně poskytuje spolehlivý slovník a spolehlivé třídy front. Při použití těchto tříd je váš stav rozdělený (pro škálovatelnost), replikován (pro dostupnost) a transactován v rámci oddílu (pro sémantiku ACID). Pojďme se podívat na typické použití spolehlivého objektu slovníku a podívat se, co vlastně dělá.

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

Všechny operace se spolehlivými objekty slovníku (s výjimkou ClearAsync, které nelze vrátit zpět), vyžadují objekt ITransaction. Tento objekt je k němu přidružený a všechny změny, které se pokoušíte provést v libovolném spolehlivém slovníku nebo spolehlivém objektu fronty v rámci jednoho oddílu. Objekt ITransaction získáte voláním Metody CreateTransaction objektu StateManager oddílu.

V kódu výše je ITransaction objekt předán do spolehlivé slovník AddAsync metoda. Interně metody slovníku, které přijímají klíč, přebírají zámek čtečky/zapisovače přidružené ke klíči. Pokud metoda upraví hodnotu klíče, metoda vezme zámek zápisu na klíč a pokud metoda čte pouze z hodnoty klíče, pak se na klíč vezme zámek pro čtení. Vzhledem k tomu, že AddAsync upraví hodnotu klíče na novou předanou hodnotu, zamkne se zámek zápisu klíče. Takže pokud se 2 (nebo více) vláken pokusí přidat hodnoty se stejným klíčem současně, jedno vlákno získá zámek zápisu a ostatní vlákna se zablokují. Ve výchozím nastavení metody blokují až 4 sekundy k získání zámku; po 4 sekundách metody vyvolá výjimku TimeoutException. Existují přetížení metody, které umožňují předat explicitní hodnotu časového limitu, pokud chcete.

Kód obvykle napíšete tak, aby reagoval na vypršení časového limitu tím, že ho zachytíte a zopakujete celou operaci (jak je znázorněno v kódu výše). V tomto jednoduchém kódu voláme jen Task.Delay a pokaždé předáváme 100 milisekund. Ve skutečnosti ale možná budete raději používat nějaký druh exponenciálního zpoždění zpětného vypnutí.

Po získání zámku AddAsync přidá odkazy na klíč a hodnotu objektu do interního dočasného slovníku přidruženého k objektu ITransaction. To vám umožní sémantiku čtení vlastních zápisů. To znamená, že po volání AddAsync, pozdější volání TryGetValueAsync pomocí stejného ITransaction objektu vrátí hodnotu, i když jste dosud potvrzena transakce.

Poznámka:

Volání TryGetValueAsync s novou transakcí vrátí odkaz na poslední potvrzenou hodnotu. Neupravujte tento odkaz přímo, protože tento obchází mechanismus pro zachování a replikaci změn. Doporučujeme nastavit hodnoty jen pro čtení, aby jediným způsobem, jak změnit hodnotu klíče, je prostřednictvím spolehlivých rozhraní API slovníku.

Potom AddAsync serializuje objekty klíče a hodnoty na bajtová pole a připojí tato bajtová pole k souboru protokolu v místním uzlu. Nakonec AddAsync odešle bajtová pole do všech sekundárních replik, aby měly stejné informace o klíči a hodnotě. I když byly informace o klíč/hodnotě zapsány do souboru protokolu, informace se nepovažují za součást slovníku, dokud transakce, ke které jsou přidruženy, nebyla potvrzena.

Ve výše uvedeném kódu volání CommitAsync potvrdí všechny operace transakce. Konkrétně připojí informace o potvrzení do souboru protokolu v místním uzlu a také odešle záznam potvrzení do všech sekundárních replik. Jakmile odpoví kvorum (většina) replik, všechny změny dat se považují za trvalé a všechny zámky spojené s klíči, které byly manipulovány pomocí objektu ITransaction, aby ostatní vlákna/transakce mohly manipulovat se stejnými klíči a jejich hodnotami.

Pokud CommitAsync není volána (obvykle kvůli vyvolání výjimky), objekt ITransaction se odstraní. Při odstraňování nepotvrzeného objektu ITransaction připojí Service Fabric k souboru protokolu místního uzlu informace o přerušení a do žádné sekundární repliky se nic neodesílají. A pak se uvolní všechny zámky spojené s klíči, které byly manipulovány prostřednictvím transakce.

Volatile Reliable Collections

V některých úlohách, jako je například replikovaná mezipaměť, je možné tolerovat občasnou ztrátu dat. Zabránění trvalosti dat na disk může při zápisu do spolehlivých slovníků umožnit lepší latenci a propustnost. Kompromis pro nedostatek trvalosti spočívá v tom, že pokud dojde ke ztrátě kvora, dojde k úplné ztrátě dat. Vzhledem k tomu, že ztráta kvora je výjimečným výskytem, může zvýšení výkonu stát za vzácnou možnost ztráty dat pro tyto úlohy.

V současné době je k dispozici volatilní podpora pouze pro spolehlivé slovníky a spolehlivé fronty, nikoli ReliableConcurrentQueues. Podívejte se na seznam upozornění , abyste se mohli rozhodnout, jestli se mají používat nestálé kolekce.

Pokud chcete ve službě povolit nestálou podporu, nastavte HasPersistedState příznak deklarace typu služby takto false:

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

Poznámka:

Stávající trvalé služby nelze vytvořit nestálé a naopak. Pokud to chcete udělat, budete muset stávající službu odstranit a pak ji nasadit s aktualizovaným příznakem. To znamená, že pokud chcete příznak změnit, musíte být ochotni zajistit úplnou ztrátu HasPersistedState dat.

Běžné nástrahy a jak se jim vyhnout

Teď, když rozumíte tomu, jak spolehlivé kolekce fungují interně, se podíváme na některé běžné zneužití těchto kolekcí. Podívejte se na následující kód:

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

Při práci s běžným slovníkem .NET můžete do slovníku přidat klíč/hodnotu a pak změnit hodnotu vlastnosti (například LastLogin). Tento kód ale nebude fungovat správně se spolehlivým slovníkem. Vzpomeňte si na předchozí diskuzi, volání AddAsync serializuje objekty klíče/hodnoty na bajtové pole a pak uloží pole do místního souboru a také je odešle do sekundárních replik. Pokud později změníte vlastnost, změní se hodnota vlastnosti pouze v paměti; nemá vliv na místní soubor ani data odesílaná do replik. Pokud se proces chybově ukončí, co je v paměti, je vyhozeno. Když se spustí nový proces nebo pokud se stane primární jinou replikou, je k dispozici stará hodnota vlastnosti.

Nemůžu zdůraznit, jak snadné je udělat druh chyby uvedené výše. A o této chybě se dozvíte jen v případě, že proces klesne. Správný způsob, jak napsat kód, je jednoduše obrátit dva řádky:

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

Tady je další příklad, který ukazuje běžnou chybu:

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

Opět platí, že s běžnými slovníky .NET výše uvedený kód funguje dobře a je běžným vzorem: vývojář k vyhledání hodnoty používá klíč. Pokud hodnota existuje, vývojář změní hodnotu vlastnosti. V případě spolehlivých kolekcí však tento kód vykazuje stejný problém jako již probíraný: Jakmile ho udělíte spolehlivé kolekci, nesmíte objekt upravovat.

Správným způsobem, jak aktualizovat hodnotu ve spolehlivé kolekci, je získat odkaz na existující hodnotu a zvážit objekt odkazující na tento odkaz neměnný. Pak vytvořte nový objekt, který je přesnou kopií původního objektu. Nyní můžete změnit stav tohoto nového objektu a zapsat nový objekt do kolekce tak, aby se serializoval na pole bajtů, připojili k místnímu souboru a odeslali do replik. Po potvrzení změn mají objekty v paměti, místní soubor a všechny repliky stejný přesný stav. Všechno je dobré!

Následující kód ukazuje správný způsob aktualizace hodnoty ve spolehlivé kolekci:

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

Definování neměnných datových typů, aby se zabránilo chybě programátora

V ideálním případě bychom chtěli, aby kompilátor hlásil chyby, když omylem vytváříte kód, který ztlumí stav objektu, který byste měli považovat za neměnný. Kompilátor jazyka C# ale nemá možnost to udělat. Abyste se vyhnuli potenciálním chybám programátorů, důrazně doporučujeme definovat typy, které používáte se spolehlivými kolekcemi, aby byly neměnné typy. Konkrétně to znamená, že se budete držet typů základních hodnot (například čísla [Int32, UInt64 atd.], DateTime, Guid, TimeSpan a podobně). Můžete také použít řetězec. Nejlepší je vyhnout se vlastnostem kolekce jako serializace a deserializace může často poškodit výkon. Pokud však chcete použít vlastnosti kolekce, důrazně doporučujeme použít . Neměnná knihovna kolekcí net (System.Collections.Immutable). Tato knihovna je k dispozici ke stažení z https://nuget.org. Doporučujeme také zapečetění tříd a vytváření polí jen pro čtení, kdykoli je to možné.

Níže uvedený typ UserInfo ukazuje, jak definovat neměnný typ s využitím výše uvedených doporučení.

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

Typ ItemId je také neměnný typ, jak je znázorněno zde:

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

Správa verzí schématu (upgrady)

Interně Reliable Collections serializují vaše objekty pomocí . Net DataContractSerializer. Serializované objekty se uchovávají na místním disku primární repliky a také se přenášejí do sekundárních replik. Vzhledem k tomu, že vaše služba zralá, je pravděpodobné, že budete chtít změnit typ dat (schématu), který vaše služba vyžaduje. Přistupujte ke správě verzí dat s velkou opatrností. Za prvé, musíte být vždy schopni deserializovat stará data. Konkrétně to znamená, že váš deserializační kód musí být nekonečně zpětně kompatibilní: Verze 333 kódu služby musí být schopna pracovat s daty umístěnými ve spolehlivé kolekci podle verze 1 kódu služby před 5 lety.

Kód služby se navíc upgraduje po jedné upgradované doméně. Během upgradu tedy máte spuštěné dvě různé verze kódu služby současně. Musíte se vyhnout tomu, aby nová verze kódu služby používala nové schéma jako staré verze kódu služby nemusí být schopná zpracovat nové schéma. Pokud je to možné, měli byste navrhnout každou verzi služby tak, aby byla kompatibilní s jednou verzí. Konkrétně to znamená, že kód služby V1 by měl být schopný ignorovat všechny prvky schématu, které explicitně nezpracuje. Při aktualizaci klíče nebo hodnoty slovníku ale musí být schopná uložit žádná data, o kterých explicitně neví, a zapsat je zpět.

Upozorňující

I když můžete změnit schéma klíče, musíte zajistit, aby byly algoritmy rovnosti a porovnání klíče stabilní. Chování spolehlivých kolekcí po změně některého z těchto algoritmů není definováno a může vést k poškození dat, ztrátě a selhání služby. Řetězce .NET lze použít jako klíč, ale jako klíč použít samotný řetězec – nepoužívejte jako klíč výsledek String.GetHashCode.

Alternativně můžete provést vícefázový upgrade.

  1. Upgrade služby na novou verzi, která
    • má původní verzi V1 i novou verzi kontraktů dat V2, která je součástí balíčku kódu služby;
    • v případě potřeby zaregistruje vlastní serializátory stavu V2;
    • provádí všechny operace s původní kolekcí V1 pomocí kontraktů dat V1.
  2. Upgrade služby na novou verzi, která
    • vytvoří novou kolekci V2;
    • provádí každou operaci přidání, aktualizace a odstranění na první V1 a potom kolekce V2 v jedné transakci;
    • provádí operace čtení pouze v kolekci V1.
  3. Zkopírujte všechna data z kolekce V1 do kolekce V2.
    • To lze provést v procesu na pozadí podle verze služby nasazené v kroku 2.
    • Znovu vyřešte všechny klíče z kolekce V1. Výčet se provádí s IsolationLevel.Snapshot ve výchozím nastavení, aby se zabránilo uzamčení kolekce po dobu trvání operace.
    • Pro každý klíč použijte samostatnou transakci pro
      • TryGetValueAsync z kolekce V1.
      • Pokud se hodnota už od spuštění procesu kopírování odebrala z kolekce V1, měl by se klíč přeskočit a v kolekci V2 se nepřečte znovu.
      • TryAddAsync hodnota do kolekce V2.
      • Pokud už byla hodnota přidána do kolekce V2 od spuštění procesu kopírování, klíč by se měl přeskočit.
      • Transakce by měla být potvrzena pouze v případě, že TryAddAsync vrátí true.
      • Rozhraní API pro přístup k hodnotám ve výchozím nastavení používají IsolationLevel.ReadRepeatable a spoléhají na uzamčení, aby se zajistilo, že hodnoty nebudou změněny jiným volajícím, dokud transakce nebude potvrzena nebo přerušena.
  4. Upgrade služby na novou verzi, která
    • provádí operace čtení pouze v kolekci V2;
    • stále provádí každou operaci přidání, aktualizace a odstranění na první verzi V1 a potom kolekce V2, aby se zachovala možnost vrácení zpět na V1.
  5. Komplexně otestujte službu a ověřte, že funguje podle očekávání.
    • Pokud jste zmeškali jakoukoli operaci přístupu k hodnotě, která nebyla aktualizována tak, aby fungovala v kolekci V1 i V2, můžete si všimnout chybějících dat.
    • Pokud některá data chybí, vraťte se ke kroku 1, odeberte kolekci V2 a opakujte proces.
  6. Upgrade služby na novou verzi, která
    • provádí všechny operace pouze v kolekci V2;
    • návrat zpět na V1 už není možné se vrácením služby zpět a vyžadovalo by se vrácení zpět s obrácenými kroky 2 až 4.
  7. Upgrade služby na novou verzi, která
  8. Počkejte na zkrácení protokolu.
    • Ve výchozím nastavení k tomu dochází každých 50 MB zápisů (přidává, aktualizuje a odebírá) do spolehlivých kolekcí.
  9. Upgrade služby na novou verzi, která
    • datové kontrakty V1 už nejsou zahrnuté v balíčku kódu služby.

Další kroky

Další informace o vytváření dopředných kompatibilních datových kontraktů najdete v tématu Předávání kompatibilních datových kontraktů.

Informace o osvědčených postupech při správě verzí datových kontraktů najdete v tématu Správa verzí kontraktů dat.

Informace o implementaci kontraktů dat odolných proti verzím najdete v tématu Zpětné volání serializace odolné proti verzím.

Informace o tom, jak poskytnout datovou strukturu, která může spolupracovat napříč několika verzemi, najdete v tématu IExtensibleDataObject.

Informace o konfiguraci spolehlivých kolekcí najdete v tématu Konfigurace replikátoru.