[MapReduce編程]用MapReduce大刀砍掉海量數據離線處理問題。

今日在CSDN看再次遇見July的這篇博文:教你如何迅速秒殺掉:99%的海量數據處理面試題

這篇文章我之前是拜讀過的,今天閒來沒事,就想拿來當做MapReduce的練習。

MapReduce這把刀太大,刀大了問題就抵不住這刀鋒了,事實上一開始我想着,這麼多些題目,當是要花不少功夫的,但當我做完一題繼續看下面的題目的時候,才發現這些題目在MapReduce模型下顯得大同小異了,看來拿大刀的人是不管砍的是木頭還是人頭的,而是直接抽象成柱形物然後掄起刀一刀就下去了。

  直入主題:

  1、海量日誌數據,提取出某日訪問百度次數最多的前K個IP。[稍微改變]

                說明:每一次訪問網頁就在日誌中記錄1次訪問者的IP,獨佔一行,一個小數據可以在這裏下載

    實在是想不出如何能在一個Job中解決這個問題,所以還是把它拉扯成了兩個Job來解決。
      Job1:將相同IP的記錄合併,形成<ip,count>形式,其中count是對這個ip的計數。
      Job2:按count排序<ip,count>並選擇前K個進行輸出。
    這裏我寫了一個可序列化的類IPAndCount,如果稍微熟悉MapReduce或者看明白我之前寫的關係型MapReduce模式:選擇、分組和組內排序你就知道這是爲了排序而準備的。MapReduce有一個“Shuffle and sort”,這個階段是利用key來對tuple進行排序的,而排序時調用的便是key的compareTo()方法。事實上如果job1輸出的數據兩足夠小,我們完全可以在內存中進行排序而利用MapReduce框架,這樣就可以省下一個Reduce階段,但是對於這個問題顯然不行。
    IPAndCount很直白,就是包裝了上述的<ip,count>。
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;


public class IPAndCount implements WritableComparable{
	Text ip;
	IntWritable count;
	
	public IPAndCount(){
		this.ip = new Text("");
		this.count = new IntWritable(1);
	}
	
	public IPAndCount(Text ip, IntWritable count){
		this.ip = ip;
		this.count = count;
	}
	
	public IPAndCount(String ip, int count){
		this.ip = new Text(ip);
		this.count = new IntWritable(count);
	}
	
	public void readFields(DataInput in) throws IOException {
		ip.readFields(in);
		count.readFields(in);
	}

	public void write(DataOutput out) throws IOException {
		ip.write(out);
		count.write(out);
	}

	public int compareTo(Object o) {
		return ((IPAndCount)o).count.compareTo(count) == 0? 
				ip.compareTo(((IPAndCount)o).ip):((IPAndCount)o).count.compareTo(count);//如果只比較count會丟失數據,應該是suffle階段的問題
	}
	
	public int hashCode(){
		return ip.hashCode();
	}
	
	public boolean equals(Object o){
		if(!(o instanceof IPAndCount))
			return false;
		IPAndCount other = (IPAndCount)o;
		return ip.equals(other.ip) && count.equals(other.count);
	}
	
	public String toString(){
		StringBuffer buf = new StringBuffer("[ip=");
		buf.append(ip.toString());
		buf.append(",count=");
		buf.append(count.toString());
		buf.append("]");
		return buf.toString();
	}
	
	public Text getIp() {
		return ip;
	}
	public void setIp(Text ip) {
		this.ip = ip;
	}
	public IntWritable getCount() {
		return count;
	}
	public void setCount(IntWritable count) {
		this.count = count;
	}
}

下面對FindActiveIp進行說明:
SumUpIpMapper和SumUpIPReducer事實上就是一個MapReduce中最基礎的詞頻統計程序WordCount。你可以加一個Combiner來優化一下,我遺漏了。
從配置中可以看見兩個Job的配置:job 和job2。
依賴關係是job -> job2,代碼中使用了JobControl來解決作業間的依賴關係,JobControl.run()方法會在作業都運行完後才返回。
Job2的輸入路徑是Job1的輸出路徑,從參數中可以看出這一點。
Job1的輸出在輸出文件中的表現是:ip,count
Job2再從文件中讀入,使用的是KeyValueTextInputFormat,它對應的是TextOutputFormat,我們可以從job1的配置中看出來。
BeforeSortIPMapper從job1的輸出中讀取數據幷包裝成IPAndCount類型,以便MapReduce框架在“shuffle and sort”階段利用它來排序。
最後SelectTopKIPReducer選出前K個進行輸出即可,在這裏我們設置最後的reduce只有一個reduce task,以使所有數據匯聚到一臺機子上進行處理。
import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.lib.ChainMapper;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.jobcontrol.ControlledJob;
import org.apache.hadoop.mapreduce.lib.jobcontrol.JobControl;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;


