1. 程式人生 > >yarn原始碼之前奏Configuration類解析

yarn原始碼之前奏Configuration類解析

2.2 Hadoop Configuration詳解
Hadoop使用了一套獨有的配置檔案管理系統,並提供自己的API,即使用org.apache.hadoop.conf.Configuration處理配置資訊。
2.2.1 Hadoop配置檔案的格式
Hadoop配置檔案採用XML格式,下面是Hadoop配置檔案的一個例子:

.<?xml version="1.0"?> 
.<?xml-stylesheet type="text/xsl" href="configuration.xsl"?> 
.<configuration> 
.  <property> 
.     <name>io.sort.factor</name> 
.     <value>10</value> 
.     <description>The number of streams to merge at once while sorting  
.     files.  This determines the number of open file handles.</description> 
.  </property> 
. 
. <property> 
.     <name>dfs.name.dir</name> 
.     <value>${hadoop.tmp.dir}/dfs/name</value> 
.     <description>Determines where on the local filesystem the DFS name  
.     nodeshould store the name table(fsimage).  ……</description> 
.  </property> 
. 
. <property> 
.     <name>dfs.web.ugi</name> 
.     <value>webuser,webgroup</value> 
.     <final>true</final> 
.     <description>The user account used by the web interface.  
.     Syntax: USERNAME,GROUP1,GROUP2, ……</description> 
.  </property> 
.</configuration> 

Hadoop配置檔案的根元素是configuration,包含若干子元素property。每一個property元素就是一個配置項,配置檔案不支援分層或分級。每個配置項一般包括配置屬性的名稱name、值value和一個關於配置項的描述description;元素final和Java中的關鍵字final類似,意味著這個配置項是“固定不變的”。final一般不出現,但在合併資源的時候,可以防止配置項的值被覆蓋。
在上面的示例檔案中,配置項dfs.web.ugi的值是“webuser,webgroup”,它是一個final配置項;從description看,這個配置項配置了Hadoop Web介面的使用者賬號,包括使用者名稱和使用者組資訊。這些資訊可以通過Configuration類提供的方法訪問。
在Configuration中,每個屬性都是String型別,但是值型別可能是以下多種型別,包括Java中的基本型別,如boolean(getBoolean)、int(getInt)、long(getLong)、float(getFloat),也可以是其他型別,如String(get)、java.io.File(getFile)、String陣列(getStrings)等。以上面配置檔案為例,getInt(“io.sort.factor”)將返回整數10;而getStrings(“dfs.web.ugi”)返回一個字串陣列,該陣列有兩個元素,分別是webuser和webgroup。
合併資源指將多個配置檔案合併,產生一個配置。如果有兩個配置檔案,也就是兩個資源,如core-default.xml和core-site.xml,通過Configuration類的loadResources()方法,把它們合併成一個配置。程式碼如下:
.Configurationconf = new Configuration();
.conf.addResource(“core-default.xml”);
.conf.addResource(“core-site.xml”);
如果這兩個配置資源都包含了相同的配置項,而且前一個資源的配置項沒有標記為final,那麼,後面的配置將覆蓋前面的配置。上面的例子中,core-site.xml中的配置將覆蓋core-default.xml中的同名配置。如果在第一個資源(core-default.xml)中某配置項被標記為final,那麼,在載入第二個資源的時候,會有警告提示。
Hadoop配置系統還有一個很重要的功能,就是屬性擴充套件。如配置項dfs.name.dir的值是 h

a d o o p . t m p .
d i r / d f s / n a m e {hadoop.tmp.dir}/dfs/name,其中, {hadoop.tmp.dir}會使用Configuration中的相應屬性值進行擴充套件。如果hadoop.tmp.dir的值是“/data”,那麼擴充套件後的dfs.name.dir的值就是“/data/dfs/name”。
使用Configuration類的一般過程是:構造Configuration物件,並通過類的addResource()方法新增需要載入的資源;然後就可以使用get方法和set方法訪問/設定配置項,資源會在第一次使用的時候自動載入到物件中。
2.2.2 Configuration的成員變數
org.apache.hadoop.conf.Configuration類圖如圖2-2所示。

