Kubernetes调度与控制器实战:从Pod Pending到集群稳定的核心机制

发布时间:2026/10/23 6:17:44
Kubernetes调度与控制器实战:从Pod Pending到集群稳定的核心机制 1. 从单机容器到集群调度为什么第四篇才真正摸到K8s的门如果你跟着这个系列一路看下来前三篇大概率已经让你把容器跑起来了、把镜像构建明白了、也把最基础的Pod概念过了一遍。但说实话前三篇的内容用Docker Compose基本都能平替甚至更简单。真正让Kubernetes区别于其他容器编排工具的东西是从你开始认真对待“调度”这件事才浮现出来的。我自己的学习路径就是这样前两周觉得K8s不过如此不就是把docker run换成了kubectl apply吗直到我尝试把一个有状态服务部署上去Pod一会儿跑到这个节点、一会儿跑到那个节点数据卷挂载对不上服务发现时灵时不灵我才意识到——K8s的核心不是“跑容器”而是“让一群机器像一台机器一样工作”。这个认知转变是第四篇笔记真正想记录的东西。这篇笔记主要围绕四个问题展开Pod到底是怎么被分配到节点上的、控制器如何保证你的期望状态不被打破、服务发现和网络在集群内部是怎么串起来的、以及资源限制和调度策略在实际操作中会给你带来哪些“惊喜”。适合已经能跑通基础Pod、但对集群行为感到困惑的读者。如果你还没跑通过第一个Pod建议先回看前三篇的内容。提示这篇笔记里的所有操作都基于一个三节点集群一个控制面、两个工作节点的模拟环境具体版本差异我会在涉及的地方标注出来。2. Pod调度的真实逻辑从Pending状态说起2.1 一个Pending的Pod背后发生了什么很多人第一次遇到Pod卡在Pending状态时第一反应是“集群坏了”。其实Pending恰恰是调度器在告诉你我找不到合适的节点放这个Pod。要理解这件事得先知道调度器的工作流程。K8s的默认调度器kube-scheduler做决策分两个阶段过滤Filtering和打分Scoring。过滤阶段把所有不满足硬性条件的节点排除掉比如资源不够、节点有污点、端口冲突、亲和性不匹配。打分阶段对剩下的节点按优先级排序选最高分的那个。我踩过的一个典型坑创建了一个要求2核CPU的Pod但两个工作节点各只剩1.5核可分配。Pod就一直Pending。kubectl describe pod的Events区域会明确写出“0/3 nodes are available: 3 Insufficient cpu”。这句话比任何文档都直接。kubectl describe pod pod-name | tail -20这个命令应该成为你遇到Pending时的第一个动作。Events里的信息通常足够定位问题不需要上来就翻调度器日志。2.2 资源请求与限制调度器眼里的“账本”调度器做过滤时看的不是节点的实际使用量而是已分配的资源请求Requests总和。这一点非常关键也是新手最容易误解的地方。假设一个节点有4核CPU上面跑了三个Pod每个Pod的requests是1核但实际使用只有0.2核。那么调度器认为这个节点只剩1核可分配而不是3.4核。这种“按请求记账”的机制保证了资源不会被超卖但也意味着你如果requests设得太大集群利用率会很低。resources: requests: cpu: 500m memory: 256Mi limits: cpu: 1 memory: 512Mi我的经验是requests按服务稳定运行时的实际消耗来设limits按峰值来设。requests设太小会导致节点被超卖设太大会浪费资源。一个比较稳妥的做法是先用监控跑一周看P95的CPU和内存使用量然后requests取P95值limits取P95的1.5到2倍。注意CPU的limits和requests不一致时Pod可能被限流throttling表现为响应变慢但不会被杀掉。内存的limits如果被超过Pod会直接被OOM Kill。这个差异在实际运维中影响很大。2.3 节点亲和性与污点容忍把Pod放到该去的地方默认调度器是“哪里有空位就去哪里”但生产环境往往需要更精细的控制。比如GPU节点只跑训练任务、SSD节点只跑数据库。这时候就要用到节点亲和性nodeAffinity和污点Taint与容忍Toleration。污点的逻辑是“节点拒绝某些Pod”容忍的逻辑是“Pod愿意接受某些污点”。两者配合使用可以实现专用节点的效果。比如给GPU节点打上污点kubectl taint nodes gpu-node-1 dedicatedgpu:NoSchedule然后只有声明了对应容忍的Pod才能调度上去tolerations: - key: dedicated operator: Equal value: gpu effect: NoSchedule节点亲和性则更灵活可以表达“尽量放在某类节点”或“必须放在某类节点”。requiredDuringSchedulingIgnoredDuringExecution是硬性要求preferredDuringSchedulingIgnoredDuringExecution是软性偏好。我一般用软性偏好做负载均衡用硬性要求做架构约束。2.4 调度失败的排查链路遇到调度问题我通常按这个顺序排查kubectl describe pod看Events确认是资源不足、亲和性不匹配还是污点问题kubectl describe node看节点的Allocated resources和Taintskubectl get pods -o wide确认现有Pod的分布如果是自定义调度器检查调度器Pod是否正常运行这个链路能覆盖90%以上的调度问题。剩下的10%通常是集群级别的配置问题比如节点NotReady或者调度器本身挂了。3. 控制器如何让“期望状态”变成现实3.1 声明式API的本质你只管说想要什么K8s最核心的设计哲学是声明式API。你告诉它“我要三个副本”它负责让三个副本一直存在。这跟命令式的“启动一个容器”有本质区别。理解这一点才能理解控制器的作用。控制器的工作模式是一个无限循环观察当前状态、对比期望状态、执行动作缩小差距。这个循环叫Reconcile Loop。Deployment控制器、ReplicaSet控制器、StatefulSet控制器都是这个模式的不同实现。我刚开始学的时候有个疑问为什么创建Deployment之后还会自动创建一个ReplicaSet后来明白了Deployment管的是“版本”ReplicaSet管的是“副本数”。滚动更新时Deployment创建新的ReplicaSet逐步增加新RS的副本、减少旧RS的副本从而实现平滑过渡。这种分层设计让每个控制器只关注一件事逻辑清晰且可组合。3.2 Deployment滚动更新的参数调优滚动更新是Deployment最常用的功能但默认参数在生产环境往往不够用。两个关键参数是maxSurge和maxUnavailable。strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxSurge: 1表示更新过程中最多多出1个PodmaxUnavailable: 0表示任何时候可用Pod数量不能少于期望值。这个组合适合对可用性要求高的服务但更新速度会慢一些。如果服务可以容忍短暂的能力下降可以把maxUnavailable设为25%更新会快很多。我的经验是无状态Web服务用maxUnavailable: 25%有状态服务或核心服务用maxUnavailable: 0。还有一个容易被忽略的参数是minReadySeconds。它定义了Pod就绪后多久才被认为可用。默认是0意味着Pod一就绪就计入可用数量。如果服务启动后需要预热比如加载缓存设一个合理的minReadySeconds能避免流量打到还没准备好的Pod上。3.3 StatefulSet与有状态服务的调度难题有状态服务是K8s里比较难处理的部分。StatefulSet给每个Pod一个稳定的网络标识如web-0、web-1和稳定的存储卷。但这也意味着Pod不能随意调度到任何节点否则存储卷可能挂载不上。如果用的是本地存储local PVPod和节点的绑定关系是硬性的。节点挂了Pod就起不来。这时候要么用网络存储如NFS、Ceph要么接受这个限制并做好节点故障预案。我在模拟环境里用local PV跑了一个有状态服务测试节点故障时发现Pod会一直Pending因为调度器找不到有对应PV的节点。kubectl describe pod会提示“node(s) had volume node affinity conflict”。这个提示很明确但第一次遇到时确实会懵。3.4 控制器不工作的常见原因控制器不工作的表现通常是你改了期望状态但实际状态没变化。排查思路控制器Pod本身是否运行正常kubectl get pods -n kube-system是否有准入控制Admission Webhook拦截了请求资源配额ResourceQuota是否已满控制器是否在等待某个依赖条件比如PVC绑定我遇到过一次Deployment不创建Pod的情况最后发现是ResourceQuota限制了Pod数量。kubectl describe quota能看到当前使用量和硬限制这个命令在排查这类问题时非常有用。4. 服务发现与集群网络Pod之间怎么找到彼此4.1 ClusterIP、NodePort、LoadBalancer的选择逻辑Service是K8s里抽象网络访问的核心对象。四种类型里ClusterIP是默认的只在集群内部可达NodePort在每个节点上开一个端口LoadBalancer通常依赖云厂商的负载均衡器ExternalName是DNS别名。选择哪种类型取决于你的服务需要被谁访问访问来源推荐类型说明集群内其他PodClusterIP最简单性能最好集群外但同一网络NodePort需要管理端口范围公网访问LoadBalancer依赖云环境或MetalLB外部服务别名ExternalName仅做DNS映射我自己的模拟环境没有云负载均衡器所以用MetalLB来提供LoadBalancer能力。安装MetalLB之后创建一个LoadBalancer类型的Service就能获得一个虚拟IP。这个方案在自建集群里很实用。4.2 CoreDNS的工作机制与排查集群内的服务发现靠的是CoreDNS。每个Service创建时CoreDNS会自动添加一条DNS记录格式是service-name.namespace.svc.cluster.local。Pod里的/etc/resolv.conf会指向CoreDNS的ClusterIP。如果服务发现不工作排查步骤确认CoreDNS Pod是否运行正常在Pod里nslookup或dig测试域名解析检查Service的Endpoints是否有值kubectl get endpoints确认NetworkPolicy没有阻断DNS流量我遇到过一次Endpoints为空的情况原因是Service的selector和Pod的labels不匹配。kubectl get endpoints service-name显示none时基本就是selector问题。这个检查应该成为服务发现问题时的标准动作。4.3 NetworkPolicy默认全通按需收紧K8s默认允许所有Pod之间互相通信。这在开发环境很方便但在生产环境是安全隐患。NetworkPolicy用来定义Pod级别的防火墙规则。apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-from-other-namespaces spec: podSelector: {} policyTypes: - Ingress ingress: - from: - podSelector: {}这个策略只允许同一命名空间内的Pod互相访问拒绝其他命名空间的流量。注意podSelector: {}表示选择所有Podingress.from.podSelector: {}表示允许来自同一命名空间所有Pod的流量。注意NetworkPolicy需要CNI插件支持。Calico、Cilium、Weave Net都支持但Flannel默认不支持。如果你的集群用的是FlannelNetworkPolicy创建了也不会生效。这个坑我踩过排查了半天才发现是CNI的问题。4.4 Ingress七层流量的入口管理Ingress是七层负载均衡的抽象通常配合Ingress Controller使用。它的价值在于用一个入口暴露多个服务基于域名或路径做路由。apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress spec: rules: - host: app.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-service port: number: 8080 - path: / pathType: Prefix backend: service: name: web-service port: number: 80这个配置把app.example.com/api路由到api-service其他路径路由到web-service。Ingress Controller如Nginx Ingress会监听Ingress资源的变化动态更新自己的配置。我用的Nginx Ingress Controller安装后需要创建一个LoadBalancer类型的Service来接收外部流量。在模拟环境里配合MetalLB整个链路就通了。5. 资源管理与调度策略的实战取舍5.1 LimitRange与ResourceQuota的配合使用LimitRange作用于单个Pod或容器设置默认的requests和limits以及最大最小值。ResourceQuota作用于命名空间限制总资源消耗。apiVersion: v1 kind: LimitRange metadata: name: default-limits namespace: dev spec: limits: - default: cpu: 500m memory: 256Mi defaultRequest: cpu: 200m memory: 128Mi type: Container这个配置让dev命名空间里所有没写resources的容器自动获得默认值。配合ResourceQuota可以防止某个团队把整个集群的资源吃光。我的经验是开发环境用LimitRange给默认值降低使用门槛生产环境用ResourceQuota做硬限制防止意外。两者配合既方便又安全。5.2 水平Pod自动伸缩的触发条件HPAHorizontal Pod Autoscaler根据CPU或内存使用率自动调整副本数。但它的工作前提是Pod必须设置了requests否则HPA无法计算使用率。kubectl autoscale deployment web --cpu-percent70 --min2 --max10这个命令创建一个HPA当CPU平均使用率超过70%时扩容最低2个副本最高10个。实际使用中CPU使用率的波动可能很剧烈导致频繁扩缩容。可以设置behavior字段来控制扩缩容的速度behavior: scaleDown: stabilizationWindowSeconds: 300这个配置让缩容决策等待5分钟避免因为短暂的负载下降就立刻缩容。5.3 节点压力驱逐与Pod优先级当节点资源不足时kubelet会驱逐一些Pod来保证节点稳定。驱逐顺序取决于Pod的QoS等级BestEffort最先被驱逐Burstable其次Guaranteed最后。QoS等级由requests和limits的配置决定Guaranteed所有容器的requests等于limitsBestEffort所有容器都没有设置requests和limitsBurstable其他情况核心服务应该配置成Guaranteed确保资源不足时不会被优先驱逐。非核心服务可以设成Burstable允许一定的资源弹性。PriorityClass是另一个控制驱逐顺序的机制。高优先级的Pod可以抢占低优先级的Pod。这个功能要谨慎使用因为抢占会导致低优先级Pod被杀死。5.4 实际项目中的资源规划思路在一个模拟项目中我负责规划一个包含Web层、API层和数据库层的应用。资源规划的思路是Web层无状态requests设小limits设中等用HPA自动伸缩API层有状态但可水平扩展requests按P95设limits按P99设数据库层有状态Guaranteed QoS固定副本数不用HPA这个规划的核心逻辑是无状态服务追求弹性有状态服务追求稳定。资源请求的设置要基于实际监控数据而不是拍脑袋。6. 那些文档里不会写的踩坑记录6.1 镜像拉取失败的几种面孔Pod卡在ImagePullBackOff或ErrImagePull原因可能有很多镜像名拼写错误镜像不存在或tag错误私有仓库认证失败网络不通排查时先看Eventskubectl describe pod会显示具体的错误信息。如果是认证问题需要创建imagePullSecrets并挂载到ServiceAccount上。我遇到过一次镜像拉取失败错误信息是“manifest unknown”最后发现是tag写成了latest但仓库里没有这个tag。这个错误信息其实很明确但第一次看到时容易忽略。6.2 服务端口配置的常见错误Service的targetPort和Pod的containerPort不匹配是服务不通的常见原因。containerPort是Pod声明自己监听的端口targetPort是Service转发到的端口。如果targetPort写错了流量就到不了容器。还有一个容易混淆的是port和nodePort。port是Service的ClusterIP上暴露的端口nodePort是每个节点上暴露的端口。ClusterIP类型的Service没有nodePort。6.3 存储卷挂载的权限问题挂载存储卷时容器内的进程可能因为权限不足而无法写入。这通常是因为存储卷的属主和容器内运行的用户不匹配。解决方法有两种一是在Pod的securityContext里设置fsGroup让K8s自动修改卷的属组二是在容器启动命令里手动chown。fsGroup的方式更优雅securityContext: fsGroup: 1000这个配置让存储卷的属组变成1000容器内GID为1000的用户就有写权限了。6.4 集群DNS解析超时的排查CoreDNS解析超时通常表现为服务间调用偶尔失败。排查时先在Pod里测试DNSkubectl exec -it pod -- nslookup kubernetes.default如果解析慢或失败检查CoreDNS的日志和资源使用情况。CoreDNS的CPU requests设得太低会导致解析延迟。我一般给CoreDNS设200m CPU requests和100Mi内存requests这个配置在中小集群里足够用。还有一个隐藏问题是conntrack表满导致DNS丢包。dmesg里如果有“nf_conntrack: table full”的提示就需要调整节点的conntrack参数。6.5 滚动更新卡住的排查思路Deployment滚动更新卡住通常是因为新Pod一直不就绪。排查步骤kubectl rollout status deployment/name看更新状态kubectl get pods看新Pod的状态kubectl describe pod看新Pod的Eventskubectl logs看新Pod的日志常见原因包括就绪探针配置不当、镜像启动失败、资源不足导致调度失败。我遇到过一次就绪探针的path写错了Pod一直不就绪滚动更新就卡住了。把探针配置改对之后更新立刻完成。提示就绪探针的initialDelaySeconds要设得合理。设太短会导致Pod还没启动完就被判定为不就绪设太长会拖慢更新速度。一般根据服务启动时间来定留20%的余量。7. 从能跑到跑得稳我的学习路径复盘回头看这个系列的四篇笔记前三篇解决的是“能不能跑起来”的问题第四篇解决的是“能不能跑得稳”的问题。调度、控制器、服务发现、资源管理这四块内容构成了K8s从玩具到工具的分水岭。我自己的体会是K8s的学习曲线陡峭不是因为概念多而是因为概念之间的关联复杂。单独看Pod、Service、Deployment都能理解但把它们放在一起加上调度器、控制器、网络插件、存储插件的交互就容易迷失。我的建议是每学一个概念就动手搭一个最小可复现的环境观察它的行为然后故意制造一些故障看系统怎么反应。这种“破坏性学习”比看文档有效得多。还有一个实用技巧养成看Events的习惯。kubectl describe和kubectl get events能解决大部分日常问题。Events里的信息是K8s组件直接输出的比任何二手资料都准确。我现在的排查流程基本是先看Events再看日志最后才去翻文档。后续如果继续深入下一步应该是自定义控制器和Operator开发以及集群的监控和日志体系。但那又是另一个故事了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询