public class FindActiveIP extends Configured implements Tool{
	
	public static class SumUpIPMapper extends Mapper<LongWritable,Text,Text,IntWritable>{
		IntWritable  one = new IntWritable(1);
		public void map(LongWritable key, Text value, Context context) 
			throws IOException,InterruptedException{
			context.write(value, one);
		}
	}

	public static class SumUpIPReducer extends Reducer<Text,IntWritable,Text,IntWritable>{
		//這裏可以選擇前k個進行輸出以優化
		public void reduce(Text key, Iterable<IntWritable> values, Context context)
			throws IOException, InterruptedException{
			int sum = 0;
			for(IntWritable v : values){
				sum += v.get();
			}
			context.write(key, new IntWritable(sum));
		}
	}
	
	
	public static class BeforeSortIPMapper extends Mapper<Text,Text,IPAndCount,Text>{
		public void map(Text key, Text value, Context context)
			throws IOException,InterruptedException{
			IPAndCount tmp = new IPAndCount(key,new IntWritable(Integer.valueOf(value.toString())));
			System.out.println(tmp);
			context.write(tmp,new Text());
		}
	}
	
	
	//set num of this reducer to one
	public static class SelectTopKIPReducer extends Reducer<IPAndCount,Text,IPAndCount,Text>{
		int counter = 0;
		int K = 10;
		public void reduce(IPAndCount key, Iterable<Text> values, Context context)
			throws IOException, InterruptedException{
			System.out.println(key);
			if(counter < K){
				context.write(key, null);
				
				counter++;
			}
			
		}
	}
	public int run(String[] args) throws Exception {
		Configuration conf = new Configuration();
		Job job = new Job(conf,"SumUpIP");
		job.setJarByClass(FindActiveIP.class);
		job.setInputFormatClass(TextInputFormat.class);
		job.setOutputFormatClass(TextOutputFormat.class);
		job.getConfiguration().set("mapred.textoutputformat.separator", ",");
		Path in = new Path(args[0]);
		Path out = new Path(args[1]);
		FileInputFormat.setInputPaths(job, in);
		FileOutputFormat.setOutputPath(job, out);
		job.setMapperClass(SumUpIPMapper.class);
		job.setReducerClass(SumUpIPReducer.class);
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		job.setNumReduceTasks(7);
		
		Configuration conf2 = new Configuration();
		Job job2 = new Job(conf2,"SortAndFindTopK");
		job2.setJarByClass(FindActiveIP.class);
		job2.setInputFormatClass(KeyValueTextInputFormat.class);
		job2.getConfiguration().set("mapreduce.input.keyvaluelinerecordreader.key.value.separator", ",");
		job2.setOutputFormatClass(TextOutputFormat.class);
		Path in2 = new Path(args[1]);
		Path out2 = new Path(args[2]);
		FileInputFormat.setInputPaths(job2,in2);
		FileOutputFormat.setOutputPath(job2, out2);
		job2.setMapperClass(BeforeSortIPMapper.class);
		job2.setReducerClass(SelectTopKIPReducer.class);
		job2.setMapOutputKeyClass(IPAndCount.class);
		job2.setMapOutputValueClass(Text.class);
		job2.setOutputKeyClass(IPAndCount.class);
		job2.setOutputValueClass(Text.class);
		job2.setNumReduceTasks(1);
		
		JobControl jobControl = new JobControl("FindTopKIP");
		ControlledJob cJob1 = new ControlledJob(conf);
		cJob1.setJob(job);
		ControlledJob cJob2 = new ControlledJob(conf2);
		cJob2.setJob(job2);
		jobControl.addJob(cJob1);
		jobControl.addJob(cJob2);
		cJob2.addDependingJob(cJob1);
		jobControl.run();
		return 0;
	}
	
	public static void main(String args[]) throws Exception{
		int res = ToolRunner.run(new FindActiveIP(), args);
		System.exit(res);
	}

}

  大刀拿習慣了,從前的大刀就成了現在的繡花針,不是繡花針不好,只是用着不順手。當你聽着歌用java寫着MapReduce,突然有人在你耳邊喊了一句:Pig~Pig~Pig~
  你很難不心動!程序員愛偷懶堪比女人愛逛街,都是爲了快樂啊~
  下面是用Pig來處理上述的問題:
  
