从0到1编写分布式文件系统——Master节点之元数据及镜像机制
在上两篇文章从0到1编写分布式文件系统——Master之通信协议和从0到1编写分布式文件系统——Master节点之通络通信机制中分别介绍了Master的通信协议和网络机制内容,本文继续对元数据结构进行阐述。
目录结构
Master主要的元数据是整个分布式文件系统的目录树,而目录中的子节点无外乎是两种对象:目录或文件。同时,目录或文件都会有共同的属性,即权限。于是,可以先定义目录和文件共同的抽象类IDirectory,并定义了两个共同的属性,name(节点名称)和inode(权限属性)。
public abstract class IDirectory implements Serializable {
public static String SEPARATOR = "/";
public static String ROOT_PARENT = "ROOTS";
/**
* 节点名称
*/
private String name;
/**
* inode 权限属性
*/
private INode iNode;
public IDirectory() {
}
}
INode类中定义了读写可执行权限、用户、用户组、大小以及时间等属性,参照了linux文件中inode的属性,如下图所示。
在IDirectory的目录子类中Directory中,只有一个属性childrenList,表示在这个目录节点下面所属的目录或文件子对象。
public class Directory extends IDirectory {
private transient List<IDirectory> childrenList;
}
在IDirectory的目录子类中File中,只有一个属性fileBlockList,表示在这个文件节点下面所属的文件块。因为文件节点下面不可能在挂在目录,就是一个单独的文件,fileBlockLis表示对这个文件中分割每个文件块的集合。
public class File extends IDirectory {
private List<FileBlock> fileBlockList;
}
整个元数据目录树的类结构如下图所示。
快照
目前,元数据是存储在内存中的,一旦程序退出则所有数据都会丢失,所以需要对整个目录树做个磁盘快照,那如何将整个目录树序列化存储到磁盘中,同时又支持随机检索以便更新呢?Java中RandomAccessFile类支持随机读写,因此可以使用RandomAccessFile来做镜像存储的底层文件对象。同时,要记录每个目录在文件的索引位置,因此,还需要额外的信息记录每个IDirectory对象的索引信息。
先定义SnapshotHeader,该类中主要有4个属性,index、isDeleted、isDirectory以及size。其中,index只是该对象序列化后在文件的存储的初始位置,方便后续读取更新操作。在每个IDirectory对象中添加SnapshotHeader属性。
public class SnapshotHeader {
/**
* 索引位置
*/
private long index;
/**
* 1 是删除
*/
private byte isDeleted = 0;
/**
* 0 is directory
* 1 is file
*/
private byte isDirectory;
private int size;
...
}
存储的格式如下图所示。
定义一个快照的通用接口,该接口主要有两个方法,write写目录元数据以及read读取快照内容恢复目录树。
public interface Snapshot {
void write(IDirectory directory);
IDirectory read();
...
}
DefaultSnapshot类实现Snapshot接口,write和read的实现如下所示,先查看写入的流程:
- 写入的时候,先根据IDirectory的SnapshotHeader是否为null,不为null则表示已存在于镜像文件中,需要先将之前存储的SnapshotHeader中isDeleted属性标记为1,表示原先存储的信息废弃。
- 生成新的SnapshotHeader对象,设置该IDirectory对象的一些镜像文件属性
- 在文件尾部写入IDirectory对象的镜像存储信息和主要内容
写入代码如下所示。
public class DefaultSnapshot implements Snapshot {
...
@Override
public void write(IDirectory directory) {
SnapshotHeader header = directory.getHeader();
if (header != null){
long index = header.getIndex();
try {
directoryFile.seek(index + 4);
directoryFile.writeByte(1);
} catch (IOException e) {
e.printStackTrace();
}
}
byte[] b = directory.serialize(serializer);
header = new SnapshotHeader();
header.setSize(b.length);
header.setIsDirectory((byte)(directory.isDirectory() ? 1 : 0));
try {
long last = directoryFile.length();
directoryFile.seek(last);
header.setIndex(last);
directoryFile.writeLong(header.getIndex());
directoryFile.writeByte(header.getIsDirectory());
directoryFile.writeByte(header.getIsDeleted());
directoryFile.writeInt(header.getSize());
directoryFile.write(b);
directory.setHeader(header);
} catch (IOException e) {
e.printStackTrace();
}
}
...
}
接下来看看读取的流程:
- 遍历整个镜像文件,依次读取SnapshotHeader属性的字节内容,过滤掉标记为已删除的目录信息,根据size读取整个IDirectory的字节内容,反序列化为IDirectory对象。 这里有个问题,读取到的是一个个独立的IDirectory对象,如何初始化为整棵目录树呢,如何得知IDirectory对象的父节点以及子节点呢?在IDirectory加入多两个属性,id和parentId,id是该目录的唯一标识符。
- 如何保证ID唯一?因为目录节点的全路径在整个目录中是独一无二的,获取其全路径的MD5编码作为该节点的ID,即可保证唯一性。 之后就可以根据这两个属性初始化整棵目录树,读取的代码如下所示,依次读取,然后存储到Map集合中,遍历Map集合设置目录节点的子节点,便可以完成目录树的初始化。
@Override
public IDirectory read() {
IDirectory root = null;
Map<String, IDirectory> cache = new HashMap<>();
long index;
byte isDirectory;
byte isDeleted;
int size;
try {
index = directoryFile.readLong();
while (index < directoryFile.length()){
isDirectory = directoryFile.readByte();
isDeleted = directoryFile.readByte();
size = directoryFile.readInt();
byte[] b = new byte[size];
int i = directoryFile.read(b);
IDirectory d = serializer.deserialize(b, IDirectory.class);
SnapshotHeader header = new SnapshotHeader();
header.setIndex(index);
header.setSize(size);
header.setIsDeleted(isDeleted);
header.setIsDirectory(isDirectory);
d.setHeader(header);
cache.put(d.getId(), d);
if ((directoryFile.getFilePointer() + 64) < directoryFile.length()){
index = directoryFile.readLong();
}else{
break;
}
}
for (Map.Entry<String, IDirectory> entry : cache.entrySet()){
IDirectory d = entry.getValue();
if (!d.getParentId().equals(IDirectory.ROOT_PARENT)){
IDirectory p = cache.get(d.getParentId());
p.addChildDirectory(d);
}else {
root = d;
}
}
cache.clear();
} catch (IOException e) {
e.printStackTrace();
}
return root;
}
其他说明
上篇文章从0到1编写分布式文件系统——Master节点之通络通信机制
详细的代码:Simple Distributed File System