MapReduce作为大数据技术栈中的经典款,虽然后辈精英层出不穷,但是是作为基础学习或者是面对经典的拆分聚合任务,其还是有相当的价值,值得为之写上一写。作为一款优秀的框架工具,它同样具有上手简单快速但支持高度自定义的特点,配置繁多,在实际应用过程中不可避免地会遇到各种各样的问题,同时在分布式执行的环境下,调试排查也是复杂不少。本篇总结了一些在应用过程中遇到的非代码逻辑问题,可谓避坑总结,同时简述了一些打包提交和入口类编写的规范。注意本篇内容基于Hadoop 2.7.3版本。

NoSuchMethodError

典型的错误信息如下:

1
ERROR [main] org.apache.hadoop.mapred.YarnChild: Error running child : java.lang.NoSuchMethodError: xxx.xxx.xxx()...

出现该种错误基本上可以肯定就是依赖的jar包出现了冲突,可能是应用程序的jar包之间冲突,也有可能是应用程序与Hadoop依赖的jar包产生冲突,当然产生jar包冲突时也有可能是其他形式的表现。对于jar包冲突来说,基本上分为两种:

  1. 冲突的包向前兼容:

    这种情况比较简单,将调整依赖至最新版本即可,如果是与hadoop的依赖冲突,那么可以增加参数mapreduce.job.user.classpath.first,参考类org.apache.hadoop.mapreduce.MRJobConfig。值得注意的是,这是针对Map和Reduce任务执行环境来说的,如果是在任务提交之前执行的代码发生了冲突,那么就需要寻找别的解决方法了,需要改变hadoop jar这个命令的执行环境(默认是优先加载hadoop的jar包),或者直接采用后面叙述的shade方法。

  2. 冲突的包向前不兼容:

    这种情况下就必须要对依赖低版本jar包的相关依赖项进行依赖重命名处理。此种最典型的当属guava这个包的冲突,简直就是万恶之源。以常见的对HBase使用maven shade插件进行处理为例,创建一个新的maven项目,仅引入指定版本的HBase以及maven-shade-plugin,pom文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 指定maven坐标 -->
    <groupId>com.yourgroupId</groupId>
    <artifactId>hbase-server-customize-shade</artifactId>
    <version>1.1.2</version>

    <dependencies>
    <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.1.2</version>
    <exclusions>
    <!-- 打包后所有依赖的class都会在jar包中,因此尽可能排除不需要的内容,防止冲突 -->
    <exclusion>
    <artifactId>hadoop-auth</artifactId>
    <groupId>org.apache.hadoop</groupId>
    </exclusion>
    <exclusion>
    <artifactId>hadoop-client</artifactId>
    <groupId>org.apache.hadoop</groupId>
    </exclusion>
    <exclusion>
    <artifactId>hadoop-common</artifactId>
    <groupId>org.apache.hadoop</groupId>
    </exclusion>
    <exclusion>
    <artifactId>hadoop-hdfs</artifactId>
    <groupId>org.apache.hadoop</groupId>
    </exclusion>
    <exclusion>
    <artifactId>hadoop-mapreduce-client-core</artifactId>
    <groupId>org.apache.hadoop</groupId>
    </exclusion>
    <exclusion>
    <artifactId>junit</artifactId>
    <groupId>junit</groupId>
    </exclusion>
    <exclusion>
    <artifactId>servlet-api-2.5</artifactId>
    <groupId>org.mortbay.jetty</groupId>
    </exclusion>
    </exclusions>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.2</version>
    <configuration>
    <!-- put your configurations here -->
    <!--<minimizeJar>true</minimizeJar>-->
    <relocations>
    <relocation>
    <!-- 将冲突的依赖项重命名 -->
    <pattern>com.google.guava</pattern>
    <shadedPattern>com.google.guava.shade.hbase</shadedPattern>
    </relocation>
    <relocation>
    <pattern>com.google.common</pattern>
    <shadedPattern>com.google.common.shade.hbase</shadedPattern>
    </relocation>
    <relocation>
    <pattern>com.google.thirdparty</pattern>
    <shadedPattern>com.shade.google.thirdparty</shadedPattern>
    </relocation>
    <relocation>
    <pattern>com.google.protobuf</pattern>
    <shadedPattern>com.google.protobuf.shade.hbase</shadedPattern>
    </relocation>
    <relocation>
    <pattern>io.netty</pattern>
    <shadedPattern>io.netty.shade.hbase</shadedPattern>
    </relocation>
    </relocations>
    </configuration>
    <executions>
    <execution>
    <phase>package</phase>
    <goals>
    <goal>shade</goal>
    </goals>
    </execution>
    </executions>
    </plugin>

    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    </configuration>
    </plugin>
    </plugins>
    </build>

    </project>

    然后使用maven执行install将包安装到本地仓库,这样就可以直接在有需要的项目中引入,或者是拷贝jar包至依赖路径中,注意打包后会有两个jar,选择shade结尾的jar包。

