当前位置:   article > 正文

fastdfs工作原理_fastdfs原理

fastdfs原理

讲完了云盘项目的设置,现在来讲一讲云盘中核心fastdfs的框架。
在介绍工作原理之前,先介绍fastdfs的传输协议。
服务之间的通信协议,遵循 header + body的设计原则。
header结构体封装

#define FDFS_PROTO_PKG_LEN_SIZE 8
typedef struct
{
    char pkg_len[FDFS_PROTO_PKG_LEN_SIZE]; //body length, not including header
    char cmd; //command code
    char status; //status code for response
} TrackerHeader;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

fastdfs协议头部由10个字节组成:
pck_len:⼀个int64_t的整型,除去TrackerHeader⻓度的报⽂⻓度。注意与上传协议中file_size区分
cmd:命令
status:返回时的状态码,发送时设置为0

上传步骤解析

client上传一次文件的流程图如下
在这里插入图片描述
先单独指出一点,从图中可以看出,storage会主动连接tracker,而tracker是不会主动连接的。上传流程简单来说,就是client主动连接tracker,tracker再返回storage地址,client再上传文件至storage的过程。这里tracker主要是做负载均衡的事情(上传下载都有做)。
这里还可以看出,fastdfs请求—响应都是串行执行的,client与tracker交互,client再与storage交互。

⽂件上传协议头部

先指出一点,fastdfs在各个工作流程的协议,除了包含TrackerHeader以外,还有一些其他字段。也就是说,fastdfs各个端之间的信令交互,header是固定的,data是不固定的,依据cmd实际值而定。
这里以上传一个文件为例来介绍各流程协议头部。

client上传连接请求

按照顺序介绍,先介绍client向tracker请求时的协议。此时,客户端向集群中任意⼀台Tracker server发起TCP连接,建⽴连接后,客户端发送上传请求报⽂。报⽂的协议就是10字节的TrackerHeader。

00000000000000006500     //10个字节 ⼗六进制
  • 1

data共10个字节,结构就是上⾯协议中谈到的FastDFS协议头部TrackerHeader。前8个字节为0,因为这个报⽂没有报⽂体,pkg_len=0,cmd=0x65(⼗六进制)=101(⼗进制),status=0。
101命令对应源码中的定义

#define TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE 101
  • 1

⽤来请求group,以及对应的storage地址。

tracker返回Storage信息

这里略去了tracker选择storage的过程。简单来说,就是根据配置进行选择的过程。
这里直接给出返回信息再进行解释

00000000000000286400 //10个字节
0x67, 0x72, 0x6f, 0x75, 0x70, 0x31, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,0x0, //16个字节
0x31, 0x32, 0x30, 0x2e, 0x32, 0x37, 0x2e, 0x31, 0x33, 0x31, 0x2e, 0x31, 0x39, 0x37, 0x0,0x0, //16个字节
000000000059d8 //7个字节, 0x59d8 -> 即是23000
00 //1个字节
  • 1
  • 2
  • 3
  • 4
  • 5

data共50个字节
第⼀⾏10个字节为TrackerHeader,其中前8个字节为报⽂体⻓度,0x28(⼗六进制)=40(⼗进制),对应的接下来的报⽂体40个字节;0x64(⼗六进制)=100(⼗进制),100命令对应源码中的定义:

#define TRACKER_PROTO_CMD_RESP 100
  • 1

第⼆⾏16个字节为Storage的组名(group name),翻译为ASCII为group1,正好是Storage中的⼀个组名。
第三⾏16个字节为Storage的ip, 翻译为ASCII为120.27.131.197
第四⾏7个字节为Storage的port, 翻译为ASCII为23000
第五⾏1个字节为storage_index 翻译为ASCII为 0

client上传文件

client拿到了tracker返回的storage服务器的group name,ip和port后,就向这台storage服务器发起TCP连接请求,建⽴连接后,开始上传⽂件,先发送⼀个告知⽂件⼤⼩的报⽂,协议内容(⼗六进制)如下

0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xa, 0xb, 0x0,    //10字节 TrackerHeader
0x0,       //storage_index 1字节
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfb,       //⽂件⼤⼩ 8字节,0xfb即是251
0x74, 0x78, 0x74, 0x0, 0x0, 0x0       //扩展名6字节,这⾥是txt
  • 1
  • 2
  • 3
  • 4

再对TrackerHeader作一个解释。前8个字节为之后所有报⽂报⽂体的⻓度,0x10c(⼗六进制)=266(⼗进制),266字节⽐真实的⽂件251字节多了15个字节,这15个字节⽤于告知storage_index,⽂件⻓度和扩展名。
cmd=0x0b(⼗六进制)=11(⼗进制),11命令对应的源码定义如下:

#define STORAGE_PROTO_CMD_UPLOAD_FILE 11
  • 1

