上一篇【第39篇】本地持久化存储——Local PV和hostPath的正确使用姿势
下一篇【第41篇】ConfigMap和Secret的持久化——subPath挂载vs自动更新


摘要

如果你跑在AWS上,存储不是问题——EBS随叫随到。但如果你是在自建机房里搭K8s,怎么办?买不起SAN,用不起云盘,难道只能hostPath?

两条路:NFS(NAS的简化版,简单粗暴,搭个NFS服务器就能用)和Ceph(企业级分布式存储,复杂但强大,AWS的EBS底层其实就是类似的东西)。NFS适合小团队——一台服务器导出目录,所有Node挂载,配合nfs-subdir-external-provisioner就能实现动态PV供应。Ceph适合有追求的团队——MON集群保证高可用、OSD管理磁盘、PG自动分布数据、自带容错和副本。

本文带你从NFS的搭建讲到Ceph的架构,最后介绍Rook这个开源项目——它让Ceph能在K8s里一键部署,运维成本降到几乎为零。


一、NFS——最朴素的分布式存储

1.1 NFS是什么

NFS(Network File System)就是一个"网络优盘"——一台服务器把自己的某个目录通过网络共享出去,其他机器挂载后就能读写。简单、成熟、几乎所有操作系统都支持。

【NFS 架构——"一台服务器,所有人共用"】

  ┌───────────────────────────────────────────────────────────┐
  │                    NFS Server (192.168.1.100)             │
  │                                                           │
  │  /exports/                                                │
  │  ├── k8s-volumes/        ← 给K8s用的存储池                 │
  │  │   ├── pvc-abc123/     ← 动态创建的PVC子目录              │
  │  │   ├── pvc-def456/                                      │
  │  │   └── pvc-ghi789/                                      │
  │  └── backups/            ← 备份目录                       │
  └───────────────────────┬───────────────────────────────────┘
                          │ NFS Protocol (TCP/UDP 2049)
             ┌────────────┼────────────┐
             │            │            │
             ▼            ▼            ▼
        ┌─────────┐ ┌─────────┐ ┌─────────┐
        │ Node-1  │ │ Node-2  │ │ Node-3  │
        │         │ │         │ │         │
        │ Pod-A   │ │ Pod-B   │ │ Pod-C   │
        │ /data───│─│─/data───│─│─/data   │
        │         │ │         │ │         │
        │ 都看到    │ │ 同一份   │ │ 数据!   │
        └─────────┘ └─────────┘ └─────────┘

1.2 搭建NFS Server

# 在存储服务器上(CentOS/Ubuntu通用)
# Step 1: 安装NFS服务
sudo apt update && sudo apt install -y nfs-kernel-server  # Ubuntu
# sudo yum install -y nfs-utils                           # CentOS

# Step 2: 创建共享目录
sudo mkdir -p /exports/k8s-volumes
sudo chown nobody:nogroup /exports/k8s-volumes
sudo chmod 777 /exports/k8s-volumes

# Step 3: 配置导出
sudo tee -a /etc/exports <<EOF
/exports/k8s-volumes  *(rw,sync,no_subtree_check,no_root_squash)
EOF

# 参数说明:
# rw                - 读写权限
# sync              - 同步写入(数据安全,性能稍低)
# no_subtree_check  - 不检查父目录权限(性能优化)
# no_root_squash    - 允许root权限(容器内root可写)

# Step 4: 应用配置并启动
sudo exportfs -ra
sudo systemctl enable --now nfs-kernel-server

# Step 5: 防火墙放行(如果有)
sudo ufw allow from 10.0.0.0/8 to any port nfs

# 验证:在另一个Node上测试挂载
sudo mount -t nfs 192.168.1.100:/exports/k8s-volumes /mnt/test
ls /mnt/test
sudo umount /mnt/test

要点:NFS的配置简单到令人发指——就5步,15分钟搞定。但别被简单蒙蔽了——这个NFS Server是单点故障。它挂了,所有依赖的Pod就跪了(处于I/O阻塞状态)。生产环境建议用商业NAS设备(Synology/QNAP)或者至少配个NFS高可用(DRBD + Pacemaker,但配置复杂度会让你想回去装Ceph)。

1.3 在K8s中原生使用NFS Volume

# 最原始的方式——直接把Pod绑定到NFS
apiVersion: v1
kind: Pod
metadata:
  name: nfs-direct-pod
