4.1.2 Orleans序列化管理器
在Orleans消息对象的定义中,Orleans消息头对象是一个定义在Orleans运行时内部的强类型对象,而通过消息体字节流传递的数据(如RPC请求消息体中的请求参数对象数组)实际为弱类型的.NET对象(即被泛化为object的.NET对象),Orleans运行时通过内置的序列化管理器(Serialization Manager)完成并处理数据传输过程中各类对象的序列化/反序列化逻辑,使Silo节点和Orleans客户端在通信过程中能够准确且高效地传输数据,并保证应用程序内的各类强类型对象在数据传输前后能被正确识别及重建。
1. Orleans通用对象序列化管理器
Orleans的内置通用对象序列化管理器的以下性质,保证了Orleans应用程序对象在进行数据传输时的“所写即所得”:
• 支持动态类型及类的多态扩展。Orleans运行时没有对传入Grain服务接口中的对象类型做任何假设或限制,因此,通用对象序列化管理器会保证数据对象在传输前后的实际类型一致。例如,若一个Grain接口的服务方法输入参数被声明为IDictionary类型,在实际运行中调用者所传入的是一个SortedDictionary类型的对象实例,处于Orleans应用集群内的远程Grain实例通过Orleans消息传输所接收到的输入对象仍将是一个由Orleans运行时本地重建后的SortedDictionary类型对象。
• 自动保存对象间的引用关系。如果同一个对象在Grain调用的不同参数中被重复使用,或在Grain调用的多个输入参数中使用了同一引用的对象,Orleans将只会将该对象实例进行一次序列化运算,并在接收方以同样的引用关系重建并还原其他对象实例对该对象的引用关系(即将在不同对象内的对象引用指针重新指向该重建后的对象实例,如图4-2所示)。例如,若Grain A向Grain B传递了一个含有100个元素的列表对象,在列表内部有10个元素实际指向了同一个本地对象,而在Grain B一侧所接收到的List对象也将包含100个元素,且其中10个元素也会指向Grain B本地内存中的同一个重建对象。
•图4-2 传输前后的对象引用关系对比
Orleans序列化管理器将Orleans应用程序内部的对象类型分为预定义类型和自定义对象类型两类。
(1)预定义类型
预定义类型是Orleans运行时内部通用对象类型的集合,包括.NET运行时定义的基本数据类型、Orleans运行时内部数据类型(如Grain Reference对象)及一些常用的.NET类型(Collection、IpEndPoint、Guid等)。
在预定义类型中,基本数据类型和常用.NET类型的序列化逻辑被包含在Orleans运行时的源码内,其构建及初始化逻辑在通用对象序列化管理器(SerializationManager)初始化步骤中执行。其他类型的对象序列化逻辑则会根据Orleans应用程序类型的不同(Grain客户端或Silo服务节点),在Orleans应用程序初始化过程中通过Orleans自动化代码生成逻辑与应用程序集类型的反射方式,按需注册至通用序列化管理器中。
(2)自定义类型
自定义类型是应用开发者在使用Orleans搭建应用服务时自定义的对象类型(如购物网站中的User类和Item类等),自定义类型的序列化逻辑是由Orleans通过代码生成器(Code Generator)在程序集编译时通过Roslyn编译器动态生成的,并在Orleans服务初始化过程中通过IOC容器注册到通用序列化管理器内部。
在生成用户自定义类型的序列化处理器时,Orleans代码生成器将依据以下原则筛选需要处理的自定义对象类型集合。
• 对于定义在用户程序集内部并引用了Orleans核心库的类型,筛选出在Grain接口方法签名或状态类签名中使用的类型,以及带有[Serializable]特性标注的类型。
• 带有[KnownType]特性标注的数据类型及[KnownAssembly]特性标注程序集中定义的所有类型。
Orleans通用对象序列化管理器实际上是维护了一组“类型-类型序列化处理器”键值对,通过传入的对象类型对对象进行相应的序列化/反序列化操作。而用户自定义对象的序列化/反序列化类型默认由代码生成器生成,极大地简化了业务的开发过程(开发人员无须在定义新的数据类型后,人工干预接口契约文件的生成及更新过程),并保证了数据类型在发送端和接收端的强一致性。Orleans对于对象的序列化/反序列化过程实际可以分为深拷贝(Deep Copy)、序列化(Serializer)及反序列化(Deserializer)三个阶段。
1)深拷贝。对象的深拷贝逻辑由对象类型绑定的序列化处理器中的深拷贝方法实现,该方法需要返回一个该对象的深拷贝对象以保证后续序列化逻辑不影响对象数据本身。对象复制阶段的一个非常重要的职责是维持对象的引用,Orleans运行时为该过程提供了一个帮助函数CheckObjectWhileCopying,开发者可以在手工复制子对象之前调用该函数以确保子对象不被重复复制。
2)序列化。序列化方法包含了对深拷贝对象的序列化逻辑,其函数签名如下。
其中input对象类型由Orleans运行时保证与所绑定的类型对象一致,而序列化方法需要将对象序列化并写入context.StreamWriter中。
3)反序列化。反序列化实际为序列化过程的逆过程,其函数签名如下。
输入参数中的expected对象同样由Orleans运行时保证与所绑定的类型对象一致,反序列化方法需要通过context.StreamReader读取出对象的序列化数据并完成对象的重建。编写序列化处理器最简单的方式是通过构造一个字节数组,并将该数组的长度与数组一同写入数据流中,在反序列化过程中通过反向处理来回复对象,该方法在对象数据类型较为紧凑且没有重复引用的子对象时具有非常高的运行效率。
2. 扩展Orleans序列化管理器
除Orleans运行时自带的通用对象序列化管理器外,Orleans运行时支持在Orleans应用程序内部使用第三方序列化管理器(需要实现IExternalSerializer接口),一些常用的第三方序列化协议已经通过Nuget包的形式在Orleans项目之外维护和发布,例如Microsoft.Orleans.Orleans-GoogleUtils包中的Orleans.Serialization.ProtobufSerializer序提供了对Google Protocol Buffers协议的序列化逻辑支持;Microsoft.Orleans.Serialization.Bond包中的Orleans.Serialization.BondSerializer提供了对Bound协议的序列化逻辑支持;若需要在应用程序中使用Newtonsoft.Json(即Json.NET)序列化,开发人员则可以直接使用Orleans核心库中的Orleans.Serialization.OrleansJsonSerializer程序集。
Orleans应用开发人员也可以通过自定义的序列化逻辑来扩展对象的默认序列化方法,但请注意,由于自定义序列化管理器的性能通常只在非常罕见的情形下优于内置序列化处理器,开发人员应当仅在自定义序列化逻辑能够显著提升系统运行效率时,对默认序列化方法进行替换及重写。
当开发者需要自定义序列化管理器时需要注意以下几点。
1)若在序列化/反序列化过程中需要忽略目标类型中的某些特定字段或属性,可以使用NonSerialized特性对其进行标注,Orleans代码生成器将自动跳过对应字段或属性的代码生成。
2)使用Immutable<T>类型或[Immutable]特性以优化不可变数据的复制过程:从Orleans运行时的角度来看,对象的不变特性声明意味着内存数据项的二进制不变性(而非逻辑不变性),即数据项的内容不会以任何形式被修改,且不会干扰并发访问该数据项的任意线程。因此Orleans序列化处理器将优化并省略对不可变数据项的复制操作(因为该数据项被声明为不可变)。
3)按需使用.NET标准库中的通用集合类型,Orleans运行时内部已经包含了对.NET标准库中通用集合类型的序列化处理器,并在字节流中对多种类型进行了特殊的缩写表示以提高性能,例如Dictionary<string,string>类型的序列化过程比List<Tuple<string,string>>更快。
一般而言,自定义序列化处理器只在对象内存在大量可以直接通过数据类型编码(或特殊编码)获取的信息时,其性能才能优于默认序列化处理器,例如,使用序列化处理器编码一个较大维度的稀疏矩阵时,将该多维数组使用“索引Z值”键值编码的方式存储将有效提升序列化处理器的压缩效率。因此,在编写自定义序列化处理器之前,应用开发人员应当应用性能分析工具仔细评估以确保序列化管理器是系统性能的瓶颈所在,并通过实际业务环境中的性能、压力测试来验证自定义序列化处理器对系统整体性能带来的提升。Orleans开发人员可以通过以下3种方式自定义指定类型的序列化处理器。
1)在自定义类型中实现序列化方法为对应的方法增加特定的特性标注(如CopierMethod、SerializerMethod、DeserializerMethod),在可以直接修改目标类型代码的应用开发场景下推荐使用此方法。
2)实现IExternalSerializer接口,并在服务配置时将序列化逻辑注册至IOC容器中,在集成外部序列化库时推荐使用此方法。
3)编写一个独立的静态类型并增加[Serializer(typeof(目标类型名))]标注,该静态类型必须同时包含3种序列化处理器依赖的方法,并通过CopierMethod、SerializerMethod、DeserializerMethod特性标注,此方法可以让开发人员对外部程序集内定义类型的序列化处理器进行替换。