深刻認識二進制序列化--記一次生產事故的思考

一 概要

二進制序列化是公司內部自研微服務框架的主要的數據傳輸處理方式,可是普通的開發人員對於二進制的學習和了解並不深刻,容易致使使用過程當中出現了問題卻沒有分析解決的思路。本文從一次生產環境的事故引入這個話題,經過對於事故的分析過程,探討了平時沒有關注到的一些技術要點。二進制序列化結果並不像Json序列化同樣具有良好的可讀性,對於序列化的結果大多數人並不瞭解,所以本文最後經過實際的例子,對照MSDN的文檔對於序列化結果進行詳細解析,並意圖經過本次分析對於二進制序列化的結果有直觀和深刻的認識。c#

二 事故描述

某天晚上突發了一批預警,當時的場景:windows

A:B,幫忙看下大家的服務,我這裏預警了數組

B:我剛發佈了一個補丁,跟我有關?數據結構

A:我這裏沒有發佈,固然有關係了,趕忙回退!框架

B:我這裏又沒改大家用到的接口,爲啥是咱們回退?微服務

A:那怪我嘍,我這裏又沒發佈過東西,趕忙回退!學習

B:這個接口很長時間沒有改過,確定是大家本身的問題。測試

A:無論誰的問題,我們先回退看看。設計

B:行吧,稍等下3d

發佈助手:回退中……(回退後預警消失)

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)

經過異常堆棧可以看出是在進行二進制反序列化時發生了異常。經過多方查閱資料,針對此問題的觀點基本能夠總結爲兩點:

  1. 反序列化使用的客戶端過舊,將反序列化使用的類替換爲最新的類。
  2. 出現該問題跟泛型集合有關,若是新增了泛型集合容易出現此類問題。

觀點一對於解決當前問題毫無幫助,觀點二卻是有些用處,通過了解,當日發佈的補丁中涉及的微服務接口並未新增泛型集合屬性,而是對於之前增長而未使用的一個泛型集合增長了賦值的邏輯。後來通過測試,確實是由此處改動形成的問題。由此也能夠看出,開發人員在平常開發過程當中所總結出來的經驗有一些侷限性,有必要深刻的分析下二進制序列化在何種狀況下會致使反序列化失敗。

四 二進制序列化與反序列化測試

爲了測試不一樣的數據類型對於反序列化的影響,針對經常使用數據類型編寫測試方案。本次測試涉及到兩個代碼解決方案,序列化的程序(簡稱V1)和反序列化的程序(簡稱V2)。

測試步驟:

  1. V1中聲明類及屬性;
  2. V1中將類對象進行二進制序列化並保存到文件中;
  3. 修改V1中類的屬性,去掉相關的屬性的聲明後從新編譯DLL;
  4. 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這個類)

測試結果總結:二進制反序列化的時候會自動兼容處理序列化一方新增的數據。可是在個別狀況下會出現反序列化的過程當中遇到異常的狀況。
出現反序列化異常的數據類型:

  1. 泛型集合
  2. 數組

這兩種數據結構並不是是必定會致使二進制反序列化報錯,而是有必定的條件。泛型集合出現反序列化異常的條件有三個:

  1. 序列化的對象新增了泛型集合;
  2. 泛型使用的是新增的類;
  3. 新增的類在反序列化的時候不存在;

數組也是相似的,只有知足上述三個條件的時候,纔會致使二進制反序列化失敗。這也是爲何以前發佈後一直沒有問題而對於其中的泛型集合進行賦值後出現微服務客戶端報錯的緣由。

既然經過測試瞭解到了二進制反序列化確實會有自動的兼容處理機制,那麼有必要深刻了解下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)

七 總結

二進制序列化和反序列化雖然是目前使用的微服務的主要數據處理方式,可是對於開發人員來講這部份內容比較神祕,對於序列化數據和反序列化機制不甚瞭解。本文中經過一次事故的分析過程,梳理總結了反序列化機制,反序列化兼容性,序列化數據結構等內容,但願經過本文的一些知識,可以消除對於二進制序列化的陌生感,增進對於二進制序列化的深刻認識。

八 參考資料

  1. Some gotchas in backward compatibility
  2. 版本容錯序列化
  3. [MS-NRBF]: .NET Remoting: Binary Format Data Structure
  4. [MS-NRBF]: 3 Structure Examples
相關文章
相關標籤/搜索