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

来源:https://www.cnblogs.com/zhu-wj/archive/2019/07/01/11117541.html
-Advertisement-
Play Games

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

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

  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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring容器是一個大工廠,負責創建、管理所有的Bean。 Spring容器支持2種格式的配置文件:xml文件、properties文件,最常用的是xml文件。 Bean在xml文件中的配置 <beans> 根元素,可包含多個<bean>元素,一個<bean>即一個Bean的配置。 <bean> ...
  • 今天我開始重新一點一滴的學習python,雖然之前也學習過python,但是當時的學習比較雜亂,知識點較為凌亂,所以感覺學習效果不是很好,寫一些程式或者是演算法都不能夠得心應手,因此決定重新學習學習python,因為之前有學習過C語言和JAVA的原因,學習過程比較快,但是還是要告誡一些初學者或者是其他 ...
  • Day1 1.電腦基礎 1. 什麼是電腦 輸入輸出設備 CPU(中央處理器):處理各種數據,相當於人的大腦 記憶體:存儲數據,相當於人的臨時記憶 硬碟:存儲數據,相當於人的永久記憶 2. 什麼是操作系統 控制電腦硬體工作的流程 軟體 3. 什麼是應用程式 安裝在操作系統之上的軟體 2.Pytho ...
  • [TOC] 一、源碼下載 Qt庫封裝了很多很控制項,種類也比較多,其中容器控制項包括:表格、樹和列表。 使用過QtDesigner的同學應該都知道,這個工具中有一個屬性編輯器,是一個表格樹控制項,就像vs中控制項屬性面板一樣。 今天我們就來介紹一款使用QTreeWidget封裝的表格樹控制項QtTreePro ...
  • 前言 只有光頭才能變強。 文本已收錄至我的GitHub倉庫,歡迎Star: "https://github.com/ZhongFuCheng3y/3y" 回顧上一篇: "《大型網站系統與Java中間件》讀書筆記(一)" 這周周末讀了第四章,現在過來做做筆記,希望能幫助到大家。 註:在看這篇文章之前, ...
  • 微信公眾號掃一掃功能提示:10003 redirect_uri功能變數名稱與後臺不一致 Senparc.Weixin組件很好用,但一個坑,不知道這和個是否有關。。 先說明下環境,centos+.net core 2.2 .netcore 直接dotnet run ,用nohup運行起來,配置埠為80,Us ...
  • Redis 是一個開源的使用 ANSI C語言編寫的支持網路、可基於記憶體也可持久化的日誌型、Key Value 資料庫。 常用它來存儲緩存數據,能非常輕鬆的實現緩存過期刷新機制。 多種語言都可以連接到 Redis 資料庫伺服器,本文將推薦一個非常簡潔的 C 連接 Redis 資料庫的開源項目。 一般 ...
  • REST & x5E38;& x7528;http& x52A8;& x8BCD; WebApi & x5728; Asp.NetCore & x4E2D;& x7684;& x5B9E;& x73B0; 3.1. & x521B;& x5EFA;WebApi& x9879;& x76EE;. 3. ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...