avatar

FastDFS

FastDFS简介

FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存 储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的 问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重 高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件 上传、下载等服务。

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],用户上传流程见下图[1]

2

1

FastDFS上传流程

流程图

3

相关概念

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件 的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需 要客户端自行保存。

虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了

store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。

数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据
文件。

文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储

服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

访问地址说明

文件上传完毕后,会返回一个地址,如下

1

group1是组名

M是只ian过了Storage组中某个节点的主文件夹地址,比如/etc/fdfs/data

02/44是通过算法算出来的2个子目录

wKg…sh是文件的名字

最后通过 http://地址:端口/group1/M00/02/44/xxxxx.jpg就可以访问上传完毕的资源

FastDFS在Docker中的安装

拉取镜像

1
$ docker pull morunchang/fastdfs

运行Tracker

复制以下代码直接运行即可

1
$ docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh

–net=host 是网络模式

运行Storage

复制一下代码,修改IP和组名

1
$ docker run -d --name storage --net=host -e TRACKER_IP=192.168.162.130:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh

–net=host 是网络模式

192.168.162.130 : 是虚拟机的IP

group1是当前组的名字,如果想新增storage服务器,再次运行该命令,注意更换组名

修改容器随docker启动而启动

1
$ docker update --restart=always tracker
1
$ docker update --restart=always storage

查看或修改nginx配置(非必须步骤)

storage和tracker安装完毕后都nginx服务器,访问地址http://虚拟机IP:8080,配置文件所在目录为 /etc/nginx。默认配置可以改,也可以不改

进入容器

1
$ docker exec -it storage  /bin/bash

打开nginx配置文件

1
$ vi /etc/nginx/conf/nginx.conf

SpringBoot项目构建文件存储服务

引入FastDFS的依赖

1
2
3
4
5
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>

在resources目录里引入配置文件

fdfs_client.conf

1
2
3
4
5
6
7
8
9
10
# 连接超时时间,单位为秒
connect_timeout = 60
# 通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后
还不能发送或接收数据,则本次网络通信失败
network_timeout = 60
charset = UTF-8
# tracker服务的nginx端口,默认8080
http.tracker_http_port = 8080
# tracker服务器地址 虚拟机地址:22122 22122是默认端口
tracker_server = 192.168.162.130:22122

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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是设置总上传的数据大小

如果不是微服务项目eureka和feign配置可以省略

创建springboot启动程序

因为当前项目不需要并且如果没有引入数据库,所以要配置exclude = {DataSourceAutoConfiguration.class},否则无法启动

1
2
3
4
5
6
7
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class,args);
}
}

文件信息封装

文件上传一般都有文件的名字、文件的内容、文件的扩展名、文件的md5值、文件的作者等相关属性,我们可以创建一个对象封装这些属性,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FastDFSFile implements Serializable{
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;

public FastDFSFile(String name, byte[] content, String ext) {
this.name = name;
this.content = content;
this.ext = ext;
}
}

FastDFS工具类封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package com.changgou.file.utils;
import com.changgou.file.FastDFSFile;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FastDFSClient {

private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class);

/***
* 初始化加载FastDFS的TrackerServer配置
*/
static {
try {
String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
ClientGlobal.init(filePath);
} catch (Exception e) {
logger.error("FastDFS Client Init Fail!",e);
}
}

/***
* 文件上传
* @param file
* @return 1.文件的组名 2.文件的路径信息
*/
public static String[] upload(FastDFSFile file) {
//获取文件的作者
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] = new NameValuePair("author", file.getAuthor());

//接收返回数据
String[] uploadResults = null;
StorageClient storageClient=null;
try {
//创建StorageClient客户端对象
storageClient = getTrackerClient();

/***
* 文件上传
* 1)文件字节数组
* 2)文件扩展名
* 3)文件作者
*/
uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
} catch (Exception e) {
logger.error("Exception when uploadind the file:" + file.getName(), e);
}

if (uploadResults == null && storageClient!=null) {
logger.error("upload file fail, error code:" + storageClient.getErrorCode());
}
//获取组名
String groupName = uploadResults[0];
//获取文件存储路径
String remoteFileName = uploadResults[1];
return uploadResults;
}

/***
* 获取文件信息
* @param groupName:组名
* @param remoteFileName:文件存储完整名
* @return
*/
public static FileInfo getFile(String groupName, String remoteFileName) {
try {
StorageClient storageClient = getTrackerClient();
return storageClient.get_file_info(groupName, remoteFileName);
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}

/***
* 文件下载
* @param groupName
* @param remoteFileName
* @return
*/
public static InputStream downFile(String groupName, String remoteFileName) {
try {
//创建StorageClient
StorageClient storageClient = getTrackerClient();

//下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
InputStream ins = new ByteArrayInputStream(fileByte);
return ins;
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}

/***
* 文件删除
* @param groupName
* @param remoteFileName
* @throws Exception
*/
public static void deleteFile(String groupName, String remoteFileName)
throws Exception {
//创建StorageClient
StorageClient storageClient = getTrackerClient();

//删除文件
int i = storageClient.delete_file(groupName, remoteFileName);
}

/***
* 获取Storage组
* @param groupName
* @return
* @throws IOException
*/
public static StorageServer[] getStoreStorages(String groupName)
throws IOException {
//创建TrackerClient
TrackerClient trackerClient = new TrackerClient();
//获取TrackerServer
TrackerServer trackerServer = trackerClient.getConnection();
//获取Storage组
return trackerClient.getStoreStorages(trackerServer, groupName);
}

/***
* 获取Storage信息,IP和端口
* @param groupName
* @param remoteFileName
* @return
* @throws IOException
*/
public static ServerInfo[] getFetchStorages(String groupName,
String remoteFileName) throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
}

/***
* 获取Tracker服务地址
* @return
* @throws IOException
*/
public static String getTrackerUrl() throws IOException {
return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port()+"/";
}

/***
* 获取Storage客户端
* @return
* @throws IOException
*/
private static StorageClient getTrackerClient() throws IOException {
TrackerServer trackerServer = getTrackerServer();
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient;
}

/***
* 获取Tracker
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerServer;
}
}

在控制器中编写上传的功能

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/file")
@CrossOrigin
public class FileController {
@PostMapping("/upload")
public Result upload(@RequestParam(value = "file") MultipartFile file)throws Exception{
FastDFSFile fastDFSFile = new FastDFSFile(file.getName(),file.getBytes(), StringUtils.getFilenameExtension(file.getOriginalFilename()));
String[] groupAndPath = FastDFSClient.upload(fastDFSFile);
return new Result(true, StatusCode.OK,"文件上传成功",FastDFSUtils.getTrackerUrl()+groupAndPath[0]+"/"+groupAndPath[1]);
}
}

测试文件上传

5

data中的地址值就是上传后文件的访问地址,直接访问即可

思考:文件上传到哪了?

分析地址信息:http://192.168.162.130:8080/group1/M00/00/00/wKiigl5g4RmAQQBnAAAq8ZibPHI365.jpg

文件在group1服务器

登录对应的storage服务器,执行以下命令查看storage的配置文件

1
$ cat /etc/fdfs/storage.conf

根据以下步骤定位文件路径

一个group服务器上可以有多个路径,可以写多个store_path_数字,这里store_path_0对应的就是M00,依次类推M11对应的就是store_path_11

6

文章作者: 微信:hao_yongliang
文章链接: https://haoyongliang.gitee.io/2020/03/01/%E5%B7%A5%E5%85%B7/FastDFS/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 郝永亮的主页
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论