部署和使用单机版 FastDFS 分布式文件系统

发布时间 2023-09-04 04:00:23作者: 乔京飞

我们工作中经常会有上传和下载文件的需求,早些年代我们一般会将上传的文件保存在网站所在的服务器上,但是现在一般网站都是负载均衡多服务器部署,因此必须要有独立的文件服务器才行。早些年代,如果有一台独立的文件服务器,一般会搭建 NFS 共享服务,给多个网站服务器之间使用。如果有多台文件服务器的话,各个服务器之间通过 rsync 进行文件的同步。

以上方案随着时代的发展,基本上都淘汰了,部署和使用过于复杂。现在比较流行的是部署 FastDFS 分布式文件系统,解决文件服务器的高可用以及多个文件服务器之间的实时同步问题。采用容器化技术(比如 Docker )部署维护非常简单,代码操作也很方便。

FastDFS 是阿里巴巴的余庆大神采用 C 语言开发的一项开源轻量级分布式文件系统,对文件进行管理(文件存储,文件同步,文件上传/下载/删除)等,特别适合以文件为载体的在线服务,如图片网站,视频网站。从2008年研发 FastDFS 开源至今,在国内很多互联网公司中备受推崇。

本篇博客采用 DockerCompose 快速实现单机版 FastDFS 分布式文件系统部署,编写 Demo 代码连接 FastDFS 服务器实现文件的上传、下载和删除操作,在博客的最后会提供源代码的下载。

FastDFS 的官网地址:https://github.com/happyfish100/fastdfs (国外网站可能访问较慢或无法访问)


一、FastDFS 架构简介

FastDFS 架构包括 TrackerServer 和 StorageServer。客户端请求 Tracker server 进行文件上传、下载,通过 TrackerServer 调度具体的 StorageServer 完成文件上传和下载。TrackerServer 作用是负载均衡和调度,通过 TrackerServer 在文件上传时可以根据一些策略找到 StorageServer 提供文件上传服务,客户端上传的文件最终存储在 StorageServer服务器上,Storageserver 没有实现自己的文件系统,而是利用操作系统的文件系统来管理文件。

FastDFS 中每个 TrackerServer 节点地位平等,收集 StorageServer 集群的状态。StorageServer 可以分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部文件会实时同步,保存的内容是一样的。组成员的地位是一致的,没有主从的概念。

image


二、搭建单机版 FastDFS

我的虚拟机 ip 地址是:192.168.136.128 ,已经安装好了 docker 和 docker-compose

首先创建 2 个目录,用来保存 TrackerServer 和 StorageServer 的数据

mkdir -p /app/fastdfs/tracker
mkdir -p /app/fastdfs/storage

然后进入 /app/fastdfs 目录,编写 docker-compose.yml 文件,内容如下:

version: '3.5'
services:
  tracker:
    image: delron/fastdfs
    container_name: tracker
    network_mode: host
    volumes:
      # 配置 TrackerServer 映射到宿主机的文件存放目录
      - /app/fastdfs/tracker:/var/fdfs
    command: tracker
  storage:
    image: delron/fastdfs
    container_name: storage
    network_mode: host
    volumes:
      # 配置 StorageServer 映射到宿主机的文件存放目录
      - /app/fastdfs/storage:/var/fdfs
    environment:
      # 配置 TrackerServer 的连接地址
      - TRACKER_SERVER=192.168.136.128:22122
      # 配置 StorageServer 文件组的名称
      - GROUP_NAME=group1
    command: storage
    depends_on:
      - tracker

具体细节如下:

  • 采用的 docker 镜像是 delron/fastdfs ,镜像中既包含 TrackerServer 也包含 StorageServer,主要根据运行的 command 来决定是那种角色
  • TrackerServer 的默认连接端口是 22122,StorageServer 的默认连接端口是 23000
  • delron/fastdfs 镜像中自带了 nginx ,默认端口是 8888,用于转发 StorageServer 中的文件请求

你会发现这次的 docker-compose.yml 中,TrackerServer 和 StorageServer 使用的网络(network_mode)是 host,我们没有使用 bridge(Docker 默认的网络模式),首先要说明一下这 2 种网络模式的区别:

  • Host 模式不会虚拟出自己的网卡,配置自己的 ip,而是使用宿主机的 ip 和端口。如果启动容器的时候使用 host 模式,那么这个容器将不会获得一个独立的 NetworkNamespace,而是和宿主机共用一个 Network Namespace。但是容器的文件系统、进程列表等还是和宿主机隔离的。
    使用host模式的容器可以直接使用宿主机的Ip地址和端口与外界通信,不需要进行NAT,最大的优势就是网络性能比较好,但是宿主机上已经使用的端口就不能再用了,网络的隔离性不好。

  • Bridge 模式是 Docker 默认的模式,该模式会为每一个容器分配、设置 ip ,并将容器连接到一个 docker0 虚拟网桥,通过 docker0 网桥以及 iptables nat 表配置与宿主之间的关联。也可以创建自定义的 bridge 网桥,连接自己创建的容器,从而使容器之前能够互相访问。

