Hadoop异常Task发现分析

前言

Hadoop作为一个大型的分布式系统,当他的规模不断的扩大,扩增到一定程度的时候,所使用的业务方自热而然的也会变多,不同的业务方会提交各种各样类型的任务,有人提交hive的查询任务,有人会写MapReduce解析程序的job.于是这就慢慢产生了一个叫"多租户"的概念.多租户最简单直接的理解就是一个大的公共自行车场,被一波人共同使用,自行车被人借光了,你就不能使用了,你就得等.但是,当这个用户越来越多的时候,一个很棘手的问题就会发生,某些不良"用户"独占大部分资源,导致其他的用户根本无法正常使用工作.今天本文所讨论的问题就是这个主题.对于此类问题,一般有2种解决方案,一个是分析管理,人工分析,然后手动操作管理,手动更改配置限定一下个别用户的资源使用上限,第二种则是用户的资源隔离,每位用户都固定分配好多少多少资源,只能用这么多.论难度而言,后者比前者更有技术难度,因为要改核心代码,今天就分析前面1种,就是分析找出这些"大户".


Hadoop现有监控的不足

把上面这个问题对应到Hadoop系统中,就是找出哪个user用的container,memory,cpu-vcores最多.单纯从Hadoop自身提供的一些工具来看,这些其实对于我们的帮助不大,不管说是ResourceManager的后台页还是JobHistory的历史job信息展示页来看,这些信息详细程序还是有的,就是太散,缺乏一个汇总这些数据信息的地方.例如1个finished job,我想要知道他是不是异常的job,那么我当然得需要知道里面的task异常的多不多,于是我就得在页面上继续往里点.这个数量小一点尚可接受,但是对于集群规模1天可达数万个job的集群时,根本难以想象.所以我们的初步目标就是2点,1个是汇总一些数据.2分析汇总后的数据,并做一些处理并展示到页面上,达到最直观的效果.


JobHistory的Task分析

在Hadoop层面,要想做到细粒度层面的分析,task级别的分析是一个不错的切入点.而Task的数据都是存在与JobHistory的.jhist文件中.于是我们可以在JobHistory的job解析的层面做一些加工.首先要知道JobHistory的页面是怎么生成的,首先他是在hadoop-mapredce-client-hs工程下的webapp包下.如下图所示的位置:


然后下面的Hs打头的类就是负责显示页面数据的逻辑代码.这部分的代码逻辑大致相同,读者可自行研究学习.然后我们找到一个与Job信息显示相关的一个类,HsJobBlock,分析一下里面的代码:

...
/*
   * (non-Javadoc)
   * @see org.apache.hadoop.yarn.webapp.view.HtmlBlock#render(org.apache.hadoop.yarn.webapp.view.HtmlBlock.Block)
   */
  @Override protected void render(Block html) {
    String jid = $(JOB_ID);
    if (jid.isEmpty()) {
      html.
        p()._("Sorry, can't do anything without a JobID.")._();
      return;
    }
    JobId jobID = MRApps.toJobID(jid);
    Job j = appContext.getJob(jobID);
    if (j == null) {
      html.
        p()._("Sorry, ", jid, " not found.")._();
      return;
    }
    List<AMInfo> amInfos = j.getAMInfos();
    JobInfo job = new JobInfo(j);
    ResponseInfo infoBlock = info("Job Overview").
        _("Job Name:", job.getName()).
        _("User Name:", job.getUserName()).
        _("Queue:", job.getQueueName()).
        _("State:", job.getState()).
        _("Uberized:", job.isUber()).
        _("Submitted:", new Date(job.getSubmitTime())).
        _("Started:", new Date(job.getStartTime())).
        _("Finished:", new Date(job.getFinishTime())).
        _("Elapsed:", StringUtils.formatTime(
            Times.elapsed(job.getStartTime(), job.getFinishTime(), false)));
...
大致意思就是获取页面上传入的jobid,然后从appContext对象中获取此id对应的job信息.appContext这个对象是一个父类,他的继承关系图如下:


从图中基本可以看出,他的实现类是JobHistory类对象.job信息从AppContext中获取完毕之后,他是如何获取更多的关于内部的task信息的呢,答案在下面这几行代码中.

...
JobId jobID = MRApps.toJobID(jid);
    Job j = appContext.getJob(jobID);
    if (j == null) {
      html.
        p()._("Sorry, ", jid, " not found.")._();
      return;
    }
    List<AMInfo> amInfos = j.getAMInfos();
    JobInfo job = new JobInfo(j);
    ResponseInfo infoBlock = info("Job Overview").
        _("Job Name:", job.getName()).
...
这一切都来自于中间的一个转换,最终得到JobInfo.JobInfo中保留了大量的关于job的信息变量,下面是其中的一部分.

@XmlRootElement(name = "job")
@XmlAccessorType(XmlAccessType.FIELD)
public class JobInfo {

  protected long submitTime;
  protected long startTime;
  protected long finishTime;
  protected String id;
  protected String name;
  protected String queue;
  protected String user;
  protected String state;
  protected int mapsTotal;
  protected int mapsCompleted;
  protected int reducesTotal;
  protected int reducesCompleted;
...
在构造函数中,会经过1步Task信息的load加载

public JobInfo(Job job) {
    this.id = MRApps.toString(job.getID());
    JobReport report = job.getReport();
    ....
    this.name = job.getName().toString();
    this.queue = job.getQueueName();
    this.user = job.getUserName();
    this.state = job.getState().toString();

    this.acls = new ArrayList<ConfEntryInfo>();
    
    if (job instanceof CompletedJob) {
      avgMapTime = 0l;
      avgReduceTime = 0l;
      avgShuffleTime = 0l;
      avgMergeTime = 0l;
      avgMapGcTime = 0L;
      avgMapElapsedTime = 0L;
      avgReduceElapsedTime = 0L;
      ...
      countTasksAndAttempts(job);
在countTaskAndAttemptes就会能拿到task的信息,然后做一些统计分析,jobInfo默认在这里只做了很浅的统计,只是一些平均运行时间和成功失败次数的统计.

/**
   * Go through a job and update the member variables with counts for
   * information to output in the page.
   *
   * @param job
   *          the job to get counts for.
   */
  private void countTasksAndAttempts(Job job) {
    numReduces = 0;
    numMaps = 0;
    final Map<TaskId, Task> tasks = job.getTasks();
    if (tasks == null) {
      return;
    }
    for (Task task : tasks.values()) {
      // Attempts counts
      Map<TaskAttemptId, TaskAttempt> attempts = task.getAttempts();
      int successful, failed, killed;
      for (TaskAttempt attempt : attempts.values()) {

        successful = 0;
        failed = 0;
        killed = 0;
        if (TaskAttemptStateUI.NEW.correspondsTo(attempt.getState())) {
          // Do Nothing
        } else if (TaskAttemptStateUI.RUNNING.correspondsTo(attempt.getState())) {
          // Do Nothing
        } else if (TaskAttemptStateUI.SUCCESSFUL.correspondsTo(attempt
            .getState())) {
          ++successful;
        } else if (TaskAttemptStateUI.FAILED.correspondsTo(attempt.getState())) {
          ++failed;
        } else if (TaskAttemptStateUI.KILLED.correspondsTo(attempt.getState())) {
          ++killed;
        }
...
前面铺垫了这么久,终于找到了一个切入点,就是这里.


自定义慢任务,异常任务筛选

我以目前我们公司对这方面的改造为1个例子,让大家真实的了解如何做到自定义的task筛选.分为2大主题,1个是slow taks,慢任务.第二个是error task,异常任务,这个主要针对的是task attempt.OK,下面仔细的描述下.

慢任务

在这里所指的慢任务当然不是仅仅指的是执行时间的长短,只要涉及到时间值的,其他的维度也都是OK的,比如我们做了GC时间慢的,还有1个很关键的是启动时间明显偏慢的,尤其是启动时间偏慢的任务,对于我们发现一些异常的问题将会非常有用.而对于这些慢的任务,我们会在页面上设置一个文本输入框,用户可以传入自己想要设置的时间阈值.然后在JobInfo中进行过滤处理.下面给出一个按照执行时间的慢任务筛选:

public Map<TaskId, Task> getElapsedSlowTasks(Job job, double ratio) {
    long tmpElapsedTime;
    double mapElapsedThresholdTime;
    double reduceElapsedThresholdTime;
    Map<TaskId, Task> slowTasks;
    
    slowTasks = new HashMap<TaskId, Task>();

    mapElapsedThresholdTime = avgMapElapsedTime * (1 + ratio);
    reduceElapsedThresholdTime = avgReduceElapsedTime * (1 + ratio);

    final Map<TaskId, Task> tasks = job.getTasks();
    if (tasks == null) {
      return slowTasks;
    }

    for (Task task : tasks.values()) {
      tmpElapsedTime =
          task.getReport().getFinishTime() - task.getReport().getStartTime();

      if (task.getType() == TaskType.MAP
          && tmpElapsedTime >= mapElapsedThresholdTime) {
        slowTasks.put(task.getID(), task);
      } else if (task.getType() == TaskType.REDUCE
          && tmpElapsedTime >= reduceElapsedThresholdTime) {
        slowTasks.put(task.getID(), task);
      }
    }

    return slowTasks;
  }
里面的许多平均值将会在前面的countTaskAndAttemptes()方法中计算好,这个方法会在前前台页面中被调用.

...
StringBuilder jobsTableData = new StringBuilder("[\n");
	    for (Job j : appContext.getAllJobs().values()) {
	    	JobId jobId = j.getID();
	    	Job jb = appContext.getJob(jobId);
	    	JobInfo job = new JobInfo(jb);
	    	
	    	taskMap = null;
	    	slowRatio = Double.parseDouble($(SLOW_TASKS_RATIO));
	    	if($(SLOW_TASKS_TYPE).equals("elapsedtime")){
	    	  taskMap = job.getElapsedSlowTasks(jb, slowRatio);
	    	}else if($(SLOW_TASKS_TYPE).equals("gctime")){
	    	  taskMap = job.getGcSlowTasks(jb, slowRatio);
	    	}else if ($(SLOW_TASKS_TYPE).equals("slowstart")) {
	    	  taskMap = job.getSlowStartTasks(jb, slowRatio);
	    	}else if ($(SLOW_TASKS_TYPE).equals("readdatalean")) {
	    	  taskMap = job.getReadDataLeanTasks(jb, slowRatio);
        }else if ($(SLOW_TASKS_TYPE).equals("writedatalean")) {
          taskMap = job.getWriteDataLeanTasks(jb, slowRatio);
        }
...
然后就是task信息的展示了.


Error TaskAttempt

异常Task尝试的信息的过滤就比较简单一些,我们可以直接在前台页中进行简单判断,过滤掉状态为SUCCEED状态的记录,一般剩下的就会是KILLED和FAILED,里面会包含了note信息,这个对于帮助我们分析问题非常有用.

...
 for(Entry<TaskAttemptId, TaskAttempt> ta: taskAttempts.entrySet()){
	          taskAttempt = ta.getValue();
	          
	          if(taskAttempt.getState() == TaskAttemptState.SUCCEEDED){
	            continue;
	          }
	          
          if (type == TaskType.MAP) {
            mapTime = taskAttempt.getFinishTime() - taskAttempt.getLaunchTime();
            shuffleTime = 0;
            mergeTime = 0;
            reduceTime = 0;
          } else {
            mapTime = 0;
            shuffleTime =
                taskAttempt.getShuffleFinishTime()
                    - taskAttempt.getLaunchTime();
            mergeTime =
                taskAttempt.getSortFinishTime()
                    - taskAttempt.getShuffleFinishTime();
            reduceTime =
                taskAttempt.getFinishTime() - taskAttempt.getSortFinishTime();
          }
	          
	          jobsTableData.append("[\"")
	          .append(jobPrefixInfo)
	          .append(dateFormat.format(new Date(taskAttempt.getLaunchTime()))).append("\",\"")
	          .append(dateFormat.format(new Date(taskAttempt.getFinishTime()))).append("\",\"")
	          .append(taskAttempt.getID()).append("\",\"")
	          .append(type).append("\",\"")
	          .append(taskAttempt.getState()).append("\",\"")
...

改造的JobHistory效果图展示

新增导航栏:


新增SlowTask执行时间等指标:


新增Error TaskAttempt任务尝试:


实现此逻辑的代码链接如下,大家可以仔细阅读这部分的代码,从如何把功能加到导航栏上再到设置链接地址,都在下面的代码链接中:

https://github.com/linyiqun/yarn-jobhistory-crawler/tree/master/slowTask


posted @ 2020-01-12 19:09  回眸,境界  阅读(94)  评论(0编辑  收藏  举报