spec:
  volumes:
  - name: nfs-storage
    nfs:
      server: 192.168.1.100
      path: /exports/k8s-volumes/my-app
  containers:
  - name: app
    image: nginx:alpine
    volumeMounts:
    - name: nfs-storage
      mountPath: /usr/share/nginx/html
# 稍微好一点——用PV/PVC包装NFS
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
  - ReadWriteMany           # NFS支持RWX!多Pod同时读写
  - ReadWriteOnce
  - ReadOnlyMany
  nfs:
    server: 192.168.1.100
    path: /exports/k8s-volumes/pv001
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 50Gi

1.4 nfs-subdir-external-provisioner——NFS动态供应

手动创建NFS PV太傻了。nfs-subdir-external-provisioner让你享受StorageClass级别的动态供应体验:

【nfs-subdir-external-provisioner——NFS版动态供应】

  PVC: "我要10G NFS存储"
      │
      ▼
  StorageClass: nfs-client
      │
      ▼
  ┌────────────────────────────────────────────────────┐
  │  nfs-subdir-external-provisioner                  │
  │  ───────────────────────────────────────────────  │
  │  1. 收到PVC请求                                    │
  │  2. 在NFS上创建子目录:                            │
  │     /exports/k8s-volumes/<namespace>-<pvcname>-<uuid>/│
  │  3. 创建PV(NFS类型,path指向新建子目录)           │
  │  4. 绑定PVC到PV                                    │
  │  5. 删除PVC时自动清理子目录                         │
  └────────────────────────────────────────────────────┘
# 部署 nfs-subdir-external-provisioner(Helm方式)
helm repo add nfs-subdir-external-provisioner \
  https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

helm install nfs-provisioner \
  nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --set nfs.server=192.168.1.100 \
  --set nfs.path=/exports/k8s-volumes \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=true \
  --set storageClass.reclaimPolicy=Delete \
  --set storageClass.archiveOnDelete=false
# 部署完成后,创建StorageClass(Helm已自动创建,这是手动方式参考)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"   # PVC删除后是否归档数据
  pathPattern: "${.PVC.namespace}-${.PVC.name}"  # 子目录命名规则
# 现在你只需要写PVC——剩下的全自动!
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-data
spec:
  storageClassName: nfs-client    # 使用NFS StorageClass
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 10Gi
EOF

# 几秒后,NFS上自动创建了 default-my-app-data-xxxxx/ 目录
# PV自动创建、自动绑定!

要点nfs-subdir-external-provisioner是自建K8s存储的"入门首选"——用最小的成本(一台NFS服务器 + 一个Helm chart)实现动态PV供应。但它不是银弹:(1) NFS单点故障风险要自己承担;(2) 所有IO走网络,延迟比本地高出不少;(3) 不适合高并发随机IO场景(数据库)。


二、Ceph——分布式存储的"瑞士军刀"

2.1 Ceph是什么——不是一块硬盘,而是一套系统

如果NFS是"一个房东把房子分租出去",那Ceph就是"整个小区的物业管理系统"——自动管理所有房子、自动分配合适的租户、自动修复坏掉的房子。

【Ceph 核心架构——三大组件协同工作】

  ┌─────────────────────────────────────────────────────────────┐
  │                    Ceph 集群                                 │
  │                                                             │
  │  ┌─────────────────┐                                        │
  │  │  MON (Monitor)  │  监视器集群(奇数台,3/5/7)              │
  │  │  ───────────────│                                        │
  │  │  • 维护集群地图  │  "谁在集群里、谁不在、数据在哪"           │
  │  │  • 选举领导者    │                                        │
  │  │  • 仲裁决策      │  MON ≠ 存储数据,MON = 管家            │
  │  └────────┬────────┘                                        │
  │           │                                                 │
  │  ┌────────┴──────────────────────────────────────────┐      │
  │  │  OSD (Object Storage Daemon)                      │      │
  │  │  ────────────────────────────────────────────     │      │
  │  │  • 每个磁盘一个OSD进程                             │      │
  │  │  • 实际存储数据                                    │      │
  │  │  • 自动副本、自动恢复                               │      │
  │  │  • 默认3副本(你可以配)                            │      │
  │  │                                                   │      │
  │  │  Node-1              Node-2              Node-3   │      │
  │  │  ┌──────────┐       ┌──────────┐       ┌──────────┐│     │
  │  │  │ OSD.0    │       │ OSD.1    │       │ OSD.2    ││     │
  │  │  │ /dev/sdb │       │ /dev/sdb │       │ /dev/sdb ││     │
  │  │  │ OSD.3    │       │ OSD.4    │       │ OSD.5    ││     │
  │  │  │ /dev/sdc │       │ /dev/sdc │       │ /dev/sdc ││     │
  │  │  └──────────┘       └──────────┘       └──────────┘│     │
  │  └────────────────────────────────────────────────────┘      │
  │                                                             │
  │  ┌─────────────────┐                                        │
  │  │  MDS (Metadata   │  元数据服务器(仅CephFS需要)            │
  │  │  Server)        │                                        │
  │  │  ───────────────│  "文件A在哪些OSD上、文件B多大、           │
  │  │  • 文件系统元数据│   谁最近修改了文件C"                     │
  │  │  • 目录结构     │                                        │
  │  └─────────────────┘                                        │
  └─────────────────────────────────────────────────────────────┘

