MapReduce全局排序实践(利用Hadoop Streaming配置项)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012369535/article/details/89287890

问题:有两个文本,a.txt和b.txt,目的是想整合两个文本按照第一列(key)的数值全局升序排列
在这里插入图片描述
这个全局排序可以在本地通过linux命令进行:

[root@master boya]# cat a.txt b.txt | sort -k1

但是输出结果并不是按照数值升序:
在这里插入图片描述
这是因为sort -k1默认队第一列的各key按照从左到右逐个比较对应的数字的ASCII码来排序的(字典排序),因此我们需要指定按照数值的大小排序:

[root@master boya]# cat a.txt b.txt | sort -k1 -n| head -20

最终结果如下:
在这里插入图片描述
如果利用MapReduce框架完成上述全局排序,将怎样来处理呢?我们知道,map的输出结果是键值对的形式,框架先将一行行的键值数据分区,同一个分区的数据聚集在一起,每个分区内的数据按照key排序,然后每个分区内的数据会被分发到对应的reduce处理,最后reduce输出结果,这个过程叫shuffle。依据此,设定一个reduce task,即强制把map输出的结果都分到一个分区中,也就交给了一个reduce处理,再以第一个字段为key,交由MapReduce去排序,看看是否能得到期望的全局排序。

首先,编写map_sort.py和red_sort.py如下:

#!/usr/local/bin/python

import sys

for line in sys.stdin:
    ss = line.strip().split('\t')
    key = ss[0]
    val = ss[1]

    print "%s\t%s" % (key, val)

可以看到,map和reduce函数只是将数据作了分割然后输出的处理,排序的功能交由MapReduce框架自动处理

执行的shell脚本run.sh如下:

set -e -x

HADOOP_CMD="/usr/local/src/hadoop/hadoop-2.6.5/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop/hadoop-2.6.5/share/hadoop/tools/lib/hadoop-streaming-2.6.5.jar"

INPUT_FILE_PATH_A="/a.txt"
INPUT_FILE_PATH_B="/b.txt"

OUTPUT_SORT_PATH="/output_sort"

$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_SORT_PATH

# Step 3.
$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A,$INPUT_FILE_PATH_B\
    -output $OUTPUT_SORT_PATH \
    -mapper "python map_sort.py" \
    -reducer "python red_sort.py" \
    -file ./map_sort.py \
    -file ./red_sort.py \
    -jobconf mapred.reduce.tasks=1 \

注意,这里设置reduce task的数目为1,即所有数据都会进入到一个partition中,这时利用框架自带的排序功能(同一partition内的数据按照key升序),看看能否对数据进行全局排序。hadoop streaming会默认把map输出结果中每一行第一个分隔符(默认为"\t")前的字段作为key,后面的作为value,并且若不指定以key中哪个字段作partition,则默认以整个key字段作partition,但这里指定reduce task为1,强制只有1一个partition了。

执行run.sh的结果如下:

在这里插入图片描述
在这里插入图片描述
可以看到,MapReduce的排序也是按照key的ASCII码来排序即字典排序,如果我们不对map和reduce函数作任何处理,就不能得到期望的按数值的全局排序。

方法一(一个reduce task)

于是,我们修改map和reduce函数,map_sort.py如下:

#!/bin/python

import sys

base_count = 10000
for line in sys.stdin:
    ss = line.strip().split('\t')
    key = ss[0]
    val = ss[1]

    new_key = base_count + int(key)
    print "%s\t%s" % (new_key, val)

前面已验证MapReduce的排序是字典排序,数据的key都是在100以内,所以给每个key加上一个基准数10000,使所有数据的key拥有相同的格式(10000—10100),这样在字典排序中“表面上”以数值的形式来排序

red_sort.py如下:

#!/bin/python

import sys

base_value = 10000

for line in sys.stdin:
    ss = line.strip().split('\t')
    new_key = ss[0]
    val = ss[1]
    
    old_key = int(new_key) - base_value
    print "%s\t%s" % (str(old_key), val)

进入到reduce函数的数据是已经被MapReduce排好序的数据,只需要将当前的key还原成原来的key输出即可,本质上还是利用MapReduce自带的字典排序,另外执行脚本run.sh不变

执行结果如下:
在这里插入图片描述
可以看出,此时的结果已是按数值的全局排序,本质上还是利用MapReduce的字典排序

方法二(多个reduce task + Hadoop Streaming配置)

