Java 项目实战:交友项目 - FastDFS 工作原理

概述

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(存储服务器)。上传下载的交互过程如下:
Storage server Tracker server 客户端 (Java) Storage server Tracker server 客户端 (Java) #mermaid-svg-3zG8dWbFqVCkOvRM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3zG8dWbFqVCkOvRM .error-icon{fill:#552222;}#mermaid-svg-3zG8dWbFqVCkOvRM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3zG8dWbFqVCkOvRM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3zG8dWbFqVCkOvRM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3zG8dWbFqVCkOvRM .marker.cross{stroke:#333333;}#mermaid-svg-3zG8dWbFqVCkOvRM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3zG8dWbFqVCkOvRM p{margin:0;}#mermaid-svg-3zG8dWbFqVCkOvRM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3zG8dWbFqVCkOvRM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3zG8dWbFqVCkOvRM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3zG8dWbFqVCkOvRM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3zG8dWbFqVCkOvRM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3zG8dWbFqVCkOvRM .sequenceNumber{fill:white;}#mermaid-svg-3zG8dWbFqVCkOvRM #sequencenumber{fill:#333;}#mermaid-svg-3zG8dWbFqVCkOvRM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3zG8dWbFqVCkOvRM .messageText{fill:#333;stroke:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3zG8dWbFqVCkOvRM .labelText,#mermaid-svg-3zG8dWbFqVCkOvRM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .loopText,#mermaid-svg-3zG8dWbFqVCkOvRM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3zG8dWbFqVCkOvRM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3zG8dWbFqVCkOvRM .noteText,#mermaid-svg-3zG8dWbFqVCkOvRM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3zG8dWbFqVCkOvRM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3zG8dWbFqVCkOvRM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3zG8dWbFqVCkOvRM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3zG8dWbFqVCkOvRM .actorPopupMenu{position:absolute;}#mermaid-svg-3zG8dWbFqVCkOvRM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-3zG8dWbFqVCkOvRM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3zG8dWbFqVCkOvRM .actor-man circle,#mermaid-svg-3zG8dWbFqVCkOvRM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3zG8dWbFqVCkOvRM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段零:Storage 定期向 Tracker 上报状态 (磁盘容量、剩余空间、同步进度) 客户端保存完整访问路径: Nginx 地址 + group + 路径 + 文件名 ① 上传请求(询问可用 Storage) ② 返回一台 Storage 的 IP + 端口 ③ 直接上传文件 ④ 返回 file_id(组名 + 路径 + 文件名) ⑤ 后台异步同步到同组其他节点 ⑥ 下载请求(按 file_id 查询) ⑦ 返回可用的 Storage 信息 ⑧ 请求文件 ⑨ 返回文件内容

两个值得注意的点:

  • Tracker 全程不碰文件内容,它只做「查表 + 策略计算」,返回的是目标 Storage 的地址。文件流直接走客户端到 Storage。
  • 下载在实际业务里其实很少走 Java 代码。因为每台 Storage 上都绑了 Nginx,客户端早就拿到了完整 URL,浏览器/APP 直接发 HTTP 请求让 Nginx 转发即可,比绕一圈 Java 后端再读文件流省事得多。
tree 复制代码
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.jpg

Storage 间的同步机制

这是理解 FastDFS 运维行为的关键:同组内所有 Storage 节点保存的内容完全一样,互为备份,没有主从概念。任何一个节点都可以独立承接读写,所以同步不可能是「先复制完再返回」,那等于把写延迟放大 N 倍。