2.2 Ceph的核心概念——PG和CRUSH算法

Ceph真正的魔力在Placement Group(PG)和CRUSH算法:

【数据在Ceph中如何分布——PG和CRUSH】

  你的文件 → 切成多个Object(默认4MB一个)

  Object → 哈希 → 分配到某个PG → PG通过CRUSH算法 → 确定存在哪些OSD上

  ────────────────────────────────────────────────────────

  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │ Object-1 │     │ Object-2 │     │ Object-3 │
  └────┬─────┘     └────┬─────┘     └────┬─────┘
       │ hash            │ hash            │ hash
       ▼                 ▼                 ▼
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  PG-42   │     │ PG-17    │     │ PG-99    │
  └────┬─────┘     └────┬─────┘     └────┬─────┘
       │                 │                 │
       │ CRUSH("PG-42")  │ CRUSH("PG-17")  │ CRUSH("PG-99")
       ▼                 ▼                 ▼
  ┌─────────────────────────────────────────────┐
  │ PG-42 → OSD.0 (主), OSD.3 (副本1), OSD.7 (副本2)│
  │ PG-17 → OSD.5 (主), OSD.1 (副本1), OSD.2 (副本2)│
  │ PG-99 → OSD.4 (主), OSD.6 (副本1), OSD.0 (副本2)│
  └─────────────────────────────────────────────┘

  CRUSH算法的威力:
  - 不需要中心化的元数据表(不像HDFS的NameNode)
  - 任何客户端都知道数据在哪(计算PG→OSD映射)
  - OSD挂了,CRUSH自动重新计算→数据自动迁移到健康的OSD
  - 完全去中心化——没有单点瓶颈

要点:CRUSH是Ceph的"智能魔方"——你给它一个PG编号,它就能算出这个PG的数据存在哪些OSD上。不需要查表、不需要中心节点、任何客户端都是平等的。这就是Ceph能做到"无限扩展、去中心化"的根本原因。NFS要查服务器地址,Ceph直接算——这就是架构上的降维打击。

2.3 CephFS vs RBD——选哪个?

Ceph对外提供三种存储接口,K8s里常用的是CephFS和RBD:

【Ceph 的三种存储接口】

  ┌────────────────────────────────────────────────┐
  │                  Ceph 存储集群                   │
  │                                                │
  │  RADOS (Reliable Autonomic Distributed Object  │
  │         Store) - 底层对象存储                    │
  └──────────┬──────────┬──────────┬───────────────┘
             │          │          │
             ▼          ▼          ▼
  ┌──────────────┐ ┌──────────┐ ┌──────────────┐
  │   CephFS     │ │  RBD     │ │  RADOSGW     │
  │ (文件系统)    │ │ (块设备)  │ │ (对象存储)    │
  │              │ │          │ │              │
  │ 像NFS一样用   │ │ 像硬盘    │ │ 像S3一样用   │
  │ 多Pod共享     │ │ 单Pod独占 │ │ HTTP API     │
  │ 需要MDS      │ │ 不需要MDS │ │ 不需要        │
  └──────────────┘ └──────────┘ └──────────────┘
