用 FreeBSD + ZFS + Samba 搭一台 500T 镜像存储

我们需要一台大容量的镜像与冷数据存储,给内网机器放系统镜像、备份和一些不常改动的大文件。预算有限,买不起商业存储,手里有的是一台塞满盘的服务器:36 块 16T SATA、两块 NVMe、两个 25G 带 RDMA 的网口。于是我们决定自己拿 FreeBSD + ZFS + Samba 组一套,把钱花在盘上,而不是机头上。

这篇文章记录我们当时的选型、踩过的点和最终落地的配置。核心判断很简单:对这种容量优先、以顺序大文件为主的场景,ZFS 的 raidz2 加上一块 SLOG、一块 L2ARC,性价比高得离谱,而且数据完整性有保障。

为什么是 FreeBSD + ZFS

ZFS 最早由 Matthew Ahrens 和 Jeff Bonwick 在 2001 年于 Sun 开发,2008 年移植到 FreeBSD。它和 Linux 内核因为 CDDL 与 GPL 许可证不兼容一直是“分家”状态,所以在 FreeBSD 上用 ZFS 是一等公民,省去了 Linux 上 DKMS 那套折腾。甲骨文收购 Sun 后 OpenSolaris 闭源,大批核心开发者出走,于 2013 年成立了 OpenZFS,现在 FreeBSD 用的就是这一支。

对我们这台存储真正重要的是 ZFS 的几个特性:存储池把所有盘抽象成一个池、写时拷贝保证掉电不会写坏已有数据、端到端校验和能发现并自动修复静默损坏、RAID-Z 提供软件级冗余、再加上透明压缩。这些加起来意味着:我们不需要硬件 RAID 卡,盘坏了换一块就能重建,而且每一个字节读出来都被校验过。

从 Linux 过来的工程师,日常命令大致这样对应:

Linux (Red Hat/Debian)FreeBSD用途
yum install / apt-get installpkg install从远程仓库安装软件包
rpm -ivh / dpkg -ipkg add安装本地包
rpm -qa / dpkg -lpkg info列出已安装软件包
lspcipciconf列出 PCI 设备
lsmodkldstat列出已载入的内核模块
modprobekldload / kldunload载入/卸载内核模块
stracetruss跟踪系统调用
ip / ifconfigifconfig配置网络接口
iptablesipfw防火墙

硬件布局

这台机器的配置:

网络: 25G * 2 with rdma
磁盘:
  36 * 16T sata        # 数据盘
  1  * 1T  nvme        # zfs log(SLOG,写缓存),其实应该上 2 块做镜像
  1  * 3.8T nvme       # zfs cache(L2ARC,读缓存)

这里有两个要说清楚的取舍。

SLOG(1T NVMe)不是通常理解的“写缓存”。它只承接同步写(ZIL),把需要落盘确认的同步请求先写到低延迟的 NVMe 上,立刻返回,再在后台批量刷到 raidz2。对 Samba 这种会发同步写的场景能明显降低延迟。代价是:SLOG 一旦在掉电瞬间损坏,有可能丢掉最后几秒还没落盘的同步写。所以严格来说它应该做成两块 NVMe 的镜像——我们当时只有一块,这是明知的妥协,写进文档提醒自己后面补上。

L2ARC(3.8T NVMe)是二级读缓存,兜住内存 ARC 装不下的热数据。它只对读有帮助,而且会占用一部分内存来维护索引,所以不是越大越好。对我们这种镜像反复被拉取的读多写少场景,它是划算的。

组 zpool

动手前先看盘:

geom disk list

然后把 36 块盘组成一个 raidz2:

zpool create datasets raidz2 da0 da1 da2 da3 da4 da5 da6 da7 da8 da9 da10 da11 da12 da13 da14 da15 da16 da17 da18 da19 da20 da21 da22 da23 da24 da25 da26 da27 da28 da29 da30 da31 da32 da33 da34 da35

为什么是 raidz2 而不是 raidz1?raidz2 相当于 RAID6 的冗余级别,允许同时坏两块盘。36 块盘的池里,重建一块 16T 盘要跑很久,而重建期间整个池的盘都在高负载读取,这正是第二块盘最容易跟着挂掉的时候。raidz1 在这个规模下几乎等于在赌命。raidz2 用两块盘的容量换来重建窗口内的安全,在可靠性、性能、容量之间取得了我们能接受的平衡。

一个诚实的提醒:把 36 块盘放进单个 vdev 不是教科书推荐做法。vdev 越宽,重建越慢、随机 IOPS 越差。更稳妥的做法是拆成几个较窄的 raidz2 vdev 条带起来。我们这台是容量优先、以顺序大文件为主,才选了单 vdev 把容量吃满——如果你的负载是随机小文件,别照抄。

加 SLOG(写日志):

zpool add datasets log nvd0

如果有两块 NVMe,SLOG 应该做镜像(强烈建议这么做):

zpool add datasets log mirror xxx xxx

