假設你有上百G的數據,你要統計出這些數據中,含有某些你感興趣的內容的數據的有多少條,你會怎麼做?在硬件條件允許的情況下,用hadoop並行計算是一個不錯的選擇。
爲了使本文得以清晰地說明,我們不妨假設如下的情況:
我們有100G的數據,分別保存在5個文件中,它們位於 /data/ 目錄下。這5個數據文件的內容均爲相同的格式,即,文件的內容大致如下:
ABCDSDFKJDKF kkk 2890876
SDKFJIEORUEW nnn 1231238
LSFKDFSSDRDE
bbb 9234999
說明:每一行內容中,首先是一個12字節的字符串,然後是一個3字節的字符串,然後是一個7個數字組成的字符串。字符串之間是用空格分隔的。
我們的問題是:在這100G的數據中,請統計出第二項字符串爲“kkk”和“nnn”的數據分別有多少條?
如果用一個非分佈式的應用程序來計算這個問題,如果計算機硬件配置不夠強勁的話,那麼估計得算到天荒地老了。
而用hadoop來並行計算,一切都是那麼簡單。
下面,我們就來看看,如何用C++開發一個hadoop上的應用程序,來完成我們的任務。
儘管hadoop平臺是用Java寫的,但是它仍然支持用C++來開發應用程序,這裏不討論優劣對比,只是基於這樣一個事實:有些人覺得用C++寫更熟悉,所以我們才用C++寫。
先說明:本文基於hadoop 0.20.2版本。
(1)首先我們需要知道map-reduce的基本原理,這裏不說了。其次我們需要知道,在用C++編寫hadoop應用程序時,需要包含三個頭文件:
#include "Pipes.hh"
#include "TemplateFactory.hh"
#include "StringUtils.hh"
這三個文件在hadoop安裝包的 “c++\Linux-amd64-64\include\” 或 “c++\Linux-i386-32\include\” 子目錄下(根據你的操作系統是64位或32位,分別對應不同的目錄)。
既然有頭文件,就需要有對應的實現文件,或者動態/靜態庫,這裏我用的是靜態庫 libhadooppipes.a 和 libhadooputils.a 。靜態庫是在Makefile中指定的,後面再說。這裏特別提醒一下大家:如果你的hadoop集羣不是隻有一臺服務器,那麼如果你編譯時使用了任何動態庫的話,在運行的時候就要保證在別的hadoop服務器上也能找到相應的動態庫,否則就會在hadoop JobTracker的詳細信息中看到找不到動態庫的錯誤提示。
(2)下面來看看程序:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#include"Pipes.hh" #include"TemplateFactory.hh" #include"StringUtils.hh" class DataCountMap: public HadoopPipes::Mapper
{ public : DataCountMap(HadoopPipes::TaskContext&context){} void map(HadoopPipes::MapContext&context)
{ std::vector<std::string>words=HadoopUtils::splitString(context.getInputValue(), "
" ); //
這裏是分割字符串,如前文所述,每一行數據中的各項是以空格來分割的。分割的結果保存到了一個std::vector中 if ( "kkk" ==words[1])
{ context.emit( "kkk" , "1" ); } else if ( "nnn" ==words[1])
{ context.emit( "nnn" , "1" ); } } }; class DataCountReduce: public HadoopPipes::Reducer
{ public : DataCountReduce(HadoopPipes::TaskContext&context){} void reduce(HadoopPipes::ReduceContext&context) { int sum=0; while (context.nextValue())
{ sum++; } context.emit(context.getInputKey(),HadoopUtils::toString(sum)); } }; int main( int argc, char *argv[]) { return HadoopPipes::runTask(HadoopPipes::TemplateFactory<DataCountMap,
DataCountReduce>()); } |
上面的程序挺簡單的,只要你知道了map-reduce的基本原理。
一個map類,一個reduce類,一個執行任務的main函數。
map類對每一行數據進行拆分,當找到我們感興趣的“kkk”或“nnn”時,就生成一條輸出的記錄(emit函數的作用);recude類對map的數據進行彙總,這裏只是簡單地計數,所以每次+1。
(3)有了代碼,我們接着就要編寫相應的Makefile了。我的Makefile如下:
HADOOP_INSTALL = /usr/local/hadoop
INCLUDE_PATH = $(HADOOP_INSTALL)/src/c++/
CC = g++
CXXFLAGS = -Wall -g \
-I${INCLUDE_PATH}pipes/api/hadoop \
-I${INCLUDE_PATH}utils/api/hadoop
LDFLAGS = -ljvm -lhadooppipes -lhadooputils -lpthread
OBJECTS=dz_count.o
dz_count: $(OBJECTS)
$(CC) $(CXXFLAGS) -o $@ $(OBJECTS) $(LDFLAGS)
其中,HADOOP_INSTALL是你的hadoop安裝路徑,其餘的 INCLUDE_PATH 等請對照你的目錄做相應更改,最後生成的可執行程序名爲dz_count。這裏沒有考慮release,因爲僅作簡單的說明用。
(4)有了代碼和Makefile,就可以編譯了。編譯得到可執行程序dz_count。將其上傳到hdfs中:
hadoop fs -put dz_count /my_dir/
其中 “/my_dir/” 是你在hdfs中的目錄。
(5)下面就可以運行我們的hadoop程序了:
hadoop pipes -D hadoop.pipes.java.recordreader=true -D hadoop.pipes.java.recordwriter=true
-input /data/ -output /my_dir/output -program /my_dir/dz_count
其中,-input /data/ 表明你的輸入數據(即你的源數據)所處的hdfs目錄爲 /data/,-output /my_dir/output 表明你的輸出文件目錄爲 /my_dir/output,“output” 這一級目錄必須不存在(如果存在會報錯),程序運行時會生成它。-program /my_dir/dz_count 表明你要運行的程序爲 /my_dir/ 目錄下的 dz_count 程序。
回車之後程序就開始執行,隨後你可以在命令行下看到它的狀態在更新,或者在hadoop JobTracker中也可以觀察到程序的運行狀態。
(6)等程序執行完後,如果任務沒有失敗的話,我們可以看到,你前面指定的hdfs輸出目錄 /my_dir/output 裏生成了一個文件(假設其名爲“part-00000”),我們就可以查看執行結果了:
hadoop fs -cat /my_dir/output/part-00000
輸出結果形爲:
kkk 178099387
nnn 678219805
表明第二項爲“kkk”的數據行共有178099387條,而“nnn”則爲678219805條。
順便再說一點廢話:
(1)如何中止一個hadoop任務?當你在命令行下提交了一個hadoop job後,就算你按Ctrl+C,也不能中止掉那個job,因爲它已經被Jobtracker接管了。這時,你要用如下命令中止它:
hadoop job -kill Job_ID
其中,Job_ID就是你提交的job的ID,可以在Jobtracker中查看到。
(2)一些基本概念:
map-reduce過程中,在map時,hadoop會將輸入的數據按一定的大小(例如100M,這個值是可以配置的)分爲若干塊來處理,一個塊對應一個map類,也就是說,一個塊只會執行map類的構造函數一次。而每一行記錄則對應一個map()方法,也就是說,一行記錄就會執行一次map()方法。因此,如果你有什麼信息需要輸出(例如std::cout)的話,就要注意了:如果在map()方法中輸出,則當輸入數據量很大時,可能就會輸出太多的信息,如果可以在map的構造函數中輸出的話,則輸出的信息會少得多。
在reduce時,對map輸出的同一個key,有一個reduce類,也就是說,無論你的同一個key有多少個value,在reduce的時候只要是同一個key,就會出現在同一個reduce類裏,在這個類裏的reduce方法中,你用
while (context.nextValue()) 循環可以遍歷所有的value,這樣就可以處理同一個key的N個value了。
正因爲在默認情況下,相同key的記錄會落到同一個reducer中,所以,當你的key的數量比你設置的reducer的數量要少的時候,就導致了某些reducer分配不到任何數據,最終輸出的某些文件(part-r-xxxxx)是空文件。如果你設置的reducer數量要少於key的數量(這是最常見的情況),那麼就會有多個key落入同一個reducer中被處理,但是,每一次reduce()方法被調用時,其中將只包含一個key,同一個reducer裏的多個key就會導致reduce()方法被多次調用。
這樣,我們就完成了一個完整的C++ hadoop分佈式應用程序的編寫。