发送成功后,接下来,client向storage发送真正的文件数据。发送文件,本质是调⽤ sendfile 发送(sendfile函数在两个⽂件描述符之间传递数据(完全在内核中操作),从⽽避免了内核缓冲区和⽤户缓冲区之间的数据拷⻉,效率很⾼,被称为零拷⻉。)
这里需要注意,如果文件太大,最后上传如果失败的话就会比较可惜。比如说,上传1G的文件上传到90%失败了,服务器会把错误文件删掉,给客户端返回错误,这样前面的90%就白传了,建议使用append方式上传。
storage根据配置文件选择store path,生成fileid。这里介绍一下生成fileid的方法。选定了存储⽬录后,storage会⽣⼀成个fileid,由storage server ip、⽂件创建时间、⽂件⼤⼩、⽂件crc32和⼀个随机数拼接⽽成,然后将这个⼆进制串进⾏base64编码,转换为可打印的字符串。也就是说,fastdfs这里没有做去重的功能,如果一直上传同样的文件,fastdfs会保存成不同id的文件,因为至少文件创建时间会不同。调⽤storage_get_filename函数将⽂件写⼊磁盘后,返回client该⽂件的路径和⽂件名,报⽂data内容(⼗六进制)如下

000000000000003c6400 //10个字节
67726f75703100000000000000000000 //16个字节
4d30302f30302f30302f774b67714856344f51517941626f3959414141415f666453706673835352e747874 //44个字节
  • 1
  • 2
  • 3

解释一下,第⼀⾏10个字节为TrackerHeader,前8个字节为报⽂体⻓度,3c(⼗六进制)=60(⼗进制),cmd=64(⼗六进制)=100(⼗进制),100命令对应的源码定义

#define STORAGE_PROTO_CMD_RESP 100
  • 1

第⼆⾏16个字节为组名(group name),翻译为ASCII为group1
第三⾏44个字节为⽂件路径和⽂件名,翻译为ASCII为M00/00/00/wKgqHV4OQQyAbo9YAAAA_fdSpmg855.txt
这样我们也就看见终端返回的文件路径和id了。

下载步骤解析

client下载一次文件的流程图如下
在这里插入图片描述
从图中可以看出,下载文件的流程与上传类似,这里不再过多介绍。
主要介绍一下,使用gdb调试,可以使用gdb attach pid到正在运行中的进程进行调试,注意storage和tracker和client分开调试,不要一起打断点调试,因为在这样的调试中,一次上传下载流程和触发多次断点,容易超时。

tracker与storage

多节点部署问题

在部署过程中,多个tracker可以部署在同一台机器,只需要修改端口即可,而storage只能每台机器搭建一个,改了端口也没用。这是因为,tracker是根据不同的IP来识别不同的storage的,而且即便是使用双网卡也不行,因为storage是根据内存和挂载磁盘的路径、总大小来设置的,所以storage只能是一台机器一个。但是storage和tracker可以部署在同一台机器上。另外,tracker对机器性能要求不高,所以可以一台机器部署多个tracker。

tracker

使用命令关闭tracker和storage时,还需要带上配置文件。这是因为,配置文件base path的data目录中有.pid文件,分别记录了tracker和storage的pid。

storage

组内多节点问题

组内设置多个storage,有好有坏。优势是,对于写多读少的情况,可以使读操作分散到不同的storage节点中,可以提高读的速度,提高读的并发量;同时,节点多,意味着数据安全性相对较高,毕竟所有节点同时宕机的可能性较低。当然,如果对数据安全性的要求特别高,可以使用RAID架构,但成本较高。storage官方推荐用多磁盘的方式(组内多节点,一个group推荐两台),而不要用RAID方式。
组内节点多的劣势,对与写较多的情况,数据写入其中一个节点后,节点间的同步压力较大,影响总体性能。

节点间通信

本地的storage实际会存储组内其他storage的信息,可以通过fdfs_monitor命令查看。
tracker之间并不会通信,信息转发都是通过storage实现的。tracker之间相互感知是靠storage发送对应的信息上报到tracker中。虽然不需要每个storage连接所有的tracker,但是建议这么做,因为为了高可用,本质上来讲,tracker也是一个集群。
fastdfs没有注册发现的机制,也就是说,增加了tracker,storage又要去配置。

IO模型

这里以storage的线程为例,tracker的与之类似,只是没有dio,不再赘述。
fastdfs的IO流程如下
在这里插入图片描述
首先说明各线程数量。默认g_accept_threads为1,g_work_threads为4,g_disk_reader_threads为1,这些参数可以通过storage.conf修改。

accept线程

入口函数accept_thread_entrance。调⽤accept获取新的连接

incomesock = accept(server_sock, (struct sockaddr*)&inaddr, &sockaddr_len);
  • 1

获取对⽅ip地址

client_addr = getPeerIpaddr(incomesock, szClientIp,IP_ADDRESS_SIZE);
  • 1

从对象池取⼀个task对象

pTask = free_queue_pop();
  • 1

