1. 程式人生 > 實用技巧 >序列化 & 反序列化

序列化 & 反序列化

zz:http://www.infoq.com/cn/articles/serialization-and-deserialization

文章作者服務於美團推薦與個性化組,該組致力於為美團使用者提供每天billion級別的高質量個性化推薦以及排序服務。從Terabyte級別的使用者行為資料,到Gigabyte級別的Deal/Poi資料;從對實時性要求毫秒以內的使用者實時地理位置資料,到定期後臺job資料,推薦與重排序系統需要多種型別的資料服務。推薦與重排序系統客戶包括各種內部服務、美團客戶端、美團網站。為了提供高質量的資料服務,為了實現與上下游各系統進行良好的對接,序列化和反序列化的選型往往是我們做系統設計的一個重要考慮因素。

本文內容按如下方式組織:

  • 第一部分給出了序列化和反序列化的定義,以及其在通訊協議中所處的位置;
  • 第二部分從使用者的角度探討了序列化協議的一些特性;
  • 第三部分描述在具體的實施過程中典型的序列化元件,並與資料庫組建進行了類比;
  • 第四部分分別講解了目前常見的幾種序列化協議的特性,應用場景,並對相關元件進行舉例;
  • 最後一部分,基於各種協議的特性,以及相關benchmark資料,給出了作者的技術選型建議。

一、定義以及相關概念

網際網路的產生帶來了機器間通訊的需求,而互聯通訊的雙方需要採用約定的協議,序列化和反序列化屬於通訊協議的一部分。通訊協議往往採用分層模型,不同模型每層的功能定義以及顆粒度不同,例如:TCP/IP協議是一個四層協議,而OSI模型卻是七層協議模型。在OSI七層協議模型中展現層(Presentation Layer)的主要功能是把應用層的物件轉換成一段連續的二進位制串,或者反過來,把二進位制串轉換成應用層的物件--這兩個功能就是序列化和反序列化。一般而言,TCP/IP協議的應用層對應與OSI七層協議模型的應用層,展示層和會話層,所以序列化協議屬於TCP/IP協議應用層的一部分。本文對序列化協議的講解主要基於OSI七層協議模型。

  • 序列化: 將資料結構或物件轉換成二進位制串的過程。
  • 反序列化:將在序列化過程中所生成的二進位制串轉換成資料結構或者物件的過程。

資料結構、物件與二進位制串

不同的計算機語言中,資料結構,物件以及二進位制串的表示方式並不相同。

資料結構和物件:對於類似Java這種完全面向物件的語言,工程師所操作的一切都是物件(Object),來自於類的例項化。在Java語言中最接近資料結構的概念,就是POJO(Plain Old Java Object)或者Javabean--那些只有setter/getter方法的類。而在C二進位制串:序列化所生成的二進位制串指的是儲存在記憶體中的一塊資料。C語言的字串可以直接被傳輸層使用,因為其本質上就是以'0'結尾的儲存在記憶體中的二進位制串。在Java語言裡面,二進位制串的概念容易和String混淆。實際上String 是Java的一等公民,是一種特殊物件(Object)。對於跨語言間的通訊,序列化後的資料當然不能是某種語言的特殊資料型別。二進位制串在Java裡面所指的是byte[],byte是Java的8中原生資料型別之一(Primitive data types)。

二、序列化協議特性

每種序列化協議都有優點和缺點,它們在設計之初有自己獨特的應用場景。在系統設計的過程中,需要考慮序列化需求的方方面面,綜合對比各種序列化協議的特性,最終給出一個折衷的方案。

通用性

通用性有兩個層面的意義。

  1. 技術層面,序列化協議是否支援跨平臺、跨語言。如果不支援,在技術層面上的通用性就大大降低了。
  2. 流行程度,序列化和反序列化需要多方參與,很少人使用的協議往往意味著昂貴的學習成本;另一方面,流行度低的協議,往往缺乏穩定而成熟的跨語言、跨平臺的公共包。

強健性/魯棒性

