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

本章,我还是和之前的其它专栏一样,先从整体架构上讲解我们自研的分布式文件系统,后续再逐一拆分讲解每个核心组件,即整体的思路依然延续 “自顶向下”的架构讲解结合"自底向上"的模块实现

通过本章的学习,你将对该分布式文件系统的整体架构有一个清晰的认识,同时我会对落地分布式文件系统的重要技术点在本章进行概要讲解。

注意:本系列我讲解的分布式文件系统只适用于大量小文件的分布式存储,并不是HDFS那种单个大文件的拆分存储,在实现上大量借鉴了Hadoop的设计,事实上,很多开源框架在数据存储这一块的设计思想都是类似的。

一、整体架构

整个分布式文件系统,我将它划分为四个部分:

  • 客户端:客户端集成在使用分布式文件系统的应用内,类似于Kafka-Client;
  • 数据节点:DataNode负责数据文件的实际存储,每个数据节点都是对等的;
  • 控制节点:NameNode通过心跳感知各个数据节点的状态,同时负责接受客户端的读/写请求,管理集群元数据信息;
  • 备份节点:BackupNode从控制节点同步元数据,并定期生成内存快照fsimage,可以看成是NameNode的备份。

节点之间的通信使用gRPC这个开源框架实现:

202307302134087201.png

1.1 NameNode

NameNode可以看成是控制节点,是整个分布式文件系统的大脑。它主要负责以下工作:

  1. 接受客户端的文件/目录操作命令,然后在内存中维护 文件目录树
  2. 记录客户端的操作日志edits log,并将日志刷到磁盘上,防止宕机导致内存文件目录树丢失;
  3. 接受BackupNode的日志拉取请求,返回edits log日志;
  4. 接受BackupNode备份节点发送过来的fsimage快照,并基于fsimage快速恢复内存文件目录树,然后将checkpoint时间点之后的editslog进行回放,恢复剩余的目录树数据。

1.2 BackupNode

BackupNode属于NameNode的备份节点,主要是为了保障NameNode的高可用。它主要负责以下工作:

  1. 定期从NameNode节点拉取Edits log日志,并进行日志回放,生成内存文件目录树;
  2. 基于内存文件目录树,定期生成内存快照文件fsimage,并将文件发送给NameNode。

1.3 DataNode

DataNode可以看成是实际的数据存储节点,DataNode之间是对等的关系。它们主要执行以下工作:

  1. DataNode启动后,需要向NameNode注册自己,NameNode会在内存维护DataNode列表,这样接收到客户端命令后就可以选择实际存活的DataNode作为响应;
  2. DataNode会定期发送心跳给NameNode,告知自己还存活;
  3. DataNode负责数据存储,客户端从NameNode获取到可用的DataNode后,会向该DataNode发送请求完成实际的文件读写操作。

1.4 DFS-Client

DFS-Client,就是分布式文件系统的客户端,各个需要使用分布式文件系统的应用都需要集成该客户端。DFS-Client主要封装了各类文件操作接口,供应用调用使用,它的底层我使用了 Google 开源的gRPC来作为客户端与服务端之间的通信组件。

关于gRPC的使用,我就不赘述了,读者可以自行参阅官方文档。gRPC底层基于Netty完成网络通信,我后续有时间会出专栏对Netty的底层原理进行讲解。

1.5 DFS-RPC

DFS-RPC,是一个基于gRPC实现的存根工具包,里面存放了通过proto编译器生成的Java gRPC模板代码。我们的分布式文件系统的各个组件之间通过gRPC框架完成通信,所以都需要依赖dfs-rpc

二、工程搭建

了解了分布式文件系统的整体架构,本节我们就开始搭建项目工程。我将整个Maven工程划分为以下模块:

202307302134118172.png

2.1 dfs-rpc

gRPC服务提供方的接口存根全部定义在dfs-rpc模块中。比如,NameNode需要提供以下RPC服务接口,供DataNode、DFS-Client、BackupNode调用:

  • DataNode注册接口;
  • DataNode心跳接口;
  • 客户端操作命令接口;
  • Edits Log日志同步接口。

