使用python编写hadoop的mapper 和reducer

参考:
https://www.cnblogs.com/hopelee/p/7476145.html
https://blog.csdn.net/zhaoyl03/article/details/8657031
https://blog.csdn.net/inte_sleeper/article/details/6614857
http://www.cnblogs.com/harveyaot/p/3205403.html

上一篇文章,学习了搭建hadoop 环境,现学习如何使用python编写mapper 和reducer。如未搭建hadoop环境,请参考hadoop搭建

Hadoop Streaming 原理

Hadoop 本身是用 Java 开发的,程序也需要用 Java 编写,但是通过 Hadoop Streaming,我们可以使用任意语言来编写程序,让 Hadoop 运行。

Hadoop Streaming 就是通过将其他语言编写的 mapper 和 reducer 通过参数传给一个事先写好的 Java 程序(Hadoop 自带的 *-streaming.jar),这个 Java 程序会负责创建 MR 作业,另开一个进程来运行 mapper,将得到的输入通过 stdin 传给它,再将 mapper 处理后输出到 stdout 的数据交给 Hadoop,经过 partition 和 sort 之后,再另开进程运行 reducer,同样通过 stdin/stdout 得到最终结果。

Python的MapReduce代码
因此,使用Python编写MapReduce代码的技巧就在于我们使用了 HadoopStreaming 来帮助我们在Map 和 Reduce间传递数据通过STDIN (标准输入)和STDOUT (标准输出).我们仅仅使用Python的sys.stdin来输入数据,使用sys.stdout输出数据,这样做是因为HadoopStreaming会帮我们办好其他事。


创建文件,上传文件

当前路径下,创建一本电子书,包含英文单词(后面mapper 和reduce 统计单词频次需要使用)

hadoop@derekUbun:/usr/local/hadoop$ bin/hdfs dfs -mkdir -p /input
hadoop@derekUbun:/usr/local/hadoop$ bin/hdfs dfs -put ./book.txt /input
编写mapper.py 文件

将下列的代码保存在/home/hadoop/example/mapper.py中,他将从STDIN读取数据并将单词成行分隔开,生成一个列表映射单词与发生次数的关系:
注意:要确保这个脚本有足够权限(chmod +x mapper.py)

#!/usr/bin/env python
import sys
# input comes from STDIN (standard input)
for line in sys.stdin:
    line = line.strip()
    words = line.split()
    for word in words:
        print '%s\t%s' % (word, 1)
编写reducer 文件

将代码存储在/home/hadoop/example/reducer.py 中,这个脚本的作用是从mapper.py 的STDOUT中读取结果,然后计算每个单词出现次数的总和,并输出结果到STDOUT。
同样,要注意脚本权限:chmod +x reducer.py

#!/usr/bin/env python
from operator import itemgetter
import sys

current_word = None
current_count = 0
word = None
# input comes from STDIN
for line in sys.stdin:
    line = line.strip()
    word, count = line.split('\t', 1)
    try:
        count = int(count)
    except ValueError:
        continue
    if current_word == word:
        current_count += count
    else:
        if current_word:
            # write result to STDOUT
            print '%s\t%s' % (current_word, current_count)
        current_count = count
        current_word = word

# do not forget to output the last word if needed!
if current_word == word:
    print '%s\t%s' % (current_word, current_count)
自测

在运行MapReduce job测试前尝试手工测试你的mapper.py 和 reducer.py脚本,以免得不到任何返回结果。这里有一些建议,关于如何测试你的Map和Reduce的功能:

hadoop@derekUbun:/usr/local/hadoop$ echo "foo foo quux labs foo bar quux" | ./mapper.py
foo      1
foo      1
quux     1
labs     1
foo      1
bar      1
quux     1
hadoop@derekUbun:/usr/local/hadoop$ echo "foo foo quux labs foo bar quux" |./mapper.py | sort |./reducer.py
bar     1
foo     3
labs    1
quux    2
Hadoop 运行

一切准备就绪,我们将在运行Python MapReduce job 在Hadoop集群上。像我上面所说的,我们使用的是HadoopStreaming 帮助我们传递数据在Map和Reduce间并通过STDIN和STDOUT,进行标准化输入输出。

hadoop@derekUbun:/usr/local/hadoop$ hadoop jar share/hadoop/tools/lib/hadoop-streaming-2.7.6.jar  \
-mapper 'python mapper.py' \
-file /home/hadoop/example/mapper.py \
-reducer 'python reducer.py' \
-file /home/hadoop/example/reducer.py 
-input hdfs:/input/book.txt \
-output output

