XFS + RustFS + JuiceFS + Fluid:15 节点 × 12 NVMe 的分布式存储与缓存方案
本文是一套可直接落地的生产方案:在 15 个节点、每节点 12 块 NVMe 的 Kubernetes 集群上,用 XFS + RustFS + JuiceFS + Fluid 搭建分布式对象存储与缓存加速体系。整条链路不依赖 OpenEBS Local PV Provisioner,直接使用 hostPath 引用节点本地盘,结构清晰、组件最少。
一、方案概要
- 文件系统:每节点 12 块数据盘统一格式化为 XFS,挂载选项包含
noatime,inode64,logbsize=256k,对齐 RustFS 官方生产推荐。 - S3 底座:RustFS 1.0.0 GA,以自定义 StatefulSet 部署 15 个副本,配合硬性 Pod 反亲和确保每节点恰好一个 RustFS Pod,每个 Pod 通过
hostPath直接挂载/data1~/data12。 - 缓存层:JuiceFS + Fluid,Worker 缓存路径为
/dataN/jfs-cache,与 RustFS 的/dataN/rustfs-data子目录隔离。 - 元数据:云 Redis(独立于 S3 层)。
- 不包含:OpenEBS Local PV Provisioner(改用
hostPath,少一个组件就少一层故障面)。
整体拓扑:
1Pod(PVC: jfs-prod)
2 └─ JuiceFS FUSE(Fluid 注入)
3 ├─ 元数据:云 Redis
4 └─ 数据块:RustFS S3(http://rustfs.rustfs.svc:9000/jfs-data)
5 └─ 15 × RustFS Pod(每节点 1 个)
6 └─ /data1..12/rustfs-data(XFS,NVMe)
7 └─ 本地缓存:/data1..12/jfs-cache(与 RustFS 数据同盘、目录隔离)
二、阶段一:磁盘识别与系统盘隔离(15 台全部执行)
目的:确认系统盘与 12 块数据盘的设备名,避免误格式化系统盘。
1lsblk -o NAME,SIZE,MODEL,MOUNTPOINT,FSTYPE
2df -h /
记录系统盘(通常挂载 / 或 /boot 的设备,如 /dev/nvme0n1),确认其余 12 块为待格式化数据盘(如 /dev/nvme1n1~/dev/nvme12n1)。
本阶段只识别,不执行任何写入操作。
三、阶段二:XFS 格式化与持久化挂载(15 台全部执行)
1. 格式化命令
RustFS 官方磁盘准备流程要求每块数据盘为独立 XFS 文件系统,使用稳定标签,并通过 UUID 挂载。结合 XFS 在对象存储场景的优化参数(inode64 允许 inode 分布在整个文件系统空间,避免早期空间耗尽;logbsize=256k 提升日志吞吐;noatime 减少无关写入),格式化命令如下:
1for i in $(seq 1 12); do
2 dev="/dev/nvme${i}n1" # 以实际设备名为准
3 label="RUSTFS${i}"
4 mkfs.xfs -f -L "$label" -i size=512 -n size=64k "$dev"
5done
参数说明:-i size=512 为对象存储海量小文件场景提供足够的 inode 密度;-n size=64k 增大目录块大小,有利于大量并发文件的目录项操作。如果对 -n size=64k 有兼容性顾虑,可仅保留 -L 标签,使用 mkfs.xfs 默认参数——挂载选项中的优化更为关键。
2. 获取 UUID 并写入 /etc/fstab
官方推荐按 UUID 挂载,避免设备名变化或标签重复导致挂载错误:
1for i in $(seq 1 12); do
2 dev="/dev/nvme${i}n1"
3 mnt="/data${i}"
4 uuid=$(blkid -s UUID -o value "$dev")
5 mkdir -p "$mnt"
6 echo "UUID=$uuid $mnt xfs defaults,noatime,inode64,logbsize=256k 0 2" >> /etc/fstab
7done
8mount -a
挂载选项 noatime 是 RustFS 官方明确建议的性能选项;inode64 和 logbsize=256k 来自 XFS 在对象存储/OSD 场景的成熟调优实践。
3. 创建 RustFS 数据子目录
在 /dataN 根目录下为 RustFS 创建独立子目录,不直接使用挂载点根目录,以便与 JuiceFS 缓存目录隔离:
1for i in $(seq 1 12); do
2 mkdir -p /data${i}/rustfs-data
3done
4. 验证
1# 确认 12 个挂载点均为 XFS
2df -hT /data1 /data2 /data12
3mount | grep /data | grep xfs | wc -l # 期望 12
4
5# 确认 inode 可用空间充足(后续监控基线)
6df -i /data1 /data2 /data12
四、阶段三:创建 RustFS 命名空间、Secret 与 Headless Service
1# rustfs-secret.yaml
2apiVersion: v1
3kind: Secret
4metadata:
5 name: rustfs-secret
6 namespace: rustfs
7type: Opaque
8stringData:
9 RUSTFS_ACCESS_KEY: "rustfs-admin-change-me"
10 RUSTFS_SECRET_KEY: "rustfs-secret-change-me" # 不能与 access_key 相同
11 RUSTFS_RPC_SECRET: "rpc-secret-change-me" # 独立于 AK/SK
1# rustfs-service.yaml
2apiVersion: v1
3kind: Service
4metadata:
5 name: rustfs
6 namespace: rustfs
7spec:
8 clusterIP: None
9 selector:
10 app: rustfs
11 ports:
12 - port: 9000
13 name: s3
14 - port: 9001
15 name: console
1kubectl create namespace rustfs
2kubectl apply -f rustfs-secret.yaml
3kubectl apply -f rustfs-service.yaml
五、阶段四:部署 RustFS StatefulSet(15 节点 × 12 盘)
关键设计:使用 StatefulSet + requiredDuringSchedulingIgnoredDuringExecution 硬反亲和,确保 15 个 Pod 恰好分散到 15 个节点,每个 Pod 通过 hostPath 看到该节点自己的 /data1~/data12。RUSTFS_VOLUMES 使用大括号展开为 /data{1...12}/rustfs-data,RustFS 根据 12 盘拓扑自动确定纠删码策略(默认 EC:4)。
1# rustfs-statefulset.yaml
2apiVersion: apps/v1
3kind: StatefulSet
4metadata:
5 name: rustfs
6 namespace: rustfs
7spec:
8 serviceName: rustfs
9 replicas: 15
10 podManagementPolicy: Parallel
11 selector:
12 matchLabels:
13 app: rustfs
14 template:
15 metadata:
16 labels:
17 app: rustfs
18 spec:
19 affinity:
20 podAntiAffinity:
21 requiredDuringSchedulingIgnoredDuringExecution:
22 - labelSelector:
23 matchExpressions:
24 - key: app
25 operator: In
26 values: ["rustfs"]
27 topologyKey: kubernetes.io/hostname
28 containers:
29 - name: rustfs
30 image: rustfs/rustfs:1.0.0
31 imagePullPolicy: IfNotPresent
32 env:
33 - name: RUSTFS_ACCESS_KEY
34 valueFrom:
35 secretKeyRef:
36 name: rustfs-secret
37 key: RUSTFS_ACCESS_KEY
38 - name: RUSTFS_SECRET_KEY
39 valueFrom:
40 secretKeyRef:
41 name: rustfs-secret
42 key: RUSTFS_SECRET_KEY
43 - name: RUSTFS_RPC_SECRET
44 valueFrom:
45 secretKeyRef:
46 name: rustfs-secret
47 key: RUSTFS_RPC_SECRET
48 - name: RUSTFS_VOLUMES
49 value: "/data{1...12}/rustfs-data"
50 - name: RUSTFS_ADDRESS
51 value: ":9000"
52 - name: RUSTFS_CONSOLE_ENABLE
53 value: "true"
54 ports:
55 - containerPort: 9000
56 name: s3
57 - containerPort: 9001
58 name: console
59 resources:
60 requests:
61 cpu: "4"
62 memory: "16Gi"
63 limits:
64 cpu: "16"
65 memory: "32Gi"
66 volumeMounts:
67 - { name: data1, mountPath: /data1 }
68 - { name: data2, mountPath: /data2 }
69 - { name: data3, mountPath: /data3 }
70 - { name: data4, mountPath: /data4 }
71 - { name: data5, mountPath: /data5 }
72 - { name: data6, mountPath: /data6 }
73 - { name: data7, mountPath: /data7 }
74 - { name: data8, mountPath: /data8 }
75 - { name: data9, mountPath: /data9 }
76 - { name: data10, mountPath: /data10 }
77 - { name: data11, mountPath: /data11 }
78 - { name: data12, mountPath: /data12 }
79 volumes:
80 - { name: data1, hostPath: { path: /data1, type: Directory } }
81 - { name: data2, hostPath: { path: /data2, type: Directory } }
82 - { name: data3, hostPath: { path: /data3, type: Directory } }
83 - { name: data4, hostPath: { path: /data4, type: Directory } }
84 - { name: data5, hostPath: { path: /data5, type: Directory } }
85 - { name: data6, hostPath: { path: /data6, type: Directory } }
86 - { name: data7, hostPath: { path: /data7, type: Directory } }
87 - { name: data8, hostPath: { path: /data8, type: Directory } }
88 - { name: data9, hostPath: { path: /data9, type: Directory } }
89 - { name: data10, hostPath: { path: /data10, type: Directory } }
90 - { name: data11, hostPath: { path: /data11, type: Directory } }
91 - { name: data12, hostPath: { path: /data12, type: Directory } }
1kubectl apply -f rustfs-statefulset.yaml
2kubectl -n rustfs get pods -o wide # 期望 15 个 Running,NODE 列恰好覆盖 15 个节点
六、阶段五:创建 JuiceFS 桶与最小权限密钥(rustfs/rc)
使用 RustFS 官方 CLI 镜像 rustfs/rc:latest,命令前缀为 rc:
1kubectl -n rustfs run rc --rm -it --image=rustfs/rc:latest -- sh
容器内:
1rc alias set rustfs http://rustfs.rustfs.svc.cluster.local:9000 \
2 rustfs-admin-change-me rustfs-secret-change-me --api S3v4
3
4rc mb rustfs/jfs-data --ignore-existing
5
6rc admin user add rustfs jfsUser '换成强密码'
7
8cat > /tmp/jfs-policy.json <<'EOF'
9{
10 "Version": "2012-10-17",
11 "Statement": [
12 {
13 "Effect": "Allow",
14 "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
15 "Resource": ["arn:aws:s3:::jfs-data", "arn:aws:s3:::jfs-data/*"]
16 }
17 ]
18}
19EOF
20
21rc admin policy create rustfs jfs-policy /tmp/jfs-policy.json
22rc admin policy attach rustfs jfs-policy --user jfsUser
七、阶段六:JuiceFS 格式化与 Fluid Secret
1. 格式化 JuiceFS
1juicefs format \
2 --storage s3 \
3 --bucket http://rustfs.rustfs.svc.cluster.local:9000/jfs-data \
4 --access-key jfsUser \
5 --secret-key 'jfsUser的强密码' \
6 "redis://:你的Redis密码@云Redis地址:6379/1" \
7 jfs-prod
2. 创建 jfs-secret
1# jfs-secret.yaml
2apiVersion: v1
3kind: Secret
4metadata:
5 name: jfs-secret
6 namespace: default
7type: Opaque
8stringData:
9 name: "jfs-prod"
10 metaurl: "redis://:你的Redis密码@云Redis地址:6379/1"
11 storage: "s3"
12 bucket: "http://rustfs.rustfs.svc.cluster.local:9000/jfs-data"
13 access-key: "jfsUser"
14 secret-key: "jfsUser的强密码"
1kubectl apply -f jfs-secret.yaml
八、阶段七:Dataset 与 JuiceFSRuntime
1. Dataset
1# dataset.yaml
2apiVersion: data.fluid.io/v1alpha1
3kind: Dataset
4metadata:
5 name: jfs-prod
6 namespace: default
7spec:
8 mounts:
9 - name: jfs-prod
10 mountPoint: "juicefs:///"
11 encryptOptions:
12 - { name: name, valueFrom: { secretKeyRef: { name: jfs-secret, key: name } } }
13 - { name: metaurl, valueFrom: { secretKeyRef: { name: jfs-secret, key: metaurl } } }
14 - { name: storage, valueFrom: { secretKeyRef: { name: jfs-secret, key: storage } } }
15 - { name: bucket, valueFrom: { secretKeyRef: { name: jfs-secret, key: bucket } } }
16 - { name: access-key, valueFrom: { secretKeyRef: { name: jfs-secret, key: access-key } } }
17 - { name: secret-key, valueFrom: { secretKeyRef: { name: jfs-secret, key: secret-key } } }
2. JuiceFSRuntime
tieredstore.levels.path 支持多路径以 : 分隔,quota 为总缓存配额(与路径数量无关),low 为每个路径的最低剩余空间比例。volumeType: hostPath 将宿主机目录挂载为缓存路径,适用于特权模式受限或需要直接利用节点 NVMe 的场景。
1# juicefsruntime.yaml
2apiVersion: data.fluid.io/v1alpha1
3kind: JuiceFSRuntime
4metadata:
5 name: jfs-prod
6 namespace: default
7spec:
8 replicas: 3
9 tieredstore:
10 levels:
11 - mediumtype: SSD
12 path: "/data1/jfs-cache:/data2/jfs-cache:/data3/jfs-cache:/data4/jfs-cache:/data5/jfs-cache:/data6/jfs-cache:/data7/jfs-cache:/data8/jfs-cache:/data9/jfs-cache:/data10/jfs-cache:/data11/jfs-cache:/data12/jfs-cache"
13 quota: 500Gi
14 low: "0.1"
15 volumeType: hostPath
16 worker:
17 options:
18 "cache-size": "614400" # MiB,600 GiB 级别,按需调整
19 fuse:
20 options:
21 "cache-size": "0" # FUSE 不额外占用本地盘
1kubectl apply -f dataset.yaml
2kubectl apply -f juicefsruntime.yaml
3kubectl get dataset jfs-prod # 期望 STATUS: Bound
九、阶段八:端到端验证
1. PVC 写入
Fluid 会按 Dataset 名称自动创建同名 PVC,用一个测试 Pod 挂载验证读写:
1# test-pod.yaml
2apiVersion: v1
3kind: Pod
4metadata:
5 name: demo-app
6 namespace: default
7spec:
8 containers:
9 - name: demo
10 image: busybox:1.36
11 command: ["sh", "-c", "echo hello-juicefs >> /data/test.log && sleep 3600"]
12 volumeMounts:
13 - name: data
14 mountPath: /data
15 volumes:
16 - name: data
17 persistentVolumeClaim:
18 claimName: jfs-prod
1kubectl apply -f test-pod.yaml
2kubectl exec -it demo-app -- cat /data/test.log
2. RustFS 侧确认对象已落盘
1kubectl -n rustfs run rc --rm -it --image=rustfs/rc:latest -- sh
容器内:
1rc alias set rustfs http://rustfs.rustfs.svc.cluster.local:9000 <AK> <SK>
2rc ls rustfs/jfs-data/chunks/
3. Fluid 缓存生效
1kubectl exec -it jfs-prod-worker-0 -- df -h | grep jfs-cache
十、生产红线
RUSTFS_RPC_SECRET必须显式设置且独立于 AK/SK,否则分布式 RPC 通信异常。- 路径风格寻址:JuiceFS
--bucket和 Secret 中bucket必须为http://host:9000/bucket-name,RustFS 不支持虚拟主机风格。 - 不使用 JuiceFS
--writeback:Pod 生命周期不稳定,写回缓存有数据丢失风险;持久性由 RustFS 纠删码保障。 - 保持
low: "0.1":JuiceFS 缓存与 RustFS 数据共用/dataN,low确保缓存盘保留至少 10% 剩余空间,防止挤占 RustFS 数据。 rustfs/rc命令前缀为rc,不要与mc混用。- 监控
df -i:XFS 的inode64已缓解 inode 分配压力,但仍建议将/dataN的 inode 使用率纳入日常监控基线。 hostPath权限边界:RustFS Pod 对/dataN有完整写权限,使用默认defaultServiceAccount 即可,不额外授予集群级 RBAC。
结语
本方案在磁盘层将 /data1~/data12 统一格式化为 XFS,挂载选项对齐 RustFS 生产推荐;RustFS 以 StatefulSet + 硬反亲和部署 15×12 拓扑,通过 hostPath 和 RUSTFS_VOLUMES 大括号展开直接引用节点磁盘;JuiceFS + Fluid 的缓存路径与 RustFS 数据子目录隔离,low 参数保护共享盘空间。整条链路无 OpenEBS 依赖,每个组件各司其职:
- XFS 负责单盘文件系统的稳定与吞吐;
- RustFS 负责跨节点的纠删码持久化;
- JuiceFS 负责 POSIX 语义与元数据;
- Fluid 负责缓存的编排与调度。
结构清晰,可直接用于部署。
