本系列基于 Apache Hadoop 2.7.2,并附 源码。
PS:Apache 基金下所有项目源码都可以在 archive.apache.org 这个网页中找到。强烈鄙视那些拿着公开源码放到论坛中提供有偿下载的行为!!!
Hadoop 运行原理概览
Hadoop 的运行过程可以划分为以下五个流程:
- client端创建并提交一个 job;
- yarn 的 ResourceManager 进行资源的分配;
- 由 NodeManager 进行加载与监控 containers;
- 通过 applicationMaster 与 ResourceManager 进行资源的申请及状态的交互,由NodeManagers进行MapReduce运行时job的管理.
- 通过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 有三个构造器:
- Configuration()
- Configuration(boolean loadDefaults)
- 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");
}
回顾一下类的加载顺序:
- 静态变量
- 静态代码块
- 成员变量
- 匿名代码块
- 构造器
- 静态方法
这里给自己提个醒:阅读源码可以先找找静态代码块,搜索“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);
}