在架构设计:文件服务的设计与实现一文中,经过实现一个文件服务来梳理了一个架构设计的通常流程,并获得以下静态架构图node
本文继续聊聊文件服务中的子模块:「存储模块」的设计,包括:git
前面的架构没有对存储进行特别设计,直接使用了本地存储。考虑到后期文件数量可能会愈来愈多,本地存储可能没法支撑,且本地存储的安全性也没有保障。为了便于后期扩展,须要对「存储」部分进行设计。github
存储的方式有不少,本地存储、NAS、分布式存储,为了能支持不一样的存储方式,须要对「存储模块」进行抽象。考虑到「存储模块」涉及到IO,是一个相对底层的模块。「上传」这个核心模块不能依赖于具体的存储,因此这里也须要对其进行依赖反转。redis
见紫色部分,UploadService调用了FileInfoRepository来存储FileInfo,而FileInfoRepository是个接口,具体实现由存储模块中的实现类来实现。算法
public interface FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException;
}
public class LocalFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}
public class NASFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}
public class DistributedFileInfoRepository implements FileInfoRepository {
public Path save(FileInfo fileInfo) throws IOException {
...
}
}
复制代码
咱们先看本地存储。最简单的实现,就是直接使用IO将文件写到对应的目录下就能够了。可是,本地存储会有以下几个问题:安全
下面咱们针对上面的问题,来一个个的解决。服务器
首先,对于多租户来讲,在咱们的架构中,实际对应的是Group,咱们按照Group的不一样,来划分目录便可。即不一样的租户有不一样的文件根目录,后期某个租户迁移时,直接迁移对应目录便可。这也稍微解决了单目录文件数量多的问题。markdown
对于单目录下,随着文件数量的增长致使访问速度降低的问题,咱们该如何解决呢?架构
若是你作过度布式系统,那么想想,咱们是否能够把单目录当作是一个服务器,访问目录下的文件当作是一个个的请求呢?若是能够,那解决单目录下访问速度慢的问题是否是就变成了「如何解决单服务器下,负载太高」的问题了?那解决服务端负载太高的方法是否适用于解决目录访问速度降低的问题呢?并发
咱们从下面几个方面来分析一下:
首先来看「解决服务端负载太高的方法」!答案很明显:分流+负载均衡!
分布式服务的负载均衡有几种方式呢?
再来看「目录访问和服务器的区别」,虽然能够把目录当作服务器,可是二者仍是有区别的:
也就是说,对于目录来讲,咱们不须要考虑建立成本。
那么针对服务器负载高的解决方案是否适合目录访问呢?或者哪一种方式适合目录访问呢?咱们一个个来分析:
能够看到,主要的问题就是建立目录的问题!如何保证在目录数量改变时,不须要调整程序呢?
实际上git已经给出了答案:
也就是说,根据sha1散列的前两位对文件进行归类。这样既解决了目录建立问题,也解决了文件分布问题。可能的问题是,「sha1散列2^80次,可能会发生一次碰撞」。这个问题对于通常文件系统来讲,好像也没有担忧的必要。
解决了「单目录文件过多,致使访问速度降低」的问题,咱们来看下一个问题:数据安全。
文件数据是存放在电脑磁盘上的,若是硬盘损坏,可能致使文件的丢失。这实际仍是一个「单点问题」!
「单点问题」的解决方案是什么呢?冗余啊!
最简单的方案就是定时去备份数据,能够有以下几种方案:
咱们继续一个个的讨论。
首先是人工备份,这是最low的方案,固然也是最简单的,即有人按期去备份就好了。问题是时效性不高,例如一天备份一次,若是磁盘在备份前坏了,那就会丢失一天的数据。同时恢复比较耗时,须要人工处理。
第二个方案是代码实现,即在上传文件时,程序就自动备份。以上面的架构为例,能够添加一个BackupListener,当上传完成后,经过事件,自动备份上传的文件。同时下载时须要断定文件是否完整,若是有问题则使用备份数据。此方案时效性获得了保障,可是将数据备份和业务放到了一块儿,且须要编码实现,增长了业务代码量。
第三个方案是libfuse,libfuse是用户态文件系统接口。下面是libfuse官方简介:
FUSE (Filesystem in Userspace)是一个构建用户态文件系统的接口。libfuse项目包括两个组件:一个fuse内核模块以及libufuse用户态库。libfuse用户态库提供了与FUSE内核模块的通信实现。
经过libfuse能够实现一个用户态文件系统。libfuse提供方法,支持挂载文件系统、取消挂载文件系统、读取内核请求及做出响应。lifuse提供了两类API:高层级的同步API和低层级的异步API。不过不管哪一种方式,内核都是经过回调的方式和主程序通信。当使用高层级API的时候,回调基于文件名和路径而不是索引节点(inodes),而且回调返回后这个进程也同时结束;当使用低层级API的时候,回调基于索引节点(inodes)工做而且响应必须使用独立的API方法返回。
简单来讲,就是能够用libfuse构建一个用户态文件系统。以前在老东家作了一个日志分析平台,日志的收集就使用了libfuse,大体架构以下:
业务系统写日志到挂载的用户态文件系统中,用户态文件系统自动转发到了后续的处理中间件:redis、消息队列、文件系统。
在这里也能够用相似的功能,即在文件上传后,用户态文件系统自动备份。此方案解耦了文件备案逻辑与业务逻辑。
最后一个方案是RAID,即廉价冗余磁盘阵列。RAID不但可备份文件,还支持并发读写,提升上传下载速率。
经常使用的RAID有:RAID0,RAID1,RAID01/RAID10,RAID5和RAID6等。咱们来看看这几种RAID的特色,以及是否适用于咱们的文件服务。你会发现从RAID0到RAID6,又是一个从单点到分布式的过程。
具体RAID相关内容可参考wiki,文末有连接!
看下面的两张图应该能更好的理解:
不管是RAID10仍是RAID01,对磁盘的使用效率都不高。那如何提升磁盘使用率呢?就有了RAID3。
对于本地存储来讲,RAID是个相对实用的解决方案,既能提升数据安全、快速扩容,也提升了读写速率。可是不管扩展多少磁盘,容量仍是相对有限,吞吐也相对有限,同时因为其仍是单点,若是文件服务自己挂掉,就会致使单点故障。因此就有了分布式文件系统。
分布式文件系统下次单独讨论!