3.5Hadoop作业中的第三方函数库
到目前为止,在Mapper和Reducer类中只使用了标准Java函数库和Hadoop函数库。这些标准函数库包括了Hadoop发行版中的类库和标准Java类库(比如String.class)。
可是,仅仅使用这些标准函数库不能够开发复杂的Hadoop作业程序,有时我们需要第三方库的支持。如前所述,Mapper和Reducer类中使用的这些库需要被发送并配置到集群中运行Mapper和Reducer实例的所有节点。
首先,你要编写一个跟介绍过的代码不同的使用第三方库的程序。代码清单3-4中是一个使用了第三方库的Word Count程序。这个例子是故意如此编写的,以便更好地演示说明。
假设你有一个包含一系列单词的文件,其中有些单词由Unicode字符和数字(0~9)组成。其他都是由Unicode字符组成。由数字和字母组成的单词常常用在合同文件中,合同文件中各种各样的标示符就是由数字和字母组成的。简便起见,像“-“和“$”这样的字符是不允许出现在单词中的。
假设我们只关心由字母和数字组成的单词。为了判断单词中是否只包含有字母和数字,我们需要用到commons-lang函数库中的StringUtils.class。使用下面的逻辑来调用相应的方法以做出正确的判断:
代码清单3-4中展示了全部源代码。
稍后讨论代码清单3-4。首先,你要理解为了使用第三方函数库,POM文件所作出的修改。代码清单3-5展示了POM文件中的有关部分。
在这个POM文件中,添加了commons-lang 2.3版本的函数库。注意添加hadoop-client函数库时使用的标签中的内容为provided。到目前为止,标签中提供的配置项会使程序在编译之后生成两个独立的JAR文件。
- prohadoop-0.0.1-SNAPSHOT.jar仅包含org.apress包及其子包中的用户自定义的类。
- prohadoop-0.0.1-SNAPSHOT-jar-with-dependencies.jar中不仅仅包含了用户自己编写的类,还包含了commons-lang-2.3.jar文件中的类。由于POM文件中,添加hadoop- client函数库依赖时配置的scope值为provided,所以生成的JAR文件中不包括这个函数依赖库。这个配置项的含义是,在程序编译时使用这个函数库,但是不把它打包到最终的JAR文件中。
为什么hadoop-client及其他的依赖函数库不包含在prohadoop-0.0.1-SNAPSHOT-jar-with-dependencies.jar文件中呢?我们知道,Maven不但解析声明的函数库,还会解析该函数库(带有正确版本号)所依赖的其他函数库。如果hadoop-client函数库及其他依赖的函数库也被打包,会导致JAR文件的臃肿不堪。hadoop-client函数库已经配置在了集群中的各个节点上,所以hadoop-client中的类都不需要打包到作业程序的JAR文件中。这是一个非常重要的优化。由于程序打包排除掉了hadoop-client函数库,整合依赖之后的JAR文件体积大大减小(比如200KB与20MB)。回想一下,这个JAR文件需要从客户端节点传输到远程节点。JAR文件的大小会影响到作业程序的启动耗时,因为只有当该文件传输完毕,作业才能够启动运行。试着去掉标签,并重新编译。
我们来讲解前文中代码清单3-4中的程序。需要注意的是有一个附加的函数库包含在程序之中。这个函数库是commons-lang-2.3.jar,远程的Mapper 和Reducer类会使用这个函数库。WordCountNewAPIV2.class扩展并实现了新的类,运行这个程序需要把它依赖的函数库JAR文件分发到执行该程序的计算节点。
使用下面的命令来执行该程序:
上面的命令、参数及其环境变量配置的解释如下:
- HADOOP_CLASSPATH中的配置确保了位于$HADOOP_HOME/bin文件夹中的hadoop命令能够访问依赖JAR文件,这个依赖JAR文件是启动MapReduce作业的客户端程序要使用的。在本示例中,这个变量不是必须的,因为示例中的MapReduce 作业的客户端程序没有使用任何第三方函数库。
- $LIBJARS变量是一个逗号分隔的函数库文件路径列表,这些函数库文件是集群中数据节点执行Mapper和Reducer类的时候要使用的。需要注意的是这个列表的分隔符不同于在HADOOP_CLASSPATH变量指定函数库的时候用到的分隔符。
- 作业程序JAR文件和$LIBJARS变量中设定的JAR文件会被发送到集群上所有执行该Map/Reduce任务的节点上。这是一个移动代码到数据附近的例子(第1章中介绍的)。这些JAR文件会被配置到远程节点的CLASSPATH变量中。main()函数中的ToolRunner类负责这个工作。
- 和 中配置的路径指向HDFS上的相关位置。
- 运行作业程序的时候要确保Reducers运行过程中不会抛出由于没有找到包含StringUtils.class的函数库JAR文件而导致的ClassNotFoundException异常。
有另一种不需要配置–libjars选项的方法来运行这个作业,其命令如下:
注意两种方法的关键区别。如果依赖的函数库已经被打包进了作业程序的JAR文件中,我们就不需要-libjars这个选项了。每当作业执行的时候,作业程序JAR文件就会被发送到远程节点上进行配置。实际上,新旧API写成的作业程序都可以使用这样的方法,而且在不使用代码清单3-4中介绍的ToolRunner类的情况下也可以使用这个方法。使用这个方法唯一的缺点就是当依赖函数库数量很大的时候,这种情况在实际情况中会经常碰到,把所有的依赖函数库都打包到JAR文件中,会导致JAR文件变得很大,这样的话,在程序构建/测试周期内会增加程序的编译时间。如果你的测试环境与开发环境不在同一个集群环境中(比如你有另外一个配置在云中的测试集群),当你需要经常修改你的作业程序代码的时候,而包含所有依赖函数库的JAR文件体积又比较大,经常移动这样的文件是比较耗费时间的。但请记住,这是执行作业程序的最通用的方法。
在这本书的后续讲解过程中,程序作业的开发均基于MapReduce框架的新API,并使用ToolRunner类来执行它。
最后,我们来讨论一下代码清单3-4中的内容,它与代码清单3-2和代码清单3-3之间有什么不同呢?其主要不同在于它们使用不同的命令行来执行作业程序;以及为了确保在远程节点上执行的Map和Reduce任务可以访问第三方函数库,在作业程序开发中使用了不同的方法来编写类:
- 代码清单3-4更多地使用GenericOptionsParser.class类来获取程序运行的参数,比如输入文件路径和输出文件路径。使用方法如下所示:
- 在提交执行Hadoop作业的命令行中,删除了-libjars和$LIBJARS_PATH参数,仅仅返回程序所必需的参数。
- 在代码清单3-4中最重要的一个方面是,在main()方法中调用ToolRunner.run()时所使用的Configuration实例必须与run()方法中用来配置作业的Configuration实例是同一个实例。为了确保这一点,run()方法中一般都会使用getConf()方法来获取Configuration实例,getConf()是在Configurable接口中定义的,并在该接口的继承类Configured类中实现的。如果使用的不是同一个Configuration实例,作业就会配置错误,远程节点上运行的Mapper 和 Reducer 任务也无法访问第三方JAR文件。
- ToolRunner.run()方法负责解析-libjars参数。它将解析这个参数的任务委托给GenericOptionsParser.class执行。把-libjars参数的值添加到Configuration对象,用这样的方法来配置远程任务。
- 代码清单3-2和代码清单3-3没有方法来传输第三方函数库。如果你想在代码清单3-2和代码清单3-3的程序中使用第三方函数库,需要打包到应用程序JAR文件中。这个打包后的文件就是使用Maven构建生成的prohadoop-0.0.1-SNAPSHOT-jar-with-dependencies。(构建这个JAR文件的过程和目的,我们已经在前面介绍过了。)
表3-3中列出了代码清单3-4中MapReduce程序的各个组件。