前面我们已经对HDFS有了很多了解,但是光说不练假把式,今天开启深入源码的纯享模式,先来看看NameNode启动流程,在代码层面,到底是如何实现的。
首先还是得从我们的前一篇提到过的NameNode类(org.apache.hadoop.hdfs.server.namenode.NameNode)开始,既然是看启动流程,那自然是先找类里面的main方法啦。
当我们启动NameNode的时候,它就会执行NameNode类的main方法,在main方法中会创建NameNode对象,代码如下:
public static void main(String argv[]) throws Exception {
... ...
// createNameNode返回NameNode对象
NameNode namenode = createNameNode(argv, null);
... ...
}
在main方法里面还有个小细节,代码如下:
if (namenode != null) { namenode.join(); }
这个会让线程阻塞在这儿。这也就是为什么我们敲 jps 命令的时候,能一直看到 namenode的原因。
从上面源码可以看到, createNameNode 这个方法会返回NameNode对象,那我们看看它是怎么做的,代码如下:
public static NameNode createNameNode(String argv[], Configuration conf) throws
IOException {
LOG.info("createNameNode " + Arrays.asList(argv));
if (conf == null)
conf = new HdfsConfiguration();
... ...
switch (startOpt) {
// 我们如果是通过 hdfs namenode -format 命令格式化的话,就会进入这里
case FORMAT: {
boolean aborted = format(conf, startOpt.getForceFormat(),
startOpt.getInteractiveFormat());
terminate(aborted ? 1 : 0);
return null; // avoid javac warning
}
// 正常启动会直接到这里
default: {
DefaultMetricsSystem.initialize("NameNode");
return new NameNode(conf);
}
}
}
可以看到在createNameNode方法中,实际上最终返回 new NameNode(conf) 对象,NameNode构建方法如下:
public NameNode(Configuration conf) throws IOException {
//这里第二个参数为 "NameNode"
// NamenodeRole 枚举类里面记录着 NAMENODE ("NameNode")
this(conf, NamenodeRole.NAMENODE);
}
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);
state = createHAState(getStartupOption(conf));
this.allowStaleStandbyReads = HAUtil.shouldAllowStandbyReads(conf);
this.haContext = createHAContext();
... ...
initializeGenericKeys(conf, nsId, namenodeId);
//我们去分析源码的时候,这样的关键的方法我们一定要留意。
initialize(conf);
... ...
this.started.set(true);
}
通过上面源码可以看到,NameNode的构造方法中,会执行 initialize(conf)方法,来进行NameNode启动流程。
下面我们重点来看下这个启动流程,进入initialize方法,可以看到其源码的主要实现如下:
protected void initialize(Configuration conf) throws IOException {
... ...
//判断是NameNode角色
if (NamenodeRole.NAMENODE == role) {
//1.启动 NameNode httpserver ,用户可以通过http访问WebUI
startHttpServer(conf);
}
//2.加载本地文件中的镜像文件和editslog到内存中
loadNamesystem(conf);
... ...
//3.createRpcServer 创建 NameNodeRpc服务端
rpcServer = createRpcServer(conf);
... ...
//4.启动CommoneService 进行 NameNode资源检查和安全模式检查
startCommonServices(conf);
... ...
}
给大家简单总结一下,在initialize方法中NameNode启动时,主要经过的4个过程:
启动NameNode HttpServer (方便用户通过http访问HDFS WebUI)
加载本地元数据(Fsimage和Editslog)
创建NameNodeRpcServer并启动(ClientRPCServer和ServiceRPCServer)
启动公共的服务(资源检查和安全模式检查)
是不是看到很多熟悉的关键词?别激动,我们接着分别深入这4个过程看看。
首先是第1个过程,startHttpServer方法主要创建HttpServer ,这样用户就可以通过WebUI来访问NameNode。
startHttpServer代码如下:
private void startHttpServer(final Configuration conf) throws IOException {
//getHttpServerBindAddress 中绑定了NameNode的IP和端口
httpServer = new NameNodeHttpServer(conf, this, getHttpServerBindAddress(conf));
//启动Http server
httpServer.start();
httpServer.setStartupProgress(startupProgress);
}
getHttpServerBindAddress(conf)中进行了NameNode节点IP和端口绑定,并返回InetSocketAddress对象,getHttpServerBindAddress(conf)源码如下:
protected InetSocketAddress getHttpServerBindAddress(Configuration conf) {
//getHttpServerAddress 绑定NameNode IP及端口
InetSocketAddress bindAddress = getHttpServerAddress(conf);
// If DFS_NAMENODE_HTTP_BIND_HOST_KEY exists then it overrides the
// host name portion of DFS_NAMENODE_HTTP_ADDRESS_KEY.
//获取 NameNode host主机
final String bindHost = conf.getTrimmed(DFS_NAMENODE_HTTP_BIND_HOST_KEY);
if (bindHost != null && !bindHost.isEmpty()) {
bindAddress = new InetSocketAddress(bindHost, bindAddress.getPort());
}
return bindAddress;
}
以上源码中getHttpServerAddress会绑定节点IP和端口。
接着往下看,httpServer.start()具体源码如下:
void start() throws IOException {
... ...
//Hadoop中封装了自己的Httpserver,形成自己的Httpserver2
HttpServer2.Builder builder = DFSUtil.httpServerTemplateForNNAndJN(conf,
httpAddr, httpsAddr, "hdfs",
DFSConfigKeys.DFS_NAMENODE_KERBEROS_INTERNAL_SPNEGO_PRINCIPAL_KEY,
DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY);
... ...
//servlet越多,支持的功能就越多
setupServlets(httpServer, conf);
//启动 httpServer 服务,对外开放绑定的端口
httpServer.start();
... ...
}
在startHttpServer方法中的httpServer.start()方法中,HDFS进行了HttpServer2封装,Hadoop中使用了自己的Httpserver进行Kerberos认证,最后通过HttpServer2.Builder.build()方法创建了hdfs自己的httpserver并调用start方法进行启动。
再看第2个过程,loadNamesystem(conf) 中会加载本地Fsimage和Editslog,具体源码如下:
protected void loadNamesystem(Configuration conf) throws IOException {
//从磁盘中加载editslog和fsimage
this.namesystem = FSNamesystem.loadFromDisk(conf);
}
loadFromDisk源码如下:
static FSNamesystem loadFromDisk(Configuration conf) throws IOException {
... ...
// 封装FSImage对象
FSImage fsImage = new FSImage(conf,
FSNamesystem.getNamespaceDirs(conf),
FSNamesystem.getNamespaceEditsDirs(conf));
// 创建 FSNamesystem 对象,并对该对象中fsimage 属性赋值fsimage
FSNamesystem namesystem = new FSNamesystem(conf, fsImage, false);
... ...
// HDFS这命名挺清晰的,这个代码看名字就知道去加载元数据的
namesystem.loadFSImage(startOpt);
.... ...
}
继续往里走,loadFSImage 源码如下:
private void loadFSImage(StartupOption startOpt) throws IOException {
final FSImage fsImage = getFSImage();
// format before starting up if requested
// 参数配置里有格式化的要求,就会在这进行格式化元数据
if (startOpt == StartupOption.FORMAT) {
fsImage.format(this, fsImage.getStorage().determineClusterId());// reuse current id
startOpt = StartupOption.REGULAR;
}
... ...
final boolean staleImage
//fsimage + editLog = new FSimage
//这里会合并元数据
= fsImage.recoverTransitionRead(startOpt, this, recovery);
// 打印日志
LOG.info("Need to save fs image? " + needToSave
+ " (staleImage=" + staleImage + ", haEnabled=" + haEnabled
+ ", isRollingUpgrade=" + isRollingUpgrade() + ")");
//(2)把合并出来的新的fsimage写到我们的磁盘上面。
fsImage.saveNamespace(this);
... ...
//(3)打开一个新的editLog开始写日志
fsImage.openEditLogForWrite();
... ...
imageLoadComplete();
}
可以从源码看到,当HDFS重启时,会将FsImage内容映射到内存中,然后再一条条执行Editslog文件中的操作,从而恢复到NameNode重启前的状态。
这也进一步验证了我们之前所说的内容。
接着是第3个过程,也就是我们很熟悉的RPC啦。
NameNodeRPCserver里面有两个主要的RPC服务:
创建NameNodeRpcServer的代码如下:
//3.createRpcServer 创建 NameNodeRpc服务端和客户端
rpcServer = createRpcServer(conf);
createRpcServer源码如下:
protected NameNodeRpcServer createRpcServer(Configuration conf) throws IOException {
return new NameNodeRpcServer(conf, this);
}
可以看到, new NameNodeRpcServer(conf, this) 方法会创建并返回一个nameNodeRpcServer对象,而这个对象里面,又会创建前面我们提到的,Rpc服务端和客户端的RpcServer。
具体实现源码如下:
public NameNodeRpcServer(Configuration conf, NameNode nn) throws IOException {
... ...
// 这个服务起来是用来监听DataNode发送过来的请求的
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();
... ...
// 这个服务是主要服务于客户端发送过来的请求的
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())
.setAlignmentContext(stateIdContext)
.build();
... ...
}
这里只是创建,NameNode serviceRpcServer和clientRpcServer,会在下一个过程启动。
经过前面3个过程后,就会开始启动一些公共的服务,比如上面刚刚提到的,NameNode的RPC服务,不过在启动前还会进行两个检查:
下面我们来看看源码怎么实现的:
//4.启动CommoneService 进行 NameNode资源检查和安全模式检查
startCommonServices(conf);
startCommonServices方法实现如下:
private void startCommonServices(Configuration conf) throws IOException {
... ...
// 启动服务 检测磁盘空间和安全模式
namesystem.startCommonServices(conf, haContext);
... ...
// RPC服务端启动起来了
rpcServer.start();
... ...
}
以上代码中
startCommonServices(conf, haContext)方法的具体源码如下:
void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
... ...
//nnResourceChecker 对象用于后续检查editslog 目录空间是否足够
nnResourceChecker = new NameNodeResourceChecker(conf);
//检查是否有足够磁盘空间存储数据
checkAvailableResources();
assert !blockManager.isPopulatingReplQueues();
StartupProgress prog = NameNode.getStartupProgress();
//开始进入安全模式
prog.beginPhase(Phase.SAFEMODE);
//获取所有可用的block
long completeBlocksTotal = getCompleteBlocksTotal();
//设置安全模式
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,completeBlocksTotal);
//启动块服务并对DataNode 心跳超时进行判断
blockManager.activate(conf, completeBlocksTotal);
... ...
}
以上代码中 nnResourceChecker = new NameNodeResourceChecker(conf); 中会设置磁盘空间最小阈值100M,然后执行 checkAvailableResources(); 方法进行检查节点磁盘空间是充足。
new NameNodeResourceChecker(conf) 源码如下:
public NameNodeResourceChecker(Configuration conf) throws IOException {
... ...
// duReserved 默认为100M
duReserved = conf.getLongBytes(DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_KEY,
DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_DEFAULT);
... ...
}
checkAvailableResources()源码如下:
void checkAvailableResources() {
... ...
//判断磁盘资源是否够用
hasResourcesAvailable = nnResourceChecker.hasAvailableDiskSpace();
... ...
}
其中 hasAvailableDiskSpace() 方法实现如下:
public boolean hasAvailableDiskSpace() {
return NameNodeResourcePolicy.areResourcesAvailable(volumes.values(),
minimumRedundantVolumes);
}
这个方法如果返回true,表示至少有一个配置的磁盘空间满足使用。
方法中的 areResourcesAvailable 实现源码如下:
static boolean areResourcesAvailable(
Collection resources,
int minimumRedundantResources) {
... ...
//检查资源是否充足
for (CheckableNameNodeResource resource : resources) {
if (!resource.isRequired()) {
redundantResourceCount++;
// isResourceAvailable 实现类为 NameNodeResourceChecker.CheckedVolume中的isResourceAvailable 方法
if (!resource.isResourceAvailable()) {
disabledRedundantResourceCount++;
}
} else {
requiredResourceCount++;
if (!resource.isResourceAvailable()) {
// Short circuit - a required resource is not available.
return false;
}
}
}
... ...
}
其中resource.isResourceAvailable()中判断磁盘是否满足最低的100M,返回true表示满足,返回false表示不满足。
具体判断源码如下:
public boolean isResourceAvailable() {
... ...
//如果磁盘空间小于100M 返回fasle
if (availableSpace < duReserved) {
LOG.warn("Space available on volume '" + volume + "' is "
+ availableSpace +
", which is below the configured reserved amount " + duReserved);
return false;
} else {
return true;
}
... ...
}
检测完磁盘可用空间后,进入安全模式,并进行可用block的检测,进而判断是否退出NameNode安全模式,具体源码在FSNmaesystem.startCommonServices中,如下:
... ...
//开始进入安全模式
prog.beginPhase(Phase.SAFEMODE);
//获取所有可用的block
long completeBlocksTotal = getCompleteBlocksTotal();
//设置安全模式
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
completeBlocksTotal);
//检测DataNode状态及是否退出安全模式
blockManager.activate(conf, completeBlocksTotal);
... ...
以上代码中blockManager.activate(conf, completeBlocksTotal)会进行block块检测,查看正常可用block数是否满足总block的99.9% 可用。
active(conf,completeBlocksTotal)具体源码如下:
public void activate(Configuration conf, long blockTotal) {
... ...
//datanodeManager对象对周期检查DataNode连接情况
datanodeManager.activate(conf);
... ...
//检测 正常 block 情况
bmSafeMode.activate(blockTotal);
... ...
}
datanodeManager.activate(conf)主要进行DataNode节点是否宕机,默认经过10分钟+30s一个DataNode没有向NameNode汇报心跳信息,则认为该DataNode宕机。(还记得核心设计里面提到的计算公式吗?)
datanodeManager.activate(conf)实现源码如下:
void activate(final Configuration conf) {
datanodeAdminManager.activate(conf);
//与DataNode心跳检测
heartbeatManager.activate();
}
heartbeatManager.activate()方法最终调用到Monitor线程的run方法进行DataNode状态监测。
bmSafeMode.activate(blockTotal)进行是否退出安全模式检查,实现源码如下:
void activate(long total) {
... ...
//设置正常可用block,并设置正常退出安全模式阈值为0.999f
setBlockTotal(total);
if (areThresholdsMet()) {//判断是否可以退出安全模式,block和datanode阈值都满足退出
boolean exitResult = leaveSafeMode(false);
Preconditions.checkState(exitResult, "Failed to leave safe mode.");
} else {//进入安全模式
// enter safe mode
status = BMSafeModeStatus.PENDING_THRESHOLD;
initializeReplQueuesIfNecessary();
reportStatus("STATE* Safe mode ON.", true);
lastStatusReport = monotonicNow();
}
... ...
}
以上代码中:
areThresholdsMet()实现如下:
private boolean areThresholdsMet() {
//如果block和datanode阈值都满足,则为True,否则返回false
... ...
synchronized (this) {
boolean isBlockThresholdMet = (blockSafe ]]>= blockThreshold);
boolean isDatanodeThresholdMet = true;
if (isBlockThresholdMet && datanodeThreshold ]]> 0) {
int datanodeNum = blockManager.getDatanodeManager().
getNumLiveDataNodes();
isDatanodeThresholdMet = (datanodeNum ]]>= datanodeThreshold);
}
return isBlockThresholdMet && isDatanodeThresholdMet;
}
今天通过一步步梳理NameNode启动的源码细节,进一步深入理解NameNode的设计与实现思路,同时还串联了前面我们提到的知识点。
感兴趣的小伙伴可以跟着文章的思路,再捋一遍源码,会更有收获!