一 概要 二進位序列化是公司內部自研微服務框架的主要的數據傳輸處理方式,但是普通的開發人員對於二進位的學習和瞭解並不深入,容易導致使用過程中出現了問題卻沒有分析解決的思路。本文從一次生產環境的事故引入這個話題,通過對於事故的分析過程,探討了平時沒有關註到的一些技術要點。二進位序列化結果並不像Json ...
一 概要
二進位序列化是公司內部自研微服務框架的主要的數據傳輸處理方式,但是普通的開發人員對於二進位的學習和瞭解並不深入,容易導致使用過程中出現了問題卻沒有分析解決的思路。本文從一次生產環境的事故引入這個話題,通過對於事故的分析過程,探討了平時沒有關註到的一些技術要點。二進位序列化結果並不像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這個類) |
測試結果總結:二進位反序列化的時候會自動相容處理序列化一方新增的數據。但是在個別情況下會出現反序列化的過程中遇到異常的情況。
出現反序列化異常的數據類型:
- 泛型集合
- 數組
這兩種數據結構並非是一定會導致二進位反序列化報錯,而是有一定的條件。泛型集合出現反序列化異常的條件有三個:
- 序列化的對象新增了泛型集合;
- 泛型使用的是新增的類;
- 新增的類在反序列化的時候不存在;
數組也是類似的,只有滿足上述三個條件的時候,才會導致二進位反序列化失敗。這也是為什麼之前發佈後一直沒有問題而對於其中的泛型集合進行賦值後出現微服務客戶端報錯的原因。
既然通過測試瞭解到了二進位反序列化確實會有自動的相容處理機制,那麼有必要深入瞭解下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”。
客戶端實際發送的數據如下:
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.
上文的數據是二進位的,能看出來序列化後的結果中包含程式集信息,被調用的方法、使用的參數類、屬性及各個屬性的值等信息。對於上述的序列化後數據進行詳細解讀的分析可以參考資料4。
6.2 類對象二進位序列化結果
對於類對象進行序列化後的結果沒有現成的例子,針對此專門設計了一個簡單的場景,將序列化後的數據保存到本地文件中。
/// <summary>
/// 自定義序列化對象
/// </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中的內容:
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 '...
對於類對象直接進行二進位序列化後的結果與遠程調用場景二進位序列化的結構有所不同。
按照[MS-NRBF]所言,序列化後的結果首先是序列化數據頭,其中包含RecordTypeEnum、TopId、HeaderId、MajorVersion和MajorVersion。這之後就是被序列化的類的一些信息,包括程式集、類名、屬性和屬性對應的值。
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)
七 總結
二進位序列化和反序列化雖然是目前使用的微服務的主要數據處理方式,但是對於開發人員來說這部分內容比較神秘,對於序列化數據和反序列化機制不甚瞭解。本文中通過一次事故的分析過程,梳理總結了反序列化機制,反序列化相容性,序列化數據結構等內容,希望通過本文的一些知識,能夠消除對於二進位序列化的陌生感,增進對於二進位序列化的深入認識。
八 參考資料
- Some gotchas in backward compatibility
- 版本容錯序列化
- [MS-NRBF]: .NET Remoting: Binary Format Data Structure
- [MS-NRBF]: 3 Structure Examples