多集群环境维护久了,一定不会对 fleet-agent 的Pending-Upgrade状态感到陌生。某个集群所有业务都正常,日志也没有明显报错,但控制面里那个 agent 就是一直卡在待升级状态,怎么等都不往下走。这个状态不产生告警,不打断业务,唯一的感知是版本发布流程在最后一步卡住,发布负责人开始四处找人问“为什么这个集群没升上去”。
这篇文章就把这个问题的成因、排查链路、处理手段和预防措施一次讲透。内容针对的读者是正在维护多集群 Fleet 体系、经常要做集群升级和版本同步的运维工程师,也适合刚接手 fleet-agent 运维、对状态流转还不熟的开发者。
1. 一次卡住的升级:现象、影响和它被忽视的原因
1.1 故障现场是什么样的
先说最常见的现场。控制面这边已经发布了最新的版本,绝大部分集群都陆续完成了升级,状态从Pending-Upgrade变成Upgrading,最后回到Active。只有一两个集群始终停在Pending-Upgrade,你刷新页面也好,等半天也好,它纹丝不动。
这时候如果点开集群详情,业务工作负载一切正常,agent 也显示在线,心跳偶尔还在更新。问题看起来像是“只是升级这一步没走完”,哪些 Pod 没起来?看不到。哪里报错了?控制面上没有红字。于是很多人第一反应是“再等等,可能是升级任务太多,排队排到后面了”,然后就去处理别的事情,几个小时后再回来看,状态还是一样。
这种状态最麻烦的地方就在这里:它没有"失败"那么显眼,却实实在在地让升级流程中断。如果这个集群是生产集群,意味着它长期停留在旧版本上,可能缺失新版本里的关键变更和安全修复,这个风险不会立刻爆发,但会一直累积。
1.2 为什么特别容易被忽略
从监控角度讲,Pending-Upgrade通常不会触发告警。监控脚本一般盯着 CPU、内存、Pod 状态、接口返码,很少有人专门监控“agent 的期望版本和实际版本是否一致”。等到下一次发版或者做安全扫描时,才发现这个集群已经落后了好几个版本版本,中间的差异已经大到不敢直接跨版本升级了。
还有一个容易被忽略的点:控制面认为 agent 在线,不代表 agent 能正常消费新的期望状态。agent 上报心跳,只说明进程还在运行,通信链路还没完全断掉,但它内部可能陷在某个错误循环里,或者根本没有收到新的升级描述。你看到“在线”这个绿灯,就很容易误判成“agent 没问题,只是升级比较慢”。
这类问题我处理过多次,基本规律是:发布流程越靠后、集群规模越大,越容易出现个别 agent 卡在Pending-Upgrade。它不可怕,但不能靠“等等看”解决,得靠一套固定的排查链路去定位。
2. Pending-Upgrade 不是“排队中”:状态流转和真正卡住的环节
2.1 agent 升级的基本流转
要理解Pending-Upgrade,先得知道正常升级是怎么流转的。控制面发布新版本后,会给对应集群生成一个新的期望状态,我习惯把它叫作 revision,也就是版本修订号。这个 revision 通过下发通道传给 agent,agent 拿到后进入Pending-Upgrade,表示“我已经知道有新版本,下一步准备应用”。
紧接着 agent 会拉取具体的资源清单,把它们应用到目标集群上。应用过程中状态变成Upgrading,应用完成后,agent 上报自己的实际版本号,让控制面确认已经达到期望版本,状态回到Active,一次升级闭环结束。
整个过程有点像手机收到系统更新推送:推送到了,状态栏出现“有新版本待安装”,你点击下载并安装,进度条跑完,手机重启,最终系统版本变成最新。Pending-Upgrade对应的就是“推送已经到了,但还没有开始下载安装”这个阶段。正常情况下这个状态只会短暂停留,长时间停留就说明某个环节断了。
2.2 卡住的位置通常在哪个环节
根据我处理过的案例,长时间卡在Pending-Upgrade,问题不外乎出在下面几个位置:
第一,agent 没有收到新的 revision。可能是控制面到 agent 的下发通道断了,或者 agent 内部用于接收变更的循环停掉了。网络策略变动、服务端地址变更、agent 进程僵死,都会导致这种情况。特征是心跳可能正常,但 revision 数据一直没到。
第二,agent 收到了 revision,但无法解析或无法应用。升级清单里引用了某个不存在的配置项,或者模板渲染需要用到的密钥缺失,agent 每次尝试都在同一个地方失败,于是不断重试,状态却始终停在原地。
第三,agent 尝试应用资源清单时被拒绝。比较典型的是权限不足或者资源配额不够。agent 使用的服务账号没有权限创建某个新类型资源,或者目标命名空间的资源配额已经打满,应用动作做不下去。
第四,agent 版本和控制面版本差距太大。控制面生成的 revision 格式较新,旧版本的 agent 读不懂,或者需要 agent 先完成自身升级才能消费新的期望状态,而自动升级机制又没触发。这种情况最隐蔽,因为日志里未必有明显报错。
2.3 别把几个相似状态混在一起
在排查之前,先把状态含义理清楚,否则方向很容易跑偏。Pending-Upgrade和OutOfSync不一样,OutOfSync表示实际运行的资源和期望状态不一致,但 agent 可能已经尝试过同步甚至同步失败;Pending-Upgrade更像是“同步这件事还没开始”。Failed则更直接,表示应用过程报错了,通常会有错误信息可以看。
说得再直白一点:Pending-Upgrade是“卡住”,OutOfSync是“有偏差”,Failed是“失败”。它们的处理思路完全不同,排查日志时关注的关键字也不同。看到Failed,先找报错原因;看到OutOfSync,检查实际资源为什么偏离;看到Pending-Upgrade,重点看 agent 为什么没有启动升级流程。
3. 锁定根因的完整排查链路:日志、状态对象、注册信息
3.1 第一步:从 agent 日志里找“最后一句话”
排查任何 agent 类问题,我的第一个动作永远是看日志,不是看控制面界面。界面是给用户看的,简化过、聚合过,可能丢掉了真正关键的报错。日志里才有原始信息。
先找到 fleet-agent 所在命名空间,看它的部署对象和日志:
kubectl -n <agent命名空间> get deployment | grep fleet-agent kubectl -n <agent命名空间> logs deployment/fleet-agent --tail=200如果日志量太大,可以先看最近一段,寻找反复出现的相同内容。不同日志关键字指向的问题方向差别很大,我自己整理了一个简单对照:
| 日志里反复出现的关键内容 | 大概率指向 |
|---|---|
context deadline exceeded、connection refused | agent 到控制面的通信链路有问题 |
forbidden、unauthorized | agent 使用的服务账号权限不足 |
| 资源配额相关报错 | 目标命名空间资源配额耗尽 |
configmap not found、secret not found | 升级清单引用了不存在的配置 |
反复出现retry、backoff | agent 内部循环陷入重试,需要看具体错误原因 |
看日志这一步能帮你快速决定后面往哪个方向深挖。日志干净不代表没有问题,但日志有明确报错,就不要急着重启,先把报错对应的根因解决掉,否则重启一百遍也只是把同一个错误重放一百遍。
3.2 第二步:对比期望修订版与实际修订版
日志没问题或者线索不够时,第二步去看状态对象。agent 会暴露自己当前的状态描述,里面最关键的是两个字段:期望修订版和实际修订版。我习惯直接对比它们是否一致。
kubectl -n <agent命名空间> get <agent状态资源> -o yaml正常状态和卡住状态的字段差异,大致是这样的:
| 检查项 | 正常状态 | 卡在 Pending-Upgrade |
|---|---|---|
| 期望修订版 | 等于目标版本 | 已经更新到最新,甚至高过实际版本 |
| 实际修订版 | 等于期望修订版 | 落后于期望修订版 |
| 最近心跳/上报时间 | 新鲜 | 可能新鲜,也可能已经过期 |
| 状态里的错误描述 | 无 | 可能有,也可能是完全空白的 |
如果期望修订版和实际修订版不一致,且这个不一致状态持续了很久,基本可以确认 agent 没有完成对新版本的消费。这时候结合日志就能缩小问题范围:日志里有网络错误,优先查网络;日志里空白,优先怀疑内部循环或者版本兼容问题。
3.3 第三步:检查注册信息与心跳
如果前两步还没能定位,就要看注册信息和心跳。
agent 和控制面之间是靠注册信息互相识别的,注册信息里通常包括集群的唯一标识、agent 的唯一标识、最近一次心跳时间、上报地址等。如果发现心跳时间已经非常久远,说明 agent 和控制面的管线其实早就断了,只是进程还在本地运行着,看起来像“在线”。
另一个值得检查的点是 agent 的唯一标识是否发生变化。有些情况下,集群里的 agent 被意外重建后,生成了新的唯一标识,控制面侧还拿着旧的标识去匹配,结果两边“互相不认识”,新期望状态无法投递给正确对象。这种情况日志往往没有明显报错,也不会产生频繁的失败记录,看起来就是“agent 在线,但升级永远不触发”。
注册信息这一层容易被忽略,因为它不在普通日志里,要打开状态对象或注册信息相关资源才能看到。但很多“查不出原因的卡住”,最后都落在这里。
3.4 一条典型的排查路径
我举一个处理过的典型路径,供你参考。当时某个集群升级卡在Pending-Upgrade,控制面显示期望版本已经更新,agent 心跳正常。第一步看日志,发现里面反复出现context deadline exceeded,说明网络层有问题。第二步排查连接,发现 agent 到控制面新迁移后的域名某条链路不通,被防火墙策略拦住了。放行之后重启了一次 agent,它重新建立连接,状态从Pending-Upgrade进入Upgrading,最后回到Active。
另一类常见路径是日志一片干净,心跳也新鲜,但版本就是不动。这种我一般先手动触发一次重新同步,如果触发后立刻恢复,说明 agent 的回收循环因为某种原因提前退出了,触发一下相当于把它“叫醒”。如果触发后没有反应,再考虑重启进程或者清理状态对象。
4. 让 agent 重新走完升级:四类解法与选择依据
4.1 第一种:温和触发一次重新同步
处理Pending-Upgrade,我强烈建议从风险最低的手段开始,而不是直接重启或者删除对象。第一步可以先尝试手动触发一次重新同步,让 agent 重新读一遍期望状态。
具体做法因系统版本而异。比较通用的思路是找到状态对象里控制面写入的升级标记或注解,修改或删除它,让 agent 认为“出现了新的期望状态”,从而进入重新协调流程。实际操作前可以先备份当前状态对象:
kubectl -n <agent命名空间> get <agent状态资源> <agent名称> -o yaml > agent-backup.yaml备份之后再做修改,做错了也能恢复。温和触发的好处是几乎无副作用,最多只会让 agent 多跑一次协调循环,不会影响集群里已有的业务资源。我处理过不少“日志干净但版本不动”的情况,温和触发之后几秒内版本就开始前进了,根本不需要走重启这一步。
4.2 第二种:重启 agent 进程
温和触发无效,或者日志显示 agent 已经陷入某种本地僵死状态时,可以重启 agent 进程。最常见的做法是滚动重启它的部署:
kubectl -n <agent命名空间> rollout restart deployment/fleet-agent重启的意义不是“碰碰运气”,而是让 agent 做三件事:重新建立到控制面的连接、重新读取注册信息、清理内存里可能已经错乱的缓存状态。很多Pending-Upgrade的根因是 agent 进程本身进入了错误循环,但还没有完全崩溃,日志不会报错,状态也不更新。这种情况下,一次干净的重启就能恢复。
重启前记得确认 agent 是 Deployment 管理的,并且有足够的副本或者重建策略来保证重启过程不中断。重启后观察它的日志,看是否出现了新的连接成功记录,然后核对状态是否从Pending-Upgrade开始变化。
4.3 第三种:清理卡住的状态对象
如果重启之后状态还是原样,就需要考虑清理卡住的状态对象,让控制面重新计算并重建它。这是风险最高的一步,操作前必须想清楚。
我强调一下:要清理的是 agent 上报状态的资源对象,不是 agent 的部署对象,也不是注册信息对象。删错对象后果很严重,轻则 agent 重新注册导致纳管关系短暂中断,重则需要重新处理集群纳管信息。实际操作前,先备份当前对象内容,并且确认你认识的“对象名称”对应的就是状态对象,而不是注册对象。
kubectl -n <agent命名空间> get <agent状态资源> <agent名称> -o yaml > agent-state-backup.yaml kubectl -n <agent命名空间> delete <agent状态资源> <agent名称>删除后,控制面会根据当前期望状态重新生成一个新的状态对象,agent 会重新读取并进入升级流程。清理操作适用于“状态对象本身已经成了死数据”的情况,比如里面的字段和控制面当前结构不兼容,或是在历史操作中残留了无法自行纠正的脏数据。
4.4 第四种:版本对齐与授权修复
最后一种情况比较特殊:不是 agent 不干活,而是它“干不动”。如果 agent 版本明显落后于控制面,或者升级清单涉及的新资源和 agent 当前的权限不匹配,再多的重启和清理也解决不了根本问题。
版本对齐的处理思路是手动把 agent 镜像升级到与控制面兼容的版本,或者先调整控制面的发布策略,让旧 agent 能够平滑过渡到中间版本,再逐步升级到目标版本。跨版本差距过大的时候不要指望一次升级到位,分阶段推进更稳妥。
授权修复则要检查 agent 使用的服务账号权限范围是否覆盖了升级清单涉及的所有资源类型和命名空间。有时新版本增加了一种新的自定义资源,而 agent 的权限清单没有跟着更新,应用动作就会在权限校验这一步被打回。日志里会出现明显的forbidden或unauthorized关键字,处理起来反倒最直接,把缺失的权限补上就行。
4.5 解法选择的决策参考
为了让你在实际操作中快速决策,我把现场特征和解法选择整理成一个对照表:
| 现场特征 | 优先尝试 | 说明 |
|---|---|---|
| 日志有网络错误 | 先修网络,再重启 agent | 否则重启后还是会卡住 |
| 日志有权限/配额报错 | 修权限/配额,再触发同步 | 从根因入手,不要只治标 |
| 日志干净,版本不动 | 温和触发重新同步 | 大概率是协调循环没跑起来 |
| 触发后无变化 | 重启 agent 进程 | 清理本地僵死状态 |
| 重启后仍无变化 | 清理状态对象 | 需要先备份并确认对象类型 |
| 版本差距过大 | 手动对齐版本 | 先兼容再升级,不要硬跳 |
这个决策表不是绝对真理,但它至少能帮你在现场少走弯路。我自己的经验是:优先处理能解释问题的环节,再考虑用重启之类的手段兜底。跳过根因直接重启,常常事倍功半。
5. 日常运维的防呆清单:别等状态卡住了再来救火
5.1 我踩过的高频误操作
处理这类问题几次之后,我总结出几个特别容易踩的坑,分享出来,希望你不用再经历一遍。
第一个坑是只重启控制面不重启 agent。Pending-Upgrade出问题的一侧在 agent,控制面重启多少次都不会让 agent 往前走。我在早期犯过这个错,看控制面组件重启后状态刷了一下,以为好了,结果过一会儿还是老样子。
第二个坑是删错对象。清理状态对象时,没有先确认对象类型就执行删除,结果把注册信息一起删了。agent 重新注册后,集群纳管信息要重新同步,升级问题没解决,反而多出一个新问题。从那以后我坚持“先备份,再删除,删之前看好对象类型”。
第三个坑是忽视时间不同步。agent 所在集群如果时钟和控制面差异过大,状态对象里的时间戳会出现偏差,导致控制面判断升级是否超时时产生错误。看起来像卡住,实际上是对不上时间。排查时顺手看一眼两边服务器时间,往往能省去大量无效操作。
第四个坑是一遇到多个 agent 同时卡住就先怀疑网络。真实情况常常是发布的新版本里包含了某个资源定义变更,所有旧版本 agent 都无法解析,于是集体停在Pending-Upgrade。此时逐个修网络毫无意义,正确做法是先看新版本的格式兼容性,再统一升级 agent。
5.2 建议纳入监控的信号
Pending-Upgrade不应该靠人肉巡检发现。几个关键信号建议尽早纳入监控体系:
期望修订版和实际修订版不一致的持续时间超过阈值,应该告警。这个信号最直接,但实现方式取决于你的监控系统是否能读取状态对象数据。能读到的,按对象字段做告警;读不到的,就定期跑脚本对比版本号。
agent 最近心跳时间一直不更新也要盯。它可能是进程僵死的前兆,比Pending-Upgrade出现得更早。还有 agent 的容器重启次数,如果频繁重启,说明进程可能一直在崩溃恢复,升级自然走不动。
另外每次发布完新版本之后,建议固定做一次全量状态巡检,把所有处于Pending-Upgrade的 agent 列出来看一眼。这是一个很便宜的动作,但能避免“发布完成好几天了才发现有一个集群掉队”的尴尬局面。
5.3 应急操作手册的一句话版本
如果要把整个处理流程压缩成一段可以贴在手边的应急口诀,我会写成这样:
先看日志,定位报错类型;再看状态,对比期望版本和实际版本;顺便检查心跳和注册信息;然后按风险从低到高处理,温和触发一次重新同步,不行就重启 agent,再不行就清理状态对象,最后重点排查版本兼容和授权问题。
我第一次处理Pending-Upgrade时来回折腾了大半天,后来养成每次升级后主动检查一遍所有 agent 状态的习惯,问题就很少再能藏住。对于这种状态,核心思路其实就一句话:它停得越久,越不能靠等待解决,越要尽快弄清楚它停在了哪个环节。把日志、状态、注册信息这三个抓手用好,大部分卡住问题都能在半小时内定位清楚。