Поделиться через


Работа с надежными коллекциями

Service Fabric предлагает модель программирования с отслеживанием состояния, доступную разработчикам .NET через Надежные коллекции. В частности, Service Fabric предоставляет классы надежного словаря и надежной очереди. При использовании этих классов состояние секционируется (для масштабируемости), реплицируется (для доступности) и выполняется в рамках секции (для семантики ACID). Давайте рассмотрим типичное использование объекта надежного словаря и посмотрим, что он делает.

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

Для всех операций с объектами надежного словаря (за исключением ClearAsync, который не является отменяемым), требуется объект ITransaction. Этот объект включает в себя все изменения, которые вы пытаетесь внести в любой надежный словарь и/или надежную очередь в одном разделе. Вы получаете объект ITransaction, вызывая метод CreateTransaction диспетчера состояния раздела.

В приведенном выше коде объект ITransaction передается в метод AddAsync надежного словаря. Внутренние методы словаря, принимающие ключ, используют блокировку чтения/записи, связанную с этим ключом. Если метод изменяет значение ключа, устанавливается блокировка записи для ключа, а если метод только читает значение ключа, то устанавливается блокировка чтения для ключа. Так как AddAsync изменяет значение ключа на новое, переданное значение, выполняется блокировка записи ключа. Таким образом, если 2 (или более) потоков пытаются добавить значения с одним ключом одновременно, один поток получит замок на запись, а другие потоки будут блокироваться. По умолчанию методы блокируют до 4 секунд, чтобы получить блокировку; через 4 секунды методы вызывают TimeoutException. Перегрузки методов существуют, что позволяет передавать явное значение времени ожидания, если вы предпочитаете.

Обычно код записывается для реагирования на TimeoutException, перехватив его и повторив всю операцию (как показано в приведенном выше коде). В этом простом коде мы просто вызываем Task.Delay, передавая 100 миллисекунда каждый раз. Но, на самом деле, вам лучше использовать экспоненциальную задержку с увеличением интервала вместо этого.

После того, как блокировка получена, AddAsync заносит ссылки на ключ и объект значения во внутренний временный словарь, связанный с объектом ITransaction. Это делается, чтобы обеспечить семантику чтения собственных записей. То есть после вызова AddAsync более поздний вызов TryGetValueAsync с помощью того же объекта ITransaction возвращает значение, даже если транзакция еще не зафиксирована.

Примечание.

Вызов TryGetValueAsync с новой транзакцией вернет ссылку на последнее зафиксированное значение. Не изменяйте ссылку напрямую, так как это обходит механизм сохранения и репликации изменений. Мы рекомендуем сделать значения только для чтения, чтобы единственный способ изменить значение ключа через надежные API словаря.

Затем AddAsync сериализует объекты ключей и значений для массивов байтов и добавляет эти массивы байтов в файл журнала на локальном узле. Наконец, AddAsync отправляет массивы байтов во все вторичные реплики, чтобы они имели одинаковые сведения о ключе или значении. Несмотря на то, что сведения о ключе и значении записываются в файл журнала, сведения не считаются частью словаря до тех пор, пока транзакция, с которыми они связаны, будет зафиксирована.

В приведенном выше коде вызов CommitAsync фиксирует все операции транзакции. В частности, он добавляет сведения о фиксации в файл журнала на локальном узле, а также отправляет запись фиксации во все вторичные реплики. Когда кворум (большинство) реплик ответили, все изменения данных считаются постоянными и все блокировки, связанные с ключами, управляемыми с помощью объекта ITransaction, выпускаются таким образом, чтобы другие потоки и транзакции могли управлять теми же ключами и их значениями.

Если CommitAsync не вызывается (обычно из-за исключения), то объект ITransaction удаляется. При удалении незафиксированного объекта ITransaction, Service Fabric добавляет сведения о прерывании в файл журнала локального узла, и ничего не нужно отправлять в любую вторичную реплику. А затем все блокировки, связанные с ключами, которые были манипулированы через транзакцию, освобождаются.

Переменные надежные коллекции

В некоторых рабочих нагрузках, таких как реплицированный кэш, например, иногда может быть разрешена потеря данных. Избегая сохранения данных на диске, можно повысить задержку и пропускную способность при записи в надежные словари. Компромисс за отсутствие сохраняемости заключается в том, что если происходит потеря кворума, произойдет полная потеря данных. Поскольку потеря кворума является редким случаем, повышение производительности может стоить редкой возможности потери данных для этих рабочих нагрузок.

В настоящее время переменная поддержка доступна только для надежных словарей и надежных очередей, а не ReliableConcurrentQueues. Ознакомьтесь со списком предостережений для принятия решения о том, следует ли использовать изменяемые коллекции.

