1. 学习FastDFS的原因
- 在分布式集群环境下,文件上传至节点A,这时通过负载均衡算法,访问到节点B,则不能访问到文件,这时
会出现有时能访问有时不能访问的问题。 - 同时要考虑为文件做冗余备份、负载均衡、线性扩容等功能,这些都是单节点文件上传所不具备的。
2. FastDFS概念
FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
3. FastDFS架构
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
注意:如果有一天,我们的存储容量不够了,我们可以扩展Storage群的组(group),比如卷n+1来提供服务。这样相对于重新使用一块大的存储空间来说,可以节约成本,这就是线性扩容。
3.1 调度服务器Tracker server
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据负载均衡的策略(例如轮询、随机)找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。
3.2 存储服务器Storage server
Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
3.3 文件上传流程
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 就是用于以后访问该文件的索引信息。文件索引信息包括:
-
组名
文件上传后所在的 storage 组名称,在文件上传成功后由storage 服务器返回,需要客户端自行保存。
-
虚拟磁盘路径
storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了 store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
注:虚拟路径需要在 storage.conf 文件中配置,实际上还是使用操作系统的文件系统来管理,只不过这里做了映射。
-
数据两级目录
storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
注:以文件名通过哈希算法,得到数据的两级目录,保证每个目录下的数据量不会太大,从而在某种程度上保证了文件检索的速度。
-
文件名
与文件上传时不同。是由存储服务器根据特定信息随机生成的,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
4. FastDFS安装
这里以Docker安装FastDFS为例。
-
拉取镜像;
docker pull morunchang/fastdfs
-
创建并后台运行tracker容器;
docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
- –net=host :使用的网络模式是–net=host,host模式可以不用映射容器端口宿主机, 替换为你机器的ip即可;
- sh tracker.sh :进入这个tracker容器后需要执行的初始化命令。
-
创建并后台运行storage容器;
docker run -d --name storage --net=host -e TRACKER_IP=<your tracker server address>:22122 -e GROUP_NAME=<group name> morunchang/fastdfs sh storage.sh
- –net=host :使用的网络模式是–net=host,host模式可以不用映射容器端口宿主机, 替换为你机器的ip即可;
- TRACKER_IP=:22122 :指定tracker server的ip地址;
- -e GROUP_NAME= :-e指定参数,指定这个storage server的组名是什么;
- sh storage.sh :进入这个 storage 容器后需要执行的初始化命令。
注意:如果想要增加新的storage服务器,再次运行该命令,需要注意的是要更换新组名。
-
修改nginx的配置;
FastDFS中内置了一个nginx服务器,它可以提供文件下载的功能。
#(1)进入storage容器的内部 docker exec -it storage /bin/bash #(2)编辑nginx的配置文件 vi /etc/nginx/conf/nginx.conf #(3)
修改以下内容(前两行已经存在,可以不用改)
location ~ /M00 { root /data/fast_data/data; ngx_fastdfs_module;#当我们访问M00的Http请求时,它把这个请求反向代理到了ngx_fastdfs_module 模块 add_header Cache-Control no-store; #禁用浏览器的缓存 }
-
重启storage容器;
docker restart storage
-
查看tracker.conf和storage.conf配置文件。
docker exec -it storage /bin/bash cd /etc/fdfs vim tracker.conf vim storage.conf
5. 案例-创建一个文件上传的微服务
以之前springcloud的学习案例为基础,创建文件上传微服务upload-service,通过fastdfs-client组件实现文件上传和删除的功能。
5.1 搭建子工程
在spring cloud项目中创建子工程,并引入如下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>springcloud</artifactId>
<groupId>com.lijinghua</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.lijinghua</groupId>
<artifactId>upload-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>upload-service</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- FastDFS依赖 -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
</dependency>
</dependencies>
</project>
5.2 编辑 application.yml 配置文件
server:
port: 9000
logging:
#file: demo.log
level:
org.springframework.web: debug
com.lijinghua: debug
pattern:
console: "%d - %msg%n"
spring:
application:
name: upload-service
#SpringMVC multipart-file相关配置
servlet:
multipart:
enabled: true
max-file-size: 10MB #单个文件上传总大小
max-request-size: 20MB #总文件上传大小
fdfs:
#连接超时
connect-timeout: 60
#读取超时
so-timeout: 60
#生成缩略图参数
thumb-image:
width: 150
height: 150
#tracker server 地址
tracker-list: 192.168.192.130:22122
eureka:
instance:
#更倾向于使用ip地址,而不是主机名
prefer-ip-address: true
#ip地址
ip-address: 127.0.0.1
#续约间隔,默认30秒
lease-renewal-interval-in-seconds: 5
#服务的实效时间, 默认90秒
lease-expiration-duration-in-seconds: 5
client:
service-url:
defaultZone: http://127.0.0.1:10000/eureka,http://127.0.0.1:10001/eureka,http://127.0.0.1:10002/eureka
5.3 编写FastDfs的配置类
引入 com.github.tobato.fastdfs.FdfsClientConfig 类即可。
@Configuration
@Import(FdfsClientConfig.class)
public class DfsConfig {
}
5.4 编写工具类FileDfsUtil
调用fastdfs-client工具方法实现文件上传和删除。
@Component
public class FileDfsUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(FileDfsUtil.class);
private final FastFileStorageClient storageClient;
@Autowired
public FileDfsUtil(FastFileStorageClient storageClient) {
this.storageClient = storageClient;
}
/**
* 上传文件功能
*
* @param multipartFile 要上传的文件
* @return
*/
public String upload(MultipartFile multipartFile) throws IOException {
//获取文件扩展名
String extName = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
//上传图片文件以及缩略图,返回了一个存储路径,即file_id
StorePath storePath = storageClient.uploadImageAndCrtThumbImage(
multipartFile.getInputStream(),//文件的输入流
multipartFile.getSize(),//文件的大小
extName,
null//元数据信息,例如作者姓名
);
return storePath.getFullPath();
}
/**
* 删除文件功能
*
* @param fileUrl 要删除的文件url
*/
public void delete(String fileUrl) {
if (StringUtils.isEmpty(fileUrl)) {
LOGGER.info("fileUrl == >>文件路径为空...");
return;
}
try {
StorePath storePath = StorePath.parseFromUrl(fileUrl);
storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
} catch (Exception e) {
LOGGER.info(e.getMessage());
}
}
}
5.5 创建FileController
创建文件上传和删除功能的controller,实现文件删除。
@RestController
public class FileController {
private final FileDfsUtil fileDfsUtil;
@Autowired
public FileController(FileDfsUtil fileDfsUtil) {
this.fileDfsUtil = fileDfsUtil;
}
/**
* 文件上传
*
* @param file
* @return
*/
@RequestMapping(value = "upload", method = RequestMethod.POST, headers = "content-type=multipart/form-data")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
String result;
try {
String path = fileDfsUtil.upload(file);
if (!StringUtils.isEmpty(path.trim())) {
result = path;
} else {
result = "上传失败";
}
} catch (Exception e) {
return new ResponseEntity("服务异常", HttpStatus.INTERNAL_SERVER_ERROR);
}
return ResponseEntity.ok(result);
}
/**
* 文件删除
* @param filePathName file_id
*/
@RequestMapping(value = "/deleteByPath", method = RequestMethod.GET)
public ResponseEntity<String> deleteByPath(String filePathName) {
// String filePathName = "group1/M00/00/00/wKhjZF3WEDmAPSglAABSZAhj0eU111.jpg" ;
String result = "删除成功";
try {
fileDfsUtil.delete(filePathName);
} catch (Exception e) {
result = "服务异常";
}
return ResponseEntity.ok(result);
}
}