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


Настройка сериализации в Orleans

Одним из важных аспектов Orleans является его поддержка настройки сериализации, которая является процессом преобразования объекта или структуры данных в формат, который может храниться или передаваться и реконструировать позже. Это позволяет разработчикам управлять кодированием и декодированием данных при отправке между разными частями системы. Настройка сериализации может быть полезна для оптимизации производительности, взаимодействия и безопасности.

Поставщики сериализации

Orleans предоставляет две реализации сериализатора:

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

Реализация пользовательского сериализатора

Чтобы создать пользовательскую реализацию сериализатора, необходимо выполнить несколько распространенных действий. Необходимо реализовать несколько интерфейсов, а затем зарегистрировать сериализатор в Orleans среде выполнения. В следующих разделах подробно описаны шаги.

Начните с реализации следующих Orleans интерфейсов сериализации:

  • IGeneralizedCodec: кодек, поддерживающий несколько типов.
  • IGeneralizedCopier: предоставляет функциональные возможности для копирования объектов нескольких типов.
  • ITypeFilter: функциональность, позволяющая загружать типы и участвовать в сериализации и десериализации.

Рассмотрим следующий пример реализации пользовательского сериализатора:

internal sealed class CustomOrleansSerializer :
    IGeneralizedCodec, IGeneralizedCopier, ITypeFilter
{
    void IFieldCodec.WriteField<TBufferWriter>(
        ref Writer<TBufferWriter> writer, 
        uint fieldIdDelta,
        Type expectedType,
        object value) =>
        throw new NotImplementedException();

    object IFieldCodec.ReadValue<TInput>(
        ref Reader<TInput> reader, Field field) =>
        throw new NotImplementedException();

    bool IGeneralizedCodec.IsSupportedType(Type type) =>
        throw new NotImplementedException();

    object IDeepCopier.DeepCopy(object input, CopyContext context) =>
        throw new NotImplementedException();

    bool IGeneralizedCopier.IsSupportedType(Type type) =>
        throw new NotImplementedException();
}

В предыдущем примере реализации:

  • Каждый интерфейс явно реализуется, чтобы избежать конфликтов с разрешением имен метода.
  • Каждый метод создает исключение NotImplementedException , указывающее, что метод не реализован. Необходимо реализовать каждый метод, чтобы обеспечить необходимые функциональные возможности.

Следующим шагом является регистрация сериализатора Orleans в среде выполнения. Обычно это достигается путем расширения ISerializerBuilder и предоставления пользовательского AddCustomSerializer метода расширения. В следующем примере показан типичный шаблон:

using Microsoft.Extensions.DependencyInjection;
using Orleans.Serialization;
using Orleans.Serialization.Serializers;
using Orleans.Serialization.Cloning;

public static class SerializationHostingExtensions
{
    public static ISerializerBuilder AddCustomSerializer(
        this ISerializerBuilder builder)
    {
        var services = builder.Services;

        services.AddSingleton<CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCodec, CustomOrleansSerializer>();
        services.AddSingleton<IGeneralizedCopier, CustomOrleansSerializer>();
        services.AddSingleton<ITypeFilter, CustomOrleansSerializer>();

        return builder;
    }
}

Дополнительные рекомендации — предоставить перегрузку, которая принимает настраиваемые параметры сериализации, относящиеся к вашей пользовательской реализации. Эти параметры можно настроить вместе с регистрацией в построителе. Эти параметры могут быть внедрены в реализацию пользовательского сериализатора.

Orleans поддерживает интеграцию с сторонними сериализаторами с помощью модели поставщика. Для этого требуется реализация IExternalSerializer типа, описанного в разделе пользовательской сериализации этой статьи. Интеграция некоторых распространенных сериализаторов поддерживается вместе с Orleansпримером:

Пользовательская реализация описана IExternalSerializer в следующем разделе.

Пользовательские внешние сериализаторы

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

