二进制序列化是公司内部自研微服务框架的主要的数据传输处理方式,但是普通的开发人员对于二进制的学习和了解并不深入,容易导致使用过程中出现了问题却没有分析解决的思路。本文从一次生产环境的事故引入这个话题,通过对于事故的分析过程,探讨了平时没有关注到的一些技术要点。二进制序列化结果并不像Json序列化一样具备良好的可读性,对于序列化的结果大多数人并不了解,因此本文最后通过实际的例子,对照MSDN的文档对于序列化结果进行详细解析,并意图通过本次分析对于二进制序列化的结果有直观和深入的认识。
二 事故描述某天晚上突发了一批预警,当时的场景:
A:B,帮忙看下你们的服务,我这里预警了
B:我刚发布了一个补丁,跟我有关?
A:我这里没有发布,当然有关系了,赶紧回退!
B:我这里又没改你们用到的接口,为啥是我们回退?
A:那怪我喽,我这里又没发布过东西,赶紧回退!
B:这个接口很长时间没有改过,肯定是你们自己的问题。
A:不管谁的问题,咱们先回退看看。
B:行吧,稍等下
发布助手:回退中……(回退后预警消失)
A:……
B:……
三 事故问题分析虽然事故发生后通过回退补丁解决了当时的问题,但是事后对于问题的分析一直进行到了深夜。
因为这次事故虽然解决起来简单,但是直接挑战了我们对于服务的认识,如果不查找到根本原因,后续的工作难以放心的开展。
以前我们对于服务的认识简单归纳为:
增加属性不会导致客户端反序列化的失败。
但是,这个并非是官方的说法,只是开发人员在使用过程中通过实际使用总结出来的规律。经验的总结往往缺乏理论的支持,在遇到问题的时候便一筹莫展。
发生问题时,客户端捕获到的异常堆栈是这样的:
System.Runtime.Serialization.SerializationException HResult=0x8013150C Message=ObjectManager 发现链接地址信息的数目无效。这通常表示格式化程序中有问题。 Source=mscorlib StackTrace: 在 System.Runtime.Serialization.ObjectManager.DoFixups() 在 System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) 在 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) 在 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream)通过异常堆栈能够看出是在进行二进制反序列化时发生了异常。通过多方查阅资料,针对此问题的观点基本可以总结为两点:
反序列化使用的客户端过旧,将反序列化使用的类替换为最新的类。
出现该问题跟泛型集合有关,如果新增了泛型集合容易出现此类问题。
观点一对于解决当前问题毫无帮助,观点二倒是有些用处,经过了解,当日发布的补丁中涉及的微服务接口并未新增泛型集合属性,而是对于以前增加而未使用的一个泛型集合增加了赋值的逻辑。后来经过测试,确实是由此处改动造成的问题。由此也可以看出,开发人员在日常开发过程中所总结出来的经验有一些局限性,有必要深入的分析下二进制序列化在何种情况下会导致反序列化失败。
四 二进制序列化与反序列化测试为了测试不同的数据类型对于反序列化的影响,针对常用数据类型编写测试方案。本次测试涉及到两个代码解决方案,序列化的程序(简称V1)和反序列化的程序(简称V2)。
测试步骤:
V1中声明类及属性;
V1中将类对象进行二进制序列化并保存到文件中;
修改V1中类的属性,去掉相关的属性的声明后重新编译DLL;
V2中引用步骤3中生成的DLL,并读取步骤2中生成的数据进行反序列化;
/// <summary> /// V1测试过程用到的类 /// </summary> [Serializable] public class ObjectItem { public string TestStr { get; set; } } /// <summary> /// V1测试过程用到的结构体 /// </summary> [Serializable] public struct StructItem { public string TestStr; }测试常用数据类型的结果:
新增数据类型 测试用的数值 反序列化是否成功int 100 成功
int[] {1,100} 成功
string "test" 成功
string[] {"a","1"} 成功
double 1d 成功
double[] {1d,2d} 成功
bool true 成功
bool[] {false,true} 成功
List<string> null 成功
List<string> {} 成功
List<string> {"1","a"} 成功
List<int> null 成功
List<int> {} 成功
List<int> {1,100} 成功
List<double> null 成功
List<double> {} 成功
List<double> {1d,100d} 成功
List<bool> null 成功
List<bool> {} 成功
List<bool> {true,false} 成功
ObjectItem null 成功
ObjectItem new ObjectItem() 成功
ObjectItem[] {} 成功
ObjectItem{} {new ObjectItem()} 失败(当反序列化时客户端没有ObjectItem这个类)
ObjectItem{} {new ObjectItem()} 成功(当反序列化时客户端有ObjectItem这个类)
List<ObjectItem> null 成功
List<ObjectItem> {} 成功
List<ObjectItem> {new ObjectItem()} 失败(当反序列化时客户端没有ObjectItem这个类)
List<ObjectItem> {new ObjectItem()} 成功(当反序列化时客户端有ObjectItem这个类)
StructItem null 成功
StructItem new StructItem() 成功
List<StructItem> null 成功
List<StructItem> {} 成功
List<StructItem> {new StructItem()} 成功(当反序列化时客户端没有ObjectItem这个类)
List<StructItem> {new StructItem()} 成功(当反序列化时客户端有ObjectItem这个类)