方法一虽然做到了全局排序,但是只有一个reduce task,若输入数据量很大,一个reduce无法做到并行计算,因此这里指定两个reduce task来作全局排序。输入数据还是a.txt b.txt,思路是想让key值为0—49的数据交给一个reduce来处理,key值为50—100的数据交给另一个reduce,然后每个reduce内部按照key的数值大小排序。

map_sort.py如下,为key为0—49的数据添加一列下标0,其他数据添加1:

#!/bin/python

import sys

base_count = 10000
for line in sys.stdin:
    ss = line.strip().split('\t')
    key = ss[0]
    val = ss[1]

    new_key = base_count + int(key)

    red_idx = 1
    if new_key < (10100 + 10000) / 2:
       red_idx = 0

    print "%s\t%s\t%s" % (red_idx, new_key, val)

red_sort.py如下:

#!/bin/python

import sys

base_value = 10000

for line in sys.stdin:
    idx_id, new_key, val = line.strip().split('\t')
   
    old_key = int(new_key) - base_value
    
    print "%s\t%s" % (str(old_key), val)

此时的run.sh如下,经过map输出后,每行数据有三个字段,设置以第一字段作分区,第一和第二字段作为排序的key:

set -e -x

HADOOP_CMD="/usr/local/src/hadoop/hadoop-2.6.5/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop/hadoop-2.6.5/share/hadoop/tools/lib/hadoop-streaming-2.6.5.jar"

INPUT_FILE_PATH_A="/a.txt"
INPUT_FILE_PATH_B="/b.txt"

OUTPUT_SORT_PATH="/output_sort"

$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_SORT_PATH

# Step 3.
$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A,$INPUT_FILE_PATH_B\
    -output $OUTPUT_SORT_PATH \
    -mapper "python map_sort.py" \
    -reducer "python red_sort.py" \
    -file ./map_sort.py \
    -file ./red_sort.py \
    -jobconf mapred.reduce.tasks=2 \
    -jobconf stream.num.map.output.key.fields=2 \ # 指定分桶内以前两列字段作排序
    -jobconf num.key.fields.for.partition=1 \ # 指定key内以第一列字段作分桶
    -partitioner org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner

num.key.fields.for.partition 设置key内前几个字段用来做partition
org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner 如果要设置key中用于partition的字段,而不是把整个key都用来做partition,就用此配置项

执行结果如下,此时有两个输出文件,每个文件内按key值排序,第一个文件内key从0到49,第二个文件key从50到100:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

Hadoop Streaming排序实践

以上的实践中我们均编写了map和reduce函数,现在不编写任何map和reduce函数,只利用hadoop streaming提供的配置项,来作排序的练习。输入数据aaa.txt如下:
在这里插入图片描述
我们知道,在hadoop streaming模式默认会把map输出的一行中遇到的第一个设定的字段分隔符“\t”前面的部分作为key,后面的作为value,如果输出的一行中没有指定的字段分隔符,则整行作为key,value被设置为空字符串。 那么对于上面的输出,如果想用map输出的前2个字段作为key,后面字段作为value,并且不使用hadoop默认的“\t”字段分隔符,而是根据该文本特点使用“.”来分割,需要如下设置:

$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A \
    -output $OUTPUT_SORT_PATH \
    -mapper "cat" \ 
    -reducer "cat" \
    -jobconf mapred.reduce.tasks=5 \
    -jobconf stream.num.map.output.key.fields=2 \  # 指定map输出中前两个字段作key
    -jobconf stream.map.output.field.separator=. \  # 指定map输出中的字段分隔符为"."

其中map和reduce阶段只作cat输出,不作任何其他的处理,执行结果如下:
在这里插入图片描述
从结果可以看出,在reduce的输出中,前两列和后两列用“\t”分隔,证明map输出时确实把用“.”分隔的前两列作为key,后面的作为value。并且前两列相同的“e.5”开头的三行被分到了同一个reduce中,证明确实以前两列作为key,并以整个key来作partition
stream.num.map.output.key.fields 设置map输出的前几个字段作为key
stream.map.output.field.separator 设置map输出的字段分隔符

KeyFieldBasePartitioner的用法

如果想要灵活设置key中用于partition的字段,而不是把整个key都用来做partition。就需要使用hadoop中的org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner了。
下面只用第一列作partition,但依然使用前两列作为key。

$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A \
    -output $OUTPUT_SORT_PATH \
    -mapper "cat" \
    -reducer "cat" \
    -partitioner org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner \
    -jobconf mapred.reduce.tasks=5 \
    -jobconf stream.num.map.output.key.fields=2 \ # 设置map输出结果以前两个字段为key
    -jobconf stream.map.output.field.separator=. \ # 设置map输出的字段分隔符
    -jobconf map.output.key.field.separator=. \ 设置key内的字段分隔符
    -jobconf num.key.fields.for.partition=1 \