task对象⾥⾯有client的封装信息,需要设置

pClientInfo = (StorageClientInfo *)pTask->arg;
pTask->event.fd = incomesock;
pClientInfo->stage = FDFS_STORAGE_STAGE_NIO_INIT;
  • 1
  • 2
  • 3

轮询线程

pClientInfo->nio_thread_index = pTask->event.fd % g_work_threads;
  • 1

通知io线程有新的连接

write(pThreadData->thread_data.pipe_fds[1], &task_addr, sizeof(task_addr)) ;
  • 1

中间代码有删减。对于accept线程而言,主要功能链就是接收客户端的连接并把客户端相关信息通知给work线程。值得注意的是,它是通过管道的方式与work线程进行通信的,相当于事件通知,即通知work线程,pipe中传输的是task对象的地址。为什么交互的方式选用管道而不是任务队列的方式呢?因为一旦一个fd选择了一个线程,这个fd就会一直在这个线程中,直至结束,相当于绑定了。每个work线程都有自己独立的pipe和epoll。

work线程

入口函数work_thread_entrance,核⼼是调⽤

ioevent_loop(&pThreadData->thread_data, storage_recv_notify_read,task_finish_clean_up, &g_continue_flag);
  • 1

storage_recv_notify_read当数据可读时触发
ioevent_loop核⼼实际是调⽤epoll_wait

pThreadData->ev_puller.iterator.count =ioevent_poll(&pThreadData->ev_puller); //返回可处理事件
  • 1

再通过循环处理事件deal_ioevents进行处理。
这里给出client_sock_read函数的流程图,以便理解。
在这里插入图片描述
这里注意ptask->size默认256k,可以通过配置文件调整,但是不建议开太大,因为对于高并发场景来说,buffer较多,每个buffer积累到一个较大值才触发dio写线程,占用内存较大。

⽂件IO线程

入口函数dio_thread_entrance。任务由storage_dio_queue_push投递,从blocked_queue_pop读取任务,其实就是一个任务队列。通过回调写⼊⽂件dio_write_file或者读取⽂件dio_read_file
在这里插入图片描述
值得注意的是,dio线程不会直接给nio线程设置各种读写事件,⽽是通过FDFS_STORAGE_STAGE_NIO_INIT、FDFS_STORAGE_STAGE_NIO_RECV、FDFS_STORAGE_STAGE_NIO_SEND、FDFS_STORAGE_STAGE_NIO_CLOSE、FDFS_STORAGE_STAGE_DIO_THREAD等状态 , 通过pipe通知nio线程响应storage_recv_notify_read进⾏io事件的处理,这些pipe就是accept线程与work线程交互的pipe。
dio线程与work线程交互的方式是队列,即封装任务+队列的方式,这个队列是公共的,所有dio线程与work线程都共用一个。dio不直接操作网络数据读写相关设置,而是使用pipe去做通知的原因,是为了统一事件源,减少耦合性。这样做,对于work线程来说,就只需要关注pipe了。
这里给出dio_write_file函数的流程图。
在这里插入图片描述

文件同步

在fastdfs之中,每个storage之间的同步都是由⼀个独⽴线程负责的,该线程中的所有操作都是以同步⽅式执⾏的。⽐如⼀组服务器有A、B、C三台机器,那么在每台机器上都有两个线程负责同步,如A机器,线程1负责同步数据到B,线程2负责同步数据到C。具体来说,有两个线程的入口函数需要介绍。tracker_report_thread_entrance连接tracker有独⽴的线程,连接n个tracker就有n个线程。storage_sync_thread_entrance,给同group的storage做同步,同组有n个storage,就有n-1个线程。

同步规则

1、只在本组内的storage server之间进⾏同步;
2、源头数据才需要同步,备份数据不需要再次同步,否则就构成环路了,源数据和备份数据区分是⽤binlog的操作类型来区分,操作类型是⼤写字⺟,表示源数据,⼩写字⺟表示备份数据,源数据与备份数据的概念后面还会提到;
3、当新增加⼀台storage server时,由已有的⼀台storage server将已有的所有数据(包括源头数据和备份数据)同步给该新增服务器。

同步日志

讲解同步过程之前,先说明一下同步日志。
同步日志存放位置,在第一个store path下,data/sync目录中。该目录存放了三个文件

120.27.131.197_23000.mark    binlog.000    binlog_index.dat
  • 1

同步状态文件

第一个文件是同步状态⽂件,记录本机到120.27.131.197机器storage的同步状态⽂件名由同步源IP_端⼝组成。storage⽂件同步采⽤增量⽅式,mark⽂件中记录已同步的位置。内容如下

