ODPS MapReduce入門

thamsyangsw發表於2014-11-12

轉載地址:http://beader.me/2014/05/05/odps-mapreduce/

MapReduce 原理簡介


??(上圖引用自 INTRODUCTION TO HADOOP )

??以MapReduce中最經典的wordcount應用為例,來分析一下MapReduce的全過程。這裡我們要統計檔案中每個單詞出現的次數。

  • Input就是我們要處理的原始資料,一共有3行。
  • Splitting步驟是分配任務,這裡把任務分給3臺機器同時處理,每臺機器只負責處理一行的資料。
  • Mapping步驟就是這3臺機器具體要做的事情。在這裡每臺機器要做的就是統計一行文字裡的單詞頻率。這裡就涉及到比較重要的一個概念,就是key和value。這裡key就是單詞,value就是這個單詞在這一行出現的次數。
  • Shuffling步驟就是對Mapping步驟產生的9行資料,按照key進行分組。這裡分成了4組,每組交給一臺電腦去處理。
  • Reducing,把相同key對應的value相加,每個key最終只輸出一行,依然是key,value的形式輸出。
  • Final result,把Reducing的輸出合併。

??(注:這裡Mapping工作交給3臺電腦,Reducing工作交給4臺電腦的說法其實是不嚴謹的,具體要用多少資源來完成MapReduce由系統根據任務的狀況決定,通常一臺電腦需要完成多個Mapping與Reducing的工作。)

??為何要如此設計?簡單來說,因為MapReduce為的是能實現分散式運算,涉及到多臺機器同時運算的步驟有Mapping和Reducing,參與Mapping工作的機器可以完全獨立工作而不需要知道其他機器上有什麼資料;參與Reducing步驟的機器,由於資料之前已經按照key進行了分組,因此其他機器上有什麼資料與他毫無關係。參與計算的機器都是互相獨立,完全不依賴其他機器的資料,這樣就可以很方便寫程式碼,因為所有參與Mapping工作的機器使用一模一樣的程式碼,所有參與Reducing工作的機器也使用一模一樣的程式碼。

??我們要在ODPS上要實現MapReduce,就需要寫兩類程式碼,一類稱為Mapper,另一類稱為Reducer。拋開前面所說的原理,我們只需要記住以下兩點:

  • Mapper每次只處理一行資料。即Mapper的Input是資料庫中的一條記錄。
  • Reducer每次要處理的是相同key下的所有記錄,通常會是多行的。

目標

??通過MapReduce計算對每個user每天對不同品牌產生的4種行為(點選、購買、收藏、購物車)的次數進行統計,並且計算在某天某user對某個brand的累計點選次數:

input table

user_id brand_id type visit_datetime
101 20001 0 06-01
101 20001 0 06-01
101 20001 0 06-02
101 20002 0 06-02
101 20002 1 06-02
101 20003 0 06-02
101 20001 0 06-03
101 20003 2 06-03
101 20003 3 06-03
101 20003 1 06-04

output table

user_id brand_id visit_datetime clicks buy collect basket cum_clicks
101 20001 06-01 2 0 0 0 2
101 20001 06-02 1 0 0 0 3
101 20001 06-03 1 0 0 0 4
101 20002 06-02 1 1 0 0 1
101 20003 06-02 1 0 0 0 1
101 20003 06-03 0 0 1 1 1
101 20003 06-04 0 1 0 0 1

??比如output table裡第三行的意思是,user_10106-03這天一共點了brand_20001 1次,從user_101第一次接觸brand_20001以來,已經累計點了4次。

??由於我們想要實現累計求和,因此我們可以在Mapping步驟中,使用(user_id,brand_id)作為key,而(type,visit_datetime)作為value。

??這麼一來,在Reducing步驟中,每個Reducer就能接受到某個user對某個brand的所有互動資訊,這樣就能衍生出我們所需的新的value,即(visit_datetime, clicks, buy, collect, basket cum_clicks)。

??在ODPS中需要達到上述目標,需要動手實現3個類,這裡我把他們命名為TestMapper,TestReducer,TestDriver,其中TestDriver用來進行一些任務的配置。下面來具體看看如何實現。

TestDriver

??Driver主要用來進行一些格式設定。在此之前你需要在eclipse中新建一個ODPS專案。然後在專案的src上右鍵->new->other,在Aliyun Open Data Processing Service下選擇MapReduce Driver…

??接著eclipse會幫我們生成一個Driver的模板。官方寫的很清楚了,所有的TODO部分是需要我們進行修改的。先來看第一個TODO:

