我們的Hadoop生產環境有兩個版本,其中一個是1.0.3,爲了支持日誌壓縮和split,我們添加了hadoop-1.2中關於Bzip2壓縮的feature. 一切運行良好。
爲了滿足公司對迭代計算的需求(複雜HiveSQL,廣告推薦算法,機器學習 etc), 我們構建了自己的Spark集羣,最初是Standalone Mode,版本spark-0.9.1,支持Shark。
上線後,問題接踵而來,最爲致命的是,shark在處理Hadooop bzip2文件時計算結果通常會有偏差,有時差的特別離譜(比如,用shark統計1個5kw行的日誌,結果只有
3kw行).
顯然shark+hive+spark+hadoop的某個環節出了bug。第一次面對這麼複雜的系統,着實頭疼。
於是,開始蠻幹,部署shark+hive+spark+hadoop開發環境,debug,查看出問題的環節。(這個過程中把Spark-core的源碼也縷了一遍),始終沒有發現什麼問題。
後來,參加了Spark技術大會,和同行交流的過程中,幡然悔悟: Spark的task是線程級併發的,而Hadoop MR的task是進程級併發的,那麼,會不會是Bzip2存在線程安全問題呢?
回來後,查看Bzip2Codec相關的代碼,終於發現了問題所在。(話說,凌晨3點,沒有eclipse,用vim 改的), 迫不及待的重新編譯Hadoop和Spark,測試,發現處理Bzip2結果OK了!
CBzip2InputStream 有一個flag變量private static boolean skipDecompression ,默認值是false. (表示是否要跳出解壓縮?)
這個變量在CBZip2InputStream的 public static long numberOfBytesTillNextMarker方法裏被置爲true. 在CBZip2InputStream.read方法裏可能被置爲false;
看來,它是一個流(inputStream)讀取過程中經常用到的flag變量.
假設,現在同一個進程內,有兩個線程都要做Bzip2流的讀取工作,它們共用一個static類型的標誌位skipDecompression. 顯然會出問題的。
解決辦法:將其skipDecompression改成非static類型,修改調用skipDecompression的靜態方法爲非static類型。
(PS: Bzip2Codec.createInputStream 方法用於將給定的流InputStream轉換成CBzip2InputStream. 是Bizp2文件讀取時的必經方法。該方法裏調用了CBZip2InputStream.numberOfBytesTillNextMarker靜態方法,這裏導致了多線程讀取BZip2流時skipDecompresion標誌位混亂)
由於向社區提交path需要漫長的過程,暫時沒有提交社區。具體的patch如下,如有同行遇到同類問題,請借鑑.
Index: src/core/org/apache/hadoop/io/compress/bzip2/CBZip2InputStream.java
===================================================================
--- src/core/org/apache/hadoop/io/compress/bzip2/CBZip2InputStream.java (版本 510)
+++ src/core/org/apache/hadoop/io/compress/bzip2/CBZip2InputStream.java (版本 525)
@@ -129,7 +129,9 @@
private int computedBlockCRC, computedCombinedCRC;
private boolean skipResult = false;// used by skipToNextMarker
- private static boolean skipDecompression = false;
+ //modified by jicheng.song
+ //private static boolean skipDecompression = false;
+ private boolean skipDecompression = false;
// Variables used by setup* methods exclusively
@@ -315,13 +317,18 @@
* @throws IOException
*
*/
- public static long numberOfBytesTillNextMarker(final InputStream in) throws IOException{
- CBZip2InputStream.skipDecompression = true;
- CBZip2InputStream anObject = null;
+ //modified by jicheng.song
+ //public static long numberOfBytesTillNextMarker(final InputStream in) throws IOException{
+ public long numberOfBytesTillNextMarker(final InputStream in) throws IOException{
+ this.skipDecompression = true;
+ //
+ this.in = new BufferedInputStream(in, 1024 * 9);// >1 MB buffer
+ this.readMode = readMode;
+ //CBZip2InputStream anObject = null;
- anObject = new CBZip2InputStream(in, READ_MODE.BYBLOCK);
+ //anObject = new CBZip2InputStream(in, READ_MODE.BYBLOCK);
- return anObject.getProcessedByteCount();
+ return this.getProcessedByteCount();
}
public CBZip2InputStream(final InputStream in) throws IOException {
@@ -395,7 +402,9 @@
if(skipDecompression){
changeStateToProcessABlock();
- CBZip2InputStream.skipDecompression = false;
+ //modified by jicheng.song
+ //CBZip2InputStream.skipDecompression = false;
+ this.skipDecompression = false;
}
final int hi = offs + len;
Index: src/core/org/apache/hadoop/io/compress/BZip2Codec.java
===================================================================
--- src/core/org/apache/hadoop/io/compress/BZip2Codec.java (版本 510)
+++ src/core/org/apache/hadoop/io/compress/BZip2Codec.java (版本 525)
@@ -148,8 +148,11 @@
// BZip2 start of block markers are of 6 bytes. But the very first block
// also has "BZh9", making it 10 bytes. This is the common case. But at
// time stream might start without a leading BZ.
+ CBZip2InputStream tmpInput = new CBZip2InputStream(seekableIn, readMode);
final long FIRST_BZIP2_BLOCK_MARKER_POSITION =
- CBZip2InputStream.numberOfBytesTillNextMarker(seekableIn);
+ //modified by jicheng.song
+ //CBZip2InputStream.numberOfBytesTillNextMarker(seekableIn);
+ tmpInput.numberOfBytesTillNextMarker(seekableIn);
long adjStart = Math.max(0L, start - FIRST_BZIP2_BLOCK_MARKER_POSITION);
((Seekable)seekableIn).seek(adjStart);