Hadoop简单实现全排序

本文介绍如何使用Hadoop实现大数据的全排序,包括TeraSort思想解析、取样方法及分区策略,适用于整形和字符串类型的排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://blog.youkuaiyun.com/yeruby/article/details/21233661


做毕设用到Hadoop的全排序处理大数据,接触Hadoop已经2个月了,进展缓慢,深刻认识到进入到一个好的团队、共同研究是多么的重要,以此


纪念我的大四一个人的毕设。废话不多说,我实现了整形和字符串型的全排序。

基础知识:

1. TeraSort思想:

关于terasort的文章很多,我没有找到那篇经典的原创。大体思想可以参看:http://hi.baidu.com/dt_zhangwei/item/c2a80032c7dbc5ff96f88dbf

我的理解:

(1)如果reducer的个数为1,那么输出一定是一个文件(part-r-00000),hadoop内部可以保证输出时已经排序好的。

这时:如果key是Text类型,按字典序排好;

  如果key是IntWriteable类型,按整形排好;

(2)如果reducer的个数大于1,那么可以保证的是每一个reducer的输出是排好序的,但是不同reducer的输出不能保证。若想实现全排序,我们只需保证:到第0个reducer的数据的最后一项一定小于到第1个reducer的数据的第一项,以此类推,到第n-1个reducer的数据的最后一项一定小于到第n个reducer的数据的第一项(假设我们job.setNumReduceTasks(n),即设定reduce任务数为n个,且按升序来排序)。

那么如何实现呢?

分为两步:取样+Partition对每条数据做标记(即发往哪个reducer做处理)

2. 取样

原理:取样工作在JobClient端进行,目的是取出n-1个排序好的样本(可以划分出n个reducer),在partition的过程中,通过将当前keyvalue对的key跟样本中数据作比较,就可以知道该keyvalue对发往哪个reducer了。

以此我们需要写自己的“取样类”:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. static class TextSampler implements IndexedSortable {  
  2.   
  3.     public ArrayList<IntWritable> records = new ArrayList<IntWritable>();//全部样本数据  
  4.   
  5.     @Override  
  6.     public int compare(int arg0, int arg1) {  
  7.         IntWritable right = records.get(arg0);  
  8.         IntWritable left = records.get(arg1);  
  9.         return right.compareTo(left);  
  10.     }  
  11.   
  12.     @Override  
  13.     public void swap(int arg0, int arg1) {  
  14.         IntWritable right = records.get(arg0);  
  15.         IntWritable left = records.get(arg1);  
  16.         records.set(arg0, left);  
  17.         records.set(arg1, right);  
  18.     }  
  19.   
  20.     public void addKey(IntWritable key) {  
  21.         records.add(key);  
  22.     }  
  23.   
  24.     public IntWritable[] createPartitions(int numPartitions) {  
  25.         int numRecords = records.size();  
  26.         if (numPartitions > numRecords) {  
  27.             throw new IllegalArgumentException("Requested more partitions than input keys (" + numPartitions +  
  28.                     " > " + numRecords + ")");  
  29.         }  
  30.         new QuickSort().sort(this0, records.size());  
  31.         float stepSize = numRecords / (float) numPartitions;//取数的步长  
  32.         IntWritable[] result = new IntWritable[numPartitions - 1];  
  33.         for (int i = 1; i < numPartitions; ++i) {  
  34.             result[i - 1] = records.get(Math.round(stepSize * i));//从全部样本数据中再抽出n-1个样本  
  35.         }  
  36.         return result;  
  37.     }  
  38. }  

说明:实现了IndexedSortable接口,IndexedSortable接口是Hadoop中的排序器,Hadoop关于可排序的数据集定义了一个抽象接口IndexedSortable,也就是说任何能够排序的数据集必须要实现两个方法,一是能够比较它的数据集中任意两项的大小,二是能够交换它的数据集中任意两项的位置。实现了这个接口我们就可以使用hadoop预定义的快排进行排序。如上:new QuickSort().sort(this, 0, records.size());

那么样本怎么得来的呢?

我们需要从分片中获得,在Job启动前必须得到n-1个取样数据——>需要对输入的数据进行控制——>需要自定义实现InputFormat接口的类。InputFormat做了2件事:

(1)InputSplit[] getSplits(JobConf job, int numSplits) throws IOException; 得到划分

