一、FastDFS 集群

注意:本部分与一、的 GlusterFS 是完全不同的产品。 FastDFS 不是 GlusterFS 的组件,也不能挂载成本地目录。请勿混淆二者的概念和命令。

1.1 FastDFS 定位与设计取舍

FastDFS 是由国内开发者余庆(happyfish100)用 C 语言编写的开源轻量级分布式文件存储系统,专为海量中小文件(图片、文档、小视频、附件)的存储与访问而设计。

核心定位:

  • 专为中小文件优化:官方推荐的文件大小范围是 4KB ~ 500MB。对超大文件(GB 以上)和极小文件(几百字节)都不是最优选择。
  • 一次写入、多次读取(WORM):FastDFS 的文件不支持随机修改(只能上传、下载、删除,以及 appender 文件的追加)。这与图片/附件类业务的访问模式完美匹配。
  • 轻量级:只有 Tracker 和 Storage 两种角色,两个二进制程序、三个配置文件,部署与运维成本极低。没有复杂的元数据集群、没有 Paxos/Raft。
  • 不做通用文件系统:刻意不实现 POSIX 语义,把复杂度留给应用层。

与通用分布式文件系统(GlusterFS/CephFS/HDFS)的关键差异:

差异点 通用分布式文件系统(如 GlusterFS) FastDFS
访问方式 可 mount 成本地目录,任何程序透明使用 不能 mount,只能通过客户端 API(C/Java/PHP/Python)或 HTTP 访问
POSIX 语义 支持(open/read/write/seek/lock/chmod) 不支持。没有 open/seek/write 语义
目录树 有完整的层级目录树,可 ls、mkdir、mv 没有用户可见的目录树。文件路径由系统自动生成(file_id),应用只能保存这个 id
文件名 用户指定 系统生成(file_id 编码了位置信息),原始文件名需应用自己存到数据库
随机改写 支持 不支持(普通文件不可改;appender 文件可追加/截断)
元数据 分布式或集中式元数据管理 几乎没有元数据服务,位置信息编码在 file_id 里
复杂度 中到高 低
适合的文件大小 中大文件 中小文件

这个取舍的意义:正因为放弃了 POSIX 和目录树,FastDFS 才能做到"存文件只需一次网络往返、无需查元数据",从而在海量小文件场景下比通用文件系统快得多、也简单得多。代价是应用必须改造:上传后拿到 file_id,自己把 原始文件名 -> file_id 的映射存进数据库(MySQL/Redis)。

典型适用场景:

  • 电商/社交网站的商品图片、头像、相册
  • 用户上传的附件、合同、发票扫描件、报表
  • 短视频、音频片段
  • 一次生成多次访问的静态资源

不适合的场景:需要挂载成目录给遗留程序用、需要随机改写(数据库文件、VM 镜像)、超大文件(几十 GB 的备份包)、需要 S3 兼容 API。

1.2 架构:Tracker 与 Storage 两种角色

                   +----------------+         +----------------+
                   |   Tracker 1    |  <-->   |   Tracker 2    |    (彼此不通信,无状态,客户端轮询)
                   |  (22122/tcp)   |         |  (22122/tcp)   |
                   +----------------+         +----------------+
                        ^      ^                    ^
              1.问"存哪"|      |心跳/汇报状态       |
                        |      v                    v
      +-----------------+   +-------------------------------------------+
      |                     |             Storage 集群                 |
      |                     |  group1                    group2        |
      |                     |  +----------+ +----------+  +----------+  |
   +--v---+   2.直连上传    |  |storage1-1| |storage1-2|  |storage2-1|  |
   |客户端| --------------->|  |23000/tcp | |23000/tcp |  |23000/tcp |  |
   +------+                 |  +----------+ +----------+  +----------+  |
      ^                     |       ^___同组互相同步___^                 |
      |  3.返回 file_id     |    (同组内容完全相同,互为备份)           |
      +---------------------+-------------------------------------------+

1.2.1 Tracker(调度服务器)的职责

职责 说明
集群状态管理 接收所有 Storage 的心跳与状态汇报,维护"哪些 group、哪些 storage、各自剩余空间、是否 ACTIVE"的内存表
上传调度 客户端上传前先问 Tracker:"我该把文件传给谁?" Tracker 按 store_lookup 策略选出一个 group 和一个 storage 返回
下载调度 客户端下载前问 Tracker:"这个 file_id 我该找谁下载?" Tracker 按 download_server 策略在该 group 内选一个 storage
同组同步协调 告知新加入的 Storage 应该从哪个源节点做全量同步

Tracker 是无状态的、可水平扩展的——这是 FastDFS 架构上最重要的一点:

  • Tracker 不存储任何文件,也不存储文件索引。它的所有信息都是 Storage 主动汇报上来的、放在内存里的运行时状态。
  • 多个 Tracker 之间彼此不通信、不选主、不同步数据。每个 Tracker 都独立地接收所有 Storage 的汇报,因此各自都拥有完整的集群视图。
  • 客户端配置多个 tracker_server,随机/轮询选一个提问。任一 Tracker 宕机,客户端换一个即可,无需切换 VIP、无需选举。
  • 因此 Tracker 的高可用非常廉价:部署 2 个(或以上)就完成了,不需要 keepalived。
  • Tracker 挂了会怎样?已知 file_id 的下载可以不经 Tracker 直接访问 Storage(HTTP 方式),但新上传会失败(找不到 storage)。

重要澄清:源文档提到"192.168.1.221 被选为 leader 节点",这里的 leader 是指 Tracker 集群中被推选出的那个"负责协调 Storage 同步与统计的 leader tracker"(由各 Storage 汇报后按规则选出,通常是 IP/端口最小或最早的那个),不是"主 Tracker",也不涉及数据主从。所有 Tracker 都同等地对外服务,leader 只多承担同组同步的源节点分配工作。因此不要误解为"FastDFS 有主备 Tracker"。

1.2.2 Storage(存储服务器)的职责

职责 说明
实际存储文件 把上传的文件落盘到 store_path 下的两级目录中
生成 file_id 上传成功后生成并返回 file_id
提供下载 通过 FastDFS 协议或 HTTP(配 Nginx)提供文件读取
同组同步 主动把本机新增/删除的文件同步给同组的其它 Storage
心跳汇报 定期向所有 Tracker 汇报自身状态、磁盘空间、统计信息

1.2.3 group(卷)的概念

group(也叫 volume)是 FastDFS 组织 Storage 的核心概念:

group1: storage1-1 (100GB) + storage1-2 (100GB) + storage1-3 (80GB)
        => 三台机器内容完全相同(互为备份)
        => group1 的可用容量 = min(100, 100, 80) = 80GB  ← 取最小值!

group2: storage2-1 (500GB) + storage2-2 (500GB)
        => group2 的可用容量 = 500GB

集群总容量 = group1 + group2 = 80 + 500 = 580GB

关键规则:

  1. 同组内所有 Storage 存储的文件完全相同,互为备份。组内节点数即为副本数。
  2. 组的容量取决于组内容量最小的那个节点(木桶效应)。因此同组机器的磁盘配置必须一致,否则浪费空间。
  3. 组内增加节点 = 提升冗余度和读性能,不增加容量。
  4. 组是扩容的单位:要增加集群总容量,必须新增一个 group。
  5. 不同 group 之间完全独立、互不同步,一个 group 故障不影响其它 group。
  6. 一个 group 内 Storage 数量建议 2~3 台(副本数),不宜过多(同步开销随节点数增长)。
  7. file_id 的第一段就是 group 名,因此文件一旦上传,它属于哪个 group 就固定了。

1.2.4 同步机制:binlog 与主动同步

FastDFS 同组内的同步是基于 binlog 的、异步的、点对点的最终一致同步。

工作过程:

  1. 客户端把文件上传到 group1 中的 storage1-1(源服务器)。
  2. storage1-1 落盘成功后立即返回 file_id 给客户端(不等同步完成,因此是异步的)。
  3. storage1-1 把这次操作("新增了文件 xxx")记录到自己的 binlog 文件中:$base_path/data/sync/binlog.000。
  4. group1 内的每一个其它 Storage(storage1-2、storage1-3)都各自启动一个同步线程,主动连接 storage1-1,读取它的 binlog,把自己缺的文件拉过来。这是"拉模式"(对每个源服务器一个线程)。
  5. 每个 Storage 用 mark 文件记录"我从某个源服务器同步到 binlog 的哪个位置了":$base_path/data/sync/{源IP}_{端口}.mark。
  6. 断点续传:重启或网络恢复后,从 mark 文件记录的偏移继续同步,不会重复或漏掉。

相关文件:

# binlog 与 mark 文件所在目录
ls -l $base_path/data/sync/
# 例如 /home/dfs/data/sync/
#   binlog.000                     本机的操作日志
#   binlog.index                   当前 binlog 序号
#   192.168.1.220_23000.mark       从 220 同步的进度标记

binlog 内容格式(每行一条):

1553836564 C M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
时间戳      操作类型  文件相对路径

操作类型:C = create(源服务器上传)、c = create(同步来的)、D = delete、d = 同步删除、A = append、a = 同步 append、T = truncate、U = 更新。

mark 文件内容:

binlog_index=0
binlog_offset=1234567
need_sync_old=1
sync_old_done=1
until_timestamp=1553836564
scan_row_count=8900
sync_row_count=8900
字段 含义
binlog_index 当前同步到第几个 binlog 文件
binlog_offset 同步到该文件的字节偏移,断点续传的关键
need_sync_old 是否需要同步历史文件(新节点加入时为 1)
sync_old_done 历史文件是否已同步完成
until_timestamp 已同步到的时间点
scan_row_count / sync_row_count 已扫描/已同步的记录数

同步的重要特性与影响:

  • 异步 = 存在同步延迟。上传成功后立即用另一个节点的 IP 去下载,可能 404。这是需要 fastdfs-nginx-module 的根本原因(详见 3.7)。
  • 时间同步至关重要:同步进度是以时间戳为依据的,节点间时间不一致会导致同步逻辑混乱甚至数据丢失。必须部署 NTP/chrony。
  • 新节点加入时的全量同步:新 Storage 加入已有 group 后,Tracker 会指定一个源节点,新节点从它做全量同步(状态显示为 SYNCING),完成后才变 ACTIVE。
  • 可通过 sync_start_time/sync_end_time 限定同步的时间窗(如只在夜间同步),用 sync_interval 控制同步节奏。

1.2.5 trunk 文件与合并存储

问题:如果单个文件只有几 KB,而文件系统的块大小是 4KB,那么大量极小文件会造成:

  • 严重的空间浪费(每个文件至少占一个块);
  • inode 消耗巨大(可能磁盘还有空间但 inode 用尽);
  • 元数据操作(创建/删除文件)开销占比过高。

解决方案:合并存储(trunk file)。把多个小文件合并写入到一个大文件(trunk 文件,默认 64MB)内部,file_id 中记录该小文件在 trunk 内的偏移量与长度。这样:

  • 一个 trunk 文件只占一个 inode,承载成千上万个小文件;
  • 空间按实际字节分配,几乎无浪费;
  • 减少文件系统的元数据压力。

配置(在 storage.conf 中):

# 是否启用合并存储
use_trunk_file = true
# trunk 文件中最小分配单元(字节),默认 256
slot_min_size = 256
# 小于此值的文件才使用合并存储(不能超过 trunk_file_size 的 1/2),默认 16KB
slot_max_size = 16KB
# trunk 文件大小,建议 64MB
trunk_file_size = 64MB
# 是否预先创建 trunk 文件
trunk_create_file_advance = false
trunk_create_file_time_base = 02:00
trunk_create_file_interval = 86400
# 预创建 trunk 文件的总大小阈值
trunk_create_file_space_threshold = 20G
# 启动时是否检查 trunk 空闲空间(建议 true)
trunk_init_check_occupying = false
trunk_init_reload_from_binlog = false
trunk_compress_binlog_min_interval = 0

在 tracker.conf 中还需配合:

# 是否使用 trunk 文件的服务器(由 tracker 指定一台 storage 作为 trunk server 统一分配空间)
use_trunk_file = true
slot_min_size = 256
slot_max_size = 16MB
trunk_file_size = 64MB
# 是否由 tracker 统一分配 trunk 空间(推荐 true)
store_server = 0

注意:合并存储会引入 trunk server 这个中心分配者,增加复杂度;且被删除小文件的空间回收(碎片整理)不够完善。只有在文件确实非常小(< 16KB)且数量极大(亿级)时才建议启用,一般场景保持默认 false 即可。

1.3 上传与下载的完整流程

1.3.1 上传流程

  客户端                    Tracker                  Storage(group1/storage1-1)
    |                          |                              |
    |--1. 请求上传(问存哪)--->|                              |
    |                          | 按 store_lookup 选 group     |
    |                          | 按 store_server 选 storage   |
    |<--2. 返回 storage 的 IP/端口 + store_path 索引 --------|(此步只与 tracker 交互)
    |                                                         |
    |--3. 直连 storage,发送文件内容 + 扩展名 ---------------->|
    |                                                         | 4. 选 store_path
    |                                                         | 5. 生成文件名(编码位置信息)
    |                                                         | 6. 落盘到两级目录
    |                                                         | 7. 写 binlog
    |<--8. 返回 file_id: group1/M00/00/00/wKgB3Fyo...  -------|
    |                                                         |
    | 9. 应用把 file_id 存入数据库                             |--10. 同组其它 storage
    |                                                         |      主动来拉取(异步)

关键点:

  1. 数据不经过 Tracker。Tracker 只回答"去找谁",实际文件流是客户端与 Storage 直连,因此 Tracker 不是带宽瓶颈。
  2. 返回 file_id 就算成功,此时同组其它节点还没有这个文件(异步同步)。
  3. 客户端必须保存 file_id,因为这是唯一能找回文件的凭据。原始文件名 FastDFS 不保存(需用 fdfs_upload_file 的 metadata 功能或自行入库)。

1.3.2 file_id 的组成结构逐段解析

group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
  |     |   |  |  |
  |     |   |  |  +-- 文件名(Base64 编码了源 IP、时间戳、文件大小、CRC32 等)
  |     |   |  +----- 二级目录(00 ~ FF,十六进制)
  |     |   +-------- 一级目录(00 ~ FF,十六进制)
  |     +------------ 虚拟磁盘路径(M00 对应 store_path0,M01 对应 store_path1 ...)
  +------------------ group 名(卷名),决定去哪个组找这个文件

逐段说明:

段 示例 含义
group 名 group1 文件所在的组。下载时据此确定该找哪一组的 Storage。这也是为什么改了 group_name 后老文件就访问不到了
虚拟磁盘路径 M00 对应 storage.conf 中的 store_path0;M01 对应 store_path1,依此类推。这是一层抽象:物理路径可以变,file_id 不变
两级目录 00/00 Storage 在 store_path 下预先创建了 256 × 256 = 65536 个目录(由 subdir_count_per_path=256 决定),用于把文件散列开,避免单目录文件数过多
文件名 wKgB3FyovhGAY8lJAAAD3C8hfQo2554045 由 Storage 生成,是 Base64 编码后的结果,内部编码了:源 Storage 的 IP 地址(4字节)+ 文件创建时间戳(4字节)+ 文件大小(8字节)+ 文件 CRC32 校验值(4字节)+ 随机数(2字节),最后可能带上传时指定的扩展名

由于文件名自带了源 IP 与时间戳,Storage 拿到 file_id 就能自己算出"这个文件是不是我这里的、是什么时候传的",这正是 fastdfs-nginx-module 能实现"本地没有就去源节点取"的技术基础。也正因如此,FastDFS 不需要任何元数据数据库。

解析 file_id 的工具:

fdfs_file_info /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
source storage id: 0
source ip address: 192.168.1.220
file create timestamp: 2019-03-13 14:35:29
file size: 988
file crc32: 791805962 (0x2F217D0A)

对应物理路径为:

{store_path0}/data/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

注意物理路径中 M00 被替换成了 data——这是虚拟磁盘路径与物理路径的映射关系。

1.3.3 下载流程

  客户端                    Tracker                  Storage(group1 内任一节点)
    |                          |                              |
    |--1. 带着 file_id 询问 --->|                              |
    |                          | 解析 group1 -> 找到该组       |
    |                          | 按 download_server 策略选一台 |
    |<--2. 返回 storage 地址 --|                              |
    |                                                         |
    |--3. 直连 storage 请求文件(带 file_id)---------------->|
    |                                                         | 4. M00 -> store_path0
    |                                                         | 5. 定位 data/00/00/文件名
    |<--6. 返回文件内容 -------------------------------------|

关键点:

  1. 同组任意节点都可以提供下载服务(因为同组内容相同),Tracker 会做负载均衡。
  2. Tracker 通过 download_server 参数控制策略:0 轮询(默认)、1 总是从源 Storage 下载。
  3. 如果用 HTTP + Nginx 方式访问,则完全不需要经过 Tracker:直接 http://storage_ip/group1/M00/00/00/xxx,Nginx 通过 mod_fastdfs 模块把 URL 映射到本地文件。
  4. 同步延迟的问题:刚上传的文件如果去访问一个尚未同步到的节点,就会 404。fastdfs-nginx-module 通过解析 file_id 中的源 IP,在本地找不到时代理到源 Storage 去取,从而屏蔽了这个问题(详见 3.7)。

1.4 环境准备与安装

1.4.1 服务器规划

源文档的双节点规划(每台机器同时兼任 Tracker 与 Storage,属于实验/小规模部署方式):

角色 IP 地址 主机名
节点 1 192.168.1.220 server-node1
节点 2 192.168.1.221 server-node2

系统版本要求:CentOS 7.x 以上。

详细的角色与端口规划表:

主机名 IP 角色 监听端口 数据目录
server-node1 192.168.1.220 Tracker + Storage(group1) + Nginx 22122、23000、80/8888 /home/dfs(base_path)、/home/dfs(store_path0)
server-node2 192.168.1.221 Tracker + Storage(group1) + Nginx 22122、23000、80/8888 /home/dfs(base_path)、/home/dfs(store_path0)

生产环境更规范的规划(Tracker 与 Storage 分离、多 group):

主机名 IP 角色 group 端口 数据盘
tracker1 192.168.1.210 Tracker + Nginx(反代) - 22122、80 系统盘即可
tracker2 192.168.1.211 Tracker + Nginx(反代) - 22122、80 系统盘即可
storage1 192.168.1.220 Storage + Nginx group1 23000、8888 /dev/sdb → /data/fastdfs1
storage2 192.168.1.221 Storage + Nginx group1 23000、8888 /dev/sdb → /data/fastdfs1
storage3 192.168.1.222 Storage + Nginx group2 23000、8888 /dev/sdb → /data/fastdfs1
storage4 192.168.1.223 Storage + Nginx group2 23000、8888 /dev/sdb → /data/fastdfs1

