
把 Kuboard 里跑着的服务从一台节点迁到另一台节点听起来就是在界面上改个选项的事实际做起来却有一堆隐藏的坑。我对这件事的印象特别深是因为有一次线上中间件集群磁盘告警我以为加个节点名就能搞定结果部署改完以后工作负载一直 Pending业务请求直接飘红。后来把节点选择、亲和性、容忍这些调度机制重新捋了一遍才真正搞懂 Kuboard 里那个调度选项背后到底在干什么。下面就把 Kuboard 里更改服务运行节点的几种方式讲清楚包括每种方式背后的调度逻辑、表单和 YAML 怎么填、改完以后怎么验证以及 Pending 之后从哪下手排查。适合正在用 Kuboard 管理 Kubernetes 集群的运维同学也适合刚接触容器调度、只知道概念还没实操过的新手。1. 服务运行节点是怎么被决定的先理解调度这张网不先把调度机制的大框架搭起来光在 Kuboard 里点来点去出了问题很容易蒙圈。Kubernetes 默认会把 Pod 调度到某个节点上这个决定由控制面里的调度器做出。调度器会综合每个节点的资源余量、污点设置、Pod 自身的亲和性要求等一系列条件给可用节点打分选一个最合适的。咱们在 Kuboard 里所见即所得的“改节点”本质上就是在修改 Pod 的调度条件让调度器只在这些条件下选节点。Kubernetes 里控制“Pod 跑到哪个节点”的机制主要有四类nodeName、nodeSelector、节点亲和性、污点与容忍。它们的力度和适用场景完全不同。方式作用对象力度典型场景nodeName直接指定某台节点强制执行绕过调度器紧急迁移、排查单节点问题nodeSelector通过节点标签匹配一组节点强制只有满足标签的节点可用按环境、机型、磁盘类型分组调度节点亲和性通过标签表达式匹配节点支持强制和偏好两种复杂条件组合、软性优先调度污点与容忍节点排斥 PodPod 声明容忍节点主动拒绝独占节点、隔离特殊工作负载用一个生活类比的例子默认调度器就像一个网约车平台它帮你自动派单选一辆离你近、价格合适的车。但如果你备注“不要拼车必须 SUV”平台就只能在符合条件的那部分车里选。nodeSelector、亲和性、容忍就是这类备注而 nodeName 相当于你直接指定车牌号让平台不要管其他条件必须派这辆车。直接指定车牌号的限制当然最死平时尽量别用。还有一个一定要建立的概念对于 Deployment、StatefulSet 这类工作负载节点选择是写在 Pod 模板template里的。也就是说Kuboard 里的“改节点”并不是直接把某个正在运行的 Pod 从一台机器挪到另一台而是修改了模板然后控制器会按照这个新模板去创建新的 Pod替换掉旧的。这也是为什么改完以后会看到 Pod 重启一遍这是正常现象不是故障。2. Kuboard 里改节点界面上你该认识的四个配置入口在 Kuboard 的工作负载编辑页面里不同小版本的菜单位置和命名会有一点差别有的版本叫“调度策略”有的版本在“高级设置”里还有的版本直接放在 YAML 编辑模式里。但不管界面长什么样核心的配置入口就是下面几个。2.1 节点选择nodeSelector最基础、最常用Kuboard 表单里通常会提供一个“节点选择”或“NodeSelector”的键值对列表。你填的每一对键值都会被写入 Pod 模板的 nodeSelector 字段。调度器会忽略所有不含这些标签的节点只在你指定的那一批节点里挑。例如想让服务只跑在拥有diskssd标签的节点上表单里就填diskssd对应到 YAML 里是这个样子spec: template: spec: nodeSelector: disk: ssd使用 nodeSelector 的关键是节点标签必须完全匹配键和值缺一不可。如果节点上只有diskssd而没有envprod但你同时填了两组键值那这个 Pod 照样会 Pending。2.2 节点亲和性支持 In、NotIn、Exists 等更复杂的条件当你的条件不是简单的一组键值对而是“我要 env 为 prod 或 staging 的节点但不要 disk 为 hdd 的”nodeSelector 就不够用了。这时候要用节点亲和性nodeAffinity。Kuboard 的调度策略区域通常能看到“节点亲和性”的添加按钮点开后可以填写 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 两类规则。前者是硬性要求不满足就没得商量后者是软性偏好实在没有符合条件的节点调度器也会往其他节点放。常见的强制表达式配置是affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: env operator: In values: - prod - staging这段 YAML 的意思是节点必须带有一个名为 env 的标签且标签值必须是 prod 或 staging。Kuboard 表单里可能做不到这么细的表达式像 operator 的取值、多个表达式之间的与或关系很多场景下直接切到 YAML 编辑模式会更顺手。2.3 容忍Toleration一定要和污点一起看节点亲和性与 nodeSelector 是从 Pod 的角度表达“我想去哪些节点”而污点是从节点的角度表达“我不欢迎哪些 Pod”。如果某个节点被打上了污点默认情况下所有不声明容忍的 Pod 都不会被调度上去。常见的单机独享场景是这么用的给节点打一个dedicateddb:NoSchedule的污点这时候只有声明了对这个污点有容忍的 Pod 才能调度上去相当于给数据库服务划了一块专属地盘。在 Kuboard 表单里添加容忍时一般需要填 key、value、operator 和 effect。如果只需要匹配 key 而不关心值可以选 Exists 操作符并且不填 value。对应 YAMLtolerations: - key: dedicated operator: Equal value: db effect: NoSchedule2.4 nodeName在 YAML 编辑模式里直接指定单节点nodeName 属于最暴力的强制指定调度器会直接忽略其他条件。Kuboard 表单里通常不会专门给这个字段留一个输入框但你可以在 YAML 编辑模式下直接加到 Pod 模板里spec: template: spec: nodeName: node-02我对 nodeName 的态度一直比较谨慎。它适合排障或者临时迁移时快速定位但不适合作为长期调度策略。一旦这台节点宕机或者维护服务既不会自动迁移到别的节点也没法换一台机器所有 Pod 都会卡在 Pending 状态。除非你能明确承担这个后果否则不要在生产环境长期用。3. 实操把服务从磁盘告警节点迁移到目标节点前面原理说了一大堆现在进入完整的实操环节。我把一个真实的迁移场景拆开带你一步步走完顺带把过程中该看的、该想的地方都指出来。场景是这样的某公司的一套 Kubernetes 集群有四台节点分别叫 node-01、node-02、node-03、node-04。其中 node-01 最近磁盘使用率到了 90% 以上上面跑着一个名叫 order-service 的微服务日志和本地缓存都在这台机器上磁盘告警以后 IO 明显变慢。运维打算把这个服务迁移到 node-02node-02 的标签比较干净而且之前就规划了envprod和diskssd。3.1 先确认目标节点的标签不要凭想象直接填标签先看目标节点到底有什么标签。在 Kuboard 的集群节点页面可以看到每个节点的 Labels也可以直接用 kubectlkubectl get nodes node-02 --show-labels假设输出里有下面这两行标签diskssd envprod那我们的 nodeSelector 就有据可依了。如果目标节点连一个规划好的标签都没有那就先给节点补标签补完再继续。在 Kuboard 里可以打开节点详情在标签区域直接添加kubectl 的方法是kubectl label nodes node-02 diskssd envprod这一步不要跳过去。我在实际操练中见过太多人直接在界面里填disk: ssd但节点上标签叫disk-type: sata结果 Pod 就永远 Pending 了。一切以节点上的真实标签为准。3.2 检查当前 Pod 跑在哪个节点改之前至少要留个底知道现在服务跑在哪、Pod 名是什么。用 kubectl 看kubectl get pods -n production -l apporder-service -o wide-o wide会多显示一列 NODE记录下旧 Pod 所在节点。这一步主要是为了迁移完成以后做对比也方便回滚。3.3 在 Kuboard 中修改工作负载的调度策略进入 Kuboard找到 production 命名空间下面的 order-service 工作负载点“编辑”。在调度策略区域选择“节点选择”添加两组键值env proddisk ssd如果界面上没有明显的节点选择区块就切到 YAML 编辑模式。Kuboard 的 YAML 编辑器会把工作负载的完整定义显示出来找到spec.template.spec这一层添加nodeSelector: env: prod disk: ssd保存后 Kuboard 会提交这次更改Deployment 控制器检测到 Pod 模板发生了变化就会触发滚动更新创建新的 ReplicaSet。3.4 调整更新策略尽量别中断服务如果 order-service 有多个副本先看一眼工作负载的更新策略。在 Kuboard 的编辑页面里找到“更新策略”或strategy相关配置推荐把滚动更新参数设置成strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0解释一下这两个参数。maxSurge 表示滚动更新时允许超出期望副本数多少个 PodmaxUnavailable 表示允许有多少个 Pod 暂时不可用。maxSurge1, maxUnavailable0的含义是先多建一个 Pod等新的 Pod 就绪以后再关掉一个旧 Pod。这样服务在整个迁移过程中始终有可用副本是线上业务最稳妥的组合。但如果这个服务只有 1 个副本那 maxUnavailable0 会有一个问题新 Pod 必须成功起来旧的才会被回收而新 Pod 需要空闲资源。如果集群资源已经被占满新 Pod 会一直 Pending滚动更新就卡住了。对单副本服务比较实际的做法有两种要么先临时把副本数提到 2迁移完再缩回 1要么接受短暂的不可用设置 maxUnavailable1允许先关旧 Pod 再开新 Pod。我在迁移核心服务的时候如果资源充足一定先用前者。3.5 保存并观察滚动更新点击保存以后不要看一眼界面就离开。Kuboard 的工作负载详情页一般会实时刷出 Pod 列表你会看到一个新 Pending 的 Pod 被创建。点击进入它的详情看“事件”那一栏如果调度成功了事件里通常不会有 ErrorPod 会从 Pending 变成 ContainerCreating最后变成 Running。同时在终端也可以交叉验证kubectl get pods -n production -l apporder-service -o wide看到新 Pod 的 NODE 列显示 node-02并且旧 Pod 逐个被 Terminating最后只剩新的迁移就完成了。如果此时想看这个 Deployment 最终的调度约束可以用kubectl get deployment -n production order-service -o jsonpath{.spec.template.spec.nodeSelector}输出里应该能看到{disk:ssd,env:prod}这样的结构。3.6 迁移完成后想回滚怎么办迁移后如果发现新节点有问题想立刻回到之前的节点只需要在 Kuboard 里把 nodeSelector 改回原来的标签组合或者直接恢复到修改前的 YAML 版本再保存一次Deployment 又会触发一轮新更新把 Pod 迁回去。如果你用 nodeName 做的迁移回滚同样是把 nodeName 删掉换成原来的 nodeSelector。这里记得不要把旧的 Pod 手动删掉来“触发”回滚直接改模板就够了。手动删除 Pod 在工作负载的 ReplicaSet 管理下会再拉起一个但你改的模板没变拉起来还是在新节点白忙一场。4. Pending 是常态常见问题与排查技巧实录改完节点配置以后最有代表性的报错就是 Pod 卡在 Pending。有时候一眼就能看出原因有时候折腾半天还不知道哪一步出了问题。我把平时排查过的高频问题整理成一个速查表里面全是实际踩过的坑。现象常见原因排查方法解决办法Pod 一直 Pending事件提示 0/1 nodes are available节点标签不匹配或节点有污点没被容忍kubectl describe pod 看 Events 里的提示核对节点真实标签补标签或补容忍多个节点可用但新 Pod 就是起不来资源不足比如 CPU、内存、端口被占满kubectl describe node 看 Allocated resources扩节点、缩其他工作负载、清缓存改完 nodeSelector旧 Pod 没被替换改的位置不对可能只改了副本数或旧 ReplicaSetkubectl get rs 看是否有新 ReplicaSet确认改的是 template.spec 里的调度配置事件提示 node(s) had taint无法调度目标节点有 NoSchedule 污点查看节点 Taints给 Pod 加对应容忍或移除污点新 Pod 反复 CrashLoopBackOff新节点缺少数据、配置文件或本地路径不存在kubectl logs 看容器启动报错确认新节点的挂载、目录权限、配置都齐全滚动更新卡住旧 Pod 不退出maxUnavailable0 且新 Pod 未就绪查看 ReplicaSet 的 Desired/Created 数调整 maxSurge/maxUnavailable或手动扩容副本有状态服务迁走后数据查询异常用了 LocalPV 或 EmptyDir数据只留在旧节点查看 PVC 的 PV 是否绑定具体节点改用共享存储或迁移前先备份恢复排查的时候最核心的命令就是kubectl describe pod。它会在 Events 部分直接告诉你调度器到底因为什么没有调度成功比如kubectl describe pod -n production order-service-xxxxx如果看到类似node(s) didnt match Pods node affinity/selector问题基本锁定在标签不匹配上看到node(s) had taint就是缺容忍或污点太硬看到Insufficient cpu/memory就是资源不够和节点选择没关系。还有一个小技巧用 kubectl 直接看 Pod 当前的调度约束长什么样可以快速确认改动有没有生效。kubectl get pod -n production -o jsonpath{.spec.nodeSelector} kubectl get pod -n production -o jsonpath{.spec.affinity}如果发现 nodeSelector 根本没出现在你已经改好的 Pod 里说明你改的可能不是当前正在用的模板。比如有的 Deployment 底下挂着多个 ReplicaSet你编辑的时候不小心选了这个 ReplicaSet 而不是工作负载本身保存以后只是改了一个孤立的历史版本没有产生新的滚动更新。遇到这种情况去 Kuboard 的版本历史或kubectl get rs里看是不是新 ReplicaSet 已经被创建了。5. 比改一次更重要的节点标签和污点规范化设计如果你只是临时救火那前面几节已经够用了。但如果你在一个稍微正式点的环境里长期负责集群我强烈建议不要每次都手动去填标签而是先把节点标签和污点的规范定下来。做一次迁移很简单可要是每个服务迁移都靠临时拼标签你的集群迟早会变成一个谁都不敢碰的黑盒子。先想清楚你这套集群里节点有哪些差异。常见维度有这么几个环境prod、staging、dev、机型cpu 强、内存大、GPU 卡、磁盘ssd、hdd、本地盘、地域或机柜zone-a、zone-b。我见过比较顺手的做法是统一用role或node-type这一类前缀清晰的标签不要临时起名。用途标签键标签值示例环境隔离envprod、staging、dev机型分类machine-typestandard、highcpu、memory、gpu磁盘类型disk-typessd、hdd归属区域zonezone-a、zone-b标签定好以后再用 nodeSelector 或者节点亲和性把服务归类。比如凡是需要 GPU 的训练任务统一配machine-type: gpu而不是某一次迁移临时指定到 node-03。这样以后 node-03 出问题你可以加一台同样标签的新节点把这个标签打上Pod 会自动流过去不需要改任何工作负载。如果说标签做的是“引导”污点做的则是“隔离”。举个例子数据库服务如果和其他业务混跑很容易因为邻居 Pod 的流量波动或者资源抢占影响稳定性。你可以给数据库专用节点打污点kubectl taint nodes node-03 dedicateddb:NoSchedule这样默认所有 Pod 都不会调度到这个节点。只有数据库自己的工作负载在 Kuboard 里添加了对dedicateddb:NoSchedule的容忍别的服务即使加了 nodeSelector 指定这台节点也上不去因为不满足污点条件。这一个组合拳的效果比单纯加标签要好得多。还有一点属于经验之谈不要在 YAML 里长期保留nodeName。为了临时迁移指定一两次 nodeName 没问题但保留在长期运行的配置里就意味着你放弃了调度器的自动容错能力。节点维护、宕机、扩容缩容任何一个变动都会让这个服务极其被动。我见过一个案例某服务绑死了 nodeName后来那台机器要下线检修运维整整折腾了一晚上因为依赖这台机器的服务不是一个而是十几个。所以能写进规范的就不要硬编码能打标签的就不要写单一节点名。6. 滚动更新参数与迁移时服务不中断最后一层保护网前面这么一顿操作服务的节点选择条件已经生效新 Pod 会逐步替换旧 Pod。但如果滚动更新参数配置得不合理整个迁移过程依然可能把服务打断。这一节我把滚动更新的几个关键参数讲透算是迁移节点的最后一层保护网。滚动更新有两种极端情况需要考虑。第一种是把maxUnavailable设置得太大比如maxUnavailable: 1而副本总数只有 1 个那更新开始的一瞬间旧 Pod 就会被杀掉新 Pod 还在创建这段时间服务整体不可用。第二种是maxUnavailable: 0, maxSurge: 0这种配置语义上不允许任何额外 Pod 和任何暂时不可用等于禁止滚动更新控制器会卡住因为新 Pod 起不来的话旧 Pod 永远不能被淘汰。对多副本的线上服务我通常用这个配置strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0它的含义是新 Pod 先起来一个等它 Ready 之后再淘汰一个旧 Pod以此类推。整个迁移过程里可用的副本数始终保持在原值服务不会出现空窗期。对单副本服务这个配置就不太合适了。要么临时把副本数提升到 2让滚动更新有喘息空间迁移完再手动缩回 1要么接受短暂中断把maxUnavailable设为 1先把旧 Pod 释放掉释放出来的资源再起新 Pod。如果你服务的启动时间特别长比如要预加载缓存、做健康检查我建议用临时扩容的方式否则即使一个 Pod 可以中断业务方也会抱怨那几秒的闪断。还有一类服务在迁移时要格外小心StatefulSet。StatefulSet 的滚动更新是顺序进行的Pod 名里的序号从 0 开始挨个替换而且每个 Pod 使用的 PVC 和节点位置有强关联。如果你用的是节点上的本地盘做存储迁移 StatefulSet 特别容易把数据留在旧节点上新 Pod 起来以后找不到数据。这种情况一定要先关注存储层把 PVC 背后的 PV 存储迁移到共享存储或新的持久化方案再谈节点调度。否则只把 Pod 调度到新节点存储还留在旧节点反而是更大的事故。迁移过程中还可以盯着 Kuboard 里的工作负载事件。如果你看到更新进度卡住多半就是 RollingUpdate 策略和资源余量之间的矛盾。不要急着去手动删除某个 Pod先把更新策略调宽松一点比如临时把 maxSurge 改成 2等新 Pod 都起来了再改回来。我个人在实际操作中的体会是改服务运行节点这件事技术上不难难的是调度条件是不是有依据、滚动更新会不会打断业务、以及有没有预留回滚的余地。所以我现在每次迁移前都会先在脑子里过一遍这三个问题目标节点的标签确认了吗更新策略和服务副本数是否匹配如果新节点有问题原来的配置怎么恢复把这三件事想明白Kuboard 里那几次点击就只是最后执行的动作而已。如果你也经常要在集群里做这类运维操作建议顺手把工作负载的更新策略和节点标签规范一起检查一遍别等到需要迁移的时候才发现标签没规划、策略是默认值。这些平时不起眼的配置恰恰是线上变更最可靠的保障。