News:三分天注定,七分靠打拼,爱拼才会赢!致力打造专业IT博客。如果你对本博客有任何意见或建议请联系作者,邮箱:blog@caokuan.cn

FastDFS 分布式文件系统

逝水无痕 168 0 条

分布式文件系统:Distributed file system,DFS,又叫做网络文件系统:Network File System。一种允许文件通过网络在多台主机上分享的文件系统,可让多机器上的多用户分享文件和存储空间。

FastDFS 是用 c 语言编写的一款开源的分布式文件系统,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合中小文件(建议范围:4KB < file_size < 500MB),对以文件为载体的在线服务,如相册网站、视频网站等。

FastDFS 架构

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

fastdfs1.gif

跟踪服务器 Tracker Server

主要做调度工作,起到均衡的作用;负责管理所有的 storage server 和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳。tracker 根据 storage 的心跳信息,建立 group==>[storage serverlist] 的映射表。

Tracker 需要管理的元信息很少,会全部存储在内存中;另外 tracker 上的元信息都是由 storage 汇报的信息生成的,本身不需要持久化任何数据,这样使得 tracker 非常容易扩展,直接增加 tracker 机器即可扩展为 tracker cluster 来服务,cluster 里每个 tracker 之间是完全对等的,所有的 tracker 都接受 stroage 的心跳信息,生成元数据信息来提供读写服务。

存储服务器 Storage Server

主要提供容量和备份服务;以 group 为单位,每个 group 内可以有多台 storage server,数据互为备份。以 group 为单位组织存储能方便的进行应用隔离、负载均衡、副本数定制(group 内 storage server 数量即为该 group 的副本数),比如将不同应用数据存到不同的 group 就能隔离应用数据,同时还可根据应用的访问特性来将应用分配到不同的 group 来做负载均衡;缺点是 group 的容量受单机存储容量的限制,同时当 group 内有机器坏掉时,数据恢复只能依赖 group 内地其他机器,使得恢复时间会很长。

group 内每个 storage 的存储依赖于本地文件系统,storage 可配置多个数据存储目录,比如有 10 块磁盘,分别挂载在 /data/disk1-/data/disk10,则可将这 10 个目录都配置为 storage 的数据存储目录。storage 接受到写文件请求时,会根据配置好的规则选择其中一个存储目录来存储文件。为了避免单个目录下的文件数太多,在 storage 第一次启动时,会在每个数据存储目录里创建 2 级子目录,每级 256 个,总共 65536 个文件,新写的文件会以 hash 的方式被路由到其中某个子目录下,然后将文件数据作为本地文件存储到该目录中。

FastDFS 的存储策略

为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用。

在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。

FastDFS 的上传过程

FastDFS 向使用者提供基本文件访问接口,比如 upload、download、append、delete 等,以客户端库的方式提供给用户使用。

Storage Server 会定期的向 Tracker Server 发送自己的存储信息。当 Tracker Server Cluster 中的 Tracker Server 不止一个时,各个 Tracker 之间的关系是对等的,所以客户端上传时可以选择任意一个 Tracker。

当 Tracker 收到客户端上传文件的请求时,会为该文件分配一个可以存储文件的 group,当选定了 group 后就要决定给客户端分配 group 中的哪一个 storage server。当分配好 storage server 后,客户端向 storage 发送写文件请求,storage 将会为文件分配一个数据存储目录。然后为文件分配一个 fileid,最后根据以上的信息生成文件名存储文件。

fastdfs2.gif

选择 tracker server

当集群中不止一个 tracker server 时,由于 tracker 之间是完全对等的关系,客户端在 upload 文件时可以任意选择一个 trakcer。

选择存储的 group

当 tracker 接收到 upload file 的请求时,会为该文件分配一个可以存储该文件的 group,支持如下选择 group 的规则: 1. Round robin,所有的 group 间轮询 2. Specified group,指定某一个确定的 group 3. Load balance,剩余存储空间多多 group 优先

选择 storage server

当选定 group 后,tracker 会在 group 内选择一个 storage server 给客户端,支持如下选择 storage 的规则: 1. Round robin,在 group 内的所有 storage 间轮询 2. First server ordered by ip,按 ip 排序 3. First server ordered by priority,按优先级排序(优先级在 storage 上配置)

选择 storage path

当分配好 storage server 后,客户端将向 storage 发送写文件请求,storage 将会为文件分配一个数据存储目录,支持如下规则: 1. Round robin,多个存储目录间轮询 2. 剩余存储空间最多的优先

生成 Fileid

选定存储目录之后,storage 会为文件生一个 Fileid,由 storage server ip、文件创建时间、文件大小、文件 crc32 和一个随机数拼接而成,然后将这个二进制串进行 base64 编码,转换为可打印的字符串。

