分布式文件存储系统的开发
前言:这是一个比较 底层的项目,从底层入手去开发一个分布式文件存储系统。此项目不同于传统的业务开发,更侧重于底层功能的实现。
![请添加图片描述](https://i-blog.csdnimg.cn/blog_migrate/221f29e72a3852d770155f0fa09171a3.png)
1 背景
1.1 业务背景
市面上的网盘、云盘等等层出不穷,百度网盘、阿里云盘、迅雷云盘等等。存储系统作为存储底层,其承接着各类业务,如文件的上传下载等等。其对数据的可用性、可靠性和稳定性具有较高的要求,需要处理网络故障、节点异常、资源扩缩容、数据一致性、版本控制、多用户重复数据的选择策略等等复杂场景。随着用户量的增大,文件存储系统将不单单只是IO流操作,其设计到更复杂的技术细节。
1.2 分布式的演进
谷歌在2003-2006年先后发布了著名的三篇论文,称为“三驾马车”,分别包括GFS(谷歌的面向大规模数据密集型应用的、可伸缩的分布式文件系统),Map Reduce(超大集群的简单数据处理),Big Table(结构化数据的分布式存储系统),由此可见分布式的意义之大。
分布式不是多么难的点,其本质是将一台服务器的任务分配到多台服务器上,通过负载均衡等各种手段实现较高的效率。我们在这个项目中需要实现将用户的文件上传到多台服务器上,以确保完成分布式文件存储系统的基本操作。
2 项目架构演进
2.1 基本IO模型
以上是最基本的模式,客户端可以向服务端进行读写操作的请求,二者中间建立一个IO通道,进行IO操作。IO操作中可以是字节流(二进制数据),也可以是字符流(方便文本传输)。
2.2 文件多份存储
如果想把文件存储到多台服务器上,则需要通过一台服务器来管控多台服务器。那么这一台服务器主要负责调度文件,负责将存储数据的服务器中读写文件,我们把管控其他台服务器的节点称为NameNode
(元数据节点),把存储数据的节点称为DataNode
(数据节点),此处就有了分布是最基本的模型。
元数据和数据的区别
一个文件的名称、版本、大小、散列值等等,我们一般称为元数据,而文件实际存储的内容我们一般称为数据。
2.3 多个元数据节点
随着项目的演进,我们可以采用创建多个NameNode
节点的办法来确保元数据节点的可靠性,DataNode
也会随着项目使用人数的增多而加大数量。
于是我们的最基本的分布式存储模型到这里就完成了。
(我们不单独拿出来技术选型给大家介绍,而会在讲解功能实现时讲解)
3 技术选型
开发语言:Java
(不过业界内存储功能开发用的比较多的是go
和C++
,不过像阿里巴巴的Fast DFS
和业界比较火的HDFS
采用的都是Java
语言);
模块间的通信:推荐RPC
通信协议,比如Thrift
、BRPC
、ProtoBuf
等,但是本项目采用的是HTTP
通信(一是本人更熟悉HTTP
通信,二是HTTP
虽然体量比较大,但功能更全,比如各种方法,如GET
、POST
等)
元数据服务中的一致性共识算法:推荐Raft
协议,但是也可以参考其他工具的协议实现。
元数据服务的信息存储推荐开源的单机存储引擎LevelDB
(字节官方推荐的工具)。
Linux文件系统介绍
Linux 系统中,坚持一切皆文件的思想,可以把目录理解成特殊的文件。
在 Linux 或 Unix 操作系统中,所有的文件和目录都被组织成以一个根节点开始的倒置的树状结构。
图源自菜鸟教程(https://www.runoob.com/linux/linux-system-contents.html)
4 核心功能
核心基本功能介绍:
- 提供数据(文件)读写功能的分布式存储服务
- 多副本存储策略
- 文件分区
- 元数据单点故障的处理
- 分布式共识算法解决元数据节点的数据一致性
- 数据单点故障的处理以及副本补全操作
- 文件在多个服务器间的动态迁移
- 分布式文件的版本控制
- 对象数据的校验与去重
- 网络故障和节点异常的处理办法
- 数据冗余机制与纠删码(理论介绍,不做实现)
1. 提供数据(文件)读写功能的分布式存储服务
由于本项目采用Java
开发,持久层更侧重于使用数据库(如MySQL
),项目设计成了将文件和目录统一当作一条数据,通过type
区分类型,通过建立parent_id
来标注父目录,来间接实现树状图功能,对parent_id
建立索引来提高查找效率(但对索引的维护会浪费性能)。
此开发方式可实现同一父目录下不会存在重名目录或文件。
项目实现了对目录的创建、目录名称的修改、目录的删除(会删除目录内的所有文件和目录(暂时设置成逻辑删除))、目录的列表展示,文件的读写(上传下载)、文件的删除、文件名称修改等。
2. 多副本存储策略
在分布式开发的环境下,一份文件可以存储多个副本来完成数据的容灾机制的保证,即一个文件被破坏后,可以找到其他副本来确保此文件的数据不会丢失,来确保完成读写操作的过程中数据不会发生意外。
有人会问,一份文件存储多份会比较浪费空间呀,需要买更多的硬盘,浪费金钱。但从可靠性角度考虑,如果因为某台服务器的意外下线导致数据不可访问甚至丢失,其对用户的损失远大于一个硬盘的成本。我们可以考虑市面上的硬盘价格,综合各种因素后,决定在本项目中将文件存储3份,存储在不同的DataNode
上。原因如下:
存储三份文件不会对用户的体验造成很大的影响,有人会问,现在上传文件需要以前文件存储一份的时间的三倍,因为需要存储三份,但很明显,这样做是很不现实的。我们采用异步写的方式完成文件的备份存储,不会占用户上传的时间来完成文件的备份的。
此处我们引入“分布式存储复制技术”,包括链式复制(链式写)和星形复制(并行写)。
链式复制和星形复制对比
多副本文件存储的选择策略
- 第一份文件是通过对文档进行MD5算法处理,生成针对单个文件唯一的结果(当然,考虑到碰撞的情况),然后对文件求余,求出存到具体哪台服务器上,确保文件处于散列状态,相对平衡;
- 第二份和第三份文件会在实际生产部署中选择离用户较近的服务器上,以增加读取的速率。
此方案尽量保证文件存储的相对平衡,但第二份和第三份文件的存储策略不容易做到磁盘的平衡使用。这算是二者的折衷考虑吧。
3. 文件分区
文件分区也是分布式文件系统中一个比较重要的环节,其主要功能是实现对大文件的分割。比如我们以128MB为一块的话,我们可以将一个大小为300MB的文件切割成128MB+128MB+44MB的三个文件,将这三个文件分别存储在不同的服务器上,好处如下:
- 便于做灾难后恢复。如果一个文件的某一部分坏掉,我们只需处理那一部分,而不用处理全部文件;
- 便于做负载均衡。合适大小的文件更适合负载均衡;
- 硬盘方面的优化(https://www.jianshu.com/p/39f4afdb5825)
文件分区的选择策略
文件分区的选择策略基于多副本存储的选择策略,只是会让文件在大于128MB的时候选择将128MB文件的MD5算法后的散列值作为数值进行求解。
4. 元数据单点故障的处理
我们在架构部分的2.3部分的模型为多个NameNode
,多个NameNode
行驶管理者的角色就会涉及到主从关系,经典的MySQL
采用的是主写从读的方式,为确保从服务器能读到最新的版本,每次发起读请求时都需要从主服务器确认一遍版本,此为比较传统的主从分离的设计思想。我在项目中采用了另一种设计思想,为主负责所有的功能,负责读和写。其它的服务器存在的意义就是在主服务器崩溃时可以即使选出主节点,来去完成NameNode
主节点的工作。
此设计理念会得到很多人的诟病,这样的话从节点
设置的意义太薄弱了。但是在真实业务场景中,主节点的作用比较重大,我们应该”看“好它,加上日志做好容灾处理。而且一般公司的体量,一台主节点完全足够,对性能影响不会太大。并且单机的设计模式相对分布式的使用会简单很多,此处功能采取单机的设计思想。
5. 分布式共识算法解决元数据节点的数据一致性
下面介绍几种分布式共识算法来完成e所述的功能:
raft开源算法
redis主从实现
6. 数据单点故障的处理以及副本补全操作
如果在传输过程中发生了正在使用的服务器崩溃的情况,那我们进行的办法是利用副本的数据将文件补全。HDFS
的设计思想是设置了一个offset
偏移量,记录好文件位置,然后寻找副本的文件所在的标记,进行读取。
对于用户而言,这一系列操作都是无感的,不会影响到用户的任何操作的体验。
7. 文件在多个服务器间的动态迁移
服务器的文件存储空间均衡是我们考虑要优化的地方,如果我们有的文件存储服务器空间所剩过小,会导致性能变差,此时我们要增加新的DataNode
文件存储服务器。增加完之后,我们需要将原来的所生空间较小的服务器上的文件转移到新插入的文件服务器中,直至处于一个相对平衡的状态。这一过程会在运维人员增加新的服务器之后在后台自动进行。
8. 分布式文件的版本控制
文件可以存在更新操作,此功能是我们所不常用到的,但在有些业务场景下必须用到,比如高中成绩单,班主任在发布后有的同学的成绩有问题,班主任需要发送一份新的文件。
为实现更新版本,我们需要维护一个version
字段,来确保用户拿到的是最新的版本,如果不是最新版本的数据则不允许读入。
此时也能体现出来单机的元数据节点的优势所在,发生冲突的概率更低。
9. 对象数据的校验与去重
在现实应用场景中,这类问题是个非常重要的问题。比如小A和小B都上传了一本名叫作《同济大学高等数学第7版上册》的教材,那么我们在服务器中应该存储一份还是两份呢?显然,存储一份是没有太大必要的,故我们选择存储一份。所以我们要开发一个功能使得名字不同(或者名字相同)但内容相同的对象共享一份存储实体,以减少空间的浪费。
还记得我们刚才讲的MD5算法求散列值吗?一模一样的文件的散列值是一样的,而名称相同但是内容不同的文件的MD5值是不一样的。所以我们可以通过求散列的方式求出文件的散列值。如果用户上传文件后将会进行MD5值的比较,MD5值相同则代表是同一个文件。
此处也要考虑到效率问题,如果一个文件大小之后KB级别,那也没必要进行求散列值比较大小,毕竟存在散列碰撞的可能,而且KB级别的大小对整个分布式存储系统不会造成多大的影响的。
10. 网络故障和节点异常的处理办法
网络故障解决方案:断点续传
在现实生产环境中不像我们调试的时候环境那么好,网络故障甚至断网(尤其是客户端)是经常发生的事情,基本思路就是记录位置,通过offset
关键字,但是断点下载比断点上传开发起来容易很多。
断点下载的实现方式:客户端在GET
对象请求时通过设置Range
头部来告诉接口服务从什么位置开始输出对象的数据;
断点上传的实现方式:断点上传回避断点下载复杂很多,这是由于HTTP服务的特性导致的。下载时并不在乎数据的完整性的,下载到哪算哪,下次继续从最后下载的数据的位置开始续传就好了。
但是对上传来说,接口服务会对数据进行散列值的校验,当发生网络故障时,传输的数据不全,导致散列值计算的结果不一样,会默认上传失败,服务端就会删除掉原来下载好的,比较耗费性能。
所以我们可以通过POST
接口提供好下载部分的散列值和大小(可以通过POST
方法得到想要部分的数据的散列值),然后通过PUT
接口将数据传入服务器中。
节点异常解决方案
节点数据异常有两种情况:
- 刚才提到的断点续传问题。由于上传数据不全导致算出的散列值不一样,从而抛弃。此问题已经被解决了。
- 被网络攻击了。网络攻击更改了我们的数据,所以建议直接丢包重传。