本文会搭建一个适合低业务访问业务量的非高可用的FastDFS集群环境:一个Tracker服务,一个storage group中两个storage服务节点;该场景不适合生产环境使用,生产环境应该增加Tracker服务数量使用负载均衡器负载或者使用keepalived进行主备,对于storage group应该搭建多个,每组storage group内节点至少在两个及以上。

在后台管理的web项目中,对于文件存储、上传、下载等操作是必不可少的。对于单节点部署的系统还可将文件存储在本机上进行操作,但是对于分布式部署的系统来说,文件存储操作问题就显现出来了,必须将文件集中存储在某处。目前一般的存储方案有:1.使用云厂商的对象存储服务;2.使用自建的文件服务器(ftp…);3.文件磁盘挂在方案;4.使用HDFS或FDFS等文件存储解决方案… 如果项目部署在可连接互联网的网络环境中可以使用云厂商的对象存储服务(安全、便利、低成本…);但是对于很多政企或者金融的项目大部分都是部署在不能连接互联网的网络环境中,那么我们就只能选择在内网搭建文件服务的方式。为了保证文件数据的安全性和完整性,本文使用分布式文件系统服务FastDFS来实践。

FastDFS简介

FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。FastDFS服务端有两个角色:跟踪器(tracker)和存储节点(storage)。跟踪器主要做调度工作,在访问上起负载均衡的作用。存储节点存储文件,完成文件管理的所有功能:就是这样的存储、同步和提供存取接口,FastDFS同时对文件的metadata进行管理。所谓文件的meta data就是文件的相关属性,以键值对(key value)方式表示,如:width=1024,其中的key为width,value为1024。文件metadata是文件属性列表,可以包含多个键值对。跟踪器和存储节点都可以由一台或多台服务器构成。跟踪器和存储节点中的服务器均可以随时增加或下线而不会影响线上服务。其中跟踪器中的所有服务器都是对等的,可以根据服务器的压力情况随时增加或减少。为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用。在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。FastDFS中的文件标识分为两个部分:卷名和文件名,二者缺一不可。(简介摘自百度百科)

原理介绍

文件上传

FastDFS以客户端库的方式提供基本的文件访问接口如upload、download、append、delete等,Storage 服务会定时的向Tracker服务发送自己的存储信息。当Tracker 服务集群中的Tracker 服务是多个时,各个Tracker服务之间的关系是对等的,因此客户端上传时会任意选择一个Trackre服务。当Tracker服务收到客户端上传文件请求时,会为该文件分配一个可以存储文件的group,当选定了group后就要决定给客户端分配group中的哪一个storage服务。当分配好storage 服务后,客户端向storage发送写文件请求,storage将会为文件分配一个数据存储目录。然后为文件分配一个文件ID标示,然后根据以上的信息生成文件名存储文件。

文件同步

上传文件后,客户端将文件写到group内的一个storage 服务即为上传文件成功,storage服务写完文件后,会由后台线程将文件同步至同group内的其他的storage 服务节点上。 每个storage服务写文件后,会同时写一份binlog,binlog里不包含文件数据,只包含文件名等元信息,这份binlog用于后台同步,storage会记录向group内其他storage同步的进度,以便重启后能接上次的进度继续同步;进度以时间戳的方式进行记录,所以最好能保证集群内的所有server的始终保持同步。最后Storage服务的同步进度会作为元数据的一部分汇报到tracker服务上,tracker服务在选择读storage的时候会以同步进度作为参考指标。

下载文件

当下载文件时,客户端先询问tracker服务下载文件的storage,参数为文件标识(卷名和文件名);然后tracker向客户端返回一台可用的storage;最后客户端直接和storage通讯完成文件下载。

环境准备

软件环境

libevent下载地址:http://libevent.org/
libfasttcommon下载地址:https://github.com/happyfish100/libfastcommon/releases
fastdfs下载地址:https://github.com/happyfish100/fastdfs/releases
fastdfs-nginx-module下载地址:https://github.com/happyfish100/fastdfs-nginx-module/releases

