最近几天发现oozie调度的任务经常会被挂起(SUSPENDED), 之前也存在被挂起的情况,但频率很低, 一周也就一两次, 出现问题时由监控脚本重跑,也不影响正常业务,但最近一两天被挂起的非常频繁,甚至一天有3,4个小时的任务被挂起, 影响正常业务.
      个人猜测跟hadoop集群状态(稳定性)有一定关系,但咨询hadoop运维人员后得知集群近几天并未做改动,也没异常。
    被挂起任务截图:

 
   
   注: loadFlash是一个hive节点,是把flash日志load到hive中, 在这里出现异常,状态变成
    START_MANUAL, 点开loadFlash结点, 如下图:

 
     
     从Error Code和Error Message可以看出,此action出现JA009 Filesystem closed异常
为了定位该问题,先来看看oozie日志吧,来到oozie安装目录, 找到oozie.log日志文件, 搜索 JA009: Filesystem closed 信息,果然,有很多该异常信息

  1. org.apache.oozie.action.ActionExecutorException: JA009: Filesystem closed  
  2.     at org.apache.oozie.action.ActionExecutor.convertException(ActionExecutor.java:361)  
  3.     at org.apache.oozie.action.hadoop.JavaActionExecutor.prepareActionDir(JavaActionExecutor.java:390)  
  4.     at org.apache.oozie.action.hadoop.JavaActionExecutor.start(JavaActionExecutor.java:636)  
  5.     at org.apache.oozie.command.wf.ActionStartCommand.call(ActionStartCommand.java:128)  
  6.     at org.apache.oozie.command.wf.ActionStartCommand.execute(ActionStartCommand.java:249)  
  7.     at org.apache.oozie.command.wf.ActionStartCommand.execute(ActionStartCommand.java:47)  
  8.     at org.apache.oozie.command.Command.call(Command.java:202)  
  9.     at org.apache.oozie.service.CallableQueueService$CompositeCallable.call(CallableQueueService.java:211)  
  10.     at org.apache.oozie.service.CallableQueueService$CallableWrapper.run(CallableQueueService.java:128)  
  11.     at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)  
  12.     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)  
  13.     at java.lang.Thread.run(Thread.java:662)  
  14. Caused by: java.io.IOException: Filesystem closed  
  15.     at org.apache.hadoop.hdfs.DFSClient.checkOpen(DFSClient.java:232)  
  16.     at org.apache.hadoop.hdfs.DFSClient.delete(DFSClient.java:648)  
  17.     at org.apache.hadoop.hdfs.DistributedFileSystem.delete(DistributedFileSystem.java:255)  
  18.     at org.apache.oozie.action.hadoop.JavaActionExecutor.prepareActionDir(JavaActionExecutor.java:383)  
  19.     ... 10 more 

      有了该异常信息, 定位问题方便多了, 将ooize源码工程导入eclipse, 找到 JavaActionExecutor.java文件,定位到 prepareActionDir方法:

  1. void prepareActionDir(FileSystem actionFs, Context context) throws ActionExecutorException {  
  2.         try {  
  3.             Path actionDir = context.getActionDir();  
  4.             Path tempActionDir = new Path(actionDir.getParent(), actionDir.getName() + ".tmp");  
  5.             if (!actionFs.exists(actionDir)) {  
  6.                 try {  
  7.                     actionFs.copyFromLocalFile(new Path(getOozieRuntimeDir(), getLauncherJarName()), new Path(  
  8.                             tempActionDir, getLauncherJarName()));  
  9.                     actionFs.rename(tempActionDir, actionDir);  
  10.                 }  
  11.                 catch (IOException ex) {  
  12.                     actionFs.delete(tempActionDir, true);  
  13.                     actionFs.delete(actionDir, true);  
  14.                     throw ex;  
  15.                 }  
  16.             }  
  17.         }  
  18.         catch (Exception ex) {  
  19.             throw convertException(ex);  
  20.         }  
  21.     } 

     从prepareActionDir 方法可以看出, 在使用actionFs的时候有可能会出现Filesystem closed异常(如果拿到的这个actionFs已经关闭自然就会抛出异常了)
