news 2026/10/6 3:45:24

K8s集群调度与PV/PVC实战:从调度原理到Redis集群部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K8s集群调度与PV/PVC实战:从调度原理到Redis集群部署

K8s学习笔记走到第五篇,这一篇把集群调度和PV/PVC放在一起聊。生产环境下,调度负责决定Pod落在哪个节点,存储决定业务数据能不能持久保留,两者一配合,集群才真正算得上“可用”。如果你已经装好了K8s,正卡在Pod调度、数据持久化、或者想部署Redis集群这类有状态服务,这篇文章可以帮你少走很多弯路。我写这份笔记的时候用的是Rocky Linux, kubeadm方式装的集群,新版K8s的调度和存储接口没有大变化,所以你本地环境只要是1.20以上的版本,都可以直接照着试。

1. 调度和存储:放在一起才能讲清楚的事

1.1 为什么要拆开“调度”和“PV/PVC”再合并讲

大多数人学习K8s的时候,会把调度和存储当成两个独立的知识点,比如今天学了nodeSelector,明天学了PersistentVolume,但到了实际部署一个真正有状态的应用,比如Redis集群,就会发现这两个概念是拧在一起的。Pod要调度到能访问对应存储的节点上,PVC要能绑定到合适的PV,PV又要能被调度后的节点挂载。你如果只懂一边,遇到问题就会两头猜。

再说个很多刚接触K8s的人容易混淆的点:K8s和Docker到底有什么区别。Docker本身解决的是单机容器的创建、运行和隔离,你在一台机器上docker run,容器只能在这一台机器上活着;而K8s是把多台机器组成一个集群,它会负责把Pod调度到最合适的节点,然后再通过抽象存储把数据留在Pod之外。所以“集群调度”和“PV/PVC”正是K8s对比Docker的核心价值之一,这期笔记专门把这两块掰开揉碎。

1.2 先明确几个对象:Namespace、Node、Pod、PV/PVC

在进入调度细节之前,得先把资源对象的位置理清楚。Namespace是逻辑隔离单位,默认有default、kube-system、kube-public这些,PV和Node是集群级的对象,不归属于任何Namespace,而PVC和Pod是命名空间级的对象,只能在使用它的Namespace里创建。

我见过很多人调PVC的时候,用kubectl get pv能看到PV,用kubectl get pvc --all-namespaces却找不到某个PVC,这就是没搞明白命名空间的作用。Pod调度时需要读取Node的信息,Node有标签、有资源、有污点;Pod需要存储时,先通过PVC去申请,PVC再和PV绑定,PV背后是具体的存储后端。整个过程可以看作:调度器决定Pod在哪里住,存储系统决定Pod住下来以后东西存在哪里。两者都需要以API Server为中心进行协同。

2. 集群调度:Pod怎么找到自己该待的节点

2.1 kube-scheduler的工作流程没那么神秘

Kube-scheduler就是K8s默认的调度器组件,它做的事情可以用一句话概括:持续监听集群里没有被分配节点的Pod,然后给每个Pod选一个最合适的节点。这里的“合适”不是拍脑袋,而是经过两轮筛选。

第一轮叫过滤,把所有不满足硬性条件的节点剔除掉,比如资源不足、端口冲突、不满足nodeSelector等;第二轮叫打分,根据一系列优先级规则给剩余节点打分,比如资源均衡度、Pod聚集度等,最后选择分数最高的那个。整个过程你可以当成选房子:过滤阶段排除掉“价格超出预算”和“离公司太远”的房源,打分阶段再选“通勤时间最短”和“生活配套最好”的选项,然后再签约。

实际工作中,你不需要关心调度器内部每个算法,但一定要知道几个核心的调度对象:nodeSelector、nodeAffinity、污点与容忍、Pod亲和性与反亲和性。这四样东西基本覆盖了日常90%的调度需求。

2.2 nodeSelector:给节点贴标签,让Pod按其选择

最简单的调度方式就是给节点打标签,然后在Pod的spec里用nodeSelector指定标签的值。比如我想让Pod只跑到GPU节点上:

apiVersion: v1 kind: Pod metadata: name: gpu-test spec: nodeSelector: gpu: "true" containers: - name: cuda image: nvidia/cuda:12.0-base

给节点打标签只需要一条命令:

kubectl label node node01 gpu=true

这个方式够用,但有几个明显的坑。第一是它只能做等值匹配,不支持“不是某节点”或者“标签存在即可”这类逻辑;第二是没有“尽量满足”的弹性,只要没有节点带gpu=true,Pod就会一直Pending。所以对于复杂的生产场景,我更推荐用nodeAffinity。