Существует три способа настройки сериализации приложений:

  1. Добавьте методы сериализации в тип и пометьте их соответствующими атрибутами (CopierMethodAttribute, SerializerMethodAttribute, ). DeserializerMethodAttribute Этот метод предпочтительнее для типов, принадлежащих приложению, то есть типов, к которым можно добавлять новые методы.
  2. Реализуйте и регистрируйте IExternalSerializer его во время настройки. Этот метод полезен для интеграции внешней библиотеки сериализации.
  3. Напишите отдельный статический класс, аннотированный с [Serializer(typeof(YourType))] помощью 3 методов сериализации в нем и те же атрибуты, что и выше. Этот метод полезен для типов, которым приложение не владеет, например, типами, определенными в других библиотеках, над которыми приложение не управляет.

Каждый из этих методов сериализации подробно описан в следующих разделах.

Общие сведения о пользовательской сериализации

Orleans сериализация выполняется на трех этапах:

  • Объекты немедленно копируются глубоко, чтобы обеспечить изоляцию.
  • Перед размещением в проводе объекты сериализуются в поток байтов сообщения.
  • При доставке в целевую активацию объекты повторно создаются (десериализированы) из полученного потока байтов.

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

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

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

Когда необходимо написать пользовательский сериализатор

Подпрограмма сериализатора вручную редко будет работать лучше, чем созданные версии. Если вы заманчивы написать один, сначала следует рассмотреть следующие варианты:

  • Если в типах данных есть поля или свойства, которые не должны быть сериализованы или скопированы, их можно пометить с помощью NonSerializedAttribute. Это приведет к пропуску этих полей при копировании и сериализации созданного кода. Используйте ImmutableAttribute и Immutable<T> по возможности, чтобы избежать копирования неизменяемых данных. Дополнительные сведения см. в разделе "Оптимизация копирования". Если вы не используете стандартные универсальные типы коллекций, не следует. Среда Orleans выполнения содержит пользовательские сериализаторы для универсальных коллекций, использующих семантику коллекций для оптимизации копирования, сериализации и десериализации. Эти коллекции также имеют специальные "сокращенные" представления в сериализованном потоке байтов, что приводит к еще большему повышению производительности. Например, это Dictionary<string, string> будет быстрее, чем a List<Tuple<string, string>>.

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

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

Добавление методов сериализации в тип

Все подпрограммы сериализатора должны быть реализованы как статические члены класса или структуры, над которыми они работают. Имена, отображаемые здесь, не являются обязательными; регистрация основана на наличии соответствующих атрибутов, а не на именах методов. Обратите внимание, что методы сериализатора не должны быть общедоступными.

Если вы не реализуете все три подпрограммы сериализации, следует пометить тип таким SerializableAttribute образом, чтобы отсутствующие методы были созданы для вас.

Копировальный аппарат

Методы копирования помечены следующими Orleans.CodeGeneration.CopierMethodAttributeспособами:

[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
    // ...
}

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

Если при копировании объекта необходимо скопировать вложенный объект, лучше всего это сделать, чтобы использовать подпрограмму SerializationManager.DeepCopyInner :

var fooCopy = SerializationManager.DeepCopyInner(foo, context);

Внимание

Важно использовать SerializationManager.DeepCopyInnerвместо SerializationManager.DeepCopyэтого контекст удостоверения объекта для полной операции копирования.

Обслуживание удостоверения объекта

Важная ответственность за подпрограмму копирования заключается в поддержании удостоверения объекта. Среда Orleans выполнения предоставляет вспомогательный класс для этой цели. Прежде чем копировать вложенный объект "по руке" (не вызывая DeepCopyInner), проверьте, уже ли он был указан следующим образом:

var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
    // Actually make a copy of foo
    context.RecordObject(foo, fooCopy);
}

Последняя строка — это вызов RecordObject, который требуется, чтобы возможные будущие ссылки на тот же объект, что foo и ссылки, были найдены должным образом CheckObjectWhileCopying.

Примечание.

Это необходимо сделать только для экземпляров классов, а не struct для примитивов .NET, таких как string, Uriи enum.

Если вы используете DeepCopyInner для копирования вложенных объектов, то для вас обрабатывается удостоверение объекта.

serializer

Методы сериализации помечены следующими Orleans.CodeGeneration.SerializerMethodAttributeспособами:

[SerializerMethod]
static private void Serialize(
    object input,
    ISerializationContext context,
    Type expected)
{
    // ...
}