FastDFS 的做法是异步后台同步:
#mermaid-svg-EKDdbALHG4FXBcgh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EKDdbALHG4FXBcgh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EKDdbALHG4FXBcgh .error-icon{fill:#552222;}#mermaid-svg-EKDdbALHG4FXBcgh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EKDdbALHG4FXBcgh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EKDdbALHG4FXBcgh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EKDdbALHG4FXBcgh .marker.cross{stroke:#333333;}#mermaid-svg-EKDdbALHG4FXBcgh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EKDdbALHG4FXBcgh p{margin:0;}#mermaid-svg-EKDdbALHG4FXBcgh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EKDdbALHG4FXBcgh .cluster-label text{fill:#333;}#mermaid-svg-EKDdbALHG4FXBcgh .cluster-label span{color:#333;}#mermaid-svg-EKDdbALHG4FXBcgh .cluster-label span p{background-color:transparent;}#mermaid-svg-EKDdbALHG4FXBcgh .label text,#mermaid-svg-EKDdbALHG4FXBcgh span{fill:#333;color:#333;}#mermaid-svg-EKDdbALHG4FXBcgh .node rect,#mermaid-svg-EKDdbALHG4FXBcgh .node circle,#mermaid-svg-EKDdbALHG4FXBcgh .node ellipse,#mermaid-svg-EKDdbALHG4FXBcgh .node polygon,#mermaid-svg-EKDdbALHG4FXBcgh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EKDdbALHG4FXBcgh .rough-node .label text,#mermaid-svg-EKDdbALHG4FXBcgh .node .label text,#mermaid-svg-EKDdbALHG4FXBcgh .image-shape .label,#mermaid-svg-EKDdbALHG4FXBcgh .icon-shape .label{text-anchor:middle;}#mermaid-svg-EKDdbALHG4FXBcgh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EKDdbALHG4FXBcgh .rough-node .label,#mermaid-svg-EKDdbALHG4FXBcgh .node .label,#mermaid-svg-EKDdbALHG4FXBcgh .image-shape .label,#mermaid-svg-EKDdbALHG4FXBcgh .icon-shape .label{text-align:center;}#mermaid-svg-EKDdbALHG4FXBcgh .node.clickable{cursor:pointer;}#mermaid-svg-EKDdbALHG4FXBcgh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EKDdbALHG4FXBcgh .arrowheadPath{fill:#333333;}#mermaid-svg-EKDdbALHG4FXBcgh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EKDdbALHG4FXBcgh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EKDdbALHG4FXBcgh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EKDdbALHG4FXBcgh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EKDdbALHG4FXBcgh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EKDdbALHG4FXBcgh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EKDdbALHG4FXBcgh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EKDdbALHG4FXBcgh .cluster text{fill:#333;}#mermaid-svg-EKDdbALHG4FXBcgh .cluster span{color:#333;}#mermaid-svg-EKDdbALHG4FXBcgh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EKDdbALHG4FXBcgh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EKDdbALHG4FXBcgh rect.text{fill:none;stroke-width:0;}#mermaid-svg-EKDdbALHG4FXBcgh .icon-shape,#mermaid-svg-EKDdbALHG4FXBcgh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EKDdbALHG4FXBcgh .icon-shape p,#mermaid-svg-EKDdbALHG4FXBcgh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EKDdbALHG4FXBcgh .icon-shape .label rect,#mermaid-svg-EKDdbALHG4FXBcgh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EKDdbALHG4FXBcgh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EKDdbALHG4FXBcgh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EKDdbALHG4FXBcgh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后台线程
客户端上传到 Storage A
A 写入本地磁盘成功
立即向客户端返回 file_id
客户端业务完成
A 按同步时间戳

把新文件推给同组 B、C
B、C 落盘并记录同步进度 binlog

上传请求在 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 机制让容量伸缩变成了纯运维操作:
#mermaid-svg-UmBR2gNYPqZ1NBJT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UmBR2gNYPqZ1NBJT .error-icon{fill:#552222;}#mermaid-svg-UmBR2gNYPqZ1NBJT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UmBR2gNYPqZ1NBJT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .marker.cross{stroke:#333333;}#mermaid-svg-UmBR2gNYPqZ1NBJT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UmBR2gNYPqZ1NBJT p{margin:0;}#mermaid-svg-UmBR2gNYPqZ1NBJT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster-label text{fill:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster-label span{color:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster-label span p{background-color:transparent;}#mermaid-svg-UmBR2gNYPqZ1NBJT .label text,#mermaid-svg-UmBR2gNYPqZ1NBJT span{fill:#333;color:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .node rect,#mermaid-svg-UmBR2gNYPqZ1NBJT .node circle,#mermaid-svg-UmBR2gNYPqZ1NBJT .node ellipse,#mermaid-svg-UmBR2gNYPqZ1NBJT .node polygon,#mermaid-svg-UmBR2gNYPqZ1NBJT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .rough-node .label text,#mermaid-svg-UmBR2gNYPqZ1NBJT .node .label text,#mermaid-svg-UmBR2gNYPqZ1NBJT .image-shape .label,#mermaid-svg-UmBR2gNYPqZ1NBJT .icon-shape .label{text-anchor:middle;}#mermaid-svg-UmBR2gNYPqZ1NBJT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .rough-node .label,#mermaid-svg-UmBR2gNYPqZ1NBJT .node .label,#mermaid-svg-UmBR2gNYPqZ1NBJT .image-shape .label,#mermaid-svg-UmBR2gNYPqZ1NBJT .icon-shape .label{text-align:center;}#mermaid-svg-UmBR2gNYPqZ1NBJT .node.clickable{cursor:pointer;}#mermaid-svg-UmBR2gNYPqZ1NBJT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .arrowheadPath{fill:#333333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UmBR2gNYPqZ1NBJT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UmBR2gNYPqZ1NBJT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UmBR2gNYPqZ1NBJT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster text{fill:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT .cluster span{color:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UmBR2gNYPqZ1NBJT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UmBR2gNYPqZ1NBJT rect.text{fill:none;stroke-width:0;}#mermaid-svg-UmBR2gNYPqZ1NBJT .icon-shape,#mermaid-svg-UmBR2gNYPqZ1NBJT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UmBR2gNYPqZ1NBJT .icon-shape p,#mermaid-svg-UmBR2gNYPqZ1NBJT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UmBR2gNYPqZ1NBJT .icon-shape .label rect,#mermaid-svg-UmBR2gNYPqZ1NBJT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UmBR2gNYPqZ1NBJT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UmBR2gNYPqZ1NBJT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UmBR2gNYPqZ1NBJT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 扩容两种姿势
自动同步历史数据
组间文件不同
同组加机器

