☰

本文是一套可直接落地的生产方案:在 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

十、生产红线

  1. RUSTFS_RPC_SECRET 必须显式设置且独立于 AK/SK,否则分布式 RPC 通信异常。
  2. 路径风格寻址:JuiceFS --bucket 和 Secret 中 bucket 必须为 http://host:9000/bucket-name,RustFS 不支持虚拟主机风格。
  3. 不使用 JuiceFS --writeback:Pod 生命周期不稳定,写回缓存有数据丢失风险;持久性由 RustFS 纠删码保障。
  4. 保持 low: "0.1":JuiceFS 缓存与 RustFS 数据共用 /dataN,low 确保缓存盘保留至少 10% 剩余空间,防止挤占 RustFS 数据。
  5. rustfs/rc 命令前缀为 rc,不要与 mc 混用。
  6. 监控 df -i:XFS 的 inode64 已缓解 inode 分配压力,但仍建议将 /dataN 的 inode 使用率纳入日常监控基线。
  7. hostPath 权限边界:RustFS Pod 对 /dataN 有完整写权限,使用默认 default ServiceAccount 即可,不额外授予集群级 RBAC。

结语

本方案在磁盘层将 /data1~/data12 统一格式化为 XFS,挂载选项对齐 RustFS 生产推荐;RustFS 以 StatefulSet + 硬反亲和部署 15×12 拓扑,通过 hostPath 和 RUSTFS_VOLUMES 大括号展开直接引用节点磁盘;JuiceFS + Fluid 的缓存路径与 RustFS 数据子目录隔离,low 参数保护共享盘空间。整条链路无 OpenEBS 依赖,每个组件各司其职:

  • XFS 负责单盘文件系统的稳定与吞吐;
  • RustFS 负责跨节点的纠删码持久化;
  • JuiceFS 负责 POSIX 语义与元数据;
  • Fluid 负责缓存的编排与调度。

结构清晰,可直接用于部署。