2.3 nodeAffinity:从“必须”到“尽量”的弹性策略

NodeAffinity相比nodeSelector灵活得多,它有两种策略,分别对应硬性要求和软性偏好。requiredDuringSchedulingIgnoredDuringExecution表示必须满足,不满足就不调度;preferredDuringSchedulingIgnoredDuringExecution表示尽量满足,如果满足不了,调度器也会找其他节点兜底。

举个实际例子。比如我想让Redis主从分到不同可用区,但不强制必须分:

affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - zone-a

很多初学者容易忽略operator的用法,NodeAffinity支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。比如我想排除某一批机器,就可以用NotIn。还有一个常见的坑是:如果你同时写nodeSelector和nodeAffinity,调度器会要求两者同时满足,不是说改了affinity就可以不写selector了。

2.4 污点与容忍:把“坏人”请下专用的节点

污点(Taint)和容忍(Toleration)是另一组对抗性设计。污点是打在节点上的标记,表示“这个节点我不喜欢某些Pod”;容忍是打在Pod上的标记,表示“我能够忍受这个污点”。有容忍的Pod才有资格被调度到带污点的节点上。

最常见的场景就是控制节点。用kubeadm安装的K8s集群,控制节点默认带一个污点,比如node-role.kubernetes.io/control-plane:NoSchedule,所以普通的Pod不会跑到Master节点上。如果我想让一个监控Pod跑到控制节点,就需要给它加对应的容忍:

tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule

污点效果主要有三种:NoSchedule表示不接受新Pod,但已经运行的Pod不受影响;PreferNoSchedule是一个软限制;NoExecute会更狠,它会驱逐节点上已有的、没有容忍的Pod。

给节点手动打污点也是一行命令:

kubectl taint nodes node02 disktype=ssd:NoSchedule

加上容忍后,Pod就可以被调度到SSD节点上。这个组合对于专用节点特别有效,比如GPU节点打上污点,只有申请了GPU的Pod带容忍调度上去,其他普通Pod就不会跑上去浪费资源。

2.5 调试:Pod一直Pending怎么排查

调度的问题绝大多数都会表现为一个现象:Pod卡在Pending,就是不Running。这时候首先用kubectl describe pod <pod-name>看底部Events,调度器会把原因直接写在里面。常见的Pending原因我整理成了一张速查表:

现象可能原因排查方式
Events显示0/3 nodes available节点资源不足或节点NotReadykubectl describe nodes
有nodeSelector不匹配节点没有对应标签kubectl get nodes --show-labels
有污点无容忍节点taints导致拒绝调度kubectl describe node
PVC Pending存储没有绑定导致Pod无法启动kubectl describe pvc
API Server不稳定kubectl命令正常但调度没反应查apiserver日志

这里我特别想提一下热词里那个“master初始化显示the api server is not healthy after 4m0.00747357s”的问题。很多人在Rocky上装K8s,kubeadm init之后看到这条日志就慌了。我遇到过几次,其实根因通常是kube-apiserver容器起不来,常见原因是镜像拉取失败、etcd健康检查失败或者证书配置问题。优先查kubectl get pods -n kube-system看apiserver是否Running,再kubectl logs -n kube-system kube-apiserver-master看具体报错,不要反复重置集群。这个错误属于安装期问题,但它也会直接影响调度,因为调度器依赖apiserver提供的数据,apiserver不健康,后面所有调度动作都转不起来。

3. PV/PVC:把存储从Pod里真正抽离出来

3.1 从emptyDir到hostPath再到PV/PVC

学习PV/PVC之前,我建议你先回顾一下容器存储的三个层次。第一个层次是emptyDir,它只是一个Pod生命周期内的临时目录,Pod删掉,数据也就没了,适合放缓存和临时文件,不适合放数据库数据。第二个层次是hostPath,它直接把宿主机目录挂载到容器里,数据能留下来,但问题很大:如果Pod被调度到另一台节点,数据就不会跟着走;而且多副本Pod同时写同一个hostPath目录很容易冲突。

第三个层次就是PV/PVC,它把存储从具体节点抽象出来。应用不需要关心PV背后是NFS、Ceph还是云盘,只需要像声明CPU和内存一样,声明“我要多少存储、什么访问模式”,然后系统去匹配或创建对应的PV。PV相当于集群里的存储资源,PVC相当于应用的存储请求。

