Lustre云上实践:ZFS OST基于对象存储的架构与部署
如果你运维过传统 HPC 集群,大概率经历过这样的场面:机柜里塞满磁盘,RAID 卡时不时亮红灯,扩容之前要先算容量、核对 IOPS,供应商给的技术参数和真实负载永远对不上。后来集群迁上云,本以为能摆脱硬件管理,结果云硬盘照样要规划容量、设置性能档位,还要接受"IOPS 上限"这种传统磁盘时代没有过的新约束。
云上真正能做到"容量不看上限、付费按量走"的存储形态,是对象存储。它按量付费、无限扩展、自带多副本容错,听起来非常适合当大规模文件系统的底座。但对象存储的硬伤也很明显:延迟高、随机写能力弱、访问协议特殊,跟 Lustre 这类面向高性能计算场景的并行文件系统,习惯上完全是两个世界。
所以当 "Show HN: Open Lustre in the cloud, with ZFS OSTs on object storage not disks" 这个项目出现时,真正值得琢磨的问题不是"它能不能跑通",而是"它到底怎么实现的,性能边界在哪里,哪些场景适合用,哪些场景千万不要碰"。
这篇文章会从存储架构和工程实践两个维度拆解这套方案。先讲清楚 Lustre、ZFS、OST、对象存储四个概念之间的协作关系,再分析把 OST 底层从磁盘换成对象存储的技术路径与风险,然后给出一套可用于测试环境验证的部署思路和参考命令,最后落到最佳实践与排查清单。即使你当前没有 HPC 环境,理解这套架构对设计云上大规模数据管道、AI 训练存储底座,也会有直接帮助。
1. 这篇文章真正要解决的问题
先别急着讨论命令,说说为什么要关注这个项目。
传统 Lustre 集群的存储层由大量本地磁盘构成,通常以 ldiskfs 或 ZFS 作为 OST 的文件系统。初次部署时,你要规划每个 OSS 节点挂多少块盘、RAID 级别怎么选、热备盘准备几块。运行期间,磁盘故障、坏块扫描、容量水位都是日常运维事项。这样的设计在自建机房时代是合理的,因为磁盘是最便宜的存储介质,而且物理机上跑 Lustre 是 HPC 领域的标准方案。
到了云上,问题出现了。你不能把一整个机柜搬进云机房,只能用云服务器和云硬盘来模拟原来的拓扑。这时候会有两个明显的不适感:
第一,容量规划从"买多少块盘"变成"买多大云盘、几个性能档位",看起来方便了,但本质上还是预先下单。如果负载增长超过预期,你得创建新云盘、挂载、迁移数据,整套流程一点也不轻。
第二,云硬盘有 IOPS 上限和带宽上限。一旦并发任务跑起来,某个 OST 的磁盘队列可能先被打满,而对象存储不存在这个瓶颈——因为它的每个请求都会经过分布式存储集群和负载均衡器,容量和性能都是横向扩展的。
这个项目的核心思路,是把 OST 的底层存储从磁盘换成对象存储,同时保留 ZFS 文件系统作为中间层,用 ZFS 来承接数据校验、压缩、缓存、快照这些能力;在上层继续暴露 Lustre 的并行文件系统语义。也就是说,Lustre 跑在云端,OST 的逻辑存储卷由 ZFS 管理,而 ZFS 的物理存储后端不是 /dev/sdb,而是对象存储 Bucket。
这篇文章适合谁读?三类人:
- HPC 或超算场景的存储工程师,正在考虑把 Lustre 迁到云上,但对云硬盘成本和运维负担有顾虑;
- AI 基础设施团队,需要为训练任务准备大规模共享存储,尤其是 checkpoint 和数据集这类"写入频率不高但单文件很大"的负载;
- 对分布式文件系统感兴趣的架构师,想理解 POSIX 文件系统与对象存储之间的转换可以做到什么程度。
如果你只是需要一个高性能文件共享目录,这个方案对你来说可能过重。它针对的是"文件数量大、单文件体积大、需要 POSIX 语义、希望降低存储成本"的组合场景。
2. Lustre、ZFS、OST、对象存储:核心概念与关系
要理解这套架构,先要把四个术语的边界弄清楚。它们经常被放在一起提,但各自承担的角色完全不同。
2.1 Lustre 的组件模型
Lustre 是一个高性能并行文件系统,在 HPC 领域有多年历史。它把文件系统的职责拆分成了几个组件:
- MGS(Management Server):管理服务,负责维护集群配置信息,所有节点启动时都要找它获取配置;
- MDS(Metadata Server)+ MDT(Metadata Target):负责文件的目录结构、文件名、权限、属性等元数据。对于一个小文件操作,真正写数据之前要先在 MDT 上完成元数据操作;
- OSS(Object Storage Server)+ OST(Object Storage Target):负责文件的实际数据内容。一个大文件会被拆成多个条带(stripe),分散写入不同的 OST;
- Client:客户端,通过 Lustre 客户端内核模块挂载文件系统。
数据读写的关键点在 OST。每个 OST 在底层本质上是一个文件系统实例,传统实现使用 ldiskfs 或 ZFS 文件系统,落在磁盘或 RAID 卷上。本项目中的"ZFS OSTs"指的就是用 ZFS 作为 OST 的文件系统实现。
2.2 ZFS 的定位
ZFS 既是一个文件系统,也是一个卷管理器。它把存储池(zpool)管理、文件系统、快照、校验、压缩、RAID 逻辑整合在一起。ZFS 不像传统方案那样先做硬件 RAID 再格式化为文件系统,而是直接管理底层设备。
ZFS 中有两个重要概念:vdev(虚拟设备)和 dataset(数据集)。一个 zpool 可以由多个 vdev 组成,每个 vdev 可以是一块磁盘、一个分区、一个硬件 RAID 卷,甚至一个文件。dataset 则是在池之上创建的独立文件系统或卷。
这个项目选择 ZFS,不只是因为它能作为文件系统,更关键的是它自带数据校验和自愈能力。当底层存储出现数据不一致时,ZFS 会在读取时通过校验和发现错误,并通过冗余副本恢复数据。这一点对对象存储场景非常重要,因为对象存储的数据访问路径比本地磁盘复杂得多,出现静默损坏的概率更高。
2.3 对象存储的访问模型
对象存储是另一种存储语义。它以 Bucket 和 Object 为单元,通过 HTTP 协议访问,最常见的 API 是 S3 协议。对象存储的优势是容量近乎无限、按量计费、跨区冗余策略灵活,但它不是一个 POSIX 文件系统,不能直接 mount,也没有目录树操作的原生接口。
要在文件系统场景中使用对象存储,通常需要借助中间层,比如 FUSE 挂载工具、云存储网关,或者干脆通过对象存储 SDK 自行实现数据读写。Lustre 本身不直接认识对象存储,所以这个项目必然存在一层转换逻辑,让 ZFS 能把对象存储当作自己的存储底座来看待。
2.4 概念关系总结
| 组件 | 作用 | 传统承载方式 | 本项目承载方式 |
|---|---|---|---|
| MGS | 管理集群配置 | 本地磁盘 | 本地磁盘或低速存储 |
| MDS/MDT | 保存文件元数据 | 本地 SSD/NVMe | 本地高性能存储(重要) |
| OSS/OST | 保存文件数据 | RAID 卷、本地磁盘 | ZFS 文件系统,底层为对象存储 |
| Client | 客户端访问 | 内核模块挂载 | 内核模块挂载 |
这里有一个容易被忽视的重点:MDT 依然应该放在本地高性能存储上。元数据操作是典型的小随机 IO,延迟敏感度极高。如果把 MDT 也放到对象存储上,一个ls命令可能要等待数百毫秒,这在生产环境完全不可接受。本项目标题强调的"OSTs on object storage",而不是"整个 Lustre 都放对象存储",这是合理的架构裁剪。
3. 为什么传统方案在云端不划算
把 OST 放在对象存储上的动机,需要从成本和运维两个角度展开。
3.1 云硬盘的费用并不低
云硬盘按容量和性能等级计费。以常见云厂商为例,一块高性能云盘的每 GB 月价格远高于对象存储的每 GB 月价格,尤其是当你需要高 IOPS 档位时,费用会成倍上升。Lustre 集群为了保证性能,通常每个 OSS 节点会配置多块高吞吐云盘,这部分预算是大头。
更麻烦的是容量利用率。你为峰值规划容量,但大多数时间存储使用率可能只有 40%~60%。本地磁盘一旦买下,不管用多少都在出钱;对象存储按实际存储量计费,没有容量囤积成本。
3.2 真正的差别是运维复杂度
对存储工程师来说,最消耗精力的不是金额,而是"容量规划"和"故障处理"。云硬盘本质上还是块存储,它可能位于某个物理宿主机上,也有自己的故障域。云厂商虽然保证了持久性,但对于文件系统层来说,你仍然要关注分区的使用率、inode 使用量、扩容流程、快照策略。
对象存储把这层几乎消灭了。你不需要知道数据具体落在哪个节点上,不需要关心底层磁盘是否健康,不需要提前规划 Bucket 的容量上限,副本策略由云厂商管理。对于团队规模有限、不想为存储运维投入太多人力的场景,这是很明显的收益。
3.3 性能代价是绕不开的
对象存储的延迟通常按毫秒甚至几十毫秒计,而本地 NVMe 盘的延迟是微秒级。顺序大块读写场景下,对象存储可以通过并发 PUT/GET 获得不错的吞吐,但随机小 IO 会非常吃力。
这意味着,这个架构能取得好效果的负载,应当具备两个特征:一是大文件居多,二是顺序读写为主。如果应用里大量小文件随机访问,对象存储很可能成为集群的瓶颈。
3.4 为什么这个时机点值得关注
过去几年,主要云厂商的对象存储服务陆续提供强一致读能力,这在一定程度上解决了对象存储的一致性短板。ZFS 又提供了校验和数据自愈。加上 Lustre 本身在并行文件系统领域已经很成熟,三个技术点叠加,让"对象存储扛主存储"这个过去属于禁忌的方案,出现了可以被认真讨论的空间。
但要注意:对象存储强一致,只代表你读到的对象是最新版本,不代表每次读取都低延迟,更不代表它擅长随机写。它只是把"损坏风险"降低了一档,并没有把一个慢存储变成快存储。
4. 架构设计:对象存储如何接入 ZFS
项目标题里最关键的词组是 "with ZFS OSTs on object storage not disks"。要实现这个目标,必须回答一个问题:ZFS 底层本来要接收块设备,现在换成对象存储,怎么接?
从工程实践看,存在两种技术路径。
4.1 文件级接入:通过 FUSE 挂载对象存储
一种思路是在 OSS 节点上先挂载一个"对象存储映射出来的文件系统",比如 rclone mount、s3fs 等工具,把 Bucket 里的某个目录映射成/mnt/ost-fuse。然后在 ZFS 创建存储池时,不指定真实磁盘,而是指定这个挂载目录里的一个大文件作为 vdev。
# 仅用于功能验证:在对象存储挂载点创建一个文件作为 ZFS vdev truncate -s 100G /mnt/ost-fuse/zfs-vdev.img zpool create -f zpool-ost /mnt/ost-fuse/zfs-vdev.img这看起来很巧妙,因为 ZFS 本身支持 file-backed vdev,所以不需要写任何特殊驱动就能让 ZFS"感知"对象存储。但它有非常大的隐患:
第一,FUSE 文件系统在用户态处理请求,性能本身就比内核文件系统差。ZFS 的每一次 IO 都要经过 FUSE 层,再转换成对象存储的 HTTP 请求,路径很长。
第二,对象存储的最终一致性曾经是普遍现象。虽然现代云厂商大多提供了强一致读,但 ZFS 对底层设备安全写入的要求极其严格。如果底层出现旧数据覆盖、部分写入丢失的情况,一致性协议栈可能会触发读校验错误,严重时导致 OST 无法挂载。
第三,file-backed vdev 在本地磁盘上都不是推荐做法,因为它绕过了 ZFS 的设备管理能力。放在对象存储上,更是把实验属性拉满。
所以文件级接入可以作为一个快速验证功能链路的方案,但绝不应该是生产架构的选择。
4.2 块级接入:通过存储网关或自定义转换层
更稳的路径,是把对象存储转换成块设备接口,再交给 ZFS。常见方式包括:
- 使用云厂商提供的存储网关服务,把 Bucket 映射为 iSCSI 块设备;
- 在 OSS 节点上部署本地网关软件,将对象存储的 GET/PUT 转换为块读块写协议。
这个方案的好处是 ZFS 面对的是一个真正的块设备,ioctl、write、flush 等语义更完整,一致性模型更接近磁盘。坏处是网关组件引入了额外运维复杂度,而且网关往往有本地缓存,严格来说"数据全部存放在对象存储"就不那么纯粹了。
也有可能,这个论坛投稿项目自己实现了一个位于对象存储与 ZFS 之间的适配层,把对象存储 API 包装成块设备接口。这种方式从架构上看最干净,但实现工作量最大,涉及块地址到对象键的映射、并发控制、一致性保证等多个难题。具体实现细节要以项目 README 和源码为准。
4.3 ZFS 在这个架构里承担了什么
无论采用哪种接入方式,ZFS 都处在非常核心的位置。它的重要性体现在四个层面:
- 校验和数据自愈:ZFS 每题写块时计算校验和,读取时校验。若对象存储某个对象损坏或读到旧版本,ZFS 能首先发现异常,并在有冗余的情况下恢复。
- 压缩:对象存储按容量计费,开启 ZFS 压缩能直接降低存储成本。对大文件数据,LZ4 或 ZSTD 压缩率通常相当可观。
- 缓存:ZFS 的 ARC 使用内存缓存热点数据,Dirty 数据在内存中累积后异步写入底层。这能在一定程度上掩盖对象存储的高延迟。
- 快照:ZFS 原生支持文件系统快照,可以配合对象存储的多版本能力,建立更细粒度的恢复点。
但要注意,ZFS 对底层存储也提出了严格要求。底层设备必须保证一定的一致性,否则 ZFS 的校验体系会不断报错。生产环境使用前,必须在真实对象存储后端上跑长时间稳定性测试,并观察zpool status和scrub的结果。
5. 环境准备与前置条件
如果你打算在测试环境里跑通这套方案的完整链路,或者至少验证核心组件之间的连通性,需要准备以下环境。本文给出的命令以通用开源工具为主,具体版本应以 Open Lustre 发行说明为准。
5.1 实例与服务规划
一个最小测试集群至少需要三类节点:
| 节点角色 | 数量 | 最低规格 | 说明 |
|---|---|---|---|
| MGS + MDS 组合节点 | 1 | 4 vCPU / 8 GB 内存 | 元数据操作密集,磁盘建议使用高性能云盘 |
| OSS 节点 | 1~2 | 8 vCPU / 16 GB 内存 | 运行 ZFS 和 Lustre OST,内存越大 ARC 缓存效果越好 |
| Client 节点 | 1 | 2 vCPU / 4 GB 内存 | 挂载 Lustre 文件系统,用于读写测试 |
测试环境可以适当降低规格,但内存不建议小于 8 GB。ZFS 的 ARC 默认会占满可用内存的一半左右,如果内存太小,缓存效果会非常微弱,同时容易触发 OOM。
5.2 操作系统与内核版本
Lustre 作为 Linux 内核文件系统,对内核版本有严格要求。Open Lustre 的发行版通常会绑定特定内核版本,安装时需要使用官方提供的配套内核与软件包。ZFS 同样有内核版本兼容性要求。推荐的策略是:到 Open Lustre 官方网站查阅当前发行说明,选择与之匹配的内核和 ZFS 版本,不要自己从源码乱编。
5.3 对象存储准备
你需要一个对象存储 Bucket,以及该 Bucket 的访问密钥。安全方面有两个建议:
- 密钥只授权给 OSS 节点,最小权限即可,比如只允许读写某个前缀;
- 不要把长期密钥写死在系统盘镜像里,优先使用云厂商的实例角色或密钥管理服务。
5.4 网络准备
Lustre 客户端与服务器之间通过 LNet 通信,默认使用 TCP 988 端口。OSS 节点访问对象存储要走云厂商内网地址,不要走公网,否则延迟和流量费用都会显著上升。测试环境中,所有节点应处于同一个 VPC 内,互通延迟尽量低。
6. 测试环境部署步骤与参考命令
下面是一套可用于验证功能链路的部署步骤。注意,这些命令用于快速跑通"Lustre + ZFS + 对象存储"的组合,不代表该项目在生产环境中的最佳部署方式。生产环境部署必须经过更完整的网络、安全和稳定性验证。
6.1 准备对象存储挂载
在 OSS 节点上安装 rclone,并配置对象存储的访问权限。创建 Bucket 后,使用 rclone mount 将 Bucket 中的一个前缀挂载到本地目录。
# 安装 rclone 后,先执行 config 命令添加远端配置 rclone config # 将 Bucket 中的 testfs 前缀挂载到 /mnt/ost-fuse mkdir -p /mnt/ost-fuse rclone mount myoss:testfs-bucket /mnt/ost-fuse \ --allow-other --daemon --vfs-cache-mode writes参数说明:
--vfs-cache-mode writes:只缓存写入数据,尽量合并小写为合理的对象存储 PUT 请求;--daemon:后台运行;--allow-other:允许其他用户访问挂载点。
这只是一种快速接入方式。前面讲过,FUSE 挂载的性能有限,所以这里仅用于打通链路。
6.2 创建 ZFS 存储池
在 OSS 节点上安装 ZFS 后,先创建用于实验的文件 vdev。注意,不要在生产环境使用这种配置。
# 在对象存储挂载点上创建 vdev 文件 truncate -s 100G /mnt/ost-fuse/zfs-vdev.img # 创建 zpool zpool create -f -o ashift=12 zpool-ost /mnt/ost-fuse/zfs-vdev.img # 查看 zpool 状态 zpool status创建完成后,设置 ZFS dataset 的相关属性。针对对象存储的特性,建议关闭 atime、调大 recordsize、开启压缩。
# 在开始创建 Lustre OST 前,先设置池的默认参数 zfs set atime=off zpool-ost zfs set compression=lz4 zpool-ost zfs set recordsize=4M zpool-ost这里把 recordsize 设置为 4M,目的是让 ZFS 尽量以大块方式访问底层对象存储,减少请求次数。Lustre 条带本身也是大块 IO,两者配合更合适。
6.3 创建并挂载 MDT
MGS + MDS 节点上,使用一块本地高性能云盘创建 ZFS 池,并格式化 MDT。MDT 不要放在对象存储上。
# 在 MGS/MDS 节点上,以本地云盘 /dev/vdb 创建 zpool zpool create -f zpool-mdt /dev/vdb # 使用 mkfs.lustre 创建 MGS + MDT 组合设备 mkfs.lustre --fsname=testfs --mgs --mdt --index=0 \ zpool-mdt/mdt # 创建挂载目录并挂载 mkdir -p /mnt/mdt mount -t lustre zpool-mdt/mdt /mnt/mdt参数说明:
--fsname=testfs:指定文件系统名;--mgs:该设备同时运行 MGS 服务;--mdt:该设备作为 MDT;--index=0:MDT 索引为 0。
6.4 创建并挂载 OST
回到 OSS 节点,使用同一个 zpool 创建 OST。由于 ZFS 数据集会在 mkfs.lustre 时创建,所以不需要提前zfs create。
# 在 OSS 节点上,创建 OST mkfs.lustre --fsname=testfs --ost --index=0 \ --mgsnode=10.0.0.1@tcp zpool-ost/ost0 # 挂载 OST mkdir -p /mnt/ost0 mount -t lustre zpool-ost/ost0 /mnt/ost0这里的 IP 地址需要换成 MGS/MDS 节点的内网 IP。注意,这一步执行时,MGS/MDS 节点必须已经在运行,否则 OSS 无法注册。
6.5 启动客户端并验证挂载
在 Client 节点上安装 Lustre 客户端内核模块后,挂载文件系统。
# 挂载 Lustre 文件系统 mkdir -p /mnt/lustre mount -t lustre 10.0.0.1@tcp:/testfs /mnt/lustre # 查看文件系统基本信息 df -h /mnt/lustre lfs df -h如果输出中显示 OST 状态为 healthy,说明 OST 已成功注册并可用。
6.6 创建条带化目录
Lustre 支持为不同目录配置不同的条带策略。为了模拟典型的 HPC 大文件场景,可以创建一个目录,将条带数设置为 1,条带大小设置为 4MB。
mkdir -p /mnt/lustre/large_data lfs setstripe -c 1 -i 0 -S 4M /mnt/lustre/large_data这样写入 large_data 目录的新文件会被分配到 OST index 0,条带大小 4MB。如果后续扩展了多个 OST,可以调整-c参数让文件跨多个 OST,以获得更高并行带宽。
6.7 通过脚本进行并发写入测试
除了手工 dd,还可以用 Python 脚本模拟多个并发任务同时向 Lustre 写入大文件。下面这个脚本会创建 4 个线程,每个线程写入一个 512MB 的大文件,用于测试并发吞吐。
#!/usr/bin/env python3 """并发写大文件到 Lustre 文件系统,用于验证并行吞吐。""" import os import subprocess import threading MOUNT_POINT = "/mnt/lustre/large_data" FILE_SIZE_MB = 512 THREAD_COUNT = 4 def write_big_file(thread_index: int) -> None: file_path = os.path.join(MOUNT_POINT, f"bigfile_{thread_index}.bin") cmd = [ "dd", "if=/dev/zero", "of={}".format(file_path), "bs=1M", "count={}".format(FILE_SIZE_MB), "oflag=direct", "conv=fdatasync", ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print("thread {} failed: {}".format(thread_index, result.stderr)) else: size_mb = FILE_SIZE_MB print("thread {} finished, size {} MB".format(thread_index, size_mb)) threads = [ threading.Thread(target=write_big_file, args=(i,)) for i in range(THREAD_COUNT) ] for t in threads: t.start() for t in threads: t.join() print("all threads finished")脚本逻辑很简单:每个线程单独运行一个dd进程,向 Lustre 挂载点写入固定大小文件,用oflag=direct尽量绕过本地 page cache,使写入压力真实落到文件系统后端。如果多个线程能同时保持可观写入速度,说明 OSS 节点和对象存储之间的吞吐路径基本可用。
6.8 验证数据能否读回
写入完成后,读取文件并计算校验值,确认数据内容没有损坏。
# 计算写进去的文件的 md5 md5sum /mnt/lustre/large_data/bigfile_*.bin # 对比读取过程中的错误数量 for f in /mnt/lustre/large_data/bigfile_*.bin; do dd if="$f" of