
1. 为什么要把装FastDFS和缩略图放在一起做1.1 存储选型时的心路历程做图片类业务的人应该都有同感文件存储选型这件事看着简单真落地时全是决策。用云厂商的对象存储按量付费功能齐全但数据量一上来账单就很酸爽自己搭MinIOS3协议兼容性确实好但小团队维护一堆依赖组件也吃不消。FastDFS在这时候反而显得特别务实——纯C实现安装包不到几百KB不依赖MySQL、Redis这些外部中间件下载编译完直接就能跑。我当时的业务场景很典型一套内部CMS系统每天会产生大量商品图、内容配图单张2~5MB高峰期一天几千张上传。图片不能丢访问要稳定而且前台页面需要多种尺寸的缩略图列表页小图、详情页中图、移动端裁剪图。把原图扔FastDFS由Nginx层来做实时缩略图裁剪这是我最开始的设想。如果你和我一样面临图片存哪、缩略图怎么做这两个问题这篇文章应该能帮你省下不少摸索时间。我会把FastDFS的完整安装过程、Nginx接入的配置细节、缩略图生成的两种实现路线以及我在实测中踩过的几个真实的坑全部过一遍。1.2 缩略图问题的现实处境很多人在搭建FastDFS的时候只把目光放在文件能上传、能下载这个层面等到前端要图了才发现问题一张原图5MB直接让浏览器去加载列表页一次要拉40张页面直接卡死。缩略图不是锦上添花的功能而是图片类业务的刚需。缩略图做在哪里、什么时候做、用什么工具做这里有三种路线上传时由后端生成多张静态缩略图分别存储访问时由Nginx的image_filter模块实时裁剪前端JS或者CSS按比例压缩显示伪缩略图不减少带宽第三种只适合要求不高的场景实际生产基本不会用前两种方案或者组合使用。我在最开始是倾向于上传时生成静态文件的因为觉得实时裁剪会有性能开销但实测下来配合Nginx的proxy_cache缓存实时裁剪的体验出乎意料地好而且维护成本低得多。这一点到后面专门讲。2. FastDFS的核心角色Tracker、Storage、文件ID2.1 三者各自干什么FastDFS里有两个核心服务进程Tracker Server和Storage Server。很多人第一次看FastDFS的架构文档会晕其实用一句大白话就能说清楚Tracker是调度中心Storage是真正的仓库。Tracker不存业务文件数据它的职责是记录每个Storage节点的状态——哪些group可用、每个group里哪些storage在线、剩余容量多少。客户端上传文件时先找Tracker要一个可写的存储节点地址然后才真正把文件发过去。也就是说Tracker更像一个路由或者中介本身不碰文件内容。Storage按照group来组织每个group里可以有一台或多台storage节点同一group内的多台storage之间互为备份数据是同步的。比如group1里有两台storage文件上传到group1的A节点后A节点会自动同步到group1的B节点这样A挂掉后B还能正常提供访问。我在实际部署时只用了一个group、一个storage节点测试环境和个人业务完全够用。如果要做高可用通常做法是每个group放两台storageTracker也可以部署两台做HA但那是后话。对于第一次上手的人单机部署先把链路跑通比纠结集群方案更重要。2.2 文件ID是怎么拼出来的FastDFS上传成功后返回的不是一个普通文件名而是一个格式固定的文件ID常见的样子是group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg这串东西拆开来看是这样的部分含义group1存储组名称M00虚拟目录对应store_path0路径下的data目录00/00按日期生成的二级目录wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg实际文件名包含反解的时间戳、IP、文件大小等元信息这个ID必须完整保留因为后续所有下载、删除操作都要靠它。我见过有人把文件ID存数据库时只存了后半截文件名结果前缀丢了整个文件变成孤儿。上传接口返回的file_id一定要整串存。关于M00这个虚拟路径有一个关键点它不是真实存在的目录而是通过软链接把真实存储路径映射过来的。后面配置Nginx模块时如果忘了做这个软链接nginx访问就会一直404这是安装中非常经典的一个坑我在第6节会专门还原整个排查过程。3. 从零到一的FastDFS安装实战3.1 编译前的依赖准备我用的环境是CentOS 7.9内核版本3.10。FastDFS本身依赖不多但编译工具链必须齐。我习惯先把这些装好yum install -y gcc gcc-c make automake autoconf libtool \ zlib zlib-devel pcre pcre-devel openssl openssl-devel \ gd gd-devel libpng libpng-devel libjpeg libjpeg-devel \ libevent libevent-devel perl perl-devel这里多说一句关于gd和libpng、libjpeg这些不是FastDFS编译必需的而是后面Nginx的image_filter缩略图模块要用的。Nginx的image_filter模块依赖gd库缺了它编译时会直接报找不到gd.h到时候得回头补装再重新configure一次纯浪费时间。另外注意一下libeventFastDFS的旧版本会用libevent做网络事件库如果你下的是新版FastDFS比如6.x依赖关系已经移到libfastcommon里可能用不到libevent了。但装上也不多余有些老的辅助工具脚本还会调用它。3.2 编译安装libfastcommon与FastDFSFastDFS从5.x开始把公共库拆成了独立的libfastcommon项目所以安装顺序必须是先libfastcommon再FastDFS本体。顺序反了你configure都会过不去。# 下载并安装 libfastcommon wget https://github.com/happyfish100/libfastcommon/archive/refs/tags/V1.0.43.tar.gz tar -zxvf V1.0.43.tar.gz cd libfastcommon-1.0.43 ./make.sh ./make.sh install # 下载并安装 FastDFS wget https://github.com/happyfish100/fastdfs/archive/refs/tags/V6.06.tar.gz tar -zxvf V6.06.tar.gz cd fastdfs-6.06 ./make.sh ./make.sh install编译过程中如果报错绝大多数情况是缺了某个开发包看错误提示缺什么装什么就行。两个项目编译安装完成后FastDFS的可执行文件fdfs_trackerd、fdfs_storaged、fdfs_upload_file等会安装到/usr/bin下配置文件模板会放在/etc/fdfs下。3.3 tracker、storage、client三份配置逐项讲解FastDFS安装完不会自动生成配置需要从模板复制。在/etc/fdfs目录下有三个模板文件tracker.conf.sample、storage.conf.sample、client.conf.sample。cd /etc/fdfs cp tracker.conf.sample tracker.conf cp storage.conf.sample storage.conf cp client.conf.sample client.conftracker.conf需要修改的核心项就两个port22122 base_path/data/fastdfs/trackerbase_path是tracker存放日志和数据文件的目录必须手动创建并且要注意权限mkdir -p /data/fastdfs/tracker mkdir -p /data/fastdfs/storage mkdir -p /data/fastdfs/storage_data mkdir -p /data/fastdfs/clientstorage.conf要改的东西多一点group_namegroup1 port23000 base_path/data/fastdfs/storage store_path_count1 store_path0/data/fastdfs/storage_data tracker_server127.0.0.1:22122 http.server_port8888逐个解释一下group_name存储组名默认group1就行多个group之间文件ID会不同portstorage服务监听端口base_pathstorage自身的日志和状态文件目录注意和store_path0分开store_path0实际存文件的根路径tracker_servertracker的地址和端口等号右边写成IP:端口格式http.server_portstorage内置HTTP服务的端口后面如果不用内置HTTP服务这个端口不会实际用到但配置上也无妨client.conf是命令行工具使用的客户端配置核心就两个base_path/data/fastdfs/client tracker_server127.0.0.1:221223.4 启动与验证以上配置完成后先启动tracker再启动storage顺序别反storage启动时要连tracker/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf /usr/bin/fdfs_storaged /etc/fdfs/storage.conf启动后用监控命令看storage是否已经注册到tracker/usr/bin/fdfs_monitor /etc/fdfs/client.conf输出里如果看到ip_addr 127.0.0.1且状态是ACTIVE说明storage已经正常上线了。接下来做一个上传测试。随便准备一个文件用fdfs_upload_file上传/usr/bin/fdfs_upload_file /etc/fdfs/client.conf /root/test.jpg如果一切正常会返回一个完整的file_idgroup1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg到这一步FastDFS本身的安装和上传链路就算通了。但离通过浏览器访问文件还差一段路因为FastDFS自带的HTTP服务能力非常弱而且直接暴露storage端口不太安全。所以下一步必须把它接进Nginx。4. Nginx接入FastDFS文件怎么变成可访问的URL4.1 为什么非要Nginx模块FastDFS每个storage节点其实内置了一个HTTP server只要开启http.server_port理论上就能通过http://ip:8888/group1/M00/00/00/xxx.jpg访问文件。但实际生产我基本不推荐这么干原因有三个方面第一内置HTTP server性能一般和Nginx完全不是一个量级。第二没法和缩略图、防盗链、缓存这些需求直接结合。第三FastDFS官方自己都推荐用Nginx fastdfs-nginx-module来做访问层有人维护的方案永远比自研引擎靠谱。fastdfs-nginx-module这个模块的作用是当Nginx收到/group1/M00/xxx这样的请求时模块按规则解析出文件ID并从storage本地磁盘找到对应文件返回。与重定向到storage内置HTTP服务不同这个模块是直接读本地文件返回性能好得多。4.2 fastdfs-nginx-module编译与安装这里有个很重要的顺序问题如果你还没装Nginx直接把fastdfs-nginx-module一起编译进去如果已经装了Nginx就得用原编译参数重新configure一次把新模块加进去然后替换二进制文件。我是在同一台机器上装Nginx所以选择后者。fastdfs-nginx-module的模块源码在src目录下编译命令是这样wget https://github.com/happyfish100/fastdfs-nginx-module/archive/refs/tags/V1.22.tar.gz tar -zxvf V1.22.tar.gz # 关键步骤修改config文件里的include路径 cd fastdfs-nginx-module-1.22/src打开这个目录下的config文件找到这行CORE_INCS$CORE_INCS /usr/local/include这一行作用是告诉Nginx编译时去哪里找fastdfs的头文件。但是FastDFS实际安装后头文件路径通常是/usr/include/fastdfs和/usr/include/fastcommon默认路径十有八九不对。如果不修改编译到这个地方会直接报找不到头文件。改成这样CORE_INCS$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon改完后回到Nginx源码目录重新编译cd /usr/local/src/nginx-1.24.0 ./configure \ --prefix/usr/local/nginx \ --with-http_stub_status_module \ --with-http_ssl_module \ --with-http_image_filter_module \ --add-module/usr/local/src/fastdfs-nginx-module-1.22/src make make install需要注意的是configure参数里加上了--with-http_image_filter_module这就是后面做实时缩略图的关键模块。如果configure时提示找不到gd库回到第3.1节看依赖。4.3 配置mod_fastdfs.conf和nginx.conffastdfs-nginx-module安装后不会自动生成配置文件需要手动从源码复制过来cp /usr/local/src/fastdfs-nginx-module-1.22/src/mod_fastdfs.conf /etc/fdfs/mod_fastdfs.conf需要修改以下关键项connect_timeout10 base_path/tmp tracker_server127.0.0.1:22122 storage_server_port23000 group_namegroup1 store_path_count1 store_path0/data/fastdfs/storage_data这里有个非常关键的配置叫做url_have_group_name默认值是false如果不改成trueNginx模块解析URL时会把group1当作普通目录来处理结果就是找不到文件。新版本配置里可能写法略有差异但你一定要确认它是trueurl_have_group_nametrueNginx的server配置里加上location规则server { listen 8888; server_name _; location /group1/M00/ { ngx_fastdfs_module; } }注意listen的端口我用了8888和storage.conf里的http.server_port保持一致这样习惯上更顺。900M00路径对应的真实磁盘路径还需要一个软链接否则模块找不到文件ln -s /data/fastdfs/storage_data/data /data/fastdfs/storage_data/data/M00软链接做完后重启Nginx然后用浏览器访问之前上传的文件http://服务器IP:8888/group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg能正常显示原图说明Nginx和FastDFS的这一层已经打通了。到这里FastDFS的安装和HTTP访问链路就完整了。接下来才是今天真正的重头戏生成缩略图。5. 缩略图的两种实现路线与我的最终选择5.1 方案一上传时生成静态缩略图这个方案的业务逻辑很简单后端收到原图并上传成功后用ImageMagick或ffmpeg生成固定尺寸的缩略图再把缩略图上传到FastDFS另一个路径下数据库里同时存原图file_id和缩略图file_id。以生成200x200的小图为例命令大概是convert /tmp/upload.jpg -resize 200x200 /tmp/upload_thumb.jpg /usr/bin/fdfs_upload_file /etc/fdfs/client.conf /tmp/upload_thumb.jpg这个方案的优点是访问时零计算开销因为缩略图在上传时就生成好了用户请求过来直接读静态文件。而且缩略图尺寸可控存储的文件都是定好大小的。缺点是显而易见的每张原图要额外生成N张缩略图多占用存储空间如果以后要新增一个尺寸规格历史图片全得重新生成一遍上传流程变长用户等上传的同时还得等缩略图生成最让我受不了的是扩展性问题。我最初按三个尺寸生成缩略图结果产品说移动端页面需要一张375宽的新尺寸历史数据脏得一塌糊涂只能写脚本重扫。5.2 方案二Nginx实时裁剪Nginx的image_filter模块可以在请求到达时动态对图片做缩放处理。客户端请求/group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123_thumb.jpgNginx先通过fastdfs模块找到原图再用image_filter把图片裁剪成指定尺寸返回。这个方案的优势不产生额外存储磁盘上始终只有一张原图尺寸规格随时加不用重导数据上传流程不受影响它的短板是需要额外的CPU资源来做实时裁剪同时必须配置缓存。但是在中小流量场景下Nginx的裁剪能力完全扛得住配合proxy_cache之后效果更好。5.3 我选择的方案及完整的location配置我的最终选择是方案二但做了一点改良不是直接对原图URL做裁剪而是在Nginx层拦截带缩略图标识的URL利用image_filter对原图做动态缩放。这样可以保持数据库里只存原图file_id前端拼URL时加一个_200x200后缀即可。实现方式是在Nginx里新增一条location规则注意它要放在fastdfs模块的location之前server { listen 8888; server_name _; # 缩略图规则匹配 _200x200、_400x400 等后缀 location ~* ^/group1/M00/(.)_(\d)x(\d)\.(jpg|jpeg|png|gif)$ { set $image_name $1; set $image_w $2; set $image_h $3; set $image_ext $4; # 通过alias直接映射到原图路径 alias /data/fastdfs/storage_data/data/$image_name.$image_ext; image_filter resize $image_w $image_h; image_filter_jpeg_quality 85; image_filter_buffer 20M; # 缓存裁剪结果减少重复计算 proxy_cache_path /data/nginx_cache levels1:2 keys_zoneimgcache:100m max_size10g inactive30d; proxy_cache imgcache; proxy_cache_valid 200 30d; add_header X-Cache-Status $upstream_cache_status; } # 原图规则 location /group1/M00/ { ngx_fastdfs_module; } }这里image_filter resize $image_w $image_h会按指定宽高等比缩放等比例不足的边会填充白色。如果你希望严格裁剪成精确尺寸而不是等比缩放需要换用image_filter crop模式image_filter crop $image_w $image_h;crop模式会从图片中心区域进行裁剪适合需要严格固定尺寸的头像场景。我在列表页小图用的是crop详情页大图用的resize两种模式配合起来效果很好。重点说一下alias这个指令。因为正则捕获的是/group1/M00/...这一整段而文件在磁盘上的真实路径是/data/fastdfs/storage_data/data/开头。直接用alias把URL映射到磁盘路径让image_filter拿到的是真实文件这样的好处是绕过了fastdfs模块不用它再解析一遍。配置完成后reload Nginx/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx -s reload然后在浏览器里测试http://服务器IP:8888/group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123_200x200.jpg如果配置正确会返回一张200x200的缩略图。同时再用curl -I看一眼响应头确认有X-Cache-Status: HIT就说明缓存生效了。6. 实测中的故障排查全记录6.1 文件上传成功但浏览器404这是最常遇到的问题。上传返回了group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg但浏览器访问就是404。我当时的排查链路是这样的先用curl看返回头确认Nginx有没有把请求交给fastdfs模块处理。然后看Nginx错误日志这一步就定位到了问题open() /data/fastdfs/storage_data/data/group1/M00/00/00/wKgKZGT2nmeAUlgxAABaG3HqIBo123.jpg failed (2: No such file or directory)注意路径里的group1——它被当作目录的一部分了这说明url_have_group_name没有生效。回到mod_fastdfs.conf检查发现配置项写的是url_have_group_nametrue但忘记把配置复制到Nginx子进程实际读取的路径或者Nginx没reload。重新确认配置文件位置和reload之后问题解决。6.2 缩略图显示黑图或直接裂图image_filter模块如果显示黑图绝大多数情况是gd库没装全。我在测试时用了一张PNG图片其他格式正常唯独PNG出问题。排查发现是libpng-devel没装image_filter在处理PNG解压时失败返回了黑图。这个问题在yum装依赖时不容易察觉因为gd库编译时如果找不到libpng会静默降级不报错只导致PNG支持缺失。检查方法很简单在Nginx源码目录下执行./configure时看输出里有没有checking for libpng相关的行以及最终的objs目录下有没有png相关的.o文件。另外还有一个可能图片文件本身尺寸巨大比如超过20Mimage_filter处理时超出buffer限制会报415。我遇到过一次通过调大image_filter_buffer解决。错误日志里会明确提示buffer size超限定位不难。6.3 并发一高Nginx挂了怎么办image_filter是CPU密集型操作并发上来以后如果每个请求都去实时裁剪Nginx的CPU会迅速打满。我在压测时遇到过一个case200并发同时请求不同图片的缩略图Nginx worker进程CPU直接100%以上请求大量超时。解决方案就是我在第5.3节里提到的proxy_cache。把裁剪结果缓存下来同一个缩略图URL第二次请求直接走缓存完全不经过image_filter。压测结果对比很明显开启缓存后相同并发下CPU从100%降到15%左右。缓存目录注意给足磁盘空间我配置的max_size10g配合inactive30d表示30天内无访问的缓存会被清理。这个策略适合图片业务因为缩略图一般不会频繁变化缓存时间长一点没关系。还有一个小技巧在Nginx前再加一层CDN或者用客户端强缓存设置缩略图URL的Cache-Control头为max-age86400。这样大部分缩略图请求根本到不了你的源站服务器压力更小。再补充一个容易被忽略的问题FastDFS的storage目录权限。如果上传成功但Nginx读取时报Permission denied检查一下/data/fastdfs/storage_data的属主。我用root创建目录但Nginx worker进程是nginx用户文件读不了。解决办法最简单的是chown -R nginx:nginx /data/fastdfs/storage_data但要注意FastDFS的storage进程如果也以root启动它往目录里写文件时默认的umask和属主可能会影响Nginx读取。妥善的做法是把storage进程和Nginx的worker都统一成nginx用户或者确保目录对其他用户有读权限。6.4 视频文件也想出缩略图虽然标题主要围绕图片缩略图但现实中用户一定会上传视频视频没有封面缩略图体验很糟。FastDFS本身不处理视频帧需要在业务层用ffmpeg抽帧之后再走上面的图片缩略图链路ffmpeg -i /data/upload/video.mp4 -ss 00:00:02 -vframes 1 /data/upload/poster.jpg抽出来这一帧再调用FastDFS上传接口之后同样走缩略图规则一套链路复用。7. 几个让我省心的配置习惯踩过的坑多了慢慢会发现有些配置纯属一次配好长期受益的类型值得单独列出来。FastDFS的日志默认只按天滚动久了磁盘会被日志填满。我在tracker和storage的配置文件里都开启了日志自动清理log_file_keep_days30这个参数按实际需求设置一般保留30天足够出事时能回溯平时不怕日志膨胀。Nginx层的连接数配置也很关键尤其是图片服务的场景。我一般会在nginx.conf的events块里调整worker_connections并开启keepalivekeepalive_timeout 65; keepalive_requests 1000;keepalive_requests设为1000意思是单个长连接最多处理1000个请求后再断开。图片服务器上很多图片加载依赖浏览器并发下载保持长连接能明显减少TCP握手次数。还有一点FastDFS的tracker和storage启动脚本我习惯注册成systemd服务而不是裸跑/usr/bin/fdfs_trackerd这样开机自启、异常退出自动拉起都能覆盖。service文件很简单核心就是ExecStart指向对应命令加配置文件路径。以tracker为例[Unit] DescriptionFastDFS Tracker Afternetwork.target [Service] Typeforking ExecStart/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start ExecStop/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf stop Restarton-failure [Install] WantedBymulti-user.targetstorage服务同理。有了systemd托管机器重启后服务自动恢复少了大半夜爬起来敲命令的尴尬。8. 性能优化建议与后续扩展方向缩略图链路跑通后我陆续做了几项优化每一项都有效果供你参考。首先是Nginx的sendfile和open_file_cache。sendfile默认是开启的能把文件从磁盘发送到网卡的过程完全挪进内核态完成减少一次用户态拷贝sendfile on; open_file_cache max1000 inactive20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;open_file_cache能让Nginx对频繁访问的文件句柄做缓存避免每次请求都重新open磁盘文件。这两项配合起来单机处理静态图片的QPS能提升不少。其次是缩略图URL的规范。我统一把小图、中图、大图的后缀定义成_200x200、_600x400、_1080x720前端所有代码都用这套规范拼接URL。后端如果尺寸有调整只需要改Nginx配置里的location规则不需要动任何业务代码这让迭代非常轻量。最后是FastDFS的group扩容。一个group的storage磁盘快满的时候新加一个group是最省事的扩展手段。上传时FastDFS会自动选择空闲的group文件ID会变成group2/M00/xxxx对现有数据完全无感。我的做法是提前把不常访问的老图片做了迁移或归档新上线的图片全部落到新group这样不用动storage节点本身也能腾出空间。回到最开始的问题——把自己从文件存哪、缩略图怎么做这两个问题里解放出来之后业务侧的推进速度快了很多。FastDFS加Nginx这套组合我用了很长一段时间稳定性和性能都让我满意。如果你刚好也在搭建图片服务按照上面的步骤走一遍再根据自己实际流量调整一下缓存参数基本不会出大问题。遇到具体卡点欢迎来交流。