1.4.2 系统环境准备

以下操作在两台主机上均需执行(主机名配置按节点区分)。

(1)关闭 firewalld

CentOS 7(firewalld):

systemctl stop firewalld
systemctl disable firewalld

生产环境应精细放行而非关闭:

firewall-cmd --permanent --add-port=22122/tcp     # Tracker
firewall-cmd --permanent --add-port=23000/tcp     # Storage
firewall-cmd --permanent --add-port=8888/tcp      # Storage 侧 Nginx HTTP
firewall-cmd --permanent --add-port=80/tcp        # 对外统一入口 Nginx
firewall-cmd --reload
firewall-cmd --list-ports

CentOS 6(iptables,仅适用于 CentOS 6):

iptables -I INPUT -p tcp --dport 22122 -j ACCEPT
iptables -I INPUT -p tcp --dport 23000 -j ACCEPT
iptables -I INPUT -p tcp --dport 8888 -j ACCEPT
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
service iptables save
service iptables restart
# 实验环境可直接关闭
service iptables stop
chkconfig iptables off

FastDFS 端口清单:

端口 服务 说明
22122 fdfs_trackerd Tracker 服务端口(tracker.conf 的 port)。Storage 和客户端都要连它
23000 fdfs_storaged Storage 服务端口(storage.conf 的 port)。客户端上传下载、同组同步都走这里
8888 Nginx(Storage 侧) HTTP 下载端口(storage.conf 的 http.server_port 与 mod_fastdfs.conf 一致)
80 Nginx(Tracker 侧反代) 对外统一入口

(2)关闭 selinux

临时关闭:

setenforce 0

永久关闭:修改 /etc/sysconfig/selinux 配置文件,将:

SELINUX=enforcing

改为:

SELINUX=disabled

/etc/sysconfig/selinux 通常是 /etc/selinux/config 的软链接,改任一个都可以。修改后需重启才完全生效,故一般配合 setenforce 0 立即生效。

(3)设置主机名

节点 1(192.168.1.220):

hostnamectl set-hostname server-node1 --static

节点 2(192.168.1.221):

hostnamectl set-hostname server-node2 --static

CentOS 6(仅适用于 CentOS 6):

vim /etc/sysconfig/network
NETWORKING=yes
HOSTNAME=server-node1
hostname server-node1

(4)配置 hosts

编辑 /etc/hosts,在文件末尾添加两行,建立 IP 与主机名的映射:

vim /etc/hosts
192.168.1.220 server-node1
192.168.1.221 server-node2

注意:两行分别对应各自节点,第二行应为 server-node2。两台机器的 hosts 内容应完全相同(都包含这两行)。

补充:FastDFS 的配置文件中 tracker_server 建议直接写 IP而不是主机名(老版本对主机名支持有限),hosts 主要用于运维便利。

(5)时间同步(重要,不可省略)

FastDFS 的同组同步依赖时间戳判断进度,节点间时间不同步会导致同步异常、文件丢失或重复同步。

CentOS 7:

yum -y install chrony
systemctl start chronyd
systemctl enable chronyd
timedatectl set-timezone Asia/Shanghai
chronyc sources -v
date

CentOS 6(仅适用于 CentOS 6):

yum -y install ntp
service ntpd start
chkconfig ntpd on
ntpdate ntp1.aliyun.com
ntpq -p

详见 08-03-时间同步与Xinetd.md

(6)准备数据盘(生产环境)

不要把 FastDFS 的数据放在系统盘上。

# 分区、格式化、挂载
fdisk /dev/sdb                      # n -> p -> 1 -> 回车 -> 回车 -> w
mkfs.xfs /dev/sdb1
mkdir -p /data/fastdfs1
mount -o noatime /dev/sdb1 /data/fastdfs1
echo '/dev/sdb1 /data/fastdfs1 xfs defaults,noatime 0 0' >> /etc/fstab
mount -a
df -hT

详见 03-01-磁盘管理与文件系统.md

目录规划:

目录 对应配置项 用途
/home/dfs 或 /data/fastdfs/base base_path(tracker/storage 各自的) 存放日志和数据元信息(binlog、mark 文件、pid 文件)。不存实际文件
/data/fastdfs1 store_path0 实际存放文件的目录。多盘时配 store_path1、store_path2...
/etc/fdfs/ - 配置文件目录
$base_path/logs/ - 日志目录(trackerd.log、storaged.log)
$base_path/data/ - tracker 的集群状态文件 / storage 的 binlog 与 mark 文件

所有 base_path 和 store_path 目录必须先手工创建,FastDFS 不会自动创建,路径不存在会直接启动失败:

mkdir -p /home/dfs
mkdir -p /data/fastdfs1
chmod 755 /home/dfs /data/fastdfs1

1.4.3 安装 FastDFS

方式一:使用现成的安装脚本(源文档做法)

分别在两个节点上执行安装。

节点 1(192.168.1.220):

cd fastdfs
./install.sh

安装完成后查看相关进程:

netstat -tnlp

节点 2(192.168.1.221):

cd fastdfs
./install.sh

安装完成后查看相关进程:

netstat -tnlp

install.sh 是打包好的一键安装脚本(内部完成依赖安装、编译、配置文件复制等)。下面给出手工编译安装的完整过程,便于理解脚本做了什么,以及在没有该脚本时自行部署。

方式二:手工编译安装(完整步骤)

第一步:安装编译依赖

yum -y install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel \
  zlib zlib-devel openssl-devel wget vim net-tools perl unzip

第二步:编译安装 libfastcommon(FastDFS 的基础公共库,必须先装)

libfastcommon 提供了 FastDFS 依赖的基础函数库(字符串处理、socket、日志、哈希表、ini 解析等)。FastDFS 无法脱离它编译,且版本需与 FastDFS 匹配(版本不匹配是编译失败的最常见原因)。

cd /usr/local/src
# 下载(也可从 GitHub release 下载 tar 包)
git clone https://github.com/happyfish100/libfastcommon.git --depth 1
cd libfastcommon
./make.sh
./make.sh install

安装结果:

# 库文件安装到
ls -l /usr/lib64/libfastcommon.so
ls -l /usr/lib/libfastcommon.so
# 头文件安装到
ls -l /usr/include/fastcommon/
# 若报找不到 libfastcommon.so,需要建软链接或更新 ld 缓存
ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so
echo '/usr/lib64' > /etc/ld.so.conf.d/fastdfs.conf
ldconfig

第三步:编译安装 fastdfs

cd /usr/local/src
git clone https://github.com/happyfish100/fastdfs.git --depth 1
cd fastdfs
./make.sh
./make.sh install

安装结果:

# 可执行文件安装到 /usr/bin/
ls /usr/bin/ | grep fdfs
fdfs_appender_test
fdfs_appender_test1
fdfs_append_file
fdfs_crc32
fdfs_delete_file
fdfs_download_file
fdfs_file_info
fdfs_monitor
fdfs_storaged
fdfs_test
fdfs_test1
fdfs_trackerd
fdfs_upload_appender
fdfs_upload_file
# 配置文件样例安装到 /etc/fdfs/
ls /etc/fdfs/
# tracker.conf.sample  storage.conf.sample  client.conf.sample  storage_ids.conf.sample

# 服务脚本安装到 /etc/init.d/
ls /etc/init.d/ | grep fdfs
# fdfs_trackerd  fdfs_storaged

第四步:复制配置文件模板(去掉 .sample 后缀)

cd /etc/fdfs/
cp tracker.conf.sample tracker.conf
cp storage.conf.sample storage.conf
cp client.conf.sample client.conf
# fastdfs 源码目录下的 http.conf 和 mime.types 也需要复制(Nginx 模块要用)
cp /usr/local/src/fastdfs/conf/http.conf /etc/fdfs/
cp /usr/local/src/fastdfs/conf/mime.types /etc/fdfs/
ls -l /etc/fdfs/

第五步:创建必需的目录

mkdir -p /home/dfs                # base_path(tracker 与 storage 共用或各自独立)
mkdir -p /data/fastdfs1           # store_path0(生产环境用独立数据盘)

1.5 配置文件逐项详解

1.5.1 tracker.conf(Tracker 配置)

两个节点均需配置。

编辑 tracker 配置文件:

vim /etc/fdfs/tracker.conf

源文档所做的修改:将存储组名称:

store_group=group2

修改为:

store_group=group1

节点 2(192.168.1.221)执行同样的修改。 注意:store_group 只在 store_lookup=1(指定组)时才生效,且这里指定的组名必须与 storage.conf 中的 group_name 一致,否则上传会失败。

完整的 tracker.conf 关键项详解:

# ========== 基础 ==========
# 配置文件是否禁用。true 表示这个配置文件无效(用于批量管理时临时禁用)
disabled = false

# 绑定的本机 IP 地址。留空表示监听所有网卡(0.0.0.0)
# 多网卡机器若只想监听内网,填写内网 IP;填错会导致其它节点连不上
bind_addr =

# Tracker 服务监听端口,默认 22122
port = 22122

# 连接超时时间(秒),针对 socket 连接
connect_timeout = 30

# 网络超时时间(秒),发送/接收数据的超时
network_timeout = 60

# ========== 路径 ==========
# base_path:存放【日志和数据元信息】的基础路径。!!必须提前手工创建,否则启动失败!!
# 目录下会自动生成 data/ 和 logs/ 两个子目录
base_path = /home/dfs

# ========== 连接与线程 ==========
# 最大并发连接数
max_connections = 1024
# 接收/发送队列内存上限
accept_threads = 1
work_threads = 4
min_buff_size = 8KB
max_buff_size = 128KB

# ========== 上传调度策略(核心) ==========
# store_lookup:上传时选择 group(卷)的策略
#   0 = round robin  轮询所有 group(默认,推荐,各组均衡增长)
#   1 = specify group 指定一个组,由下面的 store_group 指定
#   2 = load balance 选择剩余空间最大的组(新组会被打爆,慎用)
store_lookup = 0

# 当 store_lookup=1 时生效,指定要写入的组名(必须与 storage.conf 的 group_name 一致)
store_group = group1

# store_server:选定 group 后,在组内选择哪台 storage 来接收上传
#   0 = round robin  轮询(默认)
#   1 = 按 IP 排序,取第一台(作为源,其余同步)
#   2 = 按优先级排序(upload_priority 小的优先)
store_server = 0

# store_path:选定 storage 后,选择哪个 store_path(多盘时)
#   0 = round robin 轮询(默认)
#   2 = load balance 选剩余空间最大的路径
store_path = 0

# download_server:下载时在组内选择哪台 storage
#   0 = round robin 轮询(默认,负载均衡)
#   1 = 总是从源 storage 下载(可避免同步延迟导致的 404,但源节点压力大)
download_server = 0

# ========== 空间预留(重要) ==========
# 为系统或其它用途预留的存储空间。当组内所有 storage 的剩余空间都小于此值时,
# 该组将【不再接受上传】,报 "no available storage server"
# 可写绝对值(如 10G)或百分比(如 20%)
reserved_storage_space = 20%

# ========== 日志 ==========
# 日志级别:emerg | alert | crit | error | warn | notice | info | debug
log_level = info

# 运行 tracker 的用户与组,留空表示当前用户(一般留空即以 root 运行)
run_by_group =
run_by_user =

# ========== 访问控制 ==========
# 可以连接本 tracker 的 IP 白名单。* 表示不限制
# 支持形式:*  或  192.168.1.[1-100]  或  192.168.1.*  或  host[01-08,20-25].domain.com
allow_hosts = *

# 同步内存日志到磁盘的时间间隔(秒)
sync_log_buff_interval = 10

# ========== Storage 检查与心跳 ==========
# 检测 storage 存活的超时时间(秒),超过则标记为不可用
check_active_interval = 120

# 线程栈大小,不小于 64KB
thread_stack_size = 64KB

# 是否根据 IP 调整 storage 的优先级(新 storage IP 变化时用)
storage_ip_changed_auto_adjust = true

# ========== 同步相关 ==========
# storage 同步文件的最大延迟(秒),仅用于统计与判断,默认 1 天
storage_sync_file_max_delay = 86400
# 同步单个文件的最大时间(秒)
storage_sync_file_max_time = 300

# ========== 合并存储(trunk)==========
use_trunk_file = false
slot_min_size = 256
slot_max_size = 16MB
trunk_file_size = 64MB
trunk_create_file_advance = false
trunk_create_file_time_base = 02:00
trunk_create_file_interval = 86400
trunk_create_file_space_threshold = 20G
trunk_init_check_occupying = false
trunk_init_reload_from_binlog = false
trunk_compress_binlog_min_interval = 0

# ========== storage id 模式 ==========
# 是否使用 storage server ID 代替 IP(IP 可能变化的环境用)
use_storage_id = false
storage_ids_filename = storage_ids.conf
# id 类型:ip 或 id
id_type_in_filename = ip

# ========== 文件去重与回收 ==========
# 是否将文件的 slave(缩略图等)与主文件放一起
store_slave_file_use_link = false

# 是否定时轮转 error log,true 表示每天生成一个新的错误日志文件
rotate_error_log = false
# 轮转时间点
error_log_rotate_time = 00:00
# 是否压缩旧日志
compress_old_error_log = false
compress_error_log_days_before = 7
# 按大小轮转(0 表示不按大小轮转)
rotate_error_log_size = 0
# 日志保留天数(0 表示不删除)
log_file_keep_days = 0

# ========== HTTP(tracker 内置 http 已废弃,用 nginx 代替)==========
# 是否使用 tracker 自带的 http 服务(已不推荐,保持默认)
http.server_port = 8080
http.check_alive_interval = 30
http.check_alive_type = tcp
http.check_alive_uri = /status.html

重点参数总结:

参数 取值/说明 影响
bind_addr 留空监听全部网卡,或填指定 IP 填错会导致 Storage/客户端连不上,是排错重点
port 22122 需与所有 storage.conf、client.conf 中的 tracker_server 端口一致
base_path /home/dfs 必须先手工创建,存放 logs/ 和 data/
store_lookup 0 轮询 / 1 指定组 / 2 选空间最大 决定新文件写入哪个 group。多组扩容时用 0 保持均衡;刚加新组想快速填充可临时用 2
store_group 组名 仅 store_lookup=1 时生效
store_server 0 轮询 / 1 按 IP 首台 / 2 按优先级 决定组内哪台接收上传
store_path 0 轮询 / 2 空间最大 多盘时决定写哪块盘
download_server 0 轮询 / 1 从源节点 设为 1 可规避同步延迟导致的 404
reserved_storage_space 20% 或 10G 空间预留。磁盘快满时上传报 no available storage server 就是这个在起作用
run_by_group / run_by_user 留空为当前用户 若指定非 root 用户,必须确保该用户对 base_path 有读写权限,否则启动失败
allow_hosts * 或 IP 段 访问白名单,安全加固时收紧
log_level info / debug 排错时改 debug
rotate_error_log false / true 生产建议 true,防止日志无限增长撑满磁盘
check_active_interval 120 秒 Storage 超过此时间无心跳则被标记为不可用

1.5.2 storage.conf(Storage 配置)

两个节点均需配置。

编辑 storage 配置文件:

vim /etc/fdfs/storage.conf

添加一行 tracker_server 地址,指向 tracker 服务(FastDFS tracker 默认端口为 22122):

tracker_server=192.168.1.221:22122

节点 2(192.168.1.221)执行同样的配置。

重要补充:源文档只配置了一个 tracker_server,这意味着两个 Storage 都只向 192.168.1.221 上的 Tracker 汇报,192.168.1.220 上的 Tracker 实际上没有被使用(这也是日志中"221 被选为 leader"的原因——它是唯一被汇报的 tracker)。生产环境应配置多个 tracker_server 以实现 Tracker 高可用:

tracker_server=192.168.1.220:22122
tracker_server=192.168.1.221:22122

tracker_server 可以写多行,每行一个,Storage 会向所有 Tracker 汇报状态,客户端也会随机选一个提问。这就是 Tracker 水平扩展的实现方式。

完整的 storage.conf 关键项详解:

# ========== 基础 ==========
disabled = false

# group_name:本 storage 所属的组名(卷名)
# !!同组的所有 storage 必须写相同的 group_name;不同组必须不同!!
# !!这个值一旦有数据后就不能改,改了老文件的 file_id 就对不上,全部访问不到!!
group_name = group1

# 绑定本机 IP,留空监听所有网卡
bind_addr =

# 连接 tracker 时是否也绑定 bind_addr(一般 false)
client_bind = true

# Storage 服务监听端口,默认 23000
# !!同一个 group 内所有 storage 的 port 必须相同!!
port = 23000

connect_timeout = 30
network_timeout = 60

# 向 tracker 发送心跳的间隔(秒)
heart_beat_interval = 30
# 向 tracker 汇报磁盘使用情况的间隔(秒)
stat_report_interval = 60

# ========== 路径(核心) ==========
# base_path:存放【日志与数据元信息】的路径(binlog、mark 文件、pid),!!必须先创建!!
# 注意:这里【不】存放实际的文件内容
base_path = /home/dfs

# store_path_count:存放文件的路径个数(要与下面 store_path 的个数一致)
store_path_count = 1

# store_path0 ~ store_pathN:实际存放文件的目录,!!必须先手工创建!!
# 从 0 开始编号,不能跳号。对应 file_id 中的 M00、M01、M02...
# 【多盘配置】:一台机器挂多块盘时,每块盘配一个 store_path,可线性提升单机容量与 IOPS
store_path0 = /home/dfs
# store_path1 = /data/fastdfs2
# store_path2 = /data/fastdfs3

# subdir_count_per_path:每个 store_path 下创建的两级子目录个数
# 会创建 N × N 个目录(默认 256 × 256 = 65536 个)
# 文件被散列到这些目录中,避免单个目录文件数过多导致的性能下降
# !!这个值一旦有数据后不能改,改了文件就找不到了!!
subdir_count_per_path = 256

# ========== Tracker 地址(可配多个,实现 Tracker 高可用) ==========
tracker_server = 192.168.1.220:22122
tracker_server = 192.168.1.221:22122

# ========== 日志 ==========
log_level = info
run_by_group =
run_by_user =
allow_hosts = *

# 文件分布方式:0 = 轮询(默认,推荐),1 = 顺序写满一个目录再下一个
file_distribute_path_mode = 0
# mode=1 时单目录最大文件数
file_distribute_rotate_count = 100
# 写入多少字节后调用 fsync(0 表示不调用,交给系统)
fsync_after_written_bytes = 0
# 同步日志缓存到磁盘的间隔(秒)
sync_log_buff_interval = 10
# 同步 binlog 缓存到磁盘的间隔(秒)
sync_binlog_buff_interval = 10
# 把 stat 信息同步到磁盘的间隔(秒)
sync_stat_file_interval = 300
thread_stack_size = 512KB