我用一个生活化的类比:PV是你家的仓库,PVC是你向仓库发出的订单。PVC里写着“我要10Gi空间,可读可写”,系统看到订单后去仓库里找一个符合条件的PV,如果PV的容量、访问模式、StorageClass标签都对得上,PVC就会和PV绑定。如果找不到,PVC就会一直Pending,Pod自然起不来。

3.2 访问模式和回收策略:绑定前必须看懂的两个参数

PV/PVC的绑定不是随便绑,至少要匹配容量、访问模式和StorageClass名称。访问模式有三种常见值:

  • ReadWriteOnce:只能被单个节点以读写方式挂载,适合数据库单实例。
  • ReadOnlyMany:可以被多个节点以只读方式挂载,适合配置文件共享。
  • ReadWriteMany:可以被多个节点同时读写,适合文件存储、共享目录。

很多人在做Redis集群时,想用ReadWriteMany,但后端存储是本地块设备,根本支持不了多节点读写,结果PVC一直Pending。所以配置前要先确认你的存储后端能力,NFS支持ReadWriteMany,Ceph RBD一般只支持ReadWriteOnce。

回收策略上,PV有Retain和Delete两种主流策略。Retain表示PV释放后需要管理员手动清理和重置;Delete表示PV释放时直接删除底层存储资源。我建议在测试环境用Delete,生产环境先设置Retain,给自己留一条后悔路。

3.3 手动创建PV和PVC:一步一步把绑定跑通

在没有StorageClass的情况下,你得先手工建PV,再手工建PVC,最后让Pod使用PVC。下面我用NFS存储举个完整例子。首先在存储节点上准备一个目录并共享,假设IP是192.168.1.100,路径/data/nfs。

创建PV:

apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.1.100 path: /data/nfs

创建PVC:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi

创建之后,用kubectl get pv和kubectl get pvc查看状态。如果PVC的STATUS还是Pending,最常见的原因就是容量、访问模式不匹配,或者PV已经被别的PVC绑定了。PV和PVC绑定成功后,你再写Pod:

spec: containers: - name: app image: busybox volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: nfs-pvc

这里踩过一个坑:PV的容量是10Gi,PVC申请也是10Gi,但如果你PV里有storageClassName字段,而PVC里没有指定对应的StorageClass,绑定就失败。所以要么PV不写storageClassName,要么两边完全一致。

3.4 StorageClass动态供给:从“手工匹配”到“一键生成”

手工创建PV只适合测试,生产里更常见的是用StorageClass实现动态供给。你只要创建一个StorageClass,再创建PVC时指定storageClassName,系统就会自动通过Provisioner创建PV,不需要你提前手动建PV。这个过程非常像云主机自动挂云盘,PVC提需求,StorageClass后面的Provisioner去调用存储接口完成创建和挂载。

一个简单的StorageClass定义如下:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-external-provisioner reclaimPolicy: Delete

然后在PVC里指定:

spec: storageClassName: nfs-client accessModes: - ReadWriteMany resources: requests: storage: 5Gi

PV会自动生成,你看不到手工创建PV的过程,但kubectl get pv里会多出一个名称类似pvc-uuid的PV。这个方式适合在Redis集群这种需要大量持久化存储的场景下使用,后面我会专门用一个实战例子把它串起来。

4. 实战:部署Redis集群,同时用到调度策略和PVC

4.1 需求分析和拓扑规划

热词里看到很多人搜“k8s redis集群”,我做这期笔记的时候也顺手在现网环境里重新部署了一遍。Redis集群是典型的有状态应用,需要同时考虑调度和存储。

我的需求很简单:部署一个三节点的Redis集群,一主两从。每个Redis实例需要独立的存储空间,数据不能因为Pod重启而丢失;三个实例尽可能分散到不同节点,避免一台宿主机挂了导致整个集群崩溃;每个Pod需要稳定的网络标识,因为Redis集群通过节点地址组成Cluster Bus,重启后地址不能变来变去。

这三个需求对应到K8s上的方案就三个字:StatefulSet。StatefulSet本身提供了稳定的网络标识,可以通过volumeClaimTemplates为每个副本自动创建PVC,再配合PodAntiAffinity让不同副本调度到不同节点。这就是调度和存储结合的典型场景。

4.2 关键YAML:StatefulSet里如何写调度与存储

下面是一个简化但能用的Redis StatefulSet片段,重点看spec部分的affinity和volumeClaimTemplates:

apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster spec: serviceName: redis-headless replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: redis topologyKey: kubernetes.io/hostname containers: - name: redis image: redis:7.0 command: ["redis-server"] args: ["--appendonly", "yes"] volumeMounts: - name: redis-data mountPath: /data volumeClaimTemplates: - metadata: name: redis-data spec: storageClassName: nfs-client accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 5Gi

这段配置有四个关键点。

第一,serviceName: redis-headless表示这个StatefulSet要搭配Headless Service使用,每个Pod获得一个稳定的DNS名,比如redis-cluster-0.redis-headless.namespace.svc.cluster.local。

第二,podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution要求每个Redis Pod尽量落在不同节点上,topologyKey用kubernetes.io/hostname表示按主机维度区分。这里我的测试环境有三个节点,所以三个副本可以均匀分布;如果你节点不够,调度器会强制等待,Pod就会卡在Pending,这是需要留意的。

第三,volumeClaimTemplates会自动为每个Pod创建一个PVC,名称规则是<volumeClaimTemplate-name>-<StatefulSet名称>-<序号>,比如redis-data-redis-cluster-0、redis-data-redis-cluster-1。

第四,我选的访问模式是ReadWriteOnce,因为每个Redis实例只需要本节点访问自己的存储。要注意的是,如果你后端是NFS,那么storageClassName: nfs-client对应的Provisioner能创建PV,而NFS本身支持多节点读写,但这里声明为ReadWriteOnce也没有问题。

创建Headless Service:

apiVersion: v1 kind: Service metadata: name: redis-headless spec: clusterIP: None selector: app: redis ports: - port: 6379 targetPort: 6379

然后执行:

kubectl apply -f redis-sts.yaml kubectl get sts kubectl get pods -o wide kubectl get pvc

正常情况下三个Pod会陆续创建,每个Pod都有自己对应的PVC。如果你看到某个Pod卡在ContainerCreating,别急着删,先看下面这个坑。

4.3 踩坑记录:一主两从卡在ContainerCreating

我在实际操作里遇到过最典型的问题是:StorageClass配好了,PV也动态创建了,但Redis Pod一直ContainerCreating。用kubectl describe pod看到Events里报错,说挂载NFS失败,mount: wrong fs type。

排查起来分三步走。

第一步检查PVC状态,kubectl get pvc如果PVC不是Bound,问题在存储绑定,去查StorageClass的Provisioner是否正常运行,Podnfs-client-provisioner是否在Running。

第二步检查Pod挂载报错,kubectl describe pod redis-cluster-0,如果看到MountVolume相关的FailedMount信息,重点看宿主机上有没有装nfs-utils或nfs-common。Rocky系统的NFS客户端软件包没装齐,K8s向节点下发NFS挂载就会报错。

第三步检查节点容忍情况,后来我发现有一个Pod一直Pending,原因是那个节点NotReady或者taints没排掉,调度器过不去。你可以用kubectl describe node node03查看Taints,如果不希望Pod跑到这个节点,就不要把副本数超过可用节点数量。

还有一次我把podAntiAffinity写成了required: true,但集群当时只有三个节点,其中一个是控制节点带污点,实际可调度的只有两个工作节点,结果第三个副本永远Pending。后来改成preferred或给控制节点加容忍才能跑起来。这里也引出了经验:反亲和性的required策略一定要提前确认可用节点数量,否则它会非常严格地拒绝调度。

4.4 常见问题速查表

给出一张我遇到的K8s调度和存储问题速查表,方便你在实际环境里快速定位:

问题可能原因排查与解决
Pod一直Pending,Events无可用节点CPU/内存不足,节点NotReady,或标签不匹配kubectl describe node查看资源与状态
Pod一直Pending,Events提示有污点节点有Taints,Pod无Tolerations按需添加容忍或去掉节点污点
PVC一直Pending,PV没有绑定容量/访问模式/StorageClass不匹配kubectl describe pvc,比对PV字段
PV显示Released但PVC无法复用回收策略是Retain,PV没有清理手动删除PV重新创建,或改Delete策略
StatefulSet副本调度到同一节点没有配置PodAntiAffinity配置反亲和性或增加可用节点
ContainerCreating挂载不了NFS宿主机缺少NFS客户端,或存储服务不可达安装nfs-utils/nfs-common,测试存储网络
API Server不健康导致调度无反应apiserver容器异常、etcd异常查看kube-system的apiserver pod日志修复后再用

这张表是我从实际部署过程中总结出来的,基本上你在搜索“k8s redis集群”时遇到的那些问题,都能在这里找到影子。