机器及网络环境

Tracker Server1: 192.168.100.101
Storage Group1 Node1: 192.168.100.102
Storage Group1 Node2: 192.168.100.103
Tracker节点需要安装的组件:libevent、libfasttcommon、fastdfs
Storage节点需要安装的组件:libfasttcommon、fastdfs、nginx、fastdfs-nginx-module

部署Tracker服务

对应机器:192.168.100.101

安装libevent依赖

注:如果机器有外网环境直接yum -y install libevent,本文使用源码包编译安装
解压libevent源码包:tar -zxvf libevent-2.1.11-stable.tar.gz
编译安装前配置:./configure
编译安装:make && make install
默认安装位置:/usr/local/lib

安装libfasttcommon依赖

解压libfasttcommon源码包:tar -zxvf libfastcommon-1.0.41.tar.gz
编译安装:./make.sh && ./make.sh install
默认安装位置:/usr/lib64

安装FastDFS Tracker服务

解压fastdfs源码包:tar -zxvf fastdfs-6.01.tar.gz
编译安装:./make.sh && ./make.sh install
安装完成后服务及脚本都拷贝到/usr/bin 目录了:

安装完成后配置文件都拷贝到/etc/fdfs目录下了:

拷贝/etc/fdfs目录下的Tracker配置文件示例:cp /etc/fdfs/tracker.conf.sample /etc/fdfs/tracker.conf
修改 /etc/fdfs/tracker.conf中配置项:
base_path=/home/yuqing/fastdfs 改为:/work/fastdfs/tracker(该目录为自己定义,启动时会用,没有会报错)
安装完成后启动脚本都拷贝到/etc/init.d目录下了:

将/etc/init.d/fdfs_storaged删掉,因为这台机器只安装Tracker服务
注:编译安装fastdfs需要perl库依赖
设置Tracker服务开机自启动:
chkconfig –add fdfs_trackerd
chkconfig fdfs_trackerd on

启动Tracker服务:systemctl start fdfs_trackerd
重启Tracker服务:systemctl restart fdfs_trackerd
停止Tracker服务:systemctl stop fdfs_trackerd
开放Tracker服务端口:iptables -I INPUT -p tcp –dport 22122 -j ACCEPT

部署Storage服务

对应机器:192.168.100.102/103

安装libfasttcommon依赖

解压libfasttcommon源码包:tar -zxvf libfastcommon-1.0.41.tar.gz
编译安装:./make.sh && ./make.sh install
默认安装位置:/usr/lib64

安装FastDFS Storage服务

解压fastdfs源码包:tar -zxvf fastdfs-6.01.tar.gz
编译安装:./make.sh && ./make.sh install
安装完成后服务及脚本都拷贝到/usr/bin 目录了:

安装完成后配置文件都拷贝到/etc/fdfs目录下了:

拷贝/etc/fdfs目录下的Storage配置文件示例:cp /etc/fdfs/storage.conf.sample /etc/fdfs/storage.conf
修改 /etc/fdfs/storage.conf中配置项:
base_path=/home/yuqing/fastdfs 改为:base_path=/work/fastdfs/storage(该目录为自己定义,启动时会用,没有会报错)
store_path0=/home/yuqing/fastdfs 改为:store_path0=/work/fastdfs/storage(该目录为自己定义,启动时会用,没有会报错)
tracker_server=192.168.209.121:22122 改为:tracker_server=192.168.100.101:22122
安装完成后启动脚本都拷贝到/etc/init.d目录下了:

将/etc/init.d/fdfs_trackerd删掉,因为这台机器只安装Storage服务
注:编译安装fastdfs需要perl库依赖
设置Storage服务开机自启动:
chkconfig –add fdfs_storaged
chkconfig fdfs_storaged on

