1. 为什么要在Kuboard里折腾节点调度
先说个实际的场景。某公司内部跑着一个K8s集群,业务负载不算重,但也分了几类:核心数据库、日志采集、前端网关、定时任务。集群就几台工作节点,配置还不完全一样。有一台机器是SSD盘,一台是机械盘,另一台内存特别大。结果呢,K8s默认的调度器一视同仁,把一些IO密集型的Pod调度到了机械盘那台节点上,响应时不时飙到几百毫秒。后来看监控才发现,问题不在应用本身,而在Pod跑错了节点。
这种时候你就得干预调度器,明确告诉它哪些服务该固定在哪类节点上跑。这就是“更改服务运行的节点方式”要解决的核心问题。
Kuboard作为一款可视化K8s管理面板,把这类原本要写YAML、敲kubectl的操作收敛到了界面上。今天这篇就围绕Kuboard里调整服务运行节点这件事,把背后的调度原理、界面操作路径、常见坑和排查思路都过一遍。不管你是刚接手集群的运维新手,还是被调度问题困扰了一阵子的开发者,这篇文章应该都能帮到你。
先明确一下“更改服务运行的节点方式”到底包含哪些操作。按我自己的经验,日常里遇到的需求大致分这么几类:
- 某个服务只允许跑到指定的某一台节点上,比如日志采集必须落在有SSD盘的机器上。
- 某个服务可以在某一组节点里挑,比如内存优先、磁盘优先。
- 某个服务绝对不能跑在某些节点上,比如把测试环境的Pod和业务环境的Pod物理隔离开。
- 节点要维护了,需要把所有Pod从该节点赶走,迁到其它节点。
这几类需求,在K8s里分别对应着不同的实现手段:nodeSelector、节点亲和性、污点和容忍,以及节点维护时的驱逐(drain)。Kuboard里把前几项都做成了可视化的表单,操作起来比手改YAML要直观得多。
2. 先搞懂调度器的几个关键概念
在打开Kuboard界面动手之前,我建议先花几分钟把底层机制过一遍。原因很简单:界面上点几个按钮谁都会,但你如果不知道按钮背后的字段是怎么生效的,出了问题就完全抓瞎。
K8s的调度器(kube-scheduler)默认只会做一件事:给待调度的Pod找一个满足所有约束条件的节点。如果没有约束,那它就看资源够不够,够就扔过去。你“更改服务运行的节点方式”,本质上就是给Pod加约束条件,让调度器按你指定的规则来选节点。
这里涉及三个核心机制,我逐一拆开来讲。
2.1 节点选择器:最简单粗暴的硬性绑定
nodeSelector是K8s最基础、最直观的节点选择方式。实现原理就是给节点打上标签(Label),然后在Pod的spec里声明要哪些标签。调度器看到Pod带了这个selector,就只会从打了对应标签的节点里挑。
打个比方,这就是“名媛筛选法”——你要求对方必须有车、有房,才进入下一步聊。条件不满足,直接不匹配。
举一个我在Kuboard里实际配置过的例子。某业务需要读写大量本地文件,必须跑在SSD节点上。我给那台节点打了标签disk-type=ssd,然后在工作负载里加了一条nodeSelector:disk-type=ssd。保存后,这个服务的所有Pod都会优先且只能落在打了这个标签的节点上。
nodeSelector的特点是非常直接,但缺点也明显:不支持“必须满足且满足多个条件之一”这种复杂逻辑,也不支持软性偏好(比如“最好在这类节点,实在没有也行”)。如果要更灵活的调度策略,就要用到节点亲和性。
2.2 节点亲和性:带权重的进阶版选择规则
节点亲和性(nodeAffinity)解决的正是nodeSelector做不到的事。它把节点选择规则分成了两类:
- requiredDuringSchedulingIgnoredDuringExecution:硬性要求。调度时必须满足,不满足就调度失败,类似nodeSelector但支持更复杂的表达式。
- preferredDuringSchedulingIgnoredDuringExecution:软性偏好。调度时会优先选满足条件的节点,不满足也会调度到其它节点,只是优先级低。
硬性要求可以理解成“必须A、且B”;软性偏好可以理解成“最好A,没有也凑合”。表达式层面还支持In、NotIn、Exists、DoesNotExist这些操作符,能组合出节点不在某组、节点存在某标签这类规则。
在Kuboard里配置这个的时候,你不需要手写YAML,界面里会让你选择规则类型、键、操作符和值。但界面再怎么友好,你还是得先想清楚业务到底要哪种约束。
我自己之前在某集群里把一组GPU节点打了gpu-type=A100的标签,然后设置了一个软性偏好规则:优先调度到该标签的节点。结果是,在GPU节点资源充足的时候Pod都过去了,GPU资源被占满时Pod会退回到普通节点继续跑,不会因为调度不满足而一直Pending。这个特性对提高资源利用率特别有用。
2.3 污点和容忍:控制谁能上车的门禁系统
nodeSelector和节点亲和性是Pod主动“挑”节点,污点(Taint)和容忍(Toleration)则是节点主动“赶”Pod。
污点可以理解成节点挂了一块牌子:“本节点谢绝普通乘客”。默认情况下,没有特殊许可的Pod都上不了这台节点,不管你的调度规则写得多么想过来。Pod要是想强行进去,就得在spec里加上容忍(Toleration),相当于有了一张“通行证”。
污点有三种效果(Effect):
- NoSchedule:新的Pod不允许调度上来,已在运行的Pod不受影响。
- PreferNoSchedule:尽量不调度上来,实在没地方去了也可以。
- NoExecute:不仅新Pod不能上,已经在上面跑的也会被驱逐。
这个机制在Kubernetes里极其常用。节点维护前打上node.kubernetes.io/maintenance: NoSchedule,业务节点和存储节点互相隔离,或者让某些核心组件独占整台机器,都靠它。
在Kuboard里对某个工作负载配置容忍,就是在界面上填两个参数:key和effect,还有可能填value。key对应节点污点的名字,effect对应你想容忍的污点效果。这里有个容易理解错的地方:容忍不是“我要去那台被污点的节点”,而是“我不在乎节点有没有这个污点”。把自己变得能扛事,而不是专门去某台机器。
2.4 节点维护与驱逐
最后一类需求不是让Pod“去”哪,而是让Pod“走”。节点要做硬件维护、升级内核、换SSD盘的时候,你得先把该节点上的Pod全部安全地赶走。暴力的做法是把节点直接关机或者删掉节点,但这样Pod不会被自动迁移,还是停留在Terminating状态,直到上层控制器重新在别的节点上拉起新的副本。
更稳妥的方式是驱逐(drain)。它会先给节点打上不可调度的标记,然后逐一驱逐上面运行的Pod。驱逐是一个讲究优雅的过程:先触发preStop钩子,给业务留出处理收尾工作的时间,再发SIGTERM信号,等容器真正退出后,Pod会被重新调度到其它节点。
Kuboard界面上没有直接提供kubectl drain的图形化按钮,但我可以用它观察驱逐前后Pod的状态变化,再结合命令行来执行驱逐操作。这一块后面实操部分会详细说。
3. Kuboard上更改节点调度的实操路径
理论铺垫完了,现在拿着Kuboard界面一步步操作。
下面这些步骤基于我常用的Kuboard版本,界面版本不同可能在菜单名称上有细小差异,但功能入口的大致逻辑是一致的。主要涉及三个页面:工作负载(工作负载编辑页)、无状态/有状态控制器列表、节点管理。
3.1 第一步:看清节点现状与标签情况
在Kuboard的集群首页里,点击“节点”菜单,能看到集群里所有节点的资源状态、Kubelet状态、容器运行时版本等信息。最关键的,是页面下方或侧边会展示每个节点的标签(Labels)和污点(Taints)。
这里必须养成一个习惯:在配置任何调度规则之前,先花两分钟确认节点标签写没写错。我踩过一个很蠢的坑:给节点打标签时键写成disk-type,值写成SSD(大写),配置nodeSelector时填的却是ssd(小写)。标签值严格区分大小写,等Pod一直Pending了才发现根本匹配不上。
节点标签的查看,Kuboard界面上通常是在“节点详情”里,每个标签一行,键值对。如果要修改标签,Kuboard可能没有直接的编辑入口,这时用命令行很高效:
kubectl label node <node-name> disk-type=ssd kubectl label node <node-name> disk-type- # 删除标签看到节点标签已经就位,再继续下面的操作。
3.2 第二步:在工作负载里添加节点选择规则
在Kuboard里找到你想要变更运行节点的服务。进入“工作负载”页面,选中对应的无状态工作负载(Deployment),或者有状态工作负载(StatefulSet),点击“编辑”按钮。
在编辑页里找到“调度策略”相关的表单区域(不同版本可能叫“节点调度”“节点选择”之类的名字)。这里一般会提供两种配置方式:一种是可视化的表单,让你直接填标签键值对;另一种是直接编辑YAML,适合更复杂的规则。
如果是简单场景,比如我想让服务的Pod都跑到disk-type=ssd的节点上,在表单里添加一条选择器,键填disk-type,值填ssd,效果就是把nodeSelector挂到了Pod上。保存后,控制器会发起一次滚动更新(如果镜像没变,也可能会触发一次重新调度)。
有人会问:我加了nodeSelector之后,原来的Pod会不会马上被迁移过去?答案是:不会。nodeSelector只对“新建的Pod”生效。你改完工作负载的调度规则,Deployment会创建一个新的ReplicaSet,把旧Pod替换成新Pod,新Pod是带着这个规则被调度器处理的,所以它会被调度到符合规则的节点。但如果你的Pod数量不变,Deployment的滚动更新逻辑会把旧的Pod先删除再创建新的,这就会出现一个短暂的迁移过程。
从实操角度看,如果你只是想调整调度而完全不想重启Pod,那就得考虑使用“原地更新”之类的特殊机制,或者说接受一次滚动更新。绝大多数业务接受这个短暂的重启窗口。
3.3 第三步:配置节点亲和性实现更复杂的条件
如果只是单个标签条件,nodeSelector就够用了。但当你面对的是“这个服务最好跑在高内存节点上,但高内存节点不够时也能接受跑到普通节点”这类场景,就该上节点亲和性。
在Kuboard编辑工作负载的调度策略区域,选择“节点亲和性”。界面上一般会区分“硬性要求”和“软性倾向”两部分:
- 硬性要求(required):配置一条或多条规则,调度器必须满足才让Pod上节点。比如:
node-type in (highmem)。 - 软性倾向(preferred):配置规则并设置权重(1到100的整数),调度器会按权重和匹配情况打分,分数高的节点优先调度。
实操中我经常把两者混着用:硬性要求先排除掉绝对不能上的节点,软性倾向再去更优的节点上靠。例如:
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-type operator: NotIn values: - test preferredDuringSchedulingIgnoredDuringExecution: - weight: 90 preference: matchExpressions: - key: node-type operator: In values: - highmem这段配置的意思就是:Pod不能跑在node-type=test的节点上,同时优先找node-type=highmem的节点。Kuboard的YAML编辑模式里可以直接粘贴这类配置,可视化表单模式下操作逻辑是一样的,只是界面把每一段拆成了下拉框。
我个人的建议是,新人先用可视化表单把规则逻辑跑通,看生成的YAML长什么样,理解每个字段的含义。有一定经验之后再直接改YAML效率更高。
3.4 第四步:配置容忍让Pod能进入污点节点
容忍的配置位置和节点亲和性很接近,同样是在编辑工作负载时的调度策略区域里。Kuboard会提供一个“容忍”表单区域,让你填写:
- 键(Key):对应节点污点的key。
- 值(Value):对应节点的污点值,非必填。
- 效果(Effect):NoSchedule、PreferNoSchedule或NoExecute。
这里我多说一句:如果节点的污点是没有值的那种(比如node.kubernetes.io/not-ready这种系统级污点),你会看到它的value是空的,对应的容忍配置里就不需要填值,或者选上Exists这个操作符,表示只要key存在就容忍。
实际生产里最常见的配合是:给专用节点打污点,然后给对应工作负载配容忍。比如某公司有一台昂贵的GPU机器,不希望任何业务Pod跑上去,就在节点上打了一个污点:
kubectl taint nodes <gpu-node> gpu=true:NoSchedule之后只有加了容忍key=gpu, value=true, effect=NoSchedule的Pod才能调度上去。其它Pod即使集群里其它节点都满了,也不会往这台GPU机器上调度。这是资源隔离非常有效的手段。
3.5 第五步:节点维护时的节点方式变更操作
再来说节点维护和Pod迁移。这个场景虽然不完全是“服务运行的节点方式”的正面操作,但它是把服务从一个节点迁移到另一类节点最常见的等效手段。
操作步骤是这样的:
先用Kuboard确认要维护的节点上有哪些Pod。通常进入“节点详情”,能看到该节点正在运行的所有Pod。接下来执行驱逐命令:
kubectl cordon <node-name> kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data第一条命令是标记节点为不可调度,后续新Pod不会调度上去。第二条命令是驱逐已有Pod,--ignore-daemonsets是告诉系统把DaemonSet管理的Pod忽略掉(比如日志采集这种每个节点都必须有的Pod,驱逐了也会马上重建,没必要赶),--delete-emptydir-data是允许删除使用emptyDir卷的Pod(emptyDir数据本来就不保证持久化,存了也是临时数据)。
驱逐过程中,这些被驱逐的Pod会被所属控制器(比如Deployment)重新创建,调度器会找一个满足调度条件的节点放上去。所以最终效果就是:服务从被维护的节点迁移到了其它可调度节点,也就变相“改了运行节点”。
等节点维护完毕,把节点重新加入服务:
kubectl uncordon <node-name>关于驱逐的执行效果,我建议你在Kuboard的“事件”或Pod列表中盯着看。你会看到Pod的状态从Running变成Terminating,然后又出现一个相同名字前缀的Pod,状态变为Pending再到Running。如果业务原本只有1个副本,驱逐时可能出现短暂的不可用时间段,所以生产环境迁移前务必确认副本数或做足容灾。
4. 不同业务场景下的调度方案选型
配置一条调度规则很容易,难的是选对方案。我把自己在多个环境里总结的方案选择逻辑整理成了一张表,供参考。
| 业务需求 | 推荐方案 | 不推荐的理由 |
|---|---|---|
| 把Pod固定到某一台节点 | nodeSelector或硬性节点亲和性 | 直接用节点名称做选择器没毛病,但K8s标签方式更通用 |
| 只在某组节点上跑,且节点组随时间变化 | 给节点打标签 + nodeSelector | 不要用节点名硬编码,节点故障重建后名会变 |
| 尽量跑在性能好的节点上,但资源不足时可退而求其次 | 软性节点亲和性(preferred) | 硬性亲和性会导致Pod无处可调度 |
| 和另一组服务物理隔离,防止互相干扰 | 节点污点 + 容忍,或者干净写一个调度器扩展 | 只靠nodeSelector容易漏掉别人加进来的服务 |
| 节点维护,需要迁移上面所有Pod | drain驱逐 | 手动删除Pod很危险,DaemonSet和StatefulSet处理逻辑不一样 |
| 让服务必须在特定节点之间尽量均匀分布 | Pod拓扑分布约束 | nodeSelector做不到分布均匀,只能保证落在哪个集合 |
节点亲和性和nodeSelector你能用上80%的日常需求。剩下那些非常复杂的调度逻辑,比如“同时考虑节点CPU型号、GPU显存和当前负载”,就需要结合自定义调度器或者调度插件来做了。Kuboard作为管理面板,能帮你完成90%的常规操作,但永远替代不了你对调度逻辑本身的理解。
5. 常见问题与排查技巧实录
实际操作中,更改节点调度后总会出现一些奇奇怪怪的现象。我把这些年遇到的、被问得最多的几个问题整理一下。
5.1 Pod一直Pending不启动
最经典的问题。改了调度规则之后,新Pod一直处于Pending状态,不管等多久都不起来。
排查思路从两条线走:
第一步,看Pod的事件。在Kuboard里点击Pod,看事件列表,通常会有类似0/12 nodes are available: 3 node(s) didn't match node selector这样的信息。这句话非常关键,说明根本没有节点满足你的nodeSelector条件。
第二步,核对标签。可能是标签键或值的大小写不一致,也可能标签删了没打回来。在节点详情里对比一下标签和规则里的键值。还有一个比较隐蔽的情况:标签存在,但标签值用了NotIn,而你写的值包含了标签里没有的值,导致匹配不上。
解决办法:要么修正节点标签,使之满足条件;要么调整调度规则。改完后建议强制重启Pod,Pending状态有时不会立刻重新调度评估,重建Pod可以加速生效。
5.2 改了调度规则但Pod还是原来的节点
明明改了nodeSelector,刷新了几次,Pod却还在旧节点上。遇到这个问题先别慌,确认一下你改的是不是当前正在生效的ReplicaSet。
如果工作负载的Pod模板变了,Deployment会创建一个新的ReplicaSet。旧的ReplicaSet在滚动更新完成之前会保留,但不会新增Pod。你看到的是不是还停留在旧ReplicaSet里的Pod?把工作负载的“滚动更新状态”打开看看。
还有一种情况:调度规则确实改了,但旧Pod被用的是Recreate更新策略,也可能是工作负载的副本数很低,滚动更新速度很快,你刷新时正好错过。或者Pod已经被调度到目标节点了,但你在Kuboard里看到的节点名称缓存未刷新,刷新页面就能看到。
5.3 节点有污点,服务却上不去
配置了容忍之后,Pod还是Pending。事件里写的是node(s) had taint {gpu: true}, that the pod didn't tolerate。
这种问题十有八九是容忍字段写错了。比如节点的污点是gpu=true:NoSchedule,但你的容忍只写了key和effect,没写value。在K8s里,容忍匹配污点有两种模式:精确匹配(key+value+effect)和宽松匹配(key+effect,值任意)。如果你写的是精确匹配,value必须和污点完全一致;如果写宽松匹配,要用Exists操作符,表示只要key存在就容忍。
有些系统级污点还有个特点:默认值很特殊,比如node.kubernetes.io/not-ready和node.kubernetes.io/unreachable,它们的效果是NoExecute且默认所有Pod都要容忍一段时间。你要是想确保Pod在节点故障后快速迁移,就需要给Pod配置对应的容忍配置,这个在Kuboard里也可以填。
5.4 驱逐节点时部分Pod卡住不动
执行drain时,绝大多数Pod都能顺利迁移,但偶尔会见某个Pod始终处于Terminating状态,或者drain命令直接卡住。
最常见的原因是Pod有未释放的卷或还在被某类Finalizer阻塞。另一种情况是Pod没有配置preStop钩子,而进程收到SIGTERM后不退出,死活不响应。这种情况下,kubectl drain会等一段时间,超时后强制删除Pod。
我建议你在生产环境执行drain之前,先做两件事:
一是检查该节点上是否有StatefulSet管理的Pod。StatefulSet的Pod和Deployment不同,驱逐后重建不一定能被调度到同一套持久卷能用的节点上,有卷绑定问题。要格外小心。
二是确认业务是否支持优雅退出。如果没有preStop钩子,建议先手动验证一下应用收到SIGTERM后的表现,再决定是否开启drain。
如果非要强制驱逐某个卡住的Pod,可以这样操作:
kubectl delete pod <pod-name> --force --grace-period=0但这个操作是最坏的兜底方案,它跳过了优雅结束流程,可能造成数据不一致。能用正常手段解决就尽量不用。
5.5 污点和容忍配置影响到了其它服务
污点是一把“双刃剑”。给节点打了NoSchedule后,你原本打算只让部分业务避开这个节点,但实际效果是所有普通Pod都无法调度上来。如果集群资源本来就不充裕,可能直接导致一批无关Pod pending,影响面非常大。
我在一次测试环境里就干过这种事:给一台节点打了maintenance=in-progress:NoSchedule,顺手把所有相关服务都配上了容忍。结果另一组业务没有容忍,它们的Pod刚好被调度器选中这台节点,调度失败,反复重试。排查了半天才意识到污点的波及范围是全集群的,不是只有知情的人才受影响。
所以在给节点打污点之前,一定先确认集群里有没有其它业务Pod可能被“误伤”。最好在打污点前先用命令看看有没有Pod正在上面运行,以及它们的控制器是否都有容忍。
5.6 Kuboard界面上看不到调度相关配置
有些版本的Kuboard把调度配置藏得比较深,找不到入口的时候可以在工作负载编辑页里搜索“调度”“节点”“亲和”“容忍”这些关键词。极端情况下,直接把工作负载切换到YAML编辑模式,手动加nodeSelector、affinity、tolerations这些字段。
YAML编辑模式是Kuboard里最底层的兜底方案,可视化表单能做的它能做,表单做不了的它也能做。我觉得这反而是好事:界面上没绑住手脚,想怎么改就怎么改。
5.7 误操作后的快速回滚
更改节点调度配置,原则上是低风险操作,但万一改错了,比如把所有副本调度到一个标签不存在的节点上,会导致服务整体不可用。这时候最重要的不是找原因,而是快速回滚。
在Kuboard里,工作负载编辑历史是有记录的(不同版本能力略有差异)。如果找不到历史版本,直接用命令回滚:
kubectl rollout undo deployment/<deployment-name>这条命令会把Deployment回滚到上一个ReplicaSet的Pod模板,节点调度规则也会跟着回到之前的状态。整个流程大概几十秒,比手动改配置快得多。
回滚之后,记得检查一下Pod状态是否恢复Running,确认无误后再去查当初改错的原因。
6. 我实际用下来的一些经验
项目做得多了,我逐渐形成了一套相对稳妥的节点调度操作流程,分享出来供参考。
第一,所有的节点标签和污点,必须在配置调度规则之前就准备好。不要先把工作负载的调度规则配好,再去补标签,这样中间会有一段Pod Pending的窗口期。如果Pod数量多,Pending事件会刷屏告警。
第二,尽量使用“软性亲和性”,而不是“硬性要求”。除非业务有极强的合规或性能诉求,必须强制绑在某类节点上。因为硬性要求一旦失效(比如节点标签被误删),Pod会一直Pending,服务直接不可用。而软性偏好最坏情况只是调度到次优节点上,服务还是能跑,只是性能打折。
第三,不要用IP地址或节点名作为标签值。节点可能因为故障被删掉重建,新节点的名字或IP可能变了,但是你原来写的标签值还是旧值,会导致调度问题。正确的做法是给节点打上稳定的业务语义标签,比如role=webserver、disk-type=ssd、az=zone-a,这类标签即使节点替换了,也能保证规则可用。
第四,针对节点维护,建议制定一个标准操作流程。我之前在某公司做过一件小事但很有意义:把节点维护的流程写成了一份内部文档,规定所有维护操作必须先cordon再drain,不允许手动删Pod或者直接关机。这份流程执行一年,遇到的故障明显比以前少了。K8s本身就提供了这么多优雅的机制,不用白不用。
第五,任何调度策略的改动,都要考虑它和集群整体资源的关系。你强制把一些Pod绑定到某些节点上,这些节点的资源就变成了硬上限;节点资源耗尽时,即使其它节点有很多空闲资源,这些Pod也过不去。资源池会被切碎,整体利用率会下降。如果集群规模不大,节点又多,过细的调度约束反而会帮倒忙。
7. 从“能用”到“用好”的进阶建议
最后给几条延伸的思路。
如果你已经熟悉了nodeSelector、亲和性、污点容忍,接下来值得研究的是Pod拓扑分布约束(topologySpreadConstraints)。它能把一组Pod尽量分散到不同的拓扑域(比如不同的可用区、不同的节点),避免所有副本挤在同一台节点上。这在故障域容错场景里很有价值。
另外一个方向是结合节点自动伸缩:节点不足时,可以给工作负载使用cluster-autoscaler喜欢的“宽泛调度策略”,而不是把Pod钉死在某类节点上。配合节点池的标签,自动伸缩出来的节点也能自动匹配调度规则。
Kuboard这种可视化工具给运维带来的效率提升是实打实的,但它只是一个入口,真正的判断力始终来自于你对调度模型的理解深度。每次做完节点调度修改,多花两分钟看一遍生成的YAML,搞清楚界面上点的每个按钮对应的是Pod模板里的哪个字段。这样积累一段时间,很多问题不需要别人帮你排查,你自己扫一眼YAML就能定位出来。
把我平时用的一条命令放在这里,可以快速查看当前集群所有节点及标签情况,适合在配置前后做对比:
kubectl get nodes --show-labels祝各位调度顺利,少踩几个Pending的坑。