选择两级目录

当选定存储目录之后,storage 会为文件分配一个 fileid,每个存储目录下有两级 256*256 的子目录,storage 会按文件 fileid 进行两次 hash(猜测),路由到其中一个子目录,然后将文件以 fileid 为文件名存储到该子目录下。

生成文件名

当文件存储到某个子目录后,即认为该文件存储成功,接下来会为该文件生成一个文件名,文件名由 group、存储目录、两级子目录、fileid、文件后缀名(由客户端指定,主要用于区分文件类型)拼接而成。

fastdfs3.gif

FastDFS 的文件同步

写文件时,客户端将文件写至 group 内一个 storage server 即认为写文件成功,storage server 写完文件后,会由后台线程将文件同步至同 group 内其他的 storage server。

每个 storage 写文件后,同时会写一份 binlog,binlog 里不包含文件数据,只包含文件名等元信息,这份 binlog 用于后台同步,storage 会记录向 group 内其他 storage 同步的进度,以便重启后能接上次的进度继续同步;进度以时间戳的方式进行记录,所以最好能保证集群内所有 server 的时钟保持同步。

storage 的同步进度会作为元数据的一部分汇报到 tracker 上,tracke 在选择读 storage 的时候会以同步进度作为参考。

比如一个 group 内有 A、B、C 三个 storage server,A 向 C 同步到进度为 T1 (T1 以前写的文件都已经同步到 B 上了),B 向 C 同步到时间戳为 T2(T2 > T1),tracker 接收到这些同步进度信息时,就会进行整理,将最小的那个做为 C 的同步时间戳,本例中 T1 即为 C 的同步时间戳为 T1(即所有 T1 以前写的数据都已经同步到 C 上了);同理,根据上述规则,tracker 会为 A、B 生成一个同步时间戳。

FastDFS 的文件下载

客户端 uploadfile 成功后,会拿到一个 storage 生成的文件名,接下来客户端根据这个文件名即可访问到该文件。

fastdfs4.gif

跟 upload file 一样,在d ownload file 时客户端可以选择任意 tracker server。tracker 发送 download 请求给某个 tracker,必须带上文件名信息,tracker 从文件名中解析出文件的 group、大小、创建时间等信息,然后为该请求选择一个 storage 用来服务读请求。

FastDFS 性能方案

fastdfs5.gif

FastDFS 安装

软件包版本 FastDFSv5.05libfastcommonv1.0.7

下载安装 libfastcommon

下载

wget https://github.com/happyfish100/libfastcommon/archive/V1.0.7.tar.gz

解压

tar -xvf V1.0.7.tar.gz
cd libfastcommon-1.0.7

编译、安装

./make.sh
./make.sh install

创建软链接

ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so
ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so
ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so
ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so 

下载安装 FastDFS

下载 FastDFS

wget https://github.com/happyfish100/fastdfs/archive/V5.05.tar.gz

解压

tar -xvf V5.05.tar.gz
cd fastdfs-5.05

编译、安装

./make.sh
./make.sh install

配置 Tracker 服务

上述安装成功后,在 /etc/ 目录下会有一个 fdfs 的目录,进入它。会看到三个 .sample 后缀的文件,这是作者给我们的示例文件,我们需要把其中的 tracker.conf.sample 文件改为 tracker.conf 配置文件并修改它:

cp tracker.conf.sample tracker.conf
vi tracker.conf

编辑 tracker.conf

# 配置文件是否不生效,false 为生效
disabled=false
# 提供服务的端口
port=22122
# Tracker 数据和日志目录地址
base_path=//home/data/fastdfs
# HTTP 服务端口
http.server_port=80

创建 tracker 基础数据目录,即 base_path 对应的目录

mkdir -p /home/data/fastdfs

使用 ln -s 建立软链接

ln -s /usr/bin/fdfs_trackerd /usr/local/bin
ln -s /usr/bin/stop.sh /usr/local/bin
ln -s /usr/bin/restart.sh /usr/local/bin

启动服务

service fdfs_trackerd start

查看监听

netstat -unltp|grep fdfs

如果看到 22122 端口正常被监听后,这时候说明 Tracker 服务启动成功啦!

tracker server 目录及文件结构 Tracker 服务启动成功后,会在 base_path 下创建 data、logs 两个目录。目录结构如下:

${base_path}
 |__data
 | |__storage_groups.dat:存储分组信息
 | |__storage_servers.dat:存储服务器列表
 |__logs
 | |__trackerd.log: tracker server 日志文件 

配置 Storage 服务

进入 /etc/fdfs 目录,复制 FastDFS 存储器样例配置文件 storage.conf.sample,并重命名为 storage.conf

# cd /etc/fdfs
# cp storage.conf.sample storage.conf
# vi storage.conf

