一、关于

FastDFS是一款开源的分布式文件系统,功能主要包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了文件大容量存储和高性能访问的问题。FastDFS可以看做是基于文件的key value存储系统,key为文件ID,value为文件内容。

FastDFS特点:

  • 分组存储,简单灵活;
  • 对等结构,不存在单点;
  • 文件ID由FastDFS生成,作为文件访问凭证。FastDFS不需要传统的name server或meta server;
  • 大、中、小文件均可以很好支持,可以存储海量小文件;
  • 一台storage支持多块磁盘,支持单盘数据恢复;
  • 提供了nginx扩展模块,可以和nginx无缝衔接;
  • 支持多线程方式上传和下载文件,支持断点续传;
  • 存储服务器上可以保存文件附加属性。

二、结构

https://gitee.com/fastdfs100/fastdfs/raw/master/images/architect.png

tracker-server

跟踪服务器, 主要做调度工作, 起负载均衡的作用。 在内存中记录集群中所有存储组和存储服务器的状态信息, 是客户端和数据服务器交互的枢纽。 相比GFS中的master更为精简, 不记录文件索引信息, 占用的内存量很少。

storage-server

存储服务器( 又称:存储节点或数据服务器) , 文件和文件属性( metadata) 都保存到存储服务器上。 Storage server直接利用OS的文件系统调用管理文件。

group

组, 也可称为卷。 同组内服务器上的文件是完全相同的 ,同一组内的storage server之间是对等的, 文件上传、 删除等操作可以在任意一台storage server上进行 。

meta data

文件相关属性,键值对( Key Value Pair) 方式,如:width=1024,heigth=768 。

三、部署

相关配置路径如下:

/opt/fastdfs
├── build
│   ├── fastdfs                # fastdfs源码
│   ├── fastdfs-nginx-module   # fastdfs-nginx-module源码
│   └── libfastcommon          # libfastcommon源码
├── client                     # fastdfs client组件配置路径
├── nginx                      # nginx编译路径
│   ├── nginx-1.18.0           
│   └── nginx-1.18.0.tar.gz
├── storage                    
│   ├── base                   # storage数据和日志文件存储根目录
│   └── store                  # 数据和日志文件存储根目录
└── tracker                    # tracker存储日志和数据的根目录
3.1 安装相关依赖

yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim -y

3.2 安装libfatscommon

下载代码,编译安装

git clone https://github.com/happyfish100/libfastcommon.git --depth 1
cd libfastcommon/
./make.sh && ./make.sh install 
3.3 安装FastDFS

下载代码,编译安装

git clone https://github.com/happyfish100/fastdfs.git --depth 1
cd fastdfs/
./make.sh && ./make.sh install

复制配置文件,供nginx访问使用

cp ./conf/http.conf /etc/fdfs/
cp ./conf/mime.types /etc/fdfs/
3.4 安装fastdfs-nginx-module

git clone https://github.com/happyfish100/fastdfs-nginx-module.git –depth 1
cp ./fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs

3.5 安装nginx

下载nginx压缩包,并解压

wget http://nginx.org/download/nginx-1.18.0.tar.gz
tar -zxvf nginx-1.18.0.tar.gz

添加fastdfs-nginx-module模块并编译安装

cd nginx-1.18.0/
./configure --add-module=/opt/fastdfs/build/fastdfs-nginx-module/src --prefix=/usr/local/nginx/
make && make install

我这里在执行时报了以下错误:

...
configuring additional modules
adding module in /opt/fastdfs/build/fastdfs-nginx-module/src
/opt/fastdfs/build/fastdfs-nginx-module/src/config: line 2: $'\r': command not found
/opt/fastdfs/build/fastdfs-nginx-module/src/config: line 23: syntax error: unexpected end of file
 was configuredtdfs_module
...

可以使用dos2unix对文件重新编码:

dos2unix /opt/fastdfs/build/fastdfs-nginx-module/src/config
3.6 配置tracker

vim /etc/fdfs/tracker.conf