5. 从“能用”到“好用”:一点个人经验

踩过几次坑之后,我最大的感受是:调度和存储这两个主题,单独看书都觉得不难,但组合起来就像一对舞伴,任何一个节奏不对都可能摔倒。我在实际项目里经常要先问自己四个问题:这个Pod能不能容忍节点故障?数据要不要跨节点共享?底层的存储端能不能支持多节点读写?副本数量是否比可用节点数量还多?这四个问题想清楚,再去写YAML,成功率会高很多。

另外有一个小技巧分享给你。在测试环境里,我从来不用现成的Helm Chart,而是先手写裸的YAML,一个字段一个字段地验证。比如先创建一个带nodeSelector的Pod,看调度结果;再创建一个PVC,看绑定状态;最后才把两者合到一个StatefulSet里。这个过程会逼着你理解每个字段的作用,而不是依赖模板之后变成“能跑,但出了问题不知道怎么查”的状态。

集群调度和PV/PVC的学习笔记到这里,下一步我打算把Service和Ingress的流量机制整理清楚,毕竟数据落下来之后,还得让请求正确进来。如果你也在看K8s学习笔记,可以把这一篇当成第五站,前面补上安装部署、Namespace基础,后面再继续深入网络和可观测性。目前这套笔记还是在Rocky Linux加kubeadm的集群上实测过的,你换Ubuntu或者二进制方式安装也大同小异,核心思路不会变。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:45:16

伴随灵敏度分析驱动的大规模时空放疗优化:Matlab实战

这几年做肿瘤生长建模相关的仿真工作&#xff0c;有一个问题几乎每次都会被问到&#xff1a;模型里十几个生物学参数&#xff0c;到底哪些对优化结果的影响最大&#xff1f;在单次仿真里调整一个参数、对比结果变化&#xff0c;这种做法在参数少时还算能用&#xff0c;但当决策…

作者头像 李华
网站建设 2026/10/6 3:45:12

Docker从零开始:Ubuntu/CentOS安装、镜像容器命令与踩坑实录

写这篇教程的起因&#xff0c;是我见过太多人第一次接触Docker就卡在安装这一步&#xff1a;要么apt源指向官方地址半天拉不下来&#xff0c;要么装好之后不知道怎么和镜像、容器打交道&#xff0c;还有人干脆绕道去用Windows版的Docker Desktop&#xff0c;结果在Linux服务器上…

作者头像 李华
网站建设 2026/10/6 3:44:42

Android Studio Inspection位置全解析:从菜单到结果面板

1. 找过的人都有这种感觉&#xff1a;inspection 不止一个“位置”很多人在群里问“android studio inspection位置”&#xff0c;其实问的是完全不一样的三件事&#xff1a;有人要的是“运行代码检查”的菜单入口&#xff0c;有人要找的是“检查规则设置”那一整页选项&#x…

作者头像 李华
网站建设 2026/10/6 3:44:28

Docker容器中使用GPU:从NVIDIA Container Toolkit到避坑实战

1. 为什么容器“看不见”GPU&#xff0c;以及这条访问路径到底由哪几层构成我第一次在 Linux 服务器上尝试docker run --runtimenvidia ... nvidia-smi时&#xff0c;宿主机侧一切正常&#xff1a;驱动装了&#xff0c;CUDA 装了&#xff0c;显卡信息在宿主机上输出得很好看。结…

作者头像 李华
网站建设 2026/10/6 3:44:28

MySQL并发控制详解:脏读、不可重复读、幻读与MVCC/间隙锁

运维和研发联查线上问题的时候&#xff0c;我最怕听到的一句话是"这个SQL我本地跑没问题"。本地之所以没问题&#xff0c;多半不是因为SQL本身写得好&#xff0c;而是因为没有第二个事务在同一秒里跟你抢数据。MySQL 并发控制要解决的就是这种"抢"&#xf…

作者头像 李华
网站建设 2026/10/6 3:43:47

Flutter鸿蒙化实战:open_meteo天气数据接入与踩坑记录

最近在把一套 Flutter 应用往鸿蒙生态迁移&#xff0c;第一件事就是找可靠的气象数据源。我最终选了 open_meteo 这个三方库&#xff0c;免费、无需密钥、覆盖全球、支持高精度天气预报。所谓鸿蒙化适配&#xff0c;并不是说把 open_meteo 库重写一遍&#xff0c;而是要让它在鸿…

作者头像 李华