grunt> records = LOAD 'input/ipdata' AS (ip:chararray);                         
grunt> grouped_records = GROUP records BY ip;                                   
grunt> counted_records = FOREACH grouped_records GENERATE group, COUNT(records);
grunt> sorted_records = ORDER counted_records BY $1 DESC;                       
grunt> topK = LIMIT sorted_records 10;                                                  
grunt> DUMP topK;  

你看,數數,沒暈數一數,6行!僅僅6行就解決了。
行1:將文件裝入
行2:按ip分組
行3:組內計數
行4:組間按ip訪問計數排序
行5:選擇前10個數據
行6:運行並輸出。

雖然我們的Pig方法實際上跑了3個job才完成任務,相比於java寫的MapReduce多了一個job,但Pig顯然更愉快些。

這是最後結果:
(192.168.0.1,1559)
(192.168.0.21,7)
(192.168.0.14,4)
(192.168.0.10,4)
(192.168.0.12,4)
(192.168.0.32,4)
(192.168.0.13,3)
(192.168.0.3,3)
(192.168.0.2,2)
(192.168.0.11,1)

這個算法對帶寬的壓力還是比較大的,除了加一個Combiner之外,代碼中還提到了另一個小小的優化在進入第一個Reduce階段的一個reduce task中的數據足以裝入內存時,這是很容易解決的。這不是多好的優化,應當有更加好的優化方式能過濾更多的數據,否則。。。這不科學~

  2、尋找熱門查詢:搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。
    假設目前有100億個記錄(這些查詢串的重複度比較高,雖然總數是100億,但如果除去重複後,不超過10億個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門),請你統計最熱門的10個查詢串。[我還是稍微修改了題目]
    看見這個題目,我覺得我寫下去會對不起July費了萬千腦細胞辛苦的寫作成果,儘管我內心十分希望它跟上面那題一樣,但這多麼讓人不甘心又不盡興~
    現在暫且不考慮優化的問題:這個題目無非就是統計查詢串的計數,然後排序,然後取出前10個。事實上,這個問題在不考慮細節上完全可以用上面的pig腳本來處理。

不寫了,以現在的水平繼續寫實在是不優美:
3題:有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。
4題:海量數據分佈在100臺電腦中,想個辦法高效統計出這批數據的TOP10。
5題:有10個文件,每個文件1G,每個文件的每一行存放的都是用戶的query,每個文件的query都可能重複。要求你按照query的頻度排序。
7題:怎麼在海量數據中找出重複次數最多的一個?
8題:上千萬或上億數據(有重複),統計其中出現次數最多的前N個數據。
9題:一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞。

1,2,3,4,5,7,8,9題思路基本一致,值得注意的是,有時候我們完全可以確定我們需要的數據的一些特徵,比如上面的熱門查詢中熱門串一定被查詢超過1000次,那麼我們就可以使用FILTER來進行過濾以減少處理的數據(從而減少對帶寬的壓力)[filted_records = FILTER grouped_records BY SIZE(records) > 1000;]。

6題: 給定a、b兩個文件,各存放50億個url,每個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?
[Hadoop]使用DistributedCache進行復制聯結 以及 使用hadoop的datajoin包進行關係型join操作,你也可以參考Data-Intensive Text Processing with MapReduce看看原生態的join操作是怎麼進行的。
grunt> A = LOAD 'input/url1' AS (url:chararray);
grunt> B = LOAD 'input/url2' AS (url:chararray);
grunt> grouped_A = GROUP A BY url;
grunt> non_duplicated_A = FOREACH grouped_A GENERATE group;  --去重
grunt> grouped_B = GROUP B BY url;
grunt> non_duplicated_B = FOREACH grouped_B GENERATE group;  --B去重
grunt> C = JOIN non_duplicated_B BY group, non_duplicated_A BY group;  --A 、B 內聯結
grunt> D = FOREACH C GENERATE $0;   //生成重複url
grunt> DUMP D;


10題: 1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字符串。
使用pig:
grunt> records = LOAD 'input/retrived_strings' AS (str:chararray);
grunt> grouped_records = GROUP records BY str;
grunt> filted_records = FILTER grouped_records BY SIZE(records) <= 1;
grunt> DUMP filted_records;



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章