Orleans 2.0 官方文档 —— 5.5.10 集群和客户端 -> 配置指南 -> 序列化

序列化和编写自定义序列化程序

Orleans有一个高级的、可扩展的序列化框架。Orleans序列化grain请求和响应的消息中传递的数据类型,以及grain的持久化状态对象。作为此框架的一部分,Orleans会自动为这些数据类型生成序列化代码。除了为已经.NET可序列化的类型生成更有效率的序列化/反序列化之外,Orleans还尝试为不可.NET序列化的grain接口中使用的类型生成序列化程序。该框架还包括一组用于常用类型的、高效的内置序列化器:列表,字典,字符串,基元,数组等。

Orleans序列化程序有两个重要的特性,使它与许多其他第三方序列化框架不同:动态类型/任意多态和保留对象标识。

  1. 动态类型和任意多态 —— Orleans对grain调用中可以传递的类型没有任何限制,并保持实际数据类型的动态特性。这意味着,例如,如果grain接口中的方法被声明为接受IDictionary,但在运行时发送方通过SortedDictionary,则接收方确实会获得SortedDictionary(尽管“静态契约”/grain接口未指定此行为)。

  2. 保留对象标识 —— 如果在grain调用的参数中传递多个类型的同一对象,或者从参数中间接指向多个类型,则Orleans只会序列化一次。在接收方,Orleans将正确恢复所有引用,因此在反序列化之后,指向同一对象的两个指针仍然指向同一个对象。在以下场景中保留对象标识非常重要。想象一下,actor A正在向actor B发送一个包含100个条目的字典,并且字典中的10个键指向A的同一个对象obj。如果不保留对象标识,BB将收到一个包含100个条目的字典,其中10个键指向10个不同的obj克隆。B端的字典与A端的字典完全相同,其中10个键指向单个对象obj。

上述两种行为是由标准的.NET二进制序列化程序提供的,因此我们在Orleans也支持这种标准和熟悉的行为,这非常重要。

生成的序列化器

Orleans使用以下规则来决定生成哪些序列化器。规则是:

1)扫描所有的引用了核心Orleans库的程序集中的所有类型。

2)在这些程序集中:为直接在grain接口方法签名或状态类签名中引用的类型,或者标记了[Serializable]属性的任何类型,生成序列化器 。

3)此外,grain接口或实现接口的项目,通过添加一个[KnownType][KnownAssembly]程序集级别的属性,来指向用于生成序列化的任意类型,以告知代码生成器,为程序集中的特定类型或所有符合条件的类型,生成序列化器。

序列化提供程序

Orleans支持使用provider模型与第三方序列化程序集成。这需要实现本文档的自定义序列化部分中描述的IExternalSerializer类型。一些常见的序列化器集成与Orleans一起维护,例如:

  • Protocol Buffers:Orleans.Serialization.ProtobufSerializer来自Microsoft.Orleans.OrleansGoogleUtilsNuGet包。
  • Bond:Orleans.Serialization.BondSerializer来自Microsoft.Orleans.Serialization.Bond NuGet包。
  • Newtonsoft.Json AKA Json.NET:Orleans.Serialization.OrleansJsonSerializer来自核心的Orleans库。

IExternalSerializer的自定义实现,在下面的“编写自定义序列化器”部分中描述。

配置

确保所有客户端和silo上的序列化配置完全相同,这一点非常重要。如果配置不一致,则可能发生序列化错误。

可以使用ClientConfigurationGlobalConfigurationSerializationProviders属性,在代码中指定实现了IExternalSerializer的序列化提供程序:

var cfg = new ClientConfiguration();
cfg.SerializationProviders.Add(typeof(FantasticSerializer).GetTypeInfo());
var cfg = new GlobalConfiguration();
cfg.SerializationProviders.Add(typeof(FantasticSerializer).GetTypeInfo());

或者,可以在XML配置中下的属性下,指定它们:

<Messaging>
  <SerializationProviders>
    <Provider type="GreatCompany.FantasticSerializer, GreatCompany.SerializerAssembly"/>
  SerializationProviders>
Messaging>

在这两种情况下,都可以配置多个提供程序。该集合是有序的,这意味着,如果在能序列化类型A和类型B的提供程序之后,指定了只能序列化类型B的提供程序,则不会使用后者。

编写自定义序列化器

