从0到1编写分布式文件系统——Master节点之元数据及镜像机制

2,220 阅读4分钟

从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的属性,如下图所示。 image.png   在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;
}

  整个元数据目录树的类结构如下图所示。 image.png

快照

  目前,元数据是存储在内存中的,一旦程序退出则所有数据都会丢失,所以需要对整个目录树做个磁盘快照,那如何将整个目录树序列化存储到磁盘中,同时又支持随机检索以便更新呢?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;

    ...
}

存储的格式如下图所示。

image.png   定义一个快照的通用接口,该接口主要有两个方法,write写目录元数据以及read读取快照内容恢复目录树。

public interface Snapshot {
    void write(IDirectory directory);
    IDirectory read();
    ...
}

  DefaultSnapshot类实现Snapshot接口,write和read的实现如下所示,先查看写入的流程:

  1. 写入的时候,先根据IDirectory的SnapshotHeader是否为null,不为null则表示已存在于镜像文件中,需要先将之前存储的SnapshotHeader中isDeleted属性标记为1,表示原先存储的信息废弃。
  2. 生成新的SnapshotHeader对象,设置该IDirectory对象的一些镜像文件属性
  3. 在文件尾部写入IDirectory对象的镜像存储信息和主要内容

image.png 写入代码如下所示。

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