以下兩個方面的原因會導致協議不夠強健。

  1. 成熟度不夠,一個協議從制定到實施,到最後成熟往往是一個漫長的階段。協議的強健性依賴於大量而全面的測試,對於致力於提供高質量服務的系統,採用處於測試階段的序列化協議會帶來很高的風險。
  2. 語言/平臺的不公平性。為了支援跨語言、跨平臺的功能,序列化協議的制定者需要做大量的工作;但是,當所支援的語言或者平臺之間存在難以調和的特性的時候,協議制定者需要做一個艱難的決定--支援更多人使用的語言/平臺,亦或支援更多的語言/平臺而放棄某個特性。當協議的制定者決定為某種語言或平臺提供更多支援的時候,對於使用者而言,協議的強健性就被犧牲了。

可除錯性/可讀性

序列化和反序列化的資料正確性和業務正確性的除錯往往需要很長的時間,良好的除錯機制會大大提高開發效率。序列化後的二進位制串往往不具備人眼可讀性,為了驗證序列化結果的正確性,寫入方不得同時撰寫反序列化程式,或提供一個查詢平臺--這比較費時;另一方面,如果讀取方未能成功實現反序列化,這將給問題查詢帶來了很大的挑戰--難以定位是由於自身的反序列化程式的bug所導致還是由於寫入方序列化後的錯誤資料所導致。對於跨公司間的除錯,由於以下原因,問題會顯得更嚴重。

  1. 支援不到位,跨公司除錯在問題出現後可能得不到及時的支援,這大大延長了調試周期。
  2. 訪問限制,除錯階段的查詢平臺未必對外公開,這增加了讀取方的驗證難度。

如果序列化後的資料人眼可讀,這將大大提高除錯效率, XML和JSON就具有人眼可讀的優點。

效能

效能包括兩個方面,時間複雜度和空間複雜度。

  1. 空間開銷(Verbosity), 序列化需要在原有的資料上加上描述欄位,以為反序列化解析之用。如果序列化過程引入的額外開銷過高,可能會導致過大的網路,磁碟等各方面的壓力。對於海量分散式儲存系統,資料量往往以TB為單位,巨大的的額外空間開銷意味著高昂的成本。
  2. 時間開銷(Complexity),複雜的序列化協議會導致較長的解析時間,這可能會使得序列化和反序列化階段成為整個系統的瓶頸。

可擴充套件性/相容性

移動互聯時代,業務系統需求的更新週期變得更快,新的需求不斷湧現,而老的系統還是需要繼續維護。如果序列化協議具有良好的可擴充套件性,支援自動增加新的業務欄位,而不影響老的服務,這將大大提供系統的靈活度。

安全性/訪問限制

在序列化選型的過程中,安全性的考慮往往發生在跨區域網訪問的場景。當通訊發生在公司之間或者跨機房的時候,出於安全的考慮,對於跨區域網的訪問往往被限制為基於HTTP/HTTPS的80和443埠。如果使用的序列化協議沒有相容而成熟的HTTP傳輸層框架支援,可能會導致以下三種結果之一:

  1. 因為訪問限制而降低服務可用性;
  2. 被迫重新實現安全協議而導致實施成本大大提高;
  3. 開放更多的防火牆埠和協議訪問,而犧牲安全性。

三、序列化和反序列化的元件

典型的序列化和反序列化過程往往需要如下元件。

  • IDL(Interface description language)檔案:參與通訊的各方需要對通訊的內容需要做相關的約定(Specifications)。為了建立一個與語言和平臺無關的約定,這個約定需要採用與具體開發語言、平臺無關的語言來進行描述。這種語言被稱為介面描述語言(IDL),採用IDL撰寫的協議約定稱之為IDL檔案。
  • IDL Compiler:IDL檔案中約定的內容為了在各語言和平臺可見,需要有一個編譯器,將IDL檔案轉換成各語言對應的動態庫。
  • Stub/Skeleton Lib:負責序列化和反序列化的工作程式碼。Stub是一段部署在分散式系統客戶端的程式碼,一方面接收應用層的引數,並對其序列化後通過底層協議棧傳送到服務端,另一方面接收服務端序列化後的結果資料,反序列化後交給客戶端應用層;Skeleton部署在服務端,其功能與Stub相反,從傳輸層接收序列化引數,反序列化後交給服務端應用層,並將應用層的執行結果序列化後最終傳送給客戶端Stub。
  • Client/Server:指的是應用層程式程式碼,他們面對的是IDL所生存的特定語言的class或struct。
  • 底層協議棧和網際網路:序列化之後的資料通過底層的傳輸層、網路層、鏈路層以及物理層協議轉換成數字訊號在網際網路中傳遞。