Как и в случае с копировщиками, объект input, переданный сериализатору, гарантированно является экземпляром определяющего типа. Тип "ожидаемый" может игнорироваться; он основан на сведениях о типе времени компиляции об элементе данных и используется на более высоком уровне для формирования префикса типа в потоке байтов.

Чтобы сериализовать вложенные объекты, используйте подпрограмму SerializationManager.SerializeInner :

SerializationManager.SerializeInner(foo, context, typeof(FooType));

Если для foo нет определенного ожидаемого типа, можно передать значение NULL для ожидаемого типа.

Класс BinaryTokenStreamWriter предоставляет широкий спектр методов записи данных в поток байтов. Экземпляр класса можно получить с помощью context.StreamWriter свойства. См. класс для документации.

Десериализатор

Методы десериализации помечены следующими Orleans.CodeGeneration.DeserializerMethodAttributeспособами:

[DeserializerMethod]
static private object Deserialize(
    Type expected,
    IDeserializationContext context)
{
    //...
}

Тип "ожидаемый" может игнорироваться; он основан на сведениях о типе времени компиляции об элементе данных и используется на более высоком уровне для формирования префикса типа в потоке байтов. Фактический тип создаваемого объекта всегда будет типом класса, в котором определен десериализатор.

Чтобы десериализировать вложенные объекты, используйте подпрограмму SerializationManager.DeserializeInner :

var foo = SerializationManager.DeserializeInner(typeof(FooType), context);

Другой пример:

var foo = SerializationManager.DeserializeInner<FooType>(context);

Если для foo нет определенного ожидаемого типа, используйте не универсальный DeserializeInner вариант и передайте null ожидаемый тип.

Класс BinaryTokenStreamReader предоставляет широкий спектр методов для чтения данных из потока байтов. Экземпляр класса можно получить с помощью context.StreamReader свойства. См. класс для документации.

Запись поставщика сериализатора

В этом методе вы реализуете Orleans.Serialization.IExternalSerializer и добавляете его в SerializationProviderOptions.SerializationProviders свойство как на клиенте, так ClientConfiguration и GlobalConfiguration на силосах. Сведения о конфигурации см. в разделе "Поставщики сериализации".

IExternalSerializer Реализации следует шаблону, описанному ранее для сериализации с добавлением Initialize метода и IsSupportedType метода, который используется для определения того, Orleans поддерживает ли сериализатор заданный тип. Это определение интерфейса:

public interface IExternalSerializer
{
    /// <summary>
    /// Initializes the external serializer. Called once when the serialization manager creates
    /// an instance of this type
    /// </summary>
    void Initialize(Logger logger);

    /// <summary>
    /// Informs the serialization manager whether this serializer supports the type for serialization.
    /// </summary>
    /// <param name="itemType">The type of the item to be serialized</param>
    /// <returns>A value indicating whether the item can be serialized.</returns>
    bool IsSupportedType(Type itemType);

    /// <summary>
    /// Tries to create a copy of source.
    /// </summary>
    /// <param name="source">The item to create a copy of</param>
    /// <param name="context">The context in which the object is being copied.</param>
    /// <returns>The copy</returns>
    object DeepCopy(object source, ICopyContext context);

    /// <summary>
    /// Tries to serialize an item.
    /// </summary>
    /// <param name="item">The instance of the object being serialized</param>
    /// <param name="context">The context in which the object is being serialized.</param>
    /// <param name="expectedType">The type that the deserializer will expect</param>
    void Serialize(object item, ISerializationContext context, Type expectedType);

    /// <summary>
    /// Tries to deserialize an item.
    /// </summary>
    /// <param name="context">The context in which the object is being deserialized.</param>
    /// <param name="expectedType">The type that should be deserialized</param>
    /// <returns>The deserialized object</returns>
    object Deserialize(Type expectedType, IDeserializationContext context);
}

Создание сериализатора для отдельного типа

В этом методе создается новый класс, аннотированный атрибутом [SerializerAttribute(typeof(TargetType))], где TargetType сериализуется тип, и реализуется 3 подпрограммы сериализации. Правила записи этих подпрограмм идентичны тому, что при реализации IExternalSerializer. Orleans[SerializerAttribute(typeof(TargetType))] используется для определения того, что этот класс является сериализатором, TargetType и этот атрибут можно указать несколько раз в одном классе, если он может сериализовать несколько типов. Ниже приведен пример для такого класса:

public class User
{
    public User BestFriend { get; set; }
    public string NickName { get; set; }
    public int FavoriteNumber { get; set; }
    public DateTimeOffset BirthDate { get; set; }
}

[Orleans.CodeGeneration.SerializerAttribute(typeof(User))]
internal class UserSerializer
{
    [CopierMethod]
    public static object DeepCopier(
        object original, ICopyContext context)
    {
        var input = (User)original;
        var result = new User();

        // Record 'result' as a copy of 'input'. Doing this
        // immediately after construction allows for data
        // structures that have cyclic references or duplicate
        // references. For example, imagine that 'input.BestFriend'
        // is set to 'input'. In that case, failing to record
        // the copy before trying to copy the 'BestFriend' field
        // would result in infinite recursion.
        context.RecordCopy(original, result);

        // Deep-copy each of the fields.
        result.BestFriend =
            (User)context.SerializationManager.DeepCopy(input.BestFriend);

        // strings in .NET are immutable, so they can be shallow-copied.
        result.NickName = input.NickName;
        // ints are primitive value types, so they can be shallow-copied.
        result.FavoriteNumber = input.FavoriteNumber;
        result.BirthDate =
            (DateTimeOffset)context.SerializationManager.DeepCopy(input.BirthDate);

        return result;
    }

    [SerializerMethod]
    public static void Serializer(
        object untypedInput, ISerializationContext context, Type expected)
    {
        var input = (User) untypedInput;

        // Serialize each field.
        SerializationManager.SerializeInner(input.BestFriend, context);
        SerializationManager.SerializeInner(input.NickName, context);
        SerializationManager.SerializeInner(input.FavoriteNumber, context);
        SerializationManager.SerializeInner(input.BirthDate, context);
    }

    [DeserializerMethod]
    public static object Deserializer(
        Type expected, IDeserializationContext context)
    {
        var result = new User();

        // Record 'result' immediately after constructing it.
        // As with the deep copier, this
        // allows for cyclic references and de-duplication.
        context.RecordObject(result);

        // Deserialize each field in the order that they were serialized.
        result.BestFriend =
            SerializationManager.DeserializeInner<User>(context);
        result.NickName =
            SerializationManager.DeserializeInner<string>(context);
        result.FavoriteNumber =
            SerializationManager.DeserializeInner<int>(context);
        result.BirthDate =
            SerializationManager.DeserializeInner<DateTimeOffset>(context);

        return result;
    }
}

Сериализация универсальных типов

Параметр TargetType [Serializer(typeof(TargetType))] может быть открытым универсальным типом, например MyGenericType<T>. В этом случае класс сериализатора должен иметь те же универсальные параметры, что и целевой тип. Orleansбудет создавать конкретную версию сериализатора во время выполнения для каждого конкретного MyGenericType<T> типа, который сериализуется, например, по одному для каждого из MyGenericType<int> них.MyGenericType<string>

Указания по написанию сериализаторов и десериализаторов

Часто самый простой способ записи пары сериализатора или десериализатора заключается в сериализации путем создания массива байтов и записи длины массива в поток, а затем десериализации путем отмены процесса. Если массив имеет фиксированную длину, его можно опустить из потока. Это хорошо работает, если у вас есть тип данных, который можно представить компактно, и у него нет дочерних объектов, которые могут быть дублируются (поэтому вам не нужно беспокоиться о удостоверении объекта).

Другой подход, который является подходом Orleans среды выполнения для коллекций, таких как словари, хорошо подходит для классов со значительной и сложной внутренней структурой: используйте методы экземпляра для доступа к семантической содержимой объекта, сериализации этого содержимого и десериализации путем задания семантического содержимого, а не сложного внутреннего состояния. В этом подходе внутренние объекты записываются с помощью SerializeInner и считываются с помощью DeserializeInner. В этом случае обычно создается пользовательский копировщик, а также.

Если вы напишете пользовательский сериализатор, и он будет выглядеть как последовательность вызовов SerializeInner для каждого поля в классе, вам не нужен настраиваемый сериализатор для этого класса.

См. также