binlog_index=0 //binlog索引id 表示上次同步给114.215.169.66机器的最后⼀条binlog⽂件索引
binlog_offset=3944 //当前时间binlog ⼤⼩ 表示上次同步给114.215.169.66机器的最后⼀条binlog偏移量,若程序重启了,也只要从这个位置开始向后同步即可。
need_sync_old=1 //是否需要同步⽼数据
sync_old_done=1 //是否同步完成
until_timestamp=1621667115 //同步已有数据⽂件的截⾄时间
scan_row_count=68 //扫描记录数
sync_row_count=53 //同步记录数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

binlog文件

第二个文件记录storage操作步骤。storage使⽤binlog⽂件记录⽂件上传、删除等操
作,根据binlog进⾏⽂件同步。binlog中只记录⽂件ID和操作,不记录⽂件内容。⽂件最⼤大小默认1G,超过1G,会重新写下个⽂件(binlog.001),同时更新binlog_index.dat⽂件中索引值。binlog文件内容如下

1572660827 c M00/00/00/oYYBAF285luIK8jCAAAJeheau6AAAAAVgABI-cAAAmS021.xml
1572660911 D M00/00/00/oYYBAF285cOIHiVCAACI-7zX1qUAAAAVgAACC8AAIkT490.txt
1572660967 d M00/00/00/oYYBAF285luIK8jCAAAJeheau6AAAAAVgABI-cAAAmS021.xml
  • 1
  • 2
  • 3

从上⾯可以看到,binlog⽂件有三列,依次为:时间戳、操作类型、⽂件ID(不带group名称)。
⽂件操作类型采⽤单个字⺟编码,其中源头操作⽤⼤写字⺟表示,被同步的操作为对应的⼩写字⺟。⽂件操作字⺟含义如下:

C:上传⽂件(upload) c:副本创建
D:删除⽂件(delete) d:副本删除
A:追加⽂件(append) a:副本追加
M:部分⽂件更新(modify) m:副本部分⽂件更新(modify)
U:整个⽂件更新(set metadata) u:副本整个⽂件更新(set metadata)
T:截断⽂件(truncate) t:副本截断⽂件(truncate)
L:创建符号链接(⽂件去重功能,相同内容只保存⼀份)l:副本创建符号链接
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

同组内的storage之间是对等的,⽂件上传、删除等操作可以在任意⼀台storage server上进⾏。⽂件同步采⽤push⽅式,即源头服务器同步给本组的其他存储服务器。对于同组的其他storage,⼀台storage分别启动⼀个线程进⾏⽂件同步。
storage内binlog文件是共用的,也就是说,client上传下载文件的记录,和其他storage的同步操作,都记录在一起,当然每个storage机器的binlog文件还是独立的;但是同步状态文件是是线程独立的,相当于说,每个机器一个.mark文件。

第三个文件记录了当前写binlog的索引id,就是写到哪一个binlog了,较为简单,不过多介绍。

同步过程

###获取组内的其他storage信息并启动同步线程
tracker_report_thread_entrance线程负责向tracker上报信息。在storage.conf配置⽂件中,只配置了tracker的IP地址,并没有配置组内其他的storage的地址信息。因此同组的其他storage必须从tracker获取。具体过程如下:
1、Storage启动时为每⼀个配置的tracker启动⼀个线程负责与该Tracker的通讯。
2、默认每间隔30秒,与tracker发送⼀次⼼跳包,在⼼跳包的回复中,将会有该组内的其他storage信息。也就是说,每个storage都包含组内其他storage的信息。
3、storage获取到同组的其他storage信息之后,为组内的每个其他storage开启⼀个线程负责同步。

同步线程执⾏过程

storage_sync_thread_entrance为同步线程入口函数。每个同步线程负责到⼀台storage的同步,以阻塞⽅式进⾏。
1、打开对应storage的mark⽂件,如负责到114.215.169.66的同步则打开114.215.169.66_23000.mark⽂件,从中读取binlog_index、binlog_offset两个字段值,如取到值为0、100,那么就打开binlog.000⽂件,seek到100这个位置。
2、进⼊⼀个while循环,尝试着读取⼀⾏,若读取不到则睡眠等待。若读取到⼀⾏,并且该⾏的操作⽅式为源操作,如C、A、D、T(⼤写的都是),则将该⾏指定的操作同步给对⽅(⾮源操作不需要同步),同步成功后更新binlog_offset标志,该值会定期写⼊到114.215.169.66_23000.mark⽂件之中。
这里主要提示一个函数storage_sync_copy_file,它出现在两个文件中

storage_sync.c 发送同步⽂件 storage_sync_copy_file
storage_service.c 接收同步⽂件 storage_sync_copy_file
  • 1
  • 2

注意这里,发送同步文件仍然使用的是sendfile()零拷贝的方式。
最后注意一点,假如同步较为缓慢,那么有可能出现这么一个情况,在开始同步⼀个⽂件之前,该⽂件已经被客户端删除,此时同步线程将打印⼀条⽇志,然后直接接着处理后⾯的binlog。

storage的最后最早被同步时间