接下来看看prepareActionDir 方法中FileSystem actionFs参数是如何传入的,找到调用prepareActionDir 的方法:

  1. @Override  
  2.    public void start(Context context, WorkflowAction action) throws ActionExecutorException {  
  3.        try {  
  4.            XLog.getLog(getClass()).debug("Starting action " + action.getId() + " getting Action File System");  
  5.            FileSystem actionFs = getActionFileSystem(context, action);  
  6.            XLog.getLog(getClass()).debug("Preparing action Dir through copying " + context.getActionDir());  
  7.            prepareActionDir(actionFs, context);  
  8.            XLog.getLog(getClass()).debug("Action Dir is ready. Submitting the action ");  
  9.            submitLauncher(context, action);  
  10.            XLog.getLog(getClass()).debug("Action submit completed. Performing check ");  
  11.            check(context, action);  
  12.            XLog.getLog(getClass()).debug("Action check is done after submission");  
  13.        }  
  14.        catch (Exception ex) {  
  15.            throw convertException(ex);  
  16.        }  
  17.    }  

     通过FileSystem actionFs = getActionFileSystem(context, action);代码可知,actionFs 是通过getActionFileSystem方法获取的, 再来看getActionFileSystem方法:

  1. protected FileSystem getActionFileSystem(Context context, Element actionXml) throws ActionExecutorException {  
  2.         try {  
  3.             return context.getAppFileSystem();  
  4.         }  
  5.         catch (Exception ex) {  
  6.             throw convertException(ex);  
  7.         }  
  8.     } 
  1. public FileSystem getAppFileSystem() throws HadoopAccessorException, IOException, URISyntaxException {  
  2.            WorkflowJob workflow = getWorkflow();  
  3.            XConfiguration jobConf = new XConfiguration(new StringReader(workflow.getConf()));  
  4.            Configuration fsConf = new Configuration();  
  5.            XConfiguration.copy(jobConf, fsConf);  
  6.            return Services.get().get(HadoopAccessorService.class).createFileSystem(workflow.getUser(),  
  7.                    workflow.getGroup(), new URI(getWorkflow().getAppPath()), fsConf);  
  8.  
  9.        } 

     至此终于找到actionFs是通过HadoopAccessorService来获取的,看看HadoopAccessorService的createFileSystem方法:

  1. public FileSystem createFileSystem(String user, String group, URI uri, Configuration conf)  
  2.            throws HadoopAccessorException {  
  3.        validateNameNode(uri.getAuthority());  
  4.        conf = createConfiguration(user, group, conf);  
  5.        try {  
  6.            return FileSystem.get(uri, conf);  
  7.        }  
  8.        catch (IOException e) {  
  9.            throw new HadoopAccessorException(ErrorCode.E0902, e);  
  10.        }  
  11.    } 

     真相大白,oozie通过调用hadoop的FileSystem.get(uri, conf);  方法来得到FileSystem.
接着看FileSystem.get(uri, conf)源码:

  1. public static FileSystem get(URI uri, Configuration conf) throws IOException {  
  2.     String scheme = uri.getScheme();  
  3.     String authority = uri.getAuthority();  
  4.  
  5.     if (scheme == null) {                       // no scheme: use default FS  
  6.       return get(conf);  
  7.     }  
  8.  
  9.     if (authority == null) {                       // no authority  
  10.       URI defaultUri = getDefaultUri(conf);  
  11.       if (scheme.equals(defaultUri.getScheme())    // if scheme matches default  
  12.           && defaultUri.getAuthority() != null) {  // & default has authority  
  13.         return get(defaultUri, conf);              // return default  
  14.       }  
  15.     }  
  16.       
  17.     String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);  
  18.     if (conf.getBoolean(disableCacheName, false)) {  
  19.       return createFileSystem(uri, conf);  
  20.     }  
  21.  
  22.     return CACHE.get(uri, conf);  // 有缓存哦
  23.   } 

      FileSystem.get(uri, conf)方法会根据conf.getBoolean(disableCacheName, false)的值决定是创建FileSystem还是从cache中获取FileSystem, 而默认情况下conf.getBoolean(disableCacheName, false)值为flase(除非特别指定disableCacheName 值为true), 即从cache获取. 问题正是出在这里,我们的oozie作业是小时任务,并由多个action节点组成,每个action节点执行时从cache中获取FileSystem, 有可能该FileSystem因为网络原因或者其他原因已经被closed, 但仍旧被cache, 导致拿到该FileSystem的action在使用时发生IOException异常.
     
定位到问题原因后就需要设法改进,方法也很简单,只要使conf.getBoolean(disableCacheName, false)   为true即可,这样每次都会重新创建一个FileSystem, 也就不会从cache中拿到失效的FileSystem了.
     在oozie的workflow里进行如下配置:

  1. <configuration>      
  2. <name>oozie.launcher.fs.hdfs.impl.disable.cache</name> 
  3.       <value>true</value> 
  4.     </property> 
  5.   </configuration> 

   另外从源代码中发现oozie对action节点调度过程中的瞬态错误会有重试机制,默认状态下是3次,我在提交作业时修改成10次

  1. oozie.wf.action.max.retries=10 

经过上述修改后, oozie调度健壮性得到了提升^_^