为什么我们这次部署 FastDFS 采用的是 Host 模式,而不是 Bridge 模式?

原因是采用 Bridge 模式部署的话,我们当前 PC 电脑上使用 IDEA 开发运行的代码无法连接 StorageServer 服务器操作文件。

我们的代码操作 FastDFS 的流程为:

首先访问 TrackerServer 拿到 StorageServer 的地址,然后代码连接 StorageServer 进行文件操作。

当采用 Bridge 模式部署 FastDFS 时,TrackerServer 和 StorageServer 被分配的是桥接网络的 ip 地址(比如 172.17.0.x 网段),StorageServer 注册到 TrackerServer 的地址是 172.17.0.x 的容器内部 ip 地址。因此即使 TrackerServer 和 StorageServer 都可以通过端口映射将 22122 和 23000 映射到宿主机的 192.168.136.128 ,我们的程序代码可以通过宿主机 192.168.136.128 的端口 22122 访问 TrackerServer ,但是拿到的 StorageServer 是容器的内部 ip(172.17.0.x),因此程序无法访问 StorageServer 。

只有当我们开发好的代码,也打包部署到 docker 中,并且跟 TrackerServer 和 StorageServer 使用相同的桥接网络时,才能正常运行。因此我们这里代码开发使用的 docker-compose.yml 部署 FastDFS 使用 Host 网络。

接下来在 docker-compose.yml 所在的目录下,运行命令启动两个容器即可:

# 进入 docker-compose.yml 所在目录
cd /app/fastdfs
# 启动 FastDFS 的 TrackerServer 和 StorageServer 
docker-compose up -d

容器启动之后,我们可以通过以下地址访问 FastDFS 服务器:

  • 通过 192.168.136.128:22122 访问 TrackerServer
  • 通过 192.168.136.128:23000 访问 StorageServer
  • 通过 http://192.168.136.128:8888 访问 nginx(我们可以通过 nginx 直接访问 StorageServer 中上传的文件)

因此如果你的服务器防火墙开着的话,需要开放 22122 、23000 、8888 这 3 个端口


三、搭建工程

新建一个 SpringBoot 工程,结构如下所示:

image

WebMvcConfig 用来配置 Knife4j 文档,我们通过 Knife4j 文档可视化页面访问接口进行测试

FastDfsController 提供操作 FastDFS 分布式文件系统的接口,这些接口会显示在 Knife4j 文档页面中

FastDfsService 提供操作 FastDFS 的方法,供 FastDfsController 中的接口进行调用

项目工程的 pom 文件如下:

<?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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springboot_fastdfs</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>
        <!--引入 fastdfs-client 依赖,可以操作 fastdfs 服务器-->
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
            <version>1.27.2</version>
        </dependency>
        <!--使用 knife4j 功能,为了可以使用 web 界面测试接口-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

项目的 application.yml 配置文件内容如下:

server:
  port: 8090

knife4j:
  # 是否启用增强版功能
  enable: true
  # 如果是生产环境,将此设置为 true,然后就能够禁用了 knife4j 的页面
  production: false

fdfs:
  # 获取文件的时间(这里配置 2000 毫秒)
  so-timeout: 2000
  # 连接超时的时间(这里配置为 1000 毫秒)
  connect-timeout: 1000
  # 如果上传的是图片,可以让服务器生成缩略图,配置缩略图尺寸
  #thumb-image:
  #  width: 200
  #  height: 200
  # TrackerList 路由服务器,如果搭建了集群,可以配置多个
  tracker-list:
    - 192.168.136.128:22122
  # 直接访问文件的 nginx 的地址,
  # 建议后面以斜线(/)结尾,方便进行文件的地址拼接
  web-server-url: http://192.168.136.128:8888/

Spring:
  servlet:
    multipart:
      # 单个文件上传大小限制
      max-file-size: 100MB
      # 如果同时上传多个文件,上传的总大小限制
      max-request-size: 100MB

四、代码细节

首先列出 FastDfsService 的方法,具体内容如下:

package com.jobs.service;

import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.domain.proto.storage.DownloadByteArray;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;

@Slf4j
@Service
public class FastDfsService {

    //从 yml 配置文件中获取 tracker 的 client 操作对象
    //如果搭建了 tracker 集群,默认情况下负载均衡策略是轮询
    @Autowired
    private FastFileStorageClient client;

    //从 yml 中获取配置的 web server 地址
    //如果搭建的是集群的话,建议再额外搭建一个总的 nginx 转发所有的 storage 上部署的 nginx 地址
    //然后 yml 中就可以配置为总的 nginx 的访问地址
    @Autowired
    private FdfsWebServer webServer;

    //获取资源的 web 访问地址
    public String getFileFullUrl(String fdfsPath) {
        return webServer.getWebServerUrl() + fdfsPath;
    }

