10天Hadoop快速突击(3)——开发MapReduce应用程序

开发MapReduce应用程序

一、系统参数的配置

1.通过API对相关组件的参数进行配置

这些API被分成了一下几个部分:

org.apache.hadoop.conf:定义了系统参数的配置文件处理API

org.apache.hadoop.fs:定义了抽象的文件系统API

org.apache.hadoop.dfs:Hadoop分布式文件系统(HDFS)模块的实现

org.apache.hadoop.mapred:Hadoop分布式计算系统(MapReduce)模块的实现,包括任务的分发调度

org.apache.hadoop.ipc:用在网络服务器和客户端的工具,封装了网络异步I/O的基础模块

org.apache.hadoop.io:定义了通用I/O API,用于针对网络、数据库、文件等数据对象进行读写操作

Configuration类由源来设置,每个源包含以XML形式出现的一系列属性/值对。每个源以一个字符串或一个路径来命名,如果是以字符串命名,则通过类路径检查该字符串代表的路径是否存在;如果是以路径命名的,则直接通过本地文件系统进行检查,而不用类对象。

2.多个配置文件的整合

使用两个资源configuration-default.xml和configuration-site.xml来定义配置。将资源按顺序添加到Configuration之中。后添加进来的属性取值覆盖掉前面所添加资源中的属性取值。但是,有一个特例,被标记为final的属性不能被后面定义的属性覆盖。重写标记为final的属性通常会报告配置错误,同时会有警告信息被记录下来以便为诊断所用。

管理员将守护进程地址文件之中的属性标记为final,可防止用户在客户端配置文件中或作业提交参数中改变其取值。

Hadoop默认使用两个源进行配置,并按顺序加载core-default.xml和core-site.xml。其中core-default.xml用于定义系统默认的属性,core-site.xml用于定义在特定的地方重写。

二、配置开发环境

首先下载准备使用的Hadoop版本,然后将其解压到用于开发的主机上。接着,在IDE中创建一个新的项目,将解压后的文件夹根目录下的JAR文件和lib目录之下的JAR文件加入到classpath中。之后就可以编译Hadoop程序,并且可以在IDE中以本地模式运行。

Hadoop有三种不同的运行方式:单机模式、伪分布模式、完全分布模式。这里使用伪分布模式,它运行的文件系统的HDFS,能够模拟完全分布模式,看到一些分布式处理的效果。

常用的方式是编写和调试程序在单机或伪分布模式下进行,而实际处理大数据则完全分布模式下进行。

三、编写MapReduce程序

程序分为两个部分:Map部分和Reduce部分,分别实现Map和Reduce的功能。

1.Map处理

Mapper处理的数据是由InputFormat分解过的数据集,其中InputFormat的作用是将数据集切割成小数据集InputSplit,每一个InputSplit将由一个Mapper负责处理。此外InputFormat中还提供了一个RecordReader的实现,并将一个InputSplit解析成对提供给Map函数。InputFormat的默认值为TextInputFormat,它针对文本文件,按行将文本文件切割成InputSplit,并用LineRecordReader将InputSplit解析成对,key是行在文本中的位置,value是文件中的一行。

2.Reduce处理

Map处理的结果会通过partition分发到Reduce,Reduce做完Reduce操作后,将通过OutputFormat输出结果。

Mapper最终处理的结果对会被送到Reduce中进行合并,在合并的时候,有相同的key的键/值对会被送到同一个Reducer上。Reducer是所有用户定制Reducer类的基类,它的输入是key及这个key对应的所有value的一个迭代器,还有Reducer的上下文。Reduce处理的结果将通过Reducer.Context的write方法输出到文件中。

四、本地测试

如果程序要Eclipse中执行,则用户需要在run configuration中设置好参数,输入的文件夹名为input,输出的文件夹名为output。

五、运行MapReduce程序

1.打包

为了能够在命令行中运行程序,首先需要对它进行编译和打包。

2.在本地模式下运行

使用下面的命令以本地模式运行打包后的程序:

hadoop jar ***.jar inputPath outputPath

3.在集群上运行

首先,将输入的文件复制到HDFS中,

hadoop dfs -copyFromLocal /home/user/hadoop/inputFile inputFile

然后,运行程序

hadoop jar /home/user/hadoop/inputFile.jar inputFile input output

六、网络用户界面

Hadoop自带的网络用户界面http://jobtracker-host:50030/

1.JobTracker页面

JobTracker页面主要包括五个部分:

第一部分是Hadoop安装的详细信息,包括版本号、编译完成时间、JobTracker当前的运行状态和开始时间