序列化元件與資料庫訪問元件的對比

資料庫訪問對於很多工程師來說相對熟悉,所用到的元件也相對容易理解。下表類比了序列化過程中用到的部分元件和資料庫訪問元件的對應關係,以便於大家更好的把握序列化相關元件的概念。

|序列化元件|資料庫元件|說明| |-----:| |IDL|DDL|用於建表或者模型的語言| |DL file|DB Schema|表建立檔案或模型檔案| |Stub/Skeleton|lib O/R mapping|將class和Table或者資料模型進行對映|

四、幾種常見的序列化和反序列化協議

網際網路早期的序列化協議主要有COM和CORBA。

COM主要用於Windows平臺,並沒有真正實現跨平臺,另外COM的序列化的原理利用了編譯器中虛表,使得其學習成本巨大(想一下這個場景, 工程師需要是簡單的序列化協議,但卻要先掌握語言編譯器)。由於序列化的資料與編譯器緊耦合,擴充套件屬性非常麻煩。

CORBA是早期比較好的實現了跨平臺,跨語言的序列化協議。COBRA的主要問題是參與方過多帶來的版本過多,版本之間相容性較差,以及使用複雜晦澀。這些政治經濟,技術實現以及早期設計不成熟的問題,最終導致COBRA的漸漸消亡。J2SE 1.3之後的版本提供了基於CORBA協議的RMI-IIOP技術,這使得Java開發者可以採用純粹的Java語言進行CORBA的開發。

這裡主要介紹和對比幾種當下比較流行的序列化協議,包括XML、JSON、Protobuf、Thrift和Avro。

一個例子

如前所述,序列化和反序列化的出現往往晦澀而隱蔽,與其他概念之間往往相互包容。為了更好了讓大家理解序列化和反序列化的相關概念在每種協議裡面的具體實現,我們將一個例子穿插在各種序列化協議講解中。在該例子中,我們希望將一個使用者資訊在多個系統裡面進行傳遞;在應用層,如果採用Java語言,所面對的類物件如下所示:

class Address
{
	private String city;
	private String postcode;
	private String street;
}
public class UserInfo
{
	private Integer userid;
	private String name;
	private List<address> address;
}
</address>

XML&SOAP

XML是一種常用的序列化和反序列化協議,具有跨機器,跨語言等優點。 XML歷史悠久,其1.0版本早在1998年就形成標準,並被廣泛使用至今。XML的最初產生目標是對網際網路文件(Document)進行標記,所以它的設計理念中就包含了對於人和機器都具備可讀性。 但是,當這種標記文件的設計被用來序列化物件的時候,就顯得冗長而複雜(Verbose and Complex)。 XML本質上是一種描述語言,並且具有自我描述(Self-describing)的屬性,所以XML自身就被用於XML序列化的IDL。 標準的XML描述格式有兩種:DTD(Document Type Definition)和XSD(XML Schema Definition)。作為一種人眼可讀(Human-readable)的描述語言,XML被廣泛使用在配置檔案中,例如O/R mapping、 Spring Bean Configuration File 等。

SOAP(Simple Object Access protocol) 是一種被廣泛應用的,基於XML為序列化和反序列化協議的結構化訊息傳遞協議。SOAP在網際網路影響如此大,以至於我們給基於SOAP的解決方案一個特定的名稱--Web service。SOAP雖然可以支援多種傳輸層協議,不過SOAP最常見的使用方式還是XML+HTTP。SOAP協議的主要介面描述語言(IDL)是WSDL(Web Service Description Language)。SOAP具有安全、可擴充套件、跨語言、跨平臺並支援多種傳輸層協議。如果不考慮跨平臺和跨語言的需求,XML的在某些語言裡面具有非常簡單易用的序列化使用方法,無需IDL檔案和第三方編譯器, 例如Java+XStream。

