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

一 概要

二進制序列化是公司內部自研微服務框架的主要的數據傳輸處理方式,但是普通的開發人員對于二進制的學習和了解并不深入,容易導致使用過程中出現了問題卻沒有分析解決的思路。本文從一次生產環境的事故引入這個話題,通過對于事故的分析過程,探討了平時沒有關注到的一些技術要點。二進制序列化結果并不像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)

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

  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
posted @ 2019-07-01 23:33  hkant  閱讀(5967)  評論(17編輯  收藏
最新chease0ldman老人|无码亚洲人妻下载|大香蕉在线看好吊妞视频这里有精品www|亚洲色情综合网