文章目录
一、 分布式文件存储 FastDFS
1、简介
FastDFS 是一个 开源的 轻量级 分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。 特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容 (解决了 Spring MVC 上传文件时,容量不够用的问题)等机制,并注重高可用、高性能等指标,使用 FastDFS 很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度,最终由 Storage server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度(相当于是个控制中心 、 注册中心),通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务,可以将 tracker 称为追踪服务器或调度服务器。 Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将 storage 称为存储服务器。
2、工作原理
我们现在要在微服务商城系统中使用 FastDFS,来实现文件的管理,流程大概是这样的:
可以看到,Storage 会定时注册,每过一段时间会将自己的信息发送给 Tracker,和 Tracker 保持 “心跳沟通” 。用 changgou-service-file 模块来做文件管理,通过控制层访问路径 /upload,用户上传文件时,那么程序就会到 Tracker 控制中心找返回的可用的 Storage,进行上传文件。图中还提到了 “同步备份” 和 “集群”,集群是 一组一组的,比如图中的三个组 Group 合起来,是一个集群,当容量不够时,可以添加新的 Gruop,这就是上文提到的 “线性扩容”,也叫水平扩容;而每个组里的是同步节点(不是 “主从” 喔)。
再来看看这样的情况:比如说,第一次上传的时候,上传到了 192…12 这台机器上,那么这时 192…12 机器会立刻将文件同步到 192…13 机器上,这叫 “冗余备份”;用户下次再上传文件,如果 192…13 机器是空闲的,就会把 192…13 机器的 IP 地址和端口号返回给程序。同 Group 的机器里的数据总是一致的,如果某一台出故障了,也可以去其他机器上找数据,这就是 “容灾”。
3、文件上传流程
图中第 7 步里的 “磁盘” 指的是服务器。
客户端上传文件后,存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。比如:
group1/M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg
- 组名:文件上传后所在的 storage 组名称,在文件上传成功后,有 storage 服务器返回,需要客户端自行保存。
- 虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项 store_path* 对应,也就是说,指向了 Storage 组中某个节点的硬盘地址。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
- 数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
- 文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
4、框架搭建
首先,需要拉取镜像( docker pull morunchang/fastdfs
) ,在虚拟机中查看,可以看到,已经下载镜像了。
接下来,使用 docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
运行 tracker;然后使用 docker run -d --name storage --net=host -e TRACKER_IP=192.168.211.132:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
运行 storage :
参数说明:
- docker run -d :后台运行。
- 使用的网络模式是 –net=host, 192.168.211.132 是宿主机的IP
- group1 是组名,即 storage 的组,如果想要增加新的 storage 服务器,再次运行该命令,注意更换新组名。
运行结果:
可以看到,tracker 和 storage 启动成功了。
接下来,设置 restart 属性,设置开机自动启动:
在 nginx 的配置文件中:
可以看到,以 /M00 开头的请求都会去找 ngx_fastdfs_module 模块。
接下来,开始搭建文件微服务。
先在 changgou-service 中新建 module,名为 changgou-service-file ,导入依赖:
<dependencies>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
在 resources 文件夹下创建 fastDFS 的配置文件 fdfs_client.conf:
connect_timeout=60
network_timeout=60
charset=UTF-8
http.tracker_http_port=8080
tracker_server=192.168.211.132:22122
参数说明:
- connect_timeout:连接超时时间,单位为秒。
- network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败
- charset: 字符集
- http.tracker_http_port :.tracker 的 http 端口
- tracker_server: tracker 服务器 IP 和端口设置
提供 application.yml :
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
application:
name: file
server:
port: 18082
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
其中,max-file-size 是单个文件大小,max-request-size 是请求数据的大小(比如说,除了文件,还有表单)。
提供启动类:
@SpringBootApplication
@EnableEurekaClient
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class,args);
}
}
运行后报错了:
Action 里说需要把数据库放置在 classpath 下,但是这个系统是不需要使用数据库的,所以需要对数据库自动配置加载的行为进行排除。对启动类上的注释进行修改:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
运行结果:
二、文件上传
接下来,就参考上传流程,实现上传功能。
首先,对文件上传的信息进行封装:
public class FastDFSFile implements Serializable {
// 文件名
private String name;
// 文件字节数组(内容)
private byte[] content;
// 文件扩展名,比如 jpg、png、gif......
private String ext;
// 文件 MD5 摘要值,为文件生成唯一值
private String md5;
// 上传者
private String author;
/* 构造方法、setter、getter 方法省略*/
提供文件管理工具类:
public class FastDFSUtil {
/* 加载 Tracker 连接信息 */
static {
try {
// 查找 classpath 下的文件路径
String filename = new ClassPathResource("fdfs_client.conf").getPath();
// 初始化 Tracker 连接信息
ClientGlobal.init(filename);
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
}
/* 文件上传 */
public static void upload(FastDFSFile file) throws IOException, MyException {
// 附加参数
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] = new NameValuePair("附加参数", "附加参数值");
// 创建一个 Tracker 访问的客户端对象 TrackerClient
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 访问 TrackerServer 服务,获取连接信息(包含 Storage)
TrackerServer trackerServer = trackerClient.getConnection();
// 创建 StorageClient 对象,存储 Storage 的连接信息
StorageClient storageClient = new StorageClient(trackerServer, null);
// 通过 StorageClient 访问 Storage,实现文件上传,并且获取文件上传后的存储信息
// 传入的参数分别是 上传文件的字节数组、扩展名、附加参数
storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
}
}
提供控制类:
@RestController
@RequestMapping
@CrossOrigin
public class FileUploadController {
@PostMapping
/* 文件上传 */
public Result upload(@RequestParam(value = "file") MultipartFile file) throws IOException, MyException {
FastDFSFile file1 = new FastDFSFile(
// 文件名
file.getName(),
// 文件字节数组
file.getBytes(),
// 文件扩展名
StringUtils.getFilenameExtension(file.getOriginalFilename())
);
// 调用文件上传工具类 将文件传入到 FastDFS 中
FastDFSUtil.upload(file1);
return new Result(true, StatusCode.OK, "上传成功");
}
}
运行结果:
文件管理工具类中使用到的方法 storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
的返回值是 String[ ] 类型,即 String 数组,数组里有两个元素,分别是 :
- [0] : 文件上传所存储的 Storage 组名
- [1]:文件名
比如上文举的例子group1/M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg
中,组名是 group1,文件名是 M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg 。
我们把upload(FastDFSFile file)
方法的返回值改为 String[ ] ,接下来,我们把这个 String 数组取出来 ,放到响应结果中去,将控制类方法的代码修改为:
// 返回值是 String 数组
String[] filename=FastDFSUtil.upload(file1);
// 拼接访问地址
String url="http://192.168.211.132:8080/"+filename[0]+"/"+filename[1];
return new Result(true, StatusCode.OK, "上传成功",url);
运行结果:
(🔍 访问这个路径,出现了问题,中文乱码了 😅 有待解决。)
那么,上传的文件存到哪里去了呢 ❔ 在 Storage 里面,具体来看:
在虚拟机中启动 Storage ,并 vi 进入配置文件:
可以看到,Storage 的通信端口号是 23000,heart_beat_interval=30 表示每 30 s 发送一次信息。而 Tracker 的通信端口号是 22122:
还可以看到工作线程数。
store_path0 表示第一个虚拟路径,我们可以看看 /data/fast_data 目录:
上传文件时是会根据文件名计算哈希值的,值决定放置的目录。继续访问 00 目录 :
可以看到我们上传的文件。
三、文件信息获取
我们已经知道,文件是上传到 Storage 里的,那么,要获取文件信息,我们需要先访问 Tracker,才能知道文件在哪个 Storage 里。
💎 代码:
/* 获取文件信息
* @param groupName 文件的组名,比如 group1
* @param remoteFileName 文件存储路径名,比如 M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt
* @return
*/
public static void getFileInfo(String groupName, String remoteFileName) throws IOException, MyException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
//通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
StorageClient storageClient = new StorageClient(trackerServer, null);
// 获取文件信息
FileInfo fileInfo = storageClient.get_file_info(groupName, remoteFileName);
}
主方法:
public static void main(String[] args) throws IOException, MyException {
FileInfo fileInfo = getFileInfo("group1", "M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt");
System.out.println(fileInfo.getSourceIpAddr());
System.out.println(fileInfo.getFileSize());
}
运行结果:
四、文件下载
文件是存在 Storage 里的,所以和以上步骤是类似的。
💎 代码:
/* 文件下载*/
public static InputStream downLoadFile(String groupName, String remoteFileName) throws IOException, MyException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
//通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
StorageClient storageClient = new StorageClient(trackerServer, null);
byte[] buffer = storageClient.download_file(groupName, remoteFileName);
return new ByteArrayInputStream(buffer);
}
接下来看文件下载效果,将下载的字节数组写到本地磁盘。在主方法中添加代码:
// 文件下载
InputStream is = downLoadFile("group1", "M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt");
// 将文件写入本地磁盘
FileOutputStream fos = new FileOutputStream("D:/1.txt");
byte[] buffer = new byte[1024];
while (is.read(buffer) != -1) {
fos.write(buffer);
}
fos.flush();
fos.close();
fos.close();
运行成功,在 D 盘看到了 1.txt。
五、文件删除
文件是存在 Storage 里的,所以和以上步骤是类似的。
💎 代码:
/* 文件删除 */
public static int deleteFile(String groupName, String remoteFileName) throws IOException, MyException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
//通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient.delete_file(groupName, remoteFileName);
}
在主方法里调用删除文件的方法,然后去看虚拟机,确实删除了:
接下来去浏览器里访问http://192.168.211.132:8080/group1/M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt,会发现还是访问到了文档 😅,但是清除了缓存,再去访问,就 404 啦,不过这样让用户手动清除缓存,是不科学的,应该对 Nginx 设置禁止缓存,在虚拟机中启动 Storage,然后进入 /etc/nginx/conf 目录:
vi 模式进入 nginx.conf 文件,对 Nginx 设置禁止缓存,输入 add_header Cache-Control no-store;
:
再上传一遍文件(这时候注意到,生成的文件名是不同的),然后删除文件,再访问相同路径,就不需要手动清缓存,直接显示 404 啦。
六、获取 Storage、Tracker 信息
- 获取 Storage 信息
获取 Storage 信息。只需要访问 Traker,Traker 就会把 Storage 的信息返回啦。
💎 代码:
/* 获取 Storage 信息*/
public static StorageServer getStorage() throws IOException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
// 获取 Storage 信息
return trackerClient.getStoreStorage(trackerServer);
}
在主方法中进行测试:
System.out.println(getStorage());
System.out.println(getStorage().getStorePathIndex());
System.out.println(getStorage().getInetSocketAddress().getHostString());
运行结果:
可以看到,下标是 0,IP 地址是 192.168.211.132
- 根据 文件组名 和 文件存储路径 获取 Storage 组的 IP 和端口信息
💎 代码:
/* 获取 Storage IP、端口信息 */
public static ServerInfo[] getStorage1(String groupName, String filename) throws IOException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer, groupName, filename);
}
这个方法是获取文件所在的 Storage 组的信息,在主方法里需要遍历组的每个 Storage 机器:
// 获取 Storage 组的 IP和端口信息
ServerInfo[] groups = getStorage1("group1",
"M00/00/00/wKjThGAyVUiARmICAAABPGy6xXQ596.txt");
for (ServerInfo group : groups) {
System.out.println(group.getIpAddr());
System.out.println(group.getPort());
}
运行结果:
- 获取 Tracker 信息
💎 代码:
/* 获取 Tracker 信息 */
public static String getTrackerInfo() throws IOException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
// Tracker IP、HTTP 端口
int port = ClientGlobal.getG_tracker_http_port();
String ip = trackerServer.getInetSocketAddress().getHostString();
String url = "http://" + ip + ":" + port;
return url;
}
将方法返回值输出,运行结果:
可以看到,和配置文件 fdfs_client.conf 里 IP 和 端口号是一致的,在这个项目里,Nginx 和 Tracker 端口号都是 8080,Nginx 的配置文件截图如下:
我们访问文件,其实是访问 Nginx,Nginx 会访问 Tracker,然后 Tracker 看文件在哪个 Storage 里,因为 Nginx 和 Tracker 端口号一样,所以在 文件管理工具类中,拼接的访问地址原先是 :
String url="http://192.168.211.132:8080/"+filename[0]+"/"+filename[1];
可以换成动态获取 IP 的形式 (注意只能用在 Nginx 和 Tracker 端口号 一致的情况喔) :
String url= FastDFSUtil.getTrackerInfo() +"/" + filename[0] +"/"+ filename[1];
接下来,我们优化一下代码,把相同的部分提取出来,比如:
public static TrackerServer getTrackerServer() throws IOException {
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
return trackerClient.getConnection();
}
🎉 补充:Classpath
classpath 通常被用来指定配置 或者 资源文件的路径:
七、总结
(1)FastDFS 是一个 开源的 轻量级 分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了 大容量存储 和 负载均衡 的问题。
FastDFS 架构包括 Tracker server 和 Storage server。
Tracker server 的作用是负载均衡和调度(相当于是个控制中心 、 注册中心),而 客户端上传的文件最终是存储在 Storage 服务器上(相当于干活的)。
Storage 会定时注册,每过一段时间会将自己的信息发送给 Tracker,和 Tracker 保持 “心跳沟通” ♥ 。
(2)要读取 maven 项目 resource 下的文件,可以使用 Spring 的 ClassPathResource 类,构造方法的参数便是 resources 目录下的文件路径,这里使用的是相对路径(相对于resouces目录而言的),即 单纯的文件名即可。
(3)客户端上传文件后,存储服务器会将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。组名是 文件上传后所在的 storage 组名称;虚拟磁盘路径,与磁盘选项 store_path* 对应,也就是说,指向了 Storage 组中某个节点的硬盘地址。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推;文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
(4)文件的上传、下载、删除,过程都是:
// 创建 TrackerClient 对象,访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过 TrackerClient 获取 TrackerServer 的连接对象,
TrackerServer trackerServer = trackerClient.getConnection();
//通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
StorageClient storageClient = new StorageClient(trackerServer, null);
然后调用 storageClient 的 upload_file、download_file、delete_file 。
(5)如果想要获取 Storage 信息,因为 Storage 会定时向 Tracker 进行注册,所以,只需要访问 Traker,调用 trackerClient.getStoreStorage(trackerServer)
方法即可,返回的是个 StorageServer 类对象,可以调用它的 getStorePathIndex() 方法知道下标,也可以调用 getInetSocketAddress 知道 socket 地址。
如果想要根据 文件组名 和 文件存储路径 获取 Storage 组的 IP 和端口信息,
调用 trackerClient.getFetchStorages(trackerServer, groupName, filename)
即可,获取文件所在的 Storage 组的信息,因为是 “组”,所以需要遍历组的每个 Storage 机器。