天天看點

[Hadoop]序列化機制

傳統的計算機系統通過i/o操作與外界進行交流,hadoop的i/o由傳統的i/o系統發展而來,但又有些不同,hadoop需要處理p、t級别的資料,是以在org.apache.hadoop.io包中包含了一些面向海量資料處理的基本輸入輸出工具。

1 序列化

對象的序列化(serialization)用于将對象編碼成一個位元組流,以及從位元組流中重新建構對象。“将一個對象編碼成一個位元組流”稱為序列化該對象(serializing);相反的處理過程稱為反序列化(deserializing)。

序列化有三種主要的用途:

作為一種持久化格式:一個對象被序列化以後,它的編碼可以被存儲到磁盤上,供以後反序列化用。

作為一種通信資料格式:序列化結果可以從一個正在運作的虛拟機,通過網絡被傳遞到另一個虛拟機上。

作為一種拷貝、克隆(clone)機制:将對象序列化到記憶體的緩存區中,然後通過反序列化,可以得到一個對已存對象進行深拷貝的新對象。

在分布式資料進行中,主要使用上面提到的前兩種功能:資料持久化和通信資料格式。

在分析hadoop的序列化機制前,先介紹一下java内建的序列化機制。

2.java内建的序列化機制

java序列化機制将對象轉換為連續的byte資料,這些資料可以在日後還原為原先的對象狀态。在java中,使一個類的執行個體可被序列化非常簡單,隻需要在類聲明中加入implements serializable即可。serializable接口是一個标志,不具有任何成員函數,其定義如下:

serializable接口沒有任何方法,是以不需要對類進行修改,block類通過聲明它實作了serializable接口,立即可以獲得java提供的序列化功能。代碼如下:

由于序列化主要應用在與i/o相關的一些操作上,其實作是通過一對輸入/輸出流來實作的。如果想對某個對象執行序列化動作,可以在某種outputstream對象(後面還會讨論java的流)的基礎上建立一個對象流objectoutputstream對象,然後調用writeobject()就可達到目的。

writeobject()方法負責寫入實作了serializable接口對象的狀态資訊,輸出資料将被送至該outputstream。多個對象的序列化可以在objectoutputstream對象上多次調用writeobject(),分别寫入這些對象。下面是序列化一個block對象的例子:

對于java基本類型的序列化,objectoutputstream提供了writeboolean()、writebyte()等方法。

輸入過程類似,将inputstream包裝在objectinputstream中并調用readobject(),該方法傳回一個指向向上轉型後的object的引用,通過向下轉型,就可以得到正确結果。讀取對象時,必須要小心地跟蹤存儲的對象的數量、順序以及它們的類型。

java的序列化機制非常“聰明”,javadoc中對objectoutputstream的writeobject()方法的說明是:“……這個對象的類、類簽名、類的所有非暫态和非靜态成員的值,以及它所有的父類都要被寫入”,序列化機制會自動通路對象的父類,以保證對象内容的一緻性。同時,序列化機制不僅存儲對象在記憶體中的原始資料,還會追蹤通過該對象可以到達的其他對象的内部資料,并描述所有這些對象是如何被連結起來的。對于複雜的情形,java序列化機制也能應付自如:在輸出objecta和objectb時,不會重複儲存對象的序列化結果(如objectc,即objectc隻被序列化一次);對于循環引用的對象,序列化也不會陷入死循環(如圖3-1右圖的情形)。

[Hadoop]序列化機制

但是,序列化以後的對象在尺寸上有點過于充實了,以block類為例,它隻包含3個長整數,但是它的序列化結果竟然有112位元組,而blockmetadatainfo其實隻多了一個long型的成員變量,輸出結果已經膨脹到190位元組。包含3個長整數的block對象的序列化結果如下:

