Praca z elementami Reliable Collections

Usługa Service Fabric oferuje stanowy model programowania dostępny dla deweloperów platformy .NET za pośrednictwem kolekcji Reliable Collections. W szczególności usługa Service Fabric udostępnia niezawodny słownik i niezawodne klasy kolejek. W przypadku używania tych klas stan jest partycjonowany (w celu skalowalności), zreplikowany (dla dostępności) i transakcyjny w ramach partycji (dla semantyki ACID). Przyjrzyjmy się typowego użycia niezawodnego obiektu słownika i zobaczmy, co faktycznie robi.

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

Wszystkie operacje na obiektach niezawodnego słownika (z wyjątkiem funkcji ClearAsync, która nie jest niemożliwa do cofnięcia), wymagają obiektu ITransaction. Ten obiekt skojarzył z nim wszystkie zmiany, które próbujesz wprowadzić do dowolnego niezawodnego słownika i/lub niezawodnych obiektów kolejki w ramach jednej partycji. Obiekt ITransaction uzyskuje się przez wywołanie metody CreateTransaction klasy StateManager partycji.

W powyższym kodzie obiekt ITransaction jest przekazywany do metody AddAsync niezawodnego słownika. Wewnętrznie metody słownika, które akceptują klucz, przyjmują blokadę czytnika/zapisywania skojarzona z kluczem. Jeśli metoda modyfikuje wartość klucza, metoda przyjmuje blokadę zapisu na kluczu i jeśli metoda odczytuje tylko z wartości klucza, blokada odczytu jest wykonywana na kluczu. Ponieważ funkcja AddAsync modyfikuje wartość klucza na nową, przekazaną wartość, zostanie podjęta blokada zapisu klucza. Dlatego jeśli 2 (lub więcej) wątków próbuje dodać wartości z tym samym kluczem w tym samym czasie, jeden wątek uzyska blokadę zapisu, a pozostałe wątki będą blokowane. Domyślnie metody blokują maksymalnie 4 sekundy na uzyskanie blokady; po 4 sekundach metody zgłaszają wyjątek TimeoutException. Istnieją przeciążenia metody umożliwiające przekazanie jawnej wartości limitu czasu, jeśli wolisz.

Zazwyczaj kod jest pisany w celu reagowania na wyjątek TimeoutException przez przechwycenie go i ponowienie próby wykonania całej operacji (jak pokazano w powyższym kodzie). W tym prostym kodzie po prostu wywołujemy metodę Task.Delay przekazując 100 milisekund za każdym razem. Ale w rzeczywistości możesz lepiej użyć pewnego rodzaju opóźnienia wykładniczego wycofywania zamiast tego.

Po uzyskaniu blokady funkcja AddAsync dodaje odwołania do obiektu klucza i wartości do wewnętrznego słownika tymczasowego skojarzonego z obiektem ITransaction. Jest to wykonywane w celu zapewnienia semantyki read-your-own-writes. Oznacza to, że po wywołaniu metody AddAsync późniejsze wywołanie metody TryGetValueAsync przy użyciu tego samego obiektu ITransaction zwróci wartość, nawet jeśli transakcja nie zostanie jeszcze zatwierdzona.

Uwaga

Wywołanie metody TryGetValueAsync z nową transakcją zwróci odwołanie do ostatniej zatwierdzonej wartości. Nie należy bezpośrednio modyfikować tego odwołania, ponieważ pomija mechanizm utrwalania i replikowania zmian. Zalecamy stosowanie wartości tylko do odczytu, aby jedynym sposobem zmiany wartości klucza było użycie niezawodnych interfejsów API słownika.

Następnie funkcja AddAsync serializuje obiekty klucza i wartości do tablic bajtów i dołącza te tablice bajtów do pliku dziennika w węźle lokalnym. Na koniec funkcja AddAsync wysyła tablice bajtów do wszystkich replik pomocniczych, aby miały te same informacje o klucz/wartość. Mimo że informacje o kluczu/wartości zostały zapisane w pliku dziennika, informacje nie są uznawane za część słownika, dopóki transakcja, z którą są skojarzone, została zatwierdzona.