# ========== 上传优先级与磁盘预留 ==========
# 上传优先级,值越小优先级越高(tracker.conf 的 store_server=2 时生效)
upload_priority = 10
# 网卡别名前缀,留空自动获取
if_alias_prefix =

# ========== 文件去重(需要 FastDHT 支持) ==========
# check_file_duplicate:是否检测上传文件的重复(内容相同则只存一份,用符号链接指向)
#   0 = 关闭(默认)
#   1 = 开启,需要额外部署 FastDHT(一个键值数据库)来存放文件指纹
# 开启后可显著节省空间(如大量用户上传同一张图片),但增加了 FastDHT 这个依赖与故障点
check_file_duplicate = 0
# 重复文件的命名空间(check_file_duplicate=1 时必填)
key_namespace = FastDFS
# 与 FastDHT 的连接方式:0 短连接,1 长连接
keep_alive = 0
# FastDHT 服务器列表(check_file_duplicate=1 时必填)
# #include /etc/fdht/fdht_servers.conf

# ========== 同步时间窗与节奏(重要) ==========
# sync_start_time / sync_end_time:允许进行同步的时间段
# 默认 00:00 - 23:59 即全天允许同步
# 可设置为只在业务低峰同步,例如 sync_start_time=01:00, sync_end_time=05:00
# !!但这会导致白天上传的文件在夜间才有副本,冗余窗口很长,一般不建议缩窄!!
sync_start_time = 00:00
sync_end_time = 23:59

# sync_interval:同步线程每同步一个文件后的休眠时间(毫秒)
# 0 表示不休眠(最快同步,默认)
# 设为较大值可降低同步对业务 IO 的冲击,但会拉长同步延迟
sync_interval = 0

# 同步完一批后的等待时间(秒)
sync_wait_msec = 50

# ========== HTTP 服务 ==========
# http.server_port:Storage 提供 HTTP 下载服务的端口
# !!这个值必须与 Nginx 在该 Storage 上监听的端口一致(如 8888),否则 tracker 的
#    http 健康检查与部分客户端行为会异常!!
http.server_port = 8888
http.domain_name =

# ========== 合并存储(trunk) ==========
use_trunk_file = false
slot_min_size = 256
slot_max_size = 16KB
trunk_file_size = 64MB
trunk_create_file_advance = false
trunk_create_file_time_base = 02:00
trunk_create_file_interval = 86400
trunk_create_file_space_threshold = 20G
trunk_init_check_occupying = false
trunk_init_reload_from_binlog = false
trunk_compress_binlog_min_interval = 0

# ========== 其它 ==========
# 同步删除操作前的等待时间(秒)
# 防止在文件还没同步过去就同步删除操作导致的问题
# rotate 日志
rotate_error_log = false
error_log_rotate_time = 00:00
compress_old_error_log = false
rotate_access_log = false
access_log_rotate_time = 00:00
log_file_keep_days = 0
file_sync_skip_invalid_record = false
use_connection_pool = false
connection_pool_max_idle_time = 3600

重点参数总结:

参数 说明 陷阱
group_name 组名 有数据后绝对不能改!改了所有老 file_id 全部失效
bind_addr 监听 IP,留空为全部 填错则 Tracker 与客户端连不上,是排错重点
port 23000 同组内必须相同
tracker_server 可写多行,每行一个 Tracker 生产必须配 2 个以上;写错 IP/端口则 Storage 永远上不了 ACTIVE
base_path 日志与元信息目录 必须先手工创建;不存实际文件
store_path_count store_path 的个数 必须与实际配置的 store_path 行数严格一致,否则启动失败
store_path0..N 实际存文件的目录 必须先手工创建;编号不能跳号;有数据后不能改顺序
subdir_count_per_path 256(即 65536 个目录) 有数据后不能改;文件数极大时可考虑加大
sync_start_time/sync_end_time 同步时间窗 缩窄会拉长冗余窗口
sync_interval 同步间隔(毫秒) 设太大导致同步延迟明显,出现"另一个节点 404"
check_file_duplicate 0/1 文件去重 设 1 必须部署 FastDHT,否则启动失败
http.server_port 8888 必须与 Nginx 监听端口一致
upload_priority 上传优先级 值越小越优先
reserved_storage_space 在 tracker.conf 中 磁盘预留,触发后拒绝上传

1.5.3 client.conf(客户端配置)

两个节点均需配置,在 client.conf 中添加 tracker_server 地址:

vim /etc/fdfs/client.conf
tracker_server=192.168.1.221:22122

完整的关键项:

# 连接与网络超时
connect_timeout = 30
network_timeout = 60

# base_path:客户端的日志存放路径,!!必须先手工创建!!
base_path = /home/dfs

# tracker_server:可配置多个,客户端会随机选择一个(实现 Tracker 高可用)
tracker_server = 192.168.1.220:22122
tracker_server = 192.168.1.221:22122

# 日志级别
log_level = info

# 是否使用连接池
use_connection_pool = false
connection_pool_max_idle_time = 3600

# 是否从 tracker 加载 FastDFS 参数
load_fdfs_parameters_from_tracker = false
# 使用 storage id 而非 IP
use_storage_id = false
storage_ids_filename = storage_ids.conf

# HTTP 相关(用于生成访问 URL)
http.tracker_server_port = 80
#include http.conf

client.conf 只有两项是必须改的:tracker_server(指向 Tracker)和 base_path(客户端日志目录,必须存在)。所有 fdfs_* 命令行工具都要用这个文件作为第一个参数。

1.5.4 常见坑:配置文件中的路径必须先手工创建

这是 FastDFS 最高频的启动失败原因。FastDFS 不会自动创建 base_path 和 store_path 目录,一旦路径不存在,服务直接启动失败。

# 典型报错(在 base_path/logs/ 里根本看不到,因为日志目录都建不出来,只能在前台看到)
[ERROR] file: shared_func.c, line: xxx, "/home/dfs" can't be accessed, error info: No such file or directory
[ERROR] file: storage_func.c, line: xxx, mkdir "/data/fastdfs1/data" fail, errno: 13, error info: Permission denied

正确的准备步骤:

# 1. 从配置文件里把所有路径提取出来
grep -E '^(base_path|store_path[0-9]+)' /etc/fdfs/tracker.conf /etc/fdfs/storage.conf /etc/fdfs/client.conf

# 2. 逐个创建并确认权限
mkdir -p /home/dfs
mkdir -p /data/fastdfs1
chmod 755 /home/dfs /data/fastdfs1

# 3. 如果配置了 run_by_user / run_by_group(非 root 运行),必须把目录属主改过去
# chown -R fastdfs:fastdfs /home/dfs /data/fastdfs1

# 4. 确认磁盘有空间与 inode
df -h /home/dfs /data/fastdfs1
df -i /home/dfs /data/fastdfs1

# 5. 验证配置文件语法(用前台启动方式看报错最直观)
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf

其它相关的配置陷阱:

  • store_path_count 与实际 store_path 行数不一致 → 启动失败或只用了部分盘。
  • store_path 是软链接指向未挂载的盘 → 数据会写进系统盘,磁盘满后灾难。
  • 修改了 store_path0 的路径但没迁移数据 → 老文件全部 404。
  • SELinux 开启时对 /home 下的自定义目录有 context 限制 → 表现为 Permission denied,需 setenforce 0 或 restorecon。

1.6 启动与验证

1.6.1 重启 fdfs 服务生效

两个节点都要执行以下重启操作。

重启 tracker 服务:

service fdfs_trackerd restart

重启 storage 服务:

service fdfs_storaged restart

源文档使用 service 命令(FastDFS 安装包自带 /etc/init.d/fdfs_trackerd、/etc/init.d/fdfs_storaged SysV 脚本,在 CentOS 6 和 CentOS 7 上都可用;CentOS 7 会通过 systemd 的兼容层转发)。

启动顺序:必须先 Tracker,后 Storage。 因为 Storage 启动时要向 Tracker 注册并汇报状态、获取 group 信息。如果 Tracker 没起来,Storage 会不断重试(不会退出,但状态无法变 ACTIVE)。

CentOS 7(推荐用 systemd 管理):

# 方式一:使用自带的 SysV 脚本
service fdfs_trackerd start
service fdfs_storaged start
service fdfs_trackerd status
chkconfig fdfs_trackerd on
chkconfig fdfs_storaged on

# 方式二:直接调用二进制(支持 start/stop/restart 参数)
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf start
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf stop
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart

方式三:自定义 systemd 单元(生产推荐,仅 CentOS 7+)

/usr/lib/systemd/system/fdfs_trackerd.service:

[Unit]
Description=FastDFS Tracker Server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/home/dfs/data/fdfs_trackerd.pid
ExecStart=/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start
ExecStop=/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf stop
ExecReload=/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

/usr/lib/systemd/system/fdfs_storaged.service:

[Unit]
Description=FastDFS Storage Server
After=network.target remote-fs.target nss-lookup.target fdfs_trackerd.service
Requires=fdfs_trackerd.service

[Service]
Type=forking
PIDFile=/home/dfs/data/fdfs_storaged.pid
ExecStart=/usr/bin/fdfs_storaged /etc/fdfs/storage.conf start
ExecStop=/usr/bin/fdfs_storaged /etc/fdfs/storage.conf stop
ExecReload=/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start fdfs_trackerd
systemctl start fdfs_storaged
systemctl enable fdfs_trackerd fdfs_storaged
systemctl status fdfs_trackerd fdfs_storaged

CentOS 6(仅适用于 CentOS 6,无 systemd):

service fdfs_trackerd start
service fdfs_storaged start
chkconfig fdfs_trackerd on
chkconfig fdfs_storaged on
service fdfs_trackerd status

验证进程与端口:

ps -ef | grep fdfs
netstat -tnlp | grep -E '22122|23000'
ss -tunlp | grep -E '22122|23000'

期望看到:

tcp   0  0 0.0.0.0:22122   0.0.0.0:*   LISTEN   1234/fdfs_trackerd
tcp   0  0 0.0.0.0:23000   0.0.0.0:*   LISTEN   1256/fdfs_storaged

1.6.2 查看监控状态(fdfs_monitor)

fdfs_monitor /etc/fdfs/storage.conf

查看输出,确认两个 storage 均处于 ACTIVE 状态(第二个 storage 同样为 ACTIVE)。

典型完整输出与字段解读:

[2019-03-13 15:20:11] DEBUG - base_path=/home/dfs, connect_timeout=30, network_timeout=60, ...

server_count=2, server_index=0

tracker server is 192.168.1.221:22122

group count: 1

Group 1:
group name = group1
disk total space = 51200 MB
disk free space = 43008 MB
trunk free space = 0 MB
storage server count = 2
active server count = 2
storage server port = 23000
storage HTTP port = 8888
store path count = 1
subdir count per path = 256
current write server index = 0
current trunk file id = 0

    Storage 1:
        id = 192.168.1.220
        ip_addr = 192.168.1.220 (server-node1)  ACTIVE
        http domain =
        version = 6.06
        join time = 2019-03-13 14:10:22
        up time = 2019-03-13 15:02:10
        total storage = 51200 MB
        free storage = 43008 MB
        upload priority = 10
        store_path_count = 1
        subdir_count_per_path = 256
        storage_port = 23000
        storage_http_port = 8888
        current_write_path = 0
        source storage id =
        if_trunk_server = 0
        connection.alloc_count = 256
        connection.current_count = 1
        connection.max_count = 2
        total_upload_count = 12
        success_upload_count = 12
        total_append_count = 0
        success_append_count = 0
        total_modify_count = 0
        success_modify_count = 0
        total_truncate_count = 0
        success_truncate_count = 0
        total_set_meta_count = 0
        success_set_meta_count = 0
        total_delete_count = 1
        success_delete_count = 1
        total_download_count = 8
        success_download_count = 8
        total_get_meta_count = 0
        success_get_meta_count = 0
        total_create_link_count = 0
        success_create_link_count = 0
        total_delete_link_count = 0
        success_delete_link_count = 0
        total_upload_bytes = 118656
        success_upload_bytes = 118656
        total_download_bytes = 7904
        success_download_bytes = 7904
        total_sync_in_bytes = 3952
        success_sync_in_bytes = 3952
        total_sync_out_bytes = 11856
        success_sync_out_bytes = 11856
        total_file_write_count = 15
        success_file_write_count = 15
        total_file_read_count = 8
        success_file_read_count = 8
        last_heart_beat_time = 2019-03-13 15:20:05
        last_source_update = 2019-03-13 15:18:44
        last_sync_update = 2019-03-13 15:18:46
        last_synced_timestamp = 2019-03-13 15:18:44 (0s delay)

    Storage 2:
        id = 192.168.1.221
        ip_addr = 192.168.1.221 (server-node2)  ACTIVE
        ...

关键字段解读:

字段 含义 排错价值
active server count 组内 ACTIVE 的节点数 必须等于 storage server count,否则有节点异常
ip_addr = x.x.x.x ACTIVE 节点状态,见下方状态表 最重要的字段,非 ACTIVE 说明有问题
disk total/free space 组的总/剩余空间(取组内最小值) 印证"组容量取决于最小节点"
current write server index 当前正在被写入的节点索引 观察 store_server 策略是否生效
current_write_path 当前写入的 store_path 索引 多盘时观察轮询
total_upload_count / success_upload_count 累计上传次数 / 成功次数 二者差值即失败次数;也可用于观察负载是否均衡
total_download_count / success_download_count 累计/成功下载次数 观察读负载分布
total_delete_count / success_delete_count 累计/成功删除次数 -
total_upload_bytes / success_upload_bytes 上传字节数 容量增长趋势
total_sync_in_bytes 从其它节点同步进来的字节数 判断同步是否在工作
total_sync_out_bytes 同步出去的字节数 源节点的同步输出量
last_heart_beat_time 最后一次心跳时间 与当前时间差过大说明节点失联
last_source_update 本节点最后一次作为源被写入的时间 -
last_sync_update 本节点最后一次同步数据的时间 -
last_synced_timestamp 已同步到的时间点 + 延迟秒数,如 (0s delay) 判断同步延迟的关键指标。delay 持续增大说明同步跟不上或已中断
if_trunk_server 是否是 trunk server 启用合并存储时相关
source storage id 全量同步的源节点 新节点加入时会填这个
version Storage 版本 版本不一致排查
join time / up time 加入集群时间 / 本次启动时间 up time 频繁变化说明进程在反复重启

Storage 状态一览(极其重要):

状态 含义 处理
FAKE_INIT 刚创建,尚未初始化 等待
INIT 已初始化,但尚未获得同步源(还不知道从哪同步历史数据) 组内第一个节点会短暂停留后直接变 ACTIVE;非首节点长期停在这里说明组内没有可用的源节点
WAIT_SYNC 等待同步(已确定源,排队中) 等待,或检查源节点是否 ACTIVE
SYNCING 正在做全量同步(新节点加入时的正常状态) 等待完成,可用 total_sync_in_bytes 观察进度
DELETED 已被从集群删除 用 fdfs_monitor ... delete 清理残留
OFFLINE 离线(进程停了或网络断了) 检查进程与网络
ONLINE 在线但还不能提供服务 短暂过渡状态
ACTIVE 正常工作,可提供上传下载服务 这是唯一的正常状态

fdfs_monitor 的其它用法:

# 查看集群状态(可用 storage.conf 或 client.conf,只要里面有 tracker_server)
fdfs_monitor /etc/fdfs/storage.conf
fdfs_monitor /etc/fdfs/client.conf

# 从集群中删除一个 storage(下线节点时用,需先停掉该 storage 进程)
fdfs_monitor /etc/fdfs/client.conf delete group1 192.168.1.222

# 设置 storage 状态为可用/不可用(临时摘除流量)
fdfs_monitor /etc/fdfs/client.conf set_trunk_server group1 192.168.1.220

# 结合 grep 快速看状态
fdfs_monitor /etc/fdfs/storage.conf 2>/dev/null | grep -E 'ip_addr|active server count|last_synced'

1.6.3 上传文件测试

fdfs_upload_file /etc/fdfs/client.conf README

上传成功后返回一个字符串,例如:

group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

该字符串是文件存放在服务器上的有效访问路径,通过浏览器访问对应地址即可找到上传的文件。

必须记录这个 file_id——它是唯一能找回文件的凭据。实际应用中要把它存进数据库。

验证文件确实落盘:

# 根据 file_id 推算物理路径:M00 -> store_path0 的 data 目录
ls -l /home/dfs/data/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 用 find 直接找
find /home/dfs/data -name 'wKgB3Fyo*'

1.6.4 命令行工具完整用法

(1)fdfs_upload_file 上传普通文件

fdfs_upload_file <config_file> <local_filename> [storage_ip:port] [store_path_index]

# 基本用法
fdfs_upload_file /etc/fdfs/client.conf README
fdfs_upload_file /etc/fdfs/client.conf /tmp/test.jpg

# 指定上传到某台 storage 的某个 store_path(跳过 tracker 的调度)
fdfs_upload_file /etc/fdfs/client.conf /tmp/test.jpg 192.168.1.220:23000 0

(2)fdfs_download_file 下载文件

fdfs_download_file <config_file> <file_id> [local_filename]

fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 不指定本地文件名时,保存为 file_id 中的文件名部分(在当前目录)

fdfs_download_file /etc/fdfs/client.conf \
  group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045 /tmp/downloaded.txt

(3)fdfs_delete_file 删除文件

fdfs_delete_file <config_file> <file_id>

fdfs_delete_file /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

删除是同步到组内所有节点的(通过 binlog 的 D 记录)。删除后 file_id 立即失效,不可恢复。

(4)fdfs_file_info 查看文件元信息(解析 file_id)

fdfs_file_info <config_file> <file_id>

fdfs_file_info /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
source storage id: 0
source ip address: 192.168.1.220
file create timestamp: 2019-03-13 14:35:29
file size: 988
file crc32: 791805962 (0x2F217D0A)

这是排错神器:能立刻告诉你"这个文件当初是传到哪台机器的、什么时候传的、多大"。当文件访问 404 时,先用它确认源节点,再去源节点看文件是否存在、是否同步出去了。

注意:fdfs_file_info 只做本地的 file_id 解码,不联网验证文件是否真实存在。要验证存在性用 fdfs_download_file 或 HTTP 访问。

(5)fdfs_test 综合测试工具

fdfs_test <config_file> <operation> [arg1 [arg2 ...]]

