尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

K8s如何调度容器?从超级调度员视角拆解Kubernetes核心机制

发布时间:2026/9/29 18:25:01

资讯中心
01
ARTICLE

K8s如何调度容器?从超级调度员视角拆解Kubernetes核心机制

K8s如何调度容器?从超级调度员视角拆解Kubernetes核心机制
算力中心这个词听起来很硬核但说白了就是一堆服务器堆在一起给业务跑容器的地方。而Kubernetes简称K8s在这堆服务器里扮演的角色特别像一个“超级调度员”——它不生产算力也不算业务逻辑它只负责一件事让每一个容器都精准地落到最该落的那台机器上并且保证它一直活着、随时能用。很多朋友学K8s最大的障碍是被那些抽象名词劝退Pod、Node、Deployment、Service、etcd……每个词单看都能解释合在一起就不知道谁在指挥谁。我自己从kubeadm init到把业务真正跑上生产环境中间也经历过很长一段“会敲命令但不懂原理”的阶段。这篇文章不追新版本特性也不铺概念全集就顺着“超级调度员”这个视角把K8s的调度机制、核心组件、一次Pod从提交到运行的完整生命周期讲透。适合刚入门想建立整体认知的朋友也适合已经会部署但说不清调度逻辑的运维同学。1. 算力中心的调度困境为什么非要有K8s1.1 没有调度员的日子手工部署的三大痛点在没有K8s之前我们部署一个容器化应用是什么状态服务器上装了Docker把镜像拉下来docker run一下端口映射好业务就跑起来了。单台机器、两三个容器的时候这套流程完全够用。可真到了算力中心的规模——几十台节点、上千个容器、随时要扩容缩容——问题就全冒出来了。第一个痛点是端口和资源冲突。你在一台机器上起了A容器占住8080另一拨人又来部署B服务也想用8080谁后到谁扑街。哪怕你们约定好端口段总有人不按规矩来。资源也一样CPU和内存是有限的容器之间的资源争抢经常把宿主机打满。第二个痛点是故障恢复靠人肉。某个容器crash了监控告警响了运维半夜爬起来重启。一台机器宕机了上面所有容器全部哑火你要手动把服务迁到别的机器上运气好半小时运气差要折腾一夜。这种“人肉调度”在故障面前永远慢半拍。第三个痛点是发布和扩容的不可控。流量上来了想加两个副本你需要在几台机器上各跑一遍docker run还要保证新实例的参数和旧的一致。灰度发布时想把旧版本逐个替换成新版本纯靠手工一个个停、起、等根本没法标准化。这就是K8s出现的根本原因把“容器该跑在哪台机器上、怎么跑、挂了怎么处理”这件事从人的经验里抽出来变成一套有状态、有逻辑、能自动决策的调度系统。1.2 “超级调度员”到底在调度什么很多人对K8s的理解停留在“容器编排工具”这个说法不够准确。K8s调度的核心单位不是容器而是Pod。Pod是K8s里最小的调度单元里面可以有一个或多个容器。这些容器共享网络命名空间、共享存储卷就像一个房间里住着的几个室友公用厨房和洗手间必须待在同一个节点上。调度员的工作就是给每一个Pod找一个最合适的Node工作节点。这个“合适”不是随机选的而是经过一套复杂的决策流程Node的CPU和内存够不够Pod要求的端口会不会冲突有没有匹配的标签有没有特殊的亲和性要求在满足硬性条件的前提下还要尽量做到负载均衡、资源利用率最高。我习惯把K8s的调度比作高峰期出租车派单逻辑。乘客Pod要打车平台调度器先筛掉距离太远、司机正在休息、车辆限行的选项这些是“硬性过滤”再从剩下可选车辆里选一个综合评价最高的比如里程最短、路况最好、司机评分高这些是“打分排序”。最终乘客上车订单锁定。理解了这个比喻K8s的很多设计就顺理成章了为什么Pod要绑定在Node上因为车已经接单了不能同时接下一单。为什么有亲和性和反亲和性因为有时候你希望同一类Pod聚集在一起减少网络延迟有时候又希望它们分散开避免单点故障。为什么有污点和容忍因为有的Node是专用节点等闲Pod不让停。2. 拆开“超级调度员”的调度中枢控制面与工作节点2.1 控制面四件套前台、记事本、调度经理与监工K8s集群从物理角色上分两大部分控制面Control Plane和工作节点Worker Node。控制面就是调度员的大脑它由几个组件组成各自分工明确。第一个是kube-apiserver它是整个集群的前台客服。所有组件、kubectl命令、大厂的各种二次开发系统要操作集群都必须经过它。它负责接收请求、鉴权、把数据写入存储、并把变更实时推送给关心这些数据的组件。你可以不看别的组件但一定要知道所有交互都走apiserver它是集群的入口也是唯一入口。第二个是etcd它是集群的记事本。所有配置信息、期望状态、实际状态都存储在etcd里。它是一个独立的分布式键值数据库保存着“集群想要变成什么样”以及“当前是什么样”。K8s的持久性和一致性全靠etcd兜底。第三个是kube-scheduler这才是真正的调度经理。它负责监听新创建的Pod实际上是通过apiserver的事件机制然后执行刚才说的过滤、打分、绑定流程。当你执行kubectl apply提交一个Deployment后真正决定Pod去哪台Node的就是它。第四个是kube-controller-manager它是监工纠察队。集群里有很多个Controller比如Deployment Controller盯着你想要3个副本现在只有2个它就会去创建第3个Node Controller发现某台Node失联它会去清理上面的Pod。体现在期望状态和实际状态的差距然后拼命抹平差距这就是控制器的核心哲学。我一直觉得理解控制面这四个组件的关系是搞懂K8s的门槛。你可以这样记前台收单apiserver、本子记账etcd、调度经理派单scheduler、监工巡场纠偏controller-manager。2.2 工作节点三剑客管家、网络警察与容器引擎工作节点是真正跑业务的地方每个节点上一套标配三个关键组件。第一个是kubelet这是节点上的管家。它直接与容器运行时打交道负责启动Pod、监控Pod健康状态、向apiserver汇报。调度经理把Pod派给某台节点后就是kubelet来“接单”并执行的。它还会定期向控制面报告心跳和资源使用情况让调度器有据可依。第二个是kube-proxy它是节点上的网络警察。它负责维护节点的网络规则实现Service服务的负载均衡和流量转发。简单说当你访问一个Service的ClusterIP时怎么把流量转发到后端的某个Pod上就是kube-proxy干的活。它支持iptables和IPVS两种模式生产环境我推荐IPVS规则多了之后性能更稳定。第三个是容器运行时Container Runtime比如containerd、CRI-O老一点的环境还有Docker。它就是实际拉取镜像、创建容器、启停进程的引擎。K8s通过CRIContainer Runtime Interface接口和运行时通信好处是运行时解耦你想换引擎不影响上层。控制面和节点之间的协作方式是“声明式状态同步”用户声明想要的最终状态比如3个副本、端口8080、环境变量xxyyK8s就持续驱赶当前状态向期望状态靠拢而不是“命令式地”一步步执行任务。这也是后来排障时很重要的一个思维转换——遇到K8s的问题不要问“谁执行了这条命令”要问“期望状态是什么、当前差在哪”。3. 跟踪一个Pod的完整一生从YAML到运行起来3.1 提交、入库与监听的流转闭环光讲概念太虚我们直接跟踪一个Pod从提交到运行的完整流程。假设你写了这样一个最简单的Deployment YAMLapiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 200m memory: 256Mi你执行kubectl apply -f nginx.yaml之后这条命令发往apiserver。apiserver先做鉴权确认你有权限操作Deployment资源然后把这份期望状态写入etcd。这一步完成后apiserver会向集群里所有监听者广播“有新的Deployment被创建了”。Deployment Controller收到这个事件发现期望副本数3、当前副本数0于是创建3个Pod对象。这3个Pod对象同样会写入etcd并且触发新的事件。这时候真正的“调度经理”kube-scheduler就动起来了——它专门监听那些nodeName字段为空也就是还没被派发的Pod。调度完成后scheduler会把“这个Pod应该跑在node-a上”的信息写回etcd更新Pod的nodeName字段。这一更新node-a上的kubelet立刻就能感知到因为kubelet也在持续上报请求盯着有没有指派给自己的Pod。从用户操作到调度完成整个流转链路是kubectl → apiserver → etcd → scheduler → apiserver → etcd → kubelet。注意scheduler不直接和kubelet通信中间永远有apiserver在传话。这也是K8s设计的一个核心原则组件之间不做点对点直连全部通过apiserver中转。好处是架构清晰、易于扩展坏处是apiserver容易成为性能瓶颈生产环境一般要做高可用而且etcd的访问压力要留意。3.2 绑定之后kubelet的“接单”与启动流程kubelet发现有Pod派给自己之后它并不直接启动容器还要经历一个准备阶段先检查Pod所需的存储卷、网络、镜像等资源是否就绪。比如Pod声明了emptyDir或hostPathkubelet会先创建好对应目录如果要挂ConfigMap或Secret也要提前准备好。然后是拉取镜像。kubelet通过CRI请求容器运行时比如containerd去拉取nginx:1.25。这里有个常见的坑如果Pod设置了imagePullPolicy: IfNotPresent且节点上已有同名镜像它就不会重新拉取对于发版频繁的业务建议明确设置imagePullPolicy: Always或者用带唯一tag的镜像地址比如registry.internal.demo/nginx:build-20240115否则很容易在灰度验证时拉出旧镜像。镜像拉取完成后容器运行时会创建Pod沙箱pause容器再启动业务容器。pause容器里“什么正事都不干”但整个Pod的网络命名空间和Linux共享命名空间都由它持有。之后kubelet会定期通过容器运行时的RPC接口比如exec探针、stats统计检查容器的健康状态。Pod真正运行起来后kubelet还会把当前状态上报给apiserver更新Pod对象的status字段。如果你在这时候执行kubectl get pods -o wide看到Running状态说明这一步已经走完了。我建议每个运维同学都动手做一次这个实验找一个小的Deployment盯着kubectl describe pod的输出你会清晰看到“Scheduled”、“Pulled”、“Created”、“Started”这几个事件的先后顺序。这个实验做一遍比背十遍原理都管用。4. 调度器在“想”什么过滤、打分与绑定4.1 过滤阶段先把不合格的节点踢出去你看懂了调度流程就该问一句灵魂问题调度器挑选Node时到底依据什么K8s的调度器把决策分成**过滤Filtering和打分Scoring**两个阶段。过滤阶段是硬性门槛不满足直接淘汰。我整理过几个最容易触发的过滤条件过滤条件实际含义常见出错场景资源充足性节点的可分配资源必须满足Pod的requests节点内存碎片化单看总量够用但已分配requested太多端口冲突节点上已存在的Pod占用了声明端口用hostPort时最容易撞车节点选择器节点必须匹配nodeSelector的标签label打错字符调度永远Pending节点亲和性硬性亲和性必须满足标签、操作符写错Pod卡在Pending污点与容忍节点有污点Pod必须带对应容忍把NoSchedule污点理解反了导致Pod调不上来Volume拓扑挂载的存储卷限制在某些可用区云盘只在某个可用区Pod却被调度到别的区域我给你举个资源过滤的例子。假设节点总内存8Gi系统预留1Gi已运行Pod声明了requests内存总和5Gi那么这个节点可分配内存还剩2Gi。如果新Pod的requests内存是3Gi调度器直接把这个Node从候选名单里剔除。注意这里看的是requests不是limits因为requests是调度时用来做资源预留判断的limits只在运行时做硬性约束。很多人配置资源时只写limits不写requests这是一个很危险的习惯会导致调度器以为这个Pod不占资源大量堆到同一台机器上运行后却因为limits超卖把节点打爆。4.2 打分阶段优中选优的资源博弈过滤之后剩下的Node都满足硬性条件接下来进入打分阶段。调度器会为每个候选节点算一个综合分分数越高越优先。最经典的默认打分之一是leastRequestedPriority的理念尽可能往资源剩余量多、统一负载更低的节点上放让集群整体负载趋于均衡。还有几个影响打分的因素nodeAffinity的软性匹配、Pod的preferred调度策略、镜像是否已经存在于节点本地镜像已经在本地会加权重避免重复拉取延迟、Pod之间是否存在反亲和需要分散放置。这里有个容易被忽略的点调度器打分是基于“当前状态”而不是“未来状态”而且多个Pod是逐个调度的。你一次性创建10个Pod调度器在调度第1个时看到的是当前集群状态调度完第1个后第2个调度时节点剩余资源已经变了。所以当你发现10个Pod没有均匀分布在各节点时可能是资源拓扑、Marvin网络插件跨节点通信等其他因素影响不一定就是调度器“不均衡”。我做过一次实验集群里5台规格完全相同的节点提交了5个requests完全相同的Pod结果并没有每个节点分到一个而是出现了一个节点分到2个、另一个节点空闲的情况。排查下来是因为Pod之间存在某个镜像拉取状态差异以及打分时的软亲和策略。K8s的调度器从来不做“平均主义”它只按算法给出当时最优解。想要控制分布只能用反亲和性或者拓扑分布约束topologySpreadConstraints。4.3 动手写一个nodeSelector与亲和性配置光说不练假把式。在实际业务中最常用的调度控制手段就是nodeSelector和亲和性。nodeSelector的使用非常简单。先给节点打标签kubectl label node node-a diskssd kubectl label node node-b diskhdd然后在Pod模板里加spec: nodeSelector: disk: ssd这个Pod就一定会被调度到带diskssd标签的节点上。nodeSelector只支持等值匹配适合简单场景。如果你需要更复杂的逻辑比如“优先往某个区放但放不下时可以放到另一个区”那就得用nodeAffinityspec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - zone-a preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: disk operator: In values: - ssd这里的requiredDuringSchedulingIgnoredDuringExecution是硬性条件preferredDuringSchedulingIgnoredDuringExecution是软性条件——它会折算成分数但不能强留Pod。注意这两个字段的名字都带IgnoredDuringExecution意思是Pod运行期间即使节点标签变了也不会把已经运行的Pod赶走只在调度阶段生效。想改变这个行为就得用节点亲和性的新阶段或者配合descheduler那个是另一个话题了。同理Pod之间的亲和/反亲和用podAffinity和podAntiAffinity来控制。比如希望两个服务调度在同一台节点减少网络开销或者希望将不同副本的Pod尽可能分散到不同节点保证高可用。生产环境最常见的需求就是把同一Deployment的两个Replica分开到不同节点避免一台机器挂了业务全没。这个需求用反亲和性一步到位。5. 调度员的高阶操作自愈、弹性与升级5.1 控制循环为什么Pod挂了会自动拉起既然K8s叫“超级调度员”它就不能只管“把Pod放上去”就撒手不管。它还得确保Pod活着。这背后是控制器Controller模式一级哲学期望状态和实际状态的不断对齐。K8s的Deployment、StatefulSet、DaemonSet这些高级负载对象本质都是Controller只不过它们的控制逻辑不同。以Deployment为例控制器在启动后会用标签选择器筛选出属于它的Pod实时统计数量。当某个Pod因为节点故障、资源耗尽、OOM Kill而消失时控制器的定时同步循环大约每10秒左右轮询一次具体取决于--sync-period参数就会检测到当前副本数少于期望值触发创建新Pod的流程。这个新Pod又会走一遍调度流程被放到合适的节点上。这就是K8s“自愈”的最底层逻辑——并不是它奇迹般地会把坏的容器修复好而是它能用一个新的Pod替换掉死掉的那个让总量永远贴合期望值。我早期排障时犯过的一个错误业务反馈Pod挂在某个节点一直重启我第一反应是上去敲docker logs看业务日志。实际上在K8s环境里正确的排查姿势是先看Pod的restartPolicy策略默认是Always再看事件kubectl describe pod再看上一次容器的退出码kubectl logs --previous。重建的概念和修复的概念不要混在一起——K8s更擅长“重生”而不是“治疗”。5.2 HPA与节点伸缩应对算力洪峰算力中心最核心的一个诉求是弹性伸缩。这里要区分两个层面的伸缩Pod副本数伸缩和节点数伸缩。Pod副本数伸缩最常用的是HPAHorizontalPodAutoscaler。它的工作逻辑是控制器定期采集Pod的平均CPU、内存使用率或自定义指标比如QPS、队列长度超出设定的目标值就扩容副本数低于目标值就缩容。一个典型的HPA配置如下apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-demo minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这里的averageUtilization: 60表示当Pod的平均CPU使用率达到60%时触发扩容。但要注意HPA的扩缩容是有时间窗口的默认扩容冷却--horizontal-pod-autoscaler-downscale-stabilization-window是5分钟防止指标抖动导致频繁扩缩容。节点级伸缩云环境下一般配合cluster-autoscaler来做节点资源不足Pod调度不上去时自动创建新的云主机加入集群节点空闲时间过长自动回收。这部分设计跟云厂商的API高度相关不同环境下差异大但底层逻辑一定是“看有没有Pending的Pod”。**这里有一个常见的业务误解**很多团队以为上了HPA就能保证“秒级扩容”实际上HPA的默认指标采集间隔是15秒控制器判断又是另一个周期再加上扩容后应用启动时间、镜像拉取时间整体感知往往是分钟级别的。真正要求秒级弹性的场景还得靠提前预留资源或者用Serverless容器技术。5.3 滚动更新与回滚不中断业务换版本业务发布是K8s另一个高频操作。执行kubectl set image deployment/nginx-demo nginxnginx:1.26这条命令触发的就是Deployment的滚动更新。Deployment Controller收到后会创建一个新的ReplicaSet不同版本的Pod集合然后按照“先增加新版本副本再减少旧版本副本”的策略平滑切换。默认配置下它会先创建1个新Pod等它Ready之后删除1个旧Pod再创建1个新Pod……如此反复直到所有旧Pod被替换。关键参数有maxSurge滚动中额外允许超出期望值的Pod数和maxUnavailable滚动中允许不可用的Pod数。对于发布来说最核心的还有一件事存活探针livenessProbe和就绪探针readinessProbe一定要配好。你告诉Deployment“新Pod已经就绪可以接流量”靠的是readinessProbe通过了。如果没配这个探针新Pod还没启动完成就被认为Ready了流量打进来全挂滚动的策略就会非常混乱。这是我在生产环境见过最多的事故根源——没有之一。如果新版本有问题回滚也一样简单kubectl rollout undo deployment/nginx-demo它会根据Deployment维护的历史版本记录恢复到上一个ReplicaSet的状态。为了保证回滚可用不要在生产环境随意清理revisionHistoryLimit默认保留10个历史版本是很有必要的。6. 从零搭一个集群kubeadm初始化实战与踩坑记录6.1 初始化前的pre-flight检查讲完原理顺手分享一段真实部署经验。这里以kubeadm init初始化一个K8s v1.26.0集群为例展示初始化过程的关键输出和注意事项。kubeadm初始化时的日志很有意思开头就是[init] Using Kubernetes version: v1.26.0 [preflight] Running pre-flight checkspreflight是正式干活前的一道“安全检查流水线”它会逐项检查主机是否满足条件是不是有剩余磁盘、内存是否达标、端口是否被占用、网络是否通、容器运行时是否装好等等。任何一个检查项失败初始化都会停下来不会带病上路。以v1.26.0为例最常见的一个坑是cgroup driver不一致。如果容器运行时的cgroup driver是systemd而kubelet配置用的是cgroupfskubelet和containerd之间会出现资源统计口径不一致初始化时就会报错运行后也会出现各种诡异问题。我的建议是当前主流系统都在用systemd作为init进程所有组件的cgroup driver统一配置为systemd一笔都不用纠结。另一个高频坑是swap未关闭。早期K8s版本默认要求关闭swap否则kubelet无法启动。v1.26.0沿用这个策略初始化preflight会明确返回失败信息[ERROR Swap]: running with swap on is not supported. Please disable swap当时的我就踩过这个坑后来在搭建新环境时系统装完第一步就执行swapoff -a sed -i /swap/s/^/#/ /etc/fstab顺便再清一遍/etc/fstab里的swap挂载不然重启后又回来了。6.2 我踩过的坑与应对初始化成功只是开始后面还有更多坑等着你。我把自己实际踩过的几个典型问题列一下供大家参考。一是节点NotReady。这个问题十次有八次出在容器网络插件CNI没有装好。理论上K8s集群装完后节点状态是NotReady直到绕过了某个网络插件的配置才有好转。我用的最多的是Calicokubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26/manifests/calico.yaml注意检查你选的版本和Kubernetes的兼容性。装完之后查看一下kubectl get pods -n kube-system kubectl get nodes如果还是NotReady用journalctl -u kubelet -f看kubelet日志绝大多数是CNI的二进制或配置文件没生成全。二是kube-proxy模式选择。v1.26.0默认还是iptables模式Pod规模超过几百个后ipset规则会变得非常多节点上的转发延迟会明显上升。把模式切换成IPVS可以在大规模场景下获得更好的转发性能和规则维护效率。改法是在kube-proxy的ConfigMap里把mode改为ipvs然后滚动更新kube-proxy的DaemonSet。三是Pod调度不上去、一直Pending。我遇到过最气人的情况是集群明明有资源Pod就是Pending。排查顺序是kubectl describe pod xxx kubectl get events --sort-by.metadata.creationTimestamp大概率问题不出在调度器而是镜像拉取失败、PVC无法挂载、或者节点有污点而没有对应容忍。记住这个顺序不然后面排查会绕很多弯。四是NodePort端口范围冲突。默认NodePort端口范围是30000-32767如果业务需要更多端口段或者想用低于30000的端口要在apiserver上改--service-node-port-range参数。这属于初始化前就要想清楚的事后期改起来要动控制面配置稍微麻烦一些。6.3 版本选择的经验最后聊一下版本策略。我在生产环境选择K8s版本倾向于选当前稳定分支的次新小版本比如v1.26.x系列里选v1.26.0之后的补丁版本。因为.0版本往往带一些没那么严重但也不算少的bug等社区发一个或两个补丁版本后采坑成本最低。关于升级也别跳太多版本。社区支持从一个次版本升到下一个次版本比如v1.25升v1.26v1.26升v1.27尽量不要从v1.24直接跨到v1.26中间步骤更容易碰上API版本移除、配置字段失效的问题。升级之前记得先备份etcd这句话不是随便说说的。Kubernetes的升级过程本质上是控制面组件的替换和数据兼容性转换etcd里存着整个集群的命根子一旦升级过程中被误操作或网络震荡打断没有备份裸跑的恢复成本那是相当心痛。部署团队如果还在观望要不要上K8s我的建议是先拿一套不重要的中间件迁进去跑一个月盯住集群的可观测性、告警链路是否完善再看要不要扩展核心业务。别一上来就把所有服务一股脑塞进去K8s能帮你管好工作负载但前提是你已经建立了监控、日志、诊断这套配套体系的“行规”不然集群对你来说就是一个庞大又沉默的黑洞。我自己的体会是K8s这套调度系统真正值的不是“能跑多少个容器”而是把运维从“救火队员”变成“规则制定者”。你不需要再关心每台机器上跑着什么你只需要定义好期望状态剩下的交给调度员。这份从容才是算力中心最贵的东西。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。