group1 新增 Storage D
容量不变

读能力与冗余度提升
加新组

新增 group2 一台或多台
容量真正扩大
Storage 下线

同组还有副本 / 文件还在别的组
数据不丢

安全性来自组内冗余

  • 同组加机器 :新节点加入后,老节点会通过同步机制把历史数据逐步推给它,直到补齐。适合「读压力大、想多几份副本」的场景,但总容量没有变化,同组是镜像关系。
  • 加新组:这是唯一的扩容方式。组与组之间保存的文件不同,新增 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:

yaml 复制代码
# 分布式文件系统FDFS配置
fdfs:
  so-timeout: 1500          # 读取超时(毫秒)
  connect-timeout: 600      # 连接超时(毫秒)
  # 缩略图生成参数
  thumb-image:
    width: 150
    height: 150
  # TrackerList参数,支持多个
  tracker-list: 192.168.136.160:22122
  web-server-url: http://192.168.136.160:8888/

这段配置印证了前文的两个原理:

  1. 客户端只需要知道 Tracker 。tracker-list 指向 Tracker 的 22122 端口,注释明确写了「支持多个」,对应 Tracker 集群靠冗余高可用,客户端全列上、任选一台。客户端从头到尾不需要知道任何 Storage 的地址,每次上传下载前问 Tracker 要。
  2. web-server-url 就是 URL 拼接的前缀。它指向 Storage 端 Nginx 的 8888 端口。客户端上传拿到 file_id 后,拼出完整访问地址:
text 复制代码
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 下载只在少数后端处理场景使用

官方文档

总结

  • 上传下载的调度核心在 Tracker,但文件流永远直连 Storage;Tracker 只维护状态表加策略,不碰数据。
  • 同组互备 + 异步同步是 FastDFS 的性能取舍,代价是「同步延迟窗口」,刚上传就 404 要从粘性读取、Nginx 重试、业务容忍三个方向里选一个来治。
  • Tracker 高可用靠冗余不靠选举,这与 ZooKeeper 类系统是两种截然不同的集群哲学。
  • 容量以组为单位:同组加机器只加冗余不加容量,扩容量必须加新组;数据安全性来自组内副本。
  • 项目工程里 tracker-list 与 web-server-url 两个配置项,分别对应「客户端只认 Tracker」和「file_id 拼 URL 走 Nginx」两个原理落点。

FastDFS 入门案例会把 fastdfs-client 依赖引入工程,跑通第一个上传文件的测试用例。

相关推荐
Wang's Blog3 小时前
Java 项目实战:交友项目 - 保存访客记录
java·开发语言·交友
liangbo73 小时前
Spring系列01:IoC 与 DI 到底解决了什么
java·spring
江湖中的阿龙3 小时前
Oracle 紧急安全警报:Java 9.1 分漏洞 CVE-2026-91403,撤销对 eMudhra 风险证书的信任
java·安全·oracle
挖掘狂人3 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员
codigger3 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员·后端开发
合合技术团队3 小时前
企业级Agent服务平台选型指南:从需求匹配到场景落地,文档解析工具怎么选?
大数据·人工智能·agent·多模态文本智能技术·企业级agent服务平台
Java后端的Ai之路3 小时前
Java并发编程07-等待通知机制优化循环等待
java·数据库·python
AllData公司负责人3 小时前
AllData 可定义数据中台完成信创适配|银河麒麟 + 海光X86 CPU / 华为鲲鹏ARM CPU+ OceanBase / 人大金仓国产化环境部署落地
大数据·arm开发·数据库·人工智能·oceanbase·kingbase
Raas1004 小时前
AI网关如何接入?MAI Gateway部署实操教程
java·开发语言·人工智能·gateway·ai网关·mai gateway·企业级产品