Hadoop任务无报错却无输出与Sqoop卡在map 100%的排查与解决
1. 两个看似无关的故障其实指向同一类问题如果你在运维或开发Hadoop集群的过程中待过一段时间大概率会遇到这样两种让人抓狂的场景第一种MapReduce程序提交上去日志干干净净没有任何报错任务状态也显示成功但你翻遍输出目录就是找不到结果文件第二种用Sqoop把HDFS上的数据往MySQL里导任务跑到map 100%、reduce 0%就卡住不动了等半小时还是那个状态既不报错也不结束。这两个问题表面上一个是没输出一个是卡住了看起来八竿子打不着。但我在实际排查中发现它们背后往往指向同一类根因任务实际上并没有真正完成它该做的事只是框架层面没有抛出异常而已。换句话说Hadoop的容错机制在某些场景下会把失败伪装成成功或者把等待伪装成运行中。这篇文章我会把这两个问题的排查链路完整拆开从现象到根因从定位手段到修复方案一步步讲清楚。不管你是刚接触Hadoop的新手还是已经跑过几十个集群的老手这些排查思路都能直接拿去用。文章里涉及的所有操作步骤和参数配置都是我在实际环境中反复验证过的不是从文档里抄来的理论。先给一个整体判断这两个问题的高频原因集中在输出路径已存在导致任务跳过写入、Sqoop的NULL值处理与MySQL表结构冲突、资源调度器把任务挂起但状态未更新、临时目录权限异常导致commit阶段静默失败这几个方向上。下面逐个展开。2. MapReduce无报错却无输出先搞清楚成功到底意味着什么2.1 任务状态为SUCCEEDED不等于数据已写入很多人看到YARN的Web UI上任务状态显示FINISHED或者命令行返回completed successfully就默认数据一定写进去了。实际上MapReduce的任务生命周期里任务成功和数据落盘是两个不同阶段的事情。一个完整的MapReduce作业包含以下几个关键阶段Map阶段读取输入分片并产生中间结果Shuffle阶段把中间结果按key分发到对应的ReduceReduce阶段执行聚合逻辑并写入输出目录最后是OutputCommitter的commitJob阶段——这个阶段才是真正把临时文件转正的步骤。问题就出在最后这一步。如果你的作业使用了FileOutputCommitter默认就是它Reduce任务会先把结果写到_temporary目录下等所有Reduce都完成后再由commitJob统一移动到最终输出目录。如果这个移动过程出了问题——比如权限不够、目标路径已存在、NameNode在关键时刻进入了安全模式——commitJob可能静默失败而任务状态在某些Hadoop版本中仍然会显示成功。注意Hadoop 2.x和3.x在OutputCommitter的默认实现上有差异。2.x默认使用FileOutputCommitter的v1算法3.x改成了v2算法两者在任务失败重试时的行为不同。如果你在跨版本迁移时遇到这个问题版本差异是一个必须排查的方向。2.2 输出目录已存在最常见的静默跳过这是新手最容易踩的坑也是我见过频率最高的原因。MapReduce有一个硬性规定输出目录必须不存在。如果你的job.setOutputPath()指向的目录已经存在Hadoop会直接抛出一个FileAlreadyExistsException。但问题在于这个异常在某些提交方式下比如通过某些调度平台提交或者使用了特定的OutputFormat可能被吞掉导致你看到的是任务成功但目录里空空如也。排查方法很简单在提交任务之前先检查输出路径hdfs dfs -ls /your/output/path如果目录存在删掉再跑hdfs dfs -rm -r /your/output/path但这里有个经验之谈不要用-f参数跳过确认。我有一次在脚本里写了hdfs dfs -rm -r -f /output结果因为变量拼接错误把上一个任务的输出目录删了导致下游依赖全部失败。后来我改成在脚本里加一个判断if hdfs dfs -test -e /your/output/path; then echo Output path exists, removing... hdfs dfs -rm -r /your/output/path fi这样至少能保证删除操作是有条件的、可追溯的。2.3 临时目录权限问题导致commit阶段失败这个原因比输出目录已存在更隐蔽。前面说了Reduce任务先把数据写到_temporary目录这个目录通常在输出路径下面。如果提交任务的用户对输出路径的父目录没有写权限或者_temporary目录被其他用户创建过且权限不对commit阶段就会失败。排查步骤先看输出路径的父目录权限hdfs dfs -ls -d /parent/of/output再看_temporary目录是否存在以及权限hdfs dfs -ls /your/output/path/_temporary检查提交任务的用户是谁whoami在YARN上任务是以提交者的身份运行的。如果你用A用户提交任务但输出目录的owner是B用户且权限是755A用户就没有写权限。这种情况下Map和Reduce阶段可能因为不需要写最终目录而正常完成但commit阶段需要写_temporary和最终目录就会失败。修复方式有两种要么用有权限的用户提交要么提前把输出目录的权限改成可写hdfs dfs -chmod -R 777 /your/output/path但生产环境不建议用777更稳妥的做法是确保提交用户对输出路径有明确的写权限或者由管理员预先创建好目录并设置正确的owner和group。2.4 通过JobHistory和YARN日志定位真实失败点当你不确定问题出在哪个阶段时最有效的手段是查JobHistory日志。很多人只看控制台输出但控制台输出往往只显示最终状态不显示内部阶段的细节。第一步找到JobHistory的Web UI地址通常在mapred-site.xml里配置property namemapreduce.jobhistory.webapp.address/name valueyour-history-server:19888/value /property第二步在JobHistory里找到对应的作业查看Counters。重点关注这几个计数器计数器名称含义异常表现REDUCE_OUTPUT_RECORDSReduce输出的记录数为0说明Reduce没产生任何数据HDFS_BYTES_WRITTEN写入HDFS的字节数为0说明commit阶段没写入COMMITTED_HEAP_BYTES提交的堆内存异常大可能说明内存泄漏FAILED_SHUFFLEShuffle失败次数大于0说明网络或磁盘有问题如果REDUCE_OUTPUT_RECORDS是0那问题不在commit阶段而在你的业务逻辑——Reduce函数可能因为某种条件判断把所有记录都过滤掉了。这种情况我遇到过好几次尤其是当输入数据的格式和预期不一致时Reduce里的解析逻辑可能静默返回null导致所有记录被丢弃。第三步查看具体的Task日志。在YARN的Web UI上点击具体的Task Attempt可以看到syslog和stderr。重点看stderr里有没有被吞掉的异常堆栈。提示有些Hadoop发行版会把异常堆栈写到userlogs目录下的stderr文件里而不是直接显示在控制台。如果你在控制台看不到任何异常一定要去userlogs目录下翻一翻。3. Sqoop卡在map 100% reduce 0%不是卡住是在等一个永远不会来的信号3.1 Sqoop的Map-Only架构与reduce 0%的含义首先要澄清一个概念Sqoop的导入导出任务默认是Map-Only的也就是说它根本不启动Reduce任务。你在控制台看到的map 100% reduce 0%那个reduce 0%是正常的因为Sqoop压根不需要Reduce。那为什么任务会卡住不结束因为Sqoop的Map任务在完成数据写入后还需要执行一个cleanup阶段这个阶段负责提交事务、关闭数据库连接、更新元数据。如果这个阶段被阻塞了任务就会一直显示map 100%但永远不变成FINISHED。常见的阻塞原因有三个数据库连接池耗尽、MySQL表被锁、Sqoop的临时表没有正确清理。3.2 MySQL端锁等待最隐蔽的卡死原因Sqoop导出数据到MySQL时默认会使用JDBC的批量插入。如果目标表上有其他事务持有锁或者Sqoop自己的多个Map任务在竞争同一把锁就会出现锁等待。排查方法登录MySQL执行SHOW PROCESSLIST;看看有没有状态为Waiting for table metadata lock或Locked的会话。如果有找到对应的Sqoop连接看看它在等什么锁。更深入一点可以查InnoDB的锁信息SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM information_schema.INNODB_LOCKS; SELECT * FROM information_schema.INNODB_LOCK_WAITS;如果发现Sqoop的会话在等锁而持锁的会话是另一个Sqoop任务或者一个长时间未提交的事务那问题就明确了。解决方案在Sqoop命令里增加--batch参数启用JDBC批量插入减少锁竞争。调整MySQL的innodb_lock_wait_timeout参数让锁等待超时后自动失败而不是无限等待SET GLOBAL innodb_lock_wait_timeout 30;如果目标表是MyISAM引擎考虑改成InnoDB因为MyISAM只支持表级锁并发写入时更容易卡死。3.3 NULL值处理不当导致的静默阻塞Sqoop在导出时如果HDFS上的数据包含NULL值而MySQL表的对应列设置了NOT NULL约束且没有默认值插入就会失败。但Sqoop的默认行为是遇到错误后重试而不是立即失败。如果重试次数很多任务就会看起来像卡住了。排查方法在Sqoop命令里加上--verbose参数看看有没有反复出现的错误日志。或者直接查YARN的Task日志搜索SQLException或Data truncation。解决方案在Sqoop命令里指定NULL值的替换字符串--null-string \\N --null-non-string \\N如果MySQL表不允许NULL可以在Sqoop命令里用--columns指定列并用SQL表达式处理NULL--query SELECT IFNULL(col1, ), IFNULL(col2, 0) FROM source_table WHERE \$CONDITIONS更彻底的做法是提前清洗HDFS上的数据把NULL值替换成业务上合理的默认值。3.4 Sqoop临时目录与MySQL临时表残留Sqoop在导出时会先在MySQL里创建一个临时表把数据批量插入临时表然后再合并到目标表。如果上一次任务异常终止临时表可能没有被清理下一次任务创建同名临时表时就会冲突。排查方法SHOW TABLES LIKE %sqoop%;看看有没有残留的临时表。如果有手动删掉DROP TABLE IF EXISTS your_table_sqoop_tmp;另外Sqoop在HDFS上也会创建临时目录通常在/tmp/sqoop-username/compile/下面。如果这个目录权限不对或者磁盘满了也会导致任务卡住。hdfs dfs -ls /tmp/sqoop-$(whoami)/compile/ hdfs dfs -du -h /tmp/sqoop-$(whoami)/如果发现临时目录占用了大量空间可以清理掉旧的任务目录。但要注意正在运行的任务目录不能删。3.5 通过Sqoop的verbose日志和YARN日志定位卡点Sqoop的日志级别默认是INFO很多细节看不到。可以在命令里加上--verbose或者在sqoop-env.sh里调整日志配置。更有效的方法是直接看YARN上Map任务的日志。找到对应的Application ID然后yarn logs -applicationId application_xxxxx_yyyyy在日志里搜索cleanup、commit、close这些关键词看看任务卡在哪个阶段。如果日志最后一行是Starting cleanup但没有后续那说明cleanup阶段被阻塞了。注意Sqoop的日志量可能非常大尤其是当Map任务很多时。建议先用grep过滤关键信息不要直接翻整个日志文件。4. 两个问题的交叉点Hadoop的静默失败机制4.1 为什么Hadoop倾向于不报错Hadoop的设计哲学是容错优先。在分布式环境中节点故障是常态所以Hadoop框架会尽量重试而不是立即失败。这个设计在大多数场景下是合理的但在某些边界条件下会导致问题被掩盖。比如MapReduce的OutputCommitter在commit失败时会重试如果重试次数用完了还没成功理论上应该抛异常。但在某些Hadoop版本中如果异常发生在JobHistory更新之后任务状态可能已经被标记为SUCCEEDED后续的异常就不会再改变状态了。Sqoop也是类似的逻辑。它的Map任务在写入失败后会重试如果重试间隔设置得比较长任务就会看起来像卡住了。4.2 用主动探测代替被动等待既然框架不一定会报错我们就需要主动探测任务是否真的在推进。我常用的几个手段看Counters的变化在YARN Web UI上刷新页面看HDFS_BYTES_WRITTEN或REDUCE_OUTPUT_RECORDS有没有增长。如果5分钟没变化基本可以判断卡住了。看HDFS上的临时文件如果Reduce任务在写_temporary目录用hdfs dfs -ls看文件大小有没有变化。看MySQL的ProcessList对于Sqoop任务定期查SHOW PROCESSLIST看Sqoop的会话状态是Query还是Waiting。设置合理的超时在MapReduce里可以设置mapreduce.task.timeout默认是600000毫秒10分钟。如果任务超过这个时间没有更新状态TaskTracker会认为任务失败并重启它。对于Sqoop可以设置--connect-timeout和--socket-timeout参数。4.3 一个真实的排查案例我之前遇到过一个Sqoop任务卡在map 100%的情况查了MySQL的ProcessList发现Sqoop的会话在等一个metadata lock。进一步查INNODB_TRX发现有一个长时间未提交的事务持有锁。那个事务是一个同事在测试环境里手动执行了一条UPDATE语句但忘了提交。杀掉那个事务后Sqoop任务立刻完成了。这个案例说明卡死的根因可能不在Sqoop本身而在它依赖的外部系统。排查时一定要把视野放宽不要只盯着Hadoop和Sqoop的日志。5. 预防措施与日常运维建议5.1 提交任务前的检查清单为了避免这两个问题反复出现我整理了一个提交前的检查清单输出目录是否存在如果存在是否有明确的清理策略提交用户对输出路径是否有写权限HDFS的磁盘空间是否充足hdfs dfsadmin -report可以看。MySQL的目标表是否有锁SHOW PROCESSLIST可以看。Sqoop的临时目录是否有残留/tmp/sqoop-username/可以看。YARN的资源队列是否有足够的资源yarn application -list可以看。5.2 日志收集与监控建议在集群上配置集中的日志收集比如用ELK或者类似的方案。这样当任务失败时不用登录到每台机器上翻日志直接在一个地方搜索就行。对于Sqoop任务建议在脚本里加上日志重定向sqoop export ... /var/log/sqoop/export_$(date %Y%m%d_%H%M%S).log 21这样即使任务卡住了你也能从日志里看到最后执行到哪一步。5.3 参数调优的几点经验mapreduce.task.timeout默认10分钟如果任务确实需要长时间运行可以适当调大但不建议超过30分钟。mapreduce.outputcommitter.class如果遇到commit阶段的问题可以尝试换成org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter的v2实现Hadoop 3.x默认。Sqoop的--batch启用批量插入减少数据库交互次数。Sqoop的--fetch-size控制每次从HDFS读取的记录数默认是1000可以根据内存情况调整。MySQL的innodb_lock_wait_timeout建议设置为30-60秒避免无限等待。5.4 一个容易忽略的细节时区问题这个问题比较偏门但我确实遇到过。Sqoop在导出时如果HDFS上的时间字段和MySQL的时区不一致可能会导致数据写入失败。排查时可以在Sqoop命令里加上--connect参数指定时区--connect jdbc:mysql://host:3306/db?useSSLfalseserverTimezoneUTC或者在MySQL端设置time_zone参数。这个问题不会导致任务卡住但会导致数据不一致属于静默错误的范畴。6. 几个我踩过的坑和对应的解法6.1 输出目录被其他任务占用有一次两个MapReduce任务配置了相同的输出目录第一个任务还在跑第二个任务就提交了。第二个任务检测到目录存在直接失败了但控制台只显示了一个模糊的错误信息。后来我在提交脚本里加了一个检查确保同一个输出目录不会被两个任务同时使用。6.2 Sqoop的--direct模式与MySQL版本不兼容Sqoop的--direct模式使用MySQL的mysqldump和mysqlimport工具速度比JDBC快很多。但如果MySQL版本和Sqoop自带的工具版本不匹配--direct模式可能会卡住。我遇到过一次Sqoop卡在map 100%不动去掉--direct参数后立刻正常了。所以如果你用了--direct不妨先去掉试试。6.3 HDFS的配额限制导致写入失败HDFS支持目录配额如果输出目录所在的目录设置了空间配额而配额已经用完写入就会失败。但这个失败在MapReduce里可能表现为commit阶段静默失败。排查方法hdfs dfs -count -q /your/output/path看QUOTA和REM_QUOTA两列。如果REM_QUOTA是0说明配额用完了。6.4 YARN的队列资源不足导致任务挂起如果YARN的队列资源不足任务会处于ACCEPTED状态而不是RUNNING。但有些调度器比如Capacity Scheduler在资源不足时会把任务挂起而Web UI上可能仍然显示RUNNING。排查方法yarn application -list -appStates ALL看任务的实际状态。如果状态是ACCEPTED说明在等资源。6.5 NameNode进入安全模式NameNode在启动或维护时会进入安全模式此时HDFS只读不写。如果MapReduce任务在commit阶段遇到安全模式写入会失败。排查方法hdfs dfsadmin -safemode get如果返回Safe mode is ON需要等NameNode退出安全模式或者手动触发hdfs dfsadmin -safemode leave但手动离开安全模式要谨慎确保所有DataNode都已经正常上报了块信息。7. 写在最后一些个人体会这两个问题我前前后后遇到过十几次每次的根因都不完全一样。最开始我只会看控制台输出看到成功就以为真的成功了结果被坑了好几次。后来我养成了一个习惯任务完成后一定要去输出目录确认文件真的存在而且大小合理。对于Sqoop任务一定要去MySQL里SELECT COUNT(*)确认数据真的写进去了。另外一个体会是Hadoop生态的组件很多日志分散在不同的地方。YARN的日志、HDFS的日志、MySQL的日志、Sqoop自己的日志任何一个地方出问题都可能导致任务异常。所以排查时要有全局视野不要只盯着一个组件看。最后分享一个我常用的排查命令组合基本上能覆盖80%的场景# 查看YARN任务状态 yarn application -list -appStates ALL | grep your_app # 查看任务日志 yarn logs -applicationId your_app_id | grep -i error\|exception\|fail # 查看HDFS输出目录 hdfs dfs -ls -R /your/output/path # 查看MySQL连接和锁 mysql -e SHOW PROCESSLIST; SELECT * FROM information_schema.INNODB_TRX;这套组合拳打下来大部分问题都能定位到具体原因。剩下的就是根据根因去修复了。希望这些经验能帮你少走一些弯路。