第7章数据节点实现
7.1 数据块存储
第一次启动HDFS集群前,首先需要对名字节点进行格式化,从而使名字节点建立对应的文件结构
bin/hadoop namenode –format
数据节点第一次启动时创建存储目录,数据节点可以管理多个存储目录(配置项${dfs.data.dir})
${dfs.data.dir}一般有四个目录和两个文件分别为:
l blocksBeingWritten:保存当前正在写的数据块(写操作由客户端发起)
l current:保存已经写入HDFS文件系统的数据块,以及系统工作时需要的文件
l detach:配合数据节点升级,供数据块分离操作保存临时工作文件
l tmp:当前正在写的数据块
l storage:保存提示信息,版本检查
l in_use.lock保证只有一个数据节点独占该目录
配置项${dfs.datanode.numblocks}指定同一个目录下的文件和目录数目上限,比如当current目录下保存的数据块文件数目达到上限后,会创建子目录
current中保存的两种文件类型:
1) HDFS数据块:保存HDFS文件内容,如blok_249906531933748087
2) 使用meta后缀标识的校验信息文件,用于保存数据块的校验信息
current目录下的三个特殊文件
1) VERSION:java属性文件,包含运行的HDFS版本信息
2) dncp_block_verification.log.curr和dncp_block_verification.log.prev数据块扫描器工作的文件
7.2 数据节点存储的实现
数据节点的文件结构管理包括:数据(节点)存储DataStorage和文件系统数据集FSDataset
Storage->StorageInfo->DataStorage(组织Datanode磁盘数据)
Storage->StorageInfo->FSImage(组织Namenode磁盘数据)
Storage->StorageInfo->FSImage->CheckpointStorage(组织SecondaryNamenode磁盘数据)
StorageInfo中的三个属性layoutVersion,namespaceID.cTime都会保存在VERSION文件(另外两个属性storageID和storageType)中
Storage可管理多个目录,StorageDirectory存储目录
@InterfaceAudience.Private
public abstract class Storage extendsStorageInfo {
public static final StringSTORAGE_FILE_LOCK ="in_use.lock";
public static final StringSTORAGE_DIR_CURRENT ="current";
public static final StringSTORAGE_DIR_PREVIOUS ="previous";
public static final StringSTORAGE_TMP_REMOVED ="removed.tmp";
public static final StringSTORAGE_TMP_PREVIOUS ="previous.tmp";
public static final String STORAGE_TMP_FINALIZED ="finalized.tmp";
public static final StringSTORAGE_TMP_LAST_CKPT = "lastcheckpoint.tmp";
public static final StringSTORAGE_PREVIOUS_CKPT = "previous.checkpoint";
public static final String STORAGE_1_BBW ="blocksBeingWritten";
public enum StorageState {//存储结构(文件结构)状态
NON_EXISTENT,
NOT_FORMATTED,
COMPLETE_UPGRADE,
RECOVER_UPGRADE,
COMPLETE_FINALIZE,
COMPLETE_ROLLBACK,
RECOVER_ROLLBACK,
COMPLETE_CHECKPOINT,
RECOVER_CHECKPOINT,
NORMAL;
}
protected List
}
public static class StorageDirectoryimplements FormatConfirmable {
final File root; //root directory(根目录)
final boolean isShared; //是否共享
final StorageDirType dirType; // storage dir type(目录类型)
FileLock lock; // storage lock(独占锁)
public FilegetVersionFile() {//获得VERSION文件
return new File(newFile(root, STORAGE_DIR_CURRENT), STORAGE_FILE_VERSION);
}
}
@SuppressWarnings("resource")
FileLock tryLock() throws IOException {//对文件目录加锁
boolean deletionHookAdded = false;
File lockF = new File(root,STORAGE_FILE_LOCK);
if (!lockF.exists()) {
lockF.deleteOnExit();
deletionHookAdded = true;
}
RandomAccessFile file = newRandomAccessFile(lockF, "rws");
String jvmName =ManagementFactory.getRuntimeMXBean().getName();
FileLock res = null;
try {
res = file.getChannel().tryLock();
if (null == res) {
throw newOverlappingFileLockException();
}
file.write(jvmName.getBytes(Charsets.UTF_8));
LOG.info("Lock on " + lockF +" acquired by nodename " + jvmName);
}catch(OverlappingFileLockException oe) {
// Cannot read from the locked file onWindows.
String lockingJvmName = Path.WINDOWS ?"" : (" " + file.readLine());
LOG.error("It appears that anothernode " + lockingJvmName
+ " has already locked the storagedirectory: " + root, oe);
file.close();
return null;
} catch(IOException e) {
LOG.error("Failed to acquire lockon " + lockF
+ ". If this storage directoryis mounted via NFS, "
+ "ensure that the appropriatenfs lock services are running.", e);
file.close();
throw e;
}
if (!deletionHookAdded) {
// If the file existed prior to ourstartup, we didn't
// call deleteOnExit above. But sincewe successfully locked
// the dir, we can take care ofcleaning it up.
lockF.deleteOnExit();
}
return res;
}
@InterfaceAudience.Private
public classDataStorage extends Storage {
public final static StringBLOCK_SUBDIR_PREFIX = "subdir";
final static String COPY_FILE_PREFIX ="dncp_";
final static String STORAGE_DIR_DETACHED ="detach";
public final static String STORAGE_DIR_RBW ="rbw";
public final static String STORAGE_DIR_FINALIZED= "finalized";
public final static StringSTORAGE_DIR_LAZY_PERSIST = "lazypersist";
public final static String STORAGE_DIR_TMP ="tmp";
voidformat(StorageDirectory sd, NamespaceInfo nsInfo, String datanodeUuid) throwsIOException {//Datanode第一次启动时创建文件结构
sd.clearDirectory(); // create directory
this.layoutVersion =HdfsConstants.DATANODE_LAYOUT_VERSION;
this.clusterID = nsInfo.getClusterID();
this.namespaceID = nsInfo.getNamespaceID();
this.cTime = 0;
setDatanodeUuid(datanodeUuid);
createStorageID(sd, false);
writeProperties(sd);
}
7.3数据节点升级
1)升级(DataStorage.doUpgrade())
Ø 确保“current”目录存在
Ø 如果“previous”目录存在则删除该目录
Ø 确保“tmpDir”目录不存在
Ø “current”目录改名为“previous.tmp”
Ø 新创建的”current”目录下建立到“previous.tmp”目录中数据块和数据块校验信息文件的硬链接
Ø 在“current”目录下新建“VERSION”文件
Ø 将“previous.tmp”改名为“previous”目录
2)升级回滚(DataStorage.doRollback())
Ø 检查各个工作目录状态,即比较“previous”目录下的“VERSION”文件中的layoutVersion和cTime与回滚后的layoutVersion和cTime来判断(layoutVersion必须大于等于且cTime必须小于等于)
Ø 将“current”目录改名为“removed.tmp”
Ø 将“previous”目录改名为“current”
Ø 删除“removed.tmp”目录
3) 升级提交(DataStorage.doFinalize())
Ø “previous”目录改名为“finalized.tmp”
Ø 删除“finalized.tmp
public enum StorageState {//存储空间状态
NON_EXISTENT,//非FORMAT选项启动,目录不存在或者目录不可写或者配置项对应的路径是一个文件而非目录,都可认为存储空间不存在
NOT_FORMATTED,//以FORMAT选项启动,目录不存在
COMPLETE_UPGRADE,//“previous.tmp”目录存在,“current/VERSION”文件存在,可完成升级状态
RECOVER_UPGRADE,// “previous.tmp”目录存在,“current/VERSION”文件不存在,存储空间升级中恢复
COMPLETE_FINALIZE,//存在“finalized.tmp”目录,存储空间可继续提交升级
COMPLETE_ROLLBACK,//存在“previous.tmp”目录,同时“current/VERSION”文件存在,可继续提交回滚
RECOVER_ROLLBACK,// 存在“previous.tmp”目录,同时“current/VERSION”文件不存在,中断升级提交,恢复到上一个状态
COMPLETE_CHECKPOINT,//应用于名字节点,导入元数据检查点,存储空间完成检查点导入
RECOVER_CHECKPOINT,//应用于名字节点,用于导入元数据检查点,需要恢复导入检查点前的状态
NORMAL;//目录处于正常工作状态
}
public interfaceHdfsConstants{
static publicenum StartupOption{
FORMAT (“-format”)//格式化系统
REGULAR(“-regular”)//正常启动HDFS
UPGRADE(“-upgrade”)//升级系统
ROLLBACK(“-rollback”)//回滚到前一个版本
FINALIZE(“-finalize”)//提交一次升级
IMPORT(“-importCheckpoint”)//从名字节点的一个检查点恢复
}
}
Storage.analyzeStorage()//根据状态存在的条件,判断当前存储空间的状态
Storage.doRecover()//根据存储空间的状态执行相应的动作,恢复到NORMAL状态
7.4 文件系统数据集的工作机制
FSDatasetMBean(符合JMX管理框架)->FSDatasetInterface->FSDataset
JMX是SUN公司提出的一套管理框架,定义了完整的框架体系、设计模式、API接口、基于网络管理和监控服务
FSDatasetInterface接口方法分为:
1) 数据块相关:创建数据块、打开数据块上的输入、输出流、提交数据块
2) 数据块校验信息文件相关:维护数据块和校验信息文件关系,获取校验信息文件输入流等
3) 其他:包含FSDataset健康检查、关闭FSDataset的shutdown()
FSDataset的内部类
Ø FSDir:“current“目录的子目录,其成员变量children包含目录下的所有子目录,形成目录树
Ø FSVolume:对应于配置项${dfs.data.dir},存在一个或者多个FSVolume对象
Ø FSVolumeSet:管理多个FSVolume
FSDataset的成员变量
Ø volumes:管理所有的存储空间
Ø ongoingCreates:保存着当前正在进行写操作的数据块和对应文件的映射(Block->ActiveFile)
Ø volumeMap:保存已经提交的数据块和对应文件的映射(Block->DatanodeBlockInfo)
写数据块时,Datanode调用FSDatasetInterface.writeToBlock()
Ø 判断是否为有效数据块(isValidBlock()),对应数据块文件存在且数据块已提交,无论是追加操作还是错误恢复操作均调用detachBlock()方法进行必要的数据块分离操作,即去硬链接,对当前版本的修改不会影响到前一个版本
Ø 判断数据块是否处于写入过程(如果数据块保存在ongoingCreates中则表明其处于写入状态),即不能创建数据块,只能进行数据块恢复,remove()移除数据块与ActiveFile对象之间的关系
Ø 判断是否是数据块恢复
l 如果不是,则先分配FSVolume,创建临时文件,直接利用临时文件保存数据块
l 如果是且文件存在,则找到FSVolume之后,复用临时文件保存数据块
l 如果是且文件不存在(追加操作),重新打开数据块,如果是复制请求,则将临时文件创建在${dfs.data.dir}/tmp中,否则${dfs.data.dir}/blocksBeingWritten下
Ø 判断是否是数据块恢复,判断被恢复的数据块文件是否处于写入状态
l 如果是是,则继续使用该文件
l 如果是否,则重新打开数据块,打开数据块的相关信息会保存在volumeMap和ongoingCreates中,并创建包含打开文件和校验信息文件输出流的BlockWriteStreams对象
Datanode调用FSDatasetInterface.finalizeBlock(),将数据文件和校验信息文件移动到“current“的某个子目录下,更新volumeMap信息,删除ongoingCreates记录
Datanode调用FSDataset.unfinalizeBlock()删除复制不成功的数据块
Datanode调用FSDataset.invalidate()删除已经提交的数据块,采用异步磁盘操作服务降低响应时间
7.5 流式接口实现
DataNode.startDataNode():
Ø DataNode创建ServerSocket对象,绑定监听地址(${dfs.datanode.address},监听端口${dfs.datanode.port}
Ø 调用ServerSocket.setReceiveBufferSize()设置Socket接收缓存区大小
Ø 创建线程组,创建DataXceiverServer服务器,设置线程组中的线程为守护线程
Ø DataXceiverServer成员变量childSockets包含打开所有用于数据传输的Socket;成员变量maxXceiverCount由${dfs.datanode.max.xcievers}即支持的最多客户数
Ø DataXceiverServer.run()阻塞等待客户端连接,直到客户端请求到来,服务器创建新线程即DataXceiver对象(守护线程)
Ø 运行DataXceiver.run(),创建输入流进行版本检查,检查该请求是否超过数据节点的最大客户数,判断请求码从而决定读写操作
读数据
1) 准备和清理
读请求中包含的字段与请求帧一一对应
DataXceiver.readBlock();
Ø 根据socket创建输入流,根据请求帧信息创建BlockSender
Ø out.writeShort(DataTransferProtocol.OP_STATUS_SUCCESS)//发送成功应答
Ø blockSender.sendBlock(out,baseStream,null)//发送应答数据包
Ø blockScanner.verifiedByClient(block);//标记block客户端校验成功
2)“零拷贝”数据传输(java.nio.channels.FileChannel.transferTo)
BlockSender使用了transferTo(long position,long count,WritableByteChannel target)//将读取通道对象所在的文件中给定位置position处的count字节数据写入目标通道target中
3)数据发送
BlockSender.sendBlock();//发送数据包
Ø 发送应答头(数据校验类型,校验块大小和可选偏移量,以及与校验相关信息)
Ø 发送应答数据包
l packageLen(包长度包括数据长度字段、校验数据和数据块数据的长度)
l offset(偏移量):应答数据位于数据块中的起始位置
l seqno(顺序号):该数据包在应答中的顺序号
l tail(是否为最后一个应答包)
l length(数据长度):包中包含的实际数据长度
缓冲区大小pktSize默认=DataNode.PKT_HEADER_LEN+SIZE_OF_INTEGER
BUFFER_SIZE有配置项${io.file.buffer.size}
Ø BlockSender.sendChunks()计算并将应答包头部字段写入缓冲区以获得校验数据;将校验数据和数据块信息一起发送
4) 读写数据的速度控制(BlockTransferThrottler.throttle)能播啊正请求者发送/接收数据在某一个时间段的平均速度不超过制定阈值
7.6 写数据
1)准备与清理
写数据请求帧的内容可以在writeBlock()中获取,因为其一一对应于DataXceiver中的字段
Ø 构造数据块接收器对象BlockReceiver,打开数据块和校验信息文件的输出数据流
Ø DataXceiver.writeBlock()建立数据流管道,数据流管道中的每个Datanode都有两个Socket对象,一个与上游通信s(in,replyOut),一个与下游通信mirrorSock(mirrorIn,mirrorOut),在不断写的过程中数据目标列表大小和数据目标字段都会变化
Ø DataXceiver.writeBlock()通过mirrorIn读取返回码和附加信息,等待下游节点的应答
Ø DataXceiver委托BlockReceiver.receiveBlock()处理写数据的数据包,处理完这些数据包后,DataNode.notifyNamenodeReceivedBlock()通知Namenode
Ø DataXceiver.writeBlock()关闭上下游的输入/输出流,完成一次写数据请求
2) PacketResponder线程
用于从下游接收应答和从上游接收数据
PacketResponder线程从下游数据节点接收确认,并在合适的时候往上游发送
l 当前数据节点已顺利处理完该数据包
l 当前数据节点收到下游数据接待你的数据包确认
BlockReceiver.receivePacket()每处理完一个数据包,就通过PacketResponder.enqueue()将对应信息放入队列ackQueue(),处理过程在PacketResponder.run()完成即构造确认包PiplelineAck对象并通过输出流replyOut往上游节点发送
3) 数据接收
BlockReceiver.receiveBlock()接收大部分处理工作由BlockReceiver.receivePacket()完成
Ø 调用readNextPacket()读一个完整的数据包,并保存在ByteBuffer中
Ø 调用setBlockPosition()设置写数据的数据块位置和校验信息文件位置
Ø 数据包发送到下游数据节点
Ø 等待PacketResponder线程结束后
Ø 该节点将处理结果通知Namenode,关闭文件,更新数据块文件长度
Ø FSDataset.finalizeBlock()提交数据块
4) 处理结果上报名字节点(notifyNamenodeReceivedBlock()在请求队列中插入记录)
ReceivedBlockList:写操作的数据块信息
DelHints:保存删除提示信息
上述信息由Datanode服务线程独处,并形成DatanodeProtocol.blockReceived()的数组形式,远程调用通知Namenode
7.7 数据块替换、数据块拷贝、读数据块校验信息
DataXceiver.replaceBlock()
DataXceiver.copyBlock()
DataXceiver.getBlockChecksum()
7.8 数据块扫描器DataBlockScanner
周期性验证节点存储的数据块
辅助类:BlockScanInfo保存数据块扫描的基本信息
l block:数据块
l lastScanTime:最后一次扫描时间
l lastLogTime:最后一次写日志时间
l lastScanType:扫描类型
l lastScanOk:扫描结果
扫描类型包括:
NONE:还没执行扫描
REMOTE_READ:最后一次扫描结果由客户端读产生
VERIFICATION_SCAN:扫描结果有数据块扫描器产生
辅助类:LogEntry和LogFileHandler,平均每三周扫描一次结果保存在扫描器日志文件${dfs.data.dir}/current/dncp_block_verfication.log.curr/${dfs.data.dir}/current/dncp_block_verfication.log.prev
DataBlockScanner.verifyBlock()//对数据块进行校验
7.9数据节点的启停
DataNode.main()->secureMain()->createDataNode()创建并启动数据节点->instantiateDataNode()构造数据节点->startDataNode()启动数据节点主要完成下面工作
Ø 获取节点的名字和名字节点的地址,读取运行时需要的配置项
Ø 构造注册所需的DatanodeRegistration对象
Ø 建立到数据节点的IPC连接并握手
Ø 执行可能的存储空间恢复,构造数据节点使用的FSDataset
Ø 创建流接口服务器DataXceiverServer
Ø 创建数据块扫描器DataBlockScanner
Ø 创建数据节点上的HTTP服务器
Ø 创建IPC服务器
数据节点停止操作:
Ø 关闭HTTP服务器和IPC服务器
Ø 设置shouldRun=false
Ø 调用DataXceiverServer.kill()强行关闭套接字
Ø 关闭数据节点->名字节点的IPC客户端,关闭数据块扫描器
Ø 对in_use.lock文件解锁并删除
Ø 中断数据节点服务线程,并关闭FSDataset对象