從類圖可以看到,Configuration有7個主要的非靜態成員變數。
布林變數quietmode,用來設定載入配置的模式。如果quietmode為true(預設值),則在載入解析配置檔案的過程中,不輸出日誌資訊。quietmode只是一個方便開發人員除錯的變數。
陣列resources儲存了所有通過addResource()方法新增Configuration物件的資源。Configuration.addResource()有如下4種形式:
public void addResource(InputStream in)
public void addResource(Path file)
public void addResource(String name) //CLASSPATH資源
public void addResource(URL url)
布林變數loadDefaults用於確定是否載入預設資源,這些預設資源儲存在defaultResources中。注意,defaultResources是個靜態成員變數,通過方法 addDefaultResource(“core-default.xml”);addDefaultResource(“core-site.xml”);可以新增系統的預設資源。在HDFS中,會把hdfs-default.xml和hdfs-site.xml作為預設資源,並通過addDefaultResource()儲存在成員變數defaultResources中;在MapReduce中,預設資源是mapred-default.xml和mapred-site.xml。如HDFS的DataNode中,就有下面的程式碼,載入上述兩個預設資源:
//下面的程式碼來自org.apache.hadoop.hdfs.server.datanode.DataNode
static{
Configuration.addDefaultResource(“hdfs-default.xml”);
Configuration.addDefaultResource(“hdfs-site.xml”);
}
properties(private Properties properties;)、overlay(private Properties overlay;)和finalParameters(private Set finalParameters = new HashSet();)都是和配置項相關的成員變數。其中,properties和overlay的型別都是java.util.Properties。Hadoop配置檔案解析後的鍵–值對,都存放在properties中。變數finalParameters的型別是Set,用來儲存所有在配置檔案中已經被宣告為final的鍵–值對的鍵,如前面配置檔案例子中的鍵“dfs.web.ugi”。變數overlay用於記錄通過set()方式改變的配置項。也就是說,出現在overlay中的鍵–值對是應用程式中自己設定的,而不是通過對配置資源解析得到的。
Configuration中最後一個重要的成員變數是classLoader,這是一個類載入器變數,可以通過它來載入指定類,也可以通過它載入相關的資源。上面提到addResource()可以通過字串方式載入CLASSPATH資源,它其實通過Configuration中的getResource()將字串轉換成URL資源,相關程式碼如下:
public URL getResource(String name) {
return classLoader.getResource(name);
}
其中,getResource()用於根據資源的名稱查詢相應的資源,並返回讀取資源的URL 物件。
注意 這裡的資源,指的是可以通過類程式碼載入的一些資料,如影象、聲音、文字等,不是前面提到的配置資源。
瞭解了Configuration各成員變數的具體含義,Configuration類的其他部分就比較容易理解了,它們都是為了操作這些變數而實現的解析、設定、獲取方法。
2.2.3 資源載入(1)
資源通過物件的addResource()方法或類的靜態addDefaultResource()方法新增到Configuration物件中,新增的資源並不會立即被載入,只是通過reloadConfiguration()方法清空properties和finalParameters。相關程式碼如下:

public void addResource(String name) { // 以CLASSPATH資源為例  
  addResourceObject(name);  
}  
private synchronized void addResourceObject(Object resource) {  
 resources.add(resource);// 新增到成員變數resources中  
 reloadConfiguration();  
}   
public synchronized void reloadConfiguration() {  
  properties = null;// 會觸發資源的重新載入  
finalParameters.clear();  
}  

靜態方法addDefaultResource()也能清空Configuration物件中的資料(非靜態成員變數),這是通過類的靜態成員REGISTRY作為媒介進行的。

 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記錄了系統中所有的Configuration物件,所以,addDefaultResource()被呼叫時,遍歷REGISTRY中的元素並在元素(即Configuration物件)上呼叫reloadConfiguration()方法,即可觸發資源的重新載入,相關程式碼如下:
成員變數properties中的資料,直到需要的時候才會載入進來。在getProps()方法中,如果發現properties為空,將觸發loadResources()方法載入配置資源。這裡其實採用了延遲載入的設計模式,當真正需要配置資料的時候,才開始分析配置檔案。相關程式碼如下:

  protected synchronized Properties getProps() {
    if (properties == null) {
      properties = new Properties();
      HashMap<String, String[]> backup = 
        new HashMap<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();
          updatingResource.put(key, backup.get(key));
        }
      }
    }
return properties;
}

Hadoop的配置檔案都是XML形式,JAXP(Java API for XML Processing)是一種穩定、可靠的XML處理API,支援SAX(Simple API for XML)和DOM(Document Object Model)兩種XML處理方法。
SAX提供了一種流式的、事件驅動的XML處理方式,但編寫處理邏輯比較複雜,比較適合處理大的XML檔案。
DOM和SAX不同,其工作方式是:首先將XML文件一次性裝入記憶體;然後根據文件中定義的元素和屬性在記憶體中建立一個“樹形結構”,也就是一個文件物件模型,將文件物件化,文件中每個節點對應著模型中一個物件;然後使用物件提供的程式設計介面,訪問XML文件進而操作XML文件。由於Hadoop的配置檔案都是很小的檔案,因此Configuration使用DOM處理XML。
首先分析DOM載入部分的程式碼:

  private Resource loadResource(Properties properties, Resource wrapper, boolean quiet) {
    String name = UNKNOWN_RESOURCE;
    try {
      Object resource = wrapper.getResource();
      name = wrapper.getName();
         //得到用於建立DOM解析器的工廠  
      DocumentBuilderFactory docBuilderFactory 
        = DocumentBuilderFactory.newInstance();
      //ignore all comments inside the xml file
      docBuilderFactory.setIgnoringComments(true);
  //忽略XML中的註釋  
      //allow includes in the xml file
      docBuilderFactory.setNamespaceAware(true);
      //提供對XML名稱空間的支援 
      try {
//設定XInclude處理狀態為true,即允許XInclude機制  
          docBuilderFactory.setXIncludeAware(true);
      } catch (UnsupportedOperationException e) {
        LOG.error("Failed to set setXIncludeAware(true) for parser "
                + docBuilderFactory
                + ":" + e,
                e);
      }
      DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
 //獲取解析XML的DocumentBuilder物件
      Document doc = null;
      Element root = null;
      boolean returnCachedProperties = false;
      
//if判斷根據不同資源,做預處理並呼叫相應形式的DocumentBuilder.parse     
  if (resource instanceof URL) {    
//資源是URL形式              
// an URL resource
        doc = parse(builder, (URL)resource);
      } else if (resource instanceof String) {        
// a CLASSPATH resource
   //CLASSPATH資源   
        URL url = getResource((String)resource);
        doc = parse(builder, url);
      } else if (resource instanceof Path) {     
 //資源是Hadoop Path形式的     
// a file resource
        // Can't use FileSystem API or we get an infinite loop
        // since FileSystem uses Configuration API.  Use java.io.File instead.
        File file = new File(((Path)resource).toUri().getPath())
          .getAbsoluteFile();
        if (file.exists()) {
          if (!quiet) {
            LOG.debug("parsing File " + file);
          }
          doc = parse(builder, new BufferedInputStream(
              new FileInputStream(file)), ((Path)resource).toString());
        }
      } else if (resource instanceof InputStream) {
        doc = parse(builder, (InputStream) resource, null);
        returnCachedProperties = true;
      } else if (resource instanceof Properties) {
        overlay(properties, (Properties)resource);
      } else if (resource instanceof Element) {
//處理configuration子元素
        root = (Element)resource;
      }

這是整個載入檔案的過程,下面的程式碼和載入檔案無關。
2.2.3 資源載入(2)
一般的JAXP處理都是從工廠開始,通過呼叫DocumentBuilderFactory的newInstance()方法,獲得用於建立DOM解析器的工廠。這裡並沒有創建出DOM解析器,只是獲得一個用於建立DOM解析器的工廠,接下來需要對上述newInstance()方法得到的docBuilderFactory物件進行一些設定,才能進一步通過DocumentBuilderFactory,得到DOM解析器物件builder。
針對DocumentBuilderFactory物件進行的主要設定包括:
忽略XML文件中的註釋;
支援XML空間;
支援XML的包含機制(XInclude)。
XInclude機制允許將XML文件分解為多個可管理的塊,然後將一個或多個較小的文件組裝成一個大型文件。也就是說,Hadoop的一個配置檔案中,可以利用XInclude機制將其他配置檔案包含進來一併處理,下面是一個例子:
.
. ……
. <xi:include href=“conf4performance.xml”/>
. ……
.
通過XInclude機制,把配置檔案conf4performance.xml嵌入到當前配置檔案,這種方法更有利於對配置檔案進行模組化管理,同時就不需要再使用Configuration.addResource()方法載入資源conf4performance.xml了。
設定完DocumentBuilderFactory物件以後,通過docBuilderFactory.newDocumentBuilder()獲得了DocumentBuilder物件,用於從各種輸入源解析XML。在loadResource()中,需要根據Configuration支援的4種資源分別進行處理,不過這4種情況最終都呼叫DocumentBuilder.parse()(doc = parse(builder, (URL)resource);)函式,返回一個DOM解析結果。
成員函式loadResource的第二部分程式碼,就是根據DOM的解析結果設定Configuration的成員變數properties和finalParameters。
在確認XML的根節點是configuration以後,獲取根節點的所有子節點並對所有子節點進行處理。這裡需要注意,元素configuration的子節點可以是configuration,也可以是properties。如果是configuration,則遞迴呼叫loadResource(),在loadResource()的處理過程中,子節點會被作為根節點得到繼續的處理。
如果是property子節點,那麼試圖獲取property的子元素name、value和final。在成功獲得name和value的值後,根據情況設定物件的成員變數properties和finalParameters。相關程式碼如下:

     if (root == null) {
        if (doc == null) {
          if (quiet) {
            return null;
          }
          throw new RuntimeException(resource + " not found");
        }
        root = doc.getDocumentElement();
      }
      Properties toAddTo = properties;
      if(returnCachedProperties) {
        toAddTo = new Properties();
      }
      if (!"configuration".equals(root.getTagName()))
//根節點應該是configuration 
        LOG.fatal("bad conf file: top-level element not <configuration>");
//獲取根節點的所有子節點 
      NodeList props = root.getChildNodes();
      DeprecationContext deprecations = deprecationContext.get();
      for (int i = 0; i < props.getLength(); i++) {
        Node propNode = props.item(i);
        if (!(propNode instanceof Element))
          continue;
//如果子節點不是Element,忽略
        Element prop = (Element)propNode;
        if ("configuration".equals(prop.getTagName())) 
//如果子節點是configuration,遞迴呼叫loadResource進行處理 
//這意味著configuration的子節點可以是configuration  
{
          loadResource(toAddTo, new Resource(prop, name), quiet);
          continue;
        }
        if (!"property".equals(prop.getTagName()))
          LOG.warn("bad conf file: element not <property>");
//子節點是property 
        NodeList fields = prop.getChildNodes();
        String attr = null;
        String value = null;
        boolean finalParameter = false;
        LinkedList<String> source = new LinkedList<String>();
        for (int j = 0; j < fields.getLength(); j++) {
//for迴圈查詢name、value和final的值 
          Node fieldNode = fields.item(j);
          if (!(fieldNode instanceof Element))
            continue;
          Element field = (Element)fieldNode;
          if ("name".equals(field.getTagName()) && field.hasChildNodes())
            attr = StringInterner.weakIntern(
                ((Text)field.getFirstChild()).getData().trim());
          if ("value".equals(field.getTagName()) && field.hasChildNodes())
            value = StringInterner.weakIntern(
                ((Text)field.getFirstChild()).getData());
          if ("final".equals(field.getTagName()) && field.hasChildNodes())
            finalParameter = "true".equals(((Text)field.getFirstChild()).getData());
          if ("source".equals(field.getTagName()) && field.hasChildNodes())
            source.add(StringInterner.weakIntern(
                ((Text)field.getFirstChild()).getData()));
        }
        source.add(name);
        
        // Ignore this parameter if it has already been marked as 'final'
        if (attr != null) {
//如果屬性已經標誌為'final',忽略
          if (deprecations.getDeprecatedKeyMap().containsKey(attr)) {
//新增鍵-值對到properties中 
            DeprecatedKeyInfo keyInfo =
deprecations.getDeprecatedKeyMap().get(attr);
            keyInfo.clearAccessed();
//該屬性標誌為'final',新增name到finalParameters中
            for (String key:keyInfo.newKeys) {
              // update new keys with deprecated key's value 
              loadProperty(toAddTo, name, key, value, finalParameter, 
                  source.toArray(new String[source.size()]));
            }
          }
          else {
            loadProperty(toAddTo, name, attr, value, finalParameter, 
                source.toArray(new String[source.size()]));
          }
        }
      }
      
      if (returnCachedProperties) {
        overlay(properties, toAddTo);
        return new Resource(toAddTo, name);
      }
      return null;
//處理異常 
    } catch (IOException e) {
      LOG.fatal("error parsing conf " + name, e);
      throw new RuntimeException(e);
    } catch (DOMException e) {
      LOG.fatal("error parsing conf " + name, e);
      throw new RuntimeException(e);
    } catch (SAXException e) {
      LOG.fatal("error parsing conf " + name, e);
      throw new RuntimeException(e);
    } catch (ParserConfigurationException e) {
      LOG.fatal("error parsing conf " + name , e);
      throw new RuntimeException(e);
.    }

2.2.4 使用get和set訪問/設定配置項

  1. get*
    get*一共代表21個方法,它們用於在Configuration物件中獲取相應的配置資訊。這些配置資訊可以是boolean(getBoolean)、int(getInt)、long(getLong)等基本型別,也可以是其他一些Hadoop常用型別,如類的資訊(getClassByName、getClasses、getClass)、String陣列(getStringCollection、getStrings)、URL(getResource)等。這些方法裡最重要的是get()方法,它根據配置項的鍵獲取對應的值,如果鍵不存在,則返回預設值defaultValue。其他的方法都會依賴於Configuration.get(),並在get()的基礎上做進一步處理。get()方法如下:
  public String get(String name, String defaultValue) {
    String[] names = handleDeprecation(deprecationContext.get(), name);
    String result = null;
    for(String n : names) {
      result = substituteVars(getProps().getProperty(n, defaultValue));
    }
    return result;
  }

Configuration.get()會呼叫Configuration的私有方法substituteVars(),該方法會完成配置的屬性擴充套件。屬性擴充套件是指配置項的值包含 k e y {key}這種格式的變數,這些變數會被自動替換成相應的值。也就是說, {key}會被替換成以key為鍵的配置項的值。注意,如果${key}替換後,得到的配置項值仍然包含變數,這個過程會繼續進行,直到替換後的值中不再出現變數為止。
substituteVars的工作依賴於正則表示式:
.varPat:${[^}$ ]+}
由於“$”、左花括號“{”、右花括號“}”都是正則表示式中的保留字,因此需要通過“\”進行轉義。正則表示式varPat中,“${”部分用於匹配 k e y k e y {key}中的key前面的“ {”,最後的“}”部分匹配屬性擴充套件項的右花括號“}”,中間部分“[^}$ ]+”用於匹配屬性擴充套件鍵,它使用了兩個正則表示式規則:
[^ ]規則,通過[^ ]包含一系列的字元,使表示式匹配這一系列字元以外的任意一個字元。也就是說,“[^}$ ]”將匹配除了“}”、“ ”和空格以外的所有字元。注意, 後面還包含了一個空格,這個看不見的空格,是通過空格的Unicode字元\u0020新增到表示式中的。
+是一個修飾匹配次數的特殊符號,通過該符號保證了“+”前面的表示式“[^}$ ]”至少出現1次。
通過正則表示式“${[^}$ ]+}”,可以在輸入字串裡找出需要進行屬性擴充套件的地方,並通過字串替換,進行屬性擴充套件。
前面提過,如果一次屬性擴充套件完成以後,得到的表示式裡仍然包含可擴充套件的變數,那麼,substituteVars()需要再次進行屬性擴充套件。考慮下面的情況:
屬性擴充套件 k e y 1 {key1}的結果包含屬性擴充套件 {key2},而對 k e y 2 {key2}進行屬性擴充套件後,產生了一個包含 {key1}的新結果,這會導致屬性擴充套件進入死迴圈,沒辦法停止。
針對這種可能發生的情況,substituteVars()中使用了一個非常簡單而又有效的策略,即屬性擴充套件只能進行一定的次數(20次,通過Configuration的靜態成員變數MAX_SUBST定義),避免出現上面分析的屬性擴充套件死迴圈。
最後一點需要注意的是,substituteVars()中進行的屬性擴充套件,不但可以使用儲存在Configuration物件中的鍵–值對,而且還可以使用Java虛擬機器的系統屬性。在substituteVars()中,屬性擴充套件優先使用系統屬性,然後才是Configuration物件中儲存的鍵–值對。具體程式碼如下:

  private static final Pattern VAR_PATTERN =
      Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}");

  private static final int MAX_SUBST = 20;

  private String substituteVars(String expr) {
    if (expr == null) {
      return null;
    }
Matcher match = VAR_PATTERN.matcher("");
//正則表示式物件,包含正則表示式\$\{[^\}\$ ]+\} 
    String eval = expr;
for(int s=0; s<MAX_SUBST; s++) {
//最多做20次屬性擴充套件
      match.reset(eval);
      if (!match.find()) {
//什麼都沒有找到,返回 
        return eval;
      }
      String var = match.group();
      var = var.substring(2, var.length()-1); // remove ${ .. }
//獲得屬性擴充套件的鍵
      String val = null;
      try {
//看看系統屬性裡有沒有var對應的val 
//這一步保證了我們首先使用系統屬性做屬性擴充套件   
        val = System.getProperty(var);
      } catch(SecurityException se) {
        LOG.warn("Unexpected SecurityException in Configuration", se);
      }
      if (val == null) {
//看看Configuration儲存的鍵-值對裡有沒有var對應的val  
        val = getRaw(var);
      }
      if (val == null) {
//屬性擴充套件中的var沒有繫結,不做擴充套件,返回 
        return eval; // return literal ${var}: var is unbound
      }
      // substitute
      eval = eval.substring(0, match.start())+val+eval.substring(match.end());
//替換${……},完成屬性擴充套件  
    }
    throw new IllegalStateException("Variable substitution depth too large: " 
                                    + MAX_SUBST + " " + expr);
//屬性擴充套件次數過多,拋異常  
  }
  1. set*
    相對於get來說,set的大多數方法都很簡單,這些方法對輸入進行型別轉換等處理後,最終都呼叫了下面的Configuration.set()方法:
    public void set(String name, String value) {
    set(name, value, null);
    }
    對比相對複雜的Configuration.get(),成員函式set()只是簡單地呼叫了成員變數properties和overlay的setProperty()方法,儲存傳入的鍵–值對。
    getOverlay().setProperty(name, value);
    getProps().setProperty(name, value);