这个标题有点拗⼝,先举个例⼦:⼀组内有storage-A、storage-B、storage-C三台机器。对于A这台机器来说,B与C机器都会同步binlog(包括⽂件)给他,A在接收同步时会记录每台机器同步给他的最后时间(也就是binlog中的第⼀个字段timpstamp)。⽐如B最后同步给A的binlog-timestamp为100,C最后同步给A的binlog-timestamp为200,那么A机器的最后最早被同步时间就为100。
这个值的意义在于,判断⼀个⽂件是否存在某个storage上。⽐如这⾥A机器的最后最早被同步时间为100,那么如果⼀个⽂件的创建时间为99,就可以肯定这个⽂件在A上肯定有。为什么呢?⼀个⽂件会Upload到组内三台机器的任何⼀台上
1、若这个⽂件是直接Upload到A上,那么A肯定有。
2、若这个⽂件是Upload到B上,由于B同步给A的最后时间为100,也就是说在100之前的⽂件都已经同步A了,那么A肯定有。
3、同理C也⼀样。
storage会定期将每台机器同步给他的最后时间告诉给tracker,tracker在客户端要下载⼀个⽂件时,需要判断⼀个storage是否有该⽂件,只要解析⽂件的创建时间,然后与该值作⽐较,若该值⼤于创建时间,说明该storage存在这个⽂件,可以从其下载。

新增节点同步流程

先上图
在这里插入图片描述
图中蓝色的A和B是已有storage,绿色的C是新加节点,橙色的是tracker。
1、新节点storage C 启动的时候会创建线程tracker_report_thread_entrance,调⽤
tracker_report_join向tracker 发送命令TRACKER_PROTO_CMD_STORAGE_JOIN(81)报告,⾃⼰的group名称,ip,端⼝,版本号,存储⽬录数,⼦⽬录数,启动时间,⽼数据是否同步完成,当前连接的tracker信息,当前状态信息(FDFS_STORAGE_STATUS_INIT)等信息。
2、tracker收到TRACKER_PROTO_CMD_STORAGE_JOIN命令后,将上报的信息和已有(tracker数据⽂件中保存的信息)的信息进⾏⽐较,如果有则更新,没有的话,将节点及状态信息写⼊缓存和数据⽂件中,并查找同group的其他节点做为同步源,如果有返回给stroage C。
3、新节点stroage C 收到tracker响应继续流程。发送TRACKER_PROTO_CMD_STORAGE_SYNC_DEST_REQ (87)查询同步⽬标。
4、tracker收到TRACKER_PROTO_CMD_STORAGE_SYNC_DEST_REQ 请求后, 查找同group的其他节点做为同步⽬标,及时间戳返回给新storage节点。
5、新storage节点收到响应后,保存同步源及同步时间戳。继续流程,发送TRACKER_PROTO_CMD_STORAGE_BEAT(83) 给tracker。
6、tracker收到⼼跳报告后,leader trakcer(⾮leader不返回数据),把最新的group的storagelist返回给新的stroage。
7、stroage C 收到 tracker发回的storage list后,启动2个同步线程,准备将binlog同步到节点 A和B(此时还不能同步,因为stroage C 还是WAIT_SYNC 状态)。
8、这时候,其他的已在线的storage 节点 A 、B会发送⼼跳给tracker ,tracker 返回收到最新的stroage list,A、B、C返回给Storage A,B。
9、storage A,B 收到tracker响应后,会发现本地缓存中没有stroage C,会启动binlog同步线程,将数据同步给 stroage C。
10、storage A 、B分别启动storage_sync_thread_entrance 同步线程,先向 tracker 发送TRACKER_PROTO_CMD_STORAGE_SYNC_SRC_REQ(86)命令,请求同步源,tracker会把同步源IP及同步时间戳返回。
11、stroage A 、B节点的同步线程收到TRACKER_PROTO_CMD_STORAGE_SYNC_SRC_REQ响应后,会检查返回的同步源IP是否和⾃⼰本地IP⼀致,如果⼀致置need_sync_old=1表示将作为源数据把⽼的数据同步给新节点C,如果不⼀致置need_sync_old=0,则等待节点C状态为Active时再同步(增量同步)。因为,如果A、 B同时作为同步源,同步数据给C的话,C数据会重复。这⾥假设节点A,判断tracker返回的是同步源和⾃⼰的IP⼀致,A作为同步源,将数据同步给storage C节点。
12、Storage A同步线程继续同步流程,⽤同步⽬的的ip和端⼝设为⽂件名,.mark为后缀,如192.168.1.3_23000.mark,将同步信息写⼊此⽂件。将Storage C的状态置为FDFS_STORAGE_STATUS_SYNCING 上报给tracker,开始同步
(1)从data/sync⽬录下,读取binlog_index.dat中的binlog⽂件id,从binlog.000读取逐⾏读取,进⾏解析。
(2)根据操作类型,将数据同步给storage C,具体同步操作由函数storage_sync_data完成。
(3)发送数据给Stroage C,StroageC 接收数据并保存。
(4)binlog⽂件读完之后,会将Stroage C 状态置为FDFS_STORAGE_STATUS_OFFLINE,向tracker 报告,同时更新同步状态到本地⽂件mark⽂件。
(5)同步完成后调⽤ tracker_sync_notify 函数发送TRACKER_PROTO_CMD_STORAGE_SYNC_NOTIFY通知tracker同步完成,将storage C的 状态置为FDFS_STORAGE_STATUS_ONLINE
(6)当storageC向tracker发起heart beat时,tracker将其状态更改为FDFS_STORAGE_STATUS_ACTIVE。
至此,同步完成,storage C和A、B状态一样。
新机器加入时,会有一些网络拥塞,所以同组storage要放到同一个局域网内做一个集群。注意推送的规则不一样,常规情况下,是主动推送;对于新加入的机器,在状态ACTIVE之前,所有操作都是被动的。

