Hadoop 运行原理(一)Configuration解析

本系列基于 Apache Hadoop 2.7.2,并附 源码

PS:Apache 基金下所有项目源码都可以在 archive.apache.org 这个网页中找到。强烈鄙视那些拿着公开源码放到论坛中提供有偿下载的行为!!!

Hadoop 运行原理概览

在这里插入图片描述

Hadoop 的运行过程可以划分为以下五个流程:

  1. client端创建并提交一个 job;
  2. yarn 的 ResourceManager 进行资源的分配;
  3. 由 NodeManager 进行加载与监控 containers;
  4. 通过 applicationMaster 与 ResourceManager 进行资源的申请及状态的交互,由NodeManagers进行MapReduce运行时job的管理.
  5. 通过hdfs进行job配置文件、jar包的各节点分发。

Client 端干了什么?

以 WordCount 为例,它的源码位于 hadoop-2.7.2-src/hadoop-mapreduce-project/hadoop-mapreduce-examples/src/main/java/org/apache/hadoop/examples/WordCount.java,在其 main 方法中:

public static void main(String[] args) throws Exception {
  Configuration conf = new Configuration();
  String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
  if (otherArgs.length < 2) {
    System.err.println("Usage: wordcount <in> [<in>...] <out>");
    System.exit(2);
  }
  Job job = Job.getInstance(conf, "word count");
  job.setJarByClass(WordCount.class);
  job.setMapperClass(TokenizerMapper.class);
  job.setCombinerClass(IntSumReducer.class);
  job.setReducerClass(IntSumReducer.class);
  job.setOutputKeyClass(Text.class);
  job.setOutputValueClass(IntWritable.class);
  for (int i = 0; i < otherArgs.length - 1; ++i) {
    FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
  }
  FileOutputFormat.setOutputPath(job,
    new Path(otherArgs[otherArgs.length - 1]));
  System.exit(job.waitForCompletion(true) ? 0 : 1);
}

为了获得一个配置信息,这里使用了 new Configuration(),看似只是简单创建了一个对象,实则内藏玄机。

Configuration 简介

Configuration 是作业的配置信息类,任何作用的配置信息必须通过Configuration传递,因为通过Configuration可以实现在多个mapper和多个reducer任务之间共享信息。

进入这个 Configuration 类,可以找到如下代码:

  /** A new configuration. */
  public Configuration() {
    this(true);
  }

  /** A new configuration where the behavior of reading from the default 
   * resources can be turned off.
   * 
   * If the parameter {@code loadDefaults} is false, the new instance
   * will not load resources from the default files. 
   * @param loadDefaults specifies whether to load from the default files
   */
  public Configuration(boolean loadDefaults) {
    this.loadDefaults = loadDefaults;
    updatingResource = new ConcurrentHashMap<String, String[]>();
    synchronized(Configuration.class) {
      REGISTRY.put(this, null);
    }
  }
  
  /** 
   * A new configuration with the same settings cloned from another.
   * 
   * @param other the configuration from which to clone settings.
   */
  @SuppressWarnings("unchecked")
  public Configuration(Configuration other) {
   this.resources = (ArrayList<Resource>) other.resources.clone();
   synchronized(other) {
     if (other.properties != null) {
       this.properties = (Properties)other.properties.clone();
     }

     if (other.overlay!=null) {
       this.overlay = (Properties)other.overlay.clone();
     }

     this.updatingResource = new ConcurrentHashMap<String, String[]>(
         other.updatingResource);
     this.finalParameters = Collections.newSetFromMap(
         new ConcurrentHashMap<String, Boolean>());
     this.finalParameters.addAll(other.finalParameters);
   }
   
    synchronized(Configuration.class) {
      REGISTRY.put(this, null);
    }
    this.classLoader = other.classLoader;
    this.loadDefaults = other.loadDefaults;
    setQuietMode(other.getQuietMode());
  }

Configuration 的构造器

Configuration 有三个构造器:

  1. Configuration()
  2. Configuration(boolean loadDefaults)
  3. Configuration(Configuraiont other)

前两个构造器使用的是重叠构造器模式,也就是默认的无参构造器会生成一个加载了默认配置文件的 Configuration 对象, Configuration(boolean loadDefaults) 中的参数 loadDefaults 就是为了控制构造出来的对象是否需要加载默认的配置文件。当loadDefaults 为 false 时,Configuration 对象就不会将默认的配置加载到内存中。

第三个构造器则是用来复制 other 的配置的。

这里,我不禁产生这样一个疑问:Client 端没有指定要加载什么配置文件,而构造器也没有执行相关方法,那么,配置文件中的信息是怎么被读取并加载到内存中的呢?

在源码第649行有这样一段静态代码块:

static{
    //print deprecation warning if hadoop-site.xml is found in classpath
    ClassLoader cL = Thread.currentThread().getContextClassLoader();
    if (cL == null) {
      cL = Configuration.class.getClassLoader();
    }
    if(cL.getResource("hadoop-site.xml")!=null) {
      LOG.warn("DEPRECATED: hadoop-site.xml found in the classpath. " +
          "Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, "
          + "mapred-site.xml and hdfs-site.xml to override properties of " +
          "core-default.xml, mapred-default.xml and hdfs-default.xml " +
          "respectively");
    }
    addDefaultResource("core-default.xml");
    addDefaultResource("core-site.xml");
  }

回顾一下类的加载顺序:

  1. 静态变量
  2. 静态代码块
  3. 成员变量
  4. 匿名代码块
  5. 构造器
  6. 静态方法

这里给自己提个醒:阅读源码可以先找找静态代码块,搜索“static{”,一搜一个准,否则就是没有静态代码块。同时,有些属性会被预先赋值,也需要留心。

所以,这里先加载的时这段静态代码块。它调用了 addDefaultResource() 来加载资源,不过它只会按顺序加载这两个文件的名字(其实也是路径)。只有调用set* 和 get* 方法的时候,才会将配置文件中真正的属性加载到内存中。这里采用了 延时加载的设计模式,即添加的资源并不会被立即加载,只有被真正调用时才会加载。

我们来看看 addDefaultResource() 方法的源码:

  /**
   * Add a default resource. Resources are loaded in the order of the resources 
   * added.
   * @param name file name. File should be present in the classpath.
   */
  public static synchronized void addDefaultResource(String name) {
    if(!defaultResources.contains(name)) {
      defaultResources.add(name);
      for(Configuration conf : REGISTRY.keySet()) {
        if(conf.loadDefaults) {
          conf.reloadConfiguration();
        }
      }
    }
  }

由于先执行的是静态代码块,所以这时的 REGISTRY 为空,也就不会执行 for 循环里面的代码。

reloadConfiguration() 方法的源码:

  /**
   * Reload configuration from previously added resources.
   *
   * This method will clear all the configuration read from the added 
   * resources, and final parameters. This will make the resources to 
   * be read again before accessing the values. Values that are added
   * via set methods will overlay values read from the resources.
   */
  public synchronized void reloadConfiguration() {
    properties = null;                            // trigger reload
    finalParameters.clear();                      // clear site-limits
  }

reloadConfiguration() 方法将清除从添加的资源和最终参数中读取的所有配置。这将使在访问值之前再次读取资源。通过 set* 方法添加的值将覆盖从资源中读取的值。

set* 和 get*

这里的 * 代表了 20 多个方法,如 getInt()、getLong()、getBoolean(),它们都可以获取 Configuration 对象中相应的信息。这些方法最重要的set()和get()。

本来 Configuration 在 Hadoop 运行的早期不会加载配置文件的属性值,但是当在 Client 端调用 set* 或 get* 时,会在 Job 提交前就将 core-default.xml 和 core-site.xml 的所有属性值加载到内存中。

一般我们会在提交 Job 前设置一些属性,先看看其中的一个 set() 方法(位于1130行):

  /** 
   * Set the <code>value</code> of the <code>name</code> property. If 
   * <code>name</code> is deprecated, it also sets the <code>value</code> to
   * the keys that replace the deprecated key. Name will be trimmed before put
   * into configuration.
   *
   * @param name property name.
   * @param value property value.
   * @param source the place that this configuration value came from 
   * (For debugging).
   * @throws IllegalArgumentException when the value or name is null.
   */
  public void set(String name, String value, String source) {
    Preconditions.checkArgument(
        name != null,
        "Property name must not be null");
    Preconditions.checkArgument(
        value != null,
        "The value of property " + name + " must not be null");
    name = name.trim();
    DeprecationContext deprecations = deprecationContext.get();
    if (deprecations.getDeprecatedKeyMap().isEmpty()) {
      getProps();
    }
    getOverlay().setProperty(name, value);
    getProps().setProperty(name, value);
    String newSource = (source == null ? "programatically" : source);

    if (!isDeprecated(name)) {
      updatingResource.put(name, new String[] {newSource});
      String[] altNames = getAlternativeNames(name);
      if(altNames != null) {
        for(String n: altNames) {
          if(!n.equals(name)) {
            getOverlay().setProperty(n, value);
            getProps().setProperty(n, value);
            updatingResource.put(n, new String[] {newSource});
          }
        }
      }
    }
    else {
      String[] names = handleDeprecation(deprecationContext.get(), name);
      String altSource = "because " + name + " is deprecated";
      for(String n : names) {
        getOverlay().setProperty(n, value);
        getProps().setProperty(n, value);
        updatingResource.put(n, new String[] {altSource});
      }
    }
  }

这里的很多代码都是对 key 和 value 做验证。getProps() 是加载配置文件中属性的关键,后面提到的 get() 方法也会调用该方法加载配置文件中的属性。

getProps()代码如下:

  protected synchronized Properties getProps() {
    if (properties == null) {
      properties = new Properties();
      Map<String, String[]> backup =
          new ConcurrentHashMap<String, String[]>(updatingResource);
      loadResources(properties, resources, quietmode);

      if (overlay != null) {
        properties.putAll(overlay);
        for (Map.Entry<Object,Object> item: overlay.entrySet()) {
          String key = (String)item.getKey();
          String[] source = backup.get(key);
          if(source != null) {
            updatingResource.put(key, source);
          }
        }
      }
    }
    return properties;
  }

loadResources(properties, resources, quietmode) 是主要的方法,quietmode 如果为 true,就不会输出 debug 级别的日志。

get()

get()会调用 Configuration 的私有方法 substituteVars(),该方法会完成配置
的属性扩展。属性扩展是指配置项的值包含 k e y 这 种 格 式 的 变 量 , 这 些 变 量 会 被 自 动 替 换 成 相 应 的 值 。 也 就 是 说 , {key}这种格式的变量,这些变量会被自动替 换成相应的值。也就是说, key{key} 会被替换成以key为键的配置项的值。注意,如果${key}
替换后,得到的配置项值仍然包含变量,这个过程会继续进行,直到替换后的值中不再出现
变量为止。

  /**
   * Get the value of the <code>name</code> property, <code>null</code> if
   * no such property exists. If the key is deprecated, it returns the value of
   * the first key which replaces the deprecated key and is not null.
   * 
   * Values are processed for <a href="#VariableExpansion">variable expansion</a> 
   * before being returned. 
   * 
   * @param name the property name, will be trimmed before get value.
   * @return the value of the <code>name</code> or its replacing property, 
   *         or null if no such property exists.
   */
  public String get(String name) {
    String[] names = handleDeprecation(deprecationContext.get(), name);
    String result = null;
    for(String n : names) {
      result = substituteVars(getProps().getProperty(n));
    }
    return result;
  }

如果一次属性扩展完成以后,得到的表达式里仍然包含可扩展的变量,那
么,substituteVars() 需要再次进行属性扩展。考虑下面的情况:
属性扩展 k e y 1 的 结 果 包 含 属 性 扩 展 {key1}的结果包含属性扩展 key1{key2},而对${key2}进行属性扩展后,产生
了一个包含S{key1}的新结果,这会导致属性扩展进入死循环,没办法停止。
针对这种可能发生的情况,substituteVars()中使用了一个非常简单而又有效的策略,即
属性扩展只能进行一定的次数(20 次,通过Configuration的静态成员变量MAX_ SUBST定
义),避免出现上面分析的属性扩展死循环。

最后一点需要注意的是,substituteVars(中进行的属性扩展,不但可以使用保存在
Configuration对象中的键一值对,而且还可以使用Java虛拟机的系统属性。如系统属
性user.home包含了当前用户的主目录,如果用户有一个配置项需要使用这个信息,可
以通过属性扩展${user.home},来获得对应的系统属性值。而且,Java命令行可以通过
“一D《name》=《value》”的方式定义系统属性。这就提供了一个通过命令行,覆盖或者设置
Hadoop运行时配置信息的方法。在substituteVars()中,属性扩展优先使用系统属性,然后
才是Configuration对象中保存的键一值对。具体代码如下:

  /**
   * Attempts to repeatedly expand the value {@code expr} by replacing the
   * left-most substring of the form "${var}" in the following precedence order
   * <ol>
   *   <li>by the value of the Java system property "var" if defined</li>
   *   <li>by the value of the configuration key "var" if defined</li>
   * </ol>
   *
   * If var is unbounded the current state of expansion "prefix${var}suffix" is
   * returned.
   *
   * @param expr the literal value of a config key
   * @return null if expr is null, otherwise the value resulting from expanding
   * expr using the algorithm above.
   * @throws IllegalArgumentException when more than
   * {@link Configuration#MAX_SUBST} replacements are required
   */
  private String substituteVars(String expr) {
    if (expr == null) {
      return null;
    }
    String eval = expr;
    for (int s = 0; s < MAX_SUBST; s++) {
      final int[] varBounds = findSubVariable(eval);
      if (varBounds[SUB_START_IDX] == -1) {
        return eval;
      }
      final String var = eval.substring(varBounds[SUB_START_IDX],
          varBounds[SUB_END_IDX]);
      String val = null;
      try {
        val = System.getProperty(var);
      } catch(SecurityException se) {
        LOG.warn("Unexpected SecurityException in Configuration", se);
      }
      if (val == null) {
        val = getRaw(var);
      }
      if (val == null) {
        return eval; // return literal ${var}: var is unbound
      }
      final int dollar = varBounds[SUB_START_IDX] - "${".length();
      final int afterRightBrace = varBounds[SUB_END_IDX] + "}".length();
      // substitute
      eval = eval.substring(0, dollar)
             + val
             + eval.substring(afterRightBrace);
    }
    throw new IllegalStateException("Variable substitution depth too large: " 
                                    + MAX_SUBST + " " + expr);
  }
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术补完计划

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值