自我描述與遞迴

SOAP是一種採用XML進行序列化和反序列化的協議,它的IDL是WSDL. 而WSDL的描述檔案是XSD,而XSD自身是一種XML檔案。 這裡產生了一種有趣的在數學上稱之為“遞迴”的問題,這種現象往往發生在一些具有自我屬性(Self-description)的事物上。

IDL檔案舉例

採用WSDL描述上述使用者基本資訊的例子如下:

<xsd:complexType name='Address'>
	 <xsd:attribute name='city' type='xsd:string' />
	 <xsd:attribute name='postcode' type='xsd:string' />
	 <xsd:attribute name='street' type='xsd:string' />
</xsd:complexType>
<xsd:complexType name='UserInfo'>
	 <xsd:sequence>
	 <xsd:element name='address' type='tns:Address'/>
	 <xsd:element name='address1' type='tns:Address'/>
	 </xsd:sequence>
	 <xsd:attribute name='userid' type='xsd:int' />
	 <xsd:attribute name='name' type='xsd:string' />
</xsd:complexTyp>

典型應用場景和非應用場景

SOAP協議具有廣泛的群眾基礎,基於HTTP的傳輸協議使得其在穿越防火牆時具有良好安全特性,XML所具有的人眼可讀(Human-readable)特性使得其具有出眾的可除錯性,網際網路頻寬的日益劇增也大大彌補了其空間開銷大(Verbose)的缺點。對於在公司之間傳輸資料量相對小或者實時性要求相對低(例如秒級別)的服務是一個好的選擇。由於XML的額外空間開銷大,序列化之後的資料量劇增,對於資料量巨大序列持久化應用常景,這意味著巨大的記憶體和磁碟開銷,不太適合XML。另外,XML的序列化和反序列化的空間和時間開銷都比較大,對於對效能要求在ms級別的服務,不推薦使用。WSDL雖然具備了描述物件的能力,SOAP的S代表的也是simple,但是SOAP的使用絕對不簡單。對於習慣於面向物件程式設計的使用者,WSDL檔案不直觀。

JSON(Javascript Object Notation)

JSON起源於弱型別語言Javascript, 它的產生來自於一種稱之為"Associative array"的概念,其本質是就是採用"Attribute-value"的方式來描述物件。實際上在Javascript和PHP等弱型別語言中,類的描述方式就是Associative array。JSON的如下優點,使得它快速成為最廣泛使用的序列化協議之一。

  1. 這種Associative array格式非常符合工程師對物件的理解。
  2. 它保持了XML的人眼可讀(Human-readable)的優點。
  3. 相對於XML而言,序列化後的資料更加簡潔。 來自於的以下連結的研究表明:XML所產生序列化之後檔案的大小接近JSON的兩倍。http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
  4. 它具備Javascript的先天性支援,所以被廣泛應用於Web browser的應用常景中,是Ajax的事實標準協議。
  5. 與XML相比,其協議比較簡單,解析速度比較快。
  6. 鬆散的Associative array使得其具有良好的可擴充套件性和相容性。

IDL悖論

JSON實在是太簡單了,或者說太像各種語言裡面的類了,所以採用JSON進行序列化不需要IDL。這實在是太神奇了,存在一種天然的序列化協議,自身就實現了跨語言和跨平臺。然而事實沒有那麼神奇,之所以產生這種假象,來自於兩個原因。

  1. Associative array在弱型別語言裡面就是類的概念,在PHP和Javascript裡面Associative array就是其class的實際實現方式,所以在這些弱型別語言裡面,JSON得到了非常良好的支援。
  2. IDL的目的是撰寫IDL檔案,而IDL檔案被IDL Compiler編譯後能夠產生一些程式碼(Stub/Skeleton),而這些程式碼是真正負責相應的序列化和反序列化工作的元件。 但是由於Associative array和一般語言裡面的class太像了,他們之間形成了一一對應關係,這就使得我們可以採用一套標準的程式碼進行相應的轉化。對於自身支援Associative array的弱型別語言,語言自身就具備操作JSON序列化後的資料的能力;對於Java這強型別語言,可以採用反射的方式統一解決,例如Google提供的Gson。

