hadoop豆知识

一些零散的知识点。

symlink

社区本来计划在2.2.0加入对symlink的支持,但发现各种问题推迟了。
2.2.0的代码:

DistributedFileSystem.java
1
2
3
4
5
6
7
8
9
10
public void createSymlink(final Path target, final Path link,
final boolean createParent) throws AccessControlException,
FileAlreadyExistsException, FileNotFoundException,
ParentNotDirectoryException, UnsupportedFileSystemException,
IOException {
if (!FileSystem.isSymlinksEnabled()) {
throw new UnsupportedOperationException("Symlinks not supported");
}
......
}
FileSystem.java
1
2
3
4
5
6
7
8
public static boolean isSymlinksEnabled() {
if (conf == null) {
Configuration conf = new Configuration();
// 这个参数只是测试的
symlinkEnabled = conf.getBoolean("test.SymlinkEnabledForTesting", false);
}
return symlinkEnabled;
}

相关jira:
https://issues.apache.org/jira/browse/HADOOP-10020
https://issues.apache.org/jira/browse/HADOOP-10019

一直到2.5.2,也还是不支持的。
2.5.2的代码:

FileSystem.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Symlinks are temporarily disabled - see HADOOP-10020 and HADOOP-10052
private static boolean symlinksEnabled = false;
private static Configuration conf = null;
@VisibleForTesting
public static boolean areSymlinksEnabled() {
return symlinksEnabled;
}
@VisibleForTesting
public static void enableSymlinks() {
symlinksEnabled = true;
}

CDH与官方版本的对应关系

CDH的版本有点混乱

CDH版本 官方版本
5.3.x、5.2.x 2.5.0
5.1.x、5.0.x 2.3.0
4.x 2.2.0

3.x版本不是基于hadoop2的,不列了。

log-aggression删除服务

发现一个奇怪的问题,log-aggression相关的服务是在NM里面的,见LogAggregationService类。
但删除过期日志的服务是在history server里面的,见AggregatedLogDeletionService类。
意味着如果不启动history server,则hdfs上的日志不会删除。
这个设计有点奇怪。
https://issues.apache.org/jira/browse/YARN-2985

2.5.2代码导入eclipse

前提:maven、protoc.exe 2.5.0、avro
按BUILDING.txt的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cd hadoop-maven-plugins/
mvn install
cd ..
mvn eclipse:eclipse -DskipTests
# 这时已经可以导入eclipse了,但有些类是proto和avro生成的,现在导入的话hadoop-common项目会有些error
cd hadoop-common-project/hadoop-common/src/test/proto
protoc test_rpc_service.proto --java_out=../java
protoc test.proto --java_out=../java
cd ../avro
# avro的jar可以从官网下载
java -jar avro-tools-1.7.7.jar compile schema avroRecord.avsc ../java
# 这时导入的话,hadoop streaming项目的classpath有些问题,要修改hadoop streaming项目的.classpath文件
# 注释掉包含hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/conf的一行
# 有什么副作用不知道
# 在eclipse里导入existing project,选择root directory即可,eclipse会自动扫描所有项目并导入
# 导入后classpath一般会有问题,找不到M2_REPO路径。不要用eclipse内置的maven,修改Windows->Preferences->Maven->Installations,改成自己的maven路径。
# 之后所有项目都OK了,没有error,但有些warn,关系不大

补充:在JDK8的情况下导入步骤似乎又不太一样。环境:JDK8、macOS 10.12、hadoop-2.6.0-cdh5.6.1。

  • 需要修改根pom.xml中的<javaVersion>1.7</javaVersion>属性为1.8,否则maven-enforcer-plugin插件会报错。
  • 直接mvn eclipse:eclipse -DskipTests似乎有些问题,据说是eclipse插件的bug,需要用mvn org.apache.maven.plugins:maven-eclipse-plugin:2.6:eclipse -DskipTests
  • eclipse插件执行过程中出现了一个非常诡异的missing tools.jar错误,参考这个帖子解决即可。

导入eclipse后还是会报一些奇怪的error,有些error要手动修改build path引入jar包,有些error要手动修改项目编译级别到1.7(参考这个)。也可能是eclipse本身的bug。
不管怎样,最终完成后,所有error都可以消除。

顺便说下hive代码的导入。环境:JDK8、macOS 10.12、hive-1.1.0-cdh5.6.1。

参照这篇文章依次执行以下命令即可:

  • mvn clean install -DskipTests -Phadoop-2
  • mvn eclipse:clean
  • mvn eclipse:eclipse -DdownloadSources -DdownloadJavadocs -Phadoop-2(虽然我觉得这两个-D参数可以不要)

然后导入eclipse,也是会报一些奇怪的error,一般都是build path有问题。我碰到的是报一个scala-compiler-2.10.0.jar有错误,手动将有问题的项目的build path中的这个jar删除,就可以去除error。

如何开启客户端DEBUG日志

设置环境变量即可

1
export HADOOP_ROOT_LOGGER=DEBUG,console