除了自动的序列化生成之外,应用程序代码还可以为其选择的类型提供自定义序列化。Orleans建议对大多数应用程序类型使用自动的序列化生成,并且只有在您认为可以通过手动编码序列化程序来提高性能时,才会编写自定义序列化程序。本说明介绍了如何执行此操作,并识别了一些可能有用的特定情况。

应用程序可以通过3种方式自定义序列化:

  1. 向类型中添加序列化方法,并以适当的属性(CopierMethodSerializerMethodDeserializerMethod)标记它们。对于应用程序拥有的类型,即可以向其中添加新方法的类型,此方式更可取。

  2. 实现IExternalSerializer,并在配置期间注册它。此方式对于集成外部序列化库非常有用。

  3. 编写一个单独的静态类,在类上使用[Serializer(typeof(YourType))]注解,在类中使用3个序列化方法,方法的属性与上面3个属性相同。此方式对于应用程序不拥有的类型很有用,例如,应用程序无法控制的其他库中定义的类型。

以下各节详细介绍了这些方式中的每一种。

介绍

Orleans序列化分三个阶段进行:对象立即被深拷贝,以确保隔离; 在连接之前;对象被序列化为消息字节流;当对象传递到目标激活体后,从接收的字节流中重新创建(反序列化)对象。可以在消息中发送的数据类型(即可以作为方法参数或返回值传递的类型),必须具有执行这三个步骤的关联例程。我们将这些例程统称为数据类型的序列化器。

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

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

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

手工制作的序列化程器例程,很少比生成的版本更好地执行。如果您想这样做,首先应考虑以下选项:

如果数据类型中的字段或属性不必序列化或复制,则可以使用该NonSerialized属性标记它们。这将导致生成的代码在复制和序列化时跳过这些字段。尽可能使用Immutable[Immutable],以避免复制不可变的数据。有关详细信息,请参阅下面的“ 优化复制 ”部分。如果要避免使用标准的泛型集合类型,请不要这样做。Orleans运行时包含泛型集合的自定义序列化器,它们使用集合的语义来优化复制、序列化和反序列化。这些集合在序列化的字节流中也有特殊的“缩写”表示,从而带来更多的性能优势。例如,Dictionary比列表List>更快。

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

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

方法1:向类型添加序列化方法

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

除非您实现了全部的三个序列化例程,否则应使用Serializable属性标记类型,以便为您生成缺少的方法。

复制器

复制器方法使用Orleans.CopierMethod属性进行标记:

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

复制器通常是最简单的序列化例程。它们接受一个对象,保证其类型与复制器定义的类型相同,并且必须返回该对象的语义等效副本。

作为复制对象的一部分,如果需要复制子对象,最好的方法是使用SerializationManager的DeepCopyInner例程:

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

使用DeepCopyInner而不是DeepCopy,以保留完整复制操作的对象标识上下文,这非常重要。

保留对象标识

复制例程的一个重要职责是保留对象标识。Orleans运行时为此提供了一个辅助类。在“手动”复制子对象之前(即不通过调用DeepCopyInner),请检查是否已按以下方式引用了该子对象:

var fooCopy = context.CheckObjectWhileCopying(foo);

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

最后一行,即调用RecordObject,是必需的,以便可以通过CheckObjectWhileCopying,将来可以正确地找到与foo引用的同一对象的引用。

请注意,这只能用于类实例,而不能用于结构实例或.NET基元(字符串,Uris,枚举)。

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

序列化器

序列化方法使用SerializerMethod属性进行标记:

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

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

要序列化的子对象,请使用SerializationManagerSerializeInner例程:

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

如果foo没有特定的预期类型,那么您可以为预期的类型传递null。

BinaryTokenStreamWriter类为向字节流写入数据提供了多种方法。可以通过context.StreamWriter属性获取类的实例。有关此类的信息,请参阅文档。

反序列化器

反序列化方法使用DeserializerMethod属性进行标记:

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

可以忽略“预期”类型;它基于有关数据项的编译时类型信息,并在更高级别上用于在字节流中形成类型前缀。要创建的对象的实际类型,始终是定义反序列化器的类的类型。

反序列化的子对象,使用SerializationManagerDeserializeInner例程:

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

或者:

var foo = SerializationManager.DeserializeInner(context);

如果foo没有特定的预期类型,请使用非泛型的DeserializeInner方法,并为预期类型传递null

BinaryTokenStreamReader类提供了从字节流读取数据的各种方法。可以通过context.StreamReader属性获取类的实例。有关此类的信息,请参阅文档。

方法2:编写序列化器提供程序