port=22122  # tracker服务器端口(默认22122,一般不修改)
base_path = /opt/fastdfs/tracker
3.7 配置storage

vim /etc/fdfs/storage.conf

port = 23000
base_path = /opt/fastdfs/storage/base # 数据和日志文件存储根目录
store_path0 = /opt/fastdfs/storage/store # 存储目录
tracker_server = 192.168.18.55:22122 # tracker服务地址
http.server_port = 80  # http访问文件的端口(默认8888,看情况修改,和nginx中保持一致)
3.8 配置client

vim /etc/fdfs/client.conf

base_path=/opt/fastdfs/client
tracker_server=192.168.18.55:22122  # tracker服务器
3.9 配置nginx

vim /etc/fdfs/mod_fastdfs.conf

tracker_server=192.168.52.2:22122  # tracker服务地址
url_have_group_name=true #  url 地址是否包含组名/卷名
store_path0=/opt/fastdfs/storage/store # 存储目录,同storage.conf配置

vim /usr/local/nginx/conf/nginx.conf

server {
    listen       80;    ## 该端口为storage.conf中的http.server_port相同
    server_name  localhost;
    location ~/group[0-9]/ {
        ngx_fastdfs_module;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   html;
    }
}
3.10 启动
# 启动nginx
/usr/local/nginx/sbin/nginx
# 启动fastdfs
systemctl start fdfs_trackerd
systemctl start fdfs_trackerd
3.11 测试上传文件
fdfs_upload_file /etc/fdfs/client.conf ./th.jpg

上传命令后会输出地址

group1/M00/00/00/wKgSN2IdkAGAV4x7AAU-vmtQx1I522.jpg

使用得到的地址作为下载命令的参数:

fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/wKgSN2IdkAGAV4x7AAU-vmtQx1I522.jpg

使用浏览器访问可以看到上传的文件

http://192.168.18.55/group1/M00/00/00/wKgSN2IdkAGAV4x7AAU-vmtQx1I522.jpg

3.12 分布式部署

tracker配置

#服务器ip为 192.168.52.2,192.168.52.3,192.168.52.4
#我建议用ftp下载下来这些文件 本地修改
vim /etc/fdfs/tracker.conf
#需要修改的内容如下
port=22122  # tracker服务器端口(默认22122,一般不修改)
base_path=/home/dfs  # 存储日志和数据的根目录

storage配置

vim /etc/fdfs/storage.conf
#需要修改的内容如下
port=23000  # storage服务端口(默认23000,一般不修改)
base_path=/home/dfs  # 数据和日志文件存储根目录
store_path0=/home/dfs  # 第一个存储目录
tracker_server=192.168.52.2:22122  # 服务器1
tracker_server=192.168.52.3:22122  # 服务器2
tracker_server=192.168.52.4:22122  # 服务器3
http.server_port=8888  # http访问文件的端口(默认8888,看情况修改,和nginx中保持一致)

client测试

vim /etc/fdfs/client.conf
#需要修改的内容如下
base_path=/home/moe/dfs
tracker_server=192.168.52.2:22122  # 服务器1
tracker_server=192.168.52.3:22122  # 服务器2
tracker_server=192.168.52.4:22122  # 服务器3
#保存后测试,返回ID表示成功 如:group1/M00/00/00/xx.tar.gz
fdfs_upload_file /etc/fdfs/client.conf /usr/local/src/nginx-1.15.4.tar.gz

配置nginx访问

vim /etc/fdfs/mod_fastdfs.conf
#需要修改的内容如下
tracker_server=192.168.52.2:22122  # 服务器1
tracker_server=192.168.52.3:22122  # 服务器2
tracker_server=192.168.52.4:22122  # 服务器3
url_have_group_name=true
store_path0=/home/dfs

#配置nginx.conf

vim /usr/local/nginx/conf/nginx.conf
#添加如下配置
server {
    listen       8888;    ## 该端口为storage.conf中的http.server_port相同
    server_name  localhost;
    location ~/group[0-9]/ {
        ngx_fastdfs_module;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   html;
    }
}