(2)RecordReader<K, V> getRecordReader(InputSplit split, JobConf job, Reporter reporter) throws IOException; 处理每个划分,对每个划分的数据生成KeyValue对

分片不用重写。需要自定义实现RecordReader接口的类。

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. static class TeraRecordReader implements RecordReader<IntWritable, Text> {  
  2.   
  3.         private LineRecordReader in;  
  4.         private LongWritable junk = new LongWritable();  
  5.         private Text line = new Text();  
  6.   
  7.         public TeraRecordReader(Configuration job, FileSplit split) throws IOException {  
  8.             in = new LineRecordReader(job, split);  
  9.         }  
  10.   
  11.         @Override  
  12.         public void close() throws IOException {  
  13.             in.close();  
  14.         }  
  15.   
  16.         @Override  
  17.         public IntWritable createKey() {  
  18.             return new IntWritable();  
  19.         }  
  20.   
  21.         @Override  
  22.         public Text createValue() {  
  23.             return new Text();  
  24.         }  
  25.   
  26.         @Override  
  27.         public long getPos() throws IOException {  
  28.             // TODO Auto-generated method stub  
  29.             return in.getPos();  
  30.         }  
  31.   
  32.         @Override  
  33.         public float getProgress() throws IOException {  
  34.             // TODO Auto-generated method stub  
  35.             return in.getProgress();  
  36.         }  
  37.   
  38.         @Override  
  39.         public boolean next(IntWritable key, Text value) throws IOException {  
  40.             if (in.next(junk, line)) {  
  41.                     key.set(Integer.parseInt(line.toString()));  
  42.                     value.clear();  
  43.                 return true;  
  44.             } else {  
  45.                 return false;  
  46.             }  
  47.         }  
  48.     }//end RecordReader  

默认情况下会对每个分片中的每行数据得到一个形如<Key=该行的起始位置:LongWritable,Value=该行的内容的:Text>的KeyValue对,我们需要将这个KeyValue对转化成我们想要的形式<Key=该行内容:IntWritable,Value=空字符串:Text>,所以如上重写了next函数。

到此我们可以按格式读到RecordReader提供的KeyValue对了。那么接下来我们就要找到读到的数据中你认为可以当做样本的数据:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public static void writePartitionFile(JobConf conf, Path partFile) throws IOException {  
  2.     SamplerInputFormat inputFormat = new SamplerInputFormat();  
  3.     TextSampler sampler = new TextSampler();  
  4.     int partitions = conf.getNumReduceTasks(); // Reducer任务的个数  
  5.     long sampleSize = conf.getLong(SAMPLE_SIZE, 100); // 采集数据-键值对的个数  
  6.     InputSplit[] splits = inputFormat.getSplits(conf, conf.getNumMapTasks());// 获得数据分片  
  7.     int samples = Math.min(10, splits.length);// 采集分片的个数  
  8.     long recordsPerSample = sampleSize / samples;// 每个分片采集的键值对个数  
  9.     int sampleStep = splits.length / samples; // 采集分片的步长  
  10.     long records = 0;  
  11.     IntWritable key = new IntWritable();  
  12.     Text value = new Text();  
  13.     for (int i = 0; i < samples; i++) {  
  14.         //to particular split construct a record_reader  
  15.         RecordReader<IntWritable, Text> reader = inputFormat.getRecordReader(splits[sampleStep * i], conf, null);  
  16.         while (reader.next(key, value)) {  
  17.             sampler.addKey(key);  
  18.             key=new IntWritable();  
  19.             value = new Text();  
  20.             records += 1;  
  21.             if ((i + 1) * recordsPerSample <= records) {  
  22.                 break;  
  23.             }  
  24.         }  
  25.     }  
  26.     FileSystem outFs = partFile.getFileSystem(conf);  
  27.     if (outFs.exists(partFile)) {  
  28.         outFs.delete(partFile, false);  
  29.     }  
  30.     SequenceFile.Writer writer = SequenceFile.createWriter(outFs, conf, partFile, IntWritable.class, NullWritable.class);  
  31.     NullWritable nullValue = NullWritable.get();  
  32.     for (IntWritable split : sampler.createPartitions(partitions)) {  
  33.         writer.append(split, nullValue);  
  34.     }  
  35.     writer.close();  
  36. }  