在这里插入图片描述从结果可以看出,这次“e”开头的行都被分到了一个桶内,证明做partition是以第一列为准的,而key依然是前两列。并且在同一个partition内,先按照第一列排序,第一列相同的,按照第二列排序。这里要注意的是使用map.output.key.field.separator来指定key内字段的分隔符,这个参数是KeyFieldBasePartitioner和KeyFieldBaseComparator所特有的。
map.output.key.field.separator 设置key内的字段分隔符
num.key.fields.for.partition 设置key内前几个字段用来做partition

事实上KeyFieldBasePartitioner还有一个高级参数mapred.text.key.partitioner.options,这个参数可以认为是num.key.fields.for.partition的升级版,它可以指定不仅限于key中的前几个字段用做partition,而是可以单独指定key中某个字段或者某几个字段一起做partition。
比如上面的需求用mapred.text.key.partitioner.options表示为
mapred.text.key.partitioner.options=-k1,1
注意mapred.text.key.partitioner.options和num.key.fields.for.partition不需要一起使用,一起使用则以num.key.fields.for.partition为准。

这里再举一个例子,使用mapred.text.key.partitioner.options

$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A \
    -output $OUTPUT_SORT_PATH \
    -mapper "cat" \
    -reducer "cat" \
    -partitioner org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner \
    -jobconf mapred.reduce.tasks=5 \
    -jobconf stream.num.map.output.key.fields=3 \
    -jobconf stream.map.output.field.separator=. \
    -jobconf map.output.key.field.separator=. \
    -jobconf mapred.text.key.partitioner.options=-k2,3 # 以key中第2,3字段作partition

执行结果:
在这里插入图片描述
可见,这次是以前3列作为key的,而partition则以key中的第2-3列,因此以“e”开头的行被拆散了,但第二三列相同的“5,1”被分到一个桶内。在同一个桶内,依然是从key的第一列开始排序的,注意,KeyFieldBasePartitioner只影响分桶并不影响排序。
mapred.text.key.partitioner.options 设置key内某个字段或者某个字段范围用做partition

KeyFieldBaseComparator的用法

首先简单解释一下hadoop框架中key的comparator,对于hadoop所识别的所有java的key类型(在框架看来key的类型只能是java的),很多类型都自定义了基于字节的比较器,比如Text,IntWritable等等,如果不特别指定比较器而使用这些类型默认的,则会将key作为一个整体的字节数组来进行比较。而KeyFieldBaseComparator则相当于是一个可以灵活设置比较位置的高级比较器,但是它并没有自己独有的比较逻辑,而是使用默认Text的基于字典序或者通过-n来基于数字比较。

之前的例子使用KeyFieldBasePartitioner自定义了使用key中的部分字段做partition,现在我们通过org.apache.hadoop.mapred.lib.KeyFieldBasedComparator来自定义使用key中的部分字段做比较。

这次把前四列都作为key,前两列做partition,排序依据优先依据第三列正序(文本序),第四列逆序(数字序)的组合排序。

$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_A \
    -output $OUTPUT_SORT_PATH \
    -mapper "cat" \
    -reducer "cat" \
    -partitioner org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner \
    -jobconf mapred.output.key.comparator.class=org.apache.hadoop.mapred.lib.KeyFieldBasedComparator \
    -jobconf mapred.reduce.tasks=5 \
    -jobconf stream.num.map.output.key.fields=4 \
    -jobconf stream.map.output.field.separator=. \
    -jobconf map.output.key.field.separator=. \
    -jobconf mapred.text.key.partitioner.options=-k1,2 \
    -jobconf mapred.text.key.comparator.options="-k3,3 -k4nr"

执行结果:
在这里插入图片描述从结果可以看出,符合预期的按照先第三列文本正序,然后第四列基于数字逆序的排序。
另外注意,如果这种写法
mapred.text.key.comparator.options=”-k2″
则会从第二列开始,用字典序一直比较到key的最后一个字节。所以对于希望准确排序字段的需求,还是使用“k2,2”这种确定首尾范围的形式更好。另外如果给定的key中某些行需要排序的列数不够时,会比较到最后一列,缺列的行默认缺少的那一列排序值最小。
mapred.text.key.comparator.options 设置key中需要比较的字段或字段范围

猜你喜欢

转载自blog.csdn.net/u012369535/article/details/89287890
今日推荐