// TODO: specify map output types
job.setMapOutputKeySchema(SchemaUtils.fromString("user_id:string,brand_id:string"));
job.setMapOutputValueSchema(SchemaUtils.fromString("type:string,visit_datetime:string"));

這是用來設定Mapper輸出的時候,key與value的格式。按照之前說的,以(user_id,brand_id)為key,(type,visit_datetime)為value。

??第二個TODO:

InputUtils.addTable(TableInfo.builder().tableName(args[0]).build(),job);
OutputUtils.addTable(TableInfo.builder().tableName(args[1]).build(),job);

 

設定input table與output table。這裡把待會兒命令列呼叫中,第一個引數(args[0])設為input table,第二個引數(args[1])設為output table。待會兒會通過下面的命令在odps console中啟動MapReduce任務。(暫時不需要搞清楚的地方都用*代替)

 jar *** TestDriver t_alibaba_bigdata_user_brand_total_1 tb_output

其中args[0]就指代t_alibaba_bigdata_user_brand_total_1,args[1]就指代tb_output。

??第三個TODO:

// TODO: specify a mapper
job.setMapperClass(TestMapper.class)
job.setReducerClass(TestReducer.class)

告訴系統這次任務要用的Mapper和Reducer是誰,按照上面的設定之後,系統就會通知所有負責Mapping工作的電腦待會兒使用TestMapper中的程式碼進行運算,通知所有負責Reducing工作的電腦待會兒使用TestReducer中的程式碼進行運算。

??TestDriver完整程式碼如下(省略開頭import的部分)

public class TestDriver {
  public static void main (String[] args) throws OdpsException {
    JobConf job = new JobConf();

 // TODO: specify map output types
 job.setMapOutputKeySchema(SchemaUtils.fromString("user_id:string,brand_id:string"));
 job.setMapOutputValueSchema(SchemaUtils.fromString("type:string,visit_datetime:string"));

 // TODO: specify input and output tables
 InputUtils.addTable(TableInfo.builder().tableName(args[0]).build(),job);
 OutputUtils.addTable(TableInfo.builder().tableName(args[1]).build(),job);

 // TODO: specify a mapper
 job.setMapperClass(TestMapper.class)
 job.setReducerClass(TestReducer.class)

 RunningJob rj = JobClient.runJob(job);
 rj.waitForCompletion();
  }
}

 

TestMapper

??之前說過,Mapper的任務就是對讀入的一行資料,接著輸出key和value。key和value都屬於Record類,並且key和value都可以由單個或者多個欄位構成,在我們這個任務中,key由(user_id,brand_id)兩個欄位構成,value由(type,visit_datetime)構成。

??與建立TestDriver的步驟類似,使用官方的模板建立一個名為TestMapper的java程式碼,同樣官方模板把大多數程式碼都生成好了。

public void setup(TaskContext context) throws IOException {
 key = context.createMapOutputKeyRecord();
 value = context.createMapOutputValueRecord();
}

setup當中是對key和value進行初始化。其中key使用createMapOutputKeyRecord()進行初始化,value使用createMapOutputValueRecord()進行初始化。

??在map函式中,record代表讀入的一行資料,比如101, 20001, 0, 06-01,我們可以通過record.get(n)方法獲取該行記錄第n列的資料。並且方便的是,這裡可以直接對讀入的資料進行一個型別轉換。例如record.getString()會把讀入的資料轉為字串,record.getBigInt()則會把讀入的資料轉為Long型整數。

??在TestDriver的設定當中,我們已經把Mapper輸出的key定為(user_id,brand_id),value定為(type,visit_datetime),在map函式中,我們可以使用key.set()與value.set()來分別賦予這4個值。

??最後context.write(key, value)的意思是輸出這條key-value,如果不寫這行,Mapper就什麼都不輸出。一個Mapper可以有0個或多個key-value的輸出,每呼叫一次context.write(key,value)就會輸出一行。

??TestMapper完整程式碼如下(省略import部分)

 

public class TestMapper extends MapperBase{
  Record key;
  Record value;

  @Override
  public void setup(TaskContext context) throws IOException {
 key = context.createMapOutputKeyRecord();
 value = context.createMapOutputValueRecord();
  }

  @Override
  public void map(long recordNum, Record record, TaskContext context)
 throws IOException {

 key.set("user_id", record.getString(0));
 key.set("brand_id", record.getString(1));

 value.set("type", record.getString(2));
 value.set("visit_datetime", record.getString(3));

 context.write(key, value);
  }
}

 

TestReducer