超时检测机制

storage连接有定时器检测,如果同步或数据传送时数据不完整,会检测出来。
1、发送者断开,接受者通过pTask->total_lenpClientInfo->total_len等,检测到数据不完整,删除该文件。
2、发送者一直不发,接收者超时断开,接受者能检测到数据不完整,删除该文件。

小文件合并

fastdfs以指定大小作为一个trunk文件,将小文件合并为一个较大的trunk文件,目的有二,一是减少目录下文件数量,节约查找文件的时间;二是节省空间,因为文件系统中有空间对齐的问题,一个文件至少占4KB。

配置tracker⽂件

要使用小文件合并存储,需要先对tracker.conf进行配置。

# 是否启⽤trunk存储,缺省false
use_trunk_file = false
# trunk⽂件最⼩分配单元 字节,缺省256
slot_min_size = 256
# trunk内部存储的最⼤⽂件,超过该值会被独⽴存储,缺省16M
slot_max_size = 1MB
# ⽂件 trunk空间对⻬
# default value is 0 (never align)
# NOTE: the larger the alignment size, the less likely of disk
# fragmentation, but the more space is wasted.
trunk_alloc_alignment_size = 256
# 连续空闲的空间是否合并,例如256 512 ,是否合并为一个768
# default value is false
trunk_free_space_merge = true
# if delete / reclaim the unused trunk files
# default value is false
delete_unused_trunk_files = false
# trunk⽂件⼤⼩,默认 64MB
trunk_file_size = 64MB
# 是否预先创建trunk⽂件,缺省false
trunk_create_file_advance = false
# 预先创建trunk⽂件的基准时间
trunk_create_file_time_base = 02:00
# 预先创建trunk⽂件的时间间隔 ⼀天
trunk_create_file_interval = 86400
# the threshold to create trunk file
# when the free trunk file size less than the threshold,
# will create he trunk files
#trunk创建⽂件的最⼤空闲空间
trunk_create_file_space_threshold = 20G
# if check trunk space occupying when loading trunk free spaces
# the occupied spaces will be ignored
# default value is false
# since V3.09
# NOTICE: set this parameter to true will slow the loading of trunk spaces
# when startup. you should set this parameter to true when neccessary.
# 启动时是否检查每个空闲空间列表项已经被使⽤
trunk_init_check_occupying = false
# if ignore storage_trunk.dat, reload from trunk binlog
# default value is false
# set to true once for version upgrade when your version less than V3.10
# 是否纯粹从trunk-binlog重建空闲空间列表
trunk_init_reload_from_binlog = false
# the min interval for compressing the trunk binlog file
# unit: second, 0 means never compress
# FastDFS compress the trunk binlog when trunk init and trunk destroy
# recommand to set this parameter to 86400 (one day)
# default value is 0
trunk_compress_binlog_min_interval = 86400
  • 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

截取了一部分,其中一些重要的比如开启trunk选项、最小分配单元、trunk文件大小等,剩下的可以根据注释进行修改。这里提醒一下,认真看看注释,因为有的项注释中说的缺省值与配置文件中一开始给的值不一样。
这里还需要注意一点,trunk的binlog与普通文件的binlog不一样,在/data/trunk目录中,有专门的同步线程、专门的入口函数进行同步。

合并存储⽂件命名

向fastdfs上传⽂件成功时,服务器返回该⽂件的存取ID叫做fileid,当没有启动合并存储时该fileid和磁盘上实际存储的⽂件⼀⼀对应,当采⽤合并存储时就不再⼀⼀对应⽽是多个fileid对应的⽂件被存储成⼀个⼤⽂件。
下⾯将采⽤合并存储后的⼤⽂件统称为trunk⽂件,没有合并存储的⽂件统称为源⽂件。
这里要区分三个概念:
1、trunk⽂件:storage服务器磁盘上存储的实际⽂件,默认⼤⼩为64MB
2、合并存储⽂件的fileid:表示服务器启⽤合并存储后,每次上传返回给客户端的fileid,注意此时该fileId与磁盘上的⽂件没有⼀⼀对应关系
3、没有合并存储的fileid:表示服务器未启⽤合并存储时,upload时返回的fileid
trunk⽂件所在位置及⽂件名格式:fdfs_storage1/data/00/00/000001⽂件名从1开始递增,类型为int。