ClassNotFoundError

典型的错误信息如下:

1
WARN [main] org.apache.hadoop.mapred.YarnChild: Exception running child : java.lang.RuntimeException: java.lang.ClassNotFoundException: Class xxx.xxx.xxx not found

出现类找不到的问题时,可能需要进一步排查,造成此错误的原因相对比较多,可能是jar冲突了导致依赖项缺失版本新添加的类或者旧的类在新版本中被删除,也有可能没有正确设置类路径或者正确打包依赖,除此之外也有个比较冷门的可能性是使用了定制版本的包,导致特殊的类找不到,例如使用HDP的Hadoop包时指定的yarn.client.failover-proxy-providerorg.apache.hadoop.yarn.client.RequestHedgingRMFailoverProxyProvider,而这个类是HDP版本特有的,在开源公共版本中并不存在,因此需要对该配置项进行覆盖。

无法提交至Yarn

这种问题通常是在执行远程提交时发生,仔细检查启动日志即可,主要原因可能为:

  1. 依赖缺失,没有引入yarn相关的依赖导致SPI相关的类获取不到,主要的包有hadoop-yarn-client、hadoop-yarn-common、hadoop-yarn-server-web-proxy等。
  2. 配置项错误,仔细看一下启动时打印的日志信息,检查yarn的地址是否配置正确,org.apache.hadoop.mapred.YarnClientProtocolProvider是否在org.apache.hadoop.mapreduce.Cluster#initialize中被选中。

No job Control

典型的错误信息如下:

1
Exception message: /bin/bash: line 0: fg: no job control

这个错误通常是在跨操作系统远程提交提交时发生,解决方法为设置参数mapreduce.app-submission.cross-platform为true即可,借助IDE查看该参数的使用途径可以发现它主要是用来控制类路径的分隔符。

Bad substitution

典型的错误信息如下:

1
2
bad substitution
broken symlinks(find -L . -maxdepth 5 -type l -ls):

substitution意为替换,正如错误信息所述,此问题原因为错误的替换,出现替换错误的具体发生点是在yarn启动Container使用的脚本,这个错误通常是使用了特定版本的Hadoop,如HDP,并且由统一的控制软件控制配置以及环境变量,那么就可能在脚本中有一些变量替换符,而在用户执行远程提交时没有设置相关的内容导致发生错误,对于HDP来说,通常是遗漏了属性hdp.version,使用System.setProperty或者在Hadoop Configuration中增加设置即可。

MR程序打包方式