编辑 storage.conf

# 配置文件是否不生效,false 为生效
disabled=false
# 指定此 storage server 所在 组(卷)
group_name=group1
# storage server 服务端口
port=23000
# 心跳间隔时间,单位为秒 (这里是指主动向 tracker server 发送心跳)
heart_beat_interval=30
# Storage 数据和日志目录地址(根目录必须存在,子目录会自动生成)
base_path=/home/data/fastdfs/storage
# 存放文件时 storage server 支持多个路径。这里配置存放文件的基路径数目,通常只配一个目录。
store_path_count=1
# 逐一配置 store_path_count 个路径,索引号基于 0。
# 如果不配置 store_path0,那它就和 base_path 对应的路径一样。
store_path0=/home/data/fastdfs/storage
# FastDFS 存储文件时,采用了两级目录。这里配置存放文件的目录个数。 
# 如果本参数只为 N(如: 256),那么 storage server 在初次运行时,会在 store_path 下自动创建 N * N 个存放文件的子目录。
subdir_count_per_path=256
# tracker_server 的列表 ,会主动连接 tracker_server
# 有多个 tracker server 时,每个 tracker server 写一行
tracker_server=192.168.1.190:22122
# 允许系统同步的时间段 (默认是全天) 。一般用于避免高峰同步产生一些问题而设定。
sync_start_time=00:00
sync_end_time=23:59

使用 ln -s 建立软链接

ln -s /usr/bin/fdfs_storaged /usr/local/bin

启动服务

service fdfs_storaged start

查看监听

netstat -unltp | grep fdfs

启动 Storage 前确保 Tracker 是启动的。初次启动成功,会在 /home/data/fastdfs/storage 目录下创建 data、 logs 两个目录。如果看到 23000 端口正常被监听后,这时候说明 Storage 服务启动成功啦!

查看 Storage 和 Tracker 是否在通信

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

FastDFS 配置 Nginx 模块

软件包版本 openrestyv1.13.6.1fastdfs-nginx-modulev1.1.6

FastDFS 通过 Tracker 服务器,将文件放在 Storage 服务器存储, 但是同组存储服务器之间需要进行文件复制,有同步延迟的问题。

假设 Tracker 服务器将文件上传到了 192.168.1.190,上传成功后文件 ID 已经返回给客户端。此时 FastDFS 存储集群机制会将这个文件同步到同组存 192.168.1.190,在文件还没有复制完成的情况下,客户端如果用这个文件 ID 在 192.168.1.190 上取文件,就会出现文件无法访问的错误。而 fastdfs-nginx-module 可以重定向文件链接到源服务器取文件,避免客户端由于复制延迟导致的文件无法访问错误。

下载 安装 Nginx 和 fastdfs-nginx-module:

推荐您使用yum安装以下的开发库:

yum install readline-devel pcre-devel openssl-devel -y

下载最新版本并解压

wget https://openresty.org/download/openresty-1.13.6.1.tar.gz
tar -xvf openresty-1.13.6.1.tar.gz
wget https://github.com/happyfish100/fastdfs-nginx-module/archive/master.zip
unzip master.zip

配置 nginx 安装,加入 fastdfs-nginx-module 模块

./configure --add-module=../fastdfs-nginx-module-master/src/

编译、安装

make && make install

查看Nginx的模块

/usr/local/openresty/nginx/sbin/nginx -v

有下面这个就说明添加模块成功

fastdfs6.gif

复制 fastdfs-nginx-module 源码中的配置文件到 /etc/fdfs 目录, 并修改

cp /fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/
# 连接超时时间
connect_timeout=10
# Tracker Server
tracker_server=192.168.1.190:22122
# StorageServer 默认端口
storage_server_port=23000
# 如果文件ID的uri中包含/group**,则要设置为true
url_have_group_name = true
# Storage 配置的store_path0路径,必须和storage.conf中的一致
store_path0=/home/data/fastdfs/storage

复制 FastDFS 的部分配置文件到 /etc/fdfs 目录

cp /fastdfs-nginx-module/src/http.conf /etc/fdfs/
cp /fastdfs-nginx-module/src/mime.types /etc/fdfs/

配置 nginx,修改 nginx.conf

location ~/group([0-9])/M00 {
 ngx_fastdfs_module;
}

启动 Nginx

[root@iz2ze7tgu9zb2gr6av1tysz sbin]# ./nginx
ngx_http_fastdfs_set pid=9236

测试上传

[root@iz2ze7tgu9zb2gr6av1tysz fdfs]# /usr/bin/fdfs_upload_file /etc/fdfs/client.conf /etc/fdfs/4.jpg
group1/M00/00/00/rBD8EFqVACuAI9mcAAC_ornlYSU088.jpg

部署结构图

