# 儒猿分布式小文件系统 **Repository Path**: heheiscool/ruyuan-dfs ## Basic Information - **Project Name**: 儒猿分布式小文件系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 901 - **Created**: 2021-09-09 - **Last Updated**: 2021-09-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## RUYUAN-DFS 儒猿自研分布式小文件存储系统。 定位: 小文件存储 ——> 几KB~几百MB之间的文件存储。 ## 特性 - 支持亿级小文件分布式存储 ## 设计 ### 元数据管理 ![](/img/architecture.png) ### 文件目录树组织方式 ![](/img/fsdirectory.png) ### EditLog 格式 `EditLog`的存储采用`ProtoBuf序列化 + 自定义头`存储。 比如一个文件有2条`EditLog` - 第1条`EditLog`经过`ProtoBuf序列化`之后,得到的byte数组长度为18 - 第2条`EditLog`经过`ProtoBuf序列化`之后,得到的byte数组长度为25 那`EditLog`文件的内容如下: ``` 第一条EditLog 第二条EditLog +--------+-------------------------+--------+-----------------------------+ | Length | Actual Content (18byte) | Length | Actual Content (25byte) | | 0x0012 | EditLog Serialization | 0x0019 | EditLog Serialization | +--------+-------------------------+--------+-----------------------------+ ``` ### BackupNode Checkpoint机制 `内存目录树 + txId` 持久化在同一个`FsImage`文件中,将时间戳拼接在文件名称: - `fsimage-1624846255954` - `fsimage-1624846265954` - `fsimage-1624846275954` 在保存完成`FsImage`文件之后,`BackupNode`和`NameNode`都会做如下操作: - 扫描所有的`FsImage`文件,将文件按时间戳降序排序。 - 逐步校验`FsImage`文件,直到找到一个格式合法的`FsImage`文件。 - 假设上面第3个`FsImage`文件不合法,保存到一半的时候`BackupNode`宕机了,或者`BackupNode`传给`NameNode`的时候传了一半内容,`BackupNode`宕机了,导致整个文件不完整 - 判断第3个文件,校验得出第3个不合法,删除第3个文件。继续校验第2个文件,文件合法,删除其他所有`FsImage`文件,只保留第2个`FSImage`文件 - `NameNode`基于第2步得到的`FsImage`文件,读取其中的`TxId`,删除比`TxId`小的`EditLog`文件 其中`FsImage`文件格式如下: ``` +----------------+----------+--------+-------------------+ | 4 byte | 8 byte | Content | | Content Length | maxTxId | FSDirectory Serialization | +----------------+----------+----------------------------+ ``` 其中校验FSImage文件的合法性,只需要读取前4个字节,得到文件长度,然后校验文件的长度是否相等即可。 ### DataNode文件存储方式 每个DataNode从配置的根目录下面创建一个storage文件夹,里面创建000-255个文件夹,每个文件夹内再创建000-256个文件夹,总共65535个文件夹 当需要储存一个文件的时候,根据文件全路径转换后得到一个字符串,字符串经过hash取模65535,定位到目标文件夹,文件保存在目标文件夹。 举个例子: >> 假设需要储存文件`/user1/aaa/bbb/c.jpg`, 转换后得到字符串为:`user1-aaa-bbb-c.jpg`, 经过hash取模65535,结果为`/210/102/`, 则文件保存在:`{baseDir}/storage/210/102/user1-aaa-bbb-c.jpg`。 经过这样转换后所有的文件都会存放在65536个文件夹中,存储文件层级永远是3层。(无论你的文件全路径是`/a.jpg`还是`/a/a/a/a/a/a/a/a.jpg`) DataNode是一个基于key-value的对象存储库,对于`/aaa/bbb/c.jpg`的字符串,DataNode理解是一个key,而不是一个文件目录,可以达到快速访问文件的目的。 配合NameNode提供文件目录树,对外表现是一个文件目录树,但是文件存储本质上是一个key-value对象存储。 目前实现了几种方式有几种转换方式: - 简单路径转换:将`/user1/aaa/bbb/c.jpg`转换成`user1-aaa-bbb-c.jpg` - MD5:将文件全路径进行md5,得到一个32位的字符串,文件很多的时候有一定概率会hash冲突 - SHA-1:比MD5更稳定,但是也无法绝对保证不会产生hash冲突 - 对称加密,目前支持aes加密 在65536个文件夹中,除了存储每个文件本身之外,还新建了一个文件storage.info文件。 在`DataNode`启动的时候,扫描65536个文件夹的storage.info内容,得到每个文件夹内的文件存储信息,然后往NameNode上报。 ## NameNode + BackupNode 主备模式切换 BackupNode除了承担拉取EditLog文件,定时进行Checkpoint生成FsImage文件功能之外,还承担着容灾的职责。 当NameNode出现故障的时候,BackupNode需要主动升级为NameNode,并通知客户端和DataNode进行切换。所以各个节点需要做以下事情: - BackupNode连接上NameNode之后,主动将自己的信息上报给NameNode,NameNode返回自己的配置信息给BackupNode。 - NameNode收到BackupNode的信息之后,保存起来。 - 客户端和NameNode建立链接之后,NameNode主动推送BackupNode信息,客户端和BackupNode建立连接 - DataNode在和NameNode建立链接之后,NameNode主动推送BackupNode信息,DataNode和BackupNode建立连接 - NameNode宕机后,BackupNode感知到,准备升级为NameNode,执行以下步骤。 - 下发试探性请求给所有的DataNode和客户端,询问DataNode和客户端和NameNode的连接是否断开。 - BackupNode收到到所有DataNode和客户端信息后,如果超过2/3判断NameNode已经宕机,则开始升级为NameNode - BackupNode基于内存目录树 + 从NameNode同步过来的配置信息,启动NameNode程序。 - BackupNode启动完NameNode程序之后,下发请求给DataNode和客户端断开作为BackupNode时候建立的链接。 宕机的NameNode作为BackupNode重启后,同样执行上面的过程。 ## 多NameNode元数据分片架构 NameNode中的元数据保存在内存目录树中,一旦文件数量过多. (1亿条大概评估需要内存2.5GB),则一台NameNode机器的内存放不下, 所以要设计多NameNode进行元数据分片机制。 多NameNode节点数据分片规则可以有几种选择: - 根据不同的用户名经过一致性hash后划分到不同的NameNode,某个用户的数据都存储在一台NameNode上面 - 优点:数据隔离,某个用户的数据只需要去特定一台NameNode就可以找到元数据,不需从不同的NameNode节点上获取元数据, 在一些文件目录树浏览的场景下,通常都是根据用户维护去浏览数据,这种实现比较友好 - 缺点:数据量大小和用户关联性大,比如某个用户的用户数据就特别大,可能占了整个集群的80%的元数据。这种情况下,可能导致元数据分布数据不均匀,没有缓解NameNode的压力。 - 根据文件全路径进行一致性hash后划分到不同的NameNode - 优点:文件元数据分布相对均匀。 - 缺点:在文件目录树浏览这种场景,可能需要遍历所有的NameNode的文件目录树才行,但是通常NameNode如果数量不多的话,这种程度的遍历也可以接受。 目前实现为第二种,一致性hash算法可以采用16383个虚拟节点的方式,平均打散分配到每个节点。 ### Controller选举流程 - 每个节点启动时,往id比自己id小的节点发起连接,两个DataNode建立链接之后,都会往对方发送自己的当前信息,共享集群列表信息,最终达到集群所有机器都建立链接的状态 - 开始进行Controller选举,选出一个Controller,该Controller负责元数据分配,分配完毕后,将元数据广播给所有的NameNode节点。 ### 动态扩容 新节点上线后,发送选票给旧节点,旧节点发现已经有Controller,返回强制性选票给新节点,新节点遇到强制性选票则无条件确定就集群的Controller节点 - 旧节点感知到是新节点加入集群(收到强制性选票),则发个请求要求Controller进行slot重平衡. - Controller收到重平衡的请求,需要将请求进行串行化处理,以下操作需要保持原子性: - 判断需要重平衡的节点是否在上一轮已经考虑进去了,给它分配过槽位了?如果是,跳过此请求,如果否,则Controller进行重新分配slot - Controller重新分配好Slot广播新的slot分配信息,暂存起来 - 所有的NameNode节点收到广播的slot分配信息,识别出是重平衡的信息,先将新的slot信息暂存起来(不替换,还未生效) - 新加入的NameNode节点,从广播的Slot分配信息中识别出:有哪些slot是新分配给我的,每个slot对应的元数据应该从哪个NameNode节点获取 - 新加入的NameNode节点,去每个NameNode节点拉取元数据,元数据需要分批、有序的传输,避免网络包太大。在确保所有slot的元数据都拉取回来之后,上报给Controller,表示自己已经可以对外工作了 - Controller让暂存的slot分配信息生效,同时广播给所有的NameNode节点,让他们把本轮重平衡需要删除的元数据删除(需要实现删除文件功能),并让自己暂存的slot分配信息生效 - 此时完成一轮重平衡。才可以进行下一轮重平衡 假设一开始3个节点,需要新增两个节点,按顺序启动,此时Controller可能会收到2个重平衡请求,需要入队串行化处理。 ### NameNode机器宕机后高可用保障 - 普通机器宕机:由BackupNode顶上,不需要迁移元数据。 - Controller机器宕机:需要重新选举Controller, 等BackupNode顶上,不需要迁移元数据 假设现在有3个NameNode节点:namenode01、namenode02、namenode03,其中namenode02挂着一个backupnode01. 集群中发起的连接状态正常是: ``` namenode02 -> namenode01 namenode03 -> namenode02 namenode03 -> namenode01 ``` 假设此时namenode01宕机了,backupnode01作为备份节点升级为namenode节点。此时集群中的节点需要变成以下状态: ``` namenode02 -> namenode01 namenode02 -> backupnode01 namenode03 -> namenode02 --> namenode03 -> namenode02 namenode03 -> namenode01 namenode03 -> backupnode01 ``` 应该如何做呢?这里分为几个步骤: 1. backupnode01升级后,发现自己是升级后的namenode,则会主动往其他节点发起连接(不考虑节点id大小关系了) 此时节点之间的连接状态是这样的 ``` namenode02 -> namenode01 namenode03 -> namenode02 namenode03 -> namenode01 backupnode01 -> namenode02 backupnode01 -> namenode03 ``` 接着namenode02和namenode03收到请求之后,发现这个节点id比我小,还往我这里发起连接,所以他们这个时候就知道了namenode01节点已经被备份节点顶替了。 此时namenode02和namenode03首先会作为服务端关闭这些连接: ``` backupnode01 -> namenode02 backupnode01 -> namenode03 ``` 然后namenode02、namenode03会主动发起往backupnode01的链接。这个时候backupnode01发现连接变成了这样: ``` backupnode01 -> namenode02 backupnode01 -> namenode03 namenode02 -> backupnode01 namenode03 -> backupnode01 ``` backupnode01就会关闭作为客户端主动往namenode02和namenode03发起的连接,最终整个集群的链接状态保持成这样: ``` namenode02 -> backupnode01 namenode03 -> namenode02 namenode03 -> backupnode01 ``` ## proto生成命令 cd ruyuan-dfs-common && mvn protobuf:compile