Orleans 中的序列化自定义

Orleans 的一个重要方面是支持序列化的自定义,即将对象或数据结构转换为可存储或传输的格式并在以后重新构造的过程。 这样,开发人员就可以控制在系统的不同部分之间发送数据时编码和解码数据的方式。 序列化自定义可用于优化性能、互操作性和安全性。

序列化提供程序

Orleans 提供两种序列化程序实现:

若要配置其中任一包,请参阅 Orleans 中的序列化配置

自定义序列化程序实现

若要创建自定义序列化程序实现,需要执行几个常见步骤。 必须实现多个接口,然后将序列化程序注册到 Orleans 运行时。 以下各部分更详细地说明了这些步骤。

首先实现以下 Orleans 序列化接口:

请考虑以下自定义序列化程序实现示例:

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. 向类型添加序列化方法,并用相应的属性(CopierMethodAttributeSerializerMethodAttributeDeserializerMethodAttribute)标记这些方法。 对于应用拥有的类型(即,可以向其添加新方法的类型),这种方法更可取。
  2. 实现 IExternalSerializer 并在配置期间注册它。 这种方法对于集成外部序列化库非常有用。
  3. 编写一个单独的静态类并使用 [Serializer(typeof(YourType))] 批注该类,在其中包含 3 个序列化方法并使用上述相同的属性。 这种方法对于应用不拥有的类型很有用,例如,应用无法控制的其他库中定义的类型。

以下各节详细介绍了其中的每一种序列化方法。

自定义序列化简介

Orleans 序列化分三个阶段进行:

  • 立即对对象进行深层复制以确保隔离。
  • 在上线之前,将对象序列化为消息字节流。
  • 传递到目标激活时,将从接收的字节流中重新创建(反序列化)对象。

可以在消息中发送的数据类型(即,可以作为方法参数或返回值传递的类型)必须具有执行这三个步骤的关联例程。 我们将这些例程统称为数据类型的序列化程序。

类型的复制器是独立的,而序列化程序和反序列化程序是一起工作的配对。 可以只提供一个自定义的复制器,或只提供一个自定义的序列化程序和一个自定义的反序列化程序,或者可以提供这三者的自定义实现。

在 silo 启动时以及每次加载程序集时,都会为每种受支持的数据类型注册序列化程序。 对于要使用的类型的自定义序列化程序例程,注册是必需的。 序列化程序的选择基于要复制或序列化的对象的动态类型。 因此,无需为抽象类或接口创建序列化程序,因为永远不会使用它们。

何时编写自定义序列化程序

手动创建的序列化程序例程的性能很少比生成的版本更好。 如果你想要编写一个,首先应考虑以下选项:

  • 如果数据类型中的字段或属性不必序列化或复制,则你可以使用 NonSerializedAttribute 标记它们。 这会导致生成的代码在复制和序列化时跳过这些字段。 尽可能使用 ImmutableAttributeImmutable<T>,以避免复制不可变数据。 有关详细信息,请参阅优化复制。 如果你想要避免使用标准的泛型集合类型,请不要这样做。 Orleans 运行时包含泛型集合的自定义序列化程序,它们使用集合的语义来优化复制、序列化和反序列化。 这些集合在序列化的字节流中也有特殊的“缩写”表示形式,从而带来更大的性能优势。 例如,Dictionary<string, string>List<Tuple<string, string>> 速度更快。

  • 自定义序列化程序可以显著提升性能的最常见情况是,在数据类型中编码了大量的语义信息,而仅仅通过复制字段值无法提供这些信息。 例如,稀疏填充的数组通常可以通过将数组视为索引/值对的集合来更有效地序列化,即使应用将数据保持为完全实现的数组以提高操作速度,也是如此。

  • 在编写自定义的序列化程序之前,要做的一件关键事情是确定生成的序列化程序确实损害了性能。 在此处做出分析会有一点帮助,但更有价值的是,使用不同的序列化负载对应用运行端到端的应力测试来测量系统级的影响,而不是序列化的微观影响。 例如,生成一个不向 grain 方法传递参数或从 grain 方法得到结果的测试版本,只需在两端使用固定值,就可以放大序列化和复制对系统性能的影响。

向类型添加序列化方法

所有序列化程序例程都应实现为其操作的类或结构的静态成员。 此处显示的名称不是必需的;注册基于相应属性的存在状态而不是方法名称。 请注意,序列化程序方法不必是公共方法。

除非实现了所有三个序列化例程,否则应使用 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 调用)是必需的,以便将来可以通过 CheckObjectWhileCopying 正确找到与 foo 引用的同一对象的引用。

注意

只能对类实例执行此操作,而不能对 struct 实例或 .NET 基元(例如 stringUrienum)执行

如果使用 DeepCopyInner 复制子对象,则系统会为你处理对象标识。

序列化程序

使用 Orleans.CodeGeneration.SerializerMethodAttribute 标记序列化方法:

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

与复制器一样,传递给序列化程序的“输入”对象保证是定义类型的实例。 可以忽略“预期”的类型;它基于有关数据项的编译时类型信息,并在更高级别上用于在字节流中构成类型前缀。

若要序列化子对象,请使用 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 并将其添加到客户端上的 ClientConfiguration 以及 silo 上的 GlobalConfigurationSerializationProviderOptions.SerializationProviders 属性中。 有关配置的信息,请参阅序列化提供程序

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

序列化泛型类型

[Serializer(typeof(TargetType))]TargetType 参数可以是开放式泛型类型,例如 MyGenericType<T>。 在这种情况下,序列化程序类必须具有与目标类型相同的泛型参数。 Orleans 将在运行时为序列化的每个具体 MyGenericType<T> 类型创建一个具体版本的序列化程序,例如,对于 MyGenericType<int>MyGenericType<string> 各创建一个。

有关编写序列化程序和反序列化程序的提示

通常,编写序列化程序/反序列化程序对的最简单方法是构造一个字节数组,将数组长度写入流,对数组本身进行序列化,然后通过反转该过程进行反序列化。 如果是固定长度的数组,则可以从流中省略它。 有一个可以紧凑地表示并且没有可能重复的子对象的数据类型时,此方法很有效(因此不必担心对象标识)。

另一种方法是 Orleans 运行时对字典等集合采用的方法,适用于具有重要且复杂的内部结构的类:使用实例方法访问对象的语义内容,序列化该内容,并通过设置语义内容而不是复杂的内部状态来反序列化。 在此方法中,内部对象使用 SerializeInner 来写入,使用 DeserializeInner 来读取。 在这种情况下,通常也会编写自定义复印器。

如果你要编写一个自定义序列化程序,并且它最终看起来像是对类中每个字段都调用 SerializeInner 的一个序列,则不需要该类的自定义序列化程序。

另请参阅