通過前面的學習我們知道Mapper最終處理的鍵值對<key, value>,是需要送到Reducer去合併的,合併的時候,有相同key的鍵/值對會送到同一個Reducer節點中進行歸併。哪個key到哪個Reducer的分配過程,是由Partitioner規定的。在一些集羣應用中,例如分佈式緩存集羣中,緩存的數據大多都是靠哈希函數來進行數據的均勻分佈的,在Hadoop中也不例外。
image
Hadoop內置Partitioner
MapReduce的使用者通常會指定Reduce任務和Reduce任務輸出文件的數量(R)。用戶在中間key上使用分區函數來對數據進行分區,之後在輸入到後續任務執行進程。一個默認的分區函數式使用hash方法(比如常見的:hash(key) mod R)進行分區。hash方法能夠產生非常平衡的分區,鑑於此,Hadoop中自帶了一個默認的分區類HashPartitioner,它繼承了Partitioner類,提供了一個getPartition的方法,它的定義如下所示:
/** Partition keys by their {@link Object#hashCode()}. */
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
現在我們來看看HashPartitoner所做的事情,其關鍵代碼就一句:(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
這段代碼實現的目的是將key均勻分佈在Reduce Tasks上,例如:如果Key爲Text的話,Text的hashcode方法跟String的基本一致,都是採用的Horner公式計算,得到一個int整數。但是,如果string太大的話這個int整數值可能會溢出變成負數,所以和整數的上限值Integer.MAX_VALUE(即0111111111111111)進行與運算,然後再對reduce任務個數取餘,這樣就可以讓key均勻分佈在reduce上。
partitoner
自己定製Partitioner
流量彙總程序開發
這裏添加了新需求,要求流量彙總統計並按省份區分。也就是說不但計算每個用戶(手機號)的上行流量,下行流量,總流量外,還要按照每個手機號所屬不同的省份來將計算結果寫到不同的文件中(假如共4個省份,那麼需要將輸出結果寫到4個文件中,也就是說有4個分區每個分區對應一個reduce task)。
public class Flowcount {
/**
* KEYIN:默認情況下,是mr框架所讀到的一行文本的起始偏移量,Long,但是在hadoop中有自己的
* 更精簡的序列化接口(Seria會將類結構都序列化,而實際我們只需要序列化數據),所以不直接用Long,而用LongWritable
* VALUEIN:默認情況下,是mr框架所讀到的一行文本的內容,String,同上,用Text
* KEYOUT:是用戶自定義邏輯處理完成之後輸出數據中的key
* VALUEOUT:是用戶自定義邏輯處理完成之後輸出數據中的value
* @author 12706
*
*/
static class FlowcountMapper extends Mapper<LongWritable, Text, Text, FlowBean>{
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//輸入爲1234 23455 33333 33333(中間是製表符)
//第二列爲手機號,倒數第二列爲下行流量,倒數第三列爲上行流量
String line = value.toString();
String[] values = line.split("\t");
//獲取手機號
String phoneNum = values[1];
//獲取上行流量下行流量
long upFlow = new Long(values[values.length-3]);
long downFlow = new Long(values[values.length-2]);
//封裝好後寫出到輸出收集器
context.write(new Text(phoneNum), new FlowBean(upFlow,downFlow));
}
}
/**
* KEYIN VALUEIN對應mapper輸出的KEYOUT KEYOUT類型對應
* KEYOUT,VALUEOUT:是自定義reduce邏輯處理結果的輸出數據類型
* KEYOUT
* VALUEOUT
* @author 12706
*
*/
static class FlowcountReducer extends Reducer<Text, FlowBean, Text, FlowBean>{
@Override
protected void reduce(Text key, Iterable<FlowBean> beans,Context context)
throws IOException, InterruptedException {
//傳進來的實例<13345677654,beans>,即多個該電話的鍵值對
//取出values獲得上下行和總流量求和
long upFlow = 0;
long downFlow = 0;
for (FlowBean flowBean : beans) {
upFlow += flowBean.getUpFlow();
downFlow += flowBean.getDownFlow();
}
context.write(key, new FlowBean(upFlow,downFlow));
}
}
/**
* 相當於一個yarn集羣的客戶端
* 需要在此封裝mr程序的相關運行參數,指定jar包
* 最後提交給yarn
* @author 12706
* @param args
* @throws Exception
*
*/
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//指定Partitioner
job.setPartitionerClass(FlowPartitioner.class);
//設置reduce task數量
job.setNumReduceTasks(5);
job.setJarByClass(Flowcount.class);
//指定本業務job要使用的mapper,reducer業務類
job.setMapperClass(FlowcountMapper.class);
job.setReducerClass(FlowcountReducer.class);
//雖然指定了泛型,以防框架使用第三方的類型
//指定mapper輸出數據的kv類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//指定最終輸出的數據的kv類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//指定job輸入原始文件所在位置
FileInputFormat.setInputPaths(job, new Path(args[0]));
//指定job輸入原始文件所在位置
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//將job中配置的相關參數以及job所用的java類所在的jar包,提交給yarn去運行
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
public class FlowBean implements Writable{
private long upFlow;//上行流量
private long downFlow;//下行流量
private long totalFlow;//總流量
//序列化時需要無參構造方法
public FlowBean() {
}
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.totalFlow = upFlow + downFlow;
}
//序列化方法 hadoop的序列化很簡單,要傳遞的數據寫出去即可
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(totalFlow);
}
//反序列化方法 注意:反序列化的順序跟序列化的順序完全一致
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.totalFlow = in.readLong();
}
//重寫toString以便展示
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + totalFlow;
}
get,set方法
}
/**
* Mapreduce中會將map輸出的kv對,按照相同key分組,然後分發給不同的reducetask
*默認的分發規則爲:根據key的hashcode%reducetask數來分發
*所以:如果要按照我們自己的需求進行分組,則需要改寫數據分發(分組)組件Partitioner
*自定義一個CustomPartitioner繼承抽象類:Partitioner
*然後在job對象中,設置自定義partitioner: job.setPartitionerClass(CustomPartitioner.class)
* @author 12706
*
*/
public class FlowPartitioner extends Partitioner<Text, FlowBean>{
private static HashMap<String, Integer> map = new HashMap<String, Integer>();
static {
//模擬手機號歸屬地 0:北京,1:上海,2:廣州,3:深圳,4:其它
map.put("135", 0);
map.put("136", 1);
map.put("137", 2);
map.put("138", 3);
}
//返回分區號
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
//進來的數據是<13567898766,flowbean1>,flowbean1中封裝了上下行流量,總流量
String phoneNum = key.toString();
//截取手機號前3位
String num = phoneNum.substring(0, 3);
//獲取對應的省
Integer provinceId = map.get(num);
return provinceId==null?4:provinceId;
}
}
測試程序
將工程打jar包到本地,上傳到linux,啓動hadoop集羣
數據以及在hdfs下的文件均使用流量彙總程序中的。使用以下命令
[root@mini2 ~]# hadoop jar flowcount.jar com.scu.hadoop.partitioner.Flowcount /flowcount/input /flowcount/output
17/10/09 10:47:49 INFO mapreduce.JobSubmitter: number of splits:1
17/10/09 10:47:49 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1507516839481_0001
17/10/09 10:47:50 INFO impl.YarnClientImpl: Submitted application application_1507516839481_0001
17/10/09 10:47:50 INFO mapreduce.Job: The url to track the job: http://mini1:8088/proxy/application_1507516839481_0001/
17/10/09 10:47:50 INFO mapreduce.Job: Running job: job_1507516839481_0001
17/10/09 10:47:58 INFO mapreduce.Job: Job job_1507516839481_0001 running in uber mode : false
17/10/09 10:47:58 INFO mapreduce.Job: map 0% reduce 0%
17/10/09 10:48:03 INFO mapreduce.Job: map 100% reduce 0%
17/10/09 10:48:13 INFO mapreduce.Job: map 100% reduce 20%
17/10/09 10:48:14 INFO mapreduce.Job: map 100% reduce 40%
17/10/09 10:48:19 INFO mapreduce.Job: map 100% reduce 100%
17/10/09 10:48:20 INFO mapreduce.Job: Job job_1507516839481_0001 completed successfully
17/10/09 10:48:21 INFO mapreduce.Job: Counters: 50
File System Counters
FILE: Number of bytes read=863
FILE: Number of bytes written=642893
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
HDFS: Number of bytes read=2278
HDFS: Number of bytes written=551
HDFS: Number of read operations=18
HDFS: Number of large read operations=0
HDFS: Number of write operations=10
Job Counters
Killed reduce tasks=1
Launched map tasks=1
Launched reduce tasks=5
Data-local map tasks=1
...
從打印信息可以看到切片splits爲1,即一個maptask從Job Counters可以看出map tasks=1,reduce tasks=5所以輸出文件應該也有5個。
查看輸出
[root@mini2 ~]# hadoop fs -ls /flowcount/output
-rw-r--r-- 2 root supergroup 0 2017-10-09 10:48 /flowcount/output/_SUCCESS
-rw-r--r-- 2 root supergroup 84 2017-10-09 10:48 /flowcount/output/part-r-00000
-rw-r--r-- 2 root supergroup 53 2017-10-09 10:48 /flowcount/output/part-r-00001
-rw-r--r-- 2 root supergroup 104 2017-10-09 10:48 /flowcount/output/part-r-00002
-rw-r--r-- 2 root supergroup 22 2017-10-09 10:48 /flowcount/output/part-r-00003
-rw-r--r-- 2 root supergroup 288 2017-10-09 10:48 /flowcount/output/part-r-00004
確實是5個文件
查看每個文件內容
[root@mini2 ~]# hadoop fs -cat /flowcount/output/part-r-00000
13502468823 7335 110349 117684
13560436666 1116 954 2070
13560439658 2034 5892 7926
[root@mini2 ~]# hadoop fs -cat /flowcount/output/part-r-00001
13602846565 1938 2910 4848
13660577991 6960 690 7650
...