fastdfs7.gif

JAVA 客户端集成

pom.xml 引入:

<!-- fastdfs -->
<dependency>
    <groupId>org.csource</groupId>
    <artifactId>fastdfs-client-java</artifactId>
    <version>1.27</version>
</dependency>

fdfs_client.conf 配置:

#连接tracker服务器超时时长
connect_timeout = 2 
#socket连接超时时长
network_timeout = 30
#文件内容编码 
charset = UTF-8 
#tracker服务器端口
http.tracker_http_port = 8080
http.anti_steal_token = no
http.secret_key = FastDFS1234567890
#tracker服务器IP和端口(可以写多个)
tracker_server = 192.168.1.190:22122 

FastDFSClient 上传类:

public class FastDFSClient {

    private static final String CONFIG_FILENAME = "D:\\itstyle\\src\\main\\resources\\fdfs_client.conf";
    private static final String GROUP_NAME = "market1";
    private TrackerClient trackerClient = null;
    private TrackerServer trackerServer = null;
    private StorageServer storageServer = null;
    private StorageClient storageClient = null;
    static {
        try {
            ClientGlobal.init(CONFIG_FILENAME);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
    public FastDFSClient() throws Exception {
        trackerClient = new TrackerClient(ClientGlobal.g_tracker_group);
        trackerServer = trackerClient.getConnection();
        storageServer = trackerClient.getStoreStorage(trackerServer);;
        storageClient = new StorageClient(trackerServer, storageServer);
    }
    /**
     * 上传文件
     * @param file 文件对象
     * @param fileName 文件名
     * @return
     */
    public String[] uploadFile(File file, String fileName) {
        return uploadFile(file,fileName,null);
    }
    /**
     * 上传文件
     * @param file 文件对象
     * @param fileName 文件名
     * @param metaList 文件元数据
     * @return
     */
    public String[] uploadFile(File file, String fileName, Map<String,String> metaList) {
        try {
            byte[] buff = IOUtils.toByteArray(new FileInputStream(file));
            NameValuePair[] nameValuePairs = null;
            if (metaList != null) {
                nameValuePairs = new NameValuePair[metaList.size()];
                int index = 0;
                for (Iterator<Map.Entry<String,String>> iterator = metaList.entrySet().iterator(); iterator.hasNext();) {
                    Map.Entry<String,String> entry = iterator.next();
                    String name = entry.getKey();
                    String value = entry.getValue();
                    nameValuePairs[index++] = new NameValuePair(name,value);
                }
            }
            return storageClient.upload_file(GROUP_NAME,buff,fileName,nameValuePairs);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 获取文件元数据
     * @param fileId 文件ID
     * @return
     */
    public Map<String,String> getFileMetadata(String groupname,String fileId) {
        try {
            NameValuePair[] metaList = storageClient.get_metadata(groupname,fileId);
            if (metaList != null) {
                HashMap<String,String> map = new HashMap<String, String>();
                for (NameValuePair metaItem : metaList) {
                    map.put(metaItem.getName(),metaItem.getValue());
                }
                return map;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 删除文件
     * @param fileId 文件ID
     * @return 删除失败返回-1,否则返回0
     */
    public int deleteFile(String groupname,String fileId) {
        try {
            return storageClient.delete_file(groupname,fileId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;
    }
    /**
     * 下载文件
     * @param fileId 文件ID(上传文件成功后返回的ID)
     * @param outFile 文件下载保存位置
     * @return
     */
    public int downloadFile(String groupName,String fileId, File outFile) {
        FileOutputStream fos = null;
        try {
            byte[] content = storageClient.download_file(groupName,fileId);
            fos = new FileOutputStream(outFile);
            InputStream ips = new ByteArrayInputStream(content);
            IOUtils.copy(ips,fos);
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return -1;
    }
    public static void main(String[] args) throws Exception {
        FastDFSClient client = new FastDFSClient();
        File file = new File("D:\\23456.png");
        String[] result = client.uploadFile(file, "png");
        System.out.println(result.length);
        System.out.println(result[0]);
        System.out.println(result[1]);
    }
}

执行 main 方法测试返回:

2
group1
M00/00/00/rBD8EFqTrNyAWyAkAAKCRJfpzAQ227.png

本文转自 《一文搞定分布式文件系统FastDFS》

与本文相关的文章

发表我的评论
icon_mrgreen.gificon_neutral.gificon_twisted.gificon_arrow.gificon_eek.gificon_smile.gificon_confused.gificon_cool.gificon_evil.gificon_biggrin.gificon_idea.gificon_redface.gificon_razz.gificon_rolleyes.gificon_wink.gificon_cry.gificon_surprised.gificon_lol.gificon_mad.gificon_sad.gificon_exclaim.gificon_question.gif

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址