# 上传(会同时输出更详细的信息,包括 HTTP 访问 URL 和 master/slave 文件)
fdfs_test /etc/fdfs/client.conf upload /tmp/test.jpg

# 下载
fdfs_test /etc/fdfs/client.conf download group1 M00/00/00/wKgB3Fyo...

# 删除
fdfs_test /etc/fdfs/client.conf delete group1 M00/00/00/wKgB3Fyo...

# 查询文件信息
fdfs_test /etc/fdfs/client.conf query_file_info group1 M00/00/00/wKgB3Fyo...

# 设置/获取元数据(metadata,可用来存原始文件名等信息)
fdfs_test /etc/fdfs/client.conf upload_slave <master_file_id> <prefix> <local_file>

fdfs_test upload 输出示例(比 fdfs_upload_file 信息更丰富):

This is FastDFS client test program v6.06
...
group_name=group1, ip_addr=192.168.1.220, port=23000
storage_upload_by_filename
group_name=group1, remote_filename=M00/00/00/wKgB3Fyo....jpg
source ip address: 192.168.1.220
file timestamp=2019-03-13 14:35:29
file size=988
file crc32=791805962
example file url: http://192.168.1.220:8888/group1/M00/00/00/wKgB3Fyo....jpg
storage_upload_slave_by_filename
group_name=group1, remote_filename=M00/00/00/wKgB3Fyo..._big.jpg
example file url: http://192.168.1.220:8888/group1/M00/00/00/wKgB3Fyo..._big.jpg

注意 example file url 一行——它直接给出了 HTTP 访问地址,可以用来验证 Nginx 配置。

(6)fdfs_append_file / fdfs_upload_appender 追加文件

普通 FastDFS 文件不可修改,但 appender 文件支持追加写(用于日志、边录边传的视频等场景)。

# 上传一个 appender 文件(返回的 file_id 支持后续追加)
fdfs_upload_appender /etc/fdfs/client.conf /tmp/part1.log
# 返回 group1/M00/00/00/wKgB3Fyo...

# 向已有的 appender 文件追加内容
fdfs_append_file /etc/fdfs/client.conf group1/M00/00/00/wKgB3Fyo... /tmp/part2.log

# 修改 appender 文件的指定位置(从 offset 开始覆盖)
# fdfs_modify_file <config> <file_id> <offset> <local_filename>

# 截断 appender 文件到指定长度
# fdfs_truncate_file <config> <file_id> <truncated_file_size>

# 相关测试工具
fdfs_appender_test /etc/fdfs/client.conf /tmp/test.log
fdfs_appender_test1 /etc/fdfs/client.conf /tmp/test.log

注意:appender 文件不能启用合并存储(trunk),且追加操作也会通过 binlog 同步到同组其它节点。

(7)其它工具

# 计算文件的 CRC32(FastDFS 用它做文件校验)
fdfs_crc32 /tmp/test.jpg

# 集群监控(见 3.6.2)
fdfs_monitor /etc/fdfs/client.conf

命令行工具汇总表:

命令 用途
fdfs_upload_file <conf> <本地文件> 上传普通文件,返回 file_id
fdfs_download_file <conf> <file_id> [本地文件名] 下载文件
fdfs_delete_file <conf> <file_id> 删除文件
fdfs_file_info <conf> <file_id> 解析 file_id:源 IP、创建时间、大小、CRC32
fdfs_test <conf> upload\|download\|delete\|query_file_info ... 综合测试,输出信息最全(含 HTTP URL)
fdfs_upload_appender <conf> <本地文件> 上传 appender 文件(可追加)
fdfs_append_file <conf> <file_id> <本地文件> 向 appender 文件追加
fdfs_monitor <conf> [delete group ip] 查看集群状态 / 删除节点
fdfs_crc32 <文件> 计算 CRC32
fdfs_trackerd <conf> start\|stop\|restart Tracker 服务控制
fdfs_storaged <conf> start\|stop\|restart Storage 服务控制

1.6.5 下载与跨节点同步验证

在节点 1(192.168.1.220)上下载文件:

curl -O http://192.168.1.220/group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

然后在节点 2(192.168.1.221)上使用相同的文件路径、仅替换 IP 进行下载:

curl -O http://192.168.1.221/group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

对比两个节点下载到的文件内容是否一致。

上传一次文件,可在不同节点下载到相同内容,即证明集群安装成功、文件同步成功。

更严谨的验证方式(用 md5 对比):

# 1. 上传
FID=$(fdfs_upload_file /etc/fdfs/client.conf /tmp/test.jpg)
echo $FID

# 2. 从节点 1 和节点 2 分别下载
curl -s -o /tmp/from220 http://192.168.1.220/$FID
curl -s -o /tmp/from221 http://192.168.1.221/$FID

# 3. 对比三者的 md5(原文件 + 两个节点下载的)
md5sum /tmp/test.jpg /tmp/from220 /tmp/from221
# 三个 md5 值必须完全相同

# 4. 也可直接在两个节点的磁盘上验证文件确实存在
ssh 192.168.1.220 "find /home/dfs/data -name '${FID##*/}'"
ssh 192.168.1.221 "find /home/dfs/data -name '${FID##*/}'"

# 5. 用 FastDFS 协议下载(不经 HTTP)
fdfs_download_file /etc/fdfs/client.conf $FID /tmp/from_fdfs
md5sum /tmp/from_fdfs

上述 curl 使用的是 80 端口,说明该环境已在两台机器上配好了 Nginx(见 3.7)。若尚未配置 Nginx,直接用 HTTP 访问会连接失败,此时应先用 fdfs_download_file 验证。

1.6.6 查看 storage.log 日志

查看 storage 日志,可观察到集群选举与同步情况:

tail -f /etc/fdfs/logs/storaged.log

日志中可看到:

  1. 192.168.1.221 被选为 leader 节点;
  2. 文件从 192.168.1.220 节点同步成功。

路径说明:源文档写的是 /etc/fdfs/logs/storaged.log,实际日志路径由配置文件中的 base_path 决定,即 $base_path/logs/storaged.log。如果 base_path=/home/dfs,那么真实路径是 /home/dfs/logs/storaged.log。只有在把 base_path 设成 /etc/fdfs 时才会是源文档写的那个路径(不推荐这样配置,配置目录不该混放日志和数据)。

正确的日志查看方式:

# 先确认 base_path
grep '^base_path' /etc/fdfs/storage.conf
grep '^base_path' /etc/fdfs/tracker.conf

# 再看对应日志
tail -f /home/dfs/logs/storaged.log
tail -f /home/dfs/logs/trackerd.log
ls -l /home/dfs/logs/

典型的关键日志行:

[2019-03-13 15:02:10] INFO - local_host_ip_count: 2,  127.0.0.1  192.168.1.220
[2019-03-13 15:02:10] INFO - file: storage_func.c, line: xxx, tracker_client_ip: 192.168.1.220, my_server_id_str: 192.168.1.220, g_server_id_in_filename: 3691247296
[2019-03-13 15:02:11] INFO - file: tracker_client_thread.c, line: xxx, successfully connect to tracker server 192.168.1.221:22122, as a tracker client, my ip is 192.168.1.220
[2019-03-13 15:02:11] INFO - file: tracker_client_thread.c, line: xxx, tracker server 192.168.1.221:22122, set tracker leader: 192.168.1.221:22122
[2019-03-13 15:02:12] INFO - file: storage_sync.c, line: xxx, successfully connect to storage server 192.168.1.221:23000
[2019-03-13 15:18:46] INFO - file: storage_sync.c, line: xxx, sync thread to storage server 192.168.1.221:23000 started
[2019-03-13 15:18:46] INFO - file: storage_sync.c, line: xxx, file: M00/00/00/wKgB3Fyo... sync from 192.168.1.220 success

日志文件清单:

日志文件 位置 用途
trackerd.log $base_path/logs/(tracker 的 base_path) Tracker 运行日志:storage 注册、leader 选定、心跳
storaged.log $base_path/logs/(storage 的 base_path) Storage 运行日志:连接 tracker、同步进展、文件读写错误,最常看的日志
client.log $base_path/logs/(client 的 base_path) 客户端工具日志
mod_fastdfs.log /tmp/(或 mod_fastdfs.conf 的 base_path) Nginx 模块日志,HTTP 访问 404 时必看
binlog.000 $base_path/data/sync/ 同步操作日志(不是错误日志,是同步依据)

1.7 HTTP 访问与 Nginx 集成

1.7.1 为什么需要 fastdfs-nginx-module

FastDFS 自带的 HTTP 服务(http.server_port)功能弱、性能差,早已不推荐使用。生产上统一用 Nginx + fastdfs-nginx-module 提供 HTTP 访问。

但为什么不能只用普通的 Nginx root 静态文件服务?因为有一个绕不过去的问题:

核心问题:同组节点间的同步延迟。

时刻 T0:客户端上传文件到 storage1-1(192.168.1.220),立即返回 file_id
时刻 T0:应用把 file_id 存入数据库,页面立刻渲染出图片 URL
时刻 T0+10ms:用户浏览器请求这张图片,负载均衡把请求打到了 storage1-2(192.168.1.221)
        但此时 storage1-2 上还【没有】这个文件(异步同步还没完成!)
结果:HTTP 404,用户看到裂图

这个窗口通常只有几十毫秒到几秒,但在"上传后立即查看"的场景(发朋友圈、上传头像后马上显示)中必然会命中。文件越大、网络越慢、sync_interval 设得越大,窗口越长。

fastdfs-nginx-module 的解决原理:

浏览器 --> Nginx(storage1-2) --> mod_fastdfs 模块
                                    |
                                    +-- 1. 解析 URL 得到 group1 / M00 / 00/00/文件名
                                    +-- 2. 检查 group_name 是否是本机所属的组
                                    +-- 3. 把 M00 映射到本机的 store_path0
                                    +-- 4. 检查本地文件是否存在?
                                          |
                                          +-- 存在  --> 直接返回本地文件(快)
                                          |
                                          +-- 不存在 --> 【解码 file_id 拿到源 storage 的 IP】
                                                        --> 用 HTTP 代理(proxy)到源 storage 取回
                                                        --> 返回给浏览器(用户无感知)

关键在于第 4 步的"不存在"分支:file_id 的文件名部分编码了源 Storage 的 IP(见 3.3.2),所以模块无需查任何数据库就能知道"这个文件的原始副本在哪台机器上",从而直接去那里取。

另外模块还会做:

  • 校验 group_name 是否属于本机(防止访问到错误的组);
  • 支持 M00/M01 到多个 store_path 的映射;
  • 支持 trunk 文件的偏移读取(合并存储时,普通 Nginx 根本读不了 trunk 内部的小文件);
  • 支持防盗链(http.conf 中的 token 机制)。

结论:只要是多副本(同组多节点)的 FastDFS,就必须用 fastdfs-nginx-module,否则一定会遇到偶发 404。

1.7.2 模块的编译安装与 Nginx 重新编译

Nginx 的模块是编译期静态链入的(fastdfs-nginx-module 不支持动态模块加载),因此必须重新编译 Nginx。

第一步:下载模块源码

cd /usr/local/src
git clone https://github.com/happyfish100/fastdfs-nginx-module.git --depth 1
# 或下载 tar 包
# wget https://github.com/happyfish100/fastdfs-nginx-module/archive/master.zip
ls /usr/local/src/fastdfs-nginx-module/src/
# config  common.c  common.h  ngx_http_fastdfs_module.c  mod_fastdfs.conf

第二步:修改模块的 config 文件(老版本必做)

老版本的 src/config 中路径写死为 /usr/local/include,会导致编译时找不到头文件:

vim /usr/local/src/fastdfs-nginx-module/src/config

把:

CORE_INCS="$CORE_INCS /usr/local/include/fastdfs /usr/local/include/fastcommon/"

改为(去掉 local,与实际安装位置一致):

CORE_INCS="$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon/"

新版本已修正此问题,可先检查再决定是否修改。

第三步:编译 Nginx 并加入模块

情况 A:全新安装 Nginx

cd /usr/local/src
wget http://nginx.org/download/nginx-1.20.2.tar.gz
tar zxf nginx-1.20.2.tar.gz
cd nginx-1.20.2

# 安装编译依赖
yum -y install gcc gcc-c++ make pcre pcre-devel zlib zlib-devel openssl openssl-devel

./configure \
  --prefix=/usr/local/nginx \
  --sbin-path=/usr/sbin/nginx \
  --conf-path=/etc/nginx/nginx.conf \
  --error-log-path=/var/log/nginx/error.log \
  --http-log-path=/var/log/nginx/access.log \
  --pid-path=/var/run/nginx.pid \
  --with-http_stub_status_module \
  --with-http_ssl_module \
  --with-http_gzip_static_module \
  --add-module=/usr/local/src/fastdfs-nginx-module/src

make
make install
nginx -V           # 输出中应能看到 --add-module=.../fastdfs-nginx-module/src

情况 B:已有 Nginx,需要加模块(平滑升级,不中断服务)

# 1. 查看现有 Nginx 的编译参数,必须完整保留!
nginx -V
# 输出示例:configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module ...

# 2. 下载完全相同版本的源码
nginx -v            # 确认版本号,如 1.20.2
cd /usr/local/src
wget http://nginx.org/download/nginx-1.20.2.tar.gz
tar zxf nginx-1.20.2.tar.gz
cd nginx-1.20.2

# 3. 用【原有的全部参数】+ 新的 --add-module 重新 configure
./configure <把 nginx -V 输出的所有参数原样粘贴到这里> \
  --add-module=/usr/local/src/fastdfs-nginx-module/src

# 4. 只 make,【不要 make install】(避免覆盖配置文件)
make

# 5. 备份旧的二进制
cp /usr/sbin/nginx /usr/sbin/nginx.bak.$(date +%F)

# 6. 替换二进制
cp -f objs/nginx /usr/sbin/nginx
# 若提示 Text file busy,用:
# mv /usr/sbin/nginx /usr/sbin/nginx.old && cp objs/nginx /usr/sbin/nginx

# 7. 验证
nginx -t
nginx -V

# 8. 平滑重启(不中断连接)
kill -USR2 $(cat /var/run/nginx.pid)       # 启动新版本 worker
sleep 3
kill -WINCH $(cat /var/run/nginx.pid.oldbin)   # 优雅关闭旧 worker
kill -QUIT $(cat /var/run/nginx.pid.oldbin)    # 退出旧 master
# 或简单地
systemctl restart nginx

第四步:复制模块配置文件

cp /usr/local/src/fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/
# 若前面没做过,还需要 fastdfs 源码 conf 下的这两个文件
cp /usr/local/src/fastdfs/conf/http.conf /etc/fdfs/
cp /usr/local/src/fastdfs/conf/mime.types /etc/fdfs/
ls -l /etc/fdfs/

第五步:建立 M00 到 store_path0 的软链接

模块要求存在 $store_path0/data 到 M00 的映射,创建软链接以便调试与部分场景访问:

ln -s /home/dfs/data /home/dfs/data/M00
# 若 store_path0 = /data/fastdfs1
# ln -s /data/fastdfs1/data /data/fastdfs1/data/M00
ls -l /home/dfs/data/M00

注意:使用 ngx_fastdfs_module 时这个软链接并非严格必需(模块自己会做 M00 → store_path 的映射),但许多教程都会创建它,且在用普通 root 指令兜底时有用。

详见 10-02-Nginx服务与优化.md

1.7.3 mod_fastdfs.conf 逐项配置

vim /etc/fdfs/mod_fastdfs.conf
# ========== 基础 ==========
# 连接超时(秒)
connect_timeout = 10
# 网络超时(秒)
network_timeout = 30

# base_path:模块的日志存放目录(!!必须存在!!)
base_path = /tmp

# 是否从 tracker 加载 FastDFS 参数(建议 true,可自动获取 store_path 等信息)
load_fdfs_parameters_from_tracker = true

# 是否使用 storage id 代替 IP
use_storage_id = false
storage_ids_filename = storage_ids.conf

# ========== Tracker 地址(关键) ==========
# 必须与 storage.conf 中的 tracker_server 一致,可配多个
tracker_server = 192.168.1.220:22122
tracker_server = 192.168.1.221:22122

# ========== 端口与日志 ==========
# 本 Nginx 提供 HTTP 服务的端口,!!必须与 nginx.conf 中 listen 的端口一致!!
# 也应与 storage.conf 的 http.server_port 一致
storage_server_port = 23000
# 模块日志级别
log_level = info
log_filename =

# 文件不存在时的处理方式:
#   proxy    = 代理到源 storage 去取(!!推荐,这是解决同步延迟的关键!!)
#   redirect = 302 重定向到源 storage(浏览器会看到跳转)
response_mode = proxy

# ========== group 配置(最容易配错的地方) ==========
# group_name:本 Storage 所属的组名,!!必须与 storage.conf 的 group_name 完全一致!!
group_name = group1

# url_have_group_name:URL 中是否包含 group 名
#   true  = URL 形如 http://ip/group1/M00/00/00/xxx(!!推荐,多 group 时必须 true!!)
#   false = URL 形如 http://ip/M00/00/00/xxx
# !!这个值配错是 404 的第一大原因!!它必须与 Nginx 的 location 规则匹配
url_have_group_name = true

# store_path_count 与 store_pathN:
# !!必须与 storage.conf 中的完全一致(个数、顺序、路径)!!
store_path_count = 1
store_path0 = /home/dfs
# store_path1 = /data/fastdfs2

# ========== 多 group 场景(一台 Nginx 服务多个组时) ==========
# group_count:本机涉及的 group 数量。0 表示单组(用上面的 group_name/store_path 配置)
# 若一台机器上跑了多个 group 的 storage(不推荐),需设置为实际组数并补充 [group1]/[group2] 段
group_count = 0

# 单机多组时的写法示例(group_count = 2):
# [group1]
# group_name = group1
# storage_server_port = 23000
# store_path_count = 1
# store_path0 = /home/dfs
#
# [group2]
# group_name = group2
# storage_server_port = 23000
# store_path_count = 1
# store_path0 = /data/fastdfs2

必改的 4 项(其余可保持默认):

参数 必须设为 配错的后果
tracker_server 与 storage.conf 一致(可多行) 模块无法获取集群信息,访问全部失败
group_name 与本机 storage.conf 的 group_name 完全一致 404,模块认为"这个 group 不是我的"
url_have_group_name 通常 true(URL 含 group1) 404 最常见原因,必须与 Nginx location 匹配
store_path0(及 count) 与 storage.conf 的 store_path0 完全一致 404,模块在错误的目录下找文件

配置完成后验证:

# 检查三个文件的关键项是否一致(排错必做的三方对照)
grep -E '^(group_name|store_path0|store_path_count|tracker_server|http.server_port)' /etc/fdfs/storage.conf
grep -E '^(group_name|store_path0|store_path_count|tracker_server|url_have_group_name)' /etc/fdfs/mod_fastdfs.conf

1.7.4 Storage 节点上的 Nginx 配置

在每个 Storage 节点上配置 Nginx,用 ngx_fastdfs_module 处理文件请求。

vim /etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  10240;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout  65;
    client_max_body_size 300m;      # 允许的最大上传体积,按业务调整

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    server {
        # 监听端口必须与 storage.conf 的 http.server_port 一致
        listen       8888;
        server_name  localhost;

        # 核心:把 /group1/M00/ 开头的请求交给 fastdfs 模块处理
        # 注意 location 必须与 mod_fastdfs.conf 的 url_have_group_name 匹配:
        #   url_have_group_name = true  --> location ~/group[0-9]+/M00
        #   url_have_group_name = false --> location ~/M00
        location ~/group[0-9]+/M00 {
            # 可选:把 M00 直接映射到本地目录(模块会优先处理,此行主要用于兜底调试)
            # root /home/dfs/data;
            ngx_fastdfs_module;
        }

        # 只服务单个 group 时也可写成
        # location /group1/M00 {
        #     ngx_fastdfs_module;
        # }

        # 状态页,便于监控
        location /nginx_status {
            stub_status on;
            access_log off;
            allow 192.168.1.0/24;
            deny all;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

启动并验证:

nginx -t
systemctl start nginx            # CentOS 7
systemctl enable nginx
service nginx start              # CentOS 6
chkconfig nginx on

# 查看模块日志(配置有问题时这里会有明确报错)
tail -f /tmp/mod_fastdfs.log

# 直接访问验证(用之前上传得到的 file_id)
curl -I http://192.168.1.220:8888/group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 期望 HTTP/1.1 200 OK
curl -o /tmp/t.jpg http://192.168.1.220:8888/group1/M00/00/00/wKgB3Fyo...

启动时的正常日志(/tmp/mod_fastdfs.log):

[2019-03-13 16:00:01] INFO - local_host_ip_count: 2,  127.0.0.1  192.168.1.220
[2019-03-13 16:00:01] INFO - file: /usr/local/src/fastdfs-nginx-module/src/common.c, line: xxx, fastdfs apache / nginx module v1.22, response_mode=proxy, base_path=/tmp, connect_timeout=10, network_timeout=30, tracker_server_count=2, storage_server_port=23000, group_name=group1, url_have_group_name=1, group_count=0, ...

1.7.5 Tracker 节点上的 Nginx 反向代理与统一入口

为什么需要统一入口? 直接把 Storage 的 IP 暴露给前端有几个问题:

  1. 前端需要知道每个 group 在哪些 IP 上 —— 耦合严重,加节点就要改前端。
  2. Storage 节点宕机后前端无法自动切换。
  3. 无法做统一的缓存、限流、防盗链、HTTPS 卸载。
  4. 多 group 时,前端需要根据 file_id 里的 group 名去找对应的机器 —— 逻辑不该在前端。

解决方案:在 Tracker 节点(或独立的接入层)上用 Nginx 做反向代理 + upstream 负载均衡,对外只暴露一个域名/VIP。

在 Tracker 节点(192.168.1.220 / 192.168.1.221)上配置:

vim /etc/nginx/conf.d/fastdfs.conf
# ===== 定义各 group 的后端 Storage 集群 =====
upstream fdfs_group1 {
    server 192.168.1.220:8888 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.1.221:8888 weight=1 max_fails=2 fail_timeout=30s;
    keepalive 32;
    # 可选:按 URI 哈希,让同一文件固定落到同一后端,提升缓存命中率
    # hash $request_uri consistent;
}

upstream fdfs_group2 {
    server 192.168.1.222:8888 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.1.223:8888 weight=1 max_fails=2 fail_timeout=30s;
    keepalive 32;
}

# ===== 缓存区(可选但强烈推荐,能极大降低后端压力)=====
proxy_cache_path /var/cache/nginx/fastdfs
                 levels=1:2
                 keys_zone=fdfs_cache:200m
                 inactive=30d
                 max_size=20g;

server {
    listen       80;
    server_name  img.example.com;

    access_log  /var/log/nginx/fastdfs_access.log;
    error_log   /var/log/nginx/fastdfs_error.log;

    # ===== 按 group 名分流到不同的 upstream(多 group 场景的核心)=====
    location /group1/M00 {
        proxy_pass http://fdfs_group1;
        include /etc/nginx/proxy_fastdfs.conf;
    }

    location /group2/M00 {
        proxy_pass http://fdfs_group2;
        include /etc/nginx/proxy_fastdfs.conf;
    }

    # ===== 或者用正则统一匹配(单 group 或 group 数固定时更简洁)=====
    # location ~ ^/group([0-9]+)/M00 {
    #     proxy_pass http://fdfs_group$1;      # 注意:变量形式的 proxy_pass 需要 resolver
    #     include /etc/nginx/proxy_fastdfs.conf;
    # }

    location = /healthz {
        return 200 "ok\n";
        access_log off;
    }
}

公共的代理参数 /etc/nginx/proxy_fastdfs.conf:

proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_http_version 1.1;
proxy_set_header Connection "";

proxy_connect_timeout   10s;
proxy_send_timeout      60s;
proxy_read_timeout      60s;

proxy_buffering          on;
proxy_buffer_size        16k;
proxy_buffers           16 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 128k;
proxy_max_temp_file_size 256m;

# 后端故障时自动切换到下一台(配合 upstream 的 max_fails)
proxy_next_upstream error timeout http_500 http_502 http_503 http_504 http_404;

# 缓存配置
proxy_cache            fdfs_cache;
proxy_cache_valid      200 302  30d;
proxy_cache_valid      404       1m;
proxy_cache_key        $host$uri;
proxy_cache_use_stale  error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock       on;
add_header X-Cache-Status $upstream_cache_status;

# 静态资源缓存头
expires 30d;
add_header Cache-Control "public, max-age=2592000";

创建缓存目录并启动:

mkdir -p /var/cache/nginx/fastdfs
chown -R nginx:nginx /var/cache/nginx
nginx -t
systemctl reload nginx

# 验证统一入口
curl -I http://192.168.1.220/group1/M00/00/00/wKgB3Fyo...
curl -I -H 'Host: img.example.com' http://192.168.1.220/group1/M00/00/00/wKgB3Fyo...
# 观察 X-Cache-Status 头:MISS -> HIT 说明缓存生效

Tracker 层自身的高可用:两台 Tracker 上的 Nginx 配置完全相同,前面再用 keepalived 做 VIP 或用 LVS/HAProxy/云 SLB 做四层负载,即可消除接入层单点。

# keepalived 简要示例(在两台 tracker 上)
yum -y install keepalived
# 配置 VIP 192.168.1.200,MASTER/BACKUP 各一台,检查脚本检测 nginx 进程
# 前端统一访问 http://192.168.1.200/group1/M00/...

多 group 场景的完整访问链路:

浏览器 http://img.example.com/group2/M00/00/00/xxx
   |
   v
VIP 192.168.1.200(keepalived)
   |
   v
Nginx(tracker1 或 tracker2) —— location /group2/M00 匹配
   |
   v  proxy_pass http://fdfs_group2(负载均衡到 222 或 223)
   |
   v
Nginx(storage 192.168.1.222:8888) —— location ~/group[0-9]+/M00
   |
   v  ngx_fastdfs_module
   |
   +-- 本地 /data/fastdfs1/data/00/00/xxx 存在 --> 直接返回
   +-- 不存在 --> 解析 file_id 得到源 IP --> proxy 到源 storage 取回

详见 10-02-Nginx服务与优化.md

1.8 扩容与运维

1.8.1 在组内增加 Storage(提升冗余与读性能)

目的:增加副本数、提升读并发能力。注意:不增加容量!(同组内容相同,容量取最小值)

步骤:

# 在新机器 storage3(192.168.1.222)上:
# 1. 完成所有环境准备(关防火墙/放行端口、关 SELinux、主机名、hosts、时间同步、数据盘)
# 2. 安装 libfastcommon + fastdfs(版本必须与现有节点一致!)
# 3. 创建目录
mkdir -p /home/dfs /data/fastdfs1

# 4. 配置 storage.conf —— 关键是 group_name 要与目标组相同
vim /etc/fdfs/storage.conf
group_name = group1                        # 与要加入的组相同
port = 23000                               # 与同组其它节点相同
base_path = /home/dfs
store_path_count = 1
store_path0 = /data/fastdfs1               # 与同组其它节点【路径最好也一致】,便于运维
subdir_count_per_path = 256                # 必须与同组一致
tracker_server = 192.168.1.220:22122
tracker_server = 192.168.1.221:22122
http.server_port = 8888
# 5. 启动
systemctl start fdfs_storaged

# 6. 观察状态变化:INIT -> WAIT_SYNC -> SYNCING -> ACTIVE
watch -n 5 "fdfs_monitor /etc/fdfs/storage.conf 2>/dev/null | grep -E 'ip_addr|last_synced'"

# 7. 观察全量同步进度
tail -f /home/dfs/logs/storaged.log
cat /home/dfs/data/sync/*.mark            # 看 need_sync_old / sync_old_done
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'total_sync_in_bytes|source storage id'

# 8. 同步完成(状态变 ACTIVE)后,部署 Nginx + fastdfs-nginx-module
# 9. 把新节点加入 Tracker 侧 Nginx 的 upstream
vim /etc/nginx/conf.d/fastdfs.conf         # 在 upstream fdfs_group1 中加一行
nginx -t && systemctl reload nginx

重要:新节点的状态在同步完成前是 SYNCING,此时 Tracker 不会把上传请求调度给它(避免它成为源),但同步完成变 ACTIVE 后就会参与上传与下载。全量同步时间取决于已有数据量与网络带宽,TB 级数据可能需要数小时到数天。

1.8.2 增加新 group(实现容量扩展)

这是 FastDFS 唯一的容量扩容方式。

# 在两台新机器 storage3(222)、storage4(223) 上分别配置:
vim /etc/fdfs/storage.conf
group_name = group2                        # 【新的组名】
port = 23000
base_path = /home/dfs
store_path_count = 1
store_path0 = /data/fastdfs1
tracker_server = 192.168.1.220:22122
tracker_server = 192.168.1.221:22122
http.server_port = 8888
# 启动
systemctl start fdfs_storaged

# 验证:fdfs_monitor 中应出现 group count: 2
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'group count|group name|active server count'
group count: 2
Group 1:
group name = group1
active server count = 2
Group 2:
group name = group2
active server count = 2

新组加入后的行为:

  • store_lookup = 0(轮询):新上传会在 group1 和 group2 之间轮流,两组均衡增长。但 group1 可能已经快满了,还在往里写,这时需要注意。
  • store_lookup = 2(选空间最大):新上传会全部涌向新的 group2(因为它空间最大),直到与 group1 持平。扩容后想快速缓解老组压力时可临时切成 2。
  • store_lookup = 1(指定组):配合 store_group = group2 强制只往新组写,最可控。
# 修改 tracker.conf 后需重启 tracker(两台都要改)
vim /etc/fdfs/tracker.conf     # store_lookup = 2 或 1 + store_group = group2
systemctl restart fdfs_trackerd

别忘了配置 Nginx:新 group 的 Storage 上要装 fastdfs-nginx-module(mod_fastdfs.conf 中 group_name = group2),Tracker 侧 Nginx 要加 upstream fdfs_group2 与 location /group2/M00(见 3.7.5)。

注意:老文件不会迁移到新组。 file_id 里的 group 名是固定的,group1 的文件永远在 group1。FastDFS 没有 rebalance 机制(这与 GlusterFS 不同),只能靠新写入来均衡。

1.8.3 单节点多 store_path 挂多盘

目的:在不加机器的前提下扩大单节点容量、提升单机 IOPS。

# 1. 挂载新盘
mkfs.xfs /dev/sdc1
mkdir -p /data/fastdfs2
mount -o noatime /dev/sdc1 /data/fastdfs2
echo '/dev/sdc1 /data/fastdfs2 xfs defaults,noatime 0 0' >> /etc/fstab

# 2. 修改 storage.conf
vim /etc/fdfs/storage.conf
store_path_count = 2                # 【必须同步改成 2】
store_path0 = /data/fastdfs1
store_path1 = /data/fastdfs2        # 对应 file_id 中的 M01
# 3. 同步修改 mod_fastdfs.conf(必须与 storage.conf 完全一致!)
vim /etc/fdfs/mod_fastdfs.conf
store_path_count = 2
store_path0 = /data/fastdfs1
store_path1 = /data/fastdfs2
# 4. 重启服务
systemctl restart fdfs_storaged
systemctl reload nginx

# 5. 验证:monitor 中 store path count 变为 2
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'store path count|current_write_path'

# 6. 上传若干文件,观察 file_id 中出现 M01
for i in $(seq 1 10); do fdfs_upload_file /etc/fdfs/client.conf /etc/hosts; done
# 应能看到 group1/M00/... 与 group1/M01/... 交替出现(store_path = 0 轮询)

关键要点:

  • 同组内所有节点的 store_path_count 与路径顺序应保持一致,否则同步时 M01 的文件在另一节点上找不到对应路径。
  • store_path_count 只能增加不能减少(减少会导致已有 M0N 文件无法访问)。
  • mod_fastdfs.conf 必须同步修改,否则 M01 的文件通过 HTTP 访问会 404。
  • Tracker 的 store_path = 0(轮询)或 2(选空间最大)决定新文件落到哪块盘。

1.8.4 Storage 节点下线与数据迁移

# 场景 A:组内有多个节点,下线其中一个(数据在其它节点上有副本,可安全下线)
# 1. 先从 Tracker 侧 Nginx 的 upstream 中摘除,停止新流量
vim /etc/nginx/conf.d/fastdfs.conf         # 注释掉该 server 行,或加 down 标记
# server 192.168.1.221:8888 down;
nginx -t && systemctl reload nginx

# 2. 确认组内其它节点数据完整(同步无延迟)
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'ip_addr|last_synced_timestamp'
# 所有节点的 delay 应为 0s

# 3. 停止该节点的 storage 服务
systemctl stop fdfs_storaged

# 4. 从 Tracker 的集群列表中删除该节点(否则会一直显示 OFFLINE)
fdfs_monitor /etc/fdfs/client.conf delete group1 192.168.1.221

# 5. 验证
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'storage server count|active server count'
# 场景 B:下线整个 group(该组是唯一持有这些文件的地方,必须先迁移数据!)
# FastDFS 没有内置的跨 group 迁移工具,只能应用层重传:
# 1. 从数据库中取出所有属于 group1 的 file_id
# 2. 逐个从 group1 下载,再上传到 group2(tracker.conf 设 store_lookup=1, store_group=group2)
# 3. 更新数据库中的 file_id
# 4. 全部验证无误后再下线 group1

# 迁移脚本示例思路
while read fid; do
    fdfs_download_file /etc/fdfs/client.conf "$fid" /tmp/migrate.tmp || { echo "FAIL DL $fid" >> /tmp/fail.log; continue; }
    newfid=$(fdfs_upload_file /etc/fdfs/client.conf /tmp/migrate.tmp)
    [ -n "$newfid" ] && echo "$fid $newfid" >> /tmp/mapping.txt || echo "FAIL UP $fid" >> /tmp/fail.log
done < /tmp/group1_file_ids.txt

这是 FastDFS 的重要短板:没有 rebalance、没有跨 group 数据迁移能力。因此规划阶段就要想好 group 的划分,避免后期大规模搬迁。

1.8.5 同步状态检查与 binlog

# 1. 从 monitor 看同步延迟(最直观)
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'ip_addr|last_source_update|last_sync_update|last_synced_timestamp'
# last_synced_timestamp 后面的 (Ns delay) 就是延迟秒数,正常应为 0s 或很小

# 2. 看 sync 目录下的 binlog 与 mark 文件
ls -lh /home/dfs/data/sync/
-rw-r--r-- 1 root root 1.2M Mar 13 16:20 binlog.000        # 本机操作日志
-rw-r--r-- 1 root root   16 Mar 13 16:20 binlog.index      # 当前 binlog 序号
-rw-r--r-- 1 root root  256 Mar 13 16:20 192.168.1.221_23000.mark   # 从 221 同步的进度
# 3. 查看 binlog 内容(末尾即最近的操作)
tail -20 /home/dfs/data/sync/binlog.000
1553836564 C M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
1553836601 c M00/00/00/wKgB3VyovlmAaBcRAAAD3EJmFwo1119203
1553836702 D M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

操作类型速查:C 源创建、c 同步创建、D 源删除、d 同步删除、A 源追加、a 同步追加、T 源截断、t 同步截断、U 源更新。大写 = 本机作为源的操作,小写 = 从别处同步来的操作。

# 4. 查看 mark 文件,确认同步进度
cat /home/dfs/data/sync/192.168.1.221_23000.mark
binlog_index=0
binlog_offset=1245678
need_sync_old=0
sync_old_done=1
until_timestamp=1553836702
scan_row_count=8900
sync_row_count=8900

判断标准:

现象 结论
binlog_offset 接近 binlog.000 的文件大小,且在持续增长 同步正常
binlog_offset 长时间不变,但 binlog 在增长 同步已停止,查网络与对端进程
need_sync_old=1 且 sync_old_done=0 正在做全量同步(新节点加入时正常)
scan_row_count 远大于 sync_row_count 大量记录被跳过,需查日志原因
last_synced_timestamp 的 delay 持续增大 同步跟不上写入速度,考虑调小 sync_interval、加带宽
# 5. 对比两个节点的文件数量(终极验证)
find /home/dfs/data -type f ! -name 'binlog*' ! -name '*.mark' | wc -l      # 在每个节点上执行
# 同组节点的数量应基本一致(允许有几秒延迟的差异)

# 6. 对比磁盘占用
du -sh /home/dfs/data/

.fdfs_storage 相关说明:某些版本会在 store_path 下生成 .fdfs_storage_mark 文件,用于记录该路径是否已被 FastDFS 初始化(防止误用他人目录)。不要手工删除它,否则 Storage 启动时会认为该目录未初始化而报错。

1.8.6 磁盘空间预留 reserved_storage_space

在 tracker.conf 中配置:

reserved_storage_space = 20%
# 也可写绝对值
# reserved_storage_space = 10G

作用:为操作系统与 FastDFS 自身(日志、binlog、临时文件)预留空间。当组内所有 Storage 的剩余空间都低于此阈值时,Tracker 会认为该组不可写,新上传请求得到:

[ERROR] file: tracker_proto.c, line: xxx, ... no available storage server

为什么必须预留:

  1. 磁盘 100% 写满会导致 binlog 无法写入 → 同步机制彻底失效,且已写的数据可能损坏。
  2. 日志无法写入,故障时无从排查。
  3. XFS/ext4 在接近满时性能急剧下降、可能出现文件系统错误。
  4. 系统盘写满会导致整机不可用。

推荐配置:

场景 建议值
小容量盘(< 500GB) 20%
大容量盘(几 TB 以上) 10% 或绝对值 50G(百分比会预留过多)
独立数据盘、有完善监控 10%

触发后的处理:

# 1. 确认是否真的是空间问题
df -h
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'disk total space|disk free space'

# 2. 短期缓解:清理无用文件、下调 reserved(临时,不推荐)
vim /etc/fdfs/tracker.conf     # reserved_storage_space = 10%
systemctl restart fdfs_trackerd

# 3. 正确做法:新增 group 扩容(见 3.8.2),或给节点加盘(见 3.8.3)

1.8.7 备份策略

核心认知:FastDFS 的同组多副本不等于备份!

风险 同组多副本能否防护
单块磁盘损坏 能(其它节点有副本)
单台服务器宕机 能
误删文件(fdfs_delete_file 或应用 bug) 不能!删除操作会通过 binlog 同步到组内所有节点,所有副本一起消失
误删数据目录(rm -rf) 只删一个节点能恢复;脚本批量误删所有节点则不能
机房级灾难(火灾、断电、水淹) 不能(同组通常在同一机房)
勒索病毒/入侵 不能
逻辑错误(业务写坏了内容) 不能

备份方案:

# 方案 1:rsync 定期同步到独立的备份服务器(最简单实用)
rsync -avz --delete --exclude='sync/' \
  192.168.1.220:/data/fastdfs1/data/ /backup/fastdfs/group1/
# 加入 crontab,每天凌晨执行
echo '0 2 * * * /usr/bin/rsync -az --exclude=sync/ 192.168.1.220:/data/fastdfs1/data/ /backup/fastdfs/group1/ >> /var/log/fdfs_backup.log 2>&1' >> /var/spool/cron/root

# 方案 2:应用层双写到对象存储(推荐,异地容灾)
# 上传时同时写 FastDFS 和 OSS/S3/MinIO,数据库记录两个地址

# 方案 3:底层存储做快照(LVM 快照 / 云盘快照)
lvcreate -L 100G -s -n fdfs_snap /dev/vg_data/lv_fastdfs1

# 方案 4:备份配置与元信息(必做,成本极低)
tar czf /backup/fdfs_conf_$(date +%F).tar.gz /etc/fdfs/ /etc/nginx/
# 数据库中的 file_id 映射表必须一起备份!否则有文件也找不回来
mysqldump -uroot -p mydb file_mapping > /backup/file_mapping_$(date +%F).sql

特别强调:数据库中的 原始文件名 -> file_id 映射表和文件本体同等重要。丢了映射表,磁盘上的文件就成了一堆无法索引的乱码文件名,等于全部丢失。这个映射表必须与业务数据库一起做完整的备份与恢复演练。

1.8.8 监控要点

监控项 采集方式 告警阈值/条件
Storage 状态是否 ACTIVE fdfs_monitor ... \| grep ip_addr 出现非 ACTIVE 立即告警(P0)
active server count 同上 小于 storage server count 时告警
同步延迟 last_synced_timestamp 的 delay fdfs_monitor 解析 > 60s 告警
磁盘剩余空间 df 或 monitor 的 disk free space < reserved_storage_space + 10% 时告警
inode 剩余 df -i 使用率 > 80% 告警(小文件场景极易先耗尽 inode)
Tracker 进程与端口 22122 systemctl is-active / nc -z 不存活告警
Storage 进程与端口 23000 同上 不存活告警
Nginx 进程与端口 8888/80 同上 不存活告警
上传成功率 total_upload_count 与 success_upload_count 的差值增长 差值增长告警
下载成功率 / 404 率 Nginx access_log 中的 4xx/5xx 比例 404 率突增告警(可能是同步问题或 mod_fastdfs 配错)
上传/下载耗时 Nginx $request_time P99 超阈值告警
binlog 增长速率 binlog.000 文件大小变化 异常暴增可能是死循环写入
日志中的 ERROR grep ERROR $base_path/logs/*.log 出现即告警
时间偏差 chronyc tracking / ntpq -p 偏差 > 1s 告警(影响同步!)

监控脚本示例:

#!/bin/bash
# /usr/local/bin/check_fastdfs.sh
CONF=/etc/fdfs/client.conf
OUT=$(fdfs_monitor $CONF 2>/dev/null)

# 检查是否有非 ACTIVE 的节点
BAD=$(echo "$OUT" | grep 'ip_addr' | grep -v 'ACTIVE')
if [ -n "$BAD" ]; then
    echo "CRITICAL: non-active storage found:"; echo "$BAD"; exit 2
fi

# 检查同步延迟
DELAY=$(echo "$OUT" | grep -oP '\(\K[0-9]+(?=s delay\))' | sort -rn | head -1)
if [ -n "$DELAY" ] && [ "$DELAY" -gt 60 ]; then
    echo "WARNING: sync delay ${DELAY}s"; exit 1
fi

echo "OK: all storage ACTIVE, max sync delay ${DELAY:-0}s"; exit 0

1.9 性能与容量规划

1.9.1 小文件存储的优势原理

FastDFS 为什么在小文件上比通用分布式文件系统快?

优化点 原理
无元数据查询 位置信息编码在 file_id 里,下载时直接算出物理路径。GlusterFS 需要哈希计算 + 可能的 linkto 跳转 + LOOKUP 广播;HDFS 需要问 NameNode
一次网络往返完成上传 客户端问一次 Tracker(可缓存),然后直连 Storage 一次传完。没有多副本同步等待(异步)
不实现 POSIX 没有 open/seek/write/lock 的语义开销,没有目录树维护,没有 inode 分配的复杂性
两级目录预创建 65536 个目录预先建好,文件均匀散列,单目录文件数可控,底层文件系统的目录查找始终高效
异步同步 上传立即返回,不等副本落盘,写延迟等于单机写延迟
C 语言实现,极致轻量 单进程内存占用几十 MB,无 JVM 开销(对比 HDFS)
顺序写盘 文件一次性写入,不做随机改写,对 HDD 友好

1.9.2 subdir_count_per_path 与容量上限估算

subdir_count_per_path = 256

含义:在每个 store_path 下创建 两级目录,每级 256 个(00 ~ FF),共 256 × 256 = 65536 个末级目录。

/data/fastdfs1/data/
├── 00/
│   ├── 00/     ← 文件存在这一层
│   ├── 01/
│   ├── ...
│   └── FF/
├── 01/
├── ...
└── FF/

容量上限估算:

设单个末级目录建议存放的文件数上限为 N(受底层文件系统的目录查找性能限制,XFS 上建议 N ≤ 10000,超过后 readdir/lookup 明显变慢):

单个 store_path 可容纳的文件数 = 65536 × N

N = 1000  ->  6553.6 万个文件(约 6500 万)
N = 5000  ->  3.28 亿个文件
N = 10000 ->  6.55 亿个文件
N = 30000 ->  19.7 亿个文件(性能已明显下降,不推荐)

容量估算示例:

平均文件大小 单目录 5000 文件(3.28 亿个) 需要的磁盘空间
10KB 3.28 亿 约 3.3 TB
100KB 3.28 亿 约 33 TB(单盘装不下,需多 store_path 或多 group)
1MB 3.28 亿 约 328 TB(必须多 group)

实际约束往往先来自磁盘空间和 inode,而非目录数:

# 检查 inode 是否够用(小文件场景的隐形杀手)
df -i /data/fastdfs1
# 一个文件消耗一个 inode。若 inode 用尽,即使还有空间也无法创建文件
# XFS 是动态分配 inode 的,风险较小;ext4 在 mkfs 时固定,必须提前规划:
# mkfs.ext4 -i 4096 /dev/sdb1      # 每 4KB 一个 inode(适合海量小文件)
# mkfs.ext4 -N 200000000 /dev/sdb1 # 直接指定 inode 数量

调整建议:

  • subdir_count_per_path 有数据后绝对不能修改(改了文件路径就对不上,全部 404)。必须在初次部署时就规划好。
  • 默认 256(65536 目录)对大多数场景(亿级文件)已经足够。
  • 文件数预计超过 5 亿的单 store_path,考虑改用多 store_path 或多 group 分摊,而不是加大 subdir_count。
  • 加大 subdir_count 会导致启动时创建/扫描大量目录,启动变慢。
  • 更实际的做法是用 group 做水平扩展,每个 group 承载可控的文件数量。

1.9.3 合并存储 trunk 的适用场景

适用条件(三者需同时满足):

  1. 文件非常小(平均 < 16KB,如缩略图、图标、小 JSON);
  2. 文件数量极大(亿级以上);
  3. 文件很少删除(trunk 的空间回收不完善,删除多会产生碎片)。

配置(storage.conf 与 tracker.conf 都要配,见 3.2.5):

use_trunk_file = true
slot_min_size = 256
slot_max_size = 16KB
trunk_file_size = 64MB
trunk_create_file_advance = true
trunk_create_file_time_base = 02:00
trunk_create_file_interval = 86400
trunk_create_file_space_threshold = 20G
trunk_init_check_occupying = true

收益:

  • inode 消耗从"每文件一个"降到"每 64MB 一个",节省 99.99%;
  • 消除文件系统块对齐造成的空间浪费(4KB 块存 1KB 文件浪费 75%);
  • 减少元数据操作,创建文件更快。

代价与风险:

  • 引入 trunk server(由 Tracker 指定一台 Storage 统一分配 trunk 空间),成为新的中心节点与单点;
  • 删除文件后的空间回收不完善,长期运行会产生碎片,且没有官方的碎片整理工具;
  • appender 文件不支持 trunk;
  • 出问题时排查难度显著上升(文件"不在"独立的路径上);
  • 社区实践较少,坑较多。

结论:除非确实遇到 inode 耗尽或空间浪费严重的实际问题,否则保持 use_trunk_file = false。现代方案更倾向于在应用层做小文件合并,或直接使用对象存储。

1.9.4 缓存与 CDN 前置

FastDFS 存的是"一次写入多次读取"的静态资源,读多写少的特征使缓存收益极高。

用户 --> CDN 边缘节点(命中率 90%+)
           |  未命中
           v
        Nginx 反代 + proxy_cache(本地磁盘缓存,命中率再吃掉一部分)
           |  未命中
           v
        Storage 上的 Nginx + mod_fastdfs
           |
           v
        磁盘

分层缓存配置要点:

# 1. Storage/Tracker 侧 Nginx 加长缓存头,让 CDN 与浏览器都能缓存
location ~/group[0-9]+/M00 {
    ngx_fastdfs_module;
    expires 365d;                                     # 文件内容不可变,可设很长
    add_header Cache-Control "public, immutable, max-age=31536000";
}
# 2. Tracker 侧反代加本地磁盘缓存(见 3.7.5 的 proxy_cache 配置)
proxy_cache fdfs_cache;
proxy_cache_valid 200 302 30d;
proxy_cache_lock on;                                  # 防止缓存击穿
proxy_cache_use_stale error timeout updating;          # 后端故障时用过期缓存兜底

为什么 FastDFS 特别适合配 CDN:

  • file_id 一经生成永不改变,且文件内容不可修改,因此 URL 与内容一一对应,可以设置极长的缓存时间且永不需要刷新(这是最理想的 CDN 场景)。
  • 删除文件后需要主动刷新 CDN 缓存(或依赖 TTL 过期)。
  • 建议前端 URL 使用独立域名(如 img.example.com),便于 CDN 接入与 Cookie 隔离。

其它优化:

  • 图片处理(缩略图、水印)应在上传时预生成多个尺寸分别存储,或在 CDN/Nginx 层用 image_filter 模块实时处理,不要每次回源到 Storage 计算。
  • 使用 FastDFS 的 slave 文件机制存放同一主文件的不同尺寸(fdfs_test upload_slave),file_id 形如 xxx_120x120.jpg。
  • 开启 Nginx 的 sendfile on 与 tcp_nopush on,零拷贝发送静态文件。
  • 开启 gzip(对 JSON/文本有效,对已压缩的 JPEG/PNG 无意义)。

1.9.5 与对象存储的取舍

维度 FastDFS 自建 MinIO 自建 云对象存储(OSS/S3/COS)
接口标准 私有协议 + HTTP S3 兼容(生态巨大) S3 兼容
部署运维成本 低(但需自己搞定 Nginx、监控、备份) 低到中 零(全托管)
硬件成本 需自购服务器与磁盘 同 FastDFS 按量付费,小规模更便宜,超大规模可能更贵
冗余机制 同组多副本(2~3 倍空间) 纠删码(利用率 ~75%) 多可用区多副本/EC(对用户透明)
扩容 只能加 group,无 rebalance 加节点/加盘,支持自动均衡 无感知,无上限
元数据能力 几乎没有(需自建数据库存映射) 完整(对象自定义元数据、标签、版本) 完整 + 生命周期、跨区复制
版本控制 无 有 有
权限控制 无(靠 Nginx/应用层做) 有(IAM、Bucket Policy、预签名 URL) 完善(RAM、STS、防盗链、鉴权)
生命周期管理 无 有 有(自动转冷/归档/删除,大幅省钱)
CDN 集成 需自己接 需自己接 原生集成,一键开启
社区活跃度 低(近年更新极少) 高 商业支持
图片处理等增值能力 无 无 有(实时缩放、水印、格式转换、AI 识别)
数据可靠性承诺 自己负责 自己负责 SLA 保证(如 11 个 9)
适用规模 中小规模(几十 TB 内) 中大规模 任意规模

选型建议:

  1. 新项目、能上云:直接用云对象存储(OSS/S3/COS)。省掉全部运维,还自带 CDN、图片处理、生命周期。这是 2020 年后的默认答案。
  2. 必须私有化部署、需要 S3 兼容:选 MinIO。生态好、纠删码省空间、K8s 友好、社区活跃。
  3. 已有 FastDFS 集群且运行稳定:可以继续用,不必强行迁移。但不要在新项目里引入 FastDFS。
  4. 必须私有化且规模巨大、需要块+文件+对象统一:选 Ceph。
  5. 需要像本地目录一样挂载:选 GlusterFS 或 CephFS(对象存储做不到)。

FastDFS 的现状(客观评价):项目从 2008 年开始,在国内互联网(尤其是 2012-2018 年的电商、社交)有大量生产应用,稳定性经过验证,是很好的学习材料。但近年社区活跃度大幅下降、更新极少、缺少版本管理/权限/S3 接口等现代特性、没有 rebalance 能力、生态工具匮乏。在云原生与对象存储成为标准的今天,新项目已不推荐选用。学习它的价值在于理解"如何用最简单的设计解决海量小文件问题"这一经典架构思路。

1.10 FastDFS 排错清单

1.10.1 Storage 状态不是 ACTIVE

fdfs_monitor /etc/fdfs/storage.conf | grep -E 'ip_addr|status'

按状态定位:

状态 含义 处理
FAKE_INIT (0) 刚被 Tracker 记录,还未真正连上 检查 Storage 进程是否启动、端口是否可达
INIT (1) 已连上 Tracker,尚未确定同步源 稍等;组内首个节点会直接变 ACTIVE
WAIT_SYNC (2) 等待同步 检查组内是否有 ACTIVE 的源节点;源节点挂了会一直卡住
SYNCING (3) 正在同步存量数据 正常,等待完成;看 storaged.log 与 mark 文件进度
DELETED (4) 已被从集群删除 被 fdfs_monitor delete 删过;重启 Storage 会重新注册
OFFLINE (5) 离线 进程已停或网络不通
ONLINE (6) 在线但不可用(未完成初始化) 等待或查日志
ACTIVE (7) 正常可服务 目标状态
# 通用排查顺序
systemctl status fdfs_storaged                       # CentOS 7
service fdfs_storaged status                         # CentOS 6
ps -ef | grep fdfs_storaged
netstat -tnlp | grep 23000                           # 或 ss -tnlp | grep 23000
tail -100 /home/dfs/logs/storaged.log                # $base_path/logs/
# 从任一 Tracker 上测试到 Storage 的连通性
telnet 192.168.1.221 23000

长期卡在 WAIT_SYNC 的典型原因:组内唯一的其它节点不是 ACTIVE(互相等待),或两个节点 group_name 拼写不一致导致各自成了独立组(看 group count 是不是变成了 2)。

1.10.2 Storage 连不上 Tracker

# 症状:storaged.log 中反复出现
# ERROR - file: tracker_client_thread.c, line: xxx, connect to tracker server
# 192.168.1.220:22122 fail, errno: 110, error info: Connection timed out

逐项检查:

# 1. Tracker 进程与端口
systemctl status fdfs_trackerd
netstat -tnlp | grep 22122

# 2. bind_addr 配置错误(最常见的坑!)
grep bind_addr /etc/fdfs/tracker.conf
# 如果 bind_addr 被设成了 127.0.0.1,则只监听本地回环,远程绝对连不上!
# 正确做法:留空(监听所有网卡),或填本机的对外 IP
# bind_addr =

# 3. Storage 侧的 tracker_server 地址写错
grep tracker_server /etc/fdfs/storage.conf
# 必须是【IP:端口】格式,不能只写 IP;不要写 127.0.0.1(Tracker 在别的机器上)
# 主机名要保证能解析(建议直接写 IP)

# 4. 防火墙
systemctl status firewalld                            # CentOS 7
firewall-cmd --list-ports
firewall-cmd --permanent --add-port=22122/tcp && firewall-cmd --reload
iptables -L -n | grep 22122                           # CentOS 6
iptables -I INPUT -p tcp --dport 22122 -j ACCEPT && service iptables save

# 5. allow_hosts 白名单(tracker.conf)
grep allow_hosts /etc/fdfs/tracker.conf
# 默认 allow_hosts = *(全部允许);若被改成具体 IP 段,Storage 的 IP 必须在内

# 6. SELinux
getenforce                                            # 应为 Disabled 或 Permissive

# 7. 网络连通性(分层验证)
ping 192.168.1.220
telnet 192.168.1.220 22122
nc -zv 192.168.1.220 22122

# 8. 两台机器的 fastdfs 版本是否一致(协议不兼容会连上后立即断开)
fdfs_trackerd -V
fdfs_storaged -V

1.10.3 上传失败:connect timed out / no available storage server

fdfs_upload_file /etc/fdfs/client.conf /etc/hosts
报错 原因与处理
connect to tracker server ... fail client.conf 中的 tracker_server 写错,或 Tracker 未启动、端口不通(同 3.10.2)
no available storage server ① 组内没有 ACTIVE 的 Storage;② 磁盘剩余空间低于 reserved_storage_space(见 3.8.6);③ store_lookup=1 但 store_group 指定了一个不存在的组名(常见!如源文档中 store_group=group2 而实际只有 group1)
connect to storage server ... timed out Tracker 返回的 Storage IP 客户端访问不到:Storage 的 23000 端口未放行;或 Storage 配了内网 IP 而客户端在外网(需正确配置 Storage 的 bind_addr/网络)
errno: 28, No space left on device 磁盘真的满了
errno: 13, Permission denied store_path 目录权限不足(检查 run_by_user/run_by_group 与目录 owner)
errno: 2, No such file or directory base_path 或 store_path 目录不存在(见 3.10.4)
上传成功但返回的 group 名不对 store_lookup/store_group 配置问题
# 快速定位三连
fdfs_monitor /etc/fdfs/client.conf | grep -E 'group name|active server count|ip_addr|disk free space'
df -h /data/fastdfs1
grep -E 'store_lookup|store_group|reserved_storage_space' /etc/fdfs/tracker.conf

1.10.4 启动失败:base_path / store_path 不存在或权限不对

FastDFS 不会自动创建这些目录(这是新手最常踩的坑)。

# 症状:启动瞬间退出,日志中
# ERROR - file: shared_func.c, line: xxx, "/home/dfs" can't be accessed, error info: No such file or directory
# 或
# [ERROR] file: storage_func.c, line: xxx, mkdir "/data/fastdfs1/data/00" fail

# 解决:手工创建全部目录
mkdir -p /home/dfs                 # base_path(tracker 与 storage 可分开)
mkdir -p /data/fastdfs1            # store_path0
# 多 store_path 时每个都要建
mkdir -p /data/fastdfs2

# 检查权限(若配置了 run_by_user/run_by_group)
grep -E 'run_by_user|run_by_group' /etc/fdfs/storage.conf
chown -R fastdfs:fastdfs /home/dfs /data/fastdfs1    # 若以 fastdfs 用户运行
chmod 755 /home/dfs /data/fastdfs1

# 检查配置里的路径拼写(不要有多余空格、不要用 ~ 、必须绝对路径)
grep -nE '^base_path|^store_path' /etc/fdfs/tracker.conf /etc/fdfs/storage.conf

# 检查 SELinux 上下文(若未关闭 SELinux)
ls -Zd /data/fastdfs1
restorecon -Rv /data/fastdfs1

首次启动成功的标志:store_path0 下自动生成了 data/00/00 ~ data/FF/FF 共 65536 个目录,以及 data/sync/、data/.data_init_flag。

ls /data/fastdfs1/data/ | head
find /data/fastdfs1/data -maxdepth 2 -type d | wc -l    # 应为 65536 + 256 + 1 附近

1.10.5 同组文件不同步

# 1. 先确认双方状态与延迟
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'ip_addr|last_synced_timestamp'

# 2. 检查 sync_interval / sync_start_time / sync_end_time
grep -E 'sync_interval|sync_start_time|sync_end_time|sync_wait_msec' /etc/fdfs/storage.conf
sync_start_time = 00:00       # 允许同步的时间段起点
sync_end_time = 23:59         # 终点 —— 若被限制成 01:00-05:00,白天就【完全不同步】!
sync_interval = 0             # 同步线程每次同步后休息的毫秒数,0 = 不休息(最快)
sync_wait_msec = 50           # 没有新 binlog 时的等待毫秒数
# 3. 【时间不同步】是最隐蔽的原因
#    binlog 用时间戳判断同步位点,两节点时间偏差过大会导致记录被跳过或重复
date                                                  # 各节点对比
chronyc tracking                                      # CentOS 7
ntpq -p                                               # CentOS 6
systemctl restart chronyd && chronyc makestep         # 强制立即校时

详见 08-03-时间同步与Xinetd.md

# 4. 检查 binlog 是否在增长
ls -l --time-style=full-iso /home/dfs/data/sync/
watch -n 2 'ls -l /home/dfs/data/sync/binlog.000'
tail -f /home/dfs/data/sync/binlog.000

# 5. 检查 mark 文件的 binlog_offset 是否在推进(见 3.8.5 的判断表)
cat /home/dfs/data/sync/192.168.1.221_23000.mark

# 6. 检查 Storage 之间的 23000 端口互通(同步是 Storage 直连 Storage,不经过 Tracker!)
telnet 192.168.1.221 23000                            # 从 220 上执行
telnet 192.168.1.220 23000                            # 从 221 上执行
# 【常见错误】只在 Tracker 上放行了端口,忘记放行 Storage 之间的 23000

# 7. 查日志中的同步错误
grep -iE 'sync|error' /home/dfs/logs/storaged.log | tail -50

# 8. 检查 group_name 是否真的一致(不一致会变成两个独立组,各自都"正常"但互不同步)
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'group count|group name'
grep group_name /etc/fdfs/storage.conf                # 在每个节点上比对

# 9. 终极手段:重置同步(谨慎!会触发全量重扫)
systemctl stop fdfs_storaged
mv /home/dfs/data/sync/192.168.1.221_23000.mark{,.bak}
systemctl start fdfs_storaged

1.10.6 Nginx 访问 404

这是集成阶段最高频的问题,按下面顺序逐项核对:

# 1. 先绕过 Nginx,确认文件在磁盘上真的存在
fdfs_file_info /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
ls -l /data/fastdfs1/data/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 若磁盘上没有 -> 是同步问题(见 3.10.5)或文件已被删除,不是 Nginx 问题
# 2. 核对 url_have_group_name(最常见原因!)
grep url_have_group_name /etc/fdfs/mod_fastdfs.conf
url_have_group_name = true     # URL 中【带】group 名:http://ip:8888/group1/M00/00/00/xxx
# url_have_group_name = false  # URL 中【不带】group 名:http://ip:8888/M00/00/00/xxx

规则:URL 里带 group1/ 就必须设 true,不带就设 false。设错必 404。多 group 场景下必须设为 true(否则 Tracker 侧无法按 group 路由)。

# 3. 核对 group_name 与 group_count
grep -E 'group_name|group_count' /etc/fdfs/mod_fastdfs.conf
# 单 group:group_count = 0,group_name = group1(与 storage.conf 一致)
# 多 group 共用一份配置:group_count = 2,并配置 [group1]/[group2] 段

# 4. 核对 store_path 是否与 storage.conf 完全一致(顺序也要一致!)
grep -E 'store_path' /etc/fdfs/mod_fastdfs.conf /etc/fdfs/storage.conf
# mod_fastdfs 靠这个把 M00 映射到 /data/fastdfs1,写错就找不到文件

# 5. 核对 M00 软链接(当 store_path 不是 base_path 时必需)
ls -l /data/fastdfs1/data/M00
# 应为:M00 -> /data/fastdfs1/data
ln -s /data/fastdfs1/data /data/fastdfs1/data/M00     # 若缺失则创建

# 6. 核对 Nginx 的 location 正则是否匹配
grep -A3 'M00' /etc/nginx/conf.d/*.conf /etc/nginx/nginx.conf
# location ~/group[0-9]+/M00 { ngx_fastdfs_module; }
# 若 url_have_group_name=false,则应为 location ~/M00

# 7. 确认模块真的编译进去了
nginx -V 2>&1 | tr ' ' '\n' | grep fastdfs
# 应看到 --add-module=/usr/local/src/fastdfs-nginx-module/src

# 8. 确认 mod_fastdfs.conf 被正确加载(Nginx 启动日志)
tail -50 /var/log/nginx/error.log
grep -i fastdfs /var/log/nginx/error.log
# 正常启动会有:ngx_http_fastdfs_process_init pid=xxx

# 9. 确认 mod_fastdfs.conf 的 tracker_server 可达
grep tracker_server /etc/fdfs/mod_fastdfs.conf
# 模块要向 Tracker 查询文件所在节点,这里配错会导致跨节点回源失败

# 10. 检查 mod_fastdfs 自己的日志
grep -E 'base_path|log_level' /etc/fdfs/mod_fastdfs.conf
tail -50 /tmp/mod_fastdfs.log          # 或 $base_path/logs/mod_fastdfs.log

# 11. 目录权限:Nginx 的运行用户要能读 store_path
ps -ef | grep 'nginx: worker'
namei -l /data/fastdfs1/data/00/00/    # 逐级检查路径上的权限

404 原因速查表:

现象 最可能的原因
所有文件都 404 url_have_group_name 设错,或 location 正则不匹配,或模块没编进去
刚上传的文件在某一个节点上 404,另一节点正常 同步延迟(正常,模块会回源代理;若持续 404 则是模块的 tracker_server 配错,回源失败)
只有 M01 的文件 404 mod_fastdfs.conf 里漏配 store_path1/store_path_count
只有 group2 的文件 404 多 group 配置(group_count、[group2] 段)或 Tracker 侧 Nginx 缺 location /group2/M00
返回 403 而非 404 目录权限问题,或 Nginx 的 autoindex/deny 规则
返回 500 模块与 Nginx 版本不兼容,或 mod_fastdfs.conf 语法/路径错误

1.10.7 磁盘满与空间预留

df -h; df -i                                          # 空间与 inode 都要看
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'disk total space|disk free space'
grep reserved_storage_space /etc/fdfs/tracker.conf

详细处理见 3.8.6。紧急止血:把 store_lookup 改为 1 + store_group 指向有空间的组,或立刻新增 group。切勿把 reserved_storage_space 调到 0 来"解决"问题。

1.10.8 file_id 解析与 fdfs_file_info

fdfs_file_info /etc/fdfs/client.conf group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
source storage id: 0
source ip address: 192.168.1.220
file create timestamp: 2019-03-13 16:16:17
file size: 988
file crc32: 790822154 (0x2F217D0A)

用途:

  • 确认文件真实存在于集群中(比直接 ls 更权威,它会向 Tracker/Storage 查询);
  • 定位文件的源节点(source ip address),便于去对应机器上查日志/查磁盘;
  • 核对创建时间与大小,判断是否是期望的那个文件;
  • 校验 CRC32,配合 fdfs_crc32 比对本地文件,确认内容未损坏。
# 本地计算 CRC32 与之比对
fdfs_crc32 /path/to/local/file

# 物理路径推算:group1/M00/00/00/xxx
#   group1 -> 该组任一 Storage
#   M00    -> store_path0(即 /data/fastdfs1/data)
#   00/00  -> 两级目录
# => /data/fastdfs1/data/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

1.10.9 日志位置与查看

日志 路径 内容
Tracker 日志 $base_path/logs/trackerd.log(如 /home/dfs/logs/trackerd.log) 启动、Storage 注册与状态变更、调度选组
Storage 日志 $base_path/logs/storaged.log 启动、连接 Tracker、上传下载、同步过程
Storage 出错日志 $base_path/logs/storaged.log(rotate_error_log 控制切割) 同上
客户端日志 client.conf 的 base_path 下 logs/ 命令行工具的输出
mod_fastdfs 日志 mod_fastdfs.conf 的 base_path 下 logs/mod_fastdfs.log(常见 /tmp/mod_fastdfs.log) HTTP 请求处理、回源代理
Nginx 日志 /var/log/nginx/{access,error}.log HTTP 状态码、404 详情
# 注意:源文档写的 /etc/fdfs/logs/ 是不准确的(除非 base_path 恰好设成 /etc/fdfs)
# 正确定位方式:先看配置里的 base_path
grep '^base_path' /etc/fdfs/tracker.conf /etc/fdfs/storage.conf
tail -f $(grep '^base_path' /etc/fdfs/storage.conf | cut -d= -f2 | tr -d ' ')/logs/storaged.log

# 提高日志级别以便排查(排查完记得改回 info,debug 会产生海量日志)
vim /etc/fdfs/storage.conf     # log_level = debug
systemctl restart fdfs_storaged

# 快速筛错
grep -iE 'error|fail|warn' /home/dfs/logs/storaged.log | tail -50

1.10.10 重装或修改 group_name 后数据不可访问

这是最容易造成"数据丢失"错觉的坑。

误操作 后果 处理
修改 group_name 原有 file_id 里的组名(group1)在集群中不再存在 → 全部文件无法访问;同时该节点变成一个"新组"的成员 改回原来的 group_name 并重启,即可恢复。生产环境绝不要改 group_name
修改 store_path0 的路径 M00 映射到了新的空目录 → 全部 404 改回原路径,或把老数据移动到新路径下(连同目录结构)
修改 subdir_count_per_path 两级目录数量变了,老文件的路径算不出来 → 全部 404 改回原值。有数据后绝对不能改
修改 port(23000) Tracker 会把它当成一台新的 Storage(Storage 的唯一标识是 IP:port) 改回原端口
重装系统但保留数据盘 base_path 下的 data/sync/*.mark、.data_init_flag 丢失 → 被当作新节点,触发全量同步 若数据盘完好,恢复 /etc/fdfs/*.conf 后启动即可;同步会自动补齐
删除 .data_init_flag 启动时认为路径未初始化,可能重建目录结构 不要删;已删则从日志确认是否影响,通常重启后会重新生成
删除 store_path 下的 M00 软链接 HTTP 访问 404(fdfs_download_file 仍可用) 重建软链接(见 3.10.6 第 5 步)
同组两节点的 store_path 数量/顺序不一致 M01 文件在另一节点上同步失败 统一配置
# 恢复前先备份配置,便于回退
cp -a /etc/fdfs /etc/fdfs.bak.$(date +%F)

# 核对当前集群中实际存在的组名
fdfs_monitor /etc/fdfs/client.conf | grep 'group name'

# 若不小心创建了多余的组,从 Tracker 中删除该节点记录
fdfs_monitor /etc/fdfs/client.conf delete group2 192.168.1.221

运维纪律:/etc/fdfs/*.conf 应纳入配置管理(Ansible/Git),任何修改走评审;group_name、port、store_path、subdir_count_per_path 四项一经上线视为不可变。

二、GlusterFS 与 FastDFS 对比与选型

前面两部分讲的是两个完全不同的产品:GlusterFS 是分布式文件系统(可以像本地目录一样挂载使用),FastDFS 是分布式文件存储服务(只能通过 API/HTTP 访问)。二者不是竞品,解决的问题域不同。本节做系统性对比,帮助在实际项目中做出正确选择。

4.1 详细对照表

对比维度 GlusterFS FastDFS
产品定位 通用分布式文件系统(Scale-Out NAS) 专用海量中小文件存储服务
架构模式 完全对称、无中心节点(无元数据服务器) 有中心节点:Tracker(调度)+ Storage(存储)
元数据管理 无独立元数据服务,用弹性哈希(Davies-Meyer)算法根据文件路径名实时计算位置 位置信息编码在 file_id 中由客户端持有;Tracker 只管调度不存文件元数据
元数据单点风险 无 Tracker 无状态可水平扩展,多台 Tracker 互为冗余,无单点
访问方式 POSIX 挂载:FUSE(原生,推荐)、NFS、Samba/CIFS、libgfapi 不支持挂载:客户端 API(C/Java/PHP/Python)、命令行工具、HTTP(需 Nginx 模块)
能否 cd/ls/vim 直接操作 能,挂载后与本地目录无差别 不能,必须通过接口
文件名 用户自己决定,保留原始目录结构与文件名 由系统生成 file_id,原始文件名需应用自己存数据库
目录结构 完整支持任意层级目录 无目录概念(内部两级散列目录对用户不可见)
擅长的文件大小 中大文件(MB ~ GB ~ TB),大文件可用条带/分片(shard) 中小文件(4KB ~ 500MB,最佳 4KB~100MB)
小文件性能 较差(每次访问需哈希计算 + LOOKUP,元数据操作放大) 优秀(这是它的设计目标)
大文件支持 优秀(分布式卷/条带卷/分片,单文件可超单盘容量) 一般(单文件必须完整放在一个 Storage 的一块盘上,受单盘容量限制)
随机读写/修改 支持(POSIX 语义完整,可 seek、可覆写、可锁) 不支持修改,只支持追加(appender 文件);一次写入多次读取
冗余机制 复制卷(replica N,多副本)、纠删码卷(dispersed,空间利用率高)、arbiter 仲裁盘 同组多副本(组内所有节点数据完全相同,副本数 = 组内节点数)
空间利用率 分布式卷 100%;复制卷 1/N;纠删码卷可达 (k)/(k+m),如 4+2 为 67% 1/N(N = 组内节点数),没有纠删码
一致性模型 强一致(同步复制,写入所有副本才返回)+ self-heal 修复 最终一致(异步 binlog 同步,秒级延迟窗口)
一致性风险 网络分区可能导致脑裂(需 quorum 防护、需人工修复) 无脑裂概念,但有同步延迟窗口(刚上传的文件在其它节点上暂不可读,由 Nginx 模块回源解决)
扩容方式 add-brick + rebalance 自动迁移数据重新均衡 只能加新 group(增加容量)或加 store_path(加盘);没有 rebalance,老数据不迁移
缩容/数据迁移 remove-brick start/status/commit 三段式自动迁移数据 无内置迁移能力,需应用层重传
在线扩容对业务影响 可在线扩容,rebalance 期间有 I/O 压力 加组对业务无影响(新写入才用新组)
配额 支持(目录级 quota limit-usage) 不支持(只有全局 reserved_storage_space)
快照 支持(需 LVM 精简卷底层) 不支持
单文件容量上限 理论上受卷总容量限制(条带/分片可跨盘) 受单块盘容量限制
集群规模 几台到上百台(元数据无瓶颈,但节点越多 LOOKUP 广播开销越大) Tracker 2~3 台足够;Storage 可几十到上百台(按 group 划分)
部署复杂度 中(yum 安装即可,但卷类型选择、quorum、调优需要理解) 中(需编译安装、配置文件项多、Nginx 模块集成较繁琐)
运维复杂度 较高(脑裂修复、rebalance、self-heal 都需要理解与人工介入) 较低(架构简单、状态少;但缺工具,出问题靠日志)
排错难度 较高(日志分散、xattr 层面问题需要 getfattr) 中(日志集中、状态机清晰)
依赖组件 无(自包含);Samba/NFS 导出时需对应服务 强依赖 Nginx + fastdfs-nginx-module 才能提供 HTTP 访问
权限控制 POSIX 权限 + ACL(挂载后与本地文件系统一致) 无(需在 Nginx/应用层自己做鉴权、防盗链)
加密/传输安全 支持 SSL/TLS(管理与数据通道) 无(需靠网络隔离/HTTPS 前端)
社区与现状 活跃度已下降(Red Hat 2019 后重心转向 Ceph,2023 年宣布 Gluster 进入维护模式),但仍在维护,文档完善 活跃度很低(近年更新极少,社区基本停滞)
商业支持 Red Hat Gluster Storage(已逐步退场) 无
主要开发语言 C C
资源占用 中(每 brick 一个 glusterfsd 进程;FUSE 有上下文切换开销) 低(单进程,几十 MB 内存)
典型场景 虚拟机镜像存储、视频/日志/备份归档、共享 Home 目录、Web 集群共享目录、HPC 数据集 电商/社交的图片、头像、缩略图、附件、短视频、静态资源分发
典型部署年代 2010~2020 2010~2018(国内互联网)

4.2 关键差异的直观理解

用一句话概括:

  • GlusterFS:把多台机器的磁盘拼成一个"网络硬盘",挂载上去当本地目录用。你写 /mnt/data/2026/report.pdf,它就在那儿,路径和文件名都是你的。
  • FastDFS:一个"文件寄存柜",你交给它一个文件,它还你一张票(file_id),凭票取件。票必须自己保管好(存数据库),文件叫什么名字它不关心。

具体差异带来的实际影响:

# 场景:Web 集群三台机器要共享上传的图片目录

# 用 GlusterFS:
应用代码几乎不用改 —— 只要把 /var/www/uploads 挂载成 GlusterFS 卷
$ mount -t glusterfs node1:/web-volume /var/www/uploads
$ # 原来的 move_uploaded_file()、file_get_contents() 全部照常工作
优点:零改造成本
缺点:小图片多时性能一般;Nginx 直接读 FUSE 挂载点有额外开销

# 用 FastDFS:
必须改应用代码 —— 上传时调 SDK,把返回的 file_id 存进数据库
$ // $fid = $fdfs->upload_by_filename($tmp_file);
$ // INSERT INTO images (name, fid) VALUES ('头像.jpg', 'group1/M00/00/00/wKg...');
$ // 前端 URL: http://img.example.com/group1/M00/00/00/wKg...
优点:小文件性能好,天然适合 CDN,水平扩展简单
缺点:需要改造应用;文件名与目录结构的语义完全丢失
你的需求 选择
应用不想改代码,要求像本地目录一样读写 GlusterFS
需要保留原始目录结构与文件名 GlusterFS
需要随机读写/修改已有文件(数据库文件、日志追加、虚拟机磁盘) GlusterFS
存大文件(视频原片、备份包、镜像、数据集) GlusterFS
需要目录配额、快照 GlusterFS
多台 Web 服务器共享 uploads/session/静态资源目录 GlusterFS(简单)或对象存储(更现代)
存海量中小图片,由应用通过接口访问 FastDFS 或对象存储
文件一次写入、多次读取、永不修改 FastDFS 或对象存储
前端要挂 CDN 加速静态资源 FastDFS 或对象存储(URL 天然不变)
应用愿意改造为接口访问 对象存储优先,其次 FastDFS
要求空间利用率高(不愿意付 2~3 倍副本成本) GlusterFS 纠删码卷 / MinIO / Ceph
要求 S3 兼容接口 MinIO / 云对象存储(GlusterFS 与 FastDFS 都不行)
要求 块存储给虚拟机/数据库用 Ceph RBD(两者都不合适)
全新项目、可以上云 云对象存储(OSS/S3/COS)

4.3 明确的选型建议

结论一:需要"像本地目录一样挂载"、需要处理大文件、需要 POSIX 语义 → 选 GlusterFS。

典型判断依据:

  • 应用是遗留系统,用的是标准文件 I/O,改造成 API 访问成本太高;
  • 存的是虚拟机镜像、视频原片、备份文件、科学计算数据集等大文件;
  • 需要多台服务器同时读写同一份数据(共享存储语义);
  • 需要目录级配额或卷快照。

推荐配置:分布式复制卷(replica 2 + arbiter,或 replica 3) 用于一般场景;分布式纠删码卷(如 4+2) 用于对空间利用率敏感的归档场景;万兆网络;独立数据盘做 brick。

结论二:海量图片等小文件、应用愿意通过接口访问 → 选 FastDFS,或直接上对象存储。

典型判断依据:

  • 文件是"写一次读很多次、永不修改"的静态资源(图片、头像、附件、短视频);
  • 单个文件不大(几 KB 到几十 MB),但数量在千万到亿级;
  • 前端要走 CDN;
  • 应用可以接受"上传拿 file_id、下载凭 file_id"的模式,并有地方(数据库)存映射关系。

但在 2020 年之后的新项目中,这个场景的首选答案已经变成对象存储(云上用 OSS/S3/COS,私有化用 MinIO),原因见结论三。

推荐配置(若确定用 FastDFS):2 台 Tracker + 每组 2 台 Storage;Tracker 前置 Nginx/Keepalived 做统一入口;url_have_group_name = true;提前规划好 group 划分;配好 reserved_storage_space 与完整监控;独立做备份(同组副本不是备份)。

结论三:云原生时代的处境与替代趋势。

时期 主流选择 原因
2008~2013 FastDFS、MooseFS、TFS、GlusterFS 兴起 缺少成熟的开源对象存储;机房自建为主
2013~2018 GlusterFS(文件)、FastDFS(图片)、Ceph 崛起、HDFS(大数据) 各自占据细分场景;国内互联网大量落地 FastDFS
2018~2022 Ceph 统一存储、MinIO、云对象存储快速取代前两者 S3 接口成为事实标准;纠删码降低成本;K8s 需要 CSI 与对象存储
2022 至今 云对象存储为默认答案;私有化选 MinIO/Ceph;K8s 场景 Longhorn/Rook-Ceph/OpenEBS 全托管、零运维、按量付费、自带 CDN/生命周期/版本控制

具体的替代关系:

  • FastDFS → MinIO / 云对象存储。MinIO 提供 S3 兼容接口、纠删码(空间利用率约 75% 对比 FastDFS 的 50%)、完整的权限与版本控制、活跃社区、K8s 原生支持。云对象存储更进一步,连运维都不需要。
  • GlusterFS → CephFS / 云文件存储(NAS)/ JuiceFS。需要 POSIX 挂载时,CephFS 功能更完整且社区更活跃;云上直接买 NAS/EFS;JuiceFS 则把对象存储包装成 POSIX 文件系统,兼具两者优点。
  • 两者共同的替代者:云对象存储。对绝大多数"存文件"的需求,OSS/S3/COS 是成本、可靠性、运维成本综合最优的方案。

那还有必要学吗?非常有必要:

  1. 架构思想具有普适性:GlusterFS 的"无元数据服务器 + 一致性哈希"是理解 Cassandra、Dynamo、Ceph CRUSH 的基础;FastDFS 的"位置信息编码进 ID"是理解各类短链接、CDN 回源、分片路由设计的经典案例。
  2. 存量系统大量存在:国内很多 2015~2019 年上线的系统仍跑在 FastDFS/GlusterFS 上,运维岗位仍需要能接手、能排错、能迁移。
  3. 面试与考试的高频考点:卷类型、脑裂、rebalance、file_id 结构都是常见考题。
  4. 理解权衡:为什么 FastDFS 不做 POSIX?为什么 GlusterFS 小文件慢?搞懂这些"取舍",才能在面对新技术时做出正确判断。

4.4 混合使用与迁移建议

混合部署是常见的现实选择:

                          业务应用
                     /                 \
      大文件 / 需要挂载              海量小图片 / CDN 分发
              |                              |
        GlusterFS 卷                  对象存储(或 FastDFS)
      /mnt/videos  /mnt/backup         img.example.com

从 FastDFS 迁移到对象存储的可行路径(无停机):

  1. 双写阶段:应用上传时同时写 FastDFS 和对象存储,数据库同时记录两个地址;读仍走 FastDFS。
  2. 历史数据搬迁:后台任务遍历数据库中的 file_id,从 FastDFS 下载并上传到对象存储,回填新地址。
  3. 切读:读优先走对象存储,失败时回落到 FastDFS(灰度放量)。
  4. 停写:确认新链路稳定后,停止写 FastDFS。
  5. 观察期:保留 FastDFS 集群 1~3 个月只读,确认无回源请求。
  6. 下线:备份一份冷数据后下线集群。

从 GlusterFS 迁移相对简单:因为它是 POSIX 挂载,可以直接用 rsync 把数据同步到新的 NFS/CephFS/JuiceFS 挂载点,然后切换挂载点即可,应用无需改造。

# GlusterFS 迁移示例:先全量后增量,最后短暂停机做最后一次增量并切挂载点
rsync -avHAX --numeric-ids --info=progress2 /mnt/gluster-vol/ /mnt/new-storage/
# 多轮增量
rsync -avHAX --delete /mnt/gluster-vol/ /mnt/new-storage/
# 停业务 -> 最后一次 rsync -> 修改 /etc/fstab -> 重新挂载 -> 启业务

练习

练习四 搭建双 Tracker + 双 Storage 的 FastDFS 集群

题目:用 4 台(或 2 台复用)CentOS 7 主机搭建 FastDFS 集群,要求:

  1. 2 个 Tracker 互为冗余,2 个 Storage 同组互备;
  2. 完成上传、下载、删除、查看文件信息;
  3. 集成 Nginx,实现通过 HTTP 访问文件;
  4. 在 Tracker 侧配置统一入口,任意一个入口都能访问所有文件。

参考答案:

# ===== 规划 =====
# 192.168.1.220  tracker1 + storage1(group1)
# 192.168.1.221  tracker2 + storage2(group1)

# ===== 1. 系统准备(两台都做)=====
systemctl stop firewalld && systemctl disable firewalld
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux
hostnamectl set-hostname --static server-node1        # 另一台 server-node2
cat >> /etc/hosts << 'EOF'
192.168.1.220  server-node1
192.168.1.221  server-node2
EOF
systemctl start chronyd && systemctl enable chronyd
mkdir -p /home/dfs /data/fastdfs1                     # 目录必须手工创建!

# ===== 2. 安装(两台都做)=====
cd fastdfs && ./install.sh                            # 或手工编译 libfastcommon + fastdfs
netstat -tnlp | grep -E '22122|23000'

# ===== 3. tracker.conf(两台都改)=====
vim /etc/fdfs/tracker.conf
#   bind_addr =                    (留空,不要写 127.0.0.1)
#   port = 22122
#   base_path = /home/dfs
#   store_lookup = 0               (轮询;若只有一组也可用 2)
#   store_group = group1           (store_lookup=1 时才生效,注意不要写成不存在的组名)
#   reserved_storage_space = 20%

# ===== 4. storage.conf(两台都改,配置【两个】tracker)=====
vim /etc/fdfs/storage.conf
#   group_name = group1            (两台必须一致!)
#   port = 23000
#   base_path = /home/dfs
#   store_path_count = 1
#   store_path0 = /data/fastdfs1
#   subdir_count_per_path = 256
#   tracker_server = 192.168.1.220:22122
#   tracker_server = 192.168.1.221:22122
#   http.server_port = 8888

# ===== 5. client.conf =====
vim /etc/fdfs/client.conf
#   base_path = /home/dfs
#   tracker_server = 192.168.1.220:22122
#   tracker_server = 192.168.1.221:22122

# ===== 6. 启动(先 Tracker 后 Storage)=====
systemctl restart fdfs_trackerd && systemctl enable fdfs_trackerd
systemctl restart fdfs_storaged && systemctl enable fdfs_storaged
# CentOS 6:service fdfs_trackerd restart / service fdfs_storaged restart

# ===== 7. 验证集群 =====
fdfs_monitor /etc/fdfs/storage.conf
# 要点:tracker server count = 2;group count = 1;
#       storage server count = 2;active server count = 2;
#       两个节点的 status 都必须是 ACTIVE (7)

# ===== 8. 上传/下载/信息/删除 =====
fdfs_upload_file /etc/fdfs/client.conf README
# 返回:group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

FID=group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
fdfs_file_info     /etc/fdfs/client.conf $FID       # 看源 IP、时间、大小、CRC32
fdfs_download_file /etc/fdfs/client.conf $FID /tmp/down.README
md5sum README /tmp/down.README                      # 两个 md5 必须一致
fdfs_delete_file   /etc/fdfs/client.conf $FID       # 测试完再删

# ===== 9. Nginx 集成(两台 Storage 都做)=====
# 编译(fastdfs-nginx-module)
vim /usr/local/src/fastdfs-nginx-module/src/config     # 去掉路径中的 /local
cd /usr/local/src/nginx-1.20.2
./configure --prefix=/usr/local/nginx \
  --add-module=/usr/local/src/fastdfs-nginx-module/src
make && make install

cp /usr/local/src/fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/
cp /usr/local/src/fastdfs-5.11/conf/{http.conf,mime.types} /etc/fdfs/
ln -s /data/fastdfs1/data /data/fastdfs1/data/M00

vim /etc/fdfs/mod_fastdfs.conf
#   base_path = /tmp
#   tracker_server = 192.168.1.220:22122
#   tracker_server = 192.168.1.221:22122
#   group_name = group1
#   url_have_group_name = true        (URL 里带 group1,必须为 true!)
#   store_path_count = 1
#   store_path0 = /data/fastdfs1
#   response_mode = proxy             (同步未完成时代理回源,避免 404)

# Storage 侧 nginx.conf
#   server { listen 8888;
#            location ~/group[0-9]+/M00 { ngx_fastdfs_module; } }
/usr/local/nginx/sbin/nginx -t && /usr/local/nginx/sbin/nginx

# ===== 10. Tracker 侧统一入口 =====
# server { listen 80;
#   location /group1/M00 { proxy_pass http://fdfs_group1; proxy_cache fdfs_cache; } }
# upstream fdfs_group1 { server 192.168.1.220:8888; server 192.168.1.221:8888; }
nginx -t && nginx -s reload

# ===== 11. 验证 HTTP 访问 =====
FID=$(fdfs_upload_file /etc/fdfs/client.conf /etc/hosts)
curl -I http://192.168.1.220:8888/$FID     # 直连 storage1
curl -I http://192.168.1.221:8888/$FID     # 直连 storage2(可能触发回源代理)
curl -I http://192.168.1.220/$FID          # 统一入口 1
curl -I http://192.168.1.221/$FID          # 统一入口 2
# 四个都应返回 200 OK

故障演练(必做):

# 停掉 tracker1,验证上传仍然成功(Tracker 无状态、可冗余)
systemctl stop fdfs_trackerd                 # 在 220 上
fdfs_upload_file /etc/fdfs/client.conf /etc/hosts     # 仍应成功
systemctl start fdfs_trackerd

# 停掉 storage1,验证下载仍然成功(同组互备)
systemctl stop fdfs_storaged                 # 在 220 上
curl -I http://192.168.1.221/$FID            # 仍应 200
fdfs_monitor /etc/fdfs/client.conf | grep -E 'ip_addr|active server count'  # 220 变 OFFLINE
systemctl start fdfs_storaged                # 恢复后自动补同步

练习五 验证 FastDFS 同组同步

题目:验证同组两个 Storage 之间的文件同步,说明同步机制与观察方法。

参考答案:

# ===== 1. 在 node1(220)上上传文件 =====
fdfs_upload_file /etc/fdfs/client.conf README
# group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045

# ===== 2. 立即在两个节点上分别 curl 下载并对比 =====
# 在 220 上
curl -O http://192.168.1.220:8888/group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 在 221 上
curl -O http://192.168.1.221:8888/group1/M00/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 两个文件内容应完全相同
md5sum wKgB3FyovhGAY8lJAAAD3C8hfQo2554045 README

# ===== 3. 直接检查物理磁盘(最直观的证据)=====
# 在两个节点上分别执行
ls -l /data/fastdfs1/data/00/00/wKgB3FyovhGAY8lJAAAD3C8hfQo2554045
# 两边都应存在,大小相同

# ===== 4. 观察同步日志 =====
tail -f /home/dfs/logs/storaged.log
# 注意:源文档写的是 /etc/fdfs/logs/storaged.log,实际路径是 $base_path/logs/
# 可以看到:
#   [INFO] ... selection tracker server is 192.168.1.221:22122   <-- 221 被选为 leader tracker
#   [INFO] ... successfully connect to storage server 192.168.1.220:23000
#   [INFO] ... sync file to 192.168.1.221:23000 successfully      <-- 文件从 220 同步到 221

# ===== 5. 检查同步延迟 =====
fdfs_monitor /etc/fdfs/storage.conf | grep -E 'ip_addr|last_source_update|last_sync_update|last_synced_timestamp'
# last_synced_timestamp 后的 (0s delay) 表示无延迟

# ===== 6. 检查 binlog 与 mark 文件 =====
tail -5 /home/dfs/data/sync/binlog.000
#   1553836564 C M00/00/00/wKgB3Fyov...     <-- 大写 C:本机作为源创建
#   1553836601 c M00/00/00/wKgB3Vyov...     <-- 小写 c:从别处同步来的创建
cat /home/dfs/data/sync/192.168.1.221_23000.mark
#   binlog_offset 应持续推进;need_sync_old=0 且 sync_old_done=1 表示存量已同步完

# ===== 7. 批量压测同步 =====
for i in $(seq 1 200); do fdfs_upload_file /etc/fdfs/client.conf /etc/hosts >> /tmp/fids.txt; done
# 在两个节点上对比文件总数
find /data/fastdfs1/data -type f ! -name 'binlog*' ! -name '*.mark' | wc -l

关于"leader"的重要澄清:日志中出现的 selection tracker server is 192.168.1.221 指的是 Storage 从多个 Tracker 中选出一个作为同步协调的 leader tracker(用于协调组内同步的时间基准),不是说 221 这台机器成了"主节点"、220 成了"从节点"。FastDFS 的同组 Storage 是完全对等的,任何节点都可以接收上传,也都可以作为同步源,没有主从之分。

同步机制要点:

要点 说明
同步方向 Storage 直连 Storage(走 23000 端口),不经过 Tracker。所以 Storage 之间的 23000 也必须放行
同步方式 异步:上传写完本地即返回,后台线程读 binlog 推送到同组其它节点
一致性 最终一致,存在秒级延迟窗口
延迟窗口的问题 刚上传的文件立即从另一节点读取可能 404 —— 由 fastdfs-nginx-module 的 response_mode = proxy 解析 file_id 中的源 IP 回源代理来解决
断点续传 mark 文件记录 binlog_offset,节点重启或网络恢复后从断点继续,不会重头开始
新节点加入 触发全量同步(need_sync_old=1),状态经 WAIT_SYNC → SYNCING → ACTIVE
时间同步的重要性 binlog 用时间戳判断位点,节点间时间偏差会导致同步异常,必须配好 NTP/chrony

本章小结

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