检测集群,会显示会有几台服务器 有3台就会 显示 Storage 1-Storage 3的详细信息

/usr/bin/fdfs_monitor /etc/fdfs/storage.conf

四、Java调用

引入依赖

作者提供了Java的库,可以通过https://github.com/happyfish100/fastdfs-client-java下载源代码导入本地仓库,我这里直接引入的jar包文件

    <dependency>
      <groupId>org.csource</groupId>
      <artifactId>fastdfs-client-java</artifactId>
      <version>1.29-SNAPSHOT</version>
      <scope>system</scope>
      <systemPath>${basedir}\src\main\resources\lib\fastdfs-client-java-1.29-SNAPSHOT.jar</systemPath>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.11.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.9</version>
    </dependency>
配置文件

fdfs_client.conf

# 超时时间
connect_timeout = 10
network_timeout = 30
# 编码字符集
charset = UTF-8
# tracker 服务器 HTTP 协议下暴露的端口
http.tracker_http_port = 8080
# tracker 服务器的 IP 和端口
tracker_server = 192.168.18.55:22122
工具类
public class FastDFSClient {

    // 获取配置文件地址
    private static final String CONF_FILENAME = Thread.currentThread()
            .getContextClassLoader().getResource("").getPath() + "fdfs_client.conf";
    // Storage 存储服务器客户端
    private static StorageClient storageClient = null;


