1. 项目概述
1.1 核心需求解析
先说清楚一件事:OpenShift里的Local Storage并不是什么高大上的新功能,它是Kubernetes生态中原生能力的一种延伸,解决的问题非常具体——让集群里节点上的本地磁盘变成持久化存储,给有状态应用使用。
我在实际交付过的项目里,最常见的使用场景有三类:
- 数据库类应用(PostgreSQL、MongoDB、Redis)需要低延迟的本地盘,不想走网络存储
- 大数据组件(Kafka、Elasticsearch、ZooKeeper)对IOPS要求高,而且本身就是分布式的,不太依赖共享存储
- 中间件和缓存服务,数据量可控,但希望有独立的持久卷管理,不想用hostPath裸奔
说白了,Local Storage就是一层更安全的hostPath。它解决的核心痛点在于:hostPath把节点上的目录直接暴露给Pod,但完全没有生命周期管理,Pod迁移后数据路径不一致、没有容量告警、没有磁盘分配约束,运维起来非常痛苦。Local Storage通过OpenShift的Local Storage Operator把“节点磁盘”变成“标准PVC”,让应用可以用正常声明式的方式消费节点本地盘。
1.2 适用人群与前置条件
这篇内容主要写给这几种人:
- OpenShift集群管理员,正在规划有状态应用存储方案
- 应用开发或运维人员,需要为中间件类应用申请本地持久化存储
- 正在从裸Kubernetes迁移到OpenShift平台的团队,想了解OpenShift在存储这块和原生Kubernetes的差异
在开始之前,你需要具备的基础能力包括:了解OpenShift的基本集群管理操作(oc命令、集群节点查看)、对PersistentVolume/PersistentVolumeClaim/Pod调度的概念有基本认知、能处理YAML资源文件。
需要前置确认的条件有几个:
- 集群节点上确实有未使用的磁盘,或者存在空闲分区
- 磁盘是节点本地挂载的(非网络存储),路径清晰可预期
- 你对节点上磁盘的用途有明确规划——哪些盘给系统用,哪些盘给容器数据用
如果这些条件不满足,Local Storage方案在落地上会很别扭。
1.3 概念范畴与术语说明
在进入实操前,先把几个名称理清楚,避免后面被绕晕:
| 术语 | 含义 |
|---|---|
| LocalVolume | OpenShift Local Storage Operator管理的自定义资源,描述“哪些节点上的哪些磁盘要变成PV” |
| LocalVolumeSet | LocalVolume的增强形态,可以用字符串模式自动匹配设备路径,不用一个个指定 |
| StorageClass | 标准Kubernetes接口,OpenShift会把LocalVolume绑定的PV关联到一个StorageClass上,应用通过PVC来消费 |
| hostPath | Kubernetes原生卷类型,直接把节点目录映射给Pod,没有生命周期管理 |
| Local PersistentVolume | 标识为local的PersistentVolume,通常由静态Provisioner创建 |
简单理解:Local Storage Operator是一个控制循环,它监听LocalVolume/LocalVolumeSet的定义,自动在节点上发现磁盘、创建PV、关联StorageClass。你只需要声明磁盘位置和数据目录,剩下的它来。
2. 整体设计与方案选型
2.1 为什么OpenShift需要独立的Local Storage方案
在OpenShift里,很多团队一开始会选择直接用hostPath或者emptyDir来跑有状态应用,图省事。我接触的实际项目中,这么干的人不少,但他们最后都会被几个问题找上门:
首先是容量失控。hostPath没有配额,应用在里面写多少都不受控,磁盘满了整个节点直接进入NotReady状态。这个故障我见过不止一次,中招的团队最后都是靠迁移节点才能恢复。
其次是调度问题。常规的PV调度是“先有存储,再绑定Pod”,但hostPath根本没有这个机制,Pod走了目录还在,数据没有统一管理。OpenShift虽然有nodeSelector可以辅助,但本质上是人工在管。
第三是多节点一致性。你在应用里写死了节点路径,其他节点没有这个目录或没有对应磁盘,Pod一申警就翻车。数据写在某个特定节点上,Pod被调度到别的节点就全完蛋。
Local Storage方案从根本上解决这些问题:PV由Operator统一发现并注册到集群,PVC会触发节点亲和性(nodeAffinity)自动选择有对应磁盘的节点,回收策略可控,容量可见。
2.2 Local Storage Operator vs 手动创建PV
手动创建Local PV的方式是存在的,比如你在Kubernetes里手工写PV的YAML,设置local类型和nodeAffinity,在OpenShift里也能用。但我不建议这么干,原因很实际:
| 对比维度 | Local Storage Operator | 手动创建PV |
|---|---|---|
| 磁盘发现 | 自动扫描节点上指定的磁盘路径 | 手工指定,节点多了容易漏 |
| 生命周期管理 | PV由Operator统一管理,删除LocalVolume即回收 | 手工作业,删漏了容易留下孤儿PV |
| 存储类绑定 | 自动创建并提供StorageClass | 需要手工配置和关联 |
| 多节点扩展 | 加节点后只要label匹配,自动发现 | 每个节点的PV都需要手工创建 |
| 故障恢复 | PV状态异常时Operator会重新同步 | 没有自动化,出了问题只能手动处理 |
手动创建PV在测试环境、单节点环境下其实够用,但一旦上了生产环境,有一个真实节点群,你就体会到自动化的价值了。
2.3 为什么用块设备而不是直接给目录
Local Storage有一个容易踩坑的点:配置LocalVolume时,到底是给“磁盘路径”还是给“目录路径”?
我推荐的做法是:在节点上把物理磁盘(或分区)格式化好,挂载到一个独立目录,然后在LocalVolume里指定这个挂载点路径。举例说,节点上有/dev/sdb,你把它格式化成xfs并挂载到/mnt/local-storage,然后在LocalVolume的path字段写/mnt/local-storage。
这样做的好处是:PV的容量就是整个磁盘的容量,容量语义清晰;磁盘满了不会拖垮系统盘;重新挂载和替换盘也比较干净。
如果直接把一个系统盘下的大目录(比如/var/lib/data)交给LocalVolume,问题就很大:系统盘空间被容器数据吃光,节点告警信息基本都是红海。所以务必用独立磁盘挂载路径。
注意:LocalVolume定义里的
path字段,是节点上已经存在并挂载好的目录路径,不是设备路径。很多第一次接触的人以为可以写/dev/sdb,这是错的。
3. 部署流程与核心配置
3.1 环境准备与节点规划
先说我的一个典型环境,供参考:
- OpenShift版本:4.10+
- 三个master节点,两个worker节点
- worker节点各自有独立磁盘:
/dev/sdb(未分区,1TB)、/dev/sdc(未分区,500GB) - 计划把
/dev/sdb格式化为一个数据盘挂载到/mnt/disk1,用LocalVolumeSet自动匹配/dev/sdb这种类型的设备路径
在部署前,需要先确认节点上磁盘状态。在OpenShift节点上执行:
# 查看节点上的块设备 lsblk # 确认磁盘未被使用 df -hT /mnt3.2 安装Local Storage Operator
在OpenShift 4.x中,Operator通常通过OperatorHub安装。这里给出核心过程:
- 在OpenShift Web Console中,进入Operators -> OperatorHub
- 搜索“Local Storage Operator”
- 选择安装,我一般把安装模式选为“All namespaces on the cluster”,命名空间选择
openshift-local-storage - 更新策略选手动或自动都行,生产环境我建议手动,踩过地球的坑
命令行方式也可以做,如果你是全命令行工作流,可以这样干:
# 创建命名空间 oc create namespace openshift-local-storage # 创建OperatorGroup cat <<EOF | oc apply -f - apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: local-storage-operator-group namespace: openshift-local-storage spec: targetNamespaces: - openshift-local-storage EOF # 创建Subscription cat <<EOF | oc apply -f - apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: local-storage-operator namespace: openshift-local-storage spec: channel: stable name: local-storage-operator source: redhat-operators sourceNamespace: openshift-marketplace EOF安装完成后,检查Operator的Pod状态:
oc get pods -n openshift-local-storage3.3 配置LocalVolumeSet
LocalVolumeSet的用法我比较推荐,它可以通过设备路径的字符串模式批量匹配磁盘。下面是示例YAML:
apiVersion: local.storage.openshift.io/v1 kind: LocalVolumeSet metadata: name: local-disks namespace: openshift-local-storage spec: nodeSelector: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - worker-0 - worker-1 storageClassDevices: - storageClassName: "local-sc" volumeMode: Filesystem devicePaths: - /dev/sdb tolerations: - key: node.ocs.openshift.io/storage operator: Exists volumeMode: Filesystem重要参数解释:
nodeSelector:限定哪些节点参与本地存储。别省,否则每个节点都会被扫一遍devicePaths:匹配设备路径。可以写一个路径,也可以写通配符模式storageClassName:这个LocalVolumeSet关联的StorageClass名称volumeMode:Filesystem或Block。数据库和高IO应用用Block,普通文件存储用Filesystemtolerations:如果你的节点有污点,需要加容忍,否则PV创建不了
创建命令:
oc apply -f localvolumeset.yaml创建后,Operator会自动检查节点上的磁盘状态,并为每个满足条件的节点创建Local PV。
3.4 创建StorageClass
LocalVolumeSet创建后,Operator会自动创建一个StorageClass,名称就是你在storageClassDevices里写的那样。通常不需要手工创建。但如果你需要更细的控制,比如设置回收策略,可以手工创建,需要注意provisioner是kubernetes.io/no-provisioner,这是因为Local PV是静态供应,动态供应的意义不大:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-sc provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete其中volumeBindingMode: WaitForFirstConsumer是Local Storage的关键参数。它意味着PV和PVC的绑定会延迟到第一个Pod被调度时,确保Pod被调度到有对应本地磁盘的节点,而不是先绑定了再发现节点不匹配。这个特性是Local Storage能正常工作的核心逻辑,没有它,Pod会被调度到一个没有对应磁盘的节点上,然后一直ContainerCreating。
3.5 验证PV与PVC联动
创建完成后,核验一下:
# 查看PV列表 oc get pv # 查看PV详情 oc describe pv <pv-name>你应当看到类似下面的信息:
Name: local-pv-xxxx StorageClass: local-sc Status: Available Capacity: 100Gi Access Modes: RWO PersistentVolume Reclaim Policy: Delete Node Affinity: Required Terms: Term 0: kubernetes.io/hostname in [worker-0]注意看Node Affinity字段,它自动带上了节点选择器,这就是你应用PVC时,Pod会被正确调度到worker-0或worker-1的关键。
如果PV没有出现,先用下面命令查看Operator轮询的状态:
oc get localvolumeset -n openshift-local-storage oc describe localvolumeset local-disks -n openshift-local-storage最常见的状态是Discovering或Failed,可以根据事件里的报错定位(通常是设备路径写错、磁盘已经被占用等)。
4. 应用接入与实测记录
4.1 声明PVC
现在模拟一个应用场景:部署一个单节点PostgreSQL,数据存在本地磁盘上。
首先创建PVC:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-data namespace: default spec: accessModes: - ReadWriteOnce storageClassName: local-sc resources: requests: storage: 80Gi注意这里的storageClassName必须和LocalVolumeSet里配置的一致。accessModes必须是ReadWriteOnce,因为本地盘只能被单个节点读写。
4.2 部署测试应用
部署一个简单Pod来验证PVC绑定和调度:
apiVersion: v1 kind: Pod metadata: name: test-local-pod namespace: default spec: containers: - name: app image: registry.access.redhat.com/ubi8/ubi-minimal:latest command: ["/bin/sh", "-c"] args: - while true; do echo "$(date) writing to local storage" >> /mnt/data/test.log; sleep 5; done volumeMounts: - name: data mountPath: /mnt/data volumes: - name: data persistentVolumeClaim: claimName: postgres-data创建后,观察Pod调度情况:
oc get pod test-local-pod -o wide你会看到Pod被调度到了有对应PV的节点上。这是因为PVC绑定的是Local PV,而Local PV自带nodeAffinity,调度器会根据它选择节点。
进入Pod验证数据写入:
oc exec -it test-local-pod -- tail -f /mnt/data/test.log看到日志在持续写入,PV和Pod联动正常。
4.3 验证Pod重建后的数据持久性
删除这个测试Pod,再重新创建,数据应该还在:
oc delete pod test-local-pod oc apply -f test-local-pod.yaml oc exec -it test-local-pod -- tail -5 /mnt/data/test.log数据依然存在。这个验证很关键,它说明应用Pod无论怎么重建,只要PVC还在,数据就不会丢。
4.4 Block模式配置对比
如果应用需要的是裸块设备(比如某些数据库需要独占磁盘块),配置稍有不同。LocalVolumeSet部分修改如下:
spec: storageClassDevices: - storageClassName: "local-block-sc" volumeMode: Block devicePaths: - /dev/sdc volumeMode: Block然后应用侧挂载方式也不是传统的文件路径,而是设备路径:
volumes: - name: data persistentVolumeClaim: claimName: block-pvc容器内设备会出现在/dev/下,具体路径由卷挂载决定。这种模式多用于对IO延迟极其敏感的场景。
5. 常见问题与排查
5.1 PV创建了但一直是Pending状态
PV已经出现,但PVC一直Pending。我的排查顺序是:
- 先看PVC的Events:
oc describe pvc postgres-data- 查看Pod的事件:
oc describe pod test-local-pod这类问题的核心原因通常是volumeBindingMode: Immediate导致的。当StorageClass的volumeBindingMode不是WaitForFirstConsumer时,PVC的绑定过程可能发生在调度前,导致Pod被调度到没有本地PV的节点上,PV一直Pending。解决方案就是把StorageClass的volumeBindingMode改为WaitForFirstConsumer。
另外一个常见原因是节点污点没被容忍。如果节点上有污点,而LocalVolumeSet没有配置对应的tolerations,PV创建流程会被中断。
5.2 磁盘删了PV还在
Local Storage的PV由Operator管理,当节点上的磁盘被物理移除时,PV不会被立刻删除,它会进入Released状态。这是Local PV和动态存储的一个明显差异——动态存储(如Ceph RBD)目录存储删除PV是常态,但Local Storage里你想要清理旧PV,需要手动删除PV并清理节点上的数据目录。
这里有一个操作上的经验:LocalVolumeSet删除后,如果PV状态仍然是Released或Available,你需要在节点上手动清理数据目录,否则PV虽然删除了,但磁盘空间并没有释放。我在交付项目时,都会在删除PV的流程文档里加上这一步:rm -rf /mnt/local-storage/xxx。
5.3 Pod被调度到了错误的节点
有时Pod没有被调度到你期望的有磁盘的节点上。原因是PV的nodeAffinity指向的节点没问题,但可能的坑是:PVC绑定的PV确实在一个节点,但由于Pod有额外的标签选择器或亲和性设置,反而把Pod推到了其他节点。这时PV就只能在你期望的节点上待命,Pod就会一直Pending。
排查方法是把Pod调度器的最终决策过程看一遍:
oc describe pod test-local-pod | grep -A20 Events看到类似node(s) didn't match Pod's node affinity/selector,就顺着Pod调度器所看到的节点标签来检查。
5.4 节点重启后PV不见了
这个现象在一些基于老版本Kubernetes/OpenShift的Local PV实现中出现过,重启后PV处于Lost状态。原因是节点重启过程中,磁盘设备路径可能发生变化(特别是在有多块盘且没有使用UUID挂载的场景)。建议是在节点上通过/dev/disk/by-uuid或/dev/disk/by-id来确认挂载路径,保证设备路径不会因为重启而漂移。
5.5 容量监控与告警
Local Storage不像Ceph或GlusterFS那样有集中的容量监控。它的容量是分散在各个节点上的。我建议的运维手段是:在集群层面对每个Local PV设置Alert,比如Prometheus里按PV的容量使用率做告警:
( sum by (persistentvolumeclaim, namespace) ( kubelet_volume_stats_used_bytes ) / sum by (persistentvolumeclaim, namespace) ( kubelet_volume_stats_capacity_bytes ) ) > 0.85这样至少能在磁盘被写满之前收到预警,避免节点进入NotReady状态。
5.6 误删LocalVolumeSet引发的连锁问题
这个踩过坑:直接删掉正在使用的LocalVolumeSet,而没有先确认PVC的引用。这会导致PV被回收,PVC跟着报错,应用全部挂掉。
正确的操作顺序是:
- 先删除所有使用该StorageClass的Deployment/StatefulSet/Pod
- 删除PVC
- 最后删除LocalVolumeSet
如果已经误删了,别慌,先检查PV状态,如果PV状态还是Bound,说明Operator还没触发回收,马上重新创建同名的LocalVolumeSet,让PV重新接管。
6. 生产实践的经验总结
6.1 我常用的配置组合参考
| 场景 | 推荐配置 |
|---|---|
| 单节点数据库(PostgreSQL) | Filesystem模式,WaitForFirstConsumer,容量按磁盘的80%申请 |
| 消息队列(Kafka) | Block模式,多PV分摊,每个PV使用率控制在70%以下 |
| 缓存服务(Redis) | Filesystem模式,独立数据盘,不跟监控日志共用目录 |
| 大数据组件(ES) | Block或Filesystem均可,ES官方对块设备支持更好,建议Block |
6.2 关于扩容和迁移
Local Storage有一个先天的限制:单节点上的PV扩容很困难。因为物理磁盘容量固定,PVC因物理盘满而饱和后,就只能把PV删除后重新创建。这在做容量规划时要特别仔细,我给客户的建议是:按业务数据增长率的3倍预留容量。
同样,PV迁移也麻烦,本地盘的数据要迁移,只能先备份到外部,再恢复到新节点的PV里,与Ceph这类分布式存储的在线迁移体验差异巨大。所以Local Storage的选择标准就是:数据增长可控,且接受单节点故障停机恢复时间。
6.3 与OpenShift其他存储方案的取舍
很多团队会问我和OpenShift Data Foundation(ODF)怎么选。我的判断标准很简单:
需要跨节点高可用和在线扩容,选ODF或外部存储;应用能容忍单节点故障,且追求极致的IO性能,选Local Storage。Local Storage没有副本,没有RAID,没有分布式能力,它就是直通物理盘。它的优点是延迟极低、带宽高、无额外网络开销,缺点就是没冗余。
如果你做的是中等规模集群且已经有外部存储(比如企业级SAN或NAS),那Local Storage更适合做加速盘,而不是主存储。如果做的是边缘节点或小规模集群,Local Storage完全可以扛起有状态应用的主存储职责,前提是你能接受前述的运维限制。
6.4 日常运维清单
最后分享一份我落地到团队里的日常运维清单:
- 每周检查一次Local PV的容量使用率和对应节点的磁盘使用率
- 每次节点维护前,确认该节点上是否有Local PV正在承载业务,先迁移或摘除Pod
- 节点下线流程中,先删PVC和LocalVolumeSet,再清盘,最后才摘除节点
- 以设备UUID方式在
/etc/fstab里固定挂载点,避免重启后设备路径漂移 - 在告警规则中单独针对
WaitForFirstConsumer类型的PVC做绑定状态告警,避免PVC绑定异常未被发现 - 不要在同一个磁盘上既跑Local Storage又跑其他业务数据,一旦PV容量写满,节点会直接受影响
我实际踩过的最大一个坑,就是在一次节点维护时直接把节点重启了,以为是“正常操作”,结果节点上有好几个Local PV对应的磁盘因为设备路径变化全部失联,应用全部起不来。后来把挂载方式改成UUID固定后才稳定下来。这种经验,文档里一般不写,只有实际做过的人才知道痛。
如果你正在规划OpenShift集群的有状态应用存储,Local Storage是一个很值得认真评估的选项。它不像分布式存储那样自带光环,但它简单、直接、高性能,运维好了能支撑不少核心业务。把上面的配置和排查方法吃透,剩下的就是在实践中慢慢积累感觉了。