進入到第四章了,本篇主要聊的點是編碼(也就是 序列化 )與代碼升級的一些場景,來梳理存儲之中涉及到的編解碼的流程。目前主流的編解碼便是來自Apache的 Avro ,來自Facebook的 Thrift 與Google的 Protocolbuf ,在本篇之中,我們也會一一梳理各種編碼的優點與痛點。 ...
進入到第四章了,本篇主要聊的點是編碼(也就是序列化)與代碼升級的一些場景,來梳理存儲之中涉及到的編解碼的流程。目前主流的編解碼便是來自Apache的Avro,來自Facebook的Thrift與Google的Protocolbuf,在本篇之中,我們也會一一梳理各種編碼的優點與痛點。
1.非二進位的編碼格式
程式通常以至少兩種不同的表示方式處理數據:
1、在記憶體中,數據是保存在對象、結構、列表、數組、哈希表、樹、等等。這些數據結構在記憶體之中被優化為CPU可以高效訪問和操作的結構(通常這是操作系統的任務,並不需要程式員操心)。
2、而當你想把數據寫入一個文件或者通過網路發送它時,你必須把它編碼成某種形式的位元組序列(例如,一個JSON文檔)。
因此,我們需要兩種形式之間的某種轉換。(記憶體與其他位置)翻譯從記憶體中表示的數據稱之為編碼(也稱為序列化),反之稱為解碼(反序列化)。
通常編碼有如下幾種格式:
- 特定的語言格式
許多編程語言都對編碼有內置的支持,用於將記憶體對象編碼成位元組序列。例如:Java的java.io.Serializable , Ruby的Marshal, Python的pickle。但是這些編程語言內置的庫存在一些深層次的問題。 - 編碼通常與特定的編程語言捆綁在一起,用另一種語言讀取數據是非常困難的
- 為了在同一對象類型中恢複數據,解碼過程需要能夠實例化任意類,如果攻擊者可以讓您的應用程式解碼任意位元組序列,則它們可以實例化任意類。這常常是安全問題的來源。
效率(用於編碼或解碼的CPU時間,以及編碼結構的大小),java內置編碼庫臭名昭著的就是其糟糕的表現和臃腫的編碼
- JSON、XML與CSV
上面這幾種格式,也是我們在編碼之中常見到的。 - XML的描述十分精準,但是因過於冗長。
- JSON的流行主要歸功於它在Web瀏覽器中的內置支持(由於它是JavaScript的一個子集)和相對於XML的簡單性。
CSV是另一種流行的與語言無關的格式,儘管功能不強。
JSON、XML和CSV都是文本格式,因此都具有一定的可讀性。但他們也有如下一些微妙的問題:
- 關於數字的編碼有很多歧義。在XML和CSV中,不能區分恰好由數字組成的數字和字元串(除了引用外部模式)。JSON區分字元串和數字,但它不區分整數和浮點數,也不能確認精度。
- JSON與XML為Unicode字元串的支持,但他們不支持二進位字元串(位元組序列沒有字元編碼)。
- 對於XML和JSON,都有可選的模式支持。這些模式語言非常強大,因此學習和實現起來相當複雜。而CSV沒有任何模式,因此需要應用程式定義每個行和列的含義。如果應用程式添加了新行或列,則必須手動處理該更新。CSV是一個相當模糊的格式(出於是分隔符的原因)
2.二進位的編碼格式
二進位的編碼格式通常是最緊湊的編碼格式,對於一個小的數據集,編碼大小的收益是微不足道的,但一旦進入百萬兆位元組的數據集,數據格式的選擇就會有很大的影響了。接下來我們來看一個通過JSON描述的數據結構:
- MessagPack
我們來看看通過MessagePack進行二進位編碼之後的JSON格式:
二進位編碼長度為66個位元組,這僅比81位元組的文本JSON編碼小了一點。通過這樣的空間減少便喪失了可讀性的保障,我們來看看有木有更優秀的解決方式。 - Thrift
在Thrift中的數據進行編碼,需要預先在Thrift介面定義語言(IDL)中描述這樣的模式:
在Thrift之中存在兩種不同的二進位編碼格式,一種是直接使用二進位編碼的Binary格式,另一種則是使用壓縮之後的Compact格式,我們來一一看兩者的區別。
Binary格式編碼之後為59個位元組大小,並且每個欄位都有一個類型註釋(用於指示它是字元串、整數、列表等),併在需要時指定長度指示(字元串的長度、列表中項的數量)。但是和MessagePack相比就省去了欄位名等信息,取而代之的是欄位標記(1,2和3),這些是出現在模式定義中的數字。欄位標記類似於欄位別名,它們是一種簡潔的方式來描述我們所談論的欄位,而不必拼寫欄位名稱。從而減少了二進位編碼的大小。
Compact格式它包含相同的信息只有34個位元組。它通過將欄位類型和標記號打包成一個位元組,並使用可變長度整數來實現這一點。它不是為1337號使用八個完整的位元組,而是用兩個位元組編碼,每個位元組的最高位用來指示是否還有更多的位元組要來。這意味著64到63之間的數字用一個位元組編碼,8192到8191之間的數字用兩個位元組編碼,較大的數字使用更多位元組。
ProtocolBuf
Protocolbuf(只有一個二進位編碼格式)相同的數據編碼如下圖所示。它位包裝略有不同,但Thrift的Compact格式大同小異。Protobuf以33位元組匹配相同的記錄。
Avro
Avro是一個二進位編碼格式,它是發源於開源項目Hadoop,來作為Thrift的替換方案存在的,我們來看看通過Avro編碼之後的記錄,又是怎麼樣的呢?
在Avro模式之中沒有標記號。將同樣的數據進行編碼,Avro二進位編碼是32個位元組長,是上述編碼之中最緊湊的。檢查上述的位元組序列,並沒有標識欄位或數據類型。編碼簡單地由連接在一起的值組成。在解析二進位數據時,通過使用模式來確定每個欄位的數據類型。這意味著如果讀取數據的代碼與寫入數據的代碼使用完全相同的模式,二進位數據才能被正確地解碼。
3.模式升級與演化
隨著應用程式的開發,模式不可避免地需要隨著時間而改變。而在這個過程之中,二進位編碼同時保持向後和向前相容性呢?
- 欄位標記
- 從示例中可以看到,編碼的記錄只是編碼欄位的串聯。每個欄位由標簽號碼和註釋的數據類型識別(如字元串或整數)。如果沒有設置欄位值,則只需從已編碼的記錄中省略該欄位值。因此欄位標記對編碼數據的含義至關重要。我們可以更改模式中欄位的名稱,因為編碼的數據從不引用欄位名稱,但不能更改欄位的標記,因為這將使所有現有編碼數據無效。
- 可以通過添加一個新的標記號的方式向模式添加新欄位。如果舊代碼(不知道您添加的新標記號)試圖讀取由新代碼編寫的數據,包括一個新欄位,該欄位的標記號不識別,它可以簡單地忽略該欄位。數據類型註釋允許分析器來確定需要跳過多少位元組。因為每個欄位都有唯一的標記號,新代碼可以無縫連接舊的數據,因為標記號仍然具有相同的含義。但是,如果是添加了一個新欄位,則不能使它成為必需欄位。如果要添加一個欄位並使其成為必需的欄位,那麼如果新代碼讀取舊代碼編寫的數據,則該檢查將失敗,因為舊代碼將不會寫入您添加的新欄位。因此,為了保持向後相容性,在初始部署模式之後添加的每個欄位必須是可選的或具有預設值。
刪除欄位就像添加欄位一樣,這意味著只能刪除一個可選的欄位(必填欄位不能被刪除),而且您不能再次使用相同的標記號(因為您可能還有一個包含舊標記號的數據,該欄位必須被新代碼忽略)。
數據類型
如何改變欄位的數據類型?例如,將32位整數轉換為64位整數。新代碼可以很容易地讀取舊代碼編寫的數據,因為解析器可以用零填充任何丟失的位。但是,如果舊代碼讀取由新代碼編寫的數據,舊代碼仍然使用32位變數來保存值。如果解碼的64位值不適合32位,會被截斷。
Protocolbuf並沒有一個列表或數組的數據類型,而是有一個重覆的標記欄位。可以將可選的(單值)欄位轉換為重覆的(多值)欄位。讀取舊數據的新代碼看到一個具有零個或一個元素的列表(取決於欄位是否存在);讀取新數據的舊代碼只看到列表的最後一個元素。而Thrift有一個專門的列表數據類型,這是參數列表中的數據類型。這不允許像Protocolbuf那樣從單值到多值的升級,但它具有支持嵌套列表的優點。動態生成模式
Avro最大的特點是支持了動態生成模式,它的核心思想是編碼者與解碼者的模式可以不同,事實上他們只需要相容就可以了。相比於Protocolbuf和Thrift,它並不包含任何標簽數字。每當資料庫模式發生變化時,管理員必須手動更新從資料庫列名到欄位標記的映射。而Avro是每次運行時簡單地進行模式轉換。任何讀取新數據文件的程式都會感知到記錄的欄位發生了變化。
4.小結
編碼的細節不僅影響到工作效率,更重要的是會影響到應用程式和軟體的架構。Prorotocol Buf,Thrift 與 Avro,都使用一個模式來描述一個二進位編碼格式。它們的模式語言比XML模式或JSON模式要簡單得多,它支持更詳細的驗證規則,並且能夠更好的進行模式的演化升級,在性能上也有了更好的提升。