Чтобы включить нестабильную поддержку в вашей службе, установите флаг HasPersistedState в объявлении типа false службы вот так:

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

Примечание.

Существующие сохраненные службы не могут быть переменными и наоборот. Если вы хотите сделать это, необходимо удалить существующую службу, а затем развернуть службу с обновленным флагом. Это означает, что вы должны быть готовы понести полную потерю данных, если вы хотите изменить HasPersistedState флаг.

Распространенные ловушки и как избежать их

Теперь, когда вы понимаете, как работают надежные коллекции внутри страны, давайте рассмотрим некоторые распространенные неправильное использование этих коллекций. См. приведенный ниже код:

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

При работе с обычным словарем .NET можно добавить ключ или значение в словарь, а затем изменить значение свойства (например, LastLogin). Однако этот код не будет работать правильно с надежным словарем. Помните из предыдущего обсуждения, что вызов AddAsync сериализует объекты ключ/значение в массивы байтов, а затем сохраняет массивы на локальный диск, а также отправляет их вторичным репликам. Если позже изменить свойство, это изменит значение свойства только в памяти; Это не влияет на локальный файл или данные, отправленные на реплики. Если процесс завершается сбоем, то что в памяти выброшено. Когда начинается новый процесс или если другая реплика становится первичной, то старое значение свойства доступно.

Я не могу не подчеркнуть, насколько легко сделать такую ошибку, как показано выше. Вы узнаете только об ошибке, если и когда процесс завершится. Правильный способ написания кода — просто изменить две строки:

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

Ниже приведен еще один пример, показывающий распространенную ошибку:

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

Опять же, с обычными словарями .NET приведенный выше код работает хорошо и является общим шаблоном: разработчик использует ключ для поиска значения. Если значение существует, разработчик изменяет значение свойства. Однако при использовании надежных коллекций этот код проявляет ту же проблему, что и уже обсуждалось: не следует изменять объект после предоставления ему надежной коллекции.

Правильный способ обновления значения в надежной коллекции — получить ссылку на существующее значение и рассмотреть объект, на который ссылается эта ссылка, неизменяемая. Затем создайте новый объект, который является точной копией исходного объекта. Теперь вы можете изменить состояние этого нового объекта и записать новый объект в коллекцию, чтобы он сериализовался в массивы байтов, добавлен к локальному файлу и отправлен в реплики. После фиксации изменений объекты в памяти, локальный файл и все реплики имеют одинаковое точное состояние. Все хорошо!

Приведенный ниже код показывает правильный способ обновления значения в надежной коллекции:

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

Определение неизменяемых типов данных для предотвращения ошибки программиста

В идеале компилятор должен сообщать об ошибках при случайном создании кода, который мутирует состояние объекта, который должен рассматриваться как неизменяемый. Но компилятор C# не имеет возможности сделать это. Поэтому, чтобы избежать потенциальных ошибок программиста, настоятельно рекомендуется определить типы, которые вы используете с надежными коллекциями, чтобы быть неизменяемыми типами. В частности, это означает, что вы придерживаетесь основных типов значений (например, чисел [Int32, UInt64 и т. д.], DateTime, Guid, TimeSpan и т. д.). Можно также использовать String. Рекомендуется избегать использования свойств коллекций, так как их сериализация и десериализация могут часто негативно сказаться на производительности. Тем не менее, если вы хотите использовать свойства коллекции, мы настоятельно рекомендуем использовать библиотеку неизменяемых коллекций .NET (System.Collections.Immutable). Эта библиотека доступна для скачивания с https://nuget.org. Мы также рекомендуем по возможности запечатывать классы и делать поля доступными только для чтения.

В приведенном ниже типе UserInfo показано, как определить неизменяемый тип, используя приведенные выше рекомендации.

