整个系列写到第四篇,其实是个很有意思的分水岭。前几篇把集群怎么初始化、Pod怎么拉起、Service怎么暴露流量讲完以后,大多数人已经能把一个无状态服务跑得像模像样了。但真正开始往生产环境推的时候,问题就会接踵而来:数据库容器的数据放在哪?文件上传之后Pod重启文件还在不在?三个副本同时写同一个目录会不会互相打架?这些问题统归到一个话题——k8s的存储和状态。这篇是系列的第四篇,我打算把这套东西彻底掰开揉碎讲清楚,主线就是PV/PVC这套存储抽象、StorageClass动态供给,以及StatefulSet怎么用来跑有状态应用。适合已经能跑通基础Pod和Service、正准备往业务落地走的读者。1. 存储抽象这条线到底是怎么搭起来的1.1 先回到最原始的卷:emptyDir 与 hostPath 分别在解决什么问题emptyDir 和 hostPath 是k8s里最基础的两个卷类型,很多人刚开始学习时容易把它们当成"临时目录"和"服务器磁盘",这样理解没错,但不够。emptyDir的生命周期和Pod绑定,只要Pod被删除,目录里的内容也跟着没了。它真正解决的问题是同一Pod内多个容器之间的临时数据交换。最常见的例子是:Nginx容器把访问日志写进一个emptyDir,旁边一个sidecar日志收集容器从同一个目录里把日志读走处理。你不希望日志在宿主机上留一份,也不希望两个容器各写各的,那就用emptyDir把一块临时空间共享给它们。我在测试环境经常用这种方式临时调试,几行YAML就搞定,不用考虑持久化。hostPath则把节点上的某个目录直接挂载进Pod,Pod重建之后目录还在,因为它本质上是落到节点磁盘上的。听起来好像已经能持久化了,但它有个致命限制:它绑定的是某个具体节点。如果Pod后来被调度到另一台机器,数据就找不到了。哪怕你用hostPath给三个副本共享同一个目录,这三副本大概率会被调度到三台不同节点,看到的根本不是同一份数据。所以hostPath只适合做单节点测试、读取节点本身日志这一类场景,真放生产环境指望它给多副本服务共享数据,基本是给自己埋雷。1.2 卷的本质:把存储能力当作一类资源来管理到这里应该能感觉到,光靠"挂目录"这个动作是不够的。k8s的做法是从一开始就把存储当作一类独立资源来管理,这套体系的入口就是PV和PVC。PV是集群层面的一种资源,代表一块真实的存储空间。它可以是本地磁盘、NFS共享目录、Ceph块设备,也可以是某个云平台提供的云盘。PVC是用户发出的使用申请,说"我需要5Gi空间、要支持多个节点同时读、存储类型要高性能",然后由集群去匹配一块合适的PV。这两个对象之间的关系有点像水厂和住户:PV是水厂已经建好的管网,PVC是你家提交的用水申请。你不需要关心水厂管网是怎么接进来的,打开水龙头有水就行。Pod在配置里永远只引用PVC,不直接碰PV,这一层隔离是整个k8s存储体系最关键的设计。如果没有这层隔离,存储底层的一切变更都会直接冲击到业务清单。1.3 为什么必须要有PV/PVC这层抽象没有PV/PVC抽象,最简单粗暴的方式就是把存储连接信息直接写进Pod环境变量或者挂载路径里。看起来少了一道工序,但后续维护是灾难。举个例子,你的应用清单里写死了某个NFS服务器的IP和目录,一旦这台NFS服务器迁移或者换了存储后端,你得把所有写了这个地址的YAML全部改一遍。存储扩容也是,每个应用都要知道新容量。更麻烦的是权限管理,存储的访问控制和业务代码耦合在一起,谁用哪块空间、谁能写,根本理不清楚。PV/PVC把存储能力的"供给"和"消费"分开之后,业务团队只需要在清单里声明"我要这个规格的存储",底层怎么供给、怎么对接,是集群管理方的事。我见过不少跳过这层抽象直接写存储参数的团队,早晚会在换存储后端的时候被满地的IP替换和路径拼接拖垮。这不是危言耸听,是这些丢失的配置信息会变成线上事故的导火索。2. PV和PVC的供需体系:状态流转与回收策略是存储的第一道关卡2.1 四个生命周期状态,Released是最容易误解的一个PV创建之后会在几个状态之间流转,每个状态背后都有明确的含义:Available:卷处于空闲状态,还没有被任何PVC绑定。Bound:卷已经被某个PVC绑定,可以正常使用了。Released:绑定的PVC被删除,但PV数据还没有被清理,卷暂时不能再次绑定。Failed:自动回收卷失败,卷处于不可用状态。很多人第一次接触PVC被删除后的状态时会看到PV变成Released,以为它已经自动清理好了。但实际上数据可能还在,只是这个卷被"冻结"了,不能直接给下一个PVC用。这个状态需要管理员介入,确认数据之后手动处理。后面故障复盘部分我会详细讲这个场景,因为它经常成为线上存储迁移的拦路虎。2.2 绑定匹配规则:容量、访问模式、StorageClass三项全对上PVC要绑定到一个PV,不是随便抢一个就行,必须同时满足几个条件。首先是访问模式,k8s定义了三种基础访问模式:访问模式缩写典型使用场景单节点读写ReadWriteOnce数据库数据盘,绝大多数块存储默认多节点只读ReadOnlyMany配置分发、多节点共享只读数据多节点读写ReadWriteMany共享文件存储,NFS这类文件系统才能支持其次是容量。PVC声明的容量可以小于或等于PV容量,但不能大于。所以调度器在匹配时会选一块不小于需求的PV。最后是StorageClass字段,PVC和PV的storageClassName必须完全一致,或者PVC不写这个字段而集群里恰好有默认StorageClass。三个条件同时满足,绑定才会发生。这也是为什么很多人创建完PVC后看到Pending,第一件事就是去人肉检查这三项。2.3 回收策略的取舍:Delete不一定省心,Retain不一定吃亏PV销毁时的回收策略有三种:Retain、Delete、Recycle,其中Recycle在较新版本里已经被废弃了,不用多看。实际生产环境主要就是在Retain和Delete之间做选择。Delete策略的意思是,PV一旦删除,后端存储也会跟着删除。动态供给创建的PV默认大部分是Delete,这对那些数据丢了也无所谓的业务很友好,删PVC就像扔垃圾,顺手清掉后端目录。但代价是你没有第二次反悔的机会。Retain策略则是PV删除后,后端数据先保留下来,PV进入Released状态等待管理员处理。这个策略更适合数据库、重要文件这类不能丢的数据。我个人在生产环境里的经验是:重要数据永远不要依赖Delete策略的"我以为删了还能找回来",宁可多配一点Retain,再配上夜间定时备份。数据在歧义期多留一条退路,比事后找存储管理员要强得多。3. StorageClass动态供给落地:一条滚动命令背后的玄机3.1 静态供给与动态供给的差别在哪里存储供给方式分两类。静态供给是运维手动创建PV,先规划好容量、路径、访问模式,然后丢到集群里等着PVC来绑定。适合存储资源很少、规格固定的场景,比如团队只有一台NFS服务器,每个应用分一个固定目录,运维手动建十个PV就能应付。但一旦业务增长、存储资源多起来,静态供给的维护成本会直线上升,每次新应用都要人工建PV、核对容量、收权限。动态供给就完全不同了。PVC一提交,StorageClass会调用对应的provisioner组件自动创建底层存储卷,并且自动生成PV完成绑定。整个过程对系统管理员来说几乎零干预。类比一下:静态供给好比提前买好固定车位,不紧张但不够灵活;动态供给像是临时申请一个车位,系统自动划线、自动分配,用完取消申请后位置还能自动释放。动态供给依赖StorageClass中的provisioner字段,这个字段告诉k8s"谁去干活"。不同存储后端有不同provisioner实现,比如某块存储的驱动、某个已部署的NFS动态供给器等。3.2 用NFS动态供给控制器跑一个最小可用方案先说准备条件:一台NFS服务端,假设IP是192.168.1.100,导出目录是/var/nfs/k8s。然后在k8s集群里部署一个常见的NFS动态供给控制器。这个组件需要一些RBAC权限,允许它在用户集群里创建PV、操作后端目录,部署成功后,StorageClass里的provisioner字段就指向它。以下是一个最小的StorageClass定义:apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: nfs-subdir-external-provisioner parameters: archiveOnDelete: "true" reclaimPolicy: Delete allowVolumeExpansion: true这里有几个参数值得说明。archiveOnDelete: "true"的作用是,当PVC被删除时,后端目录不会被直接清空,而是被改名为带时间戳或特殊前缀的归档目录。这对不想彻底丢数据的环境很友好。reclaimPolicy用Delete,但配合archiveOnDelete其实留了一手。allowVolumeExpansion置为true,允许后续对PVC做扩容。接着创建一个PVC测试:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-data spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi storageClassName: nfs-storage提交之后,执行kubectl get pvc和kubectl get pv,可以看到PVC自动变为Bound状态,PV也自动生成。后端NFS目录会按命名规则自动创建。这就是动态供给的价值体现。3.3 StorageClass默认类与命名规范,最容易被忽略的细节如果集群里同时存在多个StorageClass,你得明确知道哪个是默认的,因为PVC不写storageClassName时,系统会自动使用默认类。查看方式:kubectl get sc输出里会有一个带(default)标记的StorageClass。如果团队对"哪个是默认类"没有共识,很容易出现测试环境PVC悄悄绑到了生产存储类上,导致测试数据污染生产卷。我习惯的命名规范是三类:一是有明确用途前缀,比如nfs-backup、nfs-test、nfs-prod;二是把容量规格写在后续文档里,便于查看;三是尽量不修改默认类,除非你有绝对把握。多环境部署时,甚至建议每个环境独立的StorageClass名称,宁可配置多一点,也别让命名歧义变成事故源头。4. StatefulSet部署实战:让有状态应用真正"有状态"4.1 Deployment和StatefulSet到底差在哪里Deployment适合无状态应用,Pod名字是随机字符串,这些Pod随时可以被替换、扩缩容,数据丢不丢无所谓。StatefulSet就不一样,它给每个Pod一个固定序号和稳定的网络标识,并且保证每个Pod都会绑定到自己的PVC上。我把两者之间的核心差异整理成了表格:对比项DeploymentStatefulSetPod名称随机后缀,如web-8d4f5有序编号,如db-0、db-1存储默认不自动绑定专属PVC通过volumeClaimTemplates自动为每个副本创建PVC扩容/缩容顺序并行,不保证顺序扩容按顺序创建,缩容逆序删除网络标识不稳定,Pod重建后变化稳定,Pod重建后仍用同一名称典型场景Web服务、无状态API数据库、消息队列、有状态中间件这个差异直接决定了有状态应用的部署方式。以数据库为例,db-0和db-1必须能稳定地互相访问,如果用Deployment,Pod一重启名字变了,连接关系可能就断了。StatefulSet用稳定的序号名称解决了这个问题。4.2 一个最小可复现的StatefulSet加PVC模板下面是一套最基础的示例,包含一个Headless Service和一个StatefulSet:apiVersion: v1 kind: Service metadata: name: mysql-svc spec: clusterIP: None selector: app: mysql ports: - port: 3306 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql-svc replicas: 2 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8 ports: - containerPort: 3306 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi storageClassName: nfs-storage这里的要点在volumeClaimTemplates。它会为每个序号创建独立PVC,比如>updateStrategy: rollingUpdate: partition: 15. 存储故障复盘:我在真实集群里踩过的坑与排查链路5.1 场景一:PVC一直Pending,卡在资源匹配上最典型的故障是创建PVC之后,kubectl get pvc一直显示Pending。我遇到过很多次,第一反应不是去看底层存储,而是先看事件:kubectl describe pvc test-data事件里通常会写Waiting for a volume to be created, either by external provisioner or by a storage class。这时候有三条链路要查。一是确认StorageClass是否存在,名字是否一致;二是确认provisioner对应的Pod是否正常运行;三是去看provisioner容器日志。有一次我在某个模拟项目里查了很久,最后发现是NFS服务端导出的目录没有正确授权,provisioner的Pod一直报权限不足。日志里permission denied非常明显,但之前我一直盯着k8s侧排查,忽略了文件系统的读写权限。后来把NFS服务端的导出目录设为可读写,PVC立刻变成Bound状态。总结一下排查顺序:先描述PVC,再验StorageClass,再看底层组件日志,最后查存储节点本身。5.2 场景二:Pod一直ContainerCreating,事件提示FailedMountPVC状态正常,但Pod一直卡在ContainerCreating,kubectl describe pod里出现FailedMount,后面跟着No such file or directory、connection timed out或permission denied。这类问题的根子往往不在k8s,而在后端存储。正确排查链路是:先看Pod调度到哪个节点,再去这个节点上手动执行挂载命令,确认后端目录是否真的可以挂载。我遇到过NFS服务端重启后导出配置没生效的情况,k8s侧看着一切正常,但节点挂载时始终timeout。还有一种很隐蔽的权限问题:后端共享目录权限是root,Pod里的容器以非root用户运行,挂载虽然成功了,但容器内没有写权限,启动阶段必然报错。处理方式不是盲目chmod 777,而是查看容器镜像声明的用户和组ID,把存储目录的属主改成对应ID,或者通过Pod的securityContext指定匹配的用户。5.3 场景三:PV状态卡在Released,无法重新绑定这个场景在手动建PV时特别常见。PVC被删除后,绑定的PV不会自动回到Available,而是停在Released。k8s这样设计是为了防止数据被无意中再次供给给别的PVC,导致新业务把旧数据覆盖。但如果运维不了解机制,就会陷入"怎么把Released改成Available"的误区。正确处理方式分两步:先确认这个PV背后的数据是否还需要保留。如果不需要,直接删除PV,再重新创建一个同名PV,状态就回到Available。如果需要保留,就应该把PV导出、归档数据,然后重建PV并调整数据目录。我之前见过有人直接改PV的reclaimPolicy从Retain改成Delete,想靠修改字段触发回收,这种方法在新版k8s里并不可靠,因为修改回收策略不会自动触发一次清理,只是改变后续删除时行为。真正的动作还是手动删除或手动处理。收尾前再分享几个存储使用心得写到这里,我在实际使用中累积下来最想强调的一点是,存储比Pod更容易让人头疼,因为Pod可以随时重建,而数据一旦丢就很难恢复。上线之前,先把备份这条通路打通,别等出问题了再去想快照和恢复演练。我还建议每个环境用不同的StorageClass名字,或者在命名上明确区分测试和生产。这样看到PVC的时候,一眼就知道它应该在哪个环境里使用,避免测试环境的数据误写到生产存储。有状态应用也不要急着第一天就全部迁到StatefulSet,挑一个不关键的中间件先试点,摸清探针、滚动更新和缩容行为,再推广到核心业务上。存储是个很大的话题,这篇只是把最常用的一条主线讲清楚。后续还能展开的方向包括:PV快照与备份、存储卷跨可用区的拓扑约束、CSI机制如何对接更多后端存储。这些内容等积累够实操案例之后再继续写。
整个系列写到第四篇,其实是个很有意思的分水岭。前几篇把集群怎么初始化、Pod怎么拉起、Service怎么暴露流量讲完以后,大多数人已经能把一个无状态服务跑得像模像样了。但真正开始往生产环境推的时候,问题就会接踵而来:数据库容器的数据放在哪?文件上传之后Pod重启文件还在不在?三个副本同时写同一个目录会不会互相打架?这些问题统归到一个话题——k8s的存储和状态。这篇是系列的第四篇,我打算把这套东西彻底掰开揉碎讲清楚,主线就是PV/PVC这套存储抽象、StorageClass动态供给,以及StatefulSet怎么用来跑有状态应用。适合已经能跑通基础Pod和Service、正准备往业务落地走的读者。1. 存储抽象这条线到底是怎么搭起来的1.1 先回到最原始的卷:emptyDir 与 hostPath 分别在解决什么问题emptyDir 和 hostPath 是k8s里最基础的两个卷类型,很多人刚开始学习时容易把它们当成"临时目录"和"服务器磁盘",这样理解没错,但不够。emptyDir的生命周期和Pod绑定,只要Pod被删除,目录里的内容也跟着没了。它真正解决的问题是同一Pod内多个容器之间的临时数据交换。最常见的例子是:Nginx容器把访问日志写进一个emptyDir,旁边一个sidecar日志收集容器从同一个目录里把日志读走处理。你不希望日志在宿主机上留一份,也不希望两个容器各写各的,那就用emptyDir把一块临时空间共享给它们。我在测试环境经常用这种方式临时调试,几行YAML就搞定,不用考虑持久化。hostPath则把节点上的某个目录直接挂载进Pod,Pod重建之后目录还在,因为它本质上是落到节点磁盘上的。听起来好像已经能持久化了,但它有个致命限制:它绑定的是某个具体节点。如果Pod后来被调度到另一台机器,数据就找不到了。哪怕你用hostPath给三个副本共享同一个目录,这三副本大概率会被调度到三台不同节点,看到的根本不是同一份数据。所以hostPath只适合做单节点测试、读取节点本身日志这一类场景,真放生产环境指望它给多副本服务共享数据,基本是给自己埋雷。1.2 卷的本质:把存储能力当作一类资源来管理到这里应该能感觉到,光靠"挂目录"这个动作是不够的。k8s的做法是从一开始就把存储当作一类独立资源来管理,这套体系的入口就是PV和PVC。PV是集群层面的一种资源,代表一块真实的存储空间。它可以是本地磁盘、NFS共享目录、Ceph块设备,也可以是某个云平台提供的云盘。PVC是用户发出的使用申请,说"我需要5Gi空间、要支持多个节点同时读、存储类型要高性能",然后由集群去匹配一块合适的PV。这两个对象之间的关系有点像水厂和住户:PV是水厂已经建好的管网,PVC是你家提交的用水申请。你不需要关心水厂管网是怎么接进来的,打开水龙头有水就行。Pod在配置里永远只引用PVC,不直接碰PV,这一层隔离是整个k8s存储体系最关键的设计。如果没有这层隔离,存储底层的一切变更都会直接冲击到业务清单。1.3 为什么必须要有PV/PVC这层抽象没有PV/PVC抽象,最简单粗暴的方式就是把存储连接信息直接写进Pod环境变量或者挂载路径里。看起来少了一道工序,但后续维护是灾难。举个例子,你的应用清单里写死了某个NFS服务器的IP和目录,一旦这台NFS服务器迁移或者换了存储后端,你得把所有写了这个地址的YAML全部改一遍。存储扩容也是,每个应用都要知道新容量。更麻烦的是权限管理,存储的访问控制和业务代码耦合在一起,谁用哪块空间、谁能写,根本理不清楚。PV/PVC把存储能力的"供给"和"消费"分开之后,业务团队只需要在清单里声明"我要这个规格的存储",底层怎么供给、怎么对接,是集群管理方的事。我见过不少跳过这层抽象直接写存储参数的团队,早晚会在换存储后端的时候被满地的IP替换和路径拼接拖垮。这不是危言耸听,是这些丢失的配置信息会变成线上事故的导火索。2. PV和PVC的供需体系:状态流转与回收策略是存储的第一道关卡2.1 四个生命周期状态,Released是最容易误解的一个PV创建之后会在几个状态之间流转,每个状态背后都有明确的含义:Available:卷处于空闲状态,还没有被任何PVC绑定。Bound:卷已经被某个PVC绑定,可以正常使用了。Released:绑定的PVC被删除,但PV数据还没有被清理,卷暂时不能再次绑定。Failed:自动回收卷失败,卷处于不可用状态。很多人第一次接触PVC被删除后的状态时会看到PV变成Released,以为它已经自动清理好了。但实际上数据可能还在,只是这个卷被"冻结"了,不能直接给下一个PVC用。这个状态需要管理员介入,确认数据之后手动处理。后面故障复盘部分我会详细讲这个场景,因为它经常成为线上存储迁移的拦路虎。2.2 绑定匹配规则:容量、访问模式、StorageClass三项全对上PVC要绑定到一个PV,不是随便抢一个就行,必须同时满足几个条件。首先是访问模式,k8s定义了三种基础访问模式:访问模式缩写典型使用场景单节点读写ReadWriteOnce数据库数据盘,绝大多数块存储默认多节点只读ReadOnlyMany配置分发、多节点共享只读数据多节点读写ReadWriteMany共享文件存储,NFS这类文件系统才能支持其次是容量。PVC声明的容量可以小于或等于PV容量,但不能大于。所以调度器在匹配时会选一块不小于需求的PV。最后是StorageClass字段,PVC和PV的storageClassName必须完全一致,或者PVC不写这个字段而集群里恰好有默认StorageClass。三个条件同时满足,绑定才会发生。这也是为什么很多人创建完PVC后看到Pending,第一件事就是去人肉检查这三项。2.3 回收策略的取舍:Delete不一定省心,Retain不一定吃亏PV销毁时的回收策略有三种:Retain、Delete、Recycle,其中Recycle在较新版本里已经被废弃了,不用多看。实际生产环境主要就是在Retain和Delete之间做选择。Delete策略的意思是,PV一旦删除,后端存储也会跟着删除。动态供给创建的PV默认大部分是Delete,这对那些数据丢了也无所谓的业务很友好,删PVC就像扔垃圾,顺手清掉后端目录。但代价是你没有第二次反悔的机会。Retain策略则是PV删除后,后端数据先保留下来,PV进入Released状态等待管理员处理。这个策略更适合数据库、重要文件这类不能丢的数据。我个人在生产环境里的经验是:重要数据永远不要依赖Delete策略的"我以为删了还能找回来",宁可多配一点Retain,再配上夜间定时备份。数据在歧义期多留一条退路,比事后找存储管理员要强得多。3. StorageClass动态供给落地:一条滚动命令背后的玄机3.1 静态供给与动态供给的差别在哪里存储供给方式分两类。静态供给是运维手动创建PV,先规划好容量、路径、访问模式,然后丢到集群里等着PVC来绑定。适合存储资源很少、规格固定的场景,比如团队只有一台NFS服务器,每个应用分一个固定目录,运维手动建十个PV就能应付。但一旦业务增长、存储资源多起来,静态供给的维护成本会直线上升,每次新应用都要人工建PV、核对容量、收权限。动态供给就完全不同了。PVC一提交,StorageClass会调用对应的provisioner组件自动创建底层存储卷,并且自动生成PV完成绑定。整个过程对系统管理员来说几乎零干预。类比一下:静态供给好比提前买好固定车位,不紧张但不够灵活;动态供给像是临时申请一个车位,系统自动划线、自动分配,用完取消申请后位置还能自动释放。动态供给依赖StorageClass中的provisioner字段,这个字段告诉k8s"谁去干活"。不同存储后端有不同provisioner实现,比如某块存储的驱动、某个已部署的NFS动态供给器等。3.2 用NFS动态供给控制器跑一个最小可用方案先说准备条件:一台NFS服务端,假设IP是192.168.1.100,导出目录是/var/nfs/k8s。然后在k8s集群里部署一个常见的NFS动态供给控制器。这个组件需要一些RBAC权限,允许它在用户集群里创建PV、操作后端目录,部署成功后,StorageClass里的provisioner字段就指向它。以下是一个最小的StorageClass定义:apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: nfs-subdir-external-provisioner parameters: archiveOnDelete: "true" reclaimPolicy: Delete allowVolumeExpansion: true这里有几个参数值得说明。archiveOnDelete: "true"的作用是,当PVC被删除时,后端目录不会被直接清空,而是被改名为带时间戳或特殊前缀的归档目录。这对不想彻底丢数据的环境很友好。reclaimPolicy用Delete,但配合archiveOnDelete其实留了一手。allowVolumeExpansion置为true,允许后续对PVC做扩容。接着创建一个PVC测试:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-data spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi storageClassName: nfs-storage提交之后,执行kubectl get pvc和kubectl get pv,可以看到PVC自动变为Bound状态,PV也自动生成。后端NFS目录会按命名规则自动创建。这就是动态供给的价值体现。3.3 StorageClass默认类与命名规范,最容易被忽略的细节如果集群里同时存在多个StorageClass,你得明确知道哪个是默认的,因为PVC不写storageClassName时,系统会自动使用默认类。查看方式:kubectl get sc输出里会有一个带(default)标记的StorageClass。如果团队对"哪个是默认类"没有共识,很容易出现测试环境PVC悄悄绑到了生产存储类上,导致测试数据污染生产卷。我习惯的命名规范是三类:一是有明确用途前缀,比如nfs-backup、nfs-test、nfs-prod;二是把容量规格写在后续文档里,便于查看;三是尽量不修改默认类,除非你有绝对把握。多环境部署时,甚至建议每个环境独立的StorageClass名称,宁可配置多一点,也别让命名歧义变成事故源头。4. StatefulSet部署实战:让有状态应用真正"有状态"4.1 Deployment和StatefulSet到底差在哪里Deployment适合无状态应用,Pod名字是随机字符串,这些Pod随时可以被替换、扩缩容,数据丢不丢无所谓。StatefulSet就不一样,它给每个Pod一个固定序号和稳定的网络标识,并且保证每个Pod都会绑定到自己的PVC上。我把两者之间的核心差异整理成了表格:对比项DeploymentStatefulSetPod名称随机后缀,如web-8d4f5有序编号,如db-0、db-1存储默认不自动绑定专属PVC通过volumeClaimTemplates自动为每个副本创建PVC扩容/缩容顺序并行,不保证顺序扩容按顺序创建,缩容逆序删除网络标识不稳定,Pod重建后变化稳定,Pod重建后仍用同一名称典型场景Web服务、无状态API数据库、消息队列、有状态中间件这个差异直接决定了有状态应用的部署方式。以数据库为例,db-0和db-1必须能稳定地互相访问,如果用Deployment,Pod一重启名字变了,连接关系可能就断了。StatefulSet用稳定的序号名称解决了这个问题。4.2 一个最小可复现的StatefulSet加PVC模板下面是一套最基础的示例,包含一个Headless Service和一个StatefulSet:apiVersion: v1 kind: Service metadata: name: mysql-svc spec: clusterIP: None selector: app: mysql ports: - port: 3306 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql-svc replicas: 2 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8 ports: - containerPort: 3306 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi storageClassName: nfs-storage这里的要点在volumeClaimTemplates。它会为每个序号创建独立PVC,比如>updateStrategy: rollingUpdate: partition: 15. 存储故障复盘:我在真实集群里踩过的坑与排查链路5.1 场景一:PVC一直Pending,卡在资源匹配上最典型的故障是创建PVC之后,kubectl get pvc一直显示Pending。我遇到过很多次,第一反应不是去看底层存储,而是先看事件:kubectl describe pvc test-data事件里通常会写Waiting for a volume to be created, either by external provisioner or by a storage class。这时候有三条链路要查。一是确认StorageClass是否存在,名字是否一致;二是确认provisioner对应的Pod是否正常运行;三是去看provisioner容器日志。有一次我在某个模拟项目里查了很久,最后发现是NFS服务端导出的目录没有正确授权,provisioner的Pod一直报权限不足。日志里permission denied非常明显,但之前我一直盯着k8s侧排查,忽略了文件系统的读写权限。后来把NFS服务端的导出目录设为可读写,PVC立刻变成Bound状态。总结一下排查顺序:先描述PVC,再验StorageClass,再看底层组件日志,最后查存储节点本身。5.2 场景二:Pod一直ContainerCreating,事件提示FailedMountPVC状态正常,但Pod一直卡在ContainerCreating,kubectl describe pod里出现FailedMount,后面跟着No such file or directory、connection timed out或permission denied。这类问题的根子往往不在k8s,而在后端存储。正确排查链路是:先看Pod调度到哪个节点,再去这个节点上手动执行挂载命令,确认后端目录是否真的可以挂载。我遇到过NFS服务端重启后导出配置没生效的情况,k8s侧看着一切正常,但节点挂载时始终timeout。还有一种很隐蔽的权限问题:后端共享目录权限是root,Pod里的容器以非root用户运行,挂载虽然成功了,但容器内没有写权限,启动阶段必然报错。处理方式不是盲目chmod 777,而是查看容器镜像声明的用户和组ID,把存储目录的属主改成对应ID,或者通过Pod的securityContext指定匹配的用户。5.3 场景三:PV状态卡在Released,无法重新绑定这个场景在手动建PV时特别常见。PVC被删除后,绑定的PV不会自动回到Available,而是停在Released。k8s这样设计是为了防止数据被无意中再次供给给别的PVC,导致新业务把旧数据覆盖。但如果运维不了解机制,就会陷入"怎么把Released改成Available"的误区。正确处理方式分两步:先确认这个PV背后的数据是否还需要保留。如果不需要,直接删除PV,再重新创建一个同名PV,状态就回到Available。如果需要保留,就应该把PV导出、归档数据,然后重建PV并调整数据目录。我之前见过有人直接改PV的reclaimPolicy从Retain改成Delete,想靠修改字段触发回收,这种方法在新版k8s里并不可靠,因为修改回收策略不会自动触发一次清理,只是改变后续删除时行为。真正的动作还是手动删除或手动处理。收尾前再分享几个存储使用心得写到这里,我在实际使用中累积下来最想强调的一点是,存储比Pod更容易让人头疼,因为Pod可以随时重建,而数据一旦丢就很难恢复。上线之前,先把备份这条通路打通,别等出问题了再去想快照和恢复演练。我还建议每个环境用不同的StorageClass名字,或者在命名上明确区分测试和生产。这样看到PVC的时候,一眼就知道它应该在哪个环境里使用,避免测试环境的数据误写到生产存储。有状态应用也不要急着第一天就全部迁到StatefulSet,挑一个不关键的中间件先试点,摸清探针、滚动更新和缩容行为,再推广到核心业务上。存储是个很大的话题,这篇只是把最常用的一条主线讲清楚。后续还能展开的方向包括:PV快照与备份、存储卷跨可用区的拓扑约束、CSI机制如何对接更多后端存储。这些内容等积累够实操案例之后再继续写。