AIGC标识 如何发现 K8s 集群里的无用资源?

前言

终于等来了国庆长假!宅在家折腾自己的 HomeLab、 K8s、 NAS 服务器集群的感觉真爽!这次长假真的是折腾了好多东西,感兴趣的读者请期待后续的爆更,等不及的话也可以直接看我的 repo - east4ming/homelab2 的 PR 和 Commits。

处女座 强迫症+洁癖发作, 想把 Homelab 集群里那些"没人认领"的资源清一清.

说实话, 集群跑久了, 谁都一样: 一堆 PVC 挂在那没人用, Service 后面一个 Pod 都没有, ConfigMap 是上一版部署留下的遗物, Deployment 副本数为 0 但对象还在。这些东西不影响业务, 但看着糟心, 而且浪费存储和 quota。人工 kubectl get 一个个翻? 数十个 namespace, 翻到明年去。

于是就有了这次实践: 用 GitOps 的方式引入 yonahd/kor, 给集群装一盏"未使用资源探照灯"。过程总体顺利, 但中间踩了一个非常典型的坑: 静默失效。不报错、不告警, 面板就是空的。这篇文章主要讲这个坑。

📝声明:

本文所有内容来自本人 Homelab 实测, 非广告非推广。kor 是上游开源项目, 本文仅记录使用体验。

最终效果展示

Kor dashboard 1

Kor dashboard 2

Kor dashboard 3

Kor Prometheus Alertrules

配置告警规则至今还没收到过告警,所以这次没有相关截图。

kor 是什么, 以及这次打算怎么用

kor 是一个 Kubernetes 未使用资源扫描工具, 支持检测 orphaned 的 Service、Deployment、ConfigMap、Secret、PVC、HPA、PDB 等一大票资源类型。

它有两种玩法:

模式 形态 输出方式 本次是否启用
prometheusExporter 常驻 Deployment, 暴露 metrics Prometheus 抓取 ✅ 启用
cronJob 定时 Job 报告输出到日志 / Slack ❌ 关闭

CronJob 模式被关掉的原因很实在: 我这边没有 Slack 凭据, 报告只能进 Pod 日志, 那还不如直接看指标。所以最终只保留 exporter 模式。

部署形态是一个包装 chart, 放在 system/kor/:

# system/kor/Chart.yaml
apiVersion: v2
name: kor
version: 0.1.0
dependencies:
  - name: kor
    version: 0.2.16
    repository: https://yonahd.github.io/kor

values 里的关键取舍:

kor:
  cronJob:
    enabled: false
  prometheusExporter:
    enabled: true
    image:
      repository: yonahdissen/kor
      tag: v0.6.9
      pullPolicy: IfNotPresent   # 替代上游默认的 latest + Always
    resources:
      requests:
        cpu: 10m
        memory: 64Mi
      limits:
        cpu: 500m
        memory: 256Mi
  exporterInterval: 30m          # 上游默认 10m, 放宽到 30m

两个地方我改了默认配置:

  1. 镜像钉死 v0.6.9 并把 imagePullPolicy 改成 IfNotPresent。上游默认是 latest + Always, 这在 Homelab 里意味着每次重启都去拉一遍, 而且版本随时可能变。钉死 + IfNotPresent, 省事也省流量。
  2. exporterInterval 从 10 分钟放宽到 30 分钟。原因是每一轮扫描都是对整个集群做 LIST, 属于实打实的 API Server 压力。Homelab 里没必要 10 分钟扫一次, 30 分钟足够了。

实测常驻开销: 约 10MiB 内存 / 24m CPU, 镜像 10.5MB, 首扫约 3 分钟。这个开销基本可以忽略。

接入既有 Prometheus 和 Grafana: 全程零改动

这次最舒服的一点是: 没有动任何既有的 Prometheus / Grafana 配置。

抓取靠 chart 自带的 ServiceMonitor。

dashboard 则放在 system/monitoring-system/, 由既有的 Grafana sidecar 加载。

