事故描述某天晚上突发了一批预警当时的场景AB帮忙看下你们的服务我这里预警了B我刚发布了一个补丁跟我有关A我这里没有发布当然有关系了赶紧回退B我这里又没改你们用到的接口为啥是我们回退A那怪我喽我这里又没发布过东西赶紧回退B这个接口很长时间没有改过肯定是你们自己的问题。A不管谁的问题咱们先回退看看。B行吧稍等下发布助手回退中……回退后预警消失A……B……三 事故问题分析虽然事故发生后通过回退补丁解决了当时的问题但是事后对于问题的分析一直进行到了深夜。因为这次事故虽然解决起来简单但是直接挑战了我们对于服务的认识如果不查找到根本原因后续的工作难以放心的开展。以前我们对于服务的认识简单归纳为增加属性不会导致客户端反序列化的失败。但是这个并非是官方的说法只是开发人员在使用过程中通过实际使用总结出来的规律。经验的总结往往缺乏理论的支持在遇到问题的时候便一筹莫展。发生问题时客户端捕获到的异常堆栈是这样的System.Runtime.Serialization.SerializationException HResult0x8013150C MessageObjectManager 发现链接地址信息的数目无效。这通常表示格式化程序中有问题。 Sourcemscorlib 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中类的属性去掉相关的属性的声明后重新编译DLLV2中引用步骤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; }测试常用数据类型的结果新增数据类型测试用的数值反序列化是否成功int100成功int[]成功stringtest成功string[]成功double1d成功double[]成功booltrue成功bool[]成功Liststringnull成功Liststring{}成功Liststring成功Listintnull成功Listint{}成功Listint成功Listdoublenull成功Listdouble{}成功Listdouble成功Listboolnull成功Listbool{}成功Listbool成功ObjectItemnull成功ObjectItemnew ObjectItem()成功ObjectItem[]{}成功ObjectItem{}失败当反序列化时客户端没有ObjectItem这个类ObjectItem{}成功当反序列化时客户端有ObjectItem这个类ListObjectItemnull成功ListObjectItem{}成功ListObjectItem失败当反序列化时客户端没有ObjectItem这个类ListObjectItem成功当反序列化时客户端有ObjectItem这个类StructItemnull成功StructItemnew StructItem()成功ListStructItemnull成功ListStructItem{}成功ListStructItem成功当反序列化时客户端没有ObjectItem这个类ListStructItem成功当反序列化时客户端有ObjectItem这个类测试结果总结二进制反序列化的时候会自动兼容处理序列化一方新增的数据。但是在个别情况下会出现反序列化的过程中遇到异常的情况。出现反序列化异常的数据类型泛型集合数组这两种数据结构并非是一定会导致二进制反序列化报错而是有一定的条件。泛型集合出现反序列化异常的条件有三个序列化的对象新增了泛型集合泛型使用的是新增的类新增的类在反序列化的时候不存在数组也是类似的只有满足上述三个条件的时候才会导致二进制反序列化失败。这也是为什么之前发布后一直没有问题而对于其中的泛型集合进行赋值后出现微服务客户端报错的原因。既然通过测试了解到了二进制反序列化确实会有自动的兼容处理机制那么有必要深入了解下MSDN上对于二进制反序列化的容错机制的理论知识。五 二进制反序列化的容错机制二进制反序列化过程中不可避免会遇到序列化与反序列化使用的程序集版本不同的情况如果强行要求反序列化的一方比如微服务的客户端一定要跟序列化的一方比如微服务的服务端时时刻刻保持一致在实际应用过程是不现实的。从.NET2.0版本开始.NET中针对二进制反序列化引入了版本容错机制(Version Tolerant Serialization简称VTS)。当使用 BinaryFormatter 时将启用 VTS 功能。VTS 功能尤其是为应用了 SerializableAttribute 特性的类包括泛型类型而启用的。 VTS 允许向这些类添加新字段而不破坏与该类型其他版本的兼容性。序列化与反序列化过程中如果遇到客户端与服务端程序集不同的情况下.NET会尽量的进行兼容所以平时使用过程中对此基本没有太大的感触甚至有习以为常的感觉。要确保版本管理行为正确修改类型版本时请遵循以下规则切勿移除已序列化的字段。如果未在以前版本中将 NonSerializedAttribute 特性应用于某个字段则切勿将该特性应用于该字段。切勿更改已序列化字段的名称或类型。添加新的已序列化字段时请应用 OptionalFieldAttribute 特性。从字段在以前版本中不可序列化中移除 NonSerializedAttribute 特性时请应用 OptionalFieldAttribute 特性。对于所有可选字段除非可接受 0 或 null 作为默认值否则请使用序列化回调设置有意义的默认值。要确保类型与将来的序列化引擎兼容请遵循以下准则始终正确设置 OptionalFieldAttribute 特性上的 VersionAdded 属性。避免版本管理分支。六 二进制序列化数据的结构通过前文已经了解了二进制序列化以及版本兼容性的理论知识。接下来有必要对于平时所用的二进制序列化结果进行直观的学习消除对于二进制序列化结果的陌生感。6.1 远程调用过程中发送的数据目前我们所使用的.NET微服务框架所使用的正是二进制的数据序列化方式。当进行远程调用的过程中客户端发给服务端的数据到底是什么样子的呢引用文档中一个现成的例子(参考资料4)上图表示的是客户端远程调用服务端的SendAddress方法并且发送的是名为Address的类对象该类有四个属性(Street One Microsoft Way, City Redmond, State WA and Zip 98054) 。服务端回复的是一个字符串“Address Received”。客户端实际发送的数据如下