聊个实际场景:你在集群里提交了一个 Pod,状态卡在 Pending 怎么等都不动,kubectl get pods 看了半天只看到一句 0/1 nodes are available。这时候你最想知道的就是调度器到底怎么想的——Pod 调度日志就是那个“怎么想的”的直接证据。
Kubernetes 调度其实是一个很容易被当成黑盒的环节。上层应用写好了镜像、配好了资源请求,提交给 API Server 之后,Pod 去哪台节点跑、为什么不去更空闲的节点、为什么明明有资源却一直 Pending,这些问题的答案都不在 yaml 里,而在 kube-scheduler 的日志里。这篇内容我结合自己排查问题的实践经验,把 Pod 节点调度日志的查看方式、核心字段含义、以及怎么用日志定位调度失败原因完整拆开讲透。适合刚接触 K8s 的运维开发,也适合被调度问题困扰许久的老手——很多细节你看一眼就能对上号。
1. 调度日志到底记录了什么
1.1 先理清调度日志和 Events 的区别
很多人排障的第一步是 kubectl describe pod xxx,看到最后 Events 列表里写着 FailedScheduling 和一句简短提示。这句话来源确实也是 kube-scheduler,但它只是调度器对外输出的事件摘要,不是完整的调度日志。
调度日志是 kube-scheduler 进程本身输出到标准输出或日志文件的内容,里面包含完整的调度流程:Pod 什么时候进入调度队列、Filter 阶段哪些节点被过滤掉、Score 阶段哪些节点拿了多少分、最终选哪个节点、绑定是否成功。如果你想真正理解“为什么是这台机器”或者“为什么哪台都不行”,必须去看调度器日志,不能只看 Events。Events 是被精简过的结论,日志才是完整的推理过程。
我见过不少同事把两者混为一谈,排查半天抓不到重点。这里你可以简单理解:Events 是调度器对外说的“一句话报告”,日志是调度器内部的“完整工作记录”。事件丢了可以再查日志,但日志被系统清理了,事件又太简略,那就真的只能靠猜了。
1.2 调度器日志去哪看
默认情况下 kube-scheduler 以静态 Pod 的方式运行在 kube-system 命名空间里。所以最直接的查看方式是:
bash复制kubectl get pods -n kube-system | grep scheduler
# 找到对应的调度器 Pod 后,直接查看日志
kubectl logs -n kube-system kube-scheduler-master-01
如果你用的是 kubeadm 部署的集群,调度器 Pod 的名字一般是 kube-scheduler-<节点名>,比如 kube-scheduler-control-plane-01。如果是二进制方式部署,那就要看 systemd 服务或者你手动启动时的输出文件,位置通常在 /var/log/kubernetes/scheduler.log 这类路径。
部分云厂商托管的集群不会把控制平面组件暴露给你,此时你需要看托管的控制平面侧日志,或者干脆使用集群的审计日志功能。但大多数自建集群和部分托管集群都能直接灌进日志查看来。
还有一类特殊场景:如果你的集群里部署了多个调度器(比如自定义调度器、或者高可用多副本的 scheduler),那么需要注意 Pod 用了哪个调度器。默认 Pod 用的调度器名是 default-scheduler,如果你通过 schedulerName 字段指定了别的调度器,那就要去那个调度器 Pod 里查日志。
1.3 日志级别怎么选
这里有一个非常重要的实操经验:Kubernetes 各组件使用 -v 参数控制日志详细程度,调度器默认的日志级别通常是 --v=2。这个级别下你能看到的调度日志非常有限,只有调度成功/失败的节点数量结果,看不到细节。
排查调度失败问题时,建议临时把调度器日志级别调到 --v=4 甚至 --v=5。在 --v=4 下,你会看到每个节点的过滤理由,比如 node "node-01" is not suitable: insufficient cpu 这种非常具体的判断理由。在 --v=5 下,还能看到更底层的打分详情,但是日志量会很大,你需要额外确认磁盘空间。
如果你是 kubeadm 部署的集群,修改方式是在 /etc/kubernetes/manifests/kube-scheduler.yaml 里调整命令行参数,kubelet 会自动感知文件变化并触发静态 Pod 重建:
yaml复制spec:
containers:
- command:
- kube-scheduler
- --v=4
注意:修改静态 Pod 的 manifest 会影响控制平面可用性,而且重建意味着调度器会短暂重启,期间新 Pod 无法被调度。所以建议在业务低峰期操作。如果只是想快速看一眼,优先用
kubectl logs看现有日志即可,不要频繁重启调度器。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 调度日志里的关键信息怎么解读
2.1 从 Pending 状态看调度阶段
一个 Pod 卡在 Pending,可能的原因不仅仅是“没有合适节点”。在 Kubernetes 中,Pod 从提交到运行要经历两个阶段:调度阶段和启动阶段。调度阶段是“选一台节点”,启动阶段是“在这台节点上拉镜像、启动容器”。
如果 Pod 一直处于 Pending,而调度日志里根本没有这个 Pod 的记录,说明它可能连调度队列都没进,或者一直卡在调度队列里。这种情况往往和 API Server 准入控制、资源版本、或者大量 Pod 竞争同一个队列有关。如果调度日志里明确写了过滤后的节点数量为 0,那就是真的没有满足条件的节点。
我在实际排查中常用的一个判断方法:先看 Pod Events,如果事件里没有任何 FailedScheduling 的反馈,再看调度器日志里是否存在这个 Pod 的调度记录。事件没有、日志也没有,说明调度器根本没有关心过这个 Pod,问题反而在更上游。
2.2 调度器日志里常见的关键词
阅读调度器日志时,你会反复看到几个关键词,我把它们整理成了一个索引表。
| 关键词片段 | 出现阶段 | 含义 |
|---|---|---|
Attempting to schedule pod |
调度开始 | 调度器从队列取出 Pod 开始处理 |
Schedule result |
调度结束 | 调度成功,包含选中的节点 |
Failed to find any fit nodes |
过滤结束 | 所有节点都不满足硬性条件 |
insufficient cpu/memory |
Filter | 节点剩余资源不足 |
node(s) didn't match node selector |
Filter | 节点标签不满足 nodeSelector 要求 |
node(s) had taint |
Filter | 节点有污点且 Pod 未容忍 |
node(s) didn't match pod anti-affinity |
Filter | 不满足反亲和性约束 |
didn't match topology spread |
Filter | 不满足拓扑分布约束 |
Scoring Node |
打分 | 进入优选阶段,各节点打分 |
看到 Failed to find any fit nodes 后,日志紧接着就会列出失败的节点计数和各节点的失败原因。但默认 -v=2 级别不会把每个节点的原因明细写全,这也就是为什么我反复强调排查时把级别拉到 --v=4。
2.3 典型的调度失败类型
根据我经手的生产环境问题统计,调度失败绝大多数集中在下面几种类型,它们在日志里的表现各不相同。
第一类是资源不足。日志里会出现 insufficient cpu / insufficient memory 这样的字眼。很多人疑惑“节点明明还有很多空闲资源”,实际上 K8s 调度依据的是节点的 Allocatable 减去已分配 Requests 之后的剩余值,不是实时使用率。一个节点如果被部署了大量小内存 Pod,即使实际负载很低,剩余可分配量也会被算得很低。这个概念必须记牢,否则你会被日志误导。
第二类是节点选择器和亲和性不匹配。日志里会出现 didn't match node selector 或 didn't match pod affinity/anti-affinity。这类问题通常是标签没打对,或者多个 yaml 里的标签名不一致。某些情况下是标签大小写或空格导致,kubectl label 后没验证生效。
第三类是污点未容忍。日志里出现 node(s) had taint,后面跟着具体的污点键值,比如 node-role.kubernetes.io/control-plane:NoSchedule。如果你确实需要调度到带污点的节点,必须在 Pod 里声明对应 tolerations。这个判断很死板,声明了就能过,没声明就不过。
第四类是卷的拓扑约束。如果你的 Pod 使用了有地域限制的持久化卷,比如云盘只能挂载到特定可用区的节点,调度器会检查目标节点是否匹配 PV 的 topology 约束。日志里可能不会直接写“volume zone mismatch”,而是以 node affinity 的形式出现。这类问题排查起来最隐蔽,因为你可能围绕着资源查了很久,结果卡在存储上。
3. 一次完整的调度失败排查实操
3.1 构造一个“调动不了”的 Pod
为了把整个排查链路走通,我用自己的环境模拟了一次典型的调度失败。我用一个很简单的 Deployment,请求 4 核 CPU 和 4Gi 内存,同时给 Pod 加了一个几乎不可能满足的节点选择器:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: schedule-debug
spec:
replicas: 1
selector:
matchLabels:
app: schedule-debug
template:
metadata:
labels:
app: schedule-debug
spec:
nodeSelector:
disktype: ssd
gpu-type: a100
containers:
- name: debug
image: busybox:1.36
command: ["/bin/sh", "-c", "sleep 3600"]
resources:
requests:
cpu: "4"
memory: "4Gi"
提交之后,kubectl get pods 显示这个 Pod 卡在 Pending。这时候我不急着看调度器日志,先按顺序走一遍排查流程。
3.2 逐层排查的完整过程
第一步使用的是 kubectl describe pod,看事件。这一步能看到调度器给出的失败摘要,但不是最终的诊断依据:
bash复制kubectl describe pod schedule-debug-xxx
Events 区域显示:
code复制FailedScheduling 0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.
这个信息只告诉我们“不匹配选择器”,但没告诉我们具体是哪个标签不满足。这时候才轮到调度器日志上场。
第二步,把调度器日志级别调整到 --v=4,然后重新创建一个 Pod,观察完整调度过程。因为调度器日志是持续输出的,直接用 --tail 的参数倒出最近的日志即可:
bash复制kubectl logs -n kube-system kube-scheduler-master-01 --tail=100 > /tmp/scheduler.log
grep -i "schedule-debug" /tmp/scheduler.log
搜索结果里出现了这样的内容:
code复制"pod":"default/schedule-debug-xxx","node":"node-01","err":"node(s) didn't match node selector"
到这里,我们确认了日志和事件一致,问题就出在 nodeSelector。第三步是核对节点的实际标签:
bash复制kubectl get nodes --show-labels
结果发现节点上根本没有 disktype=ssd 或者 gpu-type=a100 的标签。我把标签打上之后,Pod 立刻从 Pending 转成 Running。
这个例子的教训是:描述信息只给了“不匹配”的结论,日志也只是给出了同样的结论,真正有价值的是我们把“为什么不匹配”核对到了节点标签层面。如果你依赖 Events 里的摘要就直接去检查 label,当然也能解决,但在更复杂的多选择器场景中,没有日志细节你会很难定位是 disktype 还是 gpu-type 的问题。
3.3 用调度器日志还原决策过程
顺着这个例子继续,把日志级别调到 --v=4 之后,你还能看到更完整的链路。日志结构大致像这样:
code复制Attempting to schedule pod: default/schedule-debug-xxx
...
node-01 failed: node(s) didn't match node selector
node-02 failed: node(s) didn't match node selector
node-03 failed: node(s) didn't match node selector
Failed to find any fit nodes for pod: default/schedule-debug-xxx
这里每一行都是调度周期中 Filter 阶段的产物。调度器会遍历所有 Ready 节点,逐个检查 Pod 的硬性约束,不满足就过滤掉。全部过滤完之后,如果剩余节点为 0,就会输出那个 Failed 的结果。
如果 Filter 阶段有剩余节点,日志会进入打分阶段,你会看到类似 Scoring Node 这样的关键字,随后是 Schedule result。此时 Pod 会分配到得分最高的节点。这个分高不代表节点最闲,而是综合了资源、亲和性、节点分布均衡度等多种因素后的结果。理解这个决策过程,有助于你解释“为什么明明 node-B 更空,Pod 却去了 node-A”这类现象。
整个日志链路其实和源码里的调度流程一一对应:调度队列 → 调度周期(Filter → Score)→ 绑定周期。你的日志看到哪个阶段,就知道问题卡在哪。
4. 常见问题排查速查与避坑经验
4.1 常见问题速查表
我把日常工作中高频出现的一些调度日志和对应的处理方法整理成了速查表,方便你遇到问题时直接对号入座。
| 日志关键字 | 问题类型 | 常见原因 | 处理思路 |
|---|---|---|---|
insufficient cpu |
资源不足 | 节点可分配 CPU Requests 已耗尽 | 查看节点 Allocatable,释放或扩容 |
insufficient memory |
资源不足 | 节点可分配内存 Requests 已耗尽 | 查看节点 Allocatable,释放或扩容 |
didn't match node selector |
节点选择器 | Pod 的 nodeSelector 与节点标签不一致 | 核对标签 kubectl get nodes --show-labels |
didn't match pod affinity/anti-affinity |
亲和性约束 | 亲和性规则要求 label,但节点不满足 | 检查亲和性表达式与节点标签 |
node(s) had taint |
污点 | Pod 未声明匹配的 tolerations | 声明 tolerations 或移除污点 |
didn't match topology spread |
拓扑分布 | Pod 不满足 topologySpreadConstraints | 检查拓扑分布约束与节点标签 |
node(s) didn't match volume node affinity |
存储 | PV 的拓扑约束与节点不匹配 | 检查 PV 的 zone,选择同区节点 |
No resources to schedule |
调度队列 | 调度器积累了大量待调度 Pod | 查看调度队列深度,清理大批量任务 |
这张表只能覆盖大部分常见故障。实际生产中,我遇到过更加隐蔽的场景,比如某个 Pod 同时存在资源不足和污点不满足,日志只显示其中一个错误。因为调度器在 Filter 阶段是逐项检查的,某些失败项可能被前置失败项覆盖,导致你只看到最先遇到的错误。这种情况下需要逐个排除。
4.2 避坑经验:日志虽全但别过度依赖
调度器日志是排查的有力武器,但依赖它也有需要注意的地方。
第一,调度器日志默认不落盘。kubeadm 部署的静态 Pod 会把日志送到容器运行时,日志轮转会抹掉历史。生产环境排障一定要提前配置日志采集,把 /var/log/containers/kube-scheduler*.log 采集到集中式日志平台,否则故障发生后再想查,只能看到最近一小段,这在很多场景下是致命的。
第二,事件过期问题。Kubernetes 的 Event 默认保留时间只有一小时左右。堵在 Pending 半天以上,再去看 Events 可能已经被清理干净。那时候调度器日志就成为了唯一线索。所以如果你发布任务经常出现 Pending,先确认自己的监控告警,才能保证在 Event 过期前收到提醒。
第三,调度器多副本场景。高可用集群中 scheduler 可能运行了多个副本,此时日志分布在多个 Pod 里。直接用 kubectl logs -n kube-system deployment/kube-scheduler 查看(假设你的调度器以 Deployment 方式部署),或者加 --prefix 看 logs 会带出 Pod 名称。如果只盯某一个副本,可能恰好那个副本没有处理你的 Pod,导致你以为调度器没日志。
第四,也是最容易被忽略的一点:调度器日志级别不要长期保持过高的 -v。级别越高性能消耗越大。尤其是大型集群里,--v=5 的日志可以在一天内写满几十 GB 磁盘,直接拖垮控制平面。我建议平时保持默认级别,排障期间临时调高,定位到问题后立刻调回来。
4.3 另一个容易翻车的坑:千万不要忽略调度队列
有一次我在一个测试集群里排障,看到日志明明在输出 Attempting to schedule pod,但 Pod 就是一直 Pending。更诡异的是,日志并不是在调度这个 Pod,而是在调度后面排队的其他 Pod——调度器队列里积累了太多待调度的任务,你的 Pod 一直排在后面。
这种情况通常发生在调度器重启后。由于调度器的事件源和 informer 缓存需要从 API Server 重新同步,如果集群里有很多 Pod 需要被调度,队列处理能力跟不上生产节奏,就会出现调度延迟。日志里最明显的特征是:你看到的 Pod 名字永远是队列最前面的那个,和你正在排查的 Pod 对不上。
处理方式通常是等待自然消化,或者临时增加调度器性能配置(比如调整 kube-api-qps)。但这类问题在中小集群里比较少见,出问题概率不高,反而是大批量上线任务的场景偶尔会踩中。既然写到这里就一并提醒你:排查时一定要确认日志里的 Pod 名和你关心的是同一个。
5. 调度日志扩展:从单点日志到调度全链路可观测
单个 Pod 的调度日志毕竟有限。真正在生产环境里被调度问题反复折磨过后,我意识到“日志”不能停留在看单词、单条记录的层面,而是要把调度过程放到整个集群可观测体系里来看。
如果你所在团队已经有 Prometheus 监控,可以关注 kube_scheduler_pod_scheduling_duration_seconds 这个指标,它反映 Pod 从创建到被调度成功的总耗时。通过这个指标可以快速判断调度是否变慢。更细的指标还有 kube_scheduler_schedule_attempts_total,记录了调度尝试的总次数和失败次数。把日志和指标结合起来,定位效率会比单纯翻日志快一个数量级。
另一个容易被忽视的是 kubelet 侧的日志。很多时候调度器认为自己已经成功绑定了节点,但 Pod 最终状态异常,问题出在 kubelet 启动容器环节。调度日志和 kubelet 日志分属两个组件,但一个完整的问题链路往往横跨两者。比如节点磁盘压力导致 kubelet 驱逐 Pod,调度器并不知道,它只是把 Pod 调度过去,结果又因为资源问题被驱逐。这种跨组件问题,只盯调度日志是无法解决的。
我自己的做法是:排障时同时打开调度器日志、目标节点 kubelet 日志、以及 Pod 事件三个窗口,按时间轴对齐观察。时间轴对齐非常重要,因为不同组件的日志由不同进程产生,时区、精度可能不同,如果不对齐,你看到的“事件顺序”可能是误导。
另外,如果集群规模已经超过几百个节点,调度器日志量会非常大。拿到日志之后建议先用 grep 把关注 Pod 的 UID 从海量日志中过滤出来,再按时间排序阅读。实际排障中我发现很多人是直接从头翻日志,这样效率极低。先用 UID 过滤是性价比最高的方式。
5.1 自定义调度器的日志怎么看
有时候默认调度器不能满足需求,团队会引入自定义调度器(比如基于 Scheduler Framework 扩展的插件)。此时 Pod 通过 schedulerName 指定使用自定义调度器。这种场景下,日志的查看位置完全取决于你如何部署这个调度器。
如果是独立 Deployment 部署,日志直接看对应 Deployment 的 Pod。如果自定义调度器是嵌在你的业务系统里的(常见于一些平台型团队把调度逻辑写进后台服务),那日志就得找你自己的服务日志。
自定义调度器在 K8s 生态里是一个成熟的方向,但使用它有额外的维护成本。默认调度器的日志格式、语义、上下文你都熟悉,换成自定义的之后,日志可能没有统一标准,排障难度会上升。所以这里也建议:自定义调度器的日志打印,至少要包含调度周期开始、每次过滤的具体原因、打分结果、绑定结果这几个关键节点,最好和默认调度器日志风格保持一致。否则团队其他人排障时面对一堆风格不一致的日志会非常痛苦。
5.2 调度器高可用与日志持久化的配合
生产环境一般会部署多个 scheduler 副本来保证高可用,配合 leader election 机制保证同一时间只有一个调度器在工作。多副本场景下,日志会分散到不同副本 Pod 中,如果只查看固定一个副本,可能会漏掉实际执行调度那个副本的输出。
这种场景下的最佳实践是把日志统一采集到集中式平台,然后通过 Pod 的名字字段做过滤。注意,当 leader 切换时,实际工作副本也会变化,你的过滤条件可能需要覆盖所有副本。日志集中化不仅对调度器有意义,对 kubelet、API Server 同样重要,这是整个控制平面可观测性的基础。
我经历过的教训是:一个节点宕机触发了大量 Pod 重新调度,这时刚好 scheduler leader 发生切换,新 leader 需要时间重建 informer 缓存。大量 Pod 调度延迟,但所有日志都集中在旧 leader 上,我只查了新 leader 的日志,一度以为调度器没在工作。最后才意识到需要同时看两个副本的日志。所以多副本场景下,日志检索条件一定要写全,或者干脆都采集到一起。
6. 部署环境不同,日志获取方式的差异
部署方式决定了日志获取的难度,这里展开说一下不同情况下的具体操作。
自建集群(kubeadm、二进制部署),控制平面组件都是你的,日志获取最直接。kubeadm 场景下调度器是静态 Pod,可以通过容器运行时工具获取日志,也可以通过 kubectl logs。二进制部署场景下,调度器一般由 systemd 管理,日志通过 journalctl -u kube-scheduler 查看,或者由你自己配置的日志文件输出。
云厂商托管集群(比如某云容器服务),控制平面组件被托管,通常无法直接登录节点查看调度器日志。此类场景下,你需要看托管控制平面的日志服务,或者依赖集群审计日志以及监控面板。大多数托管服务商都会提供控制平面关键组件的日志查询入口。如果完全没有日志能力,只能通过 Events 和监控指标来推断调度行为。
有一种容易被忽略的场景:混合部署。节点一部分在物理机房,一部分在云上,网络和质量差异不小。调度器默认不考虑网络质量,只看资源约束。这种场景下出现调度问题,你必须结合监控看跨地域节点的网络延迟、丢包率,单纯看调度日志是查不出来的。当然这是调度策略层面的问题,不是日志本身的范畴,但排查链路必须拉长到这个维度才能找到根因。
6.1 关于 Pod 调度日志的最终建议
如果你正被一个 Pending 状态折磨,我建议你按照这样的顺序排查:先看 Events 确认是否进入调度器,再看调度器日志找到具体过滤原因,然后核对节点标签、资源、污点,最后看 kubelet 日志确认启动阶段是否正常。调度器日志是这条链路里的中轴,但也不是万能的。
我最后再分享一个提升效率的小技巧。排查调度问题的时候,不要只看一个 Pod 的日志,试着看同一批次其他 Pod 的调度情况。如果只有这一个 Pod 失败,大概率是它自身的 selector 或者 affinity 写得有问题。如果整批 Pod 都在 Pending,多数是集群级的资源或污点、调度器本身出了问题。这个对比思路能帮你快速缩小范围,不用一上来就钻进单个 Pod 的细节里。
调度日志看上去只是一行行文本,但每一行背后都是 kube-scheduler 内部调度周期的一次完整执行。把日志读明白,你基本上就把 K8s 调度这件事搞懂了大半。也希望这篇内容能帮你在下次遇到 Pending 的时候,多一份从容,少一点盲猜。