W powyższym kodzie wywołanie commitAsync zatwierdza wszystkie operacje transakcji. W szczególności dołącza informacje zatwierdzenia do pliku dziennika w węźle lokalnym, a także wysyła rekord zatwierdzenia do wszystkich replik pomocniczych. Po odpowiedzi kworum (większość) replik wszystkie zmiany danych są uznawane za trwałe i wszelkie blokady skojarzone z kluczami, które zostały manipulowane za pośrednictwem obiektu ITransaction, są zwalniane, aby inne wątki/transakcje mogły manipulować tymi samymi kluczami i ich wartościami.

Jeśli funkcja CommitAsync nie jest wywoływana (zwykle z powodu zgłoszenia wyjątku), obiekt ITransaction zostanie usunięty. W przypadku usunięcia niezatwierdzonego obiektu ITransaction usługa Service Fabric dołącza informacje przerwania do pliku dziennika węzła lokalnego i nic nie musi być wysyłane do żadnej z replik pomocniczych. Następnie wszystkie blokady skojarzone z kluczami, które zostały manipulowane za pośrednictwem transakcji, są zwalniane.

Nietrwałe niezawodne kolekcje

W niektórych obciążeniach, takich jak replikowana pamięć podręczna, na przykład można tolerować od czasu do czasu utratę danych. Unikanie trwałości danych na dysku może zapewnić lepsze opóźnienia i przepływność podczas zapisywania w słownikach Reliable. Kompromisem w przypadku braku trwałości jest to, że w przypadku utraty kworum nastąpi pełna utrata danych. Ponieważ utrata kworum jest rzadkim wystąpieniem, zwiększona wydajność może być warta rzadkiej możliwości utraty danych dla tych obciążeń.

Obecnie obsługa volatile jest dostępna tylko dla niezawodnych słowników i niezawodnych kolejek, a nie ReliableConcurrentQueues. Zapoznaj się z listą zastrzeżeń , aby poinformować Użytkownika o tym, czy używać kolekcji volatile.

Aby włączyć nietrwałą obsługę w usłudze, ustaw flagę HasPersistedState w deklaracji typu usługi na false, w następujący sposób:

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

Uwaga

Istniejące utrwalone usługi nie mogą być nietrwałe i odwrotnie. Jeśli chcesz to zrobić, musisz usunąć istniejącą usługę, a następnie wdrożyć usługę ze zaktualizowaną flagą. Oznacza to, że musisz być skłonny do ponoszenia pełnej utraty danych, jeśli chcesz zmienić flagę HasPersistedState .

Typowe pułapki i sposoby ich unikania

Teraz, gdy już wiesz, jak niezawodne kolekcje działają wewnętrznie, przyjrzyjmy się niektórym typowym nadużyciom. Zobacz poniższy kod:

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

Podczas pracy ze zwykłym słownikiem .NET można dodać klucz/wartość do słownika, a następnie zmienić wartość właściwości (np. LastLogin). Jednak ten kod nie będzie działał poprawnie w niezawodnym słowniku. Pamiętaj, że we wcześniejszej dyskusji wywołanie metody AddAsync serializuje obiekty klucza/wartości do tablic bajtów, a następnie zapisuje tablice do pliku lokalnego, a także wysyła je do replik pomocniczych. Jeśli później zmienisz właściwość, spowoduje to zmianę wartości właściwości tylko w pamięci; nie ma to wpływu na plik lokalny ani dane wysyłane do replik. Jeśli proces ulegnie awarii, zawartość pamięci zostanie wyrzucona. Gdy nowy proces zostanie uruchomiony lub gdy inna replika stanie się podstawowa, stara wartość właściwości jest dostępna.

Nie mogę podkreślić wystarczająco łatwo, jak łatwo jest popełnić rodzaj błędu pokazanego powyżej. I dowiesz się tylko o błędzie, jeśli/gdy proces ulegnie awarii. Poprawnym sposobem na napisanie kodu jest po prostu odwrócenie dwóch wierszy:

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

Oto kolejny przykład pokazujący typowy błąd:

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

Ponownie, w przypadku zwykłych słowników platformy .NET powyższy kod działa poprawnie i jest typowym wzorcem: deweloper używa klucza do wyszukiwania wartości. Jeśli wartość istnieje, deweloper zmieni wartość właściwości. Jednak w przypadku niezawodnych kolekcji ten kod wykazuje ten sam problem, jak już omówiony: nie można modyfikować obiektu po podaniu go do niezawodnej kolekcji.