仔細看block的輸出會發現,序列化的結果中包含了大量與類相關的資訊。java的序列過程在《java object serialization specification》中規範,以block為例,其結果的前兩個位元組是魔數(magic number)“ac ed”;後續兩個位元組是序列化格式的版本号,現在使用的版本号是5;接下來是類的描述資訊,包括類的版本id、是否實作writeobject()和readobject()方法等資訊,對于擁有超類的類(如blockmetadatainfo),超類的資訊也會遞歸地被儲存下來;這些資訊都寫入outputstream對象後,接下來才是對象的資料。在這個過程中,序列化輸出中儲存了大量的附加資訊,導緻序列化結果膨脹,對于需要儲存和處理大規模資料的hadoop來說,需要一個新的序列化機制。

3.hadoop序列化機制

和java序列化機制不同(在對象流objectoutputstream對象上調用writeobject()方法),hadoop的序列化機制通過調用對象的write()方法(它帶有一個類型為dataoutput的參數),将對象序列化到流中。反序列化的過程也是類似,通過對象的readfields(),從流中讀取資料。值得一提的是,java序列化機制中,反序列化過程會不斷地建立新的對象,但在hadoop的序列化機制的反序列化過程中,使用者可以複用對象:如,在block的某個對象上反複調用readfields(),可以在同一個對象上得到多個反序列化的結果,而不是多個反序列化的結果對象(對象被複用了),這減少了java對象的配置設定和回收,提高了應用的效率。

由于block對象序列化時隻輸出了3個長整數, block1的序列化結果一共有24位元組,如下所示。和java的序列化機制的輸出結果對比,hadoop的序列化結果緊湊而且快速。

4.hadoop序列化機制的特征

對于處理大規模資料的hadoop平台,其序列化機制需要具有如下特征:

緊湊:由于帶寬是hadoop叢集中最稀缺的資源,一個緊湊的序列化機制可以充分利用資料中心的帶寬。

快速:在程序間通信(包括mapreduce過程中涉及的資料互動)時會大量使用序列化機制,是以,必須盡量減少序列化和反序列化的開銷。

可擴充:随着系統的發展,系統間通信的協定會更新,類的定義會發生變化,序列化機制需要支援這些更新和變化。

互操作:可以支援不同開發語言間的通信,如c++和java間的通信。這樣的通信,可以通過檔案(需要精心設計檔案的格式)或者後面介紹的ipc機制實作。

java的序列化機制雖然強大,卻不符合上面這些要求。java serialization将每個對象的類名寫入輸出流中,這導緻了java序列化對象需要占用比原對象更多的存儲空間。同時,為了減少資料量,同一個類的對象的序列化結果隻輸出一份中繼資料,并通過某種形式的引用,來共享中繼資料。引用導緻對序列化後的流進行處理的時候,需要保持一些狀态。想象如下一種場景,在一個上百g的檔案中,反序列化某個對象,需要通路檔案中前面的某一個中繼資料,這将導緻這個檔案不能切割,并通過mapreduce來處理。同時,java序列化會不斷地建立新的對象,對于mapreduce應用來說,不能重用對象,在已有對象上進行反序列化操作,而是不斷建立反序列化的各種類型記錄,這會帶來大量的系統開銷。

5.hadoop writable機制

為了支援以上這些特性,hadoop引入org.apache.hadoop.io.writable接口,作為所有可序列化對象必須實作的接口。writable機制緊湊、快速(但不容易擴充到java以外的語言,如c、python等)。和java.io.serializable不同,writable接口不是一個說明性接口,它包含兩個方法:

writable.write()方法用于将對象狀态寫入二進制的dataoutput中,反序列化的過程由readfields()從datainput流中讀取狀态完成。下面是一個例子:

這個例子使用的是前面分析java序列化機制的block類,block實作了writable接口,即需要實作write()方法和readfields()方法,這兩個方法的實作都很簡單:block有三個成員變量,write()方法簡單地把這三個變量寫入流中,而readfields()則從流中依次讀入這些資料,并做必要的檢查。