ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes存储管理实战:从StorageClass到PV/PVC的完整指南

Kubernetes存储管理实战:从StorageClass到PV/PVC的完整指南 接手这套Kubernetes集群时第一眼看到清单我就知道存储这一关避不开。集群里跑着Kafka、Redis、Hadoop、Doris这一堆有状态服务大部分还是从裸机迁移过来的数据目录的管理方式千奇百怪。有人直接用hostPath有人用本地目录挂NFS还有人压根没配持久化Pod一重建数据就没了。当时的状态基本属于“能跑就行”但线上每次发布、每次故障转移都像开盲盒。这篇文章就把我在这个集群里做存储管理的完整思路和实操过程整理出来从StorageClass设计到PV/PVC生命周期管理再到被坑过的典型案例全部是基于实际踩坑后的复盘。内容适合正在维护K8s集群、尤其是集群里跑着大数据或中间件组件的运维和开发工程师看完可以直接回自己环境对照调整。1. 整体设计与存储架构拆解1.1 为什么存储管理是K8s集群的隐形骨架很多人刚上手Kubernetes时注意力全放在Pod、Deployment、Service这些工作负载概念上存储往往被当成“挂个盘”的小事。但只有真正维护过生产级集群的人才会明白存储管理才是整个集群稳定性的分水岭。无状态应用随便滚动重启影响面小但一旦涉及Kafka的日志目录、Redis的AOF文件、Hadoop的DataNode数据块、Doris的BE节点存储存储的任何一个环节出问题都会直接演变成数据丢失或集群长时间不可用的灾难。K8s本身对存储的设计非常清晰用三层抽象把存储和业务解耦最底层是实际的存储介质或存储系统中间是PVPersistentVolume描述存储资源的静态供给上层是PVCPersistentVolumeClaim由业务方声明存储需求。在这之上还有一个StorageClass负责把“按需创建存储”这件事自动化也就是动态供给。理解这三层的关系是做好集群存储管理的起点。我在这套集群里接手时发现大部分应用根本没有走这套抽象直接就是hostPath挂宿主机目录。hostPath不是不能用但调度器无法感知存储分布Pod被调度到其他节点时数据目录就丢了。这本质上是把K8s的调度能力亲手废掉了。后来我做的第一件事就是分批次把核心有状态服务迁到以PVC为核心的存储模型上。1.2 有些坑是架构选型时就埋下的再说一个真实的架构判断过程。集群里跑着大量大数据组件如果全部走NFS那性能一定会出问题。Kafka和Doris这种对磁盘顺序读写和延迟敏感的服务放在远程网络存储上吞吐和延迟直接不可用。但Redis和部分元数据服务对容量的要求不高对可用性要求又极高放在分布式存储上反而能获得更好的跨节点调度能力。所以我做了一个分级存储的方案设计第一级是本地SSD通过本地PV插件实现专门给Kafka、Doris、HBase这类追求极致IOPS和低延迟的组件用第二级是分布式存储通过CSI驱动的RWO卷给Redis、ZooKeeper以及部分需要跨节点漂移的服务用第三级才是NFS只跑一些日志采集、离线分析这类对延迟不敏感的Pod。这个分级的思路本质上不是选一个“最好”的存储而是根据应用的状态特征、性能敏感度和跨节点漂移需求匹配最合适的存储层级。后面我所有StorageClass和PV策略的设计都是在这个大框架下展开的。2. 存储方案选型与核心机制详解2.1 五种常见存储方案横向对比实战中接触过太多存储方案我把在K8s里真正会用到的几类做了个对比表方便判断选型。这里尽量说大实话把适用场景和坑都写清楚。存储方案供给方式性能水平跨节点共享典型场景主要坑点emptyDir临时卷依赖节点磁盘不支持缓存、临时计算Pod删除数据即消失hostPath静态手动依赖节点磁盘不支持单机调试、日志采集调度漂移后数据丢失本地PVLocal PV静态或动态最高直连本地盘不支持Kafka、数据库、Doris BE节点故障数据风险NFS动态/静态中等网络延迟支持日志、文件共享高并发下锁和延迟问题CSI分布式存储动态取决于后端支持Redis、ZooKeeper、通用负载运维成本高版本兼容需注意选型时我习惯先问三个问题应用能不能容忍节点级故障应用是读多写多还是追求低延迟应用是否需要跨节点共享数据这三个问题的答案基本就能圈定方案范围。2.2 StorageClass动态供给的核心逻辑StorageClass的作用很多人理解成“一个模板”其实它更像一条自动化流水线。你提交PVC时K8s会去找匹配的StorageClass然后调用对应的Provisioner让底层存储系统真正创建出一个卷再生成PV对象并绑定到PVC。这个流程把传统运维手动创建磁盘、格式化、挂载的步骤全部自动化了。用我集群里的YAML举个例子一个本地盘动态供给的StorageClass大概长这样apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-ssd provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer这里的provisioner用的是kubernetes.io/no-provisioner配合本地PV的静态供给方式。真正动态创建本地卷需要额外部署像OpenEBS、TopoLVM这类支持本地的CSI插件。如果你用云厂商的K8s通常直接用云盘驱动做动态供给就行。StorageClass里有两个参数非常关键。第一个是reclaimPolicy它决定PV被释放后的处理方式Delete表示删除PVC时连带存储一起删掉Retain表示保留数据需要人工处理。第二个是volumeBindingModeImmediate表示PVC创建时就立即绑定存储WaitForFirstConsumer表示等Pod调度后绑定。后者对本地盘和需要节点亲和性的存储至关重要。2.3 有状态工作负载的配套组件存储管理永远不是单独存在的它和StatefulSet这套编排机制是配合关系。StatefulSet和Deployment最大的区别在于它为每个Pod提供了稳定的网络标识和稳定的存储标识。稳定存储就靠volumeClaimTemplates实现Pod每次重建都会重新绑定到原来那个PVC上数据不丢。看一个Redis StatefulSet的片段就能直观理解volumeClaimTemplates的用法apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster spec: serviceName: redis-headless replicas: 6 selector: matchLabels: app: redis-cluster template: metadata: labels: app: redis-cluster spec: containers: - name: redis image: redis:7.0 volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: csi-distributed resources: requests: storage: 10Gi这个模板和Deployment的Pod模板是两回事。Pod模板里的存储是共享的所有副本用相同配置但实际上是同一个PVCvolumeClaimTemplates是每个副本自动生成独立的PVC命名规则是volumeClaimTemplates名称-StatefulSet名称-序号比如>apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-ssd provisioner: topolvm.io volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: true注意我开启了allowVolumeExpansion这是为后续扩容留的口子。然后提交这个YAML再创建一个测试PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-local-pvc spec: accessModes: - ReadWriteOnce storageClassName: local-ssd resources: requests: storage: 5Gi创建完PVC它会停留在Pending状态这是正常现象因为WaitForFirstConsumer正在等Pod。然后创建一个挂载这个PVC的PodapiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: app image: busybox command: [sleep, 3600] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: test-local-pvcPod一旦被调度到某个节点PVC会自动完成绑定。此时你执行kubectl get pvc可以看到状态从Pending变成Bound。整个过程不需要手动干预这就是动态供给的价值。如果是静态的hostPath方案你得自己创建PV、自己指定节点还得管理一堆资源清单动态供给把这一切都自动化了。3.3 用volumeClaimTemplates部署Kafka的完整实录前面讲过StatefulSet和volumeClaimTemplates的配合这里用Kafka做一个完整的落地实例。Kafka对存储的要求在性能之外更关键的是数据目录的稳定性和容灾能力。我选型用的是本地SSD StorageClass副本数为3。关键配置如下缩略了与存储无关的探针和资源限制部分apiVersion: apps/v1 kind: StatefulSet metadata: name: kafka spec: serviceName: kafka-hs replicas: 3 selector: matchLabels: app: kafka template: metadata: labels: app: kafka spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: kafka topologyKey: kubernetes.io/hostname containers: - name: kafka image: bitnami/kafka:3.5 env: - name: KAFKA_CFG_LOG_DIRS value: /var/lib/kafka/data volumeMounts: - name: data mountPath: /var/lib/kafka/data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: local-ssd resources: requests: storage: 500Gi这里有一个隐藏的设计细节podAntiAffinity配置了同一应用的不同副本尽量调度到不同节点。因为本地卷和节点强绑定如果三个Kafka副本落在同一个节点那节点故障等于三个副本一起挂。加了反亲和性之后每个副本落在不同节点配合Kafka自身的副本机制容灾能力才真正建立起来。volumeClaimTemplates创建出来的PVC命名会是>kubectl patch pvc>kubectl describe pvc pvc-nameEvents里会直接告诉你卡在哪一步。常见提示是waiting for first consumer to be created before binding这是正常的等待状态如果提示storageclass.storage.k8s.io xxx not found那就是SC名字写错了如果是Failed to provision volume with StorageClass xxx则要去检查CSI驱动Pod的状态。另一个容易被忽略的点是PV的节点亲和性冲突。本地PV会绑定具体节点如果该节点被打了污点或者处于NotReady状态调度器无法把Pod调度过去PVC自然绑定不了。这种问题在describe事件里不一定直接体现需要看Pod的调度事件才能定位。4.3 Kafka和Doris跑在NFS上延迟直接崩盘这是压测阶段踩过的一个大坑。当时为了省事把Kafka的日志目录放到了NFS共享存储上结果压测一上去生产者的延迟指标就开始剧烈抖动TOP命令看到大量IO等待。原因其实很直白Kafka要求的是本地磁盘级别的顺序写性能NFS多了一层网络协议栈还有锁竞争。Kafka的日志段文件要频繁调用fsyncNFS每次fsync都要走一次网络往返性能瓶颈立刻显现。这个案例给我们的教训是选存储方案之前一定要先做应用性能画像。Kafka、Doris BE节点、ES这类对IOPS和延迟敏感的应用老老实实上本地盘别偷懒。反过来像ZooKeeper这种虽然也要求低延迟但它同时要求跨节点调度本地盘反而绑定太死这时候用分布式存储更合适。4.4 Pod反复CrashLoopBackOffStorageClass却“看起来正常”这是另一个类型的坑。现象是Redis Pod反复重启PVC是Bound状态存储看起来完全正常但容器一直起不来。最后看日志发现挂在/data时出现的Permission denied错误。原因很隐蔽PV对应的后端存储文件系统属主和权限不对容器里的Redis用户没有写权限。很多人会用fsGroup来解决但fsGroup不是万能的。它需要CSI驱动支持对卷做递归chown一些网络存储或本地卷驱动在性能上不划算或默认不开启。更可控的做法是直接在应用的SecurityContext里指定runAsUser和runAsGroup和存储目录的属主保持一致。比如存储目录属主是UID 1001那容器进程就用UID 1001跑彻底绕开权限问题。fsGroup只有在卷驱动支持时才可靠对本地盘和部分CSI存储来说时机和范围都不好控制。我建议能用runAsUser解决的就不要依赖fsGroup减少一层不确定性。4.5 StatefulSet的PVC不清理扩容把旧盘也带上了还有一次踩坑是关于StatefulSet扩容的。原来的副本数是3扩容到6时新Pod挂上了PVC但PVC名字已经暴露了问题。因为volumeClaimTemplates创建的PVC命名是按序号递增的扩容前旧副本的PVC是>
返回列表