合并存储⽂件的fileid

合并存储⽂件的fileid与没有合并存储⽂件的fileid是有区别的。
1、没有合并存储时file id:⽂件名(不含后缀名)采⽤base64编码,包含如下5个字段(每个字段均为4字节整数)
group1/M00/00/00/wKgqHV4OQQyAbo9YAAAA_fdSpmg855.txt
这个⽂件名中,除了.txt为⽂件后缀,wKgqHV4OQQyAbo9YAAAA_fdSpmg855 这部分是⼀个base64编码缓冲区,组成如下:
storage_id(ip的数值型)源storage server ID或IP地址
timestamp(⽂件创建时间戳)
file_size(若原始值为32位则前⾯加⼊⼀个随机值填充,最终为64位)
crc32(⽂件内容的检验码)
随机数(引⼊随机数的⽬的是防⽌⽣成重名⽂件)

没有合并存储⽂件的fileid

如果采⽤了合并存储,⽣成的⽂件id将变⻓。
group1/M00/00/00/eBuDxWCwrDqITi98AAAA-3Qtcs8AAAAAQAAAgAAAAIA833.txt
⽂件名后⾯多了base64⽂本⻓度16字符(12个字节)。 这部分同样采⽤base64编码,包含如下3个字段(每个字段均为4字节整数):alloc_size、trunk file id、offset。具体包括了如下信息:
file_size:占⽤⼤⽂件的空间(注意按照最⼩slot-256字节进⾏对⻬)
mtime:⽂件修改时间
crc32:⽂件内容的crc32码
formatted_ext_name:⽂件扩展名
alloc_size:分配空间,⼤于或等于⽂件⼤⼩
trunk file id:⼤⽂件ID如000001
offset:⽂件内容在trunk⽂件中的偏移量
size:⽂件⼤⼩,真正的⽂件⼤⼩,file_size则为占⽤的空间⼤⼩。
最后再提示一下,fastdfs是不会自己存储文件名的,都是将fileid返回给客户端。客户端想要查找文件,需要自己存储对应的文件名。

trunk⽂件内部结构

trunk内部是由多个⼩⽂件组成,每个⼩⽂件都会有⼀个trunkHeader,以及紧跟在其后的真实数据,即header+data,数据二进制存放的结构,header结构如下

|—1byte —|— 4bytes —|— 4bytes —|—4bytes— |—4bytes—|—7bytes—|
|—filetype—|—alloc_size—|—filesize—|—crc32 —|—mtime —|—formatted_ext_name—|
|||————————————————file_data filesize bytes ——————————————————|||
|———————————————————— file_data ——————————————————————|
  • 1
  • 2
  • 3
  • 4

trunkheader固定占⽤了24字节。
下面介绍一下存储过程。
trunk⽂件为64MB(默认),因此每次创建⼀次trunk⽂件总是会产⽣空余空间,⽐如为存储⼀个10MB⽂件,创建⼀个trunk⽂件,那么就会剩下接近54MB的空间(当然,header 还会占用24字节,后⾯为了⽅便叙述暂时忽略其所占空间),下次要想再存储10MB⽂件时就不需要创建新的⽂件,存储在已经创建的trunk⽂件中即可。另外当删除⼀个存储的⽂件时,也会产⽣空余空间。
在storage内部会为每个store_path构造⼀棵以空闲块⼤⼩作为关键字的空闲平衡树,相同⼤⼩的空闲块保存在链表之中。每当需要存储⼀个⽂件时会⾸先到空闲平衡树中查找⼤于并且最接近的空闲块,然后试着从该空闲块中分割出多余的部分作为⼀个新的空闲块,加⼊到空闲平衡树中。例如:要求存储⽂件为300KB,通过空闲平衡树找到⼀个350KB的空闲块,那么就会将350KB的空闲块分裂成两块,前⾯300KB返回⽤于存储,后⾯50KB则继续放置到空闲平衡树之中。假若此时找不到可满⾜的空闲块,那么就会创建⼀个新的trunk⽂件64MB,将其加⼊到空闲平衡树之中,再次执⾏上⾯的查找操作(此时总是能够满⾜了)。

文件查重

fastdfs自己也有文件查重机制,需要在storage.conf 中进行配置。

# if check file duplicate, when set to true, use FastDHT to store file indexes# 1 or yes: need check
# 0 or no: do not check
# default value is 0
check_file_duplicate = 0
  • 1
  • 2
  • 3
  • 4

