概述
FastDFS 的引入与内部结构部分已经交代了 Tracker / Storage / Group 的架构和文件内部命名规则,本文往运维方向深挖:文件在集群里到底怎么流转、节点之间怎么同步、怎么扩容缩容,以及「刚传完的文件查不到」这个经典坑的来龙去脉。
纲要
- 全景流程:上传与下载的完整交互(Client / Tracker / Storage 三端协作)
- Storage 间的同步机制
- 同组互为备份,同步是异步的
- 同步延迟窗口与「刚上传就 404」问题
- 解决方向:读 A 写 A、Nginx 屏蔽、业务容忍
- Tracker 的调度策略
- 如何挑选 Storage(剩余空间、轮询、IP 排序)
- Tracker 集群:彼此不通信、Client 任选、靠冗余保证高可用
- 扩容与缩容
- 同组加机器(自动同步补齐)
- 加新组(真正扩容量)
- Storage 下线(数据安全性来源)
- 文件一致性:file_id 不变,一次写入多次读取
- 断点续传与大文件:appender 文件的能力边界
- 工程衔接:客户端如何找到 Tracker、file_id 如何变成可访问 URL
- 常见误解澄清表
FastDFS 里有三个角色:客户端(我们的 Java 代码)、Tracker server(调度/追踪服务器)、Storage server(存储服务器)。上传下载的交互过程如下:
两个值得注意的点:
- Tracker 全程不碰文件内容,它只做「查表 + 策略计算」,返回的是目标 Storage 的地址。文件流直接走客户端到 Storage。
- 下载在实际业务里其实很少走 Java 代码。因为每台 Storage 上都绑了 Nginx,客户端早就拿到了完整 URL,浏览器/APP 直接发 HTTP 请求让 Nginx 转发即可,比绕一圈 Java 后端再读文件流省事得多。
FastDFS 一次上传后客户端掌握的信息 ├── Tracker 返回:Storage 的 IP + 端口(临时,用完即弃) ├── Storage 返回:file_id │ ├── 组名(group1) │ ├── 虚拟磁盘路径(M00 → 对应一块挂载磁盘) │ ├── 两级目录(00/00) │ └── 文件名(含 Base64 编码的元信息:大小、时间戳、CRC32) └── 客户端落库的完整 URL └── http://192.168.136.160:8888/group1/M00/00/00/xxx.jpgStorage 间的同步机制
这是理解 FastDFS 运维行为的关键:同组内所有 Storage 节点保存的内容完全一样,互为备份,没有主从概念。任何一个节点都可以独立承接读写,所以同步不可能是「先复制完再返回」,那等于把写延迟放大 N 倍。
FastDFS 的做法是异步后台同步:
上传请求在 A 节点落盘成功的那一刻就返回了,同组其他节点的副本是 A 节点的后台线程按自己的同步 binlog 逐个补齐的。这在设计上是合理取舍,文件类业务对「毫秒级一致性」几乎无感,但它带来一个绕不开的窗口期问题。
同步延迟窗口:刚上传就 404
复现一下这个经典故障:文件刚传到同组的 Storage A,客户端立刻拿着 URL 去访问。FastDFS 取文件时会调度同组内的某一台 Storage,这次选中的偏偏是还没同步到该文件的 Storage B,B 上没有这个文件,Nginx 转发后返回 404。过几秒再访问,同步补齐了,又好了。
排查时可以先确认是不是这个原因:登录 B 节点看它对应磁盘下的同步标记文件(data目录下的 binlog 及.mark文件),对比 A 节点的同步进度;如果 B 的同步进度落后于文件名里的时间戳,就是典型的同步延迟窗口。
工程上有三个解决方向:
| 方向 | 做法 | 适用场景 |
|---|---|---|
| 读 A 写 A 的粘性 | 上传后立刻需要回显/处理的场景,直接对 A 节点发起读取(客户端记下上传时 Tracker 返回的那台 Storage 地址) | 上传后马上要生成缩略图、审核等强一致操作 |
| Nginx 层屏蔽 | 在反代层做健康检查与重试:对 B 返回 404 时 fallback 重试到 A;或 Storage 端 Nginx 配置proxy_next_upstream | 通用方案,对业务代码透明 |
| 业务层容忍 | 前端上传成功后延迟展示,或失败自动重试一次 | 对实时性要求低的场景(如小视频列表) |
交友这类业务天然适合第三种:用户上传头像/视频后,别的用户刷新列表才看到,几秒的同步窗口完全无感。
Tracker 的调度策略
如何选 Storage
客户端发上传请求时,Tracker 要回答两个问题:选哪个组、组内选哪台。常见策略:
| 决策点 | 常见策略 | 说明 |
|---|---|---|
| 选组 | tracker.conf的store_lookup:0 轮询 / 1 指定组 / 2 负载均衡 | 0 时在可用组间轮询;1 配合store_group固定写某个组;2 按剩余空间挑最宽裕的组 |
| 组内选节点 | 剩余空间优先 / 轮询 | 负载均衡模式下内置按剩余存储空间排序,空间大的优先 |
| 下载/查询选节点 | 轮询( 可配置按源 IP 就近) | 下载时可以在同组内任选一台,这也是同步延迟问题的入口 |
依据来自 Storage 周期性上报的状态:磁盘容量、剩余空间、文件同步状态。Tracker 的内存里维护着这份「集群状态表」,所以常说的「负载均衡和调度」,本质是一张会过期的状态表加上几条选择策略。
Tracker 集群:靠冗余,不靠选举
Tracker 集群的设计很「反直觉」:多台 Tracker 彼此不通信、不共享状态,客户端配置里全列上,任选一台用。每台 Tracker 各自收集 Storage 的心跳,各管各的表。
这跟 ZooKeeper 那类系统完全不同,ZooKeeper 靠多数派选举保证全局一致,Tracker 没有任何选举逻辑,高可用完全靠冗余:挂掉一台,客户端探测失败后换下一台,集群照常工作。代价是各台 Tracker 看到的 Storage 状态可能短暂不一致,但对「挑一台能用的 Storage」这种粗粒度决策来说无关紧要。
生产部署一般 2~3 台 Tracker,客户端的tracker-list全配上。
扩容与缩容
Group 机制让容量伸缩变成了纯运维操作:
- 同组加机器:新节点加入后,老节点会通过同步机制把历史数据逐步推给它,直到补齐。适合「读压力大、想多几份副本」的场景,但总容量没有变化,同组是镜像关系。
- 加新组:这是唯一的扩容方式。组与组之间保存的文件不同,新增 group 等于新增一份独立容量,Tracker 会自动把新上传的文件往新组调度。
- Storage 下线:同组下线一台,文件在同组其他节点上还有完整副本;如果下线的是整组(缩容),该组的数据就没备份了,需要先把数据迁移到别的组再操作。数据安全性完全来自组内冗余,这也是为什么生产上同组至少两台。
一个容易踩的规划坑:扩容加新组后,新组通常是新买的机器,磁盘可能更大。FastDFS 组内要求同组机器配置尽量一致(同步和调度都以组为单位),所以容量规划时按「组」为最小扩容单元来预算,而不是按单台机器。
文件一致性:file_id 不变
FastDFS 是不可变存储(immutable storage):文件写入后内容不可修改,file_id 终生不变。想「更新」一个文件,唯一办法是删掉旧的、传一份新的,拿到新的 file_id 再更新业务表,这也是所有内容类存储(对象存储等)的通用设计。
对交友这类业务反而是契合的:头像、照片、小视频都是「一次写入、多次读取」,天然没有原地改文件内容的需求。需要「改」的场景(比如重新剪视频),走的也是「新文件 + 换引用」的路子。
带来的业务约束:
- 不能像操作本地文件那样
RandomAccessFile原地改几个字节; - 想覆盖上传,只能删除后重传,file_id 会变,业务表里存 file_id 的字段必须跟着更新;
- 删除也是按 file_id 整文件删除,没有局部修改。
断点续传与大文件
FastDFS 支持 appender 文件模式:上传时声明为 appender,之后可以对它做追加写入(modify_file/append_file),配合分块上传就能实现断点续传,客户端记录已传分片偏移量,重传时从断点继续 append。但能力边界也要清楚:appender 文件在 Storage 端会占用额外的元信息管理,且不能直接转成普通文件的合并语义,大文件(GB 级)场景下更常见的做法还是客户端分片 + 业务层合并,FastDFS 只当最终存储。交友类应用的小视频通常在几十 MB 以内,用不到这套机制。
工程衔接:客户端配置
以上原理落到工程里,就是mf-app-server的一段 yml。实读工程文件mf-app-server/src/main/resources/application.yml:
# 分布式文件系统FDFS配置fdfs:so-timeout:1500# 读取超时(毫秒)connect-timeout:600# 连接超时(毫秒)# 缩略图生成参数thumb-image:width:150height:150# TrackerList参数,支持多个tracker-list:192.168.136.160:22122web-server-url:http://192.168.136.160:8888/这段配置印证了前文的两个原理:
- 客户端只需要知道 Tracker。
tracker-list指向 Tracker 的 22122 端口,注释明确写了「支持多个」,对应 Tracker 集群靠冗余高可用,客户端全列上、任选一台。客户端从头到尾不需要知道任何 Storage 的地址,每次上传下载前问 Tracker 要。 web-server-url就是 URL 拼接的前缀。它指向 Storage 端 Nginx 的 8888 端口。客户端上传拿到 file_id 后,拼出完整访问地址:
http://192.168.136.160:8888/ + group1/M00/00/00/CgDOC1.jpg └── web-server-url(Nginx) └── file_id(组名/虚拟磁盘/两级目录/文件名)对应代码里就是FdfsWebServer注入的getWebServerUrl()加上StorePath的getFullPath()拼接,落库到 MySQL 里的正是这个完整 URL。用户 APP 拿到 URL 后直接请求 Storage 的 Nginx,Nginx 转发到本机 Storage 的文件目录,整个过程不经过 Java 后端。
常见误解澄清
| 误解 | 实际情况 |
|---|---|
| 同组 Storage 之间是同步阻塞复制,传完才算成功 | 同步是异步的:上传节点落盘即返回,副本由后台线程按同步 binlog 逐步补齐 |
| Tracker 挂了一台,集群就不可用 | 多台 Tracker 彼此不通信、无选举,客户端配置多个地址任选,靠冗余保证高可用 |
| 文件传上去之后可以原地修改内容 | FastDFS 是不可变存储,file_id 不变、内容不可改;只能删旧传新并更新业务表引用 |
| 同组里加越多机器,容量越大 | 同组是镜像冗余关系,容量以「组」为最小单元;扩容量必须加新组 |
| 下载必须走 Java 客户端 API | 常规做法是拼 URL 直接请求 Storage 端 Nginx,Java API 下载只在少数后端处理场景使用 |
官方文档
- FastDFS GitHub 仓库(happyfish100/fastdfs)
- fastdfs-client(tobato 版 Java 客户端,项目所用 1.26.7)
- Nginx 官方文档(proxy_next_upstream 等指令)
总结
- 上传下载的调度核心在 Tracker,但文件流永远直连 Storage;Tracker 只维护状态表加策略,不碰数据。
- 同组互备 + 异步同步是 FastDFS 的性能取舍,代价是「同步延迟窗口」,刚上传就 404 要从粘性读取、Nginx 重试、业务容忍三个方向里选一个来治。
- Tracker 高可用靠冗余不靠选举,这与 ZooKeeper 类系统是两种截然不同的集群哲学。
- 容量以组为单位:同组加机器只加冗余不加容量,扩容量必须加新组;数据安全性来自组内副本。
- 项目工程里
tracker-list与web-server-url两个配置项,分别对应「客户端只认 Tracker」和「file_id 拼 URL 走 Nginx」两个原理落点。
FastDFS 入门案例会把fastdfs-client依赖引入工程,跑通第一个上传文件的测试用例。