第二部分是集群的一个总结信息:集群容量(用集群上可用的Map和Reduce任务槽的数量表示)及使用情况、集群上运行的Map和Reduce的数量、提交的工作总量、当前可用的TaskTracker节点数和每个节点平均可用槽的数量。

第三部分是一个正在运行的工作日程表。打开能看到工作的序列。

第四部分显示的是正在运行、完成、失败的工作,这些信息通过表格来体现。表格中的每一行代表一个工作并显示了工作的ID号、所属者、名字和进程信息。

第五部分是页面的最下面JobTracker日志的连接和JobTracker的历史信息:JobTracker运行的所有工作信息。历史记录是永久保存的。

2.工作页面

点击一个工作的ID号,进入工作页面,在工作页面的顶部是一个关于工作的一些总结性基本信息,如工作所属者、名字、执行时间等。工作文件是工作的加强配置文件,包括在工作运行期间所有有效的属性及它们的取值。

当工作运行时,可以在页面上监控它的进展情况,因为页面会周期性更新。

Reduce完成图分为3个阶段:复制(发生在Map输出转交给Reduce的TaskTracker时)、排序(发生在Reduce输入合并时)和Reduce(发生在Reduce函数起作用并产生最终输出时)。

3.返回结果

可以通过几种方式得到结果:

1)通过命令行直接显示输出文件夹中的文件。

2)将输出的文件从HDFS复制到本地文件系统上,在本地文件系统上查看。

3)通过Web界面查看输出的结果。

4.任务页面

工作页面中的一些链接可以用来查看该工作中任务的详细信息。

任务页面显示的信息以表格形式来体现,表中的每一行都表示一个任务,它包含了诸如开始时间、结束时间之类的信息,以及由TaskTracker提供的错误信息和查看单个任务的计数器的链接。

5.任务细节页面

在任务页面上可以点击任何任务来得到关于它的详细信息。

七、性能调优

性能调优具体来讲包括两个方面的内容:一个是时间性能,另一个是空间性能。衡量性能的指标就是,能够在正确完成功能的基础上,使执行的时间尽量短、占用的空间尽量小。

1.输入采用大文件

HDFS块的默认大小为64M。

如果不对小文件做合并的预处理,也可借用Hadoop中的CombineFileInputFormat,它可以将多个文件打包到一个输入单元中,从而每次执行Map操作就会处理更多的数据。同时,ConbineFileInputFormat会考虑节点和集群的位置信息,以决定哪些文件被打包到一个单元之中,所以使用CombineFileInputFormat也会使性能得到相应地提高。

2.压缩文件

在分布式系统中,不同节点的数据交换时影响整体性能的一个重要因素。另外在Hadoop的Map阶段所处理的输出大小也会影响整个MapReduce程序的执行时间。这是应为Map阶段的输出首先存储在一定大小的内存缓冲区中,如果Map输出的大小超出一定限度,Map task就会将结果写入磁盘,等Map任务结束后再将它们复制到Reduce任务的节点上。如果数据量大,中间的数据交换会占用很多的时间。

一个提高性能的方法是对Map的输出进行压缩。这样的好处有:减少存储文件的空间、加快数据在网络上(不同节点间)的传输速度,减少数据在内存和磁盘间交换的时间。

可以通过将mapred.compress.map.output属性设置为true来对Map的输出数据进行压缩,同时还可以设置Map输出数据的压缩格式,通过设置mapred.output.compression.codec属性即可进行压缩格式的设置。

3.过滤数据

数据过滤主要指在面对海量输入数据作业时,在作业执行之间先将数据中无用数据、噪声数据和异常数据清除。通过数据过滤可以降低数据处理的规模,较大程度低提高数据处理效率,同时避免异常数据或不规范数据对最终结果造成的负面影响。

如何进行数据过滤呢?在MapReduce中可以根据过滤条件利用很多办法完成数据预处理中的数据过滤,如编写预处理程序,在程序中添加过滤条件,形成真正的数据;也可以在数据处理任务的最开始代码处加上过滤条件;还可以使用特殊的过滤器数据结果来完成过滤。

Bloom Filter是一种二进制向量数据结构。在保存所有集合元素特征的同时,它能在保证高效空间效率和一定出错率的前提下迅速检测一个元素是不是集合中的成员。而且Bloom Filter的误报(false positive)只会发生在检测集合内的数据上,而不会对集合外的数据产生漏报(false negative)。这样每个检测请求返回有“在集合内(可能错误)”和“不在集合内(绝对不在集合内)”两种情况,可见Bloom Filter牺牲了极少正确率换取时间和空间,所以它不适合那些“零错误”的应用场合。在MapReduce中,Bloom Filter由Bloom Filter类(此类继承了Filter类、Filter类实现了Writable序列化接口)实现,使用add(Key key)函数将一个key值加入Filter,使用membershipTest(Key key)来测试某个key是够在Filter内。

Bloom Filter具体是如何实现的呢?如何保证空间和时间的高效性呢?如何用正确率换取时间和空间呢?(基于MapReduce中实现的Bloom Filter代码进行分析)Bloom Filter自始至终是一个M位的位数组。有两个重要接口,分别是add()和membershipTest(),add()负责保存集合元素的特征到位数组,在保存所有集合元素特征之后可以使用membershipTest()来判断某个值是否是集合中的元素。

在初始状态下,Bloom Filter的所有位都被初始化为0。为了表示集合中的所有元素,Bloom Filter使用k个互相独立的Hash函数,它们分别将集合中的每个元素映射到(1,2,...,M)这个范围上,映射的位置作为此元素特征值的一维,并将位数组中此位置的值设置为1,最终得到的k个Hash函数值将形成集合元素的特征值向量,同时此向量也被保存在位数组中。从获取k个Hash函数值到修改对应位数组值,这就是add接口所完成的任务。

利用add接口将所有集合元素的特征值向量保存到Bloom Filter之后,就可以使用此过滤去也就是membershipTest接口来判断某个值是否是集合元素。在判断时,首先还是计算待判断值的特征值向量,也就是k个Hash函数值,然后判断特征值向量每一维对应的位数组位置上的值是否是1,如果全部是1,那么membershipTest返回True,否则返回false,这就是判断值是否存在于集合中的原理。

正是Hash函数冲突的可能性导致误判的可能。由于Hash函数冲突,两个值的特征值向量也有可能冲突(k个Hash函数全部冲突)。如果两个值只有一个集合元素,那么该值的特征值向量会保存在位数组中,从而在判断另外一个非集合元素的值时,会发现该值的特征值向量已经保存在位数组中,最终返回true,形成误判。

有哪些因素影响错误率?Hash函数的个数和位数组的大小影响了错误率。位数组越大,特征值向量冲突的可能性越小,错误率也小。在位数组大小一定的情况下,Hash函数个数越多,形成的特征值向量维数越多,冲突的可能性越小;但是维数越多,占用的位数组位置越多,又提高了冲突的可能性。所以在实际中,应根据实际需要和一定的估计来确定合适的数组规模和哈希函数规模。

在Bloom Filter中插入元素和查询值都是O(1)的操作;同时它并不保存元素而是采用位数组保存特征值,并且每一位都可以重复利用。但是错误率限制了Bloom Filter的使用场景,只允许误报的场景;同时由于一位多用,Bloom Filter并不支持删除集合元素,在删除某个元素时可能会同时删除另外一个元素的部分特征值。

4.修改作业属性

属性mapred.tasktracker.map.tasks.maximum的默认值是2,属性mapred.tasktracker.reduce.tasks.maximum的默认值也是2,因此每个节点上实际处于运行状态的Map和Reduce的任务数量最多为2,而较为理想的数值应在10~100之间。因此,可以在conf目录下修改属性mapred.tasktracker.map.tasks.maximum和mapred.tasktracker.reduce.tasks.maximum的取值,将它们设置为一个较大的值,使得每个节点上同时运行的Map和Reduce任务数增加,从而缩短运行的时间,提高整体的性能。

八、MapReduce工作流

1.复杂的Map和Reduce函数

Map和Reduce都继承自MapReduce自己定义好的Mapper和Reducer基类,MapReduce框架根据用户继承Mapper和Reducer后的衍生类和类中覆盖的核心函数来识别用户定义的Map处理阶段和Reducec处理阶段,所以只有用户继承这些类并且事先其中的核心函数,提交到MapReduce框架上的作业才能按照用户的意愿被解析出来并执行。

1)setup函数

setup函数是在task启动开始就调用的。setup函数在task启动之后数据处理之前只调用一次,而覆盖的Map函数或Reduce函数会针对输入分片中的每个key调用一次。所以setup函数可以看作task上的一个全局处理,而不像在Map函数或Reduce函数中,处理只对当前输入分片中的正在处理数据产生作用。利用setup函数的特性,可以将Map或Reduce函数中的重复处理放置在setup函数中,可以将Map或Reduce函数处理过程中可能使用到的全局变量进行初始化,或从作业信息中获取全局变量,还可以监控task的启动。

需要注意的是:调用setup函数只是对应task上的全局操作,而不是整个作业的全局操作。

2)cleanup函数

cleanup函数跟setup函数相似,不同之处在于cleanup函数是在task销毁之前执行的,它的作用和setup也相似,区别仅在于它的启动处于task销毁之前。

3)run函数

此函数是Map类和Reduce类的自动方法:先调用setup函数,然后针对每个key调用一次Map函数或Reduce函数,最后销毁task之前再调用cleanup函数。这个run函数将Map阶段和Reduce阶段的代码过程呈现出来。

2.MapReduce Job中全局共享数据

在编程过程中全局变量的使用时不可避免的,但是在MapReduce中直接使用代码级别的全局变量是不现实的。这主要是应为继承Mapper基类的Map阶段类的运行和继承Reduce基类的Reduce阶段类的运行都是独立的,并不像代码看起来的那样会共享同一个Java虚拟机的资源。

1)读写HDFS文件

在MapReduce框架中,Map task和Reduce task都运行在Hadoop集群的节点上,所以Map task和Reduce task、甚至不同的Job都可以通过读写HDFS中预定好的同一个文件来实现全局共享数据。需要注意的是,针对多个Map和Reduce的写操作会产生冲突,覆盖原有数据。

这种方法的优点是能够实现读写、也比较直观;而缺点是要共享一些很小的全局数据也需要使用I/O,这将占用系统资源、增加作业完成的资源消耗。

2)配置Job属性

在配置MapReduce执行过程中,task可以读取Job的属性,可以在任务启动之初利用Configuration类中的set(String name,String value)将一些简单的全局数据封装到作业的配置属性中,然后在task中再利用Configuration类中的get(String name)获取配置到属性中的全局数据。这种方法的有优点是简单,资源消耗小,缺点是对量比较大的共享数据显得比较无力。

3)使用DistributedCache

DistributedCache是MapReduce为应用提供缓存文件的只读工具,它可以缓存文件文本文件、压缩文件和jar文件等。在使用时,用户可以在作业配置时使用本地或HDFS文件的URL来将其设置成共享缓存文件。在作业启动之后和task启动之前,MapReduce框架会将可能需要的缓存文件复制到执行任务节点的本地。这种方法的优点是每个Job共享文件只会在启动之后复制一次,并且它适用于大量的共享数据;而缺点是它是只读的。

如何使用DistrubutedCache的示例:

  • 将要缓存的文件复制到HDFS上
  • 启用作业的属性配置,并设置待缓存文件
  • 在Map函数中使用DistrubutedCache

3.链接MapReduce Job

1).线性MapRecuce Job流

MapReduce Job也是一个程序,作为程序就是将输入经过处理再输出。所以在处理复杂问题的时候,如果一个Job不能完成,最简单的办法就是设置多个有一定顺序的Job,每个Job以前有一个Job的输出作为输入,经过处理,将数据再输出到下一个Job中。这样Job流就能按照预定的代码处理数据,达到预期的目的。这种办法的具体实现非常简单:将每个Job的启动代码设置成只有上一个Job结束之后才执行,然后将Job的输入设置成上一个Job的输出路径。

2)复杂的MapReduce Job流

处理过程中数据流并不是简单的线性流。这种情况下,MapReduce框架提供了让用户将Job组织成复杂Job流的API——ControlledJob类和JobControl类(这两个类属于org.apache.hadoop.mapreduce.lib.jobcontrl包)。具体做法:先按照正常情况配置各个Job,配置完成后再将各个Job封装到对应的ControlledJob对象中,然后使用ControlledJob的addDependingJob()设置依赖关系,接着再实例化一个JobControl对象,并使用addJob()方法将所有的Job注入JobControl对象中,最后使用JobControl对象的run方法启动Job流。

3)Job设置预处理和后处理过程

在Job处理前和处理后需要做一些简单地处理,这种情况使用第一种方法仍能解决,但是如果针对这些简单的处理设置新的Job类处理稍显笨拙,这里通过在Job前或后链接Map过程来解决预处理和后处理。具体是通过MapReduce中org.apache.hadoop.mapred.lib包下的ChainMapper和ChainReducer两个静态类来实现的,这种方法最终形成的是一个独立的Job,而不是Job流,并且只有针对Job的输入输出流,各个阶段函数之间的输入输出MapReduce框架会自动组织。

需要注意的是,ChainMapper和ChainReducer只支持旧的API,即Map和Reduce必须是实现org.apache.hadoop.mapred.Mapper接口的静态类。

参考资料:

《Hadoop实战》第二版

你可能感兴趣的:(我的学习历程,BigData大数据学习与实战)