引言
FastDFS属于分布式存储范畴,分布式文件系统FastDFS非常适合中小型项目,在我接手维护公司图片服务的时候开始接触到它,本篇文章目的是总结一下FastDFS的知识点。
用了2台2核4G的阿里云服务器做集群部署,具体部署步骤请参考:https://github.com/happyfish100/fastdfs/wiki
1、FastDFS分布式文件系统概述
FastDFS是一个轻量级的开源分布式文件系统,作者为淘宝资深架构余庆。
FastDFS主要解决了分布式文件存储与高并发访问的问题,实现了负载均衡,适合存储图片、视频、文档等文件,而且支持存储服务器的在线扩容。
2、FastDFS架构
FastDFS服务端有两个角色:Tracker与Storage,其中Tracker主要做调度工作,有着负载均衡作用,Storage负责文件存取、同步等操作。
FastDFS系统结构:

2.1、Client
客户端访问FastDFS分布式存储,一般为后端应用。
2.2、Tracker
Tracker在FastDFS集群中有两大作用:
- 管理Storage集群,在Storage服务启动时,会把自己注册到Tracker上,并定期上报自身状态信息,包括磁盘剩余空间、文件同步状态、文件上传下载次数等统计信息。
- Client访问Storage服务之前,必须先访问Tracker,动态获取到Storage服务的连接信息,有着负载均衡的作用。
2.3、Storage
Storage是数据存储服务器,文件和meta data都保存在Storage服务器中。
有以下特点:
- 采用高可用的方式进行数据存储。
- FastDFS集群中,Storage按组(Group/volume)提供服务,不同组的Storage之间不会互相通信,同组内的Storage之间会相互连接,进行文件同步。
- Storage服务采用binlog文件记录文件上传、删除等更新操作,binlog中只记录文件名,不记录内容。
- 文件同步只在同组内的Storage服务之间进行,采用push方式,即圆通服务器同步给目标服务器。
- FastDFS将文件及相关的描述信息(MetaData)保存在Storage服务中,文件存储以后将返回唯一的文件标识,文件标识有组名和文件名两部分构成,MetaData是文件的描述信息,如width=1024,height=768。
3、文件上传原理
文件上传的原理如下图:

- Client询问Tracker可以上传到哪个Storage。
- Tracker返回一台可用的Storage连接信息。
- Client直接与Storage通信,完成文件上传。
- Storage保存文件以后,返回Client文件标识(组名、文件名)。
4、文件下载原理
文件下载原理如下图:

- Client询问Tracker下文文件的Storage,参数为文件标识(组名、文件名)。
- Tracker返回一台可用的Storage。
- Client与Storage通信,完成文件下载过程。
5、文件同步原理
- 同一个组内的Storage服务是对等的,文件上传、删除等操作可以在任意一台Storage服务上执行,数据会在同组内Storage内同步。
- 文件同步(上传、删除、更新)采用push方式,即源服务器同步给目标服务器。
- 只有源头数据才需要同步,如果备份数据再次同步就会形成环路。
- 当新增Storage服务时,将已有的一台Storage的所有数据(源头数据与备份数据)同步给这台新增服务器。
6、服务端文件目录
6.1、TrackerServer
1 2 3 4 5 6
| ${base_path}
|__data
| |__storage_groups.dat:存储分组信息
| |__storage_servers.dat:存储服务器列表
|__logs
|__trackerd.log:tracker server日志文件
|
6.2、StorageServer
1 2 3 4 5 6 7 8 9 10 11 12 13
| ${base_path}
|__data
| |__.data_init_flag:当前storage server 初始化信息
| |__storage_stat.dat:当前storage server统计信息
| |__sync:存放数据同步相关文件
| | |__binlog.index:当前的binlog文件索引号
| | |__binlog.###:存放更新操作记录(日志)
| | |__${ip_addr}_${port}.mark:存放同步的完成情况
| |
| |__一级目录:256个存放数据文件的目录,如:00, 1F
| |__二级目录:256个存放数据文件的目录
|__logs
|__storaged.log:storage server日志文件
|
7、服务端与客户端通讯协议
7.1、 通讯协议介绍
FastDFS服务端与客户端通讯时候采用的是自定义的通讯协议,如下图所示:

协议包由两部分组成:header和body
- header共10字节,格式如下:
- 8 bytes body length
- 1 byte command
- 1 byte status
- body数据包格式取决于具体的命令,body可以为空。
7.2、命令代码和通讯状态代码
7.2.1、Tracker管理命令代码
| 名称 |
命令 |
| 删除storage |
93 |
| 获取下载节点QUERY_FETCH_ONE |
102 |
| 获取更新节点QUERY_UPDATE |
103 |
| 不按组获取存储节点 |
101 |
| 按组获取存储节点 |
104 |
| 获取组列表 |
91 |
| 获取存储节点列表 |
92 |
7.2.2、 Store文件上传命令代码
| 名称 |
命令 |
说明 |
| 文件上传 |
11 |
一般的文件上传,上传后为主文件 |
| 上传附属文件 |
21 |
“上传从文件文件,比如主文件为xxx.jpg,从文件(缩略图)为xxx-150_150.jpg” |
| 删除文件 |
12 |
删除文件 |
| 设置文件元数据 |
13 |
上传文件创建日期,标签等 |
| 文件下载 |
14 |
|
| 获取文件元数据 |
15 |
|
| 查询文件信息 |
22 |
查询文件信息 |
| 创建支持断点续传的文件 |
23 |
创建一个支持断点续传的文件 |
| 断点续传 |
24 |
上传可断点上传的文件,如将大文件切为几份,分开上传 |
| 文件修改 |
34 |
修改支持断点上传的文件 |
| 清除文件 |
36 |
截取(清除)支持断点上传的文件 |
7.2.3、报文通讯状态代码
| 名称 |
代码 |
| 客户端关闭连接命令 |
82 |
| 连接状态检查命令 |
111 |
| 服务端正确返回报文 |
100 |
8、简单使用
我使用的是fastdfs-client-java-1.27-SNAPSHOT.jar
happyfish100/fastdfs-client-java
这个库从17年6月5号之后就停止更新了,最近又开始更新代码了,看样子要维护了啊。
简单的对客户端进行了连接池的封装,方便使用。
- 系统启动,池子管理连接
- 心跳确认连接是否可靠
- 构造器模式创建连接池
- 回调方式使用客户端
源码地址:
ClawHub/FastDFS-Pool
以下为核心代码:
1.1、 初始化连接池
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
|
public FastDFSConnPool build() { idleConnectionPool = new LinkedBlockingQueue<>(maxPoolSize); try { ClientGlobal.init(confFileName); } catch (IOException | MyException e) { throw new RuntimeException("init client global exception.", e); } TrackerServer trackerServer; for (int i = 0; i < minPoolSize; i++) { trackerServer = createTrackerServer(); if (trackerServer != null) { idleConnectionPool.offer(trackerServer); } } new HeartBeat(this).beat();
return this; }
|
1.2、 客户端执行请求
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
|
public <T> T processFdfs(CallBack<T> invoke) { TrackerServer trackerServer = null; T t; try { trackerServer = fastDFSConnPool.checkOut(); StorageClient1 storageClient = new StorageClient1(trackerServer, null); t = invoke.invoke(storageClient); fastDFSConnPool.checkIn(trackerServer); return t; } catch (Exception e) { fastDFSConnPool.drop(trackerServer); throw new RuntimeException(e); } }
|
1.3、 心跳
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
|
private class HeartBeatTask implements Runnable {
@Override public void run() { LinkedBlockingQueue<TrackerServer> idleConnectionPool = fastDFSConnPool.getIdleConnectionPool(); TrackerServer ts = null; for (int i = 0; i < idleConnectionPool.size(); i++) { try { ts = idleConnectionPool.poll(fastDFSConnPool.getWaitTimes(), TimeUnit.SECONDS); if (ts != null) { ProtoCommon.activeTest(ts.getSocket()); idleConnectionPool.add(ts); } else { break; } } catch (Exception e) { logger.error("heart beat conn have dead, and reconnect.", e); fastDFSConnPool.drop(ts); } }
} }
|
1.4、 使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| FastDFSConnPool fastDFSConnPool = new FastDFSConnPool() .confFileName("./config/fdfs_client.conf") .maxPoolSize(8) .minPoolSize(1) .reConnNum(2) .waitTimes(2).build();
FastDFSClient client = new FastDFSClient(fastDFSConnPool); String parts = client.processFdfs(storageClient -> storageClient.upload_file1("fileName", "extName", new NameValuePair[0])); byte[] bytes = client.processFdfs(storageClient -> storageClient.download_file1("fileId")); int result = client.processFdfs(storageClient -> storageClient.delete_file1("fileId")); FileInfo fileInfo = client.processFdfs(storageClient -> storageClient.get_file_info("groupName", "remoteFileName"));
|
参考
FastDFS V5.12分布式文件系统介绍
tobato/FastDFS_Client的wiki
