在Kubernetes或大规模虚拟机集群中,资源回收并不是简单删除几个Pod或关停几台机器。闲置资源的判定需要同时考虑时间窗口、历史负载、业务标注以及节点角色,否则很容易把临时低谷当作长期闲置,触发不必要的抖动。本文以一个可直接改造的回收脚本为主线,说明如何采集指标、设置阈值、执行安全下线,并把整条流程纳入定时任务。

一、闲置资源回收的判定指标从哪里来
自动回收的第一步不是执行删除,而是确认哪些资源可以被定义为闲置。对于Kubernetes集群,通常可以依赖metrics-server提供的资源使用数据。执行kubectl top nodes可以查看每个节点的CPU和内存实时使用量,执行kubectl top pods -n 命名空间可以查看Pod维度数据。但实时数据不能直接作为回收依据,因为业务可能存在周期性波峰,例如每天凌晨低负载、白天高负载。如果只在凌晨执行扫描,就可能在业务低谷时把正常节点回收。
更稳妥的做法是引入监控系统如Prometheus,按时间窗口查询平均利用率或百分位利用率。在脚本中可以通过Prometheus HTTP API获取过去6小时或12小时的平均CPU使用率。这样判断对象就从实时值变成趋势值,降低误判概率。实际实现中,可以先确定两类目标:一类是长期没有调度的空闲节点,另一类是长期低负载且未被业务声明保留的Pod。前者适合缩容节点池,后者适合释放命名空间配额。
下面给出一个查询空闲节点的代码示例,通过kubectl获取节点列表,再根据CPU使用量做初步过滤。这里假设threshold为20毫核,时间窗口已经在监控侧完成聚合,脚本只读取结果。
kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, cpu: .status.allocatable.cpu}'
这段命令只输出节点名称和可分配CPU,实际判断还要结合kubectl top nodes或Prometheus数据。必须避免仅凭一次采样就执行下线,否则当节点出现瞬时低负载时,回收动作会造成业务Pod被驱逐,影响可用性。因此脚本中应当为每个候选资源设置持续观察窗口,例如连续三次扫描均低于阈值才进入候选池。
二、回收脚本的关键实现与执行流程
下面设计一个Python脚本,使用subprocess调用kubectl命令获取节点信息,再从Kubernetes API读取注解中的保护标记。逻辑分成四个步骤:扫描候选节点、检查白名单和保护标记、执行cordon隔离、执行drain驱逐并记录日志。为了避免误操作,脚本默认使用dry-run模式,只有传入--confirm参数时才真正执行下线。
脚本中需要特别处理节点名称和注解中的特殊字符,避免Shell注入。通过列表形式传递命令参数,而不是拼接字符串。例如使用subprocess.run(["kubectl", "get", "nodes", "-o", "json"], capture_output=True, text=True),这样节点名称中的空格或特殊符号不会破坏命令结构。
#!/usr/bin/env python3
import subprocess
import json
import sys
import time
CPU_THRESHOLD_MILLICORES = 20.0
DRY_RUN = True
def run_kubectl(args):
result = subprocess.run(
["kubectl"] + args,
capture_output=True,
text=True,
check=False
)
if result.returncode != 0:
print("命令执行失败:", result.stderr.strip())
sys.exit(1)
return result.stdout
def get_low_usage_nodes():
# 这里使用kubectl top nodes获取实时CPU使用量,实际可替换为Prometheus查询
output = run_kubectl(["top", "nodes", "--no-headers"])
low_nodes = []
for line in output.splitlines():
parts = line.split()
if len(parts) < 3:
continue
name = parts[0]
cpu_str = parts[2]
if cpu_str.endswith("m"):
cpu_millicores = float(cpu_str[:-1])
else:
cpu_millicores = float(cpu_str) * 1000
if cpu_millicores < CPU_THRESHOLD_MILLICORES:
low_nodes.append(name)
return low_nodes
def has_protect_annotation(node_name):
output = run_kubectl([
"get", "node", node_name,
"-o", "jsonpath={.metadata.annotations}"
])
annotations = output.strip()
if not annotations:
return False
return "recycle-protect" in annotations and "true" in annotations
def cordon_node(name):
run_kubectl(["cordon", name])
print("已隔离节点:", name)
def drain_node(name):
run_kubectl([
"drain", name,
"--ignore-daemonsets",
"--delete-emptydir-data",
"--force",
"--timeout=60s"
])
print("已驱逐节点:", name)
def main():
global DRY_RUN
if "--confirm" in sys.argv:
DRY_RUN = False
print("扫描模式:", "dry-run" if DRY_RUN else "执行回收")
low_nodes = get_low_usage_nodes()
for node in low_nodes:
if has_protect_annotation(node):
print("跳过受保护节点:", node)
continue
print("候选闲置节点:", node)
if not DRY_RUN:
cordon_node(node)
drain_node(node)
time.sleep(5)
if __name__ == "__main__":
main()
这段脚本的回收对象是节点,但同样的思路可以迁移到Pod或命名空间。如果只需要回收空闲Pod,可以把kubectl top nodes替换为kubectl top pods --all-namespaces,再根据命名空间白名单和Pod注解进行筛选。回收Pod时,执行kubectl delete pod前务必确认该Pod不受Deployment、StatefulSet等控制器管理,否则删除后会立即被重建,起不到释放作用。更有效的方式是先缩容对应的工作负载,而不是直接删除Pod。
此外,节点drain操作会驱逐节点上所有Pod,如果某些Pod配置了PodDisruptionBudget,可能阻塞drain过程。脚本中设置了--timeout=60s,超时后需要人工介入。对于有状态服务,最好先检查StatefulSet的副本数和持久卷绑定情况,避免回收导致数据不可用。建议只对标记为node-role.kubernetes.io/worker的纯计算节点执行自动回收,控制平面节点和存储节点应默认加入白名单。
三、调度策略与防误删保护机制
自动化回收脚本真正上线前,需要把安全机制放在第一位。通常可以引入三层保护:第一层是资源标注,用户或集群管理员可以在节点上添加recycle-protect=true注解,脚本检测到该注解就跳过;第二层是白名单,例如按节点名称前缀、标签或命名空间排除;第三层是人工确认,脚本默认只输出候选列表,只有带上确认参数才执行真实操作。
调度方面,可以把脚本放入Kubernetes CronJob,也可以使用传统crontab。使用CronJob的好处是可以复用集群的权限控制,脚本容器通过ServiceAccount访问Kubernetes API,而不用在宿主机上配置kubectl凭据。下面的CronJob示例每小时执行一次扫描,并默认以dry-run模式运行,日志可通过kubectl logs查看。
apiVersion: batch/v1
kind: CronJob
metadata:
name: resource-recycle
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: recycle-sa
restartPolicy: OnFailure
containers:
- name: recycle
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
kubectl get nodes --show-labels
echo "scan completed"
这个CronJob只是一个框架,实际需要挂载包含脚本的ConfigMap,并在命令中执行真实回收脚本。ServiceAccount需要绑定最小权限,只允许get、list节点和Pod,以及cordon、drain、delete操作,且最好限定在特定命名空间或资源标签。不要把cluster-admin权限直接交给回收脚本,否则一旦出现逻辑错误,可能影响整个集群。
另一个容易忽略的点是审计日志。每次回收动作都应当记录候选资源、判断依据、执行时间和操作人。推荐将日志写入标准输出,再通过日志采集系统集中存储。对于需要审批的场景,可以要求脚本在回收前调用外部审批接口,审批通过后才执行。这样即使自动扫描发现大量闲置资源,也不会在没有人工确认的情况下大规模下线。
四、从测试环境到生产环境的落地建议
在测试环境中验证回收脚本时,可以先针对一个低优先级命名空间运行,观察回收后是否出现Pod重建、服务不可用或配额异常。建议使用--dry-run模式连续运行一到两周,确认候选列表稳定,并且没有把正常业务节点列入回收对象。之后再打开执行开关,并从小范围开始,例如只自动回收带有recycle=enabled标注的命名空间。
生产环境还要考虑与弹性伸缩组件的配合。如果使用Cluster Autoscaler,节点回收后可能被自动补充,形成反复回收和扩容的循环。此时需要在节点上添加cluster-autoscaler.kubernetes.io/scale-down-disabled=true注解,或者把回收脚本的动作改为通知Cluster Autoscaler执行缩容,而不是直接删除节点。不同云厂商的节点池管理方式不同,需要根据实际情况调整脚本的下线逻辑。
最后,定期回顾回收策略同样必要。业务形态可能变化,过去的低负载阈值在新的业务周期下不再适用。可以按月汇总回收数量、误判数量和人工回滚次数,根据这些指标调整阈值、观察窗口和白名单范围。只有让回收脚本具备可观测、可调整、可回滚的能力,才能真正降低集群成本而不增加运维风险。
集群资源回收自动化脚本Kubernetes修改时间:2026-08-22 02:51:57