对比维度CephFSRBD
类型分布式文件系统块设备
多Pod挂载✅ RWX(多读多写)❌ 仅RWO(单Pod读写)
MDS需要?✅ 需要MDS服务❌ 不需要
适用场景共享文件存储、多Pod读写数据库、单Pod独占存储
性能文件系统开销,略低于RBD裸块设备,性能更高
一致性强一致性强一致性
K8s支持✅ CSI Driver✅ CSI Driver
# CephFS PVC(多Pod共享)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cephfs-shared
spec:
  accessModes:
  - ReadWriteMany           # CephFS 支持 RWX!
  resources:
    requests:
      storage: 100Gi
  storageClassName: cephfs
---
# RBD PVC(单Pod独占,数据库专属)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rbd-db-data
spec:
  accessModes:
  - ReadWriteOnce           # RBD 只支持 RWO
  resources:
    requests:
      storage: 200Gi
  storageClassName: rbd-ssd

要点:选CephFS还是RBD,本质上是选"我要多Pod共享还是单Pod独占"。多Pod需要看同一份文件 → CephFS。数据库要最高性能、独占块设备 → RBD。两个可以在同一个Ceph集群里共存,只是用的接口不同。


三、Rook——让Ceph在K8s里一键部署

3.1 Rook解决了什么

传统Ceph部署有多痛苦?大概需要:

  • 手动配5-10台服务器
  • 每台装Ceph包、配置OSD、加MON
  • ceph-deploy/cephadm 学半天
  • 出了问题只能上Ceph社区求助

Rook一句话:把Ceph变成K8s的一个Operator——用K8s的CRD定义Ceph集群,Rook Operator自动把Ceph的各个组件(MON、OSD、MDS、RGW)变成Pod运行在K8s上。

【Rook 架构——Ceph变成K8s原生应用】

  ┌──────────────────────────────────────────────────────────┐
  │                      Kubernetes                          │
  │                                                          │
  │  ┌────────────────────────────────────────────────────┐  │
  │  │              Rook Operator                         │  │
  │  │  (Deployment, 监控CephCluster CR, 管理Ceph组件)     │  │
  │  └──────────────────┬─────────────────────────────────┘  │
  │                     │                                     │
  │        ┌────────────┼────────────┐                        │
  │        │            │            │                        │
  │        ▼            ▼            ▼                        │
  │  ┌──────────┐ ┌──────────┐ ┌──────────┐                 │
  │  │MON Pod   │ │MON Pod   │ │MON Pod   │  (3个MON)        │
  │  │Node-1    │ │Node-2    │ │Node-3    │                 │
  │  └──────────┘ └──────────┘ └──────────┘                 │
  │                                                          │
  │  ┌──────────┐ ┌──────────┐ ┌──────────┐                 │
  │  │OSD Pod   │ │OSD Pod   │ │OSD Pod   │                 │
  │  │OSD.0     │ │OSD.1     │ │OSD.2     │  (N个OSD)       │
  │  │/dev/sdb  │ │/dev/sdb  │ │/dev/sdb  │                 │
  │  │Node-1    │ │Node-2    │ │Node-3    │                 │
  │  └──────────┘ └──────────┘ └──────────┘                 │
  │                                                          │
  │  ┌──────────┐ ┌──────────┐                               │
  │  │MDS Pod   │ │MDS Pod   │  (CephFS元数据,高可用)       │
  │  │Node-1    │ │Node-2    │                               │
  │  └──────────┘ └──────────┘                               │
  └──────────────────────────────────────────────────────────┘

3.2 Rook 一键部署 Ceph

# Step 1: 部署Rook Operator(Helm方式)
helm repo add rook-release https://charts.rook.io/release
helm install --create-namespace --namespace rook-ceph rook-ceph \
  rook-release/rook-ceph

# Step 2: 创建Ceph集群(就这一个CRD!)
kubectl apply -f - <<EOF
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    image: quay.io/ceph/ceph:v17.2.7     # Quincy版本
  dataDirHostPath: /var/lib/rook          # 每个Node上的数据目录
  mon:
    count: 3                              # 3个MON(高可用)
    allowMultiplePerNode: false
  storage:
    useAllNodes: true
    useAllDevices: true                   # 使用所有未格式化的磁盘
    # deviceFilter: "^sd[b-c]"            # 或者指定特定磁盘
  dashboard:
    enabled: true
    ssl: true
  # 可选:指定特定Node和磁盘
  # nodes:
  # - name: node-1
  #   devices:
  #   - name: /dev/nvme0n1
  # - name: node-2
  #   devices:
  #   - name: /dev/nvme0n1
EOF