这种方法适合临时debug。如果要长期开启DEBUG日志,可以将这个变量写到etc/hadoop/hadoop-env.sh中。
如果用的log4j.properties文件是hadoop自带的那个,还可以设置JVM属性-Dhadoop.root.logger=DEBUG,console。一样的效果。
因为log4j本身加载配置文件时支持对JVM变量的替换。

消除客户端的warn

客户端执行fs -ls /之类的命令时出现warn,不影响使用,但看着比较烦。
常出现的warn有两种:

1
14/11/10 12:18:18 WARN hdfs.BlockReaderLocal: The short-circuit local reads feature cannot be used because libhadoop cannot be loaded.

客户端完全不需要short-circuit read功能。在hdfs-site.xml注释掉short-circuit相关配置,warn就会消失。

1
14/11/10 12:18:16 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable

hadoop客户端启动时会去java.library.path中尝试加载libhadoop,加载失败的话就会出现warn。具体为什么失败,要开debug日志看下,根据特定问题去找解决办法。

修改etc/hadoop/hadoop-env.sh,修改HADOOP_ROOT_LOGGER为DEBUG,console,再次运行命令观察输出。一个例子:

1
2
3
4
14/11/10 14:53:42 DEBUG util.NativeCodeLoader: Trying to load the custom-built native-hadoop library...
14/11/10 14:53:42 DEBUG util.NativeCodeLoader: Failed to load native-hadoop with error: java.lang.UnsatisfiedLinkError: /home/mine/jxy/test/hadoop-2.2.0/lib/native/libhadoop.so.1.0.0: /lib/libc.so.6: version `GLIBC_2.12' not found (required by /home/mine/jxy/test/hadoop-2.2.0/lib/native/libhadoop.so.1.0.0)
14/11/10 14:53:42 DEBUG util.NativeCodeLoader: java.library.path=/home/mine/jxy/test/hadoop-2.2.0/lib/native
14/11/10 14:53:42 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable

这个是由于编译时所用的glibc版本大于当前系统的glibc版本。解决方法:1.在当前系统下重新编译一次libhadoop;2.升级glibc到指定版本(未测试)。

如何查看hive-cli的日志

使用hive命令行时错误一般不会输出到stdout。而是写到文件/tmp/${user.name}/hive.log中。
另外,如果想开启DEBUG日志,可以修改$HIVE_HOME/conf/hive-log4j.properties将hive.root.logger改成DEBUG,console。
如果hive-log4j.properties不存在就将hive-log4j.properties.template重命名下。

相关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--这是HDFS上的目录-->
<property>
<name>hive.exec.scratchdir</name>
<value>/tmp/hive-${user.name}</value>
<description>Scratch space for Hive jobs</description>
</property>
<!--下面两个目录是本地磁盘上的-->
<property>
<name>hive.querylog.location</name>
<value>/tmp/${user.name}</value>
<description>
Location of Hive run time structured log file
</description>
</property>
<property>
<name>hive.exec.local.scratchdir</name>
<value>/tmp/${user.name}</value>
<description>Local scratch space for Hive jobs</description>
</property>

DN/NM挂掉后多长时间会标记为dead?

一个DN挂掉后,要过一段时间才会被判断为dead,在这段时间内NN还是会把这个DN返回给客户端,客户端尝试读取数据时就会报错。
于是想到一个问题,DN/NM挂掉后多长时间才会被认为dead?

对DN而言,有两个参数共同作用:dfs.heartbeat.interval(心跳间隔,默认3秒)、dfs.namenode.heartbeat.recheck-interval(默认5分钟)。
超时时间是2*dfs.namenode.heartbeat.recheck-interval + 10*dfs.heartbeat.interval。也就是说,默认超过10分30秒都没收到心跳,就认为DN挂掉了。
在我们的集群中,心跳是10秒,超时时间就是11分40秒。
这个逻辑见DatanodeManager类。

NM就简单很多了,心跳的超时时间是yarn.nm.liveness-monitor.expiry-interval-ms参数,默认10分钟。
这个逻辑见NMLivelinessMonitor类。
顺便说一句,NM的心跳间隔是yarn.resourcemanager.nodemanagers.heartbeat-interval-ms,默认1秒。不要随便增大NM的心跳,会降低集群的吞吐量,因为只有NM发来心跳时才能分配container。

在google的过程中,发现一个jira:HDFS-3703。社区对这种问题已经有了方案,当一个DN超过30秒没有发送心跳时,会被标记为stale状态,NN在向客户端返回时会将stale的节点放到最后一个,尽量减少出错的概率。但这个特性默认是关闭的。

关于HADOOP_CLASSPATH

仅针对MR而言。
对于MR任务需要关注两个classpath:1.客户端的classpath;2.NM端的classpath。
HADOOP_CLASSPATH变量就是用于设置客户端的classpath的,格式跟java -cp的一样:stackoverflow。注意这里的jar包不会被传到NM节点。
要将相关的jar分发到所有节点,有几种方式

  1. 实现Tool接口。用-libjars参数分发。
  2. 在代码中手动调用DistributedCache的相关方法添加lib。
  3. 导出jar包时生成一个fat jar,将所有额外的lib放到jar包的lib目录里。hadoop jar xxx.jar命令会自动将jar解压,并将lib目录里的文件同时加入客户端和NM端的classpath。

最好保证分发的文件都是777权限,那样可以在不同用户间共享,减小上传文件的消耗。
以上3种方式其实都是同样的原理,实现方式不同而已,具体的逻辑去看NM的localize过程。

如何解决jar包冲突

接上条,如果自己用的jar和hadoop自带的冲突了如何解决?
对于客户端classpath,可以export HADOOP_USER_CLASSPATH_FIRST=true,会优先加载HADOOP_CLASSPATH中指定的jar。
对于NM端classpath,可以

1
2
// 用户自己的libjar优先于hadoop自带的
conf.setBoolean(MRJobConfig.MAPREDUCE_JOB_USER_CLASSPATH_FIRST, true);

P.S.: JVM的类加载原则就是按classpath中出现的顺序加载。包名、类名都相同的话,后面的加载不会生效。

自定义JAVA_HOME

最近打算升级JDK7。
其实JDK的兼容性就一个原则:高版本的jdk可以运行低版本jdk编译的class文件,反过来不行。
如果服务端是JDK7,用hadoop jar执行一个JDK6编译的class,可以直接运行。
如果服务端是JDK6,但要执行一个JDK7编译的jar,可以手动设置JAVA_HOME,前提是服务端有对应的目录:

1
2
3
4
5
6
7
8
9
10
<!--演示用,map用JDK6执行,reduce用JDK7执行,前提服务端必须存在对应的目录-->
<property>
<name>mapreduce.map.env</name>
<value>JAVA_HOME=/usr/lib/jvm/java-6-sun</value>
</property>
<property>
<name>mapreduce.reduce.env</name>
<value>JAVA_HOME=/home/hadoop/jdk1.7.0_75</value>
</property>
<!--还有一个属性yarn.app.mapreduce.am.env可以设置AM端的环境变量,一样的道理-->

除了JAVA_HOME,还有其他一些属性可以自定义,见yarn-default.xml中的yarn.nodemanager.env-whitelist属性。

xml配置文件中的坑

之前配置的snappy压缩没有生效,后来发现是core-site.xml中io.compression.codecs最后多一个换行。。。
这个属性是逗号分隔的,而SnappyCodec是最后一个,所以没生效。
注意所有xml配置项中都不要有换行,hadoop的Configuration类似乎没有做trim操作。

0.0.0.0

在hadoop的很多配置中,都要配一个网络地址,可能是RPC,可能是web。
我们一般是直接写域名,比如hadoop0.photo.163.org:8020
但最近碰到一些虚拟机有2个IP,客户端的IP和机房的IP不一样。而直接写域名的话,服务绑定到机房IP上,客户端无法访问。
这时就要写0.0.0.0:8020。0.0.0.0可以代表本机的所有IP地址。

2.2.0和2.5.2的container executor

升级过程中突然想到个问题,2.2.0和2.5.2的cotainer executor是否兼容?
实际测试是不兼容的:

1
2
3
4
5
6
7
8
9
10
// 2.2.0
hadoop@hadoop0:~/hadoop-current/bin$ ./container-executor
Usage: container-executor --checksetup
Usage: container-executor --mount-cgroups hierarchy controller=path...
Usage: container-executor user command command-args
// 2.5.2
hadoop@hadoop40:~/hadoop-current/bin$ ./container-executor
Usage: container-executor --checksetup
Usage: container-executor --mount-cgroups hierarchy controller=path...
Usage: container-executor user yarn-user command command-args

注意参数个数的变化。如果升级时不替换container executor,NM执行任务会报错:

1
2
3
4
5
6
7
8
2015-07-09 18:36:37,930 ERROR org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor: DeleteAsUser for /home/hadoop/disk/3/local/usercache/hadoop/appcache/application_1436437428207_0001 returned with exit code: 1
ExitCodeException exitCode=1: Too few arguments (5 vs 8) for initialize container
at org.apache.hadoop.util.Shell.runCommand(Shell.java:538)
at org.apache.hadoop.util.Shell.run(Shell.java:455)
at org.apache.hadoop.util.Shell$ShellCommandExecutor.execute(Shell.java:702)
at org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor.deleteAsUser(LinuxContainerExecutor.java:381)
at org.apache.hadoop.yarn.server.nodemanager.DeletionService$FileDeletionTask.run(DeletionService.java:263)

替换container executor后就正常了。NM进程不需要重启。
其实不光是container executor,升级过程中所有的native lib都应该替换。

jdk7编译的tar包是否可用于jdk6

升级2.5.2的过程中,我们也将服务端的jdk升级到7。在编译、打包的过程中,也是用的jdk7。
那这样编译出的tar包,是否可以用于jdk6?
答案是可以的,因为hadoop的pom文件中,限定了编译级别是1.6,即使是用jdk7编译:

hadoop-project/pom.xml
1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>

用UltraEdit查看编译出来的class文件,major.minor版本也能证明确实是jdk6的class文件。