目录
Namenode介绍
Namenode如同一个管理者的角色
● 它负责维护整个hdfs文件系统的目录树结构以及文件所对应的block块信息(保存在磁盘上,因为文件与block块之间的关系不会发生变化);
● 它管理block块与datanode的映射关系(Namenode启动时候块上报,保存在内存中);
● 它能接收cilent的请求,接收datanode的块报告和心跳;
Namenode服务主要靠3个重要类实现
● Namenode类:启动类,管理配置等
● Namenode server:NameNodeRPCServer(接收和处理所有rpc请求)和NameNodeHttpServer(web)
● FSNamesystem:负责Namenode的所有逻辑,管理HDFS元数据
Namenode启动
启动流程图
启动源码解析
start-dfs.sh脚本是为了启动namenode,定位到Namenode启动类中main方法
public static void main(String argv[]) throws Exception {
...
try {
StringUtils.startupShutdownMessage(NameNode.class, argv, LOG);
//创建NN对象
NameNode namenode = createNameNode(argv, null);
if (namenode != null) {
//阻塞作用,让main线程无法结束
namenode.join();
}
} ...
}
createNameNode是重要的方法,用于构造NN对象
public static NameNode createNameNode(String argv[], Configuration conf)
throws IOException {
LOG.info("createNameNode " + Arrays.asList(argv));
//配置文件
if (conf == null)
conf = new HdfsConfiguration();
// Parse out some generic args into Configuration.
//解析
GenericOptionsParser hParser = new GenericOptionsParser(conf, argv);
argv = hParser.getRemainingArgs();
// Parse the rest, NN specific args.
StartupOption startOpt = parseArguments(argv);
...
boolean aborted = false;
//根据启动选项执行不同的方法
switch (startOpt) {
case FORMAT:
//初始化
aborted = format(conf, startOpt.getForceFormat(),startOpt.getInteractiveFormat());
case GENCLUSTERID:
case ROLLBACK:
case BOOTSTRAPSTANDBY:
case INITIALIZESHAREDEDITS:
case BACKUP:
case CHECKPOINT:
case RECOVER:
case METADATAVERSION:
case UPGRADEONLY:
default:
DefaultMetricsSystem.initialize("NameNode");
//构造NN
return new NameNode(conf);
}
}
配置NN后需要先初始化NN,进入format方法
private static boolean format(Configuration conf, boolean force,
boolean isInteractive) throws IOException {
...
FSImage fsImage = new FSImage(conf, nameDirsToFormat, editDirsToFormat);
try {
//构建FSNamesystem用于管理NN信息
FSNamesystem fsn = new FSNamesystem(conf, fsImage);
//初始化Journal
fsImage.getEditLog().initJournalsForWrite();
}
...
fsImage.format(fsn, clusterId, force);
...
return false;
}
返回createNameNode,new NameNode(conf)构造NN对象
protected NameNode(Configuration conf, NamenodeRole role)
throws IOException {
//配置
super(conf);
// 根据配置确认是否开启了HA
String namenodeId = HAUtil.getNameNodeId(conf, nsId);
//是否启用ha
this.haEnabled = HAUtil.isHAEnabled(conf, nsId);
//非HA -> ACTIVE_STATE
state = createHAState(getStartupOption(conf));
//初始化
initialize(getConf());
//初始化完成后, Namenode进入Standby状态
//在这里会开启StandbyCheckpointer里面的checkpointer 线程. 定时合并&处理images文件
state.prepareToEnterState(haContext);
state.enterState(haContext);
}
initialize是重要方法,会初始化httpserver、rpserver,构造FSNamesystem用于管理文件系统
进入initialize(getConf())方法
protected void initialize(Configuration conf) throws IOException {
//配置
//构造JvmPauseMonitor对象, 并启动
pauseMonitor = new JvmPauseMonitor();
pauseMonitor.init(conf);
pauseMonitor.start();
metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
//启动HTTP服务
if (NamenodeRole.NAMENODE == role) {
startHttpServer(conf);
}
// 初始化FSNamesystem
// NameNode将对文件系统的管理都委托给了FSNamesystem对象,
// NameNode会调用FSNamesystem.loadFromDisk()创建FSNamesystem对象。
// FSNamesystem.loadFromDisk()首先调用构造方法构造FSNamesystem对象,
// 然后将fsimage以及editlog文件加载到命名空间中。
loadNamesystem(conf);
//创建RPC服务/创建多个协议/创建serviceRpcServer和clientRpcServer
rpcServer = createRpcServer(conf);
//启动公共服务/资源检查/安全模式
startCommonServices(conf);
//启动计时器定期将NameNode度量写入日志文件。此行为可由配置禁用。
startMetricsLogger(conf);
}
重新回到initialize方法,进入startCommonServices(conf)
/** Start the services common to active and standby states */
private void startCommonServices(Configuration conf) throws IOException {
//FSNameSystem是管理HDFS的元数据的。里面涉及关于元数据的东西
namesystem.startCommonServices(conf, haContext);
registerNNSMXBean();
if (NamenodeRole.NAMENODE != role) {
startHttpServer(conf);
httpServer.setNameNodeAddress(getNameNodeAddress());
httpServer.setFSImage(getFSImage());
}
//启动服务
rpcServer.start();
try {
plugins = conf.getInstances(DFS_NAMENODE_PLUGINS_KEY,
ServicePlugin.class);
} catch (RuntimeException e) {
String pluginsValue = conf.get(DFS_NAMENODE_PLUGINS_KEY);
LOG.error("Unable to load NameNode plugins. Specified list of plugins: " +
pluginsValue, e);
throw e;
}
for (ServicePlugin p: plugins) {
try {
p.start(this);
} catch (Throwable t) {
LOG.warn("ServicePlugin " + p + " could not be started", t);
}
}
}
进入namesystem.startCommonServices(conf, haContext)
void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
//NameNode资源检查 通过core-site.xml hdfs-site.xml两个文件获取元数据存储位置
//需要检查三个目录,因为这三个目录都涉及到了元数据
//NameNode的两个目录:存储fsiamge的目录,存储editlog的目录。但是一般情况下,或者默认情况这两个使用的是同一个目录。
//Journlanode里面有也有存储元数据的目录。高可用的模式了
//获取到了要检查的目录
nnResourceChecker = new NameNodeResourceChecker(conf);
//检查是否有可用资源
checkAvailableResources();
//安全模式判断
//获取正常的block块数
long completeBlocksTotal = getCompleteBlocksTotal();
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
completeBlocksTotal);
//启动重要服务
blockManager.activate(conf, completeBlocksTotal);
}
进入到blockManager.activate(conf, completeBlocksTotal)
public void activate(Configuration conf, long blockTotal) {
//启动了等待复制的线程
pendingReconstruction.start();
//启动了管理 DataNode 的退役和维护状态线程和管理心跳的服务
datanodeManager.activate(conf);
this.redundancyThread.setName("RedundancyMonitor");
this.redundancyThread.start();
storageInfoDefragmenterThread.setName("StorageInfoMonitor");
storageInfoDefragmenterThread.start();
this.blockReportThread.start();
mxBeanName = MBeans.register("NameNode", "BlockStats", this);
bmSafeMode.activate(blockTotal);
}
回到startCommonServices,启动httpServer和rpcServer
至此,Namenode启动成功
安全模式
在NN启动过程中,还可能进入安全模式
非正常启动
例如命令行参数为recover,直接进入安全模式
正常启动
● NN会去检查目录资源,如果发现资源不足(默认100M),进入安全模式
//由于资源不足,nameNodeHasResourcesAvailable为false
boolean nameNodeHasResourcesAvailable() {
return hasResourcesAvailable;
}
//资源监控线程运行,检查资源,如果nameNodeHasResourcesAvailable为false,enterSafeMode(true)
class NameNodeResourceMonitor implements Runnable {
boolean shouldNNRmRun = true;
@Override
public void run () {
try {
while (fsRunning && shouldNNRmRun) {
checkAvailableResources();
if(!nameNodeHasResourcesAvailable()) {
String lowResourcesMsg = "NameNode low on available disk space. ";
if (!isInSafeMode()) {
LOG.warn(lowResourcesMsg + "Entering safe mode.");
} else {
LOG.warn(lowResourcesMsg + "Already in safe mode.");
}
enterSafeMode(true);
}
try {
Thread.sleep(resourceRecheckInterval);
} catch (InterruptedException ie) {
// Deliberately ignore
}
}
} catch (Exception e) {
FSNamesystem.LOG.error("Exception in NameNodeResourceMonitor: ", e);
}
}
public void stopMonitor() {
shouldNNRmRun = false;
}
}
● threshold != 0 && blockSafe < blockThreshold
block块上报 正常的块数目少于全部block数的阈值
● datanodeNum < datanodeThreshold
注册的DN数目小于设置的阈值数目,但由于datanodeThreshold默认设置为0,相当于没有启用
//进入bmSafeMode.activate(blockTotal),关于安全模式的判断
//当blockSafe >= blockThreshold && datanodeNum >= datanodeThreshold,areThresholdsMet才会为true
private boolean areThresholdsMet() {
assert namesystem.hasWriteLock();
// Calculating the number of live datanodes is time-consuming
// in large clusters. Skip it when datanodeThreshold is zero.
// We need to evaluate getNumLiveDataNodes only when
// (blockSafe >= blockThreshold) is true and hence moving evaluation
// of datanodeNum conditional to isBlockThresholdMet as well
synchronized (this) {
boolean isBlockThresholdMet = (blockSafe >= blockThreshold);
boolean isDatanodeThresholdMet = true;
if (isBlockThresholdMet && datanodeThreshold > 0) {
int datanodeNum = blockManager.getDatanodeManager().
getNumLiveDataNodes();
isDatanodeThresholdMet = (datanodeNum >= datanodeThreshold);
}
return isBlockThresholdMet && isDatanodeThresholdMet;
}
}
void activate(long total) {
//获取所有的block数
setBlockTotal(total);
//判断是否进入安全模式,如果areThresholdsMet为false,进入安全模式
if (areThresholdsMet()) {
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(),当正常块大于等于阈值且DN数目大于等于DN数阈值时候,退出安全模式
private boolean areThresholdsMet() {
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维护了文件与数据块的映射表以及数据块与数据节点的映射表,真正的数据是存储在DataNode上。
计算机中存储数据有两种:内存或磁盘
内存效率高但是危险性大
考虑到两者的优缺点,HDFS采用内存+磁盘的形式来存储元数据(数据的属性)。
Namenode通过维护镜像文件fsimage和editlog操作日志文件将元数据存储在磁盘上。
Namenode启动的时候会去指定目录加载文件,存储在内存中,方便接收rpc请求快速做出响应。
fsimage和editlog是什么?
fsimage相当于一个快照(checkpoint)保存了最新的元数据检查点,在HDFS启动时加载fsimage的信息,包含了整个HDFS文件系统的所有目录和文件的信息,editlog主要是在NameNode已经启动情况下对HDFS进行的各种更新操作进行记录,HDFS客户端执行所有的写操作都会被记录到editlog中。
为什么需要editlog文件呢?
因为随着HDFS上的文件越来越大,fsimage也会越来越大(GB级别),将HDFS操作记录到fsimage会导致系统运行的十分缓慢,成本太大。而为了防止editlog文件越来越大,需要将editlog和fsimage合并。
fsimge和editlog合并过程概述
Standalone模式下,Secondary NameNode(以下简称SNN,Namenode简称NN)通过周期性(五分钟)以GetEditLog方法获取editlog的大小,当满足一定条件时候,NN停止使用editlog,并临时生成edit.new文件(为了防止数据写入到edit文件中),SNN通过http获取了NN的fsimage和editlog文件,载入fsimage,按照editlog的记录执行操作,合并完成后发送请求告诉NN,NN拉取SNN的新fsimage文件,将edit.new文件更改为edit文件。
为什么要通过其他的Namenode去合并fsimage?
Namenode启动后主要用于接收rpc请求作出响应,为了降低Namedode压力,在Standalone模式下通过Secondary NameNode,HA模式下通过Standby Namenode
如何保证内存中元数据和磁盘元数据的一致性呢?
Namenode启动时加载磁盘上的fsimage文件到内存中,即内存中存储了文件和块的关系,在接受Datanode块上报后内存中存储了块和DN的关系,启动后对HDFS的各种更新操作都记录到editlog文件中(会写到磁盘上),在达到一定条件后(默认1个小时或者editlog达到100w条操作)会进行合并为新的fsimage(Standalone模式下通过Secondary NameNode,HA模式下通过Standby Namenode)
存储格式
fsimage:文件和块的关系
● Path 目录路径
● Replication 备份数
● ModificationTime 最后修改时间
● AccessTime 最后访问时间
● PreferredBlockSize 首选块大小 byte
● BlocksCount 块 数
● FileSize 文件大小 byte
● NSQUOTA 名称配额 限制指定目录下允许的文件和目录的数量。
● DSQUOTA 空间配额 限制该目录下允许的字节数
● Permission 权限
● UserName 用户
● GroupName 用户组
blocksMap:块和数据节点的映射关系
主要功能是保存block和其元数据的映射,block的元数据为它所在的INode和存储该block的DataNode。Block对象记录了blockid、block大小以及时间戳信息。block->DataNode的信息没有持久化存储,而是NameNode通过DataNode的块上报获取该block所在的DataNode List。block的元数据对应的类为BlocksMap#BlockInfo。
QJM
基于QJM的共享存储系统主要用于保存editlog,采用多个JournalNode的节点组成的JournalNode集群来存储editlog。每个JN保存同样的editlog副本。每次NN写editlog的时候,除了向本地磁盘写入editlog之外,也会并行地向JN集群之中的每一个JN发送写请求,只要大多数的JN节点返回成功就认为向JN集群写入editlog成功。如果有2N+1台JN,根据大多数的原则,最多可以容忍有N台JN节点挂掉。
ANN向本地磁盘写入editlog的时候也会向JN集群写入,当JN集群过半节点返回成功就认为写入成功,JN集群中有自增epoch num用于标识每次ANN的生命周期(NN切换一次,epoch +1),SNN定时从JN集群同步editlog文件(会出现SNN的editlog落后于ANN的editlog),SNN转换成ANN的时候需要将落后的editlog同步。如果ANN出现宕机情况,JN集群节点之间的editlog不一致,此时需要恢复成一致(根据epoch更新成大的数字,拒绝小数字的操作,这样原有ANN写入操作会失败,进程退出,实现隔离),SNN同步JN集群的editlog,切换为ANN。