如上所示,我们通过writer将(n-1)个样本写入到了临时的样本文件中。接下来可以启动Job了。


3. Partition对每条数据做标记(即发往哪个reducer做处理)

在map-reduce流程中,partitioner会负责“告知”每条数据的归属地reducer,这里我们要根据上面写好的临时样本文件判断每天数据的归属,因此需要自定义实现Partitioner接口的类:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. // 自定义的Partitioner    
  2. public static class TotalOrderPartitioner implements Partitioner<IntWritable, NullWritable> {    
  3.       
  4.     private IntWritable[] splitPoints;    
  5.       
  6.     public TotalOrderPartitioner() {    
  7.     }    
  8.       
  9.     @Override    
  10.     public int getPartition(IntWritable key, NullWritable value, int numReduceTasks) {    
  11.         // TODO Auto-generated method stub    
  12.         return findPartition(key);    
  13.     }    
  14.       
  15.     public void configure(JobConf conf) {    
  16.         try {    
  17.             FileSystem fs = FileSystem.get(conf);  
  18.             Path partFile = new Path(SamplerInputFormat.PARTITION_FILENAME);    
  19.             splitPoints = readPartitions(fs, partFile, conf,splitPoints); // 读取采集文件   
  20.         } catch (IOException ie) {    
  21.             throw new IllegalArgumentException("can't read paritions file", ie);    
  22.         }    
  23.     }  
  24.     //通过找区间的方式定位partition  
  25.     public int findPartition(IntWritable key) {    
  26.         int len = splitPoints.length;    
  27.         for (int i = 0; i < len; i++) {    
  28.             int res = key.compareTo(splitPoints[i]);    
  29.             if (res > 0 && i < len - 1) {    
  30.                 continue;    
  31.             } else if (res == 0) {    
  32.                 return i;    
  33.             } else if (res < 0) {    
  34.                 return i;    
  35.             } else if (res > 0 && i == len - 1) {    
  36.                 return i + 1;    
  37.             }    
  38.         }   
  39.         return 0;    
  40.     }    
  41.       
  42.     private static IntWritable[] readPartitions(FileSystem fs, Path p, JobConf job, IntWritable[] splitPoints) throws IOException {   
  43.         URI[] uris = DistributedCache.getCacheFiles(fs.getConf());  
  44.         SequenceFile.Reader reader = new SequenceFile.Reader(fs, new Path(uris[0]), job);    
  45.         ArrayList<IntWritable> parts = new ArrayList<IntWritable>();    
  46.         IntWritable key = new IntWritable();             
  47.         NullWritable value = NullWritable.get();   
  48.         while (reader.next(key, value)) {    
  49.             parts.add(key);     
  50.             key=new IntWritable();  
  51.             value = NullWritable.get();  
  52.         }    
  53.         reader.close();    
  54.         splitPoints = new IntWritable[parts.size()];  
  55.         for(int i=0;i<parts.size();i++) {  
  56.             splitPoints[i] = parts.get(i);  
  57.         }  
  58.         return splitPoints;  
  59.     }    
  60. }   

如上所示,一个自定义的Partitioner只需要实现两个功能:getPartition()和configure()。

(1)getPartition()函数返回一个0到(Reducer数目-1)之间的int值来确定将<key,value>键值对送到哪一个Reducer中。

(2)configure()使用Hadoop Job Configuration来配置partitioner,并读取样本数据。

至此,我们控制了哪些数据发往哪些reducer,且这种控制是有序的控制,在每个reducer中的数据,hadoop会自动实现排序,因此整体上实现了全排序。

以上是整形的全排序,字符串的全排序与此大同小异。

注意:伪分布式reducer的个数只能是0或1,无法设置reducer的个数。



无论字符串排序还是整型排序都是在job启动前先把采样的样本放到SequenceFile中,然后job开始后,读取SequenceFile中的样本数据到一维数组中。之后,

(1)如果是字符串排序,既可以使用字符串比较的方法通过查找区间来定位partition,也可以通过构建2层字典树(terasort中使用的方法)定位partition;

(2)如果是整形排序,就直接按找区间的方法定位partition;

个人心得,如有错误请大神不吝赐教。

版权声明:本文为博主原创文章,未经博主允许不得转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值