Condividi tramite


Lavorare con Reliable Collections

Service Fabric offre un modello di programmazione con stato disponibile per gli sviluppatori .NET tramite Reliable Collections. In particolare, Service Fabric fornisce classi reliable dictionary e reliable queue. Quando si usano queste classi, lo stato viene partizionato (per la scalabilità), replicato (per la disponibilità) e transazionato all'interno di una partizione (per la semantica ACID). Esaminiamo un uso tipico di un oggetto dizionario affidabile e vediamo cosa sta effettivamente facendo.

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

Tutte le operazioni sugli oggetti reliable dictionary (ad eccezione di ClearAsync, che non è annullabile), richiedono un oggetto ITransaction. Questo oggetto è associato a tutte le modifiche che si sta cercando di apportare a qualsiasi dizionario affidabile e/o oggetti di coda affidabile all'interno di una singola partizione. Si acquisisce un oggetto ITransaction chiamando il metodo CreateTransaction del StateManager della partizione.

Nel codice precedente, l'oggetto ITransaction viene passato al metodo AddAsync di un dizionario affidabile. Internamente, i metodi del dizionario che accettano una chiave acquisiscono un blocco lettore/scrittore associato alla chiave. Se il metodo modifica il valore della chiave, il metodo accetta un blocco di scrittura sulla chiave e se il metodo legge solo dal valore della chiave, viene acquisito un blocco di lettura sulla chiave. Poiché AddAsync modifica il valore della chiave nel nuovo valore passato, viene acquisito il blocco di scrittura della chiave. Pertanto, se 2 (o più thread) tentano di aggiungere valori con la stessa chiave contemporaneamente, un thread acquisirà il blocco di scrittura e gli altri thread verranno bloccati. Per impostazione predefinita, i metodi bloccano fino a 4 secondi per acquisire il blocco; dopo 4 secondi, i metodi generano un'eccezione TimeoutException. Esistono sovraccarichi di metodi che consentono di passare un valore di timeout esplicito, se lo si preferisce.

In genere, si scrive il codice per reagire a un'eccezione TimeoutException intercettandola e ritentando l'intera operazione (come illustrato nel codice precedente). In questo codice semplice, stiamo semplicemente chiamando Task.Delay, passando ogni volta 100 millisecondi. Tuttavia, in realtà, potrebbe essere preferibile utilizzare un ritardo esponenziale di back-off.

Dopo aver acquisito il blocco, AddAsync aggiunge i riferimenti all'oggetto chiave e valore a un dizionario temporaneo interno associato all'oggetto ITransaction. Questa operazione viene eseguita per fornire la semantica di lettura delle proprie scritture (read-your-own-writes). Ovvero, dopo aver chiamato AddAsync, una chiamata successiva a TryGetValueAsync usando lo stesso oggetto ITransaction restituirà il valore anche se non è ancora stato eseguito il commit della transazione.

Annotazioni

La chiamata a TryGetValueAsync con una nuova transazione restituirà un riferimento all'ultimo valore confermato. Non modificare direttamente il riferimento, in quanto ignora il meccanismo per rendere persistenti e replicare le modifiche. È consigliabile rendere i valori di sola lettura in modo che l'unico modo per modificare il valore di una chiave sia tramite API reliable dictionary.

AddAsync serializza quindi gli oggetti chiave e valore nelle matrici di byte e accoda queste matrici di byte a un file di log nel nodo locale. Infine, AddAsync invia le matrici di byte a tutte le repliche secondarie in modo che abbiano le stesse informazioni chiave/valore. Anche se le informazioni chiave/valore sono state scritte in un file di log, le informazioni non vengono considerate parte del dizionario finché non viene eseguito il commit della transazione a cui sono associate.