Prawidłowym sposobem aktualizacji wartości w niezawodnej kolekcji jest uzyskanie odwołania do istniejącej wartości i rozważenie obiektu, do których odwołuje się to odwołanie niezmienne. Następnie utwórz nowy obiekt, który jest dokładną kopią oryginalnego obiektu. Teraz można zmodyfikować stan tego nowego obiektu i zapisać nowy obiekt w kolekcji, tak aby był serializowany do tablic bajtów, dołączany do pliku lokalnego i wysyłany do replik. Po zatwierdzeniu zmian obiekty w pamięci, plik lokalny i wszystkie repliki mają dokładnie taki sam stan. Wszystko jest dobre!

Poniższy kod przedstawia prawidłowy sposób aktualizowania wartości w niezawodnej kolekcji:

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

Definiowanie niezmiennych typów danych w celu zapobiegania błędowi programisty

Najlepiej, aby kompilator zgłaszał błędy, gdy przypadkowo utworzysz kod, który wycisza stan obiektu, który ma być niezmienny. Jednak kompilator języka C# nie ma możliwości wykonania tej czynności. Dlatego aby uniknąć potencjalnych usterek programistów, zdecydowanie zalecamy zdefiniowanie typów używanych z niezawodnymi kolekcjami jako niezmiennymi typami. W szczególności oznacza to, że trzymasz się podstawowych typów wartości (takich jak liczby [Int32, UInt64 itp.], DateTime, Guid, TimeSpan i podobne). Możesz również użyć ciągu. Najlepiej unikać właściwości kolekcji jako serializacji i deserializacji ich często może zaszkodzić wydajności. Jeśli jednak chcesz użyć właściwości kolekcji, zdecydowanie zalecamy użycie elementu . Niezmienna biblioteka kolekcji platformy NET (System.Collections.Immutable). Ta biblioteka jest dostępna do pobrania z witryny https://nuget.org. Zalecamy również zapieczętowanie klas i tworzenie pól tylko do odczytu, gdy jest to możliwe.

Poniższy typ UserInfo pokazuje sposób definiowania niezmiennego typu korzystającego z wyżej wymienionych zaleceń.