# Step 3: 等待集群就绪
kubectl -n rook-ceph get cephcluster
# NAME        DATADIRHOSTPATH   MONCOUNT   AGE   PHASE   MESSAGE
# rook-ceph   /var/lib/rook     3          10m   Ready   Cluster created successfully

# 查看Ceph状态
kubectl -n rook-ceph exec -it deploy/rook-ceph-tools -- ceph status
#   cluster:
#     id:     a1b2c3d4-...
#     health: HEALTH_OK
#   services:
#     mon: 3 daemons
#     mgr: a(active)
#     osd: 6 osds: 6 up, 6 in
#   data:
#     pools:   1 pools, 1 pgs
#     objects: 0 objects
#     usage:   6.0 GiB used / 600 GiB avail

# Step 4: 创建StorageClass(Rook自动创建,手动参考)
# CephFS StorageClass ——支持RWX
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  fsName: myfs
  pool: myfs-data0
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: true
---
# RBD StorageClass ——高性能块存储
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
  clusterID: rook-ceph
  pool: replicapool
  imageFormat: "2"
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: true
# 使用CephFS(多Pod共享文件)
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-data
spec:
  storageClassName: rook-cephfs
  accessModes:
  - ReadWriteMany          # CephFS 支持!
  resources:
    requests:
      storage: 50Gi
EOF

# 使用RBD(数据库高性能)
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 200Gi
EOF

要点:Rook让Ceph从"运维噩梦"变成"K8s原生体验"——以前搭Ceph要几周、你搭完还不敢改配置(怕搞崩),现在一条kubectl apply命令就能搭、扩容缩容改配置都是改CRD。Rook Operator自动处理Ceph的生命周期管理。这可以说是自建K8s存储的最优方案了。


四、NFS vs Ceph vs Rook+Ceph——终极选型指南

【自建K8s存储方案选择决策树】

  你的场景是什么?
       │
       ├── 小团队(<10人),预算有限,能接受单点故障?
       │   └── NFS + nfs-subdir-external-provisioner
       │       优点:15分钟搞定,运维成本低
       │       缺点:NFS服务器是单点
       │
       ├── 中型团队,对可用性有要求,有专业运维?
       │   └── Ceph (手动部署)
       │       优点:高可用、自动容错、性能好
       │       缺点:运维成本高,需要懂Ceph
       │
       └── 有K8s经验,想一劳永逸?
           └── Rook + Ceph
               优点:K8s原生、自动运维、一键部署
               缺点:需要K8s经验,资源占用稍高
方案复杂度可靠性性能运维成本推荐场景
NFS⭐ 极低⭐⭐ 单点⭐⭐ 中等⭐ 极低开发/测试、小团队生产
Ceph手动⭐⭐⭐⭐ 高⭐⭐⭐⭐⭐ 极高⭐⭐⭐⭐ 高⭐⭐⭐⭐ 高大公司有专业运维
Rook+Ceph⭐⭐⭐ 中等⭐⭐⭐⭐⭐ 极高⭐⭐⭐⭐ 高⭐⭐ 低最佳平衡方案
NFS Provisioner⭐⭐ 低⭐⭐ 单点⭐⭐ 中等⭐⭐ 低快速上手过渡

本篇小结

自建机房的K8s存储,两条技术路线各有千秋:

NFS路线(入门):一台服务器+一个Helm chart搞定。nfs-subdir-external-provisioner让你享受动态供应,但NFS本身是单点故障——服务器挂了全跪。适合非关键业务或作为临时方案。

Ceph路线(进阶):MON集群保高可用、OSD管理磁盘、CRUSH算法智能分布数据。CephFS(多Pod共享)和RBD(单Pod高性能)覆盖全部场景。传统部署复杂,但Rook让一切变得简单——Ceph变成K8s上的原生应用,一键部署、自动运维。

我的建议:先上NFS Provisioner快速跑起来,然后花时间学Rook+Ceph做长期方案。别一上来就搞手动Ceph——你会被MON选举、OSD故障、PG状态这些概念折磨死。

下一篇聊ConfigMap和Secret的持久化——这两种K8s原生对象虽然叫"持久化",但它们的存储方式和使用姿势有很多坑。


上一篇【第39篇】本地持久化存储——Local PV和hostPath的正确使用姿势
下一篇【第41篇】ConfigMap和Secret的持久化——subPath挂载vs自动更新


更多推荐