文章做者服務於美團推薦與個性化組,該組致力於爲美團用戶提供天天billion級別的高質量個性化推薦以及排序服務。從Terabyte級別的用戶行爲數據,到Gigabyte級別的Deal/Poi數據;從對實時性要求毫秒之內的用戶實時地理位置數據,到按期後臺job數據,推薦與重排序系統須要多種類型的數據服務。推薦與重排序系統客戶包括各類內部服務、美團客戶端、美團網站。爲了提供高質量的數據服務,爲了實現與上下游各系統進行良好的對接,序列化和反序列化的選型每每是咱們作系統設計的一個重要考慮因素。html
本文內容按以下方式組織:java
互聯網的產生帶來了機器間通信的需求,而互聯通信的雙方須要採用約定的協議,序列化和反序列化屬於通信協議的一部分。通信協議每每採用分層模型,不一樣模型每層的功能定義以及顆粒度不一樣,例如:TCP/IP協議是一個四層協議,而OSI模型倒是七層協議模型。在OSI七層協議模型中展示層(Presentation Layer)的主要功能是把應用層的對象轉換成一段連續的二進制串,或者反過來,把二進制串轉換成應用層的對象--這兩個功能就是序列化和反序列化。通常而言,TCP/IP協議的應用層對應與OSI七層協議模型的應用層,展現層和會話層,因此序列化協議屬於TCP/IP協議應用層的一部分。本文對序列化協議的講解主要基於OSI七層協議模型。數據庫
相關廠商內容apache
相關贊助商編程
CNUTCon全球運維技術大會,9月10日-9月11日,上海·光大會展中心大酒店,精彩內容搶先看數組
不一樣的計算機語言中,數據結構,對象以及二進制串的表示方式並不相同。安全
數據結構和對象:對於相似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)。網絡
每種序列化協議都有優勢和缺點,它們在設計之初有本身獨特的應用場景。在系統設計的過程當中,須要考慮序列化需求的方方面面,綜合對比各類序列化協議的特性,最終給出一個折衷的方案。數據結構
通用性有兩個層面的意義。架構
如下兩個方面的緣由會致使協議不夠強健。
序列化和反序列化的數據正確性和業務正確性的調試每每須要很長的時間,良好的調試機制會大大提升開發效率。序列化後的二進制串每每不具有人眼可讀性,爲了驗證序列化結果的正確性,寫入方不得同時撰寫反序列化程序,或提供一個查詢平臺--這比較費時;另外一方面,若是讀取方未能成功實現反序列化,這將給問題查找帶來了很大的挑戰--難以定位是因爲自身的反序列化程序的bug所致使仍是因爲寫入方序列化後的錯誤數據所致使。對於跨公司間的調試,因爲如下緣由,問題會顯得更嚴重。
若是序列化後的數據人眼可讀,這將大大提升調試效率, XML和JSON就具備人眼可讀的優勢。
性能包括兩個方面,時間複雜度和空間複雜度。
移動互聯時代,業務系統需求的更新週期變得更快,新的需求不斷涌現,而老的系統仍是須要繼續維護。若是序列化協議具備良好的可擴展性,支持自動增長新的業務字段,而不影響老的服務,這將大大提供系統的靈活度。
在序列化選型的過程當中,安全性的考慮每每發生在跨局域網訪問的場景。當通信發生在公司之間或者跨機房的時候,出於安全的考慮,對於跨局域網的訪問每每被限制爲基於HTTP/HTTPS的80和443端口。若是使用的序列化協議沒有兼容而成熟的HTTP傳輸層框架支持,可能會致使如下三種結果之一:
典型的序列化和反序列化過程每每須要以下組件。
數據庫訪問對於不少工程師來講相對熟悉,所用到的組件也相對容易理解。下表類比了序列化過程當中用到的部分組件和數據庫訪問組件的對應關係,以便於你們更好的把握序列化相關組件的概念。
|序列化組件|數據庫組件|說明| |-----:| |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是一種經常使用的序列化和反序列化協議,具備跨機器,跨語言等優勢。 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)的事物上。
採用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, 它的產生來自於一種稱之爲"Associative array"的概念,其本質是就是採用"Attribute-value"的方式來描述對象。實際上在Javascript和PHP等弱類型語言中,類的描述方式就是Associative array。JSON的以下優勢,使得它快速成爲最普遍使用的序列化協議之一。
JSON實在是太簡單了,或者說太像各類語言裏面的類了,因此採用JSON進行序列化不須要IDL。這實在是太神奇了,存在一種自然的序列化協議,自身就實現了跨語言和跨平臺。然而事實沒有那麼神奇,之因此產生這種假象,來自於兩個緣由。
JSON在不少應用場景中能夠替代XML,更簡潔而且解析速度更快。典型應用場景包括:
如下是UserInfo序列化以後的一個例子:
{"userid":1,"name":"messi","address":[{"city":"北京","postcode":"1000000","street":"wangjingdonglu"}]}
Thrift是Facebook開源提供的一個高性能,輕量級RPC服務框架,其產生正是爲了知足當前大數據量、分佈式、跨語言、跨平臺數據通信的需求。 可是,Thrift並不只僅是序列化協議,而是一個RPC框架。相對於JSON和XML而言,Thrift在空間開銷和解析性能上有了比較大的提高,對於對性能要求比較高的分佈式系統,它是一個優秀的RPC解決方案;可是因爲Thrift的序列化被嵌入到Thrift框架裏面,Thrift框架自己並無透出序列化和反序列化接口,這致使其很難和其餘傳輸層協議共同使用(例如HTTP)。
對於需求爲高性能,分佈式的RPC服務,Thrift是一個優秀的解決方案。它支持衆多語言和豐富的數據類型,並對於數據字段的增刪具備較強的兼容性。因此很是適用於做爲公司內部的面向服務構建(SOA)的標準RPC框架。
不過Thrift的文檔相對比較缺少,目前使用的羣衆基礎相對較少。另外因爲其Server是基於自身的Socket服務,因此在跨防火牆訪問時,安全是一個顧慮,因此在公司間進行通信時須要謹慎。 另外Thrift序列化以後的數據是Binary數組,不具備可讀性,調試代碼時相對困難。最後,因爲Thrift的序列化和框架緊耦合,沒法支持向持久層直接讀寫數據,因此不適合作數據持久化序列化協議。
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是一個純粹的展現層協議,能夠和各類傳輸層協議一塊兒使用;Protobuf的文檔也很是完善。 可是因爲Protobuf產生於Google,因此目前其僅僅支持Java、C#### 典型應用場景和非應用場景 Protobuf具備普遍的用戶基礎,空間開銷小以及高解析性能是其亮點,很是適合於公司內部的對性能要求高的RPC調用。因爲Protobuf提供了標準的IDL以及對應的編譯器,其IDL文件是參與各方的很是強的業務約束,另外,Protobuf與傳輸層無關,採用HTTP具備良好的跨防火牆的訪問屬性,因此Protobuf也適用於公司間對性能要求比較高的場景。因爲其解析性能高,序列化後數據量相對少,很是適合應用層對象的持久化場景。
它的主要問題在於其所支持的語言相對較少,另外因爲沒有綁定的標準底層傳輸層協議,在公司間進行傳輸層協議的調試工做相對麻煩。
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的產生解決了JSON的冗長和沒有IDL的問題,Avro屬於Apache Hadoop的一個子項目。 Avro提供兩種序列化格式:JSON格式或者Binary格式。Binary格式在空間開銷和解析性能方面能夠和Protobuf媲美,JSON格式方便測試階段的調試。 Avro支持的數據類型很是豐富,包括C#### 典型應用場景和非應用場景 Avro解析性能高而且序列化以後的數據很是簡潔,比較適合於高性能的序列化服務。
因爲Avro目前非JSON格式的IDL處於實驗階段,而JSON格式的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" : { } }
如下數據來自https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking。
從上圖可得出以下結論:
以上描述的五種序列化和反序列化協議都各自具備相應的特色,適用於不一樣的場景。
感謝丁曉昀對本文的審校。