[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 также является неизменяемым типом, как показано ниже:

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

Версионирование схем (обновления)

Внутренне, надежные коллекции сериализуют ваши объекты с помощью DataContractSerializer из .NET. Сериализованные объекты сохраняются на локальном диске первичной реплики, а также передаются во вторичные реплики. По мере того как ваша служба развивается, вероятно, вам потребуется изменить тип данных (схема), который требует ваша служба. Подходите к версионированию данных с особой тщательностью. Прежде всего, необходимо всегда иметь возможность десериализации старых данных. В частности, это означает, что ваш код десериализации должен обеспечивать бесконечную обратную совместимость: версия 333 кода службы должна иметь возможность работать с данными, размещенными в надёжной коллекции версией 1 кода службы пять лет назад.

Кроме того, служебный код обновляется по одному домену обновления одновременно. Таким образом, во время обновления у вас есть две разные версии кода службы, выполняющихся одновременно. Вам не следует использовать новую версию кода службы, используя новую схему, так как старые версии кода службы могут не обрабатывать новую схему. По возможности следует разрабатывать каждую версию службы таким образом, чтобы она была совместима с одной будущей версией. В частности, это означает, что версия 1 кода службы должна игнорировать любые элементы схемы, которые он не обрабатывает явным образом. Однако он должен иметь возможность сохранять любые данные, о которых ему явно не известно, и записывать их обратно при обновлении ключа или значения словаря.

Предупреждение

Хотя можно изменить схему ключа, необходимо убедиться, что алгоритмы равенства и сравнения ключа стабильны. Поведение надежных коллекций после изменения в любом из этих алгоритмов не определено и может привести к повреждению данных, потере и сбоям служб. Строки .NET можно использовать в качестве ключа, но используйте саму строку в качестве ключа. Не используйте результат String.GetHashCode в качестве ключа.

Кроме того, можно выполнить многоэтапное обновление.

  1. Обновите службу до новой версии, которая
    • имеет как исходную версию 1, так и новую версию контрактов данных версии 2, включенную в пакет кода службы;
    • при необходимости регистрирует пользовательские сериализаторы состояния версии 2;
    • выполняет все операции с исходной коллекцией V1 с помощью контрактов данных версии 1.
  2. Обновите службу до новой версии, которая
    • создает новую коллекцию V2;
    • выполняет каждую операцию добавления, обновления и удаления сначала в коллекции V1, а затем в коллекции V2 в одной транзакции.
    • выполняет операции чтения только в коллекции V1.
  3. Скопируйте все данные из коллекции V1 в коллекцию V2.
    • Это можно сделать в фоновом процессе с помощью версии службы, развернутой на шаге 2.
    • Повторное получение всех ключей из коллекции V1. Перечисление выполняется с помощью IsolationLevel.Snapshot по умолчанию, чтобы избежать блокировки коллекции в течение операции.
    • Для каждого ключа используйте отдельную транзакцию.
      • TryGetValueAsync из коллекции V1.
      • Если значение уже удалено из коллекции версии 1 с момента начала процесса копирования, ключ должен быть пропущен и не восстановлен в коллекции версии 2.
      • TryAddAsync — значение коллекции версии 2.
      • Если с момента начала процесса копирования значение уже добавлено в коллекцию V2, ключ должен быть пропущен.
      • Транзакция должна быть зафиксирована только в том случае, если TryAddAsync возвращает true.
      • API доступа к данным по умолчанию используют IsolationLevel.ReadRepeatable и полагаются на блокировку, чтобы гарантировать, что значения не изменяются другими вызовами до тех пор, пока транзакция не будет зафиксирована или прервана.
  4. Обновите систему до новой версии, которая
    • выполняет операции чтения только в коллекции V2;
    • по-прежнему выполняет каждую операцию добавления, обновления и удаления сначала в коллекциях V1, а затем в коллекциях V2, чтобы сохранить возможность отката к V1.
  5. Комплексно протестируйте службу и убедитесь, что она работает должным образом.
    • Если вы пропустили операцию доступа к значению, которая не была обновлена для работы с коллекцией версии 1 и версии 2, возможно, отсутствуют данные.
    • Если отсутствуют какие-либо данные, вернитесь к шагу 1, удалите коллекцию V2 и повторите процесс.
  6. Обновите сервис до новой версии, которая
    • выполняет все операции исключительно с коллекцией V2;
    • Возврат к версии 1 больше невозможен с откатом службы и потребует развёртывания в обратном порядке шаги 2-4.
  7. Обновите службу до новой версии, которая
  8. Дождитесь усечения журнала.
    • По умолчанию это происходит каждые 50 МБ операций (добавление, обновление и удаление) в рамках надежных коллекций.
  9. Обновите службу до новой версии, которая
    • Больше не имеет контрактов данных версии 1, включенных в пакет кода службы.

Дальнейшие действия

Дополнительные сведения о создании контрактов на пересылку совместимых данных см. в разделе Forward-Compatible Контракты данных

Сведения о рекомендациях по управлению контрактами данных см. в статье "Управление версиями контракта данных"

Сведения о том, как реализовать контракты данных, устойчивые к изменениям версии, см. Version-Tolerant обратные вызовы сериализации

Сведения о том, как предоставить структуру данных, которая может взаимодействовать между несколькими версиями, см. в разделе IExtensibleDataObject.

Сведения о настройке надежных коллекций см. в разделе "Конфигурация репликатора"