HDFS文件系统是主从分布式文件系统,其中其中主节点为master,上面有一个非常重要的节点NameNode ,它的作用有管理整个集群的元数据信息、响应客户端的请求、接受DataNode心跳信息、定期CheckPoint等,它管理着集群。在安装hadoop之后启动之前先需要进行格式化操作,然后会用sbin/hadoop-dasmon.sh start namenode启动进程。 在源码中可以看到在NameNode类中的main方法去启动一个NameNode
/**
*/
public static void main(String argv[]) throws Exception {
if (DFSUtil.parseHelpArgument(argv, NameNode.USAGE, System.out, true)) {
System.exit(0);
}
try {
// Print a log message for starting up and shutting down
/**
* TODO 打印一些信息在启动和关闭的是偶
*/
StringUtils.startupShutdownMessage(NameNode.class, argv, LOG);
// 启动一个NameNode节点
NameNode namenode = createNameNode(argv, null);
if (namenode != null) {
namenode.join();
}
} catch (Throwable e) {
LOG.error("Failed to start namenode.", e);
terminate(1, e);
}
}
createNameNode方法将解析启动脚本中的参数,默认的就是利用构造函数实例化一个NameNode
default: {
DefaultMetricsSystem.initialize("NameNode");
return new NameNode(conf);
}
在构造函数中:
protected NameNode(Configuration conf, NamenodeRole role)
throws IOException {
this.conf = conf;
this.role = role;
// 从配置文件中获取地址信息以及其他信息
setClientNamenodeAddress(conf);
String nsId = getNameServiceId(conf);
String namenodeId = HAUtil.getNameNodeId(conf, nsId);
this.haEnabled = HAUtil.isHAEnabled(conf, nsId);
// 关于是否是 HA 高可用状态
state = createHAState(getStartupOption(conf));
this.allowStaleStandbyReads = HAUtil.shouldAllowStandbyReads(conf);
this.haContext = createHAContext();
try {
initializeGenericKeys(conf, nsId, namenodeId);
/**
* Initialize name-node.
*/
initialize(conf);
try {
// 通过ReadWriteLock读写锁和ReentrantLock可重入锁,防止线程问题
haContext.writeLock();
// 绑定ha上下文信息
state.prepareToEnterState(haContext);
// TODO Active 和 StandBy NameNode 会调用不同的方法
/**
* org.apache.hadoop.hdfs.server.namenode.ha.ActiveState#enterState
* -- Start services required in active state
* org.apache.hadoop.hdfs.server.namenode.ha.StandbyState#enterState
* -- Start services required in standby state
*/
state.enterState(haContext);
} finally {
// 释放锁
haContext.writeUnlock();
}
} catch (IOException e) {
/**
* Stop all NameNode threads and wait for all to finish.
*/
this.stop();
throw e;
} catch (HadoopIllegalArgumentException e) {
/**
* Stop all NameNode threads and wait for all to finish.
*/
this.stop();
throw e;
}
this.started.set(true);
}
在initialize方法中会:
1.启动一个http服务器,启动后通过50070端口访问hdfs的管理页面,
2.核心代码:FSNamesystem初始化【 FSNamesystem.loadFromDisk(conf)】
a.checkConfiguration(conf); 从过conf文件校验配置文件信息
b.根据conf文件实例FSImage,并调用FSImage.recoverTransitionRead,返回boolean staleImage
利用FSImage 分析存储目录,看是否需要从前一个状态中恢复?是否根据命名空间信息执行fs状态转换?如果需要的话则将image与EditLogs合并,(有点像checkpoint?),过程中会:
如果不是是HA 那么调用FSEditLog.initJournalsForWrite 和 FSEditLog.recoverUnclosedStreams
如果是HA,那么调用FSEditLog.initSharedJournalsForRead
c.boolean needToSave = staleImage && !haEnabled && !isRollingUpgrade()
判断当前images是否需要保存、是否不是高可用、是否非滚动升级中
d.needToSave 为true时,则将FSimage 保存起来在当前每一个存储目录中
e.之后如果时非HA则会创建一个新的日志segment 并写入seen_txid file,hadoop时利用双缓冲队列进行日志的高速读写,
if (!haEnabled || (haEnabled && startOpt == StartupOption.UPGRADE)
|| (haEnabled && startOpt == StartupOption.UPGRADEONLY)) {
fsImage.openEditLogForWrite();
}
f.NameNodeMetrics(收集各种指标参数),将本次loadFSImage的过程耗时放入到指标系统中
3.核心代码:创建一个rpc server实例
hadoop内部通过这个RPC实现进程之间的交互,其中在实例化的时候利用了构造者模式,例如:
// 提供给DataNodes节点的server,监听DataNodes的请求,这个server会绑定很多协议,提供不同的功能
this.serviceRpcServer = new RPC.Builder(conf)
.setProtocol(
org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class)
.setInstance(clientNNPbService)
.setBindAddress(bindHost)
.setPort(serviceRpcAddr.getPort()).setNumHandlers(serviceHandlerCount)
.setVerbose(false)
.setSecretManager(namesystem.getDelegationTokenSecretManager())
.build();
// 提供给客户端的server,监听clients的请求,这个server会绑定很多协议,提供不同的功能
this.clientRpcServer = new RPC.Builder(conf)
.setProtocol(
org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class)
.setInstance(clientNNPbService).setBindAddress(bindHost)
.setPort(rpcAddr.getPort()).setNumHandlers(handlerCount)
.setVerbose(false)
.setSecretManager(namesystem.getDelegationTokenSecretManager()).build();
4.核心代码:启动一些服务组件,包括rpc server等
启动一些公共的服务激活为active或standby stats,这个过程中调用org.apache.hadoop.hdfs.server.namenode.FSNamesystem#startCommonServices方法,在其中:
a.创建一个NameNodeResourceChecker,获取了要检查的目录(这里面会检查三个目录,分别是存储fsimage目录、存储editlog的目录、JournalNode的存储目录,前面2个是涉及NameNode的元数据存储【一般这2个默认是一个目录】,后面一个是HA模式下同步元数据有关,在org.apache.hadoop.hdfs.server.namenode.FSNamesystem#getNamespaceEditsDirs(org.apache.hadoop.conf.Configuration, boolean) 方法中,
// 默认情况下这个2个目录是同一个目录
public static List<URI> getNamespaceEditsDirs(Configuration conf,
boolean includeShared)
throws IOException {
// Use a LinkedHashSet so that order is maintained while we de-dup
// the entries.
LinkedHashSet<URI> editsDirs = new LinkedHashSet<URI>();
if (includeShared) {
// TODO JournalNode 的目录
// 通过参数 DFS_NAMENODE_SHARED_EDITS_DIR_KEY = "dfs.namenode.shared.edits.dir";
List<URI> sharedDirs = getSharedEditsDirs(conf);
// Fail until multiple shared edits directories are supported (HDFS-2782)
if (sharedDirs.size() > 1) {
throw new IOException(
"Multiple shared edits directories are not yet supported");
}
// First add the shared edits dirs. It's critical that the shared dirs
// are added first, since JournalSet syncs them in the order they are listed,
// and we need to make sure all edits are in place in the shared storage
// before they are replicated locally. See HDFS-2874.
for (URI dir : sharedDirs) {
if (!editsDirs.add(dir)) {
LOG.warn("Edits URI " + dir + " listed multiple times in " +
DFS_NAMENODE_SHARED_EDITS_DIR_KEY + ". Ignoring duplicates.");
}
}
}
// Now add the non-shared dirs.
//TODO NameNode 的相关存储目录
// 参数:DFS_NAMENODE_EDITS_DIR_KEY = "dfs.namenode.edits.dir";
for (URI dir : getStorageDirs(conf, DFS_NAMENODE_EDITS_DIR_KEY)) {
if (!editsDirs.add(dir)) {
LOG.warn("Edits URI " + dir + " listed multiple times in " +
DFS_NAMENODE_SHARED_EDITS_DIR_KEY + " and " +
DFS_NAMENODE_EDITS_DIR_KEY + ". Ignoring duplicates.");
}
}
if (editsDirs.isEmpty()) {
// If this is the case, no edit dirs have been explicitly configured.
// Image dirs are to be used for edits too.
return Lists.newArrayList(getNamespaceDirs(conf));
} else {
return Lists.newArrayList(editsDirs);
}
}
检查namenode所在节点的机器上是否有足够的磁盘空间。如果资源不够则输出警告日志,并进入安全模式。
b.在setBlockTotal()方法中,会先获取集群可用的Block块个数【总的Blocks数 - 正在构建的Block数 = 集群可以使用的Block】,(PS:Block会有COMPLETE和UnderConstruction2种不同状态,COMPLETE表示是一个可以使用的完整的block,UnderConstruction表示正在构建的状态的bloak 还不能使用)。
然后看是否满足集群启动的阈值(DFS_NAMENODE_SAFEMODE_THRESHOLD_PCT_DEFAULT = 0.999f 默认值),进行安全模式的校验:
1.threshold != 0 && blockSafe < blockThreshold):
blockSafe namenode在datanode启动后 接受的block数之和、blockThreshold :安全模式的阈值,即:上报的Block数<集群的阈值 则进入安全模式
2.(datanodeThreshold != 0 && getNumLiveDataNodes() < datanodeThreshold)
datanodeThreshold: datanode的阈值 小于此值则进入 安全模式,默认值 :DFS_NAMENODE_SAFEMODE_MIN_DATANODES_DEFAULT = 0;
3.(!nameNodeHasResourcesAvailable())
这方法是校验是否有足够的磁盘空间,这个空间有一个默认参数,DFS_NAMENODE_DU_RESERVED_DEFAULT = 1024 * 1024 * 100; // 100 MB ,所以最少需要100M的磁盘空间,否则进入安全模式 ,这个是通过NameNodeResourceChecker 这个类实现的
上面的条件之间是或者关系,即有一个满足则进入安全模式!
4.org.apache.hadoop.hdfs.server.blockmanagement.BlockManager#activate(),在FSNamesystem中有几个重要的组件,其中一个就是BlockManager,负责管理Block块的信息,它的里面有一个HeartbeatManager,因为是负责管理分布在各个DataNode上的Block块信息的,所以其内部会有一个线程定期(默认是5s)的去接受DataNode发送的心跳信息,这个线程就是在HeartbeatManager中的heartbeatThread,只要namesystem.isRunning()在运行那么就会定期进行心跳检查。
/************************************heartbeatThread的定期心跳检查部分**********************************************/
@Override
public void run() {
while(namesystem.isRunning()) {
try {
final long now = Time.monotonicNow();
if (lastHeartbeatCheck + heartbeatRecheckInterval < now) {
heartbeatCheck();
lastHeartbeatCheck = now;
}
if (blockManager.shouldUpdateBlockKey(now - lastBlockKeyUpdate)) {
synchronized(HeartbeatManager.this) {
for(DatanodeDescriptor d : datanodes) {
d.needKeyUpdate = true;
}
}
lastBlockKeyUpdate = now;
}
} catch (Exception e) {
LOG.error("Exception while checking heartbeat", e);
}
try {
Thread.sleep(5000); // 5 seconds
} catch (InterruptedException ie) {
}
}
}
/*********************************************************************************/
/************************************************************/
// org.apache.hadoop.hdfs.server.namenode.FSNamesystem#startCommonServices
/**
* Start services common to both active and standby states
*/
void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
this.registerMBean(); // register the MBean for the FSNamesystemState
writeLock();
this.haContext = haContext;
try {
/**
* // 创建了一个NameNodeResourceChecker对象,获取了要检查的目录
* 用来检查namenode所在机器上的磁盘空间是否足够
*/
nnResourceChecker = new NameNodeResourceChecker(conf);
/**
* // 检查可用资源是否足够:如果不够,日志打印警告信息,然后进入安全模式
*/
checkAvailableResources();
/**
* assert [boolean 表达式] 断言
* 如果[boolean表达式]为true,则程序继续执行。
* 如果为false,则程序抛出AssertionError,并终止执行。
*
*
*/
assert safeMode != null && !isPopulatingReplQueues();
StartupProgress prog = NameNode.getStartupProgress();
prog.beginPhase(Phase.SAFEMODE);
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
getCompleteBlocksTotal());
/**
* HDFS 的安全模式的检查(****面试题******)
*
*/
setBlockTotal();
/**
* 启动重要服务
*/
blockManager.activate(conf);
} finally {
writeUnlock();
}
registerMXBean();
DefaultMetricsSystem.instance().register(this);
if (inodeAttributeProvider != null) {
inodeAttributeProvider.start();
dir.setINodeAttributeProvider(inodeAttributeProvider);
}
snapshotManager.registerMXBean();
}
下面是namenode初始化的方法
/**
* Initialize name-node.
*
* @param conf the configuration
*/
protected void initialize(Configuration conf) throws IOException {
if (conf.get(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS) == null) {
String intervals = conf.get(DFS_METRICS_PERCENTILES_INTERVALS_KEY);
if (intervals != null) {
conf.set(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS,
intervals);
}
}
UserGroupInformation.setConfiguration(conf);
loginAsNameNodeUser(conf);
NameNode.initMetrics(conf, this.getRole());
StartupProgressMetrics.register(startupProgress);
if (NamenodeRole.NAMENODE == role) {
/**
* //启动http服务器,启动后可以通过http://namenode:50070 访问hdfs的管理页面
*/
startHttpServer(conf);
}
this.spanReceiverHost = SpanReceiverHost.getInstance(conf);
/**
* // 核心代码:FSNamesystem初始化
*/
loadNamesystem(conf);
/**
* // 核心代码:创建一个rpc server实例
*/
rpcServer = createRpcServer(conf);
if (clientNamenodeAddress == null) {
// This is expected for MiniDFSCluster. Set it now using
// the RPC server's bind address.
clientNamenodeAddress =
NetUtils.getHostPortString(rpcServer.getRpcAddress());
LOG.info("Clients are to use " + clientNamenodeAddress + " to access"
+ " this namenode/service.");
}
if (NamenodeRole.NAMENODE == role) {
httpServer.setNameNodeAddress(getNameNodeAddress());
httpServer.setFSImage(getFSImage());
}
pauseMonitor = new JvmPauseMonitor(conf);
pauseMonitor.start();
metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
/**
* // 核心代码:启动一些服务组件,包括rpc server等
*/
startCommonServices(conf);
}
总结一下这个过程中涉及的重要的类以及关键部分:
NameNode:
main(): 启动NameNode
createNameNode:读取配置文件,创建一个namenode
DefaultMetricsSystem.initialize(): 初始化默认的指标系统
NameNode(conf) :调用NameNode构造方法
initialize(conf):初始化NameNode以及相关的组件
startHttpServer(conf): 启动NameNodeHttpServer 提供访问页面
loadNamesystem(conf):FSNamesystem初始化
createRpcServer(conf):创建一个NameNodeRpcServer【其中RPC.Server serviceRpcServer(为DataNodes 服务),RPC.Server clientRpcServer(为客户端服务)】
FSNamesystem.startCommonServices(conf)
1. 创建一个NameNodeResourceChecker对象,获取了要检查的目录用来检查namenode所在机器上的磁盘空间是否足够
2. 可用资源是否足够:如果不够,日志打印警告信息
3. FSNamesystem.setBlockTotal():的安全模式的检查(****面试题******)
4. BlockManager.activate(conf):
4.1 PendingReplicationBlocks.start() :启动了等待复制的线程
4.2 datanodeManager.activate(conf) :启动了管理心跳的服务
4.2.1 启动了 管理下线datanode的服务 DecommissionManager.activate(conf)
4.2.2 启动了管理心跳的线程HeartbeatManager.activate(conf);
4.3 replicationThread.start() : block 副本复制相关线程
附上简图,