交付路径也很干净: 不新增任何 ArgoCD Application 清单, ApplicationSet 的 git generator 覆盖 system/*, namespace 取目录名 kor。

上线后确认:

  • up{job="kor-exporter"} = 1, 且未改动任何 Prometheus 配置
  • kubernetes_orphaned_resources 首扫后产生 925 条序列
  • exported_namespace 有 41 个取值

踩坑: namespace 恒为 kor 的静默失效

前面都挺顺, 坑在 dashboard 适配环节。

kor 的指标里带了资源所在的 namespace。我去翻 kor 源码, 看到标签名是 namespace, 于是很自然地就把 dashboard 的查询写成了:

sum by(namespace) (kubernetes_orphaned_resources)

上线, 打开面板。

空的。

不报错, 不告警, 查询合法返回, 就是没有数据。第一反应是 dashboard JSON 写错了, 检查半天没发现问题。然后去 Prometheus 里直接查:

kubernetes_orphaned_resources

结果出来, 每条序列上确实有 namespace 标签, 但它的值恒为 kor。

而另一个标签 exported_namespace 有 41 个不同的取值, 那才是真正的资源命名空间。

原因在于 prometheus-operator 的行为: 它会给所有抓上来的 series 附加一批目标标签(target labels), 其中包括 namespace=<exporter 所在的 namespace>, 也就是 kor。指标自带的同名标签 namespace 会与之冲突, 于是被重命名为 exported_namespace。

📝Notes: 这是 Prometheus 的通用规则: 目标标签与样本标签冲突时, 样本标签会被加上 exported_ 前缀。不是 kor 特有的问题, 任何 exporter 都可能遇到。

这就是典型的静默失效: 不报错、不告警、查询语法完全合法, 只是结果为空或恒为一个值。如果没有去核对线上实际序列的取值分布, 光读源码和文档, 根本发现不了。

修复很简单, 把查询改回 exported_namespace: (或者指定 honorLabel)

sum by(exported_namespace) (kubernetes_orphaned_resources)

改完复验, sum by(exported_namespace) 返回 42 组, 正常了。

安全边界: 只给线索, 清理由人决策

这一节我觉得比技术细节更重要。

kor 是有删除能力的, 通过 --delete 参数。我没有配置, 也没有启用。RBAC 动词只有:

verbs: ["get", "list", "watch"]

没有 delete、update、patch。也就是说, 就算 kor 想删也删不掉。

原因有二:

  1. 我对它的定位就是"探照灯", 不是"清洁工"。这个特性的职责是提供线索, 不是自动处置。
  2. kor 的资源判定存在上游已知的假阳性: 被 CRD 引用的、被动态加载的配置引用的 Secret / ConfigMap, 会被误判为"未使用"。自动删了就是事故。

所以清理流程是: 人看面板 → 人判断 → 人手工执行。

同理, 被否决的方案还有几个:

  • CronJob 模式: 没 Slack 凭据, 报告进日志等于没有。
  • kor 自动删除: 需要写权限, 叠加假阳性风险, 不划算。
  • krew / 本地二进制安装: 脱离 GitOps, 配置漂移没人管。

kor 运行结果

Kor 发现的无用资源:

kind 数 kind 数 kind 数
ReplicaSet 486 Crd 127 Secret 90
ConfigMap 68 Job 40 Pvc 30
ClusterRole 26 VolumeAttachment 25 Service 11
ClusterRoleBinding 10 RoleBinding 7 ServiceAccount 4
StorageClass 1

最大的 namespace 是 rook-ceph(141)、monitoring-system(62)、kubevirt(43)、rsshub(43)、tailscale(40)。

分类整理与「能不能删」

判据的核心是两条容易搞错的分界线:

  1. 派生对象(有 ownerReferences)只看 owner:RS 由 Deployment 生成、Job 由 CronJob、PVC 由 DataVolume。它们的标签是从父对象模板继承的——486 个 RS 里 258 个带 argocd.argoproj.io/tracking-id,按标签判会把它们误认为「GitOps 管理」而不敢清。
  2. 声明式对象才看 GitOps 标记:有 ArgoCD/Helm 归属的删了会被重建,只会制造漂移。
A 类 可直接批量清理  557   RS 486 / Job 32 / VolumeAttachment 25 / PVC 7 / Service 7
B 类 需人工确认       36   RBAC+CM 17 / VolSync 缓存 17 / prod-vm-datadisk / virt-exportproxy
C 类 禁止删除        332   Crd 127 / Secret 90 / GitOps 管理 85 / operator 拥有 24 / DataVolume 5 / StorageClass 1

可以安全删的(A 类)

对象 数 为什么安全 真实收益
ReplicaSet(旧 revision) 486 replicas=0 且状态全 0;删了不会重建,只少 rollout undo 历史 仅去噪
Job(已完成) 32 27 renovate 历史 + kube-bench + 4 个 CronJob 历史 仅去噪(日志先留)
VolumeAttachment 25 引用已不存在的 PV,CSI 残留 零——见下
Woodpecker PVC 7 2026-05 的孤儿流水线工作区,无 Pod、无 owner 名义 70 GiB,实测 ~0.5 GiB
Woodpecker Service 7 同批流水线残留的 headless Service 仅去噪

收益必须如实说:etcd 只有 71 MiB,557 个对象主要是历史噪声;rbd du 实测 7 个 WP PVC 只占约 0.5 GiB(1 个 0 B、6 个 80–84 MiB,RBD 精简置配)。那 25 条陈旧 VA 没有任何空间收益

但是我是处女座,有强迫症,明知道没用的不清理会不舒服。🤣

需人工确认(B类)

我仔细看了一遍, 发现了 4条遗留的 loft-cluster-* ClusterRole 和 loft-cluster-admin、loft-cluster-authenticated、vc-v-v-1-v-loft-default-v-v-v-1 及其 3 个 Binding(该软件早已删除)。
清理删除,爽~

绝对不能删的(挑最关键的三条)

  • kube-system/k3s-etcd-snapshot-s3-config —— 删了集群的 etcd 备份直接停摆,这是最后一道防线。
  • *-backup-repository / volsync-src-* —— Restic 仓库口令,删了历史备份再也解不开。
  • tailscale/<域名> TLS Secret —— 被 Ingress 的 spec.tls 引用,删了 HTTPS 当场断。

127 个 CRD 全部不删:删 CRD 会级联删掉其 CR,且都属于已装 operator/chart,删了会被重建。kubevirt-test/prod-vm-datadisk 也不删——virtualmachine/prod-vm(Stopped)的 datadisk 就指向它。

如何处理 Kor 假阳性?

2个办法:

  • 给假阳性的资源打标签 kor/used=true
  • Kor 命令加 flag - --exclude-labels: 这次我指定了 tailscale.com/managed=true——比逐个打标签可扩展

添加 Kor 告警规则

规则 表达式 覆盖什么
KorAuditNoData absent(kubernetes_orphaned_resources) for 1h 抓取没断但指标全没了 → 扫描卡死 / RBAC 被改坏(TargetDown 覆盖不到)
KorUnusedResourcesGrowing 7 天增长 > 150,for 6h 清理提醒
KorStaleVolumeAttachments count(...{kind="VolumeAttachment"}) > 10 for 12h CSI/节点残留,注释里要求顺带核对 Ceph 孤儿镜像
KorUnmountedPvcSpike 未挂载 PVC(排除 VolSync 缓存)7 天增长 > 10 卷占着置配额度 / 工作负载没挂上卷

刻意不做绝对数量告警:实测证明这个规模受 --exclude-labels 配置、集群历史(旧 RS/已完成 Job 只增不减)、kor 假阳性三者影响——「有几百上千条」不一定是故障,「一周涨 150 条」才是。

总结

这次实践的核心收获总结如下:

  1. 用 kor 扫描 Kubernetes 未使用资源
  2. 配置 Grafana 面板展示
  3. 人工判断并清理真正的无用资源
  4. 过滤假阳性配置
  5. 并根据实际情况配置合适的告警规则

最终让我这个处女座(强迫症+洁癖)念头通达!😄

📚️参考文档

posted @ 2026-10-15 09:43  东风微鸣  阅读(0)  评论(0)    收藏  举报