ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

虚拟机重启后ArgoCD显示Pod状态异常?故障排查与恢复复盘

虚拟机重启后ArgoCD显示Pod状态异常?故障排查与恢复复盘 凌晨两点十五分手机连着震了五下。我眯着眼划开屏幕告警群里全是同一条规则的输出几个 ArgoCD Application 的状态变成了 Degraded / Unknown后面跟着两三行应用名。再往下翻智能运维助手的故障快照里有一个关键线索——集群里某台承载业务负载的虚拟机刚刚经历了重启节点上的 Pod 健康状态大面积异常。说实话看到“虚拟机重启”这几个字我的第一反应倒不是慌而是有点烦。因为这类故障的典型特征是表面上是 ArgoCD 在报错实际上是基础层某处没恢复干净。ArgoCD 充其量是个“显示器”真正出问题的往往是它背后的 API Server、kubelet、CNI 或者存储卷。这篇复盘就完整记录一下从凌晨接到告警到最终恢复我到底做了什么、查了什么、踩了什么以及事后又给集群加了哪些防护。1. 现象还原ArgoCD 的状态到底“坏”成了什么样1.1 那一屏告警和 UI 上的“问号”先明确一个前提ArgoCD 的健康状态不是随便画出来的。它里面的 Healthy、Degraded、Progressing、Unknown 这几种状态分别代表“应用资源正常”“有资源不健康”“正在滚动/等待中”和“控制器无法评估当前状态”。凌晨那会儿我打开 ArgoCD UI 看到的是非常难受的一幕一个业务应用的 Application 整体显示为 Degraded点进去之后资源列表里有两个 Pod 的参考不一样其中一个显示 Unknown不是 Running另一个日志采集的 DaemonSet 应用倒是显示 Healthy但它的节点分布也有异常还有一个带本地存储的应用处于 Progressing像卡住了一样一直转圈。这几种状态混在一起我当时就判断这不是一种原因导致的至少有两条故障线在交叉作用一条在“资源能不能起来”的层面另一条在“ArgoCD 能不能感知资源状态”的层面。1.2 先冷静一下kubectl 看真实世界告警归告警第一件事永远是绕过展示层看数据源。我直接切到工区跑了一组命令kubectl get nodes -o wide kubectl get pods -A -o wide | grep -E checkout|fluentd|redis结果很有意思真实世界里报错的 Pod 并没有消失。业务应用里有两个 Pod 显示为 ContainerCreating有一个 2/2 Running 但 IP 和节点都变了日志采集的 Pod 正常运行本地存储的那个 Pod 卡在 ContainerCreating事件里全是卷挂载相关的报错。也就是说API Server 知道这些 Pod 存在kubelet 也知道但 ArgoCD 这边却给不出正确的状态。至少这一步能确认Pod 不是真的“没状态”而是 ArgoCD 没拿到状态或者拿到了但没法对账。1.3 快速圈定影响面不要一上来就钻进某个 Pod 的细节里。我习惯先把所有相关应用的状态拉成一张表用命令批量导出对比argocd app list -o wide再结合 kubectl 的实际 Pod 列表我列了一张对账表应用/资源ArgoCD 显示kubectl 实际节点归属checkout-service DeploymentUnknown / OutOfSync2 个 ContainerCreatingnode-03checkout-service DeploymentUnknown1 个 Running 2/2已迁到 node-01fluentd DaemonSetHealthy1 个 Running 1/1node-03redis StatefulSetProgressing1 个 ContainerCreatingnode-03这张表一出来范围就收得很清楚了除了那个被重建到其他节点的 Pod剩下的异常 Pod 全部集中在 node-03。所以整个排查的焦点就是 node-03 这台虚拟机以及 ArgoCD 对这台节点相关应用的状态同步机制。2. 排查链路为什么 ArgoCD 会比 kubectl 慢半拍2.1 ArgoCD 的健康状态评估到底怎么来的给不熟悉 ArgoCD 的朋友打个比方ArgoCD 相当于 Git 和 Kubernetes 之间的“监理”它持续把 Git 里声明的期望状态和集群里正在运行的 live 资源做对比再基于一组健康规则生成报告。这套机制的底座是 informer / list-watchargocd-application-controller 会监听集群资源变化把结果缓存在内存和 Redis 里UI 和 CLI 读的其实都是这份缓存而不是每次都实时请求 API Server。所以当你看到 ArgoCD 显示 Unknown通常意味着控制器在最近的 reconcile 周期里没能成功获取到某个资源的最新状态。可能是 API Server 端限流可能是 watch 通道断了也可能是资源对象本身在它的缓存里已经“失联”。这不是业务故障而是“监理的眼睛被蒙住了”。2.2 节点重启时控制面偷偷干的三件事虚拟机重启期间Kubernetes 控制面并不是什么都不做。你如果只知道 Pod 会被重新调度那就太天真了。实际隐含着三件重要的事node 失联后kube-controller-manager 里的 NodeLifecycleController 会打标节点状态变成 NotReady。如果 NotReady 时间超过默认的pod-eviction-timeout通常 5 分钟控制面会把该节点上的 Pod 标记为驱逐对象。有 ReplicaSet、Deployment 管理的副本会在其他节点重建裸 Pod 会被直接丢弃。节点恢复后kubelet 重新注册节点状态变回 Ready。但注意节点 Ready 不代表节点上一切恢复CNI、存储客户端、kube-proxy 等组件可能还在初始化。我当时看了一眼时间线node-03 失联到恢复 Ready刚好超过了 5 分钟。这是一个非常重要的信号直接解释了一部分 Pod 为什么不是恢复而是被“重建”到了别的节点。2.3 把“检测不到”翻译成技术语言“检测不到部分 Pod 状态”在 ArgoCD 内部其实可以拆成几种完全不同的问题第一种Pod 已经被驱逐并重建但 ArgoCD 的 watch 事件漏掉了 Delete / Create缓存里还留着旧对象状态对不上第二种Pod 明明在 API Server 里但 ArgoCD 这一轮 reconcile 里 list 请求超时或限流资源压根没进到最新缓存显示 Unknown第三种Pod 一直卡在 ContainerCreating / PendingArgoCD 的 health check 等不到 Ready 条件于是状态停留在 Progressing 或 Degraded第四种application-controller 自己所在的 Pod 也被调度过错乱了导致所有应用的状态暂时不可信。这次故障前三者都有第四种是排查时才排除掉的。2.4 嫌疑清单把所有可能先摆在桌面上实操里我喜欢把推测做成一张嫌疑表排好优先级再逐项验证嫌疑点验证方式优先级Pod 被驱逐并重建换了 UID看 Pod 的 age、node、uid 变化高节点 kubelet / 网络未恢复看 node 条件和 kubelet 日志高CNI 插件未就绪导致 ContainerCreating看 Pod 事件kubectl describe pod中PV/PVC 挂载超时看 Pod 事件和存储组件状态中ArgoCD 缓存陈旧或 watch 断流看 controller 日志hard refresh 验证中controller 本身被干扰查看 argocd namespace 下 Pod 分布低这张表不会直接告诉你答案但它能帮你把“慌乱”变成“逐步排除”。3. 顺着虚拟机重启的痕迹深挖从 kubelet 到 CNI 到存储卷3.1 节点回来了没有kubelet 的启动日志说明一切先看节点状态变化kubectl get events -A --sort-by.lastTimestamp | tail -30时间线的确和告警对上了node-03 在午夜 23:50 左右进入 NotReady23:57 左右控制面开始驱逐00:07 节点重新 Ready。这里有个细节node Ready 之后我立刻跑kubectl describe node node-03看到KubeletReady条件为 True但 NetworkUnavailable 条件一度也出现了异常波折。这基本说明kubelet 本身醒了但它所在的网络环境还没完全恢复。再深入看 kubelet 日志我用的是 journaldjournalctl -u kubelet --since 2024-12-20 23:50:00 --until 2024-12-21 00:15:00 | grep -E error|fail|timeout | head -50里面频繁出现failed to ensure that the pod: xxx cgroups exist和network plugin is not ready之类的报错。前者是容器运行时还没就绪后者是 CNI 的问题。这解释了两个 ContainerCreating 中的部分原因。3.2 CNI 没就绪的典型症状CNI 没就绪时Pod 并不会直接失败而是停在 ContainerCreating事件里反复出现类似AddNetworkList failed to set up pod sandbox的报错。因为容器运行时把沙箱建出来了但网络插件没给它分配 IP、没把 veth 接上所以 Pod 永远到不了 Ready。我确认了一下集群的 CNI 是 Calico。Calico 的 calico-node 是 DaemonSet正常情况下每台节点一个 Pod。但 node-03 重启时calico-node 自己也经历过容器重启它的依赖比如 BGP peer、隧道设备恢复得比 kubelet 慢。于是 Pod 死了网络组件还半死不活。这种情况下比较直接的处置是删掉该节点的 calico-node Pod 让它重建kubectl delete pod -n kube-system calico-node-xxxxx等它重新起来再观察异常 Pod 的事件是否消失。经验是虚拟机重启之后先把 CNI DaemonSet 的 Pod 状态过一遍别急着查业务。3.3 存储卷挂载超时如何拖住整批 Pod真正让那批 ContainerCreating 迟迟不恢复的其实是存储。我们集群里有一部分业务用了 NFS 类型的 PV而 NFS 服务端恰恰就部署在 node-03 这台虚拟机上。虚拟机一重启NFS 服务恢复比 kubelet 慢kubelet 启动后立刻尝试挂载 NFS 卷结果服务端口还没监听mount 一直卡住。用kubectl describe pod redis-0能看到一串类似MountVolume.MountDevice failed ... context deadline exceeded的事件。这种卡住不是立刻失败而是反复重试看起来就像“一直没起来”。对于有状态服务这类问题更要小心如果存储服务没恢复你原地删 Pod 也没用它还是会卡在挂载阶段。正确的顺序是先确认 NFS 服务起来了再让 Pod 恢复。3.4 为什么偏偏是“部分”Pod 出问题回到根子上为什么是“部分”而不是全部因为我面前其实有三类命运完全不同的 Pod第一类无状态、无特殊存储、没被驱逐的 Pod。虚拟机恢复后kubelet 把容器重新拉起来很快就能 RunningArgoCD 也同步得很正常第二类依赖 CNI 或 NFS 存储的 Pod。节点虽恢复但网络或存储没恢复它们卡在 ContainerCreatingArgoCD 显示 Progressing 或 Degraded第三类失联超过 5 分钟被控制器驱逐的 Pod。它们已经被重建到了别的节点但 ArgoCD 没第一时间感知到新对象于是显示 Unknown。三件事叠在一起UI 上就是“部分 Pod 检测不到”的观感。对这就是个多因叠加的假象而不是单一故障。4. 根因落定双层“陈旧状态”互相叠加的现场4.1 控制面驱逐记录节点失联的几分钟发生了什么我先调出驱逐相关的事件信息kubectl get events -A | grep -i evict在事件里能看到节点标记MarkingForEviction和 Pod 的DeletedPod相关条目。由于 node-03 失联时间超过了pod-eviction-timeout控制面认为节点已经死透于是把该节点上可重建的副本全部驱逐并调往其他节点。这完全符合 Kubernetes 的设计节点失联超时后不管后面是不是会恢复先保证工作负载可用性。但坏事就坏在这个“先保证”上等节点真的恢复原来被驱逐的 Pod 已经带着旧的身份信息不再属于 node-03而 ArgoCD 的控制器在驱逐发生的那几分钟里可能正好和 API Server 断开了 watch 连接少了几个更新事件。4.2 ArgoCD 侧watch 断流与 stale cache 的碰撞排查 ArgoCD 侧的问题我看了 controller 的日志kubectl logs -n argocd deployment/argocd-application-controller --tail200 | grep -E error|timeout|watch日志里有不少context deadline exceeded、failed to list resources以及watch channel closed的记录。这说明在 node-03 虚拟机和集群控制面短暂失联期间argocd-application-controller 到 API Server 的连接也出现过中断。informer 重新建立连接之后并没有把中断期间的所有事件都补回来——它不是可靠的变更日志它是“尽力而为”的增量同步。于是 ArgoCD 的内存在那个时刻存了一份“缺斤少两”的集群状态它以为某个 app 底下还是旧 Pod而真实世界里那个 Pod 已经换了 UID 跑到别的节点上了。4.3 重建的 Pod 换了 UID缓存却还在惦记旧 UID这是整条链路上最“神秘”也最关键的一环。Kubernetes 里资源对象的唯一标识是metadata.uid而不是名字。被驱逐重建的新 Pod哪怕名字没变UID 一定变了。ArgoCD 的资源缓存会用 group/kind/namespace/name 做索引但在内部对账逻辑里会关联 UID、resourceVersion 等细节。当缓存里的旧对象和新对象发生碰撞时控制器无法简单地把健康状况映射过去表现就是要么这个资源在 UI 里状态是 Unknown要么干脆从 resources 列表里消失。这就是你看到“部分 Pod 状态检测不到”的直接原因。Hard refresh 之所以有效就是因为它会强制绕过缓存重新从 API Server 全量拉取 live 状态用新 UID 覆盖旧引用把缓存“洗干净”。4.4 一句话总结根因这次故障的直接链条是虚拟机重启时间超过控制面驱逐阈值部分 Pod 被强制重建并更换 UID同时 ArgoCD controller 的 watch 连接在节点失联期间断流导致它保留了旧 UID 的陈旧缓存CNI 和 NFS 服务恢复缓慢又让另一批 Pod 的 Ready 过程被拖长。三层原因叠加最终表现为“重启虚拟机后 ArgoCD 检测不到部分 Pod 状态”。5. 解围操作实录从恢复业务到让 ArgoCD 重新“看见”5.1 当下第一要务先把 Pod 拉起来这种时候不能上来就重启 ArgoCD controller。先把底层恢复等待或者手动重建 calico-node 相关 Pod确认节点网络正常确认 NFS 服务进程起来showmount -e能看到导出目录对于还卡在 ContainerCreating 的 Pod删除后让其重新调度kubectl delete pod -n checkout checkout-service-xxxx kubectl delete pod -n redis redis-0这里提醒一下对 StatefulSet 的 Pod删除前先确认存储已经可挂载否则它会原地再卡一轮。我个人的习惯是删除之前先看一眼 PVC 的状态和存储提供方的情况不要盲目先删后查。5.2 等 Pod Ready 后别急着看 UI先 hard refreshPod 恢复 Ready 后ArgoCD UI 大概率还是一副“死不悔改”的样子。这不奇怪因为 ArgoCD 的缓存里还存着旧状态。这时候普通 Refresh 是不够的它只重新计算 sync 差异并不会重新列举所有 live 资源。正确的姿势是 hard refreshargocd app get checkout-service --hard-refresh或者在 UI 右上角点 HARD REFRESH 按钮。这个动作会强制 controller 放弃缓存中的资源列表重新从 API Server 全量拉取一次并重建缓存。做完之后你再打开应用详情那些 Unknown / 消失的 Pod 就会重新冒出来。5.3 把 argocd-application-controller 从“失忆”里拉出来如果 hard refresh 之后还有应用状态不对那就要考虑 controller 自身的问题。这次我们 hard refresh 后大部分应用恢复正常但有一个应用还是显示 Unknown原因大概率和 informer 内部的 API discovery 缓存过期有关。最终我执行了kubectl rollout restart deployment/argocd-application-controller -n argocd重启的过程里argocd namespace 下会先出现一个新的 controller Pod旧的退出。这个操作会影响所有应用的 reconcile所以尽量安排在工作负载相对稳定的时段而且别在业务高峰期因为一个小应用的问题去滚动全局组件。重启后观察日志里有没有大量registered informer输出确认 watch 全部重建。5.4 验证链路kubectl→API→ArgoCD→UI 全链路对账恢复动作做完不能“看起来绿了”就算完。我做了一轮全链路对账kubectl get pods -n checkout -o wide argocd app get checkout-service argocd app manifests checkout-service | head -80逐项确认API Server 视角Pod 全部 Running 且 ReadyArgoCD sync 状态OutOfSync 数量归零ArgoCD health 状态Healthy等一个 reconcile 周期后再次查看确认状态没有反复在 ArgoCD UI 里点开资源列表确认 Pod 的 UID 信息和 kubectl 一致。这一步是最容易被偷懒跳过的但恰恰是它决定了你是“修好了”还是“侥幸看着好”。6. 复盘与加固这次故障留给我的六条经验6.1 虚拟机重启前必须 cordon drain不解释最有效的修复永远在故障发生之前。给虚拟机做任何维护、迁移、套餐调整操作前一定先排空节点kubectl cordon node-03 kubectl drain node-03 --ignore-daemonsets --delete-emptydir-data这么做等于把驱逐的主动权从 kube-controller-manager 手里拿回到自己手里。否则它会在几十分钟后强制驱逐 Pod驱逐完你的虚拟机又恢复了两边打架ArgoCD 的缓存就得看热闹。运维流程有时候比技术方案更值钱我们团队后来把这条写进了虚拟机维护 SOP。6.2 给你的存储和网络组件加优先启动/自愈逻辑虚拟机上如果承载了 NFS 服务或 CNI 关键组件要保证它们能快速自愈为 calico-node、nfs-server 这类组件给足资源并配置合适的健康检查NFS 服务进程要配置Restartalways并设置网络依赖避免开机后服务比网络先起虚拟机重启后第一时间巡检这些底层组件而不是直接看业务 Pod。6.3 ArgoCD 侧预防合理设置 health check 与自动刷新策略ArgoCD 的 health check 机制是可配置的。这次故障里健康检查脚本和存储恢复速度互相等待导致状态迟迟无法判定。建议给自定义 health check 脚本设置超时避免无限等待定期触发 hard refresh而不是完全依赖普通 refresh 或手动操作在 argocd-cm ConfigMap 中合理配置timeout.reconciliation让 controller 的 reconcile 频率符合业务预期。data: timeout.reconciliation: 180s不要设太短否则 API Server 压力大也不要太长否则故障恢复后状态更新太慢。这里是需要根据集群规模调的。6.4 监控维度别只看 Pod 状态要看 ArgoCD 的健康指标Pod 状态指标在节点重启这种场景下会骗人。ArgoCD 的监控指标其实比 Pod 状态更早暴露问题curl -s http://localhost:8082/metrics | grep argocd_app_health_status建议配置报警规则argocd_app_health_status{health_statusUnknown}持续 5 分钟就触发。同时关注 controller 重建、watch 中断等自身 metrics。这类指标能帮你把“业务故障”和“控制器失明”区分开。6.5 智能运维助手的“故障解析”怎么用才有价值这次排查里智能运维助手的故障快照给了一个方向性提示它把“节点重启时间”“驱逐事件”“ArgoCD 告警”三者做了关联直接把我从“盲猜”的窗口期拉了出来。但我的体会是不要完全相信它给的结论要相信它给的关联数据。比如这次它给出的核心关联是“node-03 重启时长超过 5 分钟”这个信息单凭告警根本看不出来但它关联出来之后我的排查就直接切入驱逐 缓存这条主线。智能助手更像是把散落的证据整理好真正判断和取舍还是得靠你脑子里那套对系统原理的理解。判断完以后建议把这次的关联思路沉淀成 runbook下次再遇到同类告警直接按过往 playbook 执行。6.6 建议写一份 VM 重启应急预案文档最后沉淀成文档别让经验留在个人脑袋里。我们的 VM 重启应急预案现在包含重启前置检核是否已 cordon、drain是否已评估存储依赖重启后检核节点状态、CNI、NFS、业务 Pod Ready、ArgoCD health异常分支处理hard refresh 无效时的升级路径、controller 重启的决策条件干系人通知和变更时间窗。写文档不是应付流程是为了让团队里任何一个人在下一次凌晨值班时能在 20 分钟内完成我这次折腾了两小时才走完的路。最后说点个人体会这次故障虽然让 ArgoCD 状态“瞎”了一阵但并没有造成真正的业务中断最根本的原因是有状态服务上并没有流量重建也没丢数据。但如果当时正赶上大促或者流量高峰可能就没这么轻松了。处理这种“重启虚拟机后 ArgoCD 状态异常”的告警我现在已经养成了肌肉记忆先提权看基本事实再排底层资源再查 controller 日志最后再考虑 hard refresh 和重建。事实证明这个顺序比在 UI 上反复刷新有效太多了。
返回列表