启动Storage服务:systemctl start fdfs_storaged
重启Storage服务:systemctl restart fdfs_storaged
停止Storage服务:systemctl stop fdfs_storaged
开放Storage服务端口:
iptables -I INPUT -p tcp –dport 23000 -j ACCEPT
iptables -I INPUT -p tcp –dport 8888 -j ACCEPT

Storage服务启动正常,进入数据目录中,可以看到已经有了初始数据信息:

验证当前Storage服务和Tracker服务通信情况:
/usr/bin/fdfs_monitor /etc/fdfs/storage.conf

安装Nginx和fdfs-nginx模块

在每个Storage服务节点上安装Nginx服务以及fdfs-nginx模块。
首先安装nginx需要的依赖:yum install -y gcc gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel
解压nginx安装包:tar -zxvf nginx-1.16.1.tar.gz
安装配置Nginx:./configure –prefix=/work/nginx –add-module=../fastdfs-nginx-module-1.21/src
编译安装Nginx:make && make install
将fastdfs-nginx-module-1.21/src下的mod_fastdfs.conf拷贝到/etc/fdfs下
修改mod_fastdfs.conf如下:
连接超时时间: connect_timeout=10
Tracker服务地址:tracker_server=192.168.100.101:22122
Storage服务端口:storage_server_port=23000
如果文件ID的uri中包含/group**,则要设置为true:url_have_group_name = true
Storage配置的store_path0路径,必须和storage.conf中的一致:store_path0=/work/fastdfs/storage
拷贝/etc/fdfs/torage_ids.conf.sample 为torage_ids.conf,内容修改为当前group的storage节点信息:

修改各个Storage机器上的nginx服务的配置如下:

使用Java客户端测试FastDFS

java项目Maven依赖

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
项目地址:https://github.com/tobato/FastDFS_Client
目前客户端主要依赖于SpringBoot,因此必须引入:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/>
</parent>

FastDFS 依赖包:
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
</dependency>

将FastDFS引入项目:
@Import(FdfsClientConfig.class)

在application.yml当中配置Fdfs相关参数:
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs:
so-timeout: 1500
connect-timeout: 600
thumb-image:
width: 150
height: 150
tracker-list:
- 192.168.100.101:22122

使用接口服务对Fdfs服务端进行操作,主要接口包括:
TrackerClient - TrackerServer接口
GenerateStorageClient - 一般文件存储接口 (StorageServer接口)
FastFileStorageClient - 为方便项目开发集成的简单接口(StorageServer接口)
AppendFileStorageClient - 支持文件续传操作的接口 (StorageServer接口)

实际测试案例

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
package com.maxbill;

import com.github.tobato.fastdfs.domain.fdfs.MetaData;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.github.tobato.fastdfs.service.TrackerClient;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Log4j2
@Component
public class FdfsClientUtil {

private static FdfsClientUtil fdfsClientUtil;


@Autowired
private TrackerClient trackerClient;

@Autowired
private FastFileStorageClient storageClient;

@PostConstruct
public void init() {
fdfsClientUtil = this;
}

private static Set<MetaData> getMetaData(Map<String, String> infoMap) {
Set<MetaData> metaDataSet = new HashSet<>();
metaDataSet.add(new MetaData("createUser", "maxbill"));
metaDataSet.add(new MetaData("createDate", "2019-11-18"));
return metaDataSet;
}

public static String uploadFile(File file, Map<String, String> infoMap) {
try {
String fileName = file.getName();
String fileType = fileName.substring(fileName.lastIndexOf("\\") + 1);
log.info("upload file name: {}", fileName);
StorePath path = fdfsClientUtil.storageClient.uploadFile(new FileInputStream(file), file.length(), fileType, getMetaData(infoMap));
log.info("upload success path: {}", path);
Set<MetaData> metaData = fdfsClientUtil.storageClient.getMetadata(path.getGroup(), path.getPath());
log.info("upload success meta: {}", metaData);
return path.getFullPath();
} catch (Exception e) {
log.error(e.getMessage());
return e.getMessage();
}
}

}