第一行是告诉 Hadoop 运行 Streaming 的 Java 程序,接下来的是参数:
这里的mapper 后面跟的其实是一个命令。也就是说,-mapper 和 -reducer 后面跟的文件名不需要带上路径。而 -file 后的参数需要带上路径,为了让 Hadoop 将程序分发给其他机器,需要-file 参数指明要分发的程序放在哪里。

注意:如果你在 mapper 后的命令用了引号,加上路径名反而会报错说找不到这个程序。(因为 -file 选项会将对应的本地参数文件上传至 Hadoop Streaming 的工作路径下,所以再执行 -mapper 对应的参数命令能直接找到对应的文件。

-input 和 -output 后面跟的是 HDFS 上的路径名,这里的 input/book.txt 指的是input 文件夹下刚才上传的文本文件,注意 -output 后面跟着的需要是一个不存在于 HDFS 上的路径,在产生输出的时候 Hadoop 会帮你创建这个文件夹,如果已经存在的话就会产生冲突。因此每次执行 Hadoop Streaming 前可以通过脚本命令 hadoop fs -rmr 清除输出路径。

由于 mapper 和 reducer 参数跟的实际上是命令,所以如果每台机器上 python 的环境配置不一样的话,会用每台机器自己的配置去执行 python 程序。

结果获取

如果运行中遇到问题,注意看报错,然后进行调整。
运行结束之后,结果存储在hdfs上 output目录下。
查看结果:hadoop@derekUbun:/usr/local/hadoop$ bin/hdfs dfs -ls output
从hdfs拷贝至本地:hadoop@derekUbun:/usr/local/hadoop$ bin/hdfs dfs -get output/* ./

注:如果结果中包含_SUCCESS 则说明本次运行成功。


后续

Python 依赖库问题

用hadoop streaming可以运行python写的map-reduce作业。但是如果map/reduce依赖于其他库呢?
比如,map中依赖于我们自己写的一个库:hadoop_lib.py中的某些方法。这时在运行streaming作业的时候,如果还是像原来一样的命令行,就会出现”Broken Pipe”之类的异常。
解决方法就是加上-file参数,并加上依赖的库文件。如果有多个依赖的文件,可以用多次-file参数,或者用-files。这样实际上hadoop是把文件放入它的分布式缓存中,然后在执行task的目录创建一个到实际文件的链接。

而在map.py/reduce.py中,我们直接导入即可:

import hadoop_lib

这个依赖库默认是和map/reduce程序在同一路径下。这对于开发可不太好,总不能把lib里的东西都放到特定作业的目录里来吧。

另一种方法就是在hadoop上引入第三方库时,可以将job 依赖的所有第三方的third-party package都放进 ./lib 中使用tar打包成.tgz格式。
–archives 参数会自动上传到job的task目录并且自动解压缩,可以使用#表示解压缩后的文件夹名称。
我的命令如下,测试运行ok,可参考:

hadoop jar share/hadoop/tools/lib/hadoop-streaming-2.7.6.jar  -archives /home/hadoop/srchadoop/lib.tar.gz -mapper 'lib.tar.gz/python/bin/python mapper.py' -reducer 'lib.tar.gz/python/bin/python reducer.py' -file /home/hadoop/srchadoop/mapper.py -file /home/hadoop/srchadoop/reducer.py  -input hdfs:/input/* -output /output
性能优化

使用 Python 编写 Hadoop Streaming 程序有几点需要注意:
1、在能使用 iterator 的情况下,尽量使用 iterator,避免将 stdin 的输入大量储存在内存里,否则会严重降低性能。关于iterator 和generator相关可参考了解Python生成器
2、Streaming 不会帮你分割 key 和 value 传进来,传进来的只是一个个字符串而已,需要你自己在代码里手动调用 split()。
3、从 stdin 得到的每一行数据末尾似乎会有 ‘\n’ ,保险起见一般都需要用 rstrip() 来去掉。
4、在想获得 key-value list 而不是一个个处理 key-value pair 时,可以使用 groupby 配合 itemgetter 将 key 相同的 key-value pair 组成一个个 group,得到类似 Java 编写的 reduce 可以直接获取一个 Text 类型的 key 和一个 iterable 作为 value 的效果。注意 itemgetter 的效率比 lambda 表达式的效率要高,所以用 itemgetter 比较好。

编写 Hadoop Streaming 程序的基本模版:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Some description here...
"""
import sys
from operator import itemgetter
from itertools import groupby

def read_input(file):
"""Read input and split."""
    for line in file:
    yield line.rstrip().split('\t')

def main():
    data = read_input(sys.stdin)
    for key, kviter in groupby(data, itemgetter(0)):
        # some code here..

if __name__ == "__main__":
    main()

猜你喜欢

转载自blog.csdn.net/marywang56/article/details/80395519