典型應用場景和非應用場景

JSON在很多應用場景中可以替代XML,更簡潔並且解析速度更快。典型應用場景包括:

  1. 公司之間傳輸資料量相對小,實時性要求相對低(例如秒級別)的服務。
  2. 基於Web browser的Ajax請求。
  3. 由於JSON具有非常強的前後相容性,對於介面經常發生變化,並對可調式性要求高的場景,例如Mobile app與服務端的通訊。
  4. 由於JSON的典型應用場景是JSON+HTTP,適合跨防火牆訪問。總的來說,採用JSON進行序列化的額外空間開銷比較大,對於大資料量服務或持久化,這意味著巨大的記憶體和磁碟開銷,這種場景不適合。沒有統一可用的IDL降低了對參與方的約束,實際操作中往往只能採用文件方式來進行約定,這可能會給除錯帶來一些不便,延長開發週期。 由於JSON在一些語言中的序列化和反序列化需要採用反射機制,所以在效能要求為ms級別,不建議使用。

IDL檔案舉例

以下是UserInfo序列化之後的一個例子:

{"userid":1,"name":"messi","address":[{"city":"北京","postcode":"1000000","street":"wangjingdonglu"}]}

Thrift

Thrift是Facebook開源提供的一個高效能,輕量級RPC服務框架,其產生正是為了滿足當前大資料量、分散式、跨語言、跨平臺資料通訊的需求。 但是,Thrift並不僅僅是序列化協議,而是一個RPC框架。相對於JSON和XML而言,Thrift在空間開銷和解析效能上有了比較大的提升,對於對效能要求比較高的分散式系統,它是一個優秀的RPC解決方案;但是由於Thrift的序列化被嵌入到Thrift框架裡面,Thrift框架本身並沒有透出序列化和反序列化介面,這導致其很難和其他傳輸層協議共同使用(例如HTTP)。

典型應用場景和非應用場景

對於需求為高效能,分散式的RPC服務,Thrift是一個優秀的解決方案。它支援眾多語言和豐富的資料型別,並對於資料欄位的增刪具有較強的相容性。所以非常適用於作為公司內部的面向服務構建(SOA)的標準RPC框架。

不過Thrift的文件相對比較缺乏,目前使用的群眾基礎相對較少。另外由於其Server是基於自身的Socket服務,所以在跨防火牆訪問時,安全是一個顧慮,所以在公司間進行通訊時需要謹慎。 另外Thrift序列化之後的資料是Binary陣列,不具有可讀性,除錯程式碼時相對困難。最後,由於Thrift的序列化和框架緊耦合,無法支援向持久層直接讀寫資料,所以不適合做資料持久化序列化協議。

IDL檔案舉例

struct Address
{ 1: required string city;
	2: optional string postcode;
	3: optional string street;
} struct UserInfo
{ 1: required string userid;
	2: required i32 name;
	3: optional list<address> address;
}
</address>

Protobuf

Protobuf具備了優秀的序列化協議的所需的眾多典型特徵。

  1. 標準的IDL和IDL編譯器,這使得其對工程師非常友好。
  2. 序列化資料非常簡潔,緊湊,與XML相比,其序列化之後的資料量約為1/3到1/10。
  3. 解析速度非常快,比對應的XML快約20-100倍。
  4. 提供了非常友好的動態庫,使用非常簡介,反序列化只需要一行程式碼。

Protobuf是一個純粹的展示層協議,可以和各種傳輸層協議一起使用;Protobuf的文件也非常完善。 但是由於Protobuf產生於Google,所以目前其僅僅支援Java、C#### 典型應用場景和非應用場景 Protobuf具有廣泛的使用者基礎,空間開銷小以及高解析效能是其亮點,非常適合於公司內部的對效能要求高的RPC呼叫。由於Protobuf提供了標準的IDL以及對應的編譯器,其IDL檔案是參與各方的非常強的業務約束,另外,Protobuf與傳輸層無關,採用HTTP具有良好的跨防火牆的訪問屬性,所以Protobuf也適用於公司間對效能要求比較高的場景。由於其解析效能高,序列化後資料量相對少,非常適合應用層物件的持久化場景。

