
1. 一次真实的调度事故凌晨两点的电话先说个我亲身经历的事。之前在一家互联网公司维护生产集群某个凌晨两点监控突然报警核心业务的Pod大量出现Pending状态。登上去一看三台工作节点里两台已经资源耗尽第三台虽然有空余但上面打了污点而业务Pod没有容忍结果就是调度器死活不把Pod放上去。更麻烦的是当时我们的Deployment副本数已经缩到最低Pod起不来直接导致流量开始积压。那次事故排查了将近一个小时才定位到原因本质就是我对Pod的调度机制理解停留在“会用”层面没有深入到“为什么”。后来花了不少时间把整个Pod创建到调度的链路从头到尾捋了一遍才真正有底气应对这类问题。这篇是企业数字化底座K8s实践系列的第二篇专门讲Pod的创建和调度。文章不会停留在“怎么写个YAML跑起来”这种入门教程而是沿着一次Pod从提交到运行再到调度器如何选择节点的完整路径去拆解。适合已经会部署K8s、平时在写Deployment但遇到Pod调度异常时容易慌的工程师阅读。读完这篇你能回答这几个问题一个kubectl apply之后系统内部到底发生了什么调度器的筛选和打分是怎么工作的企业里常见的多环境隔离、节点资源控制、应用亲和性到底应该用哪套机制去实现以及最关键的一类问题——Pod卡在Pending、容器反复重启、调度结果不如预期该从哪里下手排查。2. 企业数字化底座中的Pod为什么它是重点我们聊K8s聊容器但企业落地时最终执行具体业务的是Pod。如果说不理解Pod就谈不上理解K8s可能有点绝对但至少在生产环境里Pod是运维和研发之间交接的核心边界。2.1 Pod在K8s中的定位Pod是Kubernetes中最小的调度和运行单元它里面可以有一个或多个容器。企业实践里最常见的形态是一个Pod里跑一个主容器挂一两个辅助容器——比如日志采集的sidecar、网络代理的sidecar。这些容器共享同一个网络命名空间、同一个存储卷、同一个Pause容器提供的“基础设施”。但这带来一个问题既然是共享的那调度时的资源核算、健康检查、网络配置都需要以整个Pod为单位去做。你在Deployment里写了requests和limits看起来是给容器用的但实际上调度器要看的是整个Pod里所有容器的总和。这个细节很多人会忽略等Pod起不来或者被驱逐的时候才开始排查。2.2 创建到调度一次Pod生命周期的起点企业环境里的Pod绝大多数不是手动创建的而是经过Deployment、StatefulSet、DaemonSet这些控制器去管理的。我们常说的“Pod创建”其实是一套控制器驱动下的事件链API Server收到请求 → 写入Etcd → Controller Manager感知 → 创建Pod对象 → Scheduler调度 → kubelet执行。这个过程如果每一步都顺畅几秒内Pod就能Running。但一旦中间哪个环节卡住Pod就会停在某个状态不动最常见的就是Pending。要理解为什么Pending就必须把这条链路里的角色职责分清楚。我画过一张图辅助理解把创建Pod想象成公司里的一次员工入职。API Server是前台负责收材料和做初步检查Etcd是档案室把所有人的信息永久存档Controller Manager是HR根据编制数量判断要不要招人Scheduler是招聘系统里的分配算法决定这个人分到哪个部门kubelet是具体办公楼的物业负责把工位、电脑、网络都给准备好。这样类比下来当你看到Pod卡住第一步应该判断卡在哪个环节是前台没收到材料HR没审批还是分配算法的规则不匹配对应到K8s里就要去看Pod事件、看控制器状态、看调度器日志。3. Pod创建全流程拆解一次请求的完整旅程这一节把Pod创建到调度的链路拆开来看。我会尽量还原请求经过每个组件时发生的核心操作以及对应可以去哪里排查。3.1 从kubectl到API Server准入与控制kubectl apply -f deploy.yaml这个命令本质上是把YAML转换为JSON然后通过HTTPS请求发到API Server的/apis/apps/v1/namespaces/default/deployments这个路径。API Server收到请求后首先做的是认证Authentication和授权Authorization。认证确认“你是谁”K8s支持客户端证书、Bearer Token、Basic Auth等。授权确认“你能干什么”常见的是RBAC。这里企业实践里有个常见需求是“只读用户”也就是只允许查看资源不允许修改这样方便审计和协作。然后API Server会执行一个非常关键的机制——准入控制Admission Control。准入控制器可以拦截请求在资源被持久化之前进行校验或修改。生产环境里常见的准入控制器包括NamespaceLifecycle禁止在正在终止的Namespace里创建资源LimitRanger如果Namespace配了LimitRange这里的默认值会被注入ResourceQuota检查Namespace的资源配额是否够用MutatingAdmissionWebhook/ValidatingAdmissionWebhook企业经常用它来做自定义策略比如强制给所有Pod注入sidecar、禁止使用latest镜像标签准入通过后Deployment对象才会被写入Etcd。注意这个阶段还没有Pod出现只有Deployment这个期望状态被保存了。你可以用kubectl get deployment看到它但kubectl get pod还是空的。3.2 Controller Manager从Deployment到ReplicaSet再到PodController Manager里跑着几十个控制器其中Deployment Controller负责监听Deployment的变化。当它发现一个Deployment的期望副本数是3但实际对应的ReplicaSet下面Pod数量是0时就会去创建ReplicaSet。ReplicaSet Controller再去根据Pod模板创建具体的Pod对象。这个两级控制器的设计经常被忽略但它是有意的。Deployment只关心版本和副本数ReplicaSet负责管理具体某一批Pod。滚动更新时Deployment会创建新的ReplicaSet同时缩掉旧的。这些Pod对象被创建后状态是Pending并且会被写入一个关键字段——spec.nodeName是空的因为还没有节点来认领它。这个阶段如果出问题通常是Deployment配置了不存在的镜像、拉取凭证缺失、或者模板里配了非法字段。排查方法很直接kubectl get deployment name -o yaml kubectl get rs -o wide kubectl describe rs name3.3 Scheduler用预选和优选决定节点归属调度器kube-scheduler通过API Server的Watch机制监听那些spec.nodeName为空的新Pod。它的任务很明确为每个待调度的Pod找到一个最合适的节点然后通过API Server写回Binding记录。调度过程分两个阶段。第一阶段叫预选Filtering/Feasibility就是把不满足硬性条件的节点全部淘汰掉。这一阶段靠一组Predicates规则实现企业里常用的包括节点资源是否满足Pod的requests——CPU和内存都够不够节点是否有命中的污点TaintsPod是否有对应的容忍Tolerations节点选择器nodeSelector是否匹配节点亲和性nodeAffinity的requiredDuringSchedulingIgnoredDuringExecution规则是否满足Pod亲和性和反亲和性是否满足端口是否冲突宿主机的hostPort有没有被占用存储卷的可用区限制比如云盘只在某几个可用区第二阶段叫优选Scoring对通过预选的节点打分。K8s内置了多个打分策略比如NodeResourcesLeastAllocated倾向于选择资源剩余更多的节点便于资源均衡NodeResourcesBalancedAllocation倾向选择CPU和内存配比均衡的节点NodeAffinityPriority给匹配了亲和性规则的节点加分TaintTolerationPriority给能容忍污点的节点固定加分受污染程度越低的节点越优先最后调度器选分数最高的节点把Pod和节点的绑定关系通过POST写回API ServerEtcd中Pod的spec.nodeName就被赋值了。如果到这里还没有被调度的节点Pod会一直Pending。大多数调度问题根因都在预选阶段。想要快速判断是哪条规则卡住了核心命令是kubectl describe pod name | tail -20里面会直接给出类似0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had taint...的信息非常直观。3.4 kubelet执行从绑定到容器真正运行当Pod的spec.nodeName被写好后对应节点上的kubelet会通过Watch或List-Watch机制感知到这个变化开始执行Pod的真正创建。kubelet会做几件事创建PodSandbox即Pause容器为整个Pod准备好网络和IPC等命名空间调用容器运行时接口CRI拉取镜像、启动容器配置网络调用CNI插件比如Calico、Cilium给Pod分配IP打通网络策略挂载存储卷调用CSI插件执行容器启动命令启动后持续做健康检查Liveness和Readiness Probe这个阶段常见的问题就多了镜像拉不下来ImagePullBackOff、镜像存在但启动即退出CrashLoopBackOff、网络插件和Pod CIDR冲突、存储卷挂载超时等。有一个点值得特别提一下kubelet是从Etcd里通过Watch机制感知到Pod的而不是调度器直接通知kubelet。这意味着即使调度器完成了绑定如果某个节点的kubelet和API Server之间的通信出了问题这个节点上的Pod也不会被创建看起来就会一直Pending。整个创建链路到这里就通了。回顾一下这张“流程图”kubectl → API Server → Etcd → Controller Manager → 创建ReplicaSet → 创建Pod → Scheduler → 预选 → 优选 → 写回Binding → kubelet → Sandbox → 容器 → CNI → CSI → Running你会发现整个系统是完全解耦的事件驱动架构每个组件只关注自己负责的那段通过Etcd的Watch机制互相联动。理解了这个后续排查问题时才能快速定位“卡在哪一环”。4. 调度策略实践企业环境最常见的四类需求调度器的默认行为是“资源均衡”就是把Pod尽量散到不同节点上。但在真实企业环境里运维团队通常会有更明确的诉求默认的均衡策略未必够用。这一节讲四个最常见的场景以及对应应该用哪套机制。4.1 固定机型与区域nodeSelector和节点亲和性有些业务有硬性的资源要求。比如AI训练任务需要有GPU卡大数据任务需要大内存机型关键业务希望只跑在专门的高性能节点上。这时候nodeSelector是最简单的方案。先给节点打标签然后在Pod模板里指定。kubectl label node node-01 disktypessdspec: nodeSelector: disktype: ssdnodeSelector的问题是只能做等值匹配不支持更复杂的规则。如果你的需求是“只要不是arm架构的节点就行”或者“优先选择有SSD的节点但没有也可以”那就得用节点亲和性。spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: zone operator: In values: - east-1注意这里有两个时间维度的词requiredDuringScheduling表示调度时必须满足preferredDuringScheduling表示调度时优先考虑。企业实践里建议用软性规则做首选策略因为硬性规则如果写错会让Pod直接Pending。4.2 隔离与共置Pod亲和性和反亲和性有时候业务之间希望靠得近有时候希望离得远。Pod亲和性podAffinity负责把相关的Pod调度到同一拓扑域Pod反亲和性podAntiAffinity负责分散。一个很常见的场景是网关服务和它依赖的后端服务希望放在同一节点上减少网络延迟而同一个服务的多个副本希望尽量分散到不同节点避免单点故障。spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: nginx topologyKey: kubernetes.io/hostname这里的topologyKey是关键它决定“同一拓扑域”是什么范围。kubernetes.io/hostname表示同一个节点topology.kubernetes.io/zone表示同一个可用区。生产环境里跨可用区的高可用部署经常用zone级别的反亲和。需要提醒的是Pod反亲和性是有调度成本的。如果你设置了requiredDuringSchedulingIgnoredDuringExecution的强反亲和并且Pod副本数大于节点数那必然会有Pod调度不上只能Pending等待。所以线上建议优先用preferred软性规则。4.3 污点和容忍隔离与避让的组合拳Taints和Tolerations是K8s里非常强大但也最容易被用错的机制。它的逻辑是节点可以打上污点Pod声明容忍对应的污点后才能被调度到该节点。常用的操作# 给节点打污点NoSchedule表示不调度新Pod kubectl taint nodes node-02 gputrue:NoSchedule # 移除污点 kubectl taint nodes node-02 gputrue:NoSchedule-Pod侧声明容忍spec: tolerations: - key: gpu operator: Exists effect: NoSchedule这里有三个效果值需要理解NoSchedule不调度新的Pod但不强制驱逐已在运行的PodPreferNoSchedule尽量不调度NoExecute不仅不调度还会驱逐节点上不满足容忍条件的现有Pod企业实践中我常用的组合是在GPU节点、园区专线节点打上NoSchedule污点然后只允许有对应容忍的Pod调度上去。但要注意污点不代表“完全不让某些Pod上来”只代表“没有声明容忍的Pod不能上来”。K8s里有个系统级的Toleration比如node-role.kubernetes.io/control-plane:NoSchedule所以控制面节点天然不会被普通业务Pod占用。4.4 资源、优先级和拓扑分布把集群“玩明白”除了节点和Pod之间的亲和性控制企业环境里还有三类机制必须掌握。资源请求与限制Requests Limits。调度器的资源判断只参考requests不会参考limits。这意味着如果你把所有Pod的requests都写得很小、limits写得很大那调度器会觉得节点很空闲结果运行时内存打爆触发内核OOM或者容器被驱逐。正确的做法是requests要按实际压测后的稳态用量来设置limits可以适当放宽但别离谱。优先级PriorityClass。当集群资源不足时低优先级的Pod会被驱逐或被高优先级Pod抢占。这个机制在混合部署离线任务和在线任务时特别有用。apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: 核心业务高优先级拓扑分布约束topologySpreadConstraints。这个属于比较新的能力用来控制Pod在拓扑域间的均匀分布。以前我们要实现多可用区均衡通常是写反亲和现在更推荐直接用topologySpreadConstraints。spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: nginxmaxSkew: 1表示各个可用区的Pod数量差最多不超过1DoNotSchedule表示无法满足就不调度。这套机制能实现比单纯的反亲和更精细的均衡控制。5. 故障排查实战从Pod到集群的调度问题处理最后这一节是重头戏。前面讲了原理这里讲怎么用。我整理了实践中最高频的调度和创建问题、根因、排查命令每个都是踩过坑才总结出来的。5.1 五分钟定位问题必查日志和事件排查任何Pod问题第一步永远是看事件第二步看容器状态第三步才去看日志和抓包。kubectl get events --sort-by.lastTimestamp kubectl describe pod pod-name -n namespace kubectl logs pod-name -n namespace --previousPod状态基本能告诉我们根因方向Pod状态常见根因优先排查位置Pending没有节点满足调度条件describe pod尾部事件、节点资源ContainerCreating镜像拉取失败、存储卷挂载超时、CNI异常节点kubelet日志、docker pull手动验证CrashLoopBackOff应用启动即退出、探针失败、配置错误logs --previous查看上一实例日志ImagePullBackOff镜像不存在、仓库鉴权失败、网络不通describe里会给出具体的ErrImagePull原因Completed/Running但不健康启动命令错误、探针配置过严排查探针配置、启动日志遇到调度失败记得看Scheduler日志这步很多新手会忽略。Scheduler是独立Pod日志里会直接记录哪些Pod经过预选和优选后的结果。kubectl logs -n kube-system kube-scheduler-node-name --tail2005.2 高频问题速查表下面这些问题是群里、社区里被反复问到的我统一整理出来。问题1Pod一直Pendingdescribe显示0/3 nodes available最常见的是节点资源不足。这里有个坑调度器关注的是requests不是节点实际使用量。如果你在节点上跑了些不通过K8s管理的容器比如docker直接启动的进程这部分资源占用K8s是不感知的但实际内存可能已经快满了。排查时用kubectl describe node看Allocated resources同时用free -m看真实内存两边对比才能真正定位。问题2镜像拉取总是超时企业内网如果没有配置镜像代理从Docker Hub拉镜像会非常难受。建议在节点上配置好/etc/containerd/config.toml的sandbox_image和registry mirrors。另外很多镜像仓库有访问限流如果大规模滚动更新把并发拉取开得太大会触发限流导致ImagePullBackOff。解决方案是控制发布并发数。问题3Pod被调度过去了但一直ContainerCreating这种通常是存储卷或者网络的问题。最有效的排查手段是直接登录节点用crictl ps -a看容器状态用journalctl -u kubelet -f看kubelet日志。CNI相关的错误会在kubelet日志里体现比如failed to setup network for pod。如果是Calico还需要检查calico-node的日志。问题4多个Pod都调到了同一台节点明明其他节点更空原因可能是那个节点只有一个匹配某条亲和性规则的Pod或者你的Pod设置了podAffinity它想和某个已有的Pod共置又或者节点上打了特殊的标签正好匹配了nodeSelector。用kubectl get nodes --show-labels和kubectl describe pod结合看一下。问题5Deployment滚动更新时新Pod起不来滚动更新时会先创建新Pod等Ready之后才销毁旧Pod。如果新Pod一直Pending旧的Pod又占用着资源这就可能形成了死锁。解决办法用kubectl rollout status看进度如果是资源问题临时增加节点或调整maxUnavailable参数。5.3 一个真实的调度排查案例之前我处理过一个典型的“Pod打散失败”案例。一个三副本的微服务配置了requiredDuringScheduling的Pod反亲和要求不同副本在不同节点上。但集群只有两台工作节点结果第三个Pod一直Pending。刚开始同事以为是调度器坏了查了半天一切正常。其实原因很简单强反亲和在副本数大于节点数时是无解的。后来改成preferred软反亲第三个Pod就被调度到已有的节点上了。但是业务方坚持要三个副本完全隔离最后方案是加了一台工作节点。这个案例想说明两件事第一调度配置一定要和集群容量匹配第二软规则和硬规则的取舍直接决定了系统的弹性。生产环境能用软规则的地方尽量不要用硬规则。5.4 从Pod到集群故障排查的思维模型最后分享一个排查思维模型是我自己总结的“三层定位法”第一层看Pod自身。事件、状态、日志80%的问题在这一层就能解决。第二层看节点。如果Pod没问题但起不来登录节点看kubelet、容器运行时、CNI的日志。节点上的内核日志dmesg也值得关注OOM、网络栈异常都会在这里留下痕迹。第三层看集群层。所有Pod都异常或大面积Pending那可能是配额不足、API Server性能问题、Etcd和kubelet失联、控制面组件异常。这时候优先检查系统组件的健康状态。按照这个顺序排查基本不会被复杂问题绕晕。6. 关于Pod调度的几点内部经验写到这里再分享几个我在实际项目里沉淀下来的习惯不一定写在哪本手册里但对维护企业级K8s集群很有帮助。资源请求一定要压测后填真实值。很多人写requests和limits靠拍脑袋CPU写500m、内存写1Gi结果线上业务一忙就触发限流和OOMKilled。压测工具压出来的稳态数据才是合理的requests基线limits预留30%左右的buffer。给关键业务提前设计污点和亲和性。不要等出了问题再去想要不要隔离。核心数据库、消息队列、在线交易这类业务提前规划专属节点池节点打上污点业务Pod声明容忍避免被其他业务抢占。PodTopologySpreadConstraints值得投入时间研究。如果你规划多可用区部署这个能力比手写反亲和更优雅控制粒度也更细。不过要注意maxSkew设置太小会导致可用区数量少时调度失败建议配合whenUnsatisfiable: ScheduleAnyway的兜底方案。监控一定要覆盖到调度链路。我在监控里会重点盯三个指标Pending Pod数量、调度失败次数调度器QPS里的error数、以及节点上的Allocatable和Requests的差值。一旦Requests接近Allocatable意味着节点资源非常紧张即使有limits也可能出问题。最后说一个小技巧。排查调度问题时很多人喜欢直接看kubectl describe但有些深层问题需要看调度器的历史事件。K8s 1.27之后的版本可以开调度器追踪Tracing把调度链路串起来看对分析复杂调度问题很有帮助。如果集群版本比较老先用kubectl get events --all-namespaces --field-selector reasonFailedScheduling把失败事件都捞出来也能高效定位。Pod创建和调度这个话题说到底是理解K8s这套控制器的协作逻辑。理解的深度直接决定你在生产环境里的反应速度。后续系列文章我会继续讲企业里更复杂的部署形态、存储和网络方案的选型以及监控告警体系的搭建这些都是数字化底座里绕不开的部分。