在此方法中,您实现了Orleans.Serialization.IExternalSerializer,并将其添加到客户端的ClientConfiguration和silo的GlobalConfiguration这二者的SerializationProviders属性。配置详见上面的序列化提供程序部分。

IExternalSerializer的实现,遵循上述方法1中序列化方法描述的模式,并添加了Initialize方法和IsSupportedType方法,Orleans用IsSupportedType来确定序列化器是否支持给定类型。这是接口定义:

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

    /// 
    /// Informs the serialization manager whether this serializer supports the type for serialization.
    /// 
    /// The type of the item to be serialized
    /// A value indicating whether the item can be serialized.
    bool IsSupportedType(Type itemType);

    /// 
    /// Tries to create a copy of source.
    /// 
    /// The item to create a copy of
    /// The context in which the object is being copied.
    /// The copy
    object DeepCopy(object source, ICopyContext context);

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

    /// 
    /// Tries to deserialize an item.
    /// 
    /// The context in which the object is being deserialized.
    /// The type that should be deserialized
    /// The deserialized object
    object Deserialize(Type expectedType, IDeserializationContext context);
}

方法3:为单个类型编写序列化程序

在此方法中,您将编写一个用[SerializerAttribute(typeof(TargetType))]属性注释的新类,其中TargetType是要序列化的类型,并实现3个序列化例程。如何编写这些例程的规则与方法1相同。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 which 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);
        result.NickName = input.NickName; // strings in .NET are immutable, so they can be shallow-copied.
        result.FavoriteNumber = input.FavoriteNumber; // ints are primitive value types, so they can be shallow-copied.
        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 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(context);
        result.NickName = SerializationManager.DeserializeInner<string>(context);
        result.FavoriteNumber = SerializationManager.DeserializeInner<int>(context);
        result.BirthDate = SerializationManager.DeserializeInner(context);

        return result;
    }
}

序列化泛型

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

编写序列化器和反序列化器的提示

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

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

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

后备(Fallback)序列化

Orleans支持在运行时传输任意类型,因此内置代码生成器无法确定将提前传输的整个类型集。此外,某些类型不能为其生成序列化器,因为它们不可访问(例如private)或具有不可访问的字段(例如,readonly)。因此,需要对意外或无法提前生成序列化器的类型进行实时序列化。负责这些类型的序列化程序,称为后备序列化器。Orleans有两个后备序列化器:

  • Orleans.Serialization.BinaryFormatterSerializer它使用了.NET的BinaryFormatter ; 和
  • Orleans.Serialization.ILBasedSerializer它在运行时发出CIL指令来创建序列化器,这些序列化器利用Orleans的序列化框架来序列化每个字段。这意味着,如果不可访问的类型MyPrivateType包含具有自定义序列化器的字段MyType,则将使用该自定义序列化器对该类型进行序列化。

可以使用客户端的ClientConfiguration和silo的GlobalConfiguration这二者的FallbackSerializationProvider属性,来配置后备序列化器。

var cfg = new ClientConfiguration();
cfg.FallbackSerializationProvider = typeof(FantasticSerializer).GetTypeInfo();
var cfg = new GlobalConfiguration();
cfg.FallbackSerializationProvider = typeof(FantasticSerializer).GetTypeInfo();

或者,可以在XML配置中指定后备序列化提供程序:

<Messaging>
  <FallbackSerializationProvider type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
Messaging>

BinaryFormatterSerializer 是默认的后备序列化程序。

异常的序列化

使用后备序列化器来序列化异常。使用默认配置,BinaryFormatterSerializer是后备序列化器,因此必须遵循ISerializable模式,以确保对异常类型中的所有属性进行正确的序列化。

下面是正确实现了序列化的异常类型的示例:

[Serializable]
public class MyCustomException : Exception
{
    public string MyProperty { get; }

    public MyCustomException(string myProperty, string message)
        : base(message)
    {
        this.MyProperty = myProperty;
    }

    public MyCustomException(string transactionId, string message, Exception innerException)
        : base(message, innerException)
    {
        this.MyProperty = transactionId;
    }

    // Note: This is the constructor called by BinaryFormatter during deserialization
    public MyCustomException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        this.MyProperty = info.GetString(nameof(this.MyProperty));
    }

    // Note: This method is called by BinaryFormatter during serialization
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue(nameof(this.MyProperty), this.MyProperty);
    }
}

使用不可变类型优化复制