    static {
        try {
            // 加载配置文件
            ClientGlobal.init(CONF_FILENAME);
            // 初始化 Tracker 客户端
            TrackerClient trackerClient = new TrackerClient(ClientGlobal.g_tracker_group);
            // 初始化 Tracker 服务端
            TrackerServer trackerServer = trackerClient.getTrackerServer();
            // 初始化 Storage 服务端
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
            // 初始化 Storage 客户端
            storageClient = new StorageClient(trackerServer, storageServer);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }

    /**
     * 文件上传
     *
     * @param inputStream 上传的文件的字节输入流
     * @param fileName    上传的文件的原始名
     * @return
     */
    public static String[] uploadFile(InputStream inputStream, String fileName) {
        try {
            // 准备字节数组
            byte[] fileBuff = null;
            // 文件元数据
            NameValuePair[] metaList = null;
            if (inputStream != null) {
                // 查看文件的长度
                int len = inputStream.available();
                // 初始化元数据数组
                metaList = new NameValuePair[2];
                // 第一组元数据,文件的原始名称
                metaList[0] = new NameValuePair("file_name", fileName);
                // 第二组元数据,文件的长度
                metaList[1] = new NameValuePair("file_length", String.valueOf(len));
                // 创建对应长度的字节数组
                fileBuff = new byte[len];
                // 将输入流中的字节内容,读到字节数组中
                inputStream.read(fileBuff);
            }
            /*
                上传文件。
                参数含义:要上传的文件的内容(使用字节数组传递),上传的文件的类型(扩展名),元数据
             */
            String[] fileids = storageClient.upload_file(fileBuff, getFileExt(fileName), metaList);
            return fileids;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 文件上传
     *
     * @param file     上传的文件
     * @param fileName 上传的文件的原始名
     * @return
     */
    public static String[] uploadFile(File file, String fileName) {
        try (FileInputStream fis = new FileInputStream(file)) {
            return uploadFile(fis, fileName);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取文件后缀名(不带点)
     *
     * @param fileName
     * @return 如:"jpg" or ""
     */
    private static String getFileExt(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf(".") + 1); // 不带最后的点
    }

    /**
     * 获取文件详情
     *
     * @param groupName      组/卷名,默认值:group1
     * @param remoteFileName 文件名,例如:"M00/00/00/wKgKZl9tkTCAJAanAADhaCZ_RF0495.jpg"
     * @return 文件详情
     */
    public static FileInfo getFileInfo(String groupName, String remoteFileName) {
        try {
            return storageClient.get_file_info(groupName == null ? "group1" : groupName, remoteFileName);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取元数据
     *
     * @param groupName      组/卷名,默认值:group1
     * @param remoteFileName 文件名,例如:"M00/00/00/wKgKZl9tkTCAJAanAADhaCZ_RF0495.jpg"
     * @return 文件的元数据数组
     */
    public static NameValuePair[] getMetaData(String groupName, String remoteFileName) {
        try {
            // 根据组名和文件名通过 Storage 客户端获取文件的元数据数组
            return storageClient.get_metadata(groupName == null ? "group1" : groupName, remoteFileName);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 文件下载
     *
     * @param groupName      组/卷名,默认值:group1
     * @param remoteFileName 文件名,例如:"M00/00/00/wKgKZl9tkTCAJAanAADhaCZ_RF0495.jpg"
     * @return 文件的字节输入流
     */
    public static InputStream downloadFile(String groupName, String remoteFileName) {
        try {
            // 根据组名和文件名通过 Storage 客户端获取文件的字节数组
            byte[] bytes = storageClient.download_file(groupName == null ? "group1" : groupName, remoteFileName);
            // 返回字节流对象
            InputStream inputStream = new ByteArrayInputStream(bytes);
            return inputStream;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void downloadFile(String groupName, String remoteFileName, String outPath) throws IOException, MyException {
        // 根据组名和文件名通过 Storage 客户端获取文件的字节数组
        byte[] bytes = storageClient.download_file(groupName == null ? "group1" : groupName, remoteFileName);
        // 返回字节流对象
        FileUtils.writeByteArrayToFile(new File(outPath), bytes);
    }

    /**
     * 文件删除
     *
     * @param groupName      组/卷名,默认值:group1
     * @param remoteFileName 文件名,例如:"M00/00/00/wKgKZl9tkTCAJAanAADhaCZ_RF0495.jpg"
     * @return 0为成功,非0为失败
     */
    public static int deleteFile(String groupName, String remoteFileName) {
        int result = -1;
        try {
            // 根据组名和文件名通过 Storage 客户端删除文件
            result = storageClient.delete_file(groupName == null ? "group1" : groupName, remoteFileName);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 修改一个已经存在的文件
     *
     * @param oldGroupName 旧组名
     * @param oldFileName  旧文件名
     * @param file         新文件
     * @param fileName     新文件名
     * @return
     */
    public static String[] modifyFile(String oldGroupName, String oldFileName, File file, String fileName) {
        // 先上传
        String[] fileids = uploadFile(file, fileName);
        if (fileids == null) {
            return null;
        }
        // 再删除
        int delResult = deleteFile(oldGroupName, oldFileName);
        if (delResult != 0) {
            return null;
        }
        return fileids;
    }
}
测试
public class FastDFSDemo {

    public static final String TEST_FILE_PATH = "E:\\images\\carrot03.png";
    public static final String TEST_FILE_NAME = "carrot03.png";
    public static final String TEST_GROUP_NAME = "";
    public static final String TEST_REMOTE_FILE_NAME = "";
    public static final String TEST_FILE_OUT_PATH = "E:\\images\\carrot03.download.png";

    public static void main(String[] args) throws IOException, MyException {
        // 上传文件
        String[] uploadInfo = FastDFSClient.uploadFile(new File(TEST_FILE_PATH), TEST_FILE_NAME);
        System.out.println(Arrays.toString(uploadInfo));
        // 获取文件详情
        final FileInfo fileInfo = FastDFSClient.getFileInfo(uploadInfo[0], uploadInfo[1]);
        System.out.println(fileInfo.toString());
        // 获取文件数据
        NameValuePair[] metaDatas = FastDFSClient.getMetaData(uploadInfo[0], uploadInfo[1]);
        for (NameValuePair metaData : metaDatas) {
            System.out.println(metaData.getName() + ":" + metaData.getValue());
        }
        // 文件下载
        FastDFSClient.downloadFile(uploadInfo[0], uploadInfo[1], TEST_FILE_OUT_PATH);
        // 文件删除
        int result = FastDFSClient.deleteFile("group1", "M00/00/00/wKgKZl9xMdiAcOLdAADhaCZ_RF0096.jpg");
        System.out.println("result = " + result);
    }
}