这⾥依赖FastDHT去处理,本质⽽⾔就是对上传的⽂件内容进⾏hash计算或者md5计算出来唯⼀值并保存,然后每次上传⽂件统计出来的唯⼀值都去存储的数据⾥⾯查找,如果有同样的则认为⽂件重复。但这种⽅式导致每个上传⽂件都要在服务器做实时唯⼀值计算,⽐较消耗cpu的资源,建议还是类似云盘项目的⽅式去做,⽂件唯⼀值由客户端去计算,计算出来唯⼀值后去mysql + redis的组合⾥⾯查询是否存在同样的⽂件。

性能测试与优化

fastdfs中提供了性能的测试文件,可以进行单机测试、局域网测试、外网测试等,这里不再详细介绍。这里提示一点,这些测试文件计算时间是计算每个文件上传/下载/删除的用时,单位是ms,如果文件小,特别是在单机或局域网测试的情况下,单个文件操作不足1ms,那么就会计为0ms再进行累加,这样就有错误,建议改为us。
再提示一下,如果在云服务器进行测试,需要注意内外网地址的区分,还要查看一下防火墙,是否开启了相应端口。

性能优化

关于fastdfs性能,本质⽽⾔要考虑的点有,⽹络带宽、磁盘读写速度、⽂件⼤⼩、同组storage的个数。
对于上传文件而言,提升性能的方式可以有,增加group、增加带宽、使⽤读写性能⾼的磁盘。
对于下载而言,提升性能的方式可以有,增加storage、增加group、增加带宽、使⽤读写性能⾼的磁盘。
具体分析不再赘述,只是提示一点,要清楚性能的瓶颈,着手瓶颈来优化性能。一般而言,fastdfs的瓶颈是在storage,所以并发量要结合带宽、磁盘读写速率、上传/下载文件来计算。
实际商用的group数的配置,是先通过一个group来设置,然后通过类似乘n的操作推广到多个group中。

参数调优

最⼤并发连接数设置

在配置文件storage.conf和tracker.conf都有配置
参数名:max_connections
缺省值:1024
fastdfs采⽤预先分配好buffer队列的做法,分配的内存⼤⼩为max_connections *buff_size的空间,因此配置的连接数越⼤,消耗的内存越多。所以不建议配置得过⼤,以避免⽆谓的内存开销。但是,如果传的基本上是大文件,建议设大一些,因为文件越大,用户占用连接的时间越长(上传/下载耗时增加),后面的客户端就连不进来。当然,fastdfs不建议存储大文件,因为大文件一般要分块进行存储,而fastdfs没有这一操作,直接一整个给存进来。
另外,最大连接数还可以根据带宽÷每个连接最小速率进行一个理论上的计算。服务器也可以控制下载速度,例如设置一个定时器,读取文件出来后,等待5ms再输出。

⼯作线程数设置

也是在配置文件storage.conf和tracker.conf都有配置
参数名: work_threads
缺省值:4
为了避免CPU上下⽂切换的开销,以及不必要的资源消耗,不建议将本参数设置得过⼤。为了发挥出多个CPU的效能,系统中的线程数总和,应等于CPU总数。
对于tracker server,公式为work_threads + 1 = CPU数
对于storage server,公式为work_threads + 1 + (disk_reader_threads +disk_writer_threads) * store_path_count = CPU数

storage⽬录数设置

在storage.conf中配置
参数名:subdir_count_per_path
缺省值:256
fastdfs采⽤⼆级⽬录的做法,⽬录会在 fastdfs初始化时⾃动创建。存储海量⼩⽂件, 打开了trunk存储⽅式的情况下,建议将本参数适当改⼩,⽐如设置为32,此时存放⽂件的⽬录数为 32 * 32 =1024。也可以通过计算结果来设置,假如trunk⽂件⼤⼩采⽤缺省值64MB,磁盘空间为2TB,那么每个⽬录下存放的trunk⽂件数均值为2TB / (1024 * 64MB) = 32个。

storage磁盘读写线程设置

disk_rw_separated:磁盘读写是否分离
disk_reader_threads:单个磁盘读线程数
disk_writer_threads:单个磁盘写线程数
如果磁盘读写混合,单个磁盘读写线程数为读线程数和写线程数之和,对于单盘挂载⽅式,磁盘读写线程分别设置为1即可;如果磁盘做了RAID,那么需要酌情加⼤读写线程数,这样才能最⼤程度地发挥磁盘性能。

storage同步延迟相关设置

sync_binlog_buff_interval:将binlog buffer写⼊磁盘的时间间隔,取值⼤于0,缺省值为60s
sync_wait_msec:如果没有需要同步的⽂件,对binlog进⾏轮询的时间间隔,取值⼤于0,缺省值为100ms
sync_interval:同步完⼀个⽂件后,休眠的毫秒数,缺省值为0
为了缩短⽂件同步时间,可以将上述3个参数适当调⼩即可。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/170005
推荐阅读
相关标签
  

闽ICP备14008679号