【Kubernetes从入门到精通】第40篇:NFS和Ceph——自建分布式存储的那些年
上一篇【第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 │ │ 不需要 │
└──────────────┘ └──────────┘ └──────────────┘
| 对比维度 | CephFS | RBD |
|---|---|---|
| 类型 | 分布式文件系统 | 块设备 |
| 多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自动更新
更多推荐
所有评论(0)