理解FastDFS&nginx访问流程:
掌握FastDFS错误日志查看方法: base_path/log 中
理解FastDFS文件同步原理
掌握FastDFS文件合并存储机制
掌握FastDFS图片压缩机制
理解FastDFS快速定位文件机制
由于经常需要为FastDFS存储的文件提供http下载服务,但是FastDFS在其storage及tracker都内置了http服务, 但性能表现却不尽如人意。
所以增加了基于当前主流web服务器的扩展模块(包括nginx/apache),其用意在于利用web服务器直接对本机storage数据文件提供http服务,以提高文件下载的性能。
其实不使用Nginx的扩展模块,只安装web服务器(Nginx和Apache),也可以对存储的文件进行访问。那么为什么要使用Nginx的扩展模块来访问存储的文件,原因有两个:
模块结构
在每一台storage服务器主机上部署Nginx及FastDFS扩展模块,由Nginx模块对storage存储的文件提供http下载服务, 仅当当前storage节点找不到文件时会向源storage主机发起redirect或proxy动作。
概念说明
从Tracker Server获取到同组storage server集合后,就会为除了自己之外的其他同组storage server分别开启同步线程。
如果组内有三个Storage Server,那么每台Storage Server都应该开启两个独立同步线程。
binlog.index # 记录当前使用的binlog文件序号,如为100,则表示使用binlog.010
192.168.1.2_33450.mark
192.168.1.3_33450.mark
binlog.100
同步状态文件,192.168.1.2_33450.mark 记录本机到192.168.1.2_33450的同步状态。
binlog_index=0 # 对应于哪个binlog
binlog_offset=1334 # binlog.xxx的偏移量,可以直接这个偏移量获取下一行记录, 同步到哪一行了
need_sync_old=1 #本storage是否是对侧storage(10.100.66.82)的源结点, 同时是否需要从起点同步所有的记录
sync_old_done=1 # 是否同步完成过
until_timestamp=1457542256 # 上次同步时间结点
scan_row_count=23 # 总记录数
sync_row_count=11 #已同步记录数
Binlog文件内容:在该文件中是以binlog日志组成,比如:
1470292943 c M00/03/61/QkIPAFdQCL-AQb_4AAIAi4iqLzk223.jpg
1470292948 C M00/03/63/QkIPAFdWPUCAfiraAAG93gO_2Ew311.png
1470292954 d M00/03/62/QkIPAFdWOyeAO3eUAABvALuMG64183.jpg
1470292959 C M00/01/23/QUIPAFdVQZ2AL_o-AAAMRBAMk3s679.jpg
文件说明:
其中的每一条记录都是使用[空格符]分成三个字段,分别为:
C 表示源创建、c 表示副本创建
A 表示源追加、a 表示副本追加
D 表示源删除、d 表示副本删除
从fastdfs文件同步原理中我们知道Storaged server之间的同步都是由一个独立线程负责的,这个线程中的所有操作都是以同步方式执行的。比如一组服务器有A、B、C三台机器,那么在每台机器上都有两个线程负责同步,如A机器,线程1负责同步数据到B,线程2负责同步数据到C。每个同步线程负责到一台Storage的同步,以阻塞方式进行。
在Storage.conf配置文件中,只配置了Tracker的IP地址,并没有配置组内其他的Storage。因此同组的其他Storage必须从Tracker获取。具体过程如下:
1)Storage启动时为每一个配置的Tracker启动一个线程负责与该Tracker的通讯。
2)默认每间隔30秒,与Tracker发送一次心跳包,在心跳包的回复中,将会有该组内的其他Storage信息。
3)Storage获取到同组的其他Storage信息之后,为组内的每个其他Storage开启一个线程负责同步。
每个同步线程负责到一台Storage的同步,以阻塞方式进行。
1)打开对应Storage的mark文件,如负责到10.0.1.1的同步则打开10.0.1.1_23000.mark文件, 从中读取binlog_index、binlog_offset两个字段值, 如取到值为:1、100,那么就打开binlog.001文件,seek到100这个位置。
2)进入一个while循环,尝试着读取一行,若读取不到则睡眠等待。若读取到一行, 并且该行的操作方式为源操作,如C、A、D、T(大写的都是), 则将该行指定的操作同步给对方(非源操作不需要同步), 同步成功后更新binlog_offset标志,该值会定期写入到10.0.1.1_23000.mark文件之中。
举个例子:一组内有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存在这个文件,可以从其下载。
Tracker也会定期将该值写入到一个文件之中,storage_sync_timestamp.dat,内容如下:
group1,10.0.0.1, 0, 1408524351, 1408524352
group1,10.0.0.2, 1408524353, 0, 1408524354
group1,10.0.0.3, 1408524355, 1408524356, 0
每一行记录了,对应Storage同步给其他Storage的最后时间,如第一行表示10.0.0.1同步给10.0.0.2的最后时间为1408524351,同步给10.0.0.3的最后时间为1408524352;同理可以知道后面两行的含义了。每行中间都有一个0,这个零表示自己同步给自己,没有记录。按照纵列看,取非零最小值就是最后最早同步时间了,如第三纵列为10.0.0.1被同步的时间,其中1408524353就是其最后最早被同步时间。
问题一:同步到中途,把源文件删除,备份服务器怎么办?
源文件删除,同步就读取不到数据,把中断写入日志,直接同步下一条数据。
问题二:同步到中途,服务器宕机,如何解决?
Make,binlog 同步文件,记录同步偏移量,时间戳。 服务器宕机,下次重新启动,重新加载同步文件,发现没有同步完成。继续同步。
问题三:此时需要下载文件,如何确定从那台storage服务器下载文件呢? 此时服务器同步未完成?
Tracker 通过解析文件的创建时间和每台Storage机器的最小同步时间, 创建时间小于最小同步时间,说明该Storage存在这个文件,可以从其下载。
在处理【海量小文件(LOSF)】问题上,文件系统处理性能会受到显著的影响,在读写次数(IOPS)与吞吐量(Throughput)这两个指标上会有不少的下降。通常我们认为大小在【1MB以内】的文件称为小文件,【百万级数量及以上】称为海量,由此量化定义海量小文件问题,以下简称LOSF(全称lots of small files)
由于小文件数据内容较少,因此元数据的访问性能对小文件访问性能影响巨大。当前主流的磁盘文件系统基本都是面向大文件高聚合带宽设计的,而不是小文件的低延迟访问。磁盘文件系统中,目录项(dentry)、索引节点(inode)和数据(data)保存在存储介质的不同位置上。因此,访问一个文件需要经历至少3次独立的访问。
这样,并发的小文件访问就转变成了大量的随机访问,而这种访问对于广泛使用的磁盘来说是非常低效的。同时,文件系统通常采用Hash树、 B+树或B*树来组织和索引目录,这种方法不能在数以亿计的大目录中很好的扩展,海量目录下检索效率会明显下降。正是由于单个目录元数据组织能力的低效,文件系统使用者通常被鼓励把文件分散在多层次的目录中以提高性能。然而,这种方法会进一步加大路径查询的开销
磁盘文件系统使用块来组织磁盘数据,并在inode中使用多级指针或hash树来索引文件数据块。数据块通常比较小,一般为1KB、2KB或4KB。当文件需要存储数据时,文件系统根据预定的策略分配数据块,分配策略会综合考虑数据局部性、存储空间利用效率等因素,通常会优先考虑大文件I/O带宽。
对于大文件,数据块会尽量进行连续分配,具有比较好的空间局部性。对于小文件,尤其是大文件和小文件混合存储或者经过大量删除和修改后,数据块分配的随机性会进一步加剧,数据块可能零散分布在磁盘上的不同位置,并且会造成大量的磁盘碎片(包括内部碎片和外部碎片),不仅造成访问性能下降,还导致大量磁盘空间浪费。对于特别小的小文件,比如小于4KB,inode与数据分开存储,这种数据布局也没有充分利用空间局部性,导致随机I/O访问,目前已经有文件系统实现了data in inode。
对于小文件的I/O访问过程,读写数据量比较小,这些流程太过复杂,系统调用开销太大,尤其是其中的open()操作占用了大部分的操作时间。当面对海量小文件并发访问,读写之前的准备工作占用了绝大部分系统时间,有效磁盘服务时间非常低,从而导致小I/O性能极度低下。
因此一种【解决海量小文件】的途径就是将小文件【合并存储】成大文件,使用seek来定位到大文件的指定位置来访问该小文件。
FastDFS提供了【合并存储】功能,可以将【海量小文件】,合并存储为【大文件】。
默认创建的大文件为64MB,然后在该大文件中存储很多小文件。大文件中容纳一个小文件的空间称为一个Slot,规定Slot最小值为256字节,最大为16MB,也就是小于256字节的文件也需要占用256字节,超过16MB的文件不会合并存储而是创建独立的文件。
当没有启动合并存储时fileid和磁盘上实际存储的文件一一对应;当采用合并存储时就不再一一对应,而是多个fileid对应的文件被存储成一个大文件。
注:下面将采用合并存储后的大文件统称为trunk文件,没有合并存储的文件统称为源文件;
合并存储时fileid:
group1/M00/00/00/CgAEbFQWWbyIPCu1AAAFr1bq36EAAAAAQAAAAAAAAXH82.conf
可以看出合并存储的fileid更长,因为其中需要加入保存的大文件id以及偏移量。
trunk内部是由多个小文件组成,每个小文件都会有一个trunkHeader(占用24个字节),以及紧跟在其后的真实数据,结构如下:
|||——————————————————— 24bytes———-----———-——————————————————|||
|—1byte —-|— 4bytes ——|—4bytes —|—4bytes—|—4bytes—|——— 7bytes ————-|———2bytes————|
|—filetype—|—alloc_size—|—filesize—|—crc32—–|—mtime—|—formatted_ext_name—|—file_data filesize bytes—|
||———————file_data————————————————————————————||
FastDFS提供了合并存储功能的实现,所有的配置都在tracker.conf文件之中,具体摘录如下:
注意:开启合并存储只需要设置use_trunk_file = true和store_server=1
Trunk文件为64MB(默认),因此每次创建一次Trunk文件总是会产生空余空间,比如为了存储一个10MB文件,创建一个Trunk文件,那么就会剩下接近54MB的空间(TrunkHeader 会24字节,后面为了方便叙述暂时忽略其所占空间),下次要想再次存储10MB文件时就不需要创建新的文件,存储在已经创建的Trunk文件中即可。
另外当删除一个存储的文件时,也会产生空余空间。
在Storage内部会为每个store_path构造一颗以空闲块大小作为关键字的空闲平衡树,相同大小的空闲块保存在链表之中。
每当需要存储一个文件时会首先到空闲平衡树中查找大于并且最接近的空闲块,然后试着从该空闲块中分割出多余的部分作为一个新的空闲块,加入到空闲平衡树中。
例如:
假若所有的Storage都具有分配空闲空间的能力(upload文件时自主决定存储到哪个TrunkFile之中),那么可能会由于同步延迟导致数据冲突。
例如:
Storage-A中上传了一个文件A.txt 100KB,将其保存到000001这个TrunkFile的开头,与此同时,Storage-B也上传了一个文件B.txt 200KB,也将其保存在000001这个TrunkFile文件的开头,当Storage-B收到Storage-A的同步信息时,他无法将A.txt 保存在000001这个trunk文件的开头,因此这个位置已经被B.txt占用。
为了处理这种冲突,引入了TrunkServer概念,只有TrunkServer才有权限分配空闲空间,决定文件应该保存到哪个TrunkFile的什么位置。TrunkServer由Tracker指定,并且在心跳信息中通知所有的Storage。
引入TrunkServer之后,一次Upload请求,Storage的处理流程图如下:
开启了合并存储服务后,除了原本的源文件(trunk文件和未合并文件)同步之外,TrunkServer还多了TrunkBinlog的同步(非TrunkServer没有TrunkBinlog同步)。源文件的同步与没有开启合并存储时过程完全一样,都是从binlog触发同步文件。
TrunkBinlog记录了TrunkServer所有分配与回收空闲块的操作,由TrunkServer同步给同组中的其他Storage。TrunkServer为同组中的其他Storage各创建一个同步线程,【每秒】将TrunkBinlog的变化同步出去。同组的Storage接收到TrunkBinlog【只是保存到文件中,不做其他任何操作】
TrunkBinlog文件文件记录如下:
1410750754 A 0 0 0 1 0 67108864
1410750754 D 0 0 0 1 0 67108864
各字段含义如下(按照顺序):
时间戳
操作类型(A:增加,D:删除)
当作为TrunkServer的Storage启动时可以从TrunkBinlog文件中中加载所有的空闲块分配与加入操作,这个过程就可以实现空闲平衡树的重建。
当长期运行时,随着空闲块的不断删除添加会导致TrunkBinlog文件很大,那么加载时间会很长,FastDFS为了解决这个问题,引入了检查点文件storage_trunk.dat,每次TrunkServer进程退出时,会将当前内存里的空闲平衡树导出为storage_trunk.dat文件,该文件的第一行为TrunkBinlog的offset,也就是该检查点文件负责到这个offset为止的TrunkBinlog。也就是说下次TrunkServer启动的时候,先加载storage_trunk.dat文件,然后继续加载这个offset之后的TrunkBinlog文件内容。
上文提到的storage_trunk.dat既是检查点文件,其实也是一个压缩文件,因为从内存中将整个空闲平衡树直接导出,没有了中间步骤(创建、删除、分割等步骤),因此文件就很小。这种方式虽然实现了TrunkServer自身重启时快速加载空闲平衡树的目的,但是并没有实际上缩小TrunkBinlog文件的大小。假如这台TrunkServer宕机后,Tracker会选择另外一台机器作为新的TrunkServer,这台新的TrunkServer就必须从很庞大的TrunkBinlog中加载空闲平衡树,由于TrunkBinlog文件很大,这将是一个很漫长的过程
为了减少TrunkBinlog,可以选择压缩文件,在TrunkServer初始化完成后,或者退出时,可以将storage_trunk.dat与其负责偏移量之后的TrunkBinlog进行合并,产生一个新的TrunkBinlog。由于此时的TrunkBinlog已经从头到尾整个修改了,就需要将该文件完成的同步给同组内的其他Storage,为了达到该目的,FastDFS使用了如下方法:
nginx_http_image_filter_module 在nginx 0.7.54以后才出现的,用于对JPEG, GIF和PNG图片进行转换处理(压缩图片、裁剪图片、旋转图片)。这个模块默认不被编译,所以要在编译nginx源码的时候,加入相关配置信息(下面会给出相关配置)。
# 检测nginx模块安装情况
/kkb/server/nginx/sbin/nginx -V
# 安装gd,HttpImageFilterModule模块需要依赖gd-devel的支持
yum -y install gd-devel
# 将http_image_filter_module包含进来
cd /root/nginx-1.15.6
./configure \
--prefix=/kkb/server/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--with-http_gzip_static_module \
--add-module=/opt/fastdfs-nginx-module-1.20/src \ #http模块必须添加
--with-http_stub_status_module \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_image_filter_module
make && make install
# 注意:重新编译时,必须添加上nginx编译http的扩展模块,否则无法访问fastdfs图片
location ~ group1/M00/(.+)_(\d+)x(\d+)\.(jpg|gif|png) {
# 设置别名(类似于root的用法)
alias /kkb/server/fastdfs/storage/data/;
# fastdfs中的ngx_fastdfs_module模块
ngx_fastdfs_module;
set $w $2;
set $h $3;
if ($w != "0") {
rewrite group1/M00(.+)_(\d+)x(\d+)\.(jpg|gif|png)$ group1/M00$1.$4 break;
}
if ($h != "0") {
rewrite group1/M00(.+)_(\d+)x(\d+)\.(jpg|gif|png)$ group1/M00$1.$4 break;
}
#根据给定的长宽生成缩略图
image_filter resize $w $h;
#原图最大2M,要裁剪的图片超过2M返回415错误,需要调节参数image_filter_buffer
image_filter_buffer 2M;
#try_files group1/M00$1.$4 $1.jpg;
}
# 重启Nginx, 由于添加了新的模块,所以nginx需要重新启动,不需要重新加载reload
/kkb/server/nginx/sbin/nginx -s stop
/kkb/server/nginx/sbin/nginx