1. 写在前面:单节点集群为什么也要折腾StorageClass
先聊点实际的。很多人觉得单节点K8s集群嘛,就是拿来练练手、跑跑测试,能调度Pod不就行了,存储够用就行,搞什么StorageClass。我在自己搭环境的时候也是这个心态,直到第一次在单节点上部署一个有状态应用——当时部署的是带数据库的私有化工具,创建Pod之后发现PVC一直Pending,事件里写着waiting for a volume to be created, either by external provisioner or manually created by system administrator。当时一查,集群里根本没有默认StorageClass,有状态应用直接卡死在创建卷这一步。
这事给我提了个醒:不管你的集群是一个节点还是几十个节点,只要你想跑任何需要持久化数据的应用——数据库、消息队列、带挂载目录的中间件、日志收集组件——StorageClass就是绕不开的基础设施。单节点集群虽然只有一台机器,但它依然是完整意义上的K8s环境,Pod调度、持久化存储、配置管理这些机制一个都不少。没有StorageClass,等于你把房子盖好了但没通水管,住进去才发现洗澡都成问题。
这篇文章写给谁?给那些在自己电脑上、虚拟机里、或者一台物理服务器上用kubeadm、k3s、minikube之类工具搭了单节点K8s集群的人,尤其是刚入门没多久、对PV、PVC、StorageClass三者关系还有点模糊,想给集群补上持久化存储能力,但不知道从哪里下手的同学。我按实际操作的顺序把整件事拆开讲:先理清概念,再选型,再动手装,最后把踩过的坑和排查思路全盘托出。按着这个流程走一遍,你的单节点集群也能有一个能用的StorageClass。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先理清概念:PV、PVC、StorageClass三者到底什么关系
动手之前不把概念理清楚,装完可能也是懵的,出了问题更是一头雾水。网上关于PV、PVC、StorageClass的解释很多,但大部分要么太学术,要么绕来绕去。我用自己的话把这个事讲透。
2.1 从一个实际需求说起
假设你在K8s里部署了一个MySQL,数据必须写进磁盘,而且Pod重启、删除、重新调度之后,数据都还在。那你就需要一块“持久化存储”。问题是K8s里的存储不像我们平时插U盘那样即插即用,它有一整套抽象机制,把“存储的提供方”和“存储的使用方”彻底分开。
这里有三层概念:
- PV(PersistentVolume,持久化卷):这是存储资源的实体,它可以是NFS服务上的一个目录、云服务商的一块云盘、或者本地宿主机上的一个目录。PV是集群层面的资源,由管理员创建,或者说由系统自动创建,它跟具体某个Pod没有直接关系。
- PVC(PersistentVolumeClaim,持久化卷声明):这是用户或者说工作负载对存储的请求,像一个“申请单”,告诉K8s“我需要多大空间、什么读写模式”。K8s会把PVC和一个能满足条件的PV绑定在一起。
- StorageClass(存储类):这是最上面的一层抽象,它规定了“当PVC来申请存储时,系统应该怎么创建PV”。StorageClass像一个模板,里面写好了存储的供应方式、回收策略、挂载参数等。
2.2 用租房来类比
我习惯用租房来类比这三者的关系。PV就是你在小区里拥有的几套房,这些房子是实实在在的资产;PVC就是你去中介那里提出的需求,说我要租一个两室一厅、月租三千以内;StorageClass则是中介的“自动找房系统”,只要需求一提交,系统立刻按照你设定的条件去匹配或者新建一套房子。没有StorageClass的时候,你得手动去找合适的PV然后跟PVC绑定,就像没有中介服务,自己跑断腿去联系房东。有了StorageClass,PVC一提交,系统自动就把存储卷给你建好了,省去大量人工操作。
2.3 手动绑定和动态供给的区别
理解了这三层关系,你会发现StorageClass真正牛的地方在于“动态供给”。没有StorageClass的传统做法是:管理员预先创建一批PV,用户在PVC里提出需求,K8s的调度器去匹配一个大小合适、模式兼容的PV,然后绑定。这个流程对运维人员来说相当痛苦,你得预估集群未来需要多少存储,提前准备PVC,实际用量往往和预估差得很远,不是浪费就是不够用。
有了StorageClass之后,整个过程变成了:用户创建PVC,声明需要的存储大小和访问模式,StorageClass里的provisioner组件看到这个PVC之后,自动去底层存储系统创建一个PV,然后绑定给PVC。整个过程不需要人工介入,扩容、新开存储都是秒级完成。这也是为什么现在几乎所有云平台和自建集群都会默认安装一个StorageClass的原因——它是动态存储的基础。
2.4 单节点集群的特殊性
单节点集群和真正的生产集群在StorageClass这块有一个本质区别:生产环境通常有分布式存储系统(比如Ceph、GlusterFS、云盘),底层存储本身是高可用的;而单节点集群只有一台机器,你压根不需要分布式存储,因为只有一个节点的数据副本没有任何意义(副本都落在同一块磁盘上),还会白白增加配置复杂度。所以在单节点上,最优解其实是把本地磁盘用起来,让K8s在这个基础上提供动态供给的StorageClass能力。这意味着我们的方案选型会跟生产环境完全不一样,这一点我下面会展开说。
3. 方案选型:单节点集群装哪种StorageClass最合适
搞清楚概念之后,下一步就是选型。网上搜StorageClass相关的资料,出来最多的方案是NFS、Ceph RBD、云盘插件三件套。但放在单节点集群这个场景下,这三者各有各的问题,我先逐一分析,再给结论。
3.1 NFS:好用但要多维护一个服务
NFS是很多入门教程里推荐的方案,配置不算复杂,K8s官方也提供了NFS的provisioner(nfs-subdir-external-provisioner)。NFS的核心思路是:在一台机器上开一个NFS服务,导出某个目录,然后让K8s通过NFS协议去读写数据。
单节点集群里装NFS当然可以做,但有个很尴尬的问题:K8s节点和NFS服务在同一台物理机器上。数据从K8s的Pod里写出来,经过网络协议栈再到本机的NFS服务,然后再写回本地磁盘,这中间多了一层不必要的网络封装。如果你是想搭一个接近生产架构的环境,NFS可以帮你练手;但如果只是想让单节点集群有持久化能力,这层封装纯属给自己找麻烦。另外一个让人头疼的问题是,NFS服务需要你单独维护,比如设置开机自启、处理权限、处理断连,还多了一个故障点。单节点环境本身就图省事,再加一个外部依赖,体验并不好。
3.2 Ceph RBD:重量级选手,不适合单节点
Ceph RBD是生产环境里最有名的分布式存储方案之一,功能非常强大,提供块存储,性能也不错。但它的部署复杂度也是出了名的高:至少需要三个MON、多个OSD,还要管理一堆crushmap之类的东西。在单节点上装Ceph,先不说资源占用,光是把这些组件跑起来、调通K8s的provisioner对接,就足够让你怀疑人生了。而且Ceph的副本机制在单节点上毫无意义——三个副本都写同一块磁盘,等同于拿大炮打蚊子,还徒增故障概率。
3.3 云盘插件:依赖公有云环境
如果你用的是云平台提供的K8s服务,比如各种云托管集群,那么StorageClass基本是开箱即用的,因为云平台已经把CSI插件和云盘服务对接好了。但我们这里说的是自建单节点集群,也许是虚拟机、也许是裸机服务器,根本没有云盘API可以调用,所以这条路线可以直接排除。
3.4 本地卷方案:我最终的选择
排除了NFS、Ceph和云盘之后,剩下的最优选择基本就是利用本地磁盘的StorageClass实现方案。这类方案里最有名的是Rancher开源的local-path-provisioner,k3s默认集成的就是它,也可以独立部署到标准K8s集群中。它的原理不复杂:每个节点上通过hostPath类型的PV,把节点本地的某个目录映射给Pod使用,同时通过一个控制器监听PVC的创建,自动在节点上创建对应目录并生成PV。
在单节点集群上,这个方案的好处非常明显:
- 零额外依赖,不需要部署NFS服务,也不需要数据库。
- 部署极简,一条YAML文件加一条命令就能跑起来,Pod数量也少。
- 性能好,数据直接写在本地磁盘,不走任何网络封装。
- 满足动态供给,PVC一提交,控制器自动建目录、建PV,体验跟云盘一样顺滑。
当然它也有明显的短板:数据只存在一个节点上,节点挂了数据就没了。但这话放在单节点集群里根本不算短板,因为你的K8s本来就是单点的——节点挂了一切都完蛋,也不差这一点。所以对于单节点集群来说,local-path-provisioner就是最合适、最贴切的方案,没有之一。
确定方案之后,下面进入实操阶段。我会用kubeadm搭出来的标准K8s集群作为示例环境,其他方式搭的单节点集群步骤大同小异,需要差异点我会在文中说明。
4. 实操记录:在单节点K8s集群中部署local-path-provisioner
4.1 第一步:确认集群环境
安装之前,先确认两件事。第一,集群和你本地的kubectl能正常通信,这一点应该不用多说。第二,确认集群当前有没有StorageClass,看看是不是已经有默认存储类了。
bash复制kubectl get sc
如果输出显示No resources found,说明集群里确实没有StorageClass,可以放心往下走。如果已经有了,说明你可能装了别的插件(有些K8s发行版默认自带),那你需要想清楚是保留还是替换,不要盲目再装一个。查看节点信息也很重要:
bash复制kubectl get nodes -o wide
我这边环境是单节点,角色标签是control-plane,这也是单节点集群常见的样子,master和worker共用同一个节点。这个信息后面排查问题时会用到,比如Pod调度时如果节点上有污点,就需要确认是否容忍了污点。
4.2 第二步:获取并检查部署清单
local-path-provisioner的官方部署方式很直接,一般情况下直接apply它提供的YAML即可。但在国内网络环境下,建议先看看仓库里的文件内容再动手,因为有些镜像地址可能需要替换成你能拉取到的镜像源。官方的方式大概是:
bash复制kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
如果因为网络原因下载不下来,可以先去镜像站或者GitHub页面把YAML内容下载到本地,再做修改。整个YAML里面主要包含这么几个对象:
- Namespace:默认取名
local-path-storage,所有相关资源都跑到这个命名空间下。 - ServiceAccount:给provisioner提供RBAC权限。
- ClusterRole和ClusterRoleBinding:让provisioner有权限操作PV、PVC、节点等资源。
- ConfigMap:里面有一段
helperPod的配置,后面单独讲。 - Deployment:运行真正干活的应用,官方默认副本数设为1,因为
local-path的方案是直接把目录挂在当前节点上,多副本没有意义。 - StorageClass:会创建一个名为
local-path的存储类,默认回收策略是Delete。
其中我要重点说一下helperPod这个配置。local-path-provisioner的原理是在节点上自动创建目录,然后把目录以hostPath的形式挂载给Pod。但这里有一个细节:它在准备存储时需要一个辅助Pod来执行一些操作(比如创建目录、设置权限),这个辅助Pod的运行参数就是又helperPod来定义的。YAML默认指定了以busybox镜像作为helper Pod的镜像,如果你所在的网络环境拉不到busybox,或者你的集群配置了私有镜像仓库,就需要在ConfigMap里把image换成自己能拉取的版本。
4.3 第三步:部署前的本地化修改
我不建议直接一条命令apply上生产环境(这里指任何正式使用的环境),虽然官方YAML很完善,但至少有几处值得检查。
第一处是镜像地址。Deployment里使用的镜像名通常是rancher/local-path-provisioner:v0.0.24(版本号可能随release更新)。如果你的集群配置了镜像加速器,也许直接拉取没问题;但如果拉取超时,要么把镜像换成registry.cn-hangzhou.aliyuncs.com之类的国内镜像地址,要么在节点上手动docker pull再打个tag,总之确保kubelet能拉到镜像。
第二处是helperPod的镜像。同样,如果你拉不到busybox,也要一并换掉。我实际遇到过一个场景:因为安全要求,集群里所有容器镜像只能从内网Harbor拉取,busybox在外面根本拉不到。这个时候需要把ConfigMap里的image字段改成内网地址,并确保tag存在。
第三处是StorageClass的配置项,值得细看。我贴一下我使用的StorageClass配置:
yaml复制apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
volumeBindingMode这个参数很关键。它有两个可选值:Immediate和WaitForFirstConsumer。Immediate表示PVC创建后立即触发绑定,但在local-path这种依赖具体节点的本地存储方案里,如果在PVC提交的时候Pod还没被调度,provisioner并不知道该在哪个节点上创建目录,就会出现问题。所以正确做法是使用WaitForFirstConsumer,等Pod真正被调度到某个节点之后,才开始真正创建目录和绑定卷。这个逻辑和正常的调度流程是匹配的,务必不要改成Immediate。
reclaimPolicy这里我用的Delete,意味着PVC被删除以后,PV和底层目录会一并清除。如果改成Retain,删除PVC后数据还会保留在节点上,方便后续手工恢复,但也会产生一堆孤立的PV。单节点测试环境用Delete其实就够了,后面我会单独讲回收策略的坑。
4.4 第四步:部署并验证
修改完配置,就可以执行部署了。我习惯先把YAML存成文件,再apply,而不是直接用URL,方便后面改动和追溯。
bash复制kubectl apply -f local-path-storage.yaml
执行完之后,先看Pod状态:
bash复制kubectl get pods -n local-path-storage
正常情况下状态是Running。如果一直是Pending,多半是镜像拉取失败,或者节点资源不够,具体原因用kubectl describe pod -n local-path-storage去看事件。然后查看StorageClass是否创建成功:
bash复制kubectl get sc
这一步如果能看到名为local-path的存储类,并且标记为(default),说明默认存储类已经自动设置了。要是没有显示default,你可以手动执行:
bash复制kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
到了这一步,部署本身就算完成了。但StorageClass到底好不好用,必须创建PVC实际测一遍才算数。
5. 实战测试:创建PVC并部署一个有状态应用
部署完成不等于能用,我习惯用一个最小化的有状态应用做冒烟测试。这里我用一个nginx加一块PVC的方式,最直观地确认整个链路是通的。
5.1 创建PVC验证动态供给
先写一个PVC:
yaml复制apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: local-path
注意,storageClassName明确指定为local-path。如果你在StorageClass上设置了is-default-class,PVC里也可以不写storageClassName,系统会自动用默认存储类。但显式写出来更清晰,也不容易出错,尤其是集群里存在多个StorageClass的时候。
创建之后立即查看状态:
bash复制kubectl get pvc test-pvc
一个关键点:因为volumeBindingMode设置的是WaitForFirstConsumer,刚创建完PVC时你会看到它的状态是Pending。这很正常,别慌,因为Pod还没调度上来,还没有人“消费”这个PVC。这时候去查看PV,你会发现还没有生成对应的PV:
bash复制kubectl get pv
5.2 部署测试Pod触发绑定
接着创建一个Pod来使用这个PVC:
yaml复制apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: test-pvc
创建后,等Pod进入Running,再去看PVC和PV的状态:
bash复制kubectl get pvc
kubectl get pv
这时候PVC应该变为Bound,同时系统里出现一个名字类似pvc-xxxx的PV。这就是动态供给在起作用:Pod调度到节点之后,provisioner观察到PVC和Pod的绑定关系,自动创建一个PV并关联到PVC上。
进入Pod验证数据是否落在了节点上的真实目录:
bash复制kubectl exec -it test-pod -- sh
echo "hello local path" > /data/test.txt
exit
然后在宿主机上找到对应的数据目录。local-path-provisioner默认把数据放在/opt/local-path-provisioner下,可以用命令查看:
bash复制ls -l /opt/local-path-provisioner/
find /opt/local-path-provisioner -type f
如果能看到刚才写入的test.txt,说明整个链路完全打通:PVC -> StorageClass -> provisioner -> 节点本地目录,数据真实落盘成功。
5.3 验证持久化:删除Pod数据不丢失
持久化的意义在于Pod的生命周期不应当影响数据。这是StorageClass的核心价值所在。把刚才的测试Pod删除,再重新创建一个使用同一个PVC的Pod,数据还在不在?
bash复制kubectl delete pod test-pod
重新apply刚刚的Pod YAML,等Pod起来后进入容器:
bash复制kubectl exec -it test-pod -- sh
cat /data/test.txt
看到内容还在,说明持久化生效了。如果你要进一步测试,甚至可以删掉PVC再重新创建,不过这里因为回收策略是Delete,删PVC时PV和宿主机的数据目录都会一并清空,所以这个测试建议放在回收策略的内容里再讲。
到这里,一个可用的、动态供给的StorageClass就真正落地了。但原样照搬只能算“能跑”,下面聊聊使用过程中最常踩的坑和排查思路,这些都是文档里不会写的。
6. 踩坑总结:SC使用中的高频问题与排障思路
单节点集群跑起来之后,StorageClass相关的坑会陆陆续续冒出来。我把实际环境里碰到过的、以及群里朋友遇到的高频问题整理成一张速查表,再挑几个典型的详细展开。
6.1 问题速查表
| 现象 | 常见原因 | 排查方式 | 解决办法 |
|---|---|---|---|
| PVC一直Pending | 没有StorageClass或storageClassName写错 | kubectl get sc查看存储类 |
创建StorageClass或修改PVC中的存储类名称 |
PVC事件报no volume plugin matched |
使用了不存在的provisioner | 检查StorageClass的provisioner字段 | 确认provisioner与部署的组件一致 |
PVC事件报failed to provision volume with StorageClass |
provisioner Pod异常、镜像拉取失败或节点标签不对 | kubectl logs -n local-path-storage查看日志 |
定位日志中的具体错误并修复 |
| Pod调度失败且PVC没有绑定 | volumeBindingMode配置成了Immediate | 查看StorageClass配置 | 改用WaitForFirstConsumer |
| 使用pod所在节点没有对应目录 | provisioner没有权限或目录创建失败 | 检查节点上目录权限和provisioner日志 | 手动创建目录并授权,或修复helperPod镜像 |
| 删除PVC后数据依然占磁盘 | reclaimPolicy设置为Retain | 查看StorageClass配置 | 如不需要保留则改为Delete |
| 拉取镜像超时导致provisioner起不来 | 网络问题 | 查看deployment镜像地址 | 替换为可达的镜像地址 |
| 默认StorageClass没有生效 | 未设置annotation | kubectl get sc查看是否带default标记 |
打annotation标记为默认存储类 |
| 重启节点后Pod数据丢失 | 数据写在了容器可写层 | 检查Volume挂载方式 | 确认挂载的是PVC而不是emptyDir |
6.2 典型问题一:PVC一直Pending但没有任何事件
这个坑很隐蔽。现象是PVC创建后状态一直是Pending,但kubectl describe pvc里看不到明显的Error事件。这种情况多半不是不够存储空间,而是volumeBindingMode设置成了Immediate,导致provisioner在Pod还没出来的时候就开始尝试创建PV。对于local-path这类本地存储方案来说,控制器不知道该在哪个节点上创建目录,就会一直等待或者重试。
排查方法很简单:kubectl get sc local-path -o yaml看一下volumeBindingMode字段。如果是Immediate,把它改成WaitForFirstConsumer就行。修改方法不是直接edit Edit StorageClass,而是重建或者patch:
bash复制kubectl patch storageclass local-path -p '{"volumeBindingMode":"WaitForFirstConsumer"}'
改完之后,删除已有PVC重新创建。你会发现PVC依然Pending,但这回是正常现象,等Pod调度上来之后它就会自动变成Bound。
6.3 典型问题二:provisioner Pod处于CrashLoopBackOff
单节点上部署出来最常遇到的状态是Pod一直在CrashLoopBackOff。先看日志:
bash复制kubectl logs -n local-path-storage deployment/local-path-provisioner
常见错误大概有这么几类:
- 镜像拉取失败:报错是
Failed to pull image,这种最直接,换镜像源就行。 - RBAC权限不足:日志里出现
forbidden字样,多半是RBAC资源没创建成功,或者你部署的时候被安全策略拦截了。重新apply一遍完整YAML能解决大部分问题。 - 节点亲和性导致调度失败:如果单节点上有控制平面污点(Taint),而provisioner的Pod没有对应的容忍(Toleration),Pod也会调度不上。但官方YAML里一般已经考虑了这种情况,如果你从其他地方复制了一份被魔改过的YAML,就可能中招。处理方式是查看Pod事件:
kubectl describe pod -n local-path-storage,里面会明确提示有没有污点导致的NoSchedule。
6.4 典型问题三:删除了PVC,但磁盘空间没释放
这个问题的根源在reclaimPolicy。单节点集群的磁盘空间本来就不大,如果建了几个大容量的测试PVC,删掉之后磁盘空间没释放,过几天就会发现磁盘满了。
检查方法:
bash复制kubectl get pv
如果PV的状态是Released而不是Deleted,那就是回收策略的问题。如果你的StorageClass设置为Retain,那么PVC删除后PV会保留,并且需要管理员手动清理。对单节点测试环境,我建议直接用Delete策略,PVC删了就跟着删PV和底层目录,省心。如果已经养成了Retain的习惯,那么定期手动清理/opt/local-path-provisioner下面那些孤儿目录是必须做的事。
值得一提的坑是,有些版本的local-path-provisioner在Delete策略下有延迟,删除PVC后目录不会立即消失,会在这个时间差里等待一段时间再清理。如果等了几分钟还没释放,可以去节点上看看是不是有Pod还在占用卷,确认没有之后手动删目录即可。
6.5 典型问题四:Pod调度到别的节点,数据“丢”了
单节点集群一般不会出现这个问题,但如果你后面把单节点扩容成了多节点,local-path就会立刻暴露一个明显局限。数据被创建在Pod所在的节点上,如果Pod被调度到另一个节点,PVC还是同一个,但另一个节点上并没有对应的数据目录,应用启动后可能看到的是空目录,看起来就像“数据丢了”。
在多节点场景下,local-path并不能直接解决问题,它不是分布式存储。如果你的集群规模一定会增长,我建议换用NFS或者直接上Longhorn之类的分布式长期存储。对于铁打的单节点环境,local-path完全没毛病,但也别指望它以后能无缝迁移到多节点架构。
6.6 关于镜像替换的一个实操建议
国内网络环境下,rancher/local-path-provisioner和busybox都有可能拉取失败。我的习惯是在apply之前,先把YAML拉到本地,把里面所有的image字段统一替换成能拉到的镜像地址。如果公司有内网镜像仓库,优先替换为内网地址;如果个人环境,可以用阿里云镜像加速器上的公共地址,或者先在节点上手动用docker把镜像pull下来,再通过docker tag改成YAML中的名字。
这里有个细节:如果用的容器运行时是containerd而不是docker,那手动拉镜像的方式就不一样了,需要用crictl pull或者用nerdctl。可以先用kubectl describe pod -n local-path-storage看完整的事件信息,确认到底是哪个镜像拉取失败,再有针对性地处理,不要盲目改。
7. StorageClass的进阶用法:默认类、回收策略和多存储类并存
基础功能跑通之后,再介绍几个实用配置,让StorageClass更像生产环境里那样有“管理思维”。
7.1 设置默认StorageClass
当一个集群里有多个存储类的时候,如果创建PVC时不写明storageClassName,那K8s会去找带default标记的存储类。没有默认标记的话,创建出来的PVC会一直Pending,因为系统不知道该用哪个存储方案。把某个存储类设为默认用一条命令就可以:
bash复制kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
需要注意的是,默认存储类只能有一个,否则行为不确定。如果原本已经有默认存储类,需要先把旧的那个去掉标记,再给新的打上标记。
7.2 同一个provisioner创建多个StorageClass
local-path-provisioner虽然原理简单,但你完全可以基于同一个provisioner创建多个StorageClass,区别在于参数不同。比如一个StorageClass用Delete回收策略,适合临时数据;另一个用Retain策略,适合需要保留的数据。
yaml复制apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path-retain
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
创建之后,集群里就有两个存储类了,使用时在PVC里显式指定即可。这种方式不需要装第二套插件,只是多加几个描述不同行为的YAML对象,理解起来也不难。
7.3 限制存储空间和访问模式
local-path的PV本质上就是hostPath目录和一条StorageClass绑定,K8s本身不强制限制hostPath目录的大小,所以PVC里申请10Gi还是100Gi,底层并不会真正做配额。这是一个很容易踩的认知误区:你想限制一个应用最多使用多少空间,local-path并不能可靠保证。如果你需要严格的配额能力,必须换其他存储方案(比如Ceph、NFS配合配额)。单节点测试环境里,这一点通常不用太纠结,但心里要有数。
访问模式方面,local-path创建的PV通常支持ReadWriteOnce(单节点读写)。ReadOnlyMany和ReadWriteMany这种跨节点的多读多写,在单节点集群里其实也没实际意义——因为你只有一个节点,一个Pod在读写,别的Pod也只能调度到同一个节点,localhost下的共享只会发生在这个节点内。所以在单节点集群中,把访问模式设成ReadWriteOnce就够了,这也是最符合实际的设置。
8. 最后再分享几点实际操作中的体会
整个过程走下来,最大的感受是:单节点集群安装StorageClass这件事本身不难,难的是想明白“我需要哪种存储”以及“这个方案的边界在哪里”。很多初学者一上来就想追求完整、生产级的方案,结果是往单节点环境里塞了一个巨大的分布式存储系统,既慢又难维护,反而掩盖了更核心的知识点。
按照我个人习惯,搭单节点环境时存储这块的优先级是这样的:先用默认的local-path快速跑通业务,把K8s的基本机制吃透;如果后面确实要做更接近生产的实验,再考虑部署NFS或者Longhorn之类的外部存储,逐步引入网络存储、配额、快照这些特性。如果你刚开始学,千万不要跳步,先让你的有状态应用在这个最小化集群里跑起来,比什么都重要。
还有一个小技巧:单节点集群里,节点本身通常带有控制平面的污点,默认情况下普通Pod是调度不上去的。如果你的节点只有control-plane角色,没有node角色,或者带有node-role.kubernetes.io/control-plane:NoSchedule这样的污点,那需要给Pod或者Deployment加上对应的容忍度,或者把节点标记为可调度。方法很简单:
bash复制kubectl taint nodes --all node-role.kubernetes.io/control-plane-
这个命令会把所有控制平面的污点去掉,单节点集群这么操作很安全,否则你会发现PVC就算绑定了,Pod也一直停留在Pending状态。
最后再强调一次回收策略的问题。测试环境里用Delete省心,但如果你在单节点上跑了一些不重要的数据库,又不想因为误删PVC导致数据直接从磁盘上消失,那就把那个重要命名空间的PVC对应StorageClass改成Retain,并且定期去/opt/local-path-provisioner下面备份目录。这套组合拳在单节点集群上非常实用,不需要额外存储服务,也能给你提供一份“保底”。