加 L2ARC(读缓存):

zpool add datasets cache nvd1

组完看一眼状态:

zpool list
NAME       SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP  HEALTH  ALTROOT
datasets   524T  95.6T   428T        -         -     0%    18%  1.00x  ONLINE  -
zpool status datasets
  pool: datasets
 state: ONLINE
  scan: none requested
config:

	NAME        STATE     READ WRITE CKSUM
	datasets    ONLINE       0     0     0
	  raidz2-0  ONLINE       0     0     0
	    da0     ONLINE       0     0     0
	    da1     ONLINE       0     0     0
	    da2     ONLINE       0     0     0
	    da3     ONLINE       0     0     0
	    da4     ONLINE       0     0     0
	    da5     ONLINE       0     0     0
	    da6     ONLINE       0     0     0
	    da7     ONLINE       0     0     0
	    da8     ONLINE       0     0     0
	    da9     ONLINE       0     0     0
	    da10    ONLINE       0     0     0
	    da11    ONLINE       0     0     0
	    da12    ONLINE       0     0     0
	    da13    ONLINE       0     0     0
	    da14    ONLINE       0     0     0
	    da15    ONLINE       0     0     0
	    da16    ONLINE       0     0     0
	    da17    ONLINE       0     0     0
	    da18    ONLINE       0     0     0
	    da19    ONLINE       0     0     0
	    da20    ONLINE       0     0     0
	    da21    ONLINE       0     0     0
	    da22    ONLINE       0     0     0
	    da23    ONLINE       0     0     0
	    da24    ONLINE       0     0     0
	    da25    ONLINE       0     0     0
	    da26    ONLINE       0     0     0
	    da27    ONLINE       0     0     0
	    da28    ONLINE       0     0     0
	    da29    ONLINE       0     0     0
	    da30    ONLINE       0     0     0
	    da31    ONLINE       0     0     0
	    da32    ONLINE       0     0     0
	    da33    ONLINE       0     0     0
	    da34    ONLINE       0     0     0
	    da35    ONLINE       0     0     0
	logs	
	  nvd0      ONLINE       0     0     0
	cache
	  nvd1      ONLINE       0     0     0

errors: No known data errors

524T 裸容量,去掉 raidz2 的两块校验盘和 ZFS 自己的开销,实际可用约 451T。

挂载

zfs set mountpoint=/datasets datasets
zfs mount datasets

确认挂上了:

df -h
Filesystem     Size    Used   Avail Capacity  Mounted on
/dev/ada0p2    429G     11G    384G     3%    /
devfs          1.0K    1.0K      0B   100%    /dev
datasets       451T    9.5G    451T     0%    /datasets

装 Samba 对外提供服务

内网大量是 Windows 机器,所以走 SMB 最省事:

pkg install samba413-4.13.17_1

开机自启,两种写法任选其一(sysrc 本质上就是帮你改 /etc/rc.conf):

sysrc samba_server_enable=YES
# 等价于在 /etc/rc.conf 里加一行:
# samba_server_enable="YES"

然后是 smb.conf。几个值是有意调过的,不是默认:

[global]
    workgroup = SAMBA
    security = user
    server string = ZFS SAMBA Version %v
    passdb backend = tdbsam
    hosts allow = 127. 10.20.193. 192.168.23. 192.168.1. 10.20.191. 10.88.88.
    load printers = no
    use sendfile = yes
    socket options = TCP_NODELAY SO_RCVBUF=16384 SO_SNDBUF=16384
    write raw = yes
    read raw = yes
    max xmit = 65535
    unix charset = UTF-8
    dos charset = GB2312
    server multi channel support = yes
    max protocol = SMB3
[zfs]
    comment = ZFS SAMBA Version %v Image Storage
    path = /datasets/
    browseable = yes
    writable = yes
    public = no
    guest ok = no
    create mask = 0755
    directory mode = 0755
    security = share

几个关键项的理由:use sendfile = yes 让内核直接把文件从 ZFS 送到 socket,省去用户态来回拷贝,对这种大文件传输很有用;server multi channel support = yes 配合 SMB3 和两个 25G 口,能让单个客户端同时用上多条链路;hosts allow 把访问限制在我们自己的几个内网段里,这是第一道访问控制;dos charset = GB2312 是为了兼容老 Windows 客户端的中文文件名。

最后 smbpasswd -a 建用户,重启 Samba 服务,客户端就能挂载了。

之后我们做了什么

这套跑起来之后,有几件事是我们按计划补上或持续在做的:给 SLOG 补第二块 NVMe 做镜像,消掉那个已知的单点;配置定期 zpool scrub,让 ZFS 主动巡检并修复静默损坏,而不是等读到坏数据才发现;监控 L2ARC 命中率,确认那块 3.8T 读缓存确实在干活。如果你也在用一堆便宜盘攒存储,这套组合值得抄——但 SLOG 一定要做镜像,别留着那个坑。

Published At (Updated At) Tagged with