Nel codice precedente la chiamata a CommitAsync esegue il commit di tutte le operazioni della transazione. In particolare, aggiunge le informazioni di commit al file di log nel nodo locale e invia il record di commit a tutte le repliche secondarie. Dopo aver risposto un quorum (maggioranza) delle repliche, tutte le modifiche ai dati vengono considerate permanenti e tutti i blocchi associati alle chiavi modificate tramite l'oggetto ITransaction vengono rilasciati in modo che altri thread/transazioni possano modificare le stesse chiavi e i relativi valori.

Se CommitAsync non viene chiamato (in genere a causa di un'eccezione generata), l'oggetto ITransaction viene eliminato. Quando si elimina un oggetto ITransaction di cui non è stato eseguito il commit, Service Fabric aggiunge le informazioni di interruzione al file di log del nodo locale e non deve essere inviato alcun elemento a una delle repliche secondarie. Vengono quindi rilasciati tutti i blocchi associati alle chiavi modificate tramite la transazione.

Raccolte volatili affidabili

In alcuni carichi di lavoro, ad esempio una cache replicata, è possibile tollerare occasionalmente la perdita di dati. Evitare la persistenza dei dati su disco può consentire latenze e velocità effettiva migliori durante la scrittura in Reliable Dictionaries. Il compromesso per una mancanza di persistenza è che, se si verifica una perdita di quorum, si verificherà una perdita completa di dati. Poiché la perdita del quorum è una rara occorrenza, l'aumento delle prestazioni può essere utile per la rara possibilità di perdita di dati per tali carichi di lavoro.

Attualmente, il supporto volatile è disponibile solo per Reliable Dictionaries e Reliable Queues e non per ReliableConcurrentQueues. Vedere l'elenco delle avvertenze per decidere se utilizzare raccolte volatili.

Per abilitare il supporto volatile nel servizio, impostare il HasPersistedState flag nella dichiarazione del tipo di servizio su false, come indicato di seguito:

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

Annotazioni

I servizi persistenti esistenti non possono essere resi volatili e viceversa. Se si vuole farlo, sarà necessario eliminare il servizio esistente e quindi distribuire il servizio con il flag aggiornato. Ciò significa che è necessario essere disposti a causare una perdita completa di dati se si vuole modificare il HasPersistedState flag.

Insidie comuni e come evitarle

Ora che si capisce come funzionano internamente le raccolte Reliable Collections, si esaminerà alcuni usi impropri comuni di tali raccolte. Vedere il codice seguente:

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

Quando si usa un normale dizionario .NET, è possibile aggiungere una chiave/valore al dizionario e quindi modificare il valore di una proprietà , ad esempio LastLogin. Tuttavia, questo codice non funzionerà correttamente con un dizionario affidabile. Ricordarsi dalla discussione precedente, la chiamata a AddAsync serializza gli oggetti chiave/valore in matrici di byte e quindi salva le matrici in un file locale e le invia anche alle repliche secondarie. Se in seguito si modifica una proprietà, il valore della proprietà viene modificato solo in memoria; non influisce sul file locale o sui dati inviati alle repliche. Se il processo si arresta in modo anomalo, il contenuto della memoria viene scartato. Quando viene avviato un nuovo processo o se un'altra replica diventa primaria, il valore della proprietà precedente è quello disponibile.

Non posso sottolineare abbastanza quanto sia facile fare il tipo di errore mostrato sopra. E, si apprenderà solo l'errore se/quando il processo diventa inattivo. Il modo corretto per scrivere il codice consiste semplicemente nell'invertire le due righe:

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

Ecco un altro esempio che mostra un errore comune:

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

Anche in questo caso, con i normali dizionari .NET, il codice precedente funziona correttamente ed è un modello comune: lo sviluppatore usa una chiave per cercare un valore. Se il valore esiste, lo sviluppatore modifica il valore di una proprietà. Tuttavia, con raccolte affidabili, questo codice presenta lo stesso problema già descritto: non è necessario modificare un oggetto dopo averlo dato a una raccolta affidabile.

Il modo corretto per aggiornare un valore in una raccolta reliable consiste nel ottenere un riferimento al valore esistente e considerare l'oggetto a cui fa riferimento non modificabile. Creare quindi un nuovo oggetto che rappresenta una copia esatta dell'oggetto originale. A questo punto, è possibile modificare lo stato di questo nuovo oggetto e scrivere il nuovo oggetto nell'insieme in modo che venga serializzato in matrici di byte, aggiunto al file locale e inviato alle repliche. Dopo aver confermato le modifiche, gli oggetti in memoria, il file locale e tutte le repliche hanno esattamente lo stesso stato. Tutto è buono!

Il codice seguente illustra il modo corretto per aggiornare un valore in una raccolta Reliable Collection:

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

Definire tipi di dati non modificabili per evitare errori programmatori

Idealmente, il compilatore dovrebbe segnalare errori quando si produce accidentalmente codice che modifica lo stato di un oggetto che si suppone di considerare non modificabile. Tuttavia, il compilatore C# non ha la possibilità di eseguire questa operazione. Per evitare potenziali bug programmatori, è quindi consigliabile definire i tipi usati con raccolte affidabili per essere tipi non modificabili. In particolare, ciò significa che ci si attacca ai tipi di valore principale (ad esempio numeri [Int32, UInt64 e così via], DateTime, Guid, TimeSpan e simili. È anche possibile usare String. È consigliabile evitare le proprietà della raccolta perché la serializzazione e la deserializzazione possono spesso compromettere le prestazioni. Tuttavia, se si desidera utilizzare le proprietà della raccolta, consigliamo vivamente l'uso della libreria delle raccolte immutabili di .NET (System.Collections.Immutable). Questa libreria è disponibile per il download da https://nuget.org. Raccomandiamo inoltre di sigillare le classi e di rendere i campi di sola lettura quando possibile.

Il tipo UserInfo riportato di seguito illustra come definire un tipo non modificabile sfruttando i consigli indicati in precedenza.

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

Il tipo ItemId è anche un tipo non modificabile, come illustrato di seguito:

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

Controllo delle versioni dello schema (aggiornamenti)

Internamente, Reliable Collections serializza gli oggetti usando il DataContractSerializer di .NET. Gli oggetti serializzati vengono salvati in modo permanente nel disco locale della replica primaria e vengono trasmessi anche alle repliche secondarie. Con la maturità del servizio, è probabile che si voglia modificare il tipo di dati (schema) richiesto dal servizio. Affronta la gestione delle versioni dei tuoi dati con grande attenzione. Prima di tutto, è necessario essere sempre in grado di deserializzare i dati obsoleti. In particolare, questo significa che il codice di deserializzazione deve essere infinitamente compatibile con le versioni precedenti: la versione 333 del codice del servizio deve essere in grado di operare sui dati inseriti in una raccolta affidabile alla versione 1 del codice di servizio 5 anni fa.

Inoltre, il codice del servizio viene aggiornato un dominio di aggiornamento alla volta. Quindi, durante un aggiornamento, si hanno due versioni diverse del codice del servizio in esecuzione contemporaneamente. È necessario evitare che la nuova versione del codice del servizio usi il nuovo schema perché le versioni precedenti del codice del servizio potrebbero non essere in grado di gestire il nuovo schema. Quando possibile, è consigliabile progettare ogni versione del servizio in modo che sia compatibile con una sola versione. In particolare, ciò significa che la versione 1 del codice del servizio deve essere in grado di ignorare tutti gli elementi dello schema che non gestisce in modo esplicito. Tuttavia, deve essere in grado di salvare qualsiasi dato di cui non ha conoscenza esplicita e ripristinarlo quando si aggiorna una chiave o un valore del dizionario.

Avvertimento

Anche se è possibile modificare lo schema di una chiave, è necessario assicurarsi che gli algoritmi di uguaglianza e confronto della chiave siano stabili. Il comportamento delle raccolte affidabili dopo una modifica di uno di questi algoritmi non è definito e può causare danneggiamento dei dati, perdita e arresto anomalo del servizio. Le stringhe .NET possono essere usate come chiave, ma usano la stringa stessa come chiave. Non usare il risultato di String.GetHashCode come chiave.

In alternativa, è possibile eseguire un aggiornamento a più fasi.

  1. Aggiornare il servizio a una nuova versione
    • ha sia la versione V1 originale che la nuova versione V2 dei contratti dati inclusi nel pacchetto del codice del servizio;
    • registra serializzatori di stato V2 personalizzati, se necessario;
    • esegue tutte le operazioni sulla raccolta originale V1 usando i contratti dati V1.
  2. Aggiornare il servizio a una nuova versione
    • crea una nuova raccolta V2;
    • esegue ogni operazione di aggiunta, aggiornamento ed eliminazione sulla prima V1 e quindi sulle raccolte V2 in una singola transazione;
    • esegue operazioni di lettura solo nella raccolta V1.
  3. Copiare tutti i dati dalla raccolta V1 alla raccolta V2.
    • Questa operazione può essere eseguita in un processo in background dalla versione del servizio distribuita nel passaggio 2.
    • Recupera tutte le chiavi dalla raccolta V1. L'enumerazione viene eseguita con IsolationLevel.Snapshot per impostazione predefinita per evitare di bloccare la raccolta per la durata dell'operazione.
    • Per ogni chiave, usare una transazione separata per
      • TryGetValueAsync dalla raccolta V1.
      • Se il valore è già stato rimosso dall'insieme V1 dall'avvio del processo di copia, la chiave deve essere ignorata e non ripresa nell'insieme V2.
      • TryAddAsync il valore dell'insieme V2.
      • Se l'elemento è già stato aggiunto alla collezione V2 dall'avvio del processo di copia, la chiave deve essere ignorata.
      • Il commit della transazione deve essere eseguito solo se restituisce TryAddAsynctrue.
      • Le API di accesso ai valori usano IsolationLevel.ReadRepeatable per impostazione predefinita e si basano sul blocco per garantire che i valori non vengano modificati da un altro chiamante finché non viene eseguito il commit o l'interruzione della transazione.
  4. Aggiornare il servizio a una nuova versione
    • esegue operazioni di lettura solo sulla raccolta V2;
    • esegue comunque ogni operazione di aggiunta, aggiornamento ed eliminazione nella prima V1 e quindi nelle raccolte V2 per mantenere l'opzione di rollback alla versione 1.
  5. Testare il servizio in modo completo e verificare che funzioni come previsto.
    • Se non è stata eseguita alcuna operazione di accesso ai valori che non è stata aggiornata per funzionare sia nella raccolta V1 che nella versione 2, si potrebbero notare dati mancanti.
    • Se mancano dati, eseguire il rollback al passaggio 1, rimuovere la raccolta V2 e ripetere il processo.
  6. Aggiornare il servizio a una nuova versione
    • esegue tutte le operazioni solo sulla raccolta V2;
    • Tornare alla versione 1 non è più possibile con un rollback del servizio e richiederebbe proseguire seguendo i passaggi invertiti da 2 a 4.
  7. Aggiornare il servizio a una nuova versione che
  8. Attendere il troncamento del log.
    • Per impostazione predefinita, questo avviene ogni 50 MB di scritture (aggiunge, aggiorna e rimuove) a raccolte affidabili.
  9. Aggiornare il servizio a una nuova versione
    • non include più i contratti dati V1 inclusi nel pacchetto di codice del servizio.

Passaggi successivi

Per saperne di più sulla creazione di contratti di dati compatibili con le versioni future, vedere Forward-Compatible Contratti di dati

Per informazioni sulle procedure consigliate per il controllo delle versioni dei contratti dati, vedere Controllo delle versioni del contratto dati

Per informazioni su come implementare contratti dati a tolleranza di versione, vedere Version-Tolerant Callback di serializzazione

Per informazioni su come fornire una struttura di dati che può interagire tra più versioni, vedere IExtensibleDataObject

Per informazioni su come configurare Reliable Collections, consultare Configurazione del Replicator