它的主要問題在於其所支援的語言相對較少,另外由於沒有繫結的標準底層傳輸層協議,在公司間進行傳輸層協議的除錯工作相對麻煩。

IDL檔案舉例

message Address
{
	required string city=1;
		optional string postcode=2;
		optional string street=3;
}
message UserInfo
{
	required string userid=1;
	required string name=2;
	repeated Address address=3;
}

Avro

Avro的產生解決了JSON的冗長和沒有IDL的問題,Avro屬於Apache Hadoop的一個子專案。 Avro提供兩種序列化格式:JSON格式或者Binary格式。Binary格式在空間開銷和解析效能方面可以和Protobuf媲美,JSON格式方便測試階段的除錯。 Avro支援的資料型別非常豐富,包括C#### 典型應用場景和非應用場景 Avro解析效能高並且序列化之後的資料非常簡潔,比較適合於高效能的序列化服務。

由於Avro目前非JSON格式的IDL處於實驗階段,而JSON格式的IDL對於習慣於靜態型別語言的工程師來說不直觀。

IDL檔案舉例

protocol Userservice {
record Address {
string city;
string postcode;
string street;
}
record UserInfo {
string name;
int userid;
array<Address> address = [];
}
}

所對應的JSON Schema格式如下:

{
  "protocol" : "Userservice",
  "namespace" : "org.apache.avro.ipc.specific",
  "version" : "1.0.5",
  "types" : [ {
	"type" : "record",
	"name" : "Address",
	"fields" : [ {
	  "name" : "city",
	  "type" : "string"
	}, {
	  "name" : "postcode",
	  "type" : "string"
	}, {
	  "name" : "street",
	  "type" : "string"
	} ]
  }, {
	"type" : "record",
	"name" : "UserInfo",
	"fields" : [ {
	  "name" : "name",
	  "type" : "string"
	}, {
	  "name" : "userid",
	  "type" : "int"
	}, {
	  "name" : "address",
	  "type" : {
		"type" : "array",
		"items" : "Address"
	  },
	  "default" : [ ]
	} ]
  } ],
  "messages" : { }
}

五、Benchmark以及選型建議

Benchmark

以下資料來自https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking

解析效能

序列化之空間開銷

從上圖可得出如下結論:

  1. XML序列化(Xstream)無論在效能和簡潔性上比較差;
  2. Thrift與Protobuf相比在時空開銷方面都有一定的劣勢;
  3. Protobuf和Avro在兩方面表現都非常優越。

選型建議

以上描述的五種序列化和反序列化協議都各自具有相應的特點,適用於不同的場景。

  1. 對於公司間的系統呼叫,如果效能要求在100ms以上的服務,基於XML的SOAP協議是一個值得考慮的方案。
  2. 基於Web browser的Ajax,以及Mobile app與服務端之間的通訊,JSON協議是首選。對於效能要求不太高,或者以動態型別語言為主,或者傳輸資料載荷很小的的運用場景,JSON也是非常不錯的選擇。
  3. 對於除錯環境比較惡劣的場景,採用JSON或XML能夠極大的提高除錯效率,降低系統開發成本。
  4. 當對效能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關係。
  5. 對於T級別的資料的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化後的資料儲存在Hadoop子專案裡,Avro會是更好的選擇。
  6. 由於Avro的設計理念偏向於動態型別語言,對於動態語言為主的應用場景,Avro是更好的選擇。
  7. 對於持久層非Hadoop專案,以靜態型別語言為主的應用場景,Protobuf會更符合靜態型別語言工程師的開發習慣。
  8. 如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇。
  9. 如果序列化之後需要支援不同的傳輸層協議,或者需要跨防火牆訪問的高效能場景,Protobuf可以優先考慮。

參考文獻

  1. http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
  2. https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
  3. http://en.wikipedia.org/wiki/Serialization
  4. http://en.wikipedia.org/wiki/Soap
  5. http://en.wikipedia.org/wiki/XML
  6. http://en.wikipedia.org/wiki/JSON
  7. http://avro.apache.org/
  8. http://www.oracle.com/technetwork/java/rmi-iiop-139743.html