一、关于
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);
}
}