    //上传文件
    public String uploadFile(MultipartFile file) {
        try {
            StorePath storePath = client.uploadFile(file.getInputStream(), file.getSize(),
                    FilenameUtils.getExtension(file.getOriginalFilename()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //上传文件
    public String uploadFile(File file) {
        try {
            if (file.isDirectory()) {
                log.error("请上传文件,不要上传文件夹");
                return null;
            }

            FileInputStream inputStream = new FileInputStream(file);
            File f = new File("");
            StorePath storePath = client.uploadFile(inputStream, file.length(),
                    FilenameUtils.getExtension(file.getName()), null);
            return webServer.getWebServerUrl() + storePath.getFullPath();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //下载文件
    public byte[] downloadFile(String fdfsPath) {
        if (StringUtils.isEmpty(fdfsPath)) {
            return null;
        }

        try {
            StorePath storePath = StorePath.parseFromUrl(fdfsPath);
            return client.downloadFile(storePath.getGroup(), storePath.getPath(), new DownloadByteArray());
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    //删除文件
    public Boolean deleteFile(String fdfsPath) {
        if (StringUtils.isEmpty(fdfsPath)) {
            return false;
        }

        try {
            StorePath storePath = StorePath.parseFromUrl(fdfsPath);
            client.deleteFile(storePath.getGroup(), storePath.getPath());
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
}

然后列出 FastDfsController 的接口代码,具体内容如下:

package com.jobs.controller;

import com.jobs.service.FastDfsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@Api(tags = "FastDFS 操作接口")
@RestController
@RequestMapping("/fdfs")
public class FastDfsController {

    @Autowired
    private FastDfsService fastDfsService;

    @ApiOperation("上传文件")
    @ApiImplicitParam(name = "file", value = "上传的目标文件",
            dataType = "java.io.File", paramType = "query", required = true)
    @PostMapping("/upload")
    //注意:针对 MultipartFile 需要增加 @RequestPart 注解,否则 knife4j 接口无法显示文件上传操作。
    public Map uploadFile(@RequestPart("file") MultipartFile file) {
        String fdfsPath = fastDfsService.uploadFile(file);

        Map<String, String> map = new HashMap<>();
        if (StringUtils.isNotBlank(fdfsPath)) {
            map.put("fdfs_path", fdfsPath);
            map.put("file_url", fastDfsService.getFileFullUrl(fdfsPath));
        }

        return map;
    }

    //必须要加上 produces = "application/octet-stream" ,否则 knife4j 接口无法下载文件
    @ApiOperation(value = "下载文件", produces = "application/octet-stream")
    @ApiImplicitParam(name = "fdfsPath", value = "fdfs文件路径(不包含web域名)", required = true)
    @GetMapping("/download")
    public void downloadFile(String fdfsPath, HttpServletResponse response) {
        //获取文件名(包含后缀名)
        String fileName = FilenameUtils.getName(fdfsPath);
        byte[] bytes = fastDfsService.downloadFile(fdfsPath);
        if (bytes != null) {
            try {
                //下载文件的响应类型,这里统一设置成了文件流
                //你可以根据自己所提供下载的文件类型,使用不同的响应 mime 类型
                response.setContentType("application/octet-stream;charset=utf-8");
                //设置下载弹出框中默认显示的文件名称,如果指定中文名称的话,需要转成 iso8859-1 编码,解决乱码问题
                fileName = new String(fileName.getBytes(), "iso8859-1");
                response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
                IOUtils.write(bytes, response.getOutputStream());

            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    @ApiOperation("删除文件")
    @ApiImplicitParam(name = "fdfsPath", value = "fdfs文件路径(不包含web域名)", required = true)
    @PostMapping("/delete")
    public String deleteFile(String fdfsPath) {
        Boolean flag = fastDfsService.deleteFile(fdfsPath);
        return flag ? "delete success" : "delete fail";
    }
}

五、测试验证

启动项目(我们配置的启动端口是 8090),访问 http://localhost:8090/doc.html 打开 knifi4j 接口文档界面

image

点击左侧【上传文件】,打开上传文件的界面,我们这里上传一张图片进行测试,当然上传任何文件类型都可以的。

image

把 file_url 复制一下,粘贴到浏览器地址栏上,直接访问 StorageServer 中存储的图片文件

image

点击 knife4j 文档左侧【下载文件】,打开下载文件的界面,把上传文件接口返回的 fdfs_path 的值输入,然后发送请求

image

点击 knife4j 文档左侧【删除文件】,打开删除文件接口,把上传文件接口返回的 fdfs_path 的值输入,然后发送请求

image

图片删除成功之后,我们再使用 Ctrl + F5 强制刷新(去除缓存)直接 web 访问图片的页面,可以发现已经是 404 了,图片已经不存在了

image


Ok 到此为止,有关单机版 FastDFS 的部署以及代码操作已经介绍完毕,希望对大家有用。

本篇博客的 Demo 源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_fastdfs.zip