前言
在linux文件系统中,i-node节点一直是一个非常重要的设计,同样在HDFS中,也存在这样的一个类似的角色,不过他是一个全新的类,INode.class,后面的目录类等等都是他的子类。最近学习了部分HDFS的源码结构,就好好理一理这方面的知识,帮助大家更好的从深层次了解Hadoop分布式系统文件。
HDFS文件相关的类设计
在HDFS中与文件相关的类主要有这么几个
1.INode--这个就是最底层的一个类,抽象类,提炼一些文件目录共有的属性。
2.INodeFile--文件节点类,继承自INode,表示一个文件
3.INodeDirectory--文件目录类,也是继承自INode.他的孩子中是文件集也可能是目录
4.INodeDirectoryWithQuota--有配额限制的目录,这是为了适应HDFS中的配额策略。
5.INodeFileUnderConstruction--处于构建状态的文件类,可以从INodeFile中转化而来。
我大体上分为了以上这么几个类,后续将从上述类中挑出部分代码,来了解作者的巧妙的设计思想。
INode
INode基础抽象类,保存了文件,目录都可能会共有的基本属性,如下
[java] view plain copy print ?
1. /**
2. *Wekeepanin-memoryrepresentationofthefile/blockhierarchy.
3. *ThisisabaseINodeclasscontainingcommonfieldsforfileand
4. *directoryinodes.
5. */
6. abstractclassINodeimplementsComparable<byte[]>{
7. //文件/目录名称
8. protectedbyte[]name;
9. //父目录
10. protectedINodeDirectoryparent;
11. //最近一次的修改时间
12. protectedlongmodificationTime;
13. //最近访问时间
14. protectedlongaccessTime;
INode作为基础类,在权限控制的设计方面采用了64位的存储方式,前16位保留访问权限设置,中间16~40位保留所属用户组标识符,41~63位保留所属用户标识符。如下
[java] view plain copy print ?
1. //OnlyupdatedbyupdatePermissionStatus(...).
2. //Othercodesshouldnotmodifyit.
3. privatelongpermission;
4.
5. //使用long整数的64位保存,分3段保存,分别为mode模式控制访问权限,所属组,所属用户
6. privatestaticenumPermissionStatusFormat{
7. MODE(0,16),
8. GROUP(MODE.OFFSET+MODE.LENGTH,25),
9. USER(GROUP.OFFSET+GROUP.LENGTH,23);
10.
11. finalintOFFSET;
12. finalintLENGTH;//bitlength
13. finallongMASK;
14.
15. PermissionStatusFormat(intoffset,intlength){
16. OFFSET=offset;
17. LENGTH=length;
18. MASK=((-1L)>>>(64-LENGTH))<<OFFSET;
19. }
20.
21. //与掩码计算并右移得到用户标识符
22. longretrieve(longrecord){
23. return(record&MASK)>>>OFFSET;
24. }
25.
26. longcombine(longbits,longrecord){
27. return(record&~MASK)|(bits<<OFFSET);
28. }
29. }
要获取这些值,需要与内部的掩码值计算并作左右移操作。在这里存储标识符的好处是比纯粹的字符串省了很多的内存,那么HDFS是如何通过标识符数字获取用户组或用户名的呢,答案在下面
[java] view plain copy print ?
1. /**Getusername*/
2. publicStringgetUserName(){
3. intn=(int)PermissionStatusFormat.USER.retrieve(permission);
4. //根据整形标识符,SerialNumberManager对象中取出,避免存储字符串消耗大量内存
5. returnSerialNumberManager.INSTANCE.getUser(n);
6. }
7. /**Setuser*/
8. protectedvoidsetUser(Stringuser){
9. intn=SerialNumberManager.INSTANCE.getUserSerialNumber(user);
10. updatePermissionStatus(PermissionStatusFormat.USER,n);
11. }
12. /**Getgroupname*/
13. publicStringgetGroupName(){
14. intn=(int)PermissionStatusFormat.GROUP.retrieve(permission);
15. returnSerialNumberManager.INSTANCE.getGroup(n);
16. }
17. /**Setgroup*/
18. protectedvoidsetGroup(Stringgroup){
19. intn=SerialNumberManager.INSTANCE.getGroupSerialNumber(group);
20. updatePermissionStatus(PermissionStatusFormat.GROUP,n);
21. }
就是在同一的SerialNumberManager对象中去获取的。还有在这里定义了统一判断根目录的方法,通过判断名称长度是否为0
[java] view plain copy print ?
1. /**
2. *Checkwhetherthisistherootinode.
3. *根节点的判断标准是名字长度为0
4. */
5. booleanisRoot(){
6. returnname.length==0;
7. }
在INode的方法中还有一个个人感觉比较特别的设计就是对于空间使用的计数,这里的空间不单单指的是磁盘空间大小这么一个,他还包括了name space命名空间,这都是为了后面的HDFS的配额机制准备的变量。详情请看
[java] view plain copy print ?
1. /**Simplewrapperfortwocounters:
2. *nsCount(namespaceconsumed)anddsCount(diskspaceconsumed).
3. */
4. staticclassDirCounts{
5. longnsCount=0;
6. longdsCount=0;
7.
8. /**returnsnamespacecount*/
9. longgetNsCount(){
10. returnnsCount;
11. }
12. /**returnsdiskspacecount*/
13. longgetDsCount(){
14. returndsCount;
15. }
16. }
至于怎么使用的,在后面的INodeDirectory中会提及。由于篇幅有限,在INode里还有许多的方法,比如移除自身方法,设计的都挺不错的。
[java] view plain copy print ?
1. //移除自身节点方法
2. booleanremoveNode(){
3. if(parent==null){
4. returnfalse;
5. }else{
6.
7. parent.removeChild(this);
8. parent=null;
9. returntrue;
10. }
11. }
.
INodeDirectory
下面来分析分析目录节点类,说到目录,当然一定会有的就是孩子节点了,下面是目录类的成员变量:
[java] view plain copy print ?
1. /**
2. *DirectoryINodeclass.
3. */
4. classINodeDirectoryextendsINode{
5. protectedstaticfinalintDEFAULT_FILES_PER_DIRECTORY=5;
6. finalstaticStringROOT_NAME="";
7.
8. //保存子目录或子文件
9. privateList<INode>children;
知道为什么判断root的依据是length==0?了吧,因为ROOT_NAME本身就被定义为空字符串了。在INodeDirectory中,普通的移除节点的方法如下,采用的是二分搜索的办法
[java] view plain copy print ?
1. //移除节点方法
2. INoderemoveChild(INodenode){
3. assertchildren!=null;
4. //用二分法寻找文件节点
5. intlow=Collections.binarySearch(children,node.name);
6. if(low>=0){
7. returnchildren.remove(low);
8. }else{
9. returnnull;
10. }
11. }
如果是我,我估计马上联想到的是遍历搜寻,设计理念确实不同。添加一个孩子的方法,与此类似,不过要多做一步的验证操作
[java] view plain copy print ?
1. /**
2. *Addachildinodetothedirectory.
3. *
4. *@paramnodeINodetoinsert
5. *@paraminheritPermissioninheritpermissionfromparent?
6. *@returnnullifthechildwiththisnamealreadyexists;
7. *node,otherwise
8. */
9. <TextendsINode>TaddChild(finalTnode,booleaninheritPermission){
10. if(inheritPermission){
11. FsPermissionp=getFsPermission();
12. //makesurethepermissionhaswxfortheuser
13. //判断用户是否有写权限
14. if(!p.getUserAction().implies(FsAction.WRITE_EXECUTE)){
15. p=newFsPermission(p.getUserAction().or(FsAction.WRITE_EXECUTE),
16. p.getGroupAction(),p.getOtherAction());
17. }
18. node.setPermission(p);
19. }
20.
21. if(children==null){
22. children=newArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
23. }
24. //二分查找
25. intlow=Collections.binarySearch(children,node.name);
26. if(low>=0)
27. returnnull;
28. node.parent=this;
29. //在孩子列表中进行添加
30. children.add(-low-1,node);
31. //updatemodificationtimeoftheparentdirectory
32. setModificationTime(node.getModificationTime());
33. if(node.getGroupName()==null){
34. node.setGroup(getGroupName());
35. }
36. returnnode;
37. }
目录的删除一般都是以递归的方式执行,同样在这里也是如此
[java] view plain copy print ?
1. //递归删除文件目录下的所有block块
2. intcollectSubtreeBlocksAndClear(List<Block>v){
3. inttotal=1;
4. //直到是空目录的情况,才直接返回
5. if(children==null){
6. returntotal;
7. }
8. for(INodechild:children){
9. //递归删除
10. total+=child.collectSubtreeBlocksAndClear(v);
11. }
12.
13. //删除完毕之后,置为空操作,并返回文件数计数结果
14. parent=null;
15. children=null;
16. returntotal;
17. }
递归调用的是子类的同名方法。下面的方法是与上面提到的某个变量有直接关系的方法,配额限制相关,不过这个是命名空间的计数,一个目录算1个,1个新的文件又算一个命名空间,你可以理解为就是文件,目录总数的限制,但是在INodeDirectory中是受限的,受限制的类叫做INodeDirectoryWithQuota,也是将要介绍的类。
[java] view plain copy print ?
1. /**{@inheritDoc}*/
2. DirCountsspaceConsumedInTree(DirCountscounts){
3. counts.nsCount+=1;
4. if(children!=null){
5. for(INodechild:children){
6. child.spaceConsumedInTree(counts);
7. }
8. }
9. returncounts;
10. }
INodeDirectoryWithQuota
与上个类相比就多了Quota这个单词,Quota在英文中的意思就是”配额“,有容量限制,在这里的容量限制有2个维度,第一个命名空间限制,磁盘空间占用限制,前者避免你创建过多的目录文件,后者避免你占用过大的空间。变量定义如下,他是继承自目录类的
[java] view plain copy print ?
1. /**
2. *DirectoryINodeclassthathasaquotarestriction
3. *存在配额限制的目录节点,继承自目录节点
4. */
5. classINodeDirectoryWithQuotaextendsINodeDirectory{
6. //命名空间配额
7. privatelongnsQuota;///NameSpacequota
8. //名字空间计数
9. privatelongnsCount;
10. //磁盘空间配额
11. privatelongdsQuota;///diskspacequota
12. //磁盘空间占用大小
13. privatelongdiskspace;
一般此类都可以通过非配额类以参数的形式构造而来,如下
[java] view plain copy print ?
1. /**Convertanexistingdirectoryinodetoonewiththegivenquota
2. *给定目录,通过传入配额限制将之转为配额目录
3. *@paramnsQuotaNamespacequotatobeassignedtothisinode
4. *@paramdsQuotaDiskspacequotatobeassignedtothisindoe
5. *@paramotherTheotherinodefromwhichallotherpropertiesarecopied
6. */
7. INodeDirectoryWithQuota(longnsQuota,longdsQuota,INodeDirectoryother)
8. throwsQuotaExceededException{
9. super(other);
10. INode.DirCountscounts=newINode.DirCounts();
11. other.spaceConsumedInTree(counts);
12. this.nsCount=counts.getNsCount();
13. this.diskspace=counts.getDsCount();
14. setQuota(nsQuota,dsQuota);
15. }
限制的关键方法如下,如果超出规定的配额值,则会抛异常
[java] view plain copy print ?
1. /**Verifyifthenamespacecountdiskspacesatisfiesthequotarestriction
2. *给定一定的误差限制,验证命名空间计数和磁盘空间是否使用超出相应的配额限制,超出则抛异常
3. *@throwsQuotaExceededExceptionifthegivenquotaislessthanthecount
4. */
5. voidverifyQuota(longnsDelta,longdsDelta)throwsQuotaExceededException{
6. //根据误差值计算新的计数值
7. longnewCount=nsCount+nsDelta;
8. longnewDiskspace=diskspace+dsDelta;
9. if(nsDelta>0||dsDelta>0){
10. //判断新的值是否超出配额的值大小
11. if(nsQuota>=0&&nsQuota<newCount){
12. thrownewNSQuotaExceededException(nsQuota,newCount);
13. }
14. if(dsQuota>=0&&dsQuota<newDiskspace){
15. thrownewDSQuotaExceededException(dsQuota,newDiskspace);
16. }
17. }
18. }
INodeFile
与目录相对应的类就是文件类,在HDFS中文件对应的就是许多个block块嘛,所以比如会有block列表组,当然他也可能会定义每个block的副本数,HDFS中默认是3,
[java] view plain copy print ?
1. classINodeFileextendsINode{
2. staticfinalFsPermissionUMASK=FsPermission.createImmutable((short)0111);
3.
4. //NumberofbitsforBlocksize
5. //48位存储block数据块的大小
6. staticfinalshortBLOCKBITS=48;
7.
8. //Headermask64-bitrepresentation
9. //Format:[16bitsforreplication][48bitsforPreferredBlockSize]
10. //前16位保存副本系数,后48位保存优先块大小,下面的headermask做计算时用
11. staticfinallongHEADERMASK=0xffffL<<BLOCKBITS;
12.
13. protectedlongheader;
14. //文件数据block块
15. protectedBlockInfoblocks[]=null;
仔细观察,在这里设计者又用长整型变量保存属性值,这里是用前16位保存副本系数后48位保留块大小,对于这个
PreferredBlockSize我个人结合后面的代码,猜测就是我们在hdfs-site.xml文件中设置的块大小值,默认64M.在这里的Block信息就保存在了BlockInfo.如何利用header来求得副本系数呢,在这里给出的办法还是做位运算然后位移操作:
[java] view plain copy print ?
1. /**
2. *Getblockreplicationforthefile
3. *@returnblockreplicationvalue
4. *得到副本系数通过与掩码计算并右移48位
5. */
6. publicshortgetReplication(){
7. return(short)((header&HEADERMASK)>>BLOCKBITS);
8. }
空间计数的计算如下,注意这里的磁盘空间占用计算
[java] view plain copy print ?
1. @Override
2. DirCountsspaceConsumedInTree(DirCountscounts){
3. //命名空间消耗加1
4. counts.nsCount+=1;
5. //累加磁盘空间消耗大小
6. counts.dsCount+=diskspaceConsumed();
7. returncounts;
8. }
因为是单个文件,命名空间只递增1,对于每个block块的大小计算,可不是简单的block.size这么简单,还要考虑副本情况和最后的文件块正在被写入的情况
[java] view plain copy print ?
1. //计算磁盘空间消耗的大小
2. longdiskspaceConsumed(Block[]blkArr){
3. longsize=0;
4. for(Blockblk:blkArr){
5. if(blk!=null){
6. size+=blk.getNumBytes();
7. }
8. }
9. /*Ifthelastblockisbeingwrittento,useprefferedBlockSize
10. *ratherthantheactualblocksize.
11. *如果最后一个块正在被写,用内部设置的prefferedBlockSize的值做替换
12. */
13. if(blkArr.length>0&&blkArr[blkArr.length-1]!=null&&
14. isUnderConstruction()){
15. size+=getPreferredBlockSize()-blocks[blocks.length-1].getNumBytes();
16. }
17.
18. //每个块乘以相应的副本数
19. returnsize*getReplication();
20. }
在block的操作中,一般都是针对最后一个block块的获取,移除操作
[java] view plain copy print ?
1. /**
2. *Returnthelastblockinthisfile,ornulliftherearenoblocks.
3. *获取文件的最后一个block块
4. */
5. BlockgetLastBlock(){
6. if(this.blocks==null||this.blocks.length==0)
7. returnnull;
8. returnthis.blocks[this.blocks.length-1];
9. }
[java] view plain copy print ?
1. /**
2. *addablocktotheblocklist
3. *往block列表中添加block块
4. */
5. voidaddBlock(BlockInfonewblock){
6. if(this.blocks==null){
7. this.blocks=newBlockInfo[1];
8. this.blocks[0]=newblock;
9. }else{
10. intsize=this.blocks.length;
11. BlockInfo[]newlist=newBlockInfo[size+1];
12. System.arraycopy(this.blocks,0,newlist,0,size);
13. newlist[size]=newblock;
14. this.blocks=newlist;
15. }
16. }
INodeFileUnderConstruction
这个类的名字有点长,他的意思是处于构建状态的文件类,是INodeFile的子类,就是当文件被操作的时候,就转变为此类的形式了,与block读写操作的关系比较密切些。变量定义如下
[java] view plain copy print ?
1. //处于构建状态的文件节点
2. classINodeFileUnderConstructionextendsINodeFile{
3. //写文件的客户端名称,也是这个租约的持有者
4. StringclientName;//leaseholder
5. //客户端所在的主机
6. privatefinalStringclientMachine;
7. //如果客户端同样存在于集群中,则记录所在的节点
8. privatefinalDatanodeDescriptorclientNode;//ifclientisaclusternodetoo.
9.
10. //租约恢复时的节点
11. privateintprimaryNodeIndex=-1;//thenodeworkingonleaserecovery
12. //最后一个block块所处的节点组,又名数据流管道成员
13. privateDatanodeDescriptor[]targets=null;//locationsforlastblock
14. //最近租约恢复时间
15. privatelonglastRecoveryTime=0;
对于处于构建状态的节点来说,他的操作也是往最后一个block添加数据,设计在这里还保留了最后一个块的所在的数据节点列表。相关方法
[java] view plain copy print ?
1. //设置新的block块,并且为最后的块赋值新的targes节点
2. synchronizedvoidsetLastBlock(BlockInfonewblock,DatanodeDescriptor[]newtargets
3. )throwsIOException{
4. if(blocks==null||blocks.length==0){
5. thrownewIOException("Tryingtoupdatenon-existantblock(newblock="
6. +newblock+")");
7. }
8. BlockInfooldLast=blocks[blocks.length-1];
9. if(oldLast.getBlockId()!=newblock.getBlockId()){
10. //Thisshouldnothappen-thismeansthatwe'reperformingrecovery
11. //onaninternalblockinthefile!
12. NameNode.stateChangeLog.error(
13. "Tryingtocommitblocksynchronizationforaninternalblockon"
14. +"inode="+this
15. +"newblock="+newblock+"oldLast="+oldLast);
16. thrownewIOException("Tryingtoupdateaninternalblockof"+
17. "pendingfile"+this);
18. }
19.
20. //如果新的block时间比老block的还小的话,则进行警告
21. if(oldLast.getGenerationStamp()>newblock.getGenerationStamp()){
22. NameNode.stateChangeLog.warn(
23. "Updatinglastblock"+oldLast+"ofinode"+
24. "underconstruction"+this+"withablockthat"+
25. "hasanoldergenerationstamp:"+newblock);
26. }
27.
28. blocks[blocks.length-1]=newblock;
29. setTargets(newtargets);
30. //重置租约恢复时间,这样操作的话,下次租约检测时将会过期
31. lastRecoveryTime=0;
32. }
移除块操作也是移除最后一个block,数据节点列表也将被清空
[java] view plain copy print ?
1. /**
2. *removeablockfromtheblocklist.Thisblockshouldbe
3. *thelastoneonthelist.
4. *在文件所拥有的block列表中移动掉block,这个block块应该是最后一个block块
5. */
6. voidremoveBlock(Blockoldblock)throwsIOException{
7. if(blocks==null){
8. thrownewIOException("Tryingtodeletenon-existantblock"+oldblock);
9. }
10. intsize_1=blocks.length-1;
11. if(!blocks[size_1].equals(oldblock)){
12. //如果不是最末尾一个块则将会抛异常
13. thrownewIOException("Tryingtodeletenon-lastblock"+oldblock);
14. }
15.
16. //copytoanewlist
17. BlockInfo[]newlist=newBlockInfo[size_1];
18. System.arraycopy(blocks,0,newlist,0,size_1);
19. blocks=newlist;
20.
21. //Removetheblocklocationsforthelastblock.
22. //最后一个数据块所对应的节点组就被置为空了
23. targets=null;
24. }
另外判断租约过期的方法如下
[java] view plain copy print ?
1. /**
2. *UpdatelastRecoveryTimeifexpired.
3. *@returntrueiflastRecoveryTimeisupdated.
4. *设置最近的恢复时间
5. */
6. synchronizedbooleansetLastRecoveryTime(longnow){
7. booleanexpired=now-lastRecoveryTime>NameNode.LEASE_RECOVER_PERIOD;
8. if(expired){
9. //如果过期了则设置为传入的当前时间
10. lastRecoveryTime=now;
11. }
12. returnexpired;
13. }
总结
HDFS中的INode文件相关源码分析就是上述所说的了,上面只是部分我的代码分析,全部代码链接如下
https://github.com/linyiqun/hadoop-hdfs,后续将会继续更新HDFS其他方面的代码分析。
参考文献
《Hadoop技术内部–HDFS结构设计与实现原理》.蔡斌等