[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 jest również niezmiennym typem, jak pokazano tutaj:

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

Przechowywanie wersji schematu (uaktualnienia)

Wewnętrznie kolekcje Reliable Collections serializują obiekty przy użyciu metody . DataContractSerializer platformy NET. Serializowane obiekty są utrwalane na dysku lokalnym repliki podstawowej i są również przesyłane do replik pomocniczych. W miarę dojrzewania usługi prawdopodobnie zechcesz zmienić rodzaj danych (schemat) wymagany przez usługę. Podejście do przechowywania wersji danych z wielką starannością. Przede wszystkim zawsze musisz mieć możliwość deserializacji starych danych. W szczególności oznacza to, że kod deserializacji musi być nieskończenie zgodny z poprzednimi wersjami: wersja 333 kodu usługi musi być w stanie działać na danych umieszczonych w niezawodnej kolekcji w wersji 1 kodu usługi 5 lat temu.

Ponadto kod usługi jest uaktualniany po jednej domenie uaktualniania jednocześnie. Dlatego podczas uaktualniania masz dwie różne wersje kodu usługi uruchomionego jednocześnie. Należy unikać używania nowej wersji kodu usługi, ponieważ stare wersje kodu usługi mogą nie być w stanie obsłużyć nowego schematu. Jeśli to możliwe, należy zaprojektować każdą wersję usługi, aby przekazywać dane zgodne z jedną wersją. W szczególności oznacza to, że wersja 1 kodu usługi powinna być w stanie zignorować wszystkie elementy schematu, które nie są jawnie obsługiwane. Jednak musi być w stanie zapisać wszystkie dane, o których nie wiadomo jawnie i zapisać je z powrotem podczas aktualizowania klucza słownika lub wartości.

Ostrzeżenie

Chociaż można zmodyfikować schemat klucza, należy upewnić się, że algorytmy równości i porównania klucza są stabilne. Zachowanie niezawodnych kolekcji po zmianie jednego z tych algorytmów jest niezdefiniowane i może prowadzić do uszkodzenia, utraty i awarii usługi danych. Ciągi .NET mogą być używane jako klucz, ale użyj samego ciągu jako klucza — nie należy używać wyniku string.GetHashCode jako klucza.

Alternatywnie można przeprowadzić uaktualnienie wielofazowe.

  1. Uaktualnianie usługi do nowej wersji
    • ma zarówno oryginalną wersję 1, jak i nową wersję 2 kontraktów danych zawartych w pakiecie kodu usługi;
    • rejestruje niestandardowe serializatory stanu V2, w razie potrzeby;
    • wykonuje wszystkie operacje na oryginalnej kolekcji w wersji 1 przy użyciu kontraktów danych w wersji 1.
  2. Uaktualnianie usługi do nowej wersji
    • tworzy nową kolekcję w wersji 2;
    • Wykonuje każdą operację dodawania, aktualizowania i usuwania w pierwszej wersji 1, a następnie kolekcji V2 w jednej transakcji;
    • wykonuje operacje odczytu tylko w kolekcji V1.
  3. Skopiuj wszystkie dane z kolekcji V1 do kolekcji w wersji 2.
    • Można to zrobić w procesie w tle przez wersję usługi wdrożoną w kroku 2.
    • Odzyskaj wszystkie klucze z kolekcji V1. Wyliczenie jest wykonywane z elementem IsolationLevel.Snapshot domyślnie, aby uniknąć blokowania kolekcji przez czas trwania operacji.
    • Dla każdego klucza użyj oddzielnej transakcji do
      • TryGetValueAsync z kolekcji V1.
      • Jeśli wartość została już usunięta z kolekcji w wersji 1 od rozpoczęcia procesu kopiowania, klucz powinien zostać pominięty i nie zostanie zachowany w kolekcji w wersji 2.
      • TryAddAsync wartość kolekcji w wersji 2.
      • Jeśli wartość została już dodana do kolekcji w wersji 2 od rozpoczęcia procesu kopiowania, należy pominąć klucz.
      • Transakcja powinna zostać zatwierdzona TryAddAsync tylko wtedy, gdy zwraca wartość true.
      • Interfejsy API dostępu do wartości używają metody IsolationLevel.ReadRepeatable domyślnie i polegają na zablokowaniu, aby zagwarantować, że wartości nie zostaną zmodyfikowane przez inny obiekt wywołujący do momentu zatwierdzenia lub przerwania transakcji.
  4. Uaktualnianie usługi do nowej wersji
    • wykonuje operacje odczytu tylko w kolekcji w wersji 2;
    • nadal wykonuje każdą operację dodawania, aktualizowania i usuwania w pierwszej wersji 1, a następnie kolekcji w wersji 2, aby zachować opcję wycofywania do wersji 1.
  5. Kompleksowo przetestuj usługę i upewnij się, że działa zgodnie z oczekiwaniami.
    • Jeśli pominięto każdą operację dostępu do wartości, która nie została zaktualizowana w celu działania zarówno w kolekcji W wersji 1, jak i V2, możesz zauważyć brakujące dane.
    • Jeśli brakuje jakichkolwiek danych, wróć do kroku 1, usuń kolekcję w wersji 2 i powtórz proces.
  6. Uaktualnianie usługi do nowej wersji
    • wykonuje wszystkie operacje tylko w kolekcji w wersji 2;
    • powrót do wersji 1 nie jest już możliwy z wycofywaniem usługi i wymagałby wycofywania z powrotem z odwróconymi krokami 2–4.
  7. Uaktualnianie usługi nowej wersji, która
  8. Poczekaj na obcięcie dziennika.
    • Domyślnie dzieje się to co 50 MB zapisów (dodaje, aktualizuje i usuwa) do niezawodnych kolekcji.
  9. Uaktualnianie usługi do nowej wersji
    • nie ma już kontraktów danych w wersji 1 zawartych w pakiecie kodu usługi.

Następne kroki

Aby dowiedzieć się więcej o tworzeniu zgodnych kontraktów danych, zobacz Kontrakty danych zgodne z przekazywaniem

Aby dowiedzieć się więcej na temat najlepszych rozwiązań dotyczących kontraktów danych dotyczących wersji, zobacz Przechowywanie wersji kontraktu danych

Aby dowiedzieć się, jak zaimplementować kontrakty danych odporne na wersje, zobacz Wywołania zwrotne serializacji odporne na wersje

Aby dowiedzieć się, jak zapewnić strukturę danych, która może współdziałać w wielu wersjach, zobacz IExtensibleDataObject

Aby dowiedzieć się, jak skonfigurować niezawodne kolekcje, zobacz Konfiguracja replikatora