Orleans有一个功能,可以用来避免与序列化包含不可变类型的消息相关的一些开销。本节介绍该功能及其应用,从与之相关的上下文开始。

Orleans的序列化

当调用grain方法时,Orleans运行时会生成方法参数的深层副本,并从副本中形成请求。这可以防止调用代码在将数据传递给被调用的grain之前,修改参数对象。

如果被调用的grain位于不同的silo上,则副本最终被序列化为字节流,并通过网络发送到目标silo,在那里它们被反序列化为对象。如果被调用的grain位于同一个silo上,则副本将直接传递给被调用的方法。

返回值的处理方式相同:首先是复制,然后可能是序列化和反序列化。

请注意,所有3个进程(复制,序列化和反序列化)都遵循对象标识。换句话说,如果您传递一个列表,其中包含两个相同对象,那么在接收端,您将得到一个列表,其中也包含两个相同对象,而不是两个具有相同值的对象。

优化复制

在许多情况下,深层复制是不必要的。例如,一种可能的场景是Web前端,它从其客户端接收一个字节数组,并将该请求(包括字节数组)传递给一个grain进行处理。前端进程一旦将数组传递给grain,就不会对数组做任何事情;特别是,它不会重用该数组来接收未来的请求。在grain内部,解析字节数组以获取输入数据,但不进行修改。grain返回它创建的另一个字节数组,以传递回Web客户端; 它一返回该数据就将其数组。Web前端将结果字节数组传递回其客户端,无不进行修改。

在这种情况下,不需要复制请求或响应字节数组。不幸的是,Orleans运行时无法自行解决这个问题,因为它无法判断数组是否会在以后由Web前端或grain进行修改。在所有可能的世界中,我们有一些.NET机制来指示一个值不再被修改;因没有这种机制,我们为此添加了特定于Orleans的机制:Immutable包装类和[Immutable]属性。

使用 Immutable

Orleans.Concurrency.Immutable包装类用于表示一个值可以被认为是不变的;也就是说,底层的值不会被修改,因此安全地共享数据并不需要复制。请注意,使用Immutable意味着值的提供者和值的接收者都不会在将来修改它;它不是单方面的承诺,而是双方的共同承诺。

使用Immutable很简单:在grain接口中,不是传递T,而是传递Immutable。例如,在上述场景中,谷物方法是:

Task<byte[]> ProcessRequest(byte[] request);

改为:

Taskbyte[]>> ProcessRequest(Immutable<byte[]> request);

要创建一个Immutable,只需使用构造函数:

Immutable<byte[]> immutable = new Immutable<byte[]>(buffer);

要获取不可变的值,请使用以下.Value属性:

byte[] buffer = immutable.Value;

运用 [Immutable]

对于用户定义的类型,可以向该类型添加[Orleans.Concurrency.Immutable]属性。这指示Orleans的序列化器避免复制此类型的实例。以下代码段演示如何使用[Immutable]表示不可变类型。传输过程中不会复制此类型。

[Immutable]
public class MyImmutableType
{
    public MyImmutableType(int value)
    {
        this.MyValue = value;
    }

    public int MyValue { get; }
}

Orleans的不变性

对于Orleans的目的而言,不变性是一个相当严格的声明:数据项的内容不会以任何可能改变该项语义的方式进行修改,也不会干扰另一个线程同时访问该项。确保这一点最安全的方法是根本不修改该数据项:按位不变性,而不是逻辑不变性。

在某些情况下,将其放宽到逻辑不变性是安全的,但必须注意确保变化的代码是线程安全的;因为处理多线程很复杂,而且在Orleans的上下文中并不常见,我们强烈建议不要采用这种方法,并建议坚持按位不变性。

序列化最佳实践

序列化在Orleans有两个主要目的:

  1. 作为在运行时在grain和客户端之间传输数据的报文的格式。
  2. 作为一种存储格式,用于保存长期要用的数据,以供以后检索。

Orleans生成的序列化器因其灵活性、性能和多功能性而适用于第一个目的。它们不适合第二个目的,因为它们没有显式地版本容错。建议用户为持久化数据配置一个版本容错的序列化程序,例如Protocol Buffers。来自Microsoft.Orleans.OrleansGoogleUtils NuGet包的Orleans.Serialization.ProtobufSerializer支持Protocol Buffers。应使用所选特定序列化器的最佳实践,以确保版本容错。可以使用如上所述的SerializationProviders配置属性,来配置第三方序列化器。

你可能感兴趣的:(Orleans)