前两天,一同事在测试集群测试自己写的mapreduce程序遇到了错误,让我帮忙定位原因。

一. 报错现象

具体现象是reducetask执行到70-80%的时候就会报以下错误:

java.lang.RuntimeException: Error while running command to get file permissions : java.io.IOException: Cannot run program "/bin/ls": java.io.IOException: error=12, Cannot allocate memory
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:488)
    at org.apache.hadoop.util.Shell.runCommand(Shell.java:200)
    at org.apache.hadoop.util.Shell.run(Shell.java:182)
    at org.apache.hadoop.util.Shell$ShellCommandExecutor.execute(Shell.java:375)
    at org.apache.hadoop.util.Shell.execCommand(Shell.java:461)
    at org.apache.hadoop.util.Shell.execCommand(Shell.java:444)
    at org.apache.hadoop.fs.FileUtil.execCommand(FileUtil.java:712)
    at org.apache.hadoop.fs.RawLocalFileSystem$RawLocalFileStatus.loadPermissionInfo(RawLocalFileSystem.java:448)
二. 排查步骤

从错误日志上来看,是reducetask执行”/bin/ls”命令时内存不足导致任务执行失败。初步断定原因可能有以下两点:
1> reducetask进程内存分配不够,导致报错
2> ls 文件数量过大,导致本来足够的内存变的不够,出现报错
经过检查并没有发现有内存开销大的代码块,所以可以排除第一点可能性。接着把目标放在第二点上。
在mapreduce计算模型中涉及到本地文件操作的主要有maptask计算结果落地和reducetask结果输出,从执行进度情况来看,可以判定reduce阶段的copy,sort两步都没有问题,问题应该出现在最后一步计算上,reduce的计算逻辑也没有问题,问题只能出在最后一步context.write()上了。
继续定位,发现MR驱动配置中使用的是我们内部扩展的XXXXOutputFormat类,这个XXXXOutputFormat类支持自定义输出文件名称以及输出文件大小,取代默认的文件名part-r-0000和不可控制的文件大小。该同学在使用这个类自定义输出文件名时,想把所有reduce阶段的key对应的values都输出到单独的文件中,于是在自定义文件名时增加了key字符后缀。
其实,想法是好的,这样也确实可以达到目的,但是却忘记了每个reducetask中涉及到的key可能有几十上百万个,也就是说一个reducetask可能会输出几十上百万个单独文件,这样的话,reducetask内部调用shell /bin/ls命令出现内存不足的现象就可以解释了。调整代码后,MR执行正常。

三. 问题思考

分布式计算模型忌讳的不是数据量有多大或者计算有多复杂,而是数据倾斜。这也是MR默认使用Hash取模的方式进行分区的原因,HashPartitioner不见得是最好的分区方式,但是却是最保险最安全的分区方式,因为它至少可以保证所有的map输出数据可以平均的分配进行下一步计算。
这次计算错误其实也算是一种变相的数据倾斜,因为单reduce最终输出文件过多,导致单计算单元计算压力过大,出现了错误,是一个很好的教训。