2023-07-30
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/320

内存文件目录树,在一些开源的分布式存储系统中,有时也叫做Namespace或者元数据。它是NameNode节点的一种记录客户端文件操作命令的抽象数据结构,维护在NameNode的自身内存中。

在上一章《dfs客户端》中,我提到NameNodeRpcServer在接受到Client的文件操作RPC请求后,会委托给 FSNameSystem 处理:

    public class NameNodeServiceImpl extends NameNodeServiceGrpc.NameNodeServiceImplBase {
        // 负责管理元数据的核心组件
        private FSNameSystem namesystem;
    
        /**
         * 创建目录
         */
        @Override
        public void mkdir(MkDirRequest request, StreamObserver<MkDirResponse> responseObserver) {
            try {
                MkDirResponse response = null;
                if (!isRunning) {
                    response = MkDirResponse.newBuilder().setStatus(STATUS_SHUTDOWN).build();
                } else {
                    this.namesystem.mkdir(request.getPath());
                    response = MkDirResponse.newBuilder().setStatus(STATUS_SUCCESS).build();
                }
                responseObserver.onNext(response);
                responseObserver.onCompleted();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

FSNameSystem 就是NameNode进行元数据管理的核心组件,本章,我就来实现FSNameSystem。

一、元数据管理

FSNameSystem内部包含了两个核心组件:

  • FSDirectory:负责维护元数据,即内存文件目录树;
  • FSEditlog:负责管理edits log日志。
    /**
     * 负责管理元数据的核心组件
     */
    public class FSNameSystem {
    
        // 负责管理内存文件目录树的组件
        private FSDirectory directory;
    
        // 负责管理edits log写入磁盘的组件
        private FSEditlog editlog;
    
        // 最近一次checkpoint更新到的txid
        private long checkpointTxid = 0L;
    
        public FSNameSystem() {
            this.directory = new FSDirectory();
            this.editlog = new FSEditlog();
            recoverNamespace();
        }
    
        /**
         * 创建目录
         *
         * @param path 目录路径
         * @return 是否成功
         */
        public Boolean mkdir(String path) throws Exception {
            this.directory.mkdir(path);
            this.editlog.logEdit("{'OP':'MKDIR','PATH':'" + path + "'}");
            return true;
        }
    
        //...
    }

可以看到,FSNameSystem把对内存文件目录树的操作委托给了FSDirectory

关于FSEditlog组件和edits log日志,我会在后续章节详细讲解,本章我们只关注内存文件目录树。

1.1 数据结构

我们需要先考虑下内存文件目录树的结构是怎么样的?内存文件目录树,本质是一个父子层级关系的数据结构,比如:

    /root
      /usr
      /local
      /app
    /home
      /kafka
        /data
          /access.log

对目录/文件进行增删改,本质都是在更新该内存里的数据结构,所以我们定义一个INode类,代表文件目录树中的一个节点:

    /**
     * 代表文件目录树中的一个目录
     */
    public static class INode {
        private String path;
        private List<INode> children;
    
        public INode() {
        }
    
        public INode(String path) {
            this.path = path;
            this.children = new LinkedList<INode>();
        }
    
        public void addChild(INode inode) {
            this.children.add(inode);
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        public List<INode> getChildren() {
            return children;
        }
    
        public void setChildren(List<INode> children) {
            this.children = children;
        }
    
        @Override
        public String toString() {
            return "INode [path=" + path + ", children=" + children + "]";
        }
    }

1.2 FSDirectory

定义完文件目录树的基本结构后,我们还需要一个组件 FSDirectory 对文件和目录进行封装,并暴露一些对外的接口。我这里只实现了mkdir接口,你们可以根据业务需要实现各种其它维护文件树的接口:

    /**
     * 管理文件目录树的核心组件
     */
    public class FSDirectory {
    
        // 内存中的文件目录树
        private INode dirTree;
    
        public FSDirectory() {
            // 初始化时只有根节点
            this.dirTree = new INode("/");
        }
    
        /**
         * 创建目录
         *
         * @param path 目录路径, eg: path = /usr/warehouse/hive
         */
        public void mkdir(String path) {
            synchronized (dirTree) {
                // eg: /usr/warehouse/hive -> ["","usr","warehosue","hive"]
                String[] pathes = path.split("/");
                INode parent = dirTree;
    
                for (String splitedPath : pathes) {
                    if (splitedPath.trim().equals("")) {
                        continue;
                    }
    
                    INode dir = findDirectory(parent, splitedPath);
                    if (dir != null) {
                        parent = dir;
                        continue;
                    }
    
                    INode child = new INode(splitedPath);
                    parent.addChild(child);
                    parent = child;
                }
            }
            // printDirTree(dirTree, "");
        }
    
        /**
         * 查找子目录
         * <p>
         * 在dir目录下查找子目录path
         */
        private INode findDirectory(INode dir, String path) {
            if (dir.getChildren().size() == 0) {
                return null;
            }
    
            for (INode child : dir.getChildren()) {
                if (child instanceof INode) {
                    INode childDir = (INode) child;
                    if ((childDir.getPath().equals(path))) {
                        return childDir;
                    }
                }
            }
            return null;
        }
    
        public INode getDirTree() {
            return dirTree;
        }
    
        public void setDirTree(INode dirTree) {
            this.dirTree = dirTree;
        }
    }

上面需要特别注意的一点是,我使用了synchronized对内存文件目录树进行加锁,以防止并发修改时出现问题。

二、总结

本章,我对NameNode中的文件目录树结构进行了讲解,同时给出了一个比较简单的实现,很多分布式文件系统的实现思想都是类似的。下一章开始,我将对最核心的edits log日志进行讲解。

202307302136392531.png

阅读全文