深入認(rèn)識(shí)二進(jìn)制序列化--記一次生產(chǎn)事故的思考
一 概要
二進(jìn)制序列化是公司內(nèi)部自研微服務(wù)框架的主要的數(shù)據(jù)傳輸處理方式,但是普通的開發(fā)人員對(duì)于二進(jìn)制的學(xué)習(xí)和了解并不深入,容易導(dǎo)致使用過程中出現(xiàn)了問題卻沒有分析解決的思路。本文從一次生產(chǎn)環(huán)境的事故引入這個(gè)話題,通過對(duì)于事故的分析過程,探討了平時(shí)沒有關(guān)注到的一些技術(shù)要點(diǎn)。二進(jìn)制序列化結(jié)果并不像Json序列化一樣具備良好的可讀性,對(duì)于序列化的結(jié)果大多數(shù)人并不了解,因此本文最后通過實(shí)際的例子,對(duì)照MSDN的文檔對(duì)于序列化結(jié)果進(jìn)行詳細(xì)解析,并意圖通過本次分析對(duì)于二進(jìn)制序列化的結(jié)果有直觀和深入的認(rèn)識(shí)。
二 事故描述
某天晚上突發(fā)了一批預(yù)警,當(dāng)時(shí)的場(chǎng)景:
A:B,幫忙看下你們的服務(wù),我這里預(yù)警了
B:我剛發(fā)布了一個(gè)補(bǔ)丁,跟我有關(guān)?
A:我這里沒有發(fā)布,當(dāng)然有關(guān)系了,趕緊回退!
B:我這里又沒改你們用到的接口,為啥是我們回退?
A:那怪我嘍,我這里又沒發(fā)布過東西,趕緊回退!
B:這個(gè)接口很長(zhǎng)時(shí)間沒有改過,肯定是你們自己的問題。
A:不管誰的問題,咱們先回退看看。
B:行吧,稍等下
發(fā)布助手:回退中……(回退后預(yù)警消失)
A:……
B:……
三 事故問題分析
雖然事故發(fā)生后通過回退補(bǔ)丁解決了當(dāng)時(shí)的問題,但是事后對(duì)于問題的分析一直進(jìn)行到了深夜。
因?yàn)檫@次事故雖然解決起來簡(jiǎn)單,但是直接挑戰(zhàn)了我們對(duì)于服務(wù)的認(rèn)識(shí),如果不查找到根本原因,后續(xù)的工作難以放心的開展。
以前我們對(duì)于服務(wù)的認(rèn)識(shí)簡(jiǎn)單歸納為:
增加屬性不會(huì)導(dǎo)致客戶端反序列化的失敗。
但是,這個(gè)并非是官方的說法,只是開發(fā)人員在使用過程中通過實(shí)際使用總結(jié)出來的規(guī)律。經(jīng)驗(yàn)的總結(jié)往往缺乏理論的支持,在遇到問題的時(shí)候便一籌莫展。
發(fā)生問題時(shí),客戶端捕獲到的異常堆棧是這樣的:
System.Runtime.Serialization.SerializationException
?HResult=0x8013150C
?Message=ObjectManager 發(fā)現(xiàn)鏈接地址信息的數(shù)目無效。這通常表示格式化程序中有問題。
?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)
通過異常堆棧能夠看出是在進(jìn)行二進(jìn)制反序列化時(shí)發(fā)生了異常。通過多方查閱資料,針對(duì)此問題的觀點(diǎn)基本可以總結(jié)為兩點(diǎn):
反序列化使用的客戶端過舊,將反序列化使用的類替換為最新的類。
出現(xiàn)該問題跟泛型集合有關(guān),如果新增了泛型集合容易出現(xiàn)此類問題。
觀點(diǎn)一對(duì)于解決當(dāng)前問題毫無幫助,觀點(diǎn)二倒是有些用處,經(jīng)過了解,當(dāng)日發(fā)布的補(bǔ)丁中涉及的微服務(wù)接口并未新增泛型集合屬性,而是對(duì)于以前增加而未使用的一個(gè)泛型集合增加了賦值的邏輯。后來經(jīng)過測(cè)試,確實(shí)是由此處改動(dòng)造成的問題。由此也可以看出,開發(fā)人員在日常開發(fā)過程中所總結(jié)出來的經(jīng)驗(yàn)有一些局限性,有必要深入的分析下二進(jìn)制序列化在何種情況下會(huì)導(dǎo)致反序列化失敗。
四 二進(jìn)制序列化與反序列化測(cè)試
為了測(cè)試不同的數(shù)據(jù)類型對(duì)于反序列化的影響,針對(duì)常用數(shù)據(jù)類型編寫測(cè)試方案。本次測(cè)試涉及到兩個(gè)代碼解決方案,序列化的程序(簡(jiǎn)稱V1)和反序列化的程序(簡(jiǎn)稱V2)。
測(cè)試步驟:
V1中聲明類及屬性;
V1中將類對(duì)象進(jìn)行二進(jìn)制序列化并保存到文件中;
修改V1中類的屬性,去掉相關(guān)的屬性的聲明后重新編譯DLL;
V2中引用步驟3中生成的DLL,并讀取步驟2中生成的數(shù)據(jù)進(jìn)行反序列化;
/// <summary>/// V1測(cè)試過程用到的類/// </summary>[Serializable]
public class ObjectItem{
? ?public string TestStr { get; set; }
}/// <summary>/// V1測(cè)試過程用到的結(jié)構(gòu)體/// </summary>[Serializable]
public struct StructItem{
? ?public string TestStr;
}
測(cè)試常用數(shù)據(jù)類型的結(jié)果:
新增數(shù)據(jù)類型測(cè)試用的數(shù)值反序列化是否成功int100成功int[]成功string"test"成功string[]成功double1d成功double[]成功booltrue成功bool[]成功List<string>null成功List<string>{}成功List<string>成功List<int>null成功List<int>{}成功List<int>成功List<double>null成功List<double>{}成功List<double>成功List<bool>null成功List<bool>{}成功List<bool>成功ObjectItemnull成功ObjectItemnew ObjectItem()成功ObjectItem[]{}成功ObjectItem{}失敗(當(dāng)反序列化時(shí)客戶端沒有ObjectItem這個(gè)類)ObjectItem{}成功(當(dāng)反序列化時(shí)客戶端有ObjectItem這個(gè)類)List<ObjectItem>null成功List<ObjectItem>{}成功List<ObjectItem>失?。ó?dāng)反序列化時(shí)客戶端沒有ObjectItem這個(gè)類)List<ObjectItem>成功(當(dāng)反序列化時(shí)客戶端有ObjectItem這個(gè)類)StructItemnull成功StructItemnew StructItem()成功List<StructItem>null成功List<StructItem>{}成功List<StructItem>成功(當(dāng)反序列化時(shí)客戶端沒有ObjectItem這個(gè)類)List<StructItem>成功(當(dāng)反序列化時(shí)客戶端有ObjectItem這個(gè)類)
測(cè)試結(jié)果總結(jié):二進(jìn)制反序列化的時(shí)候會(huì)自動(dòng)兼容處理序列化一方新增的數(shù)據(jù)。但是在個(gè)別情況下會(huì)出現(xiàn)反序列化的過程中遇到異常的情況。
出現(xiàn)反序列化異常的數(shù)據(jù)類型:
泛型集合
數(shù)組
這兩種數(shù)據(jù)結(jié)構(gòu)并非是一定會(huì)導(dǎo)致二進(jìn)制反序列化報(bào)錯(cuò),而是有一定的條件。泛型集合出現(xiàn)反序列化異常的條件有三個(gè):
序列化的對(duì)象新增了泛型集合;
泛型使用的是新增的類;
新增的類在反序列化的時(shí)候不存在;
數(shù)組也是類似的,只有滿足上述三個(gè)條件的時(shí)候,才會(huì)導(dǎo)致二進(jìn)制反序列化失敗。這也是為什么之前發(fā)布后一直沒有問題而對(duì)于其中的泛型集合進(jìn)行賦值后出現(xiàn)微服務(wù)客戶端報(bào)錯(cuò)的原因。
既然通過測(cè)試了解到了二進(jìn)制反序列化確實(shí)會(huì)有自動(dòng)的兼容處理機(jī)制,那么有必要深入了解下MSDN上對(duì)于二進(jìn)制反序列化的容錯(cuò)機(jī)制的理論知識(shí)。
五 二進(jìn)制反序列化的容錯(cuò)機(jī)制
二進(jìn)制反序列化過程中不可避免會(huì)遇到序列化與反序列化使用的程序集版本不同的情況,如果強(qiáng)行要求反序列化的一方(比如微服務(wù)的客戶端)一定要跟序列化的一方(比如微服務(wù)的服務(wù)端)時(shí)時(shí)刻刻保持一致在實(shí)際應(yīng)用過程是不現(xiàn)實(shí)的。從.NET2.0版本開始,.NET中針對(duì)二進(jìn)制反序列化引入了版本容錯(cuò)機(jī)制(Version Tolerant Serialization,簡(jiǎn)稱VTS)。
當(dāng)使用 BinaryFormatter 時(shí),將啟用 VTS 功能。VTS 功能尤其是為應(yīng)用了 SerializableAttribute 特性的類(包括泛型類型)而啟用的。 VTS 允許向這些類添加新字段,而不破壞與該類型其他版本的兼容性。
序列化與反序列化過程中如果遇到客戶端與服務(wù)端程序集不同的情況下,.NET會(huì)盡量的進(jìn)行兼容,所以平時(shí)使用過程中對(duì)此基本沒有太大的感觸,甚至有習(xí)以為常的感覺。
要確保版本管理行為正確,修改類型版本時(shí)請(qǐng)遵循以下規(guī)則:
切勿移除已序列化的字段。
如果未在以前版本中將 NonSerializedAttribute 特性應(yīng)用于某個(gè)字段,則切勿將該特性應(yīng)用于該字段。
切勿更改已序列化字段的名稱或類型。
添加新的已序列化字段時(shí),請(qǐng)應(yīng)用 OptionalFieldAttribute 特性。
從字段(在以前版本中不可序列化)中移除 NonSerializedAttribute 特性時(shí),請(qǐng)應(yīng)用 OptionalFieldAttribute 特性。
對(duì)于所有可選字段,除非可接受 0 或 null 作為默認(rèn)值,否則請(qǐng)使用序列化回調(diào)設(shè)置有意義的默認(rèn)值。
要確保類型與將來的序列化引擎兼容,請(qǐng)遵循以下準(zhǔn)則:
始終正確設(shè)置 OptionalFieldAttribute 特性上的 VersionAdded 屬性。
避免版本管理分支。
六 二進(jìn)制序列化數(shù)據(jù)的結(jié)構(gòu)
通過前文已經(jīng)了解了二進(jìn)制序列化以及版本兼容性的理論知識(shí)。接下來有必要對(duì)于平時(shí)所用的二進(jìn)制序列化結(jié)果進(jìn)行直觀的學(xué)習(xí),消除對(duì)于二進(jìn)制序列化結(jié)果的陌生感。
6.1 遠(yuǎn)程調(diào)用過程中發(fā)送的數(shù)據(jù)
目前我們所使用的.NET微服務(wù)框架所使用的正是二進(jìn)制的數(shù)據(jù)序列化方式。當(dāng)進(jìn)行遠(yuǎn)程調(diào)用的過程中,客戶端發(fā)給服務(wù)端的數(shù)據(jù)到底是什么樣子的呢?
引用文檔中一個(gè)現(xiàn)成的例子(參考資料4):

上圖表示的是客戶端遠(yuǎn)程調(diào)用服務(wù)端的SendAddress方法,并且發(fā)送的是名為Address的類對(duì)象,該類有四個(gè)屬性:(Street = "One Microsoft Way", City = "Redmond", State = "WA" and Zip = "98054") 。服務(wù)端回復(fù)的是一個(gè)字符串“Address Received”。
客戶端實(shí)際發(fā)送的數(shù)據(jù)如下:
0000 ?00 01 00 00 00 FF FF FF FF 01 00 00 00 00 00 00 .....????.......
0010 ?00 15 14 00 00 00 12 0B 53 65 6E 64 41 64 64 72 ........SendAddr
0020 ?65 73 73 12 6F 44 4F 4A 52 65 6D 6F 74 69 6E 67 ess.oDOJRemoting
0030 ?4D 65 74 61 64 61 74 61 2E 4D 79 53 65 72 76 65 Metadata.MyServe
0040 ?72 2C 20 44 4F 4A 52 65 6D 6F 74 69 6E 67 4D 65 r, DOJRemotingMe
0050 ?74 61 64 61 74 61 2C 20 56 65 72 73 69 6F 6E 3D tadata, Version=
0060 ?31 2E 30 2E 32 36 32 32 2E 33 31 33 32 36 2C 20 1.0.2622.31326,
0070 ?43 75 6C 74 75 72 65 3D 6E 65 75 74 72 61 6C 2C Culture=neutral,
0080 ?20 50 75 62 6C 69 63 4B 65 79 54 6F 6B 65 6E 3D PublicKeyToken=
0090 ?6E 75 6C 6C 10 01 00 00 00 01 00 00 00 09 02 00 null............
00A0 ?00 00 0C 03 00 00 00 51 44 4F 4A 52 65 6D 6F 74 .......QDOJRemot
00B0 ?69 6E 67 4D 65 74 61 64 61 74 61 2C 20 56 65 72 ingMetadata, Ver
00C0 ?73 69 6F 6E 3D 31 2E 30 2E 32 36 32 32 2E 33 31 sion=1.0.2622.31
00D0 ?33 32 36 2C 20 43 75 6C 74 75 72 65 3D 6E 65 75 326, Culture=neu
00E0 ?74 72 61 6C 2C 20 50 75 62 6C 69 63 4B 65 79 54 tral, PublicKeyT
00F0 ?6F 6B 65 6E 3D 6E 75 6C 6C 05 02 00 00 00 1B 44 oken=null......D
0100 ?4F 4A 52 65 6D 6F 74 69 6E 67 4D 65 74 61 64 61 OJRemotingMetada
0110 ?74 61 2E 41 64 64 72 65 73 73 04 00 00 00 06 53 ta.Address.....S
0120 ?74 72 65 65 74 04 43 69 74 79 05 53 74 61 74 65 treet.City.State
0130 ?03 5A 69 70 01 01 01 01 03 00 00 00 06 04 00 00 .Zip............
0140 ?00 11 4F 6E 65 20 4D 69 63 72 6F 73 6F 66 74 20 ..One Microsoft
0150 ?57 61 79 06 05 00 00 00 07 52 65 64 6D 6F 6E 64 Way......Redmond
0160 ?06 06 00 00 00 02 57 41 06 07 00 00 00 05 39 38 ......WA......98
0170 ?30 35 34 0B ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 054. ?
上文的數(shù)據(jù)是二進(jìn)制的,能看出來序列化后的結(jié)果中包含程序集信息,被調(diào)用的方法、使用的參數(shù)類、屬性及各個(gè)屬性的值等信息。對(duì)于上述的序列化后數(shù)據(jù)進(jìn)行詳細(xì)解讀的分析可以參考資料4。
6.2 類對(duì)象二進(jìn)制序列化結(jié)果
對(duì)于類對(duì)象進(jìn)行序列化后的結(jié)果沒有現(xiàn)成的例子,針對(duì)此專門設(shè)計(jì)了一個(gè)簡(jiǎn)單的場(chǎng)景,將序列化后的數(shù)據(jù)保存到本地文件中。
/// <summary>/// 自定義序列化對(duì)象/// </summary>[Serializable]
public class MyObject{
? ?public bool BoolMember { get; set; }
? ?public int IntMember { get; set; }
}/// <summary>/// 程序入口/// </summary>class Program{
? ?static void Main(string[] args)
? ?{
? ? ? ?var obj = new MyObject();
? ? ? ?obj.BoolMember = true;
? ? ? ?obj.IntMember = 10000;
? ? ? ?IFormatter formatter = new BinaryFormatter();
? ? ? ?Stream stream = new FileStream("data.dat", FileMode.Create, FileAccess.Write, FileShare.None);
? ? ? ?formatter.Serialize(stream, obj);
? ? ? ?stream.Close();
? ?}
}
data.dat中的內(nèi)容:
0000: 00 01 00 00 00 ff ff ff ff 01 00 00 00 00 00 00 ?................
0010: 00 0c 02 00 00 00 4e 42 69 6e 61 72 79 53 65 72 ?......NBinarySer
0020: 69 61 6c 69 7a 65 50 72 61 63 74 69 73 65 2c 20 ?ializePractise,
0030: 56 65 72 73 69 6f 6e 3d 31 2e 30 2e 30 2e 30 2c ?Version=1.0.0.0,
0040: 20 43 75 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c ? Culture=neutral
0050: 2c 20 50 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e ?, PublicKeyToken
0060: 3d 6e 75 6c 6c 05 01 00 00 00 20 42 69 6e 61 72 ?=null..... Binar
0070: 79 53 65 72 69 61 6c 69 7a 65 50 72 61 63 74 69 ?ySerializePracti
0080: 73 65 2e 4d 79 4f 62 6a 65 63 74 02 00 00 00 1b ?se.MyObject.....
0090: 3c 42 6f 6f 6c 4d 65 6d 62 65 72 3e 6b 5f 5f 42 ?<BoolMember>k__B
00a0: 61 63 6b 69 6e 67 46 69 65 6c 64 1a 3c 49 6e 74 ?ackingField.<Int
00b0: 4d 65 6d 62 65 72 3e 6b 5f 5f 42 61 63 6b 69 6e ?Member>k__Backin
00c0: 67 46 69 65 6c 64 00 00 01 08 02 00 00 00 01 10 ?gField..........
00d0: 27 00 00 0b ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'...
對(duì)于類對(duì)象直接進(jìn)行二進(jìn)制序列化后的結(jié)果與遠(yuǎn)程調(diào)用場(chǎng)景二進(jìn)制序列化的結(jié)構(gòu)有所不同。
按照[MS-NRBF]所言,序列化后的結(jié)果首先是序列化數(shù)據(jù)頭,其中包含RecordTypeEnum、TopId、HeaderId、MajorVersion和MajorVersion。這之后就是被序列化的類的一些信息,包括程序集、類名、屬性和屬性對(duì)應(yīng)的值。
Binary Serialization Format
? SerializationHeaderRecord:
? ? ? RecordTypeEnum: SerializedStreamHeader (0x00)
? ? ? TopId: 1 (0x1)
? ? ? HeaderId: -1 (0xFFFFFFFF)
? ? ? MajorVersion: 1 (0x1)
? ? ? MinorVersion: 0 (0x0)
? Record Definition:
? ? ? RecordTypeEnum: SystemClassWithMembers (0x02)
? ? ? ClassInfo:
? ? ? ? ? ?ObjectId: ?(0x4e000000)
? ? ? ? ? ?LengthPrefixedString:
? ? ? ? ? ? ? ?Length: 78 (0x4e)
? ? ? ? ? ? ? ?String: BinarySerializePractise, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
? ? ? ? ? ?ObjectId: ?(0x00000001)
? ? ? ? ? ?LengthPrefixedString:
? ? ? ? ? ? ? ?Length: 32 (0x20)
? ? ? ? ? ? ? ?String: BinarySerializePractise.MyObject
? ? ? ? ? ?MemberCount: 2(0x00000002)
? ? ? ? ? ?LengthPrefixedString:
? ? ? ? ? ? ? ?Length: 27(0x1b)
? ? ? ? ? ? ? ?String: <BoolMember>k__BackingField
? ? ? ? ? ?LengthPrefixedString:
? ? ? ? ? ? ? ?Length: 26(0x1a)
? ? ? ? ? ? ? ?String: <IntMember>k__BackingField
? ? ? ? ? ?ObjectId:0x08010000
? ? ? ? ? ?Length:0x00000002
? ? ? ? ? ?Value:1(0x01)
? ? ? ? ? ?Value:10000(0x00002710)
? ?MessageEnd:
? ? ? ? ? ? RecordTypeEnum: MessageEnd (0x0b)
七 總結(jié)
二進(jìn)制序列化和反序列化雖然是目前使用的微服務(wù)的主要數(shù)據(jù)處理方式,但是對(duì)于開發(fā)人員來說這部分內(nèi)容比較神秘,對(duì)于序列化數(shù)據(jù)和反序列化機(jī)制不甚了解。本文中通過一次事故的分析過程,梳理總結(jié)了反序列化機(jī)制,反序列化兼容性,序列化數(shù)據(jù)結(jié)構(gòu)等內(nèi)容,希望通過本文的一些知識(shí),能夠消除對(duì)于二進(jìn)制序列化的陌生感,增進(jìn)對(duì)于二進(jìn)制序列化的深入認(rèn)識(shí)。