对于简单的MR程序来说,打包没有任何问题,但是在实际项目中总是会用到各种各样的工具和依赖,那么就必须把第三方依赖带上,通常来说主要的办法有如下几种:

  1. 手动将依赖拷贝至集群中每一台机器的类路径,或者指定新的类路径,这样可能需要重启集群。
  2. 使用-libjars参数(由org.apache.hadoop.util.GenericOptionsParser提供)或者手动调用Configuration的setClassloader方法。
  3. 配置mapreduce.admin.user.env添加类路径,不过这同样需要每一台机子上都有相关依赖。
  4. 使用Fatjar的形式,将依赖全部打入jar中,这种方式最方便但是增加了网络传输量,jar包会膨胀至几百mb,但是对于内网来说这并不是什么太大的问题。Fatjar也有两种形式,一种是将依赖jar中的class全部解出来放在一起,另外一种则是hadoop提供的标准形式,将依赖的jar放入jar包中的lib目录下,实际运行时hadoop会解包处理。

个人还是比较倾向于使用hadoop提供的标准打包形式,这样看起来比较清晰,也比较方便,不过打成该种形式的jar还是需要小小调试配置一番,以maven打包为例,可以借助maven-assembly插件来帮助我们完成该工作,首先引入assembly插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<build>
<plugins>
<!-- other plugins ... like jar plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<!--<version>3.1.0</version>-->
<configuration>
<descriptors>
<!-- 指定使用的assembly文件 -->
<descriptor>src/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

然后在src目录下新建assemly.xml文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<assembly>
<!-- id,同时也是jar包文件名后缀 -->
<id>cascading</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!-- 默认为添加为.class,指定为false,则以jar包的方式导入 -->
<unpack>false</unpack>
<scope>compile</scope>
<!-- 指定路径,放到lib目录下 -->
<outputDirectory>lib</outputDirectory>
<!-- 使用exclude或include来调整需要的依赖 -->
<excludes>
<exclude>org.apache.hadoop:*</exclude>
<exclude>org.apache.spark:*</exclude>
</excludes>
<!--<includes>
<include>commons-csv:*</include>
</includes>-->
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.outputDirectory}</directory>
<outputDirectory>/</outputDirectory>
<!-- 调整jar包中的其他文件 -->
<excludes>
<exclude>*.xml</exclude>
</excludes>
</fileSet>
</fileSets>
</assembly>

配置完后使用maven打包命令即可打包出标准的包含完整依赖的jar包。

远程提交MR程序

在开发过程中,尤其是在使用IDE进行开发的过程中,对于需要打包上传到服务器再执行hadoop jar命令这一步对我来说着实不想来这么一手,虽然这也是点两下的事情,但是总要点这么两下,有种浑身难受的感觉。尽管有很多测试工具如minicluster等能够帮助你快速测试,但是提交到真实集群进行验证也是常见的情况,能够直接点击IDE中的绿色箭头来直接提交到集群会令人十分舒爽。

这方面的资料并不多,因为毕竟放到服务器上使用hadoop命令才是正统,但是个人觉得能令人舒爽那必然有其价值所在,下面简单介绍一下在Win中使用IDEA进行远程提交的方法。

首先为了防止参数冲突或者意外覆盖,推荐把集群中的配置文件core-site.xml、hdfs-site.xml、mapred-site.xml和yarn-site.xml这四个配置文件拷贝放入resource目录下(类目录下的这几个文件会被自动加载,加载代码位于job类的静态代码中),同时将其参数中涉及本机回环ip(0.0.0.0)的改成服务器的具体ip或者别名,同时要确保hosts文件中集群各个ip别名要与服务器的配置保持一致(这是在同一个内网中的情况下)。其次如果没有在系统PATH变量中指定hadoop目录(hadoop.home.dir),那么也需要额外使用System.setProperty进行设置(本机至少得有个hadoop且版本兼容)。最后需要添加两个配置项:

  1. mapreduce.app-submission.cross-platform:设为true,可以直接在代码中加,也可以直接写入mapred-site.xml
  2. mapreduce.job.jar:值为打包生成的jar的路径(如果有额外的依赖项,打包还是不可避免的)

这样就基本完成了(但是通常还是会出现各种奇怪的问题),同时也可以充分利用IDEA能够指定运行参数的功能,使得代码可以兼容远程提交和正规的haoop jar提交。另外值得注意的是打包时要排除这几个拷贝过来的xml文件,防止正式提交时发生错误。