我们需要通过ProtoBuf的工具来生成这些接口,我会在下一章讲解gRPC的基本使用。

2.2 dfs-client

客户端比较简单,需要依赖dfs-rpc。本项目中,我暂且只定义一个接口作为示例。集成客户端的应用系统可以通过RPC方式对文件/目录进行操作。后续,我们也可以根据自身需要对客户端的功能进行扩展:

    /**
     * 作为文件系统的接口
     */
    public interface FileSystem {
    
        /**
         * 创建目录
         * @param path 目录对应的路径
         * @throws Exception
         */
        void mkdir(String path) throws Exception;
    }

2.3 dfs-namenode

NameNode是分布式文件系统的大脑,同样需要依赖dfs-rpc。它的内部包含了一些核心组件,后续章节我会详细讲解,本节,我们来看下它的启动类:

    /**
     * NameNode启动类
     */
    public class NameNode {
    
        // 负责管理集群元数据的核心组件,即内存文件目录树
        private FSNameSystem namesystem;
    
        // 负责管理集群DataNode的组件
        private DataNodeManager datanodeManager;
    
        // NameNode对外提供rpc服务的Server
        private NameNodeRpcServer rpcServer;
    
        // Edits Log同步组件
        private EditLogReplicator replicator;
    
        // fsimage同步组件
        private FSImageUploadServer fsimageUploadServer;
    
        public static void main(String[] args) throws Exception {
            NameNode namenode = new NameNode();
            namenode.init();
            namenode.start();
        }
    
        private void init() {
            this.namesystem = new FSNameSystem();
            this.datanodeManager = new DataNodeManager();
            this.replicator = new EditLogReplicator(namesystem);
            this.rpcServer = new NameNodeRpcServer(this.namesystem, this.datanodeManager, this.replicator);
            this.fsimageUploadServer = new FSImageUploadServer();
        }
    
        private void start() throws Exception {
            this.rpcServer.start();
            this.fsimageUploadServer.start();
            this.rpcServer.blockUntilShutdown();
        }
    }

2.4 dfs-datanode

DataNode负责存储实际的数据,由NameNode进行调度,同样需要依赖dfs-rpc。我们来看下它的启动类,它的内部包含了一些核心组件,后续章节我会详细讲解:

    /**
     * DataNode启动类
     */
    public class DataNode {
        private volatile Boolean isRunning;
    
        // 负责跟NameNode通信的组件
        private NameNodeConnService connService;
    
        private void initialize() {
            this.isRunning = true;
            this.connService = new NameNodeConnService();
        }
    
        private void start() {
            this.connService.start();
            try {
                while (isRunning) {
                    Thread.sleep(1000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            DataNode datanode = new DataNode();
            datanode.initialize();
            datanode.start();
        }
    }

2.5 BackupNode

BackupNode备份节点,同样需要依赖dfs-rpc。我们来看下它的启动类,它的内部包含了一些线程,启动后就会通过RPC方式与NameNode交互:

    /**
     * BackupNode启动类
     */
    public class BackupNode {
        private volatile Boolean isRunning = true;
    
        private FSNameSystem namesystem;
        private NameNodeRpcClient namenode;
    
        public static void main(String[] args) {
            BackupNode backupNode = new BackupNode();
            backupNode.init();
            backupNode.start();
        }
    
        public void init() {
            this.namesystem = new FSNameSystem();
            this.namenode = new NameNodeRpcClient();
        }
    
        public void start() {
            // 定期拉取Edits Log线程
            EditsLogFetcher editsLogFetcher = new EditsLogFetcher(this, namesystem, namenode);
            editsLogFetcher.start();
    
            // 定期生成fsimage快照线程
            FSImageCheckPointer checkPointer = new FSImageCheckPointer(this, namesystem, namenode);
            checkPointer.start();
        }
    
        public Boolean isRunning() {
            return isRunning;
        }
    }

三、总结

本章,我对分布式文件系统的整体架构进行了讲解,帮助读者理清楚系统的各个核心模块的作用和关系,从下一章开始,我将具体分析每个模块的功能并开始进行编码。

阅读全文