??通常Driver和Mapper方面都很簡單,大多情況下,計算工作都在Reducing步驟完成,因此Reducer的程式碼會略多一些。同樣按照前面的方法生成名為TestReducer的Reducer類。

??因為我們要按天來彙總四類行為出現的次數,因此這裡我使用一個TreeMap來儲存每天四種行為出現的次數。

Map typeCounter = new TreeMap();

這裡又再囉嗦一遍,Mapper每次只處理一行資料,而Reducer通常處理的不止一行,而是會處理屬於相同key的所有資料。翻到文章開頭的那張圖片,圖中第二個Reducer,所有key為Car的記錄,全部交給該一個Reducer處理。

??因此reduce函式當中的values引數是一個Iterator,通過呼叫values.next()來讀取所有屬於該key的記錄。每讀取一行記錄,都對計數器typeCounter進行對應type的累加操作。

??Reducer的output是一個Record類,可以通過output.set(n)來設定該output第n列的數值,同樣使用context.write(output)輸出一行資料。在本文的例子中,對於一個key(user_id,brand_id)而言,有N個不同的visit_datetime,最終就會輸出N行資料。因此可以看到TestReducer的context.write(output)是寫在一個for-loop裡的,會被呼叫多次,每次會輸出一行。類似的操作如果使用SQL實現,就非常的費神了,而使用MapReduce,反而簡單許多。

??TestReducer的完整程式碼(省略模板中的import部分)

 

import java.util.Map;
import java.util.TreeMap;

public class TestReducer extends ReducerMap{
  Record output;

  @Override
  public void setup(TaskContext context) throws IOException {
 output = context.createOutputRecord();
  }

  @Override
  public void reduce(Record key, Iterator values, TaskContext context)
 throws IOException {

 Map typeCounter = new TreeMap();

 while (values.hasNext()) {
   Record val = values.next();
   String date = val.getString("visit_datetime");
   int type = Integer.parseInt(val.getString("type"));

   if (typeCounter.containsKey(date)) {
  typeCounter.get(date)[type]++;
   } else {
  Long[] counter = new Long[]{0L, 0L, 0L, 0L};
  counter[type]++;
  typeCounter.put(date, counter);
   }
 }

 output.set(0, key.getString("user_id"));
 output.set(1, key.getString("brand_id"));
 Long cumClicks = 0L;
 
 for (String date : typeCounter.keySet()){
   output.set(2, date);
   output.set(3, typeCounter.get(date)[0]);
   output.set(4, typeCounter.get(date)[1]);
   output.set(5, typeCounter.get(date)[2]);
   output.set(6, typeCounter.get(date)[3]);
   cumClicks += typeCounter.get(date)[0];
   output.set(7, cumClicks);
   context.write(output);
 }
  }
}

 

打包、上傳、建表、執行

??1. 在Package Explorer中你之前建立的ODPS專案下的src上右鍵,選擇Export,然後選擇Java底下的JAR file。接著設定下JAR包存放的位置與檔名。這裡假設我們放在C:\TOOLS\test.jar,然後點Finish。

??2. 開啟odps console,新建一個resource。

odps:tianchi_1234> create resource jar C:/tools/test.jar -f

3. 在實際執行之前,需要先建立一個表,作為結果輸出的位置。這裡我們就叫它tb_output好了。
??進入sql,建立表格

odps:tianchi_1234> sql
odps:sql:tianchi_1234> drop table if exists tb_output;
odps:sql:tianchi_1234> create table tb_output (user_id string, brand_id string, visit_datetime string, clicks bigint,
                       buy bigint, collect bigint, basket bigint, cum_clicks bigint);

4. 在odps console下,執行MapReduce任務
odps:tianchi_1234> jar -resources test.jar --classpath c:/tools/test.jar TestDriver t_alibaba_bigdata_user_brand_total_1 tb_output;

結尾

  1. 本人幾乎是第一次寫Java,當中如有一些不規範的地方,希望您不吝賜教。
  2. 因為每次MapReduce的執行時間都會很長,建議每次做MapReduce任務的時候,可以先產生一份非常小的table,先拿這個小table做實驗,確定結果正確後,再對整張表進行操作。
  3. odps官方文件的例子中,Mapper,Reducer,Driver是寫在同一個檔案下的,這樣做也可以,但不建議這樣寫。之前我嘗試這麼做,當Mapper或者Reducer有錯誤的時候,無法提示錯誤在第幾行。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26613085/viewspace-1330078/,如需轉載,請註明出處,否則將追究法律責任。

相關文章