1. 问题根因Node Local DNS为何会成为磁盘故障的“风暴眼”先说结论如果是小规模集群Node Local DNS挂了最多也就是Pod解析慢一点、偶尔超时重试不至于全线崩溃。但一旦节点磁盘写满情况就完全不一样了。我那次遇到的现象非常典型——某天早上kubectl get pods一看一批业务Pod变成Evicted节点状态NotReadySSH上去df -h/var/lib/docker直接100%。然后我注意到kube-system下面node-local-dns这个DaemonSet也在反复CrashLoopBackOff整个节点的DNS解析忽好忽坏业务方报“服务间歇性不可用”。Node Local DNS在K8s里的定位很特殊它以DaemonSet形式在每个节点上跑一个CoreDNS缓存通过hostNetwork方式监听节点上的特定地址通常是169.254.20.10:53或:53再用iptables/ipvs规则把Pod发往kube-dns Service Cluster IP的DNS查询重定向到本地缓存。这么做的好处是省去了跨节点转发DNS请求的延迟也减轻了CoreDNS副本的压力。但代价是它跟节点本地文件系统深度绑定日志目录、缓存目录都落在节点磁盘上。磁盘故障对Node Local DNS的杀伤力是双重的。第一重磁盘满会导致CoreDNS写日志失败、写缓存失败进程开始出现异常行为甚至崩溃第二重kubelet本身对节点磁盘有驱逐机制当nodefs或imagefs使用率超过阈值默认是nodefs.available10%kubelet会把节点标记为DiskPressure并开始驱逐节点上可驱逐的Pod而Node Local DNS作为一个普通的DaemonSet Pod如果没有设置特别高的PriorityClass同样可能被Evicted。一旦它被驱逐它会因为有DaemonSet控制器而马上重新调度回这个节点但重新创建后由于磁盘依然满继续CrashLoop形成一个死循环。更麻烦的是磁盘满之后kubelet自身的状态上报、Pod管理能力都会打折。Kubelet要定期向API Server上报节点状态磁盘写不进去一些临时文件、状态文件写入失败表现为节点心跳异常最终被判定为NotReady。节点NotReady之后该节点上的Pod对外服务开始大量报错。Node Local DNS这时候恰好是故障放大器DNS解析不稳定会让业务Pod报“域名解析失败”看起来就像服务本身挂了。传统的人肉处理流程是接到告警后SSH登录节点先df -h看磁盘再du -sh /var/lib/docker、/var/log、/tmp找大目录手动删镜像、清日志、重启containerd/docker等磁盘使用率降下来后再恢复节点。听起来不复杂但实际操作中每一步都充满变数不知道哪些日志可以安全清理、清理完容器日志容器会不会有异常、镜像是不是被某个Pod引用、节点磁盘压力是不是已经导致某些系统服务异常。等处理完业务影响已经持续很长时间了。所以当时我就决定必须把这套“人肉救火”流程做成一个自动化系统。目标很明确在磁盘使用率达到危险阈值之前自动介入分级处置该清理清理该隔离隔离同时一定不能误伤Node Local DNS这种关键基础组件。2. 自愈系统设计从“人肉救火”到“自动拆弹”2.1 分层设计与模块划分整个系统我把它设计成四层采集层、决策层、执行层、保护层。采集层负责拿到节点磁盘的真实状态数据决策层根据数据判断该执行什么动作执行层负责做清理或隔离动作保护层确保动作不会误伤关键组件。采集层用的是node_exporter加Prometheus。每个节点上本来就会跑node_exporter它的node_filesystem_avail_bytes、node_filesystem_size_bytes、node_filesystem_files_free这些指标可以直接拿来判断磁盘空间与inode使用情况。Kubernetes官方也有节点监控推荐的指标不需要额外写采集器。决策层我选的是Prometheus的告警规则加Alertmanager再由一个自研控制器接收Alertmanager的webhook。之所以不直接在节点上写个脚本死循环跑df命令是因为Prometheus天然有时间序列数据可以配置持续时间、连续触发次数这些条件避免因为瞬时抖动触发自愈动作。而且告警规则本身就带标签、描述、阈值维护起来直观得多。执行层是自研控制器加一次性Job。控制器收到告警后先判断故障类型和节点名称然后构造一个清理Job投递到K8s里Job以特权模式挂载节点的/目录和/var/lib/docker在容器内执行清理脚本。清理完后Job上报结果控制器再根据剩余磁盘空间决定是否升级处理手段比如节点排空。保护层是整个系统里最容易被忽略但又最关键的一部分。自愈系统本质上是在节点上做“破坏性操作”一旦误判清理脚本可能把正在使用的文件删了或者把关键组件所在目录误清理造成比磁盘故障更严重的后果。所以必须把Node Local DNS、kube-system命名空间下所有Pod都拉进保护名单清理动作严格限定在业务Pod汇报的日志文件、无主容器镜像、系统临时文件这些“安全区”内。2.2 技术选型为什么用Prometheus告警加控制器我在设计初始考虑过几个方案。方案一是纯粹在节点上放一个systemd定时任务或脚本每5分钟执行一次df判断超阈值就清理。这个方案最简单但问题很明显无法区分当前处于什么业务压力时段无法在清理失败之后自动升级手段日志不可追溯也拿不到指标用于后续分析。方案二用Prometheus告警规则直接触发一个脚本或者K8s Job但通过Alertmanager做webhook转发。这个方案相对灵活Prometheus本身支持配置多条件触发告警内容包括节点名称、当前磁盘使用率控制器可以根据告警内容精细决策。最后我选了方案二再叠加控制器。为什么一定要有个控制器而不是让Job自己决定动作因为Job需要从集群角度判断边界条件比如某个节点是否已经被cordonPodDisruptionBudget是否允许驱逐该节点上的PodNode Local DNS所在节点是否允许清理其日志目录。这些都是单独的脚本判断不了的必须走Kubernetes API结合集群状态做决策。而且有了控制器所有动作都有审计日志出了事故能查每个时间点到底干了什么。2.3 安全阀与保护策略自愈系统设计里安全阀比自愈动作本身还重要。我一开始就在代码里写了一条铁律任何清理动作都不能触碰/kube-system命名空间下的Pod文件Node Local DNS作为DaemonSet同样受保护。具体到实现清理Job里所有需要删除文件的路径都必须经过白名单校验白名单之外的一律拒绝执行。第二个安全阀是冷却时间。同一个节点如果被连续告警控制器的处置频率要受限默认同一节点两次完整的处置流程之间至少间隔30分钟防止清理动作本身造成新的波动。还要配置最大处置次数比如一个节点在2小时之内被处置了3次仍然没有恢复控制器必须停止自愈动作把节点状态改成“需要人工介入”只保留告警通知。第三个安全阀是“先清理、后隔离、再排空”的三级递进策略每一级都有明确的触发条件和退出条件。清理完如果磁盘使用率回到安全线就直接结束不升级到节点隔离只有清理后仍然超阈值才允许cordon节点停止调度cordon之后还有问题才做非关键工作负载的驱逐。这个递进设计核心目的是把对业务的影响降到最低。驱逐Pod会对业务造成抖动所以在执行驱逐动作前必须确认清理已经无效而不是盲目把人家的Pod全干掉。3. 核心实现监控、告警与自愈动作3.1 采集指标与告警规则采集层直接复用node_exporter的标准指标不需要额外开端口或部署新的exporter。Kubernetes很多发行版自带Prometheus Operator通过ServiceMonitor就可以抓取node_exporter的指标如果没有现成环境可以手动添加一个scrape job。我实际在用的告警规则文件大概长这样groups: - name: node-disk-selfheal rules: - alert: NodeDiskUsageHigh expr: (1 - (node_filesystem_avail_bytes{fstype!~tmpfs|overlay|squashfs} / node_filesystem_size_bytes{fstype!~tmpfs|overlay|squashfs})) 0.85 for: 5m labels: severity: warning selfheal: true annotations: summary: Node {{ $labels.instance }} disk usage high description: Disk usage on {{ $labels.instance }} mount {{ $labels.mountpoint }} has exceeded 85% for 5 minutes. Current value: {{ $value }} - alert: NodeInodeUsageHigh expr: (1 - (node_filesystem_files_free{fstype!~tmpfs|overlay|squashfs} / node_filesystem_files{fstype!~tmpfs|overlay|squashfs})) 0.85 for: 5m labels: severity: warning selfheal: true annotations: summary: Node {{ $labels.instance }} inode usage high description: Inode usage on {{ $labels.instance }} mount {{ $labels.mountpoint }} has exceeded 85% for 5 minutes. Current value: {{ $value }} - alert: NodeDiskUsageCritical expr: (1 - (node_filesystem_avail_bytes{fstype!~tmpfs|overlay|squashfs} / node_filesystem_size_bytes{fstype!~tmpfs|overlay|squashfs})) 0.93 for: 2m labels: severity: critical selfheal: true annotations: summary: Node {{ $labels.instance }} disk usage critical description: Disk usage on {{ $labels.instance }} mount {{ $labels.mountpoint }} has exceeded 93% for 2 minutes. Action required immediately.这里有一个注意点告警表达式里我故意过滤掉了tmpfs、overlay和squashfs文件系统。tmpfs是内存文件系统不存在真正的磁盘占用overlay是容器镜像层它的使用量其实已经映射到对应的主机文件系统上了。不过在取“overlay”的过滤时机上不同环境不一样有些发行版会把/var/lib/docker/overlay2单独挂载成独立分区这时删除镜像就会释放这一层的空间所以还得看实际部署环境没有统一答案。告警规则里label加了selfheal: true这是控制器判断要不要自动处理的开关。不是所有告警都适合自动处理比如某些业务自定义的告警处理方式可能涉及业务变更控制器不应该介入。只有打了selfheal标签的告警才会进入自愈流程。Alertmanager的配置需要加一个webhook接收器把匹配到selfhealtrue的告警转发给自愈控制器route: group_by: [alertname, instance] group_wait: 10s group_interval: 2m repeat_interval: 30m receiver: selfheal-controller routes: - matchers: - selfheal true receiver: selfheal-controller receivers: - name: selfheal-controller webhook_configs: - url: http://selfheal-controller.kube-system.svc:8080/webhook send_resolved: true3.2 清理工具实现清理Job是整个系统真正干活的模块。它的核心是一个Shell脚本加一个白名单机制。我把它做成了一个镜像控制器按需通过Kubernetes Job API创建Pod来执行。清理脚本的核心逻辑分四步。第一步是盘点找出占空间最大的几个目录并计算它们各自占用的空间记录到日志里第二步是清理docker镜像找出不被任何容器引用的悬空镜像和无用镜像执行docker image prune第三步是清理日志包括journald日志、/var/log下的老日志文件但这里只清理超过一定时间的、已经被压缩的日志不碰正在写入文件第四步是清理临时文件比如/tmp下超过7天没有访问的文件。#!/bin/bash set -euo pipefail NODE_HOSTNAME${NODE_HOSTNAME:-} THRESHOLD_CLEANUP80 LOG_FILE/var/log/selfheal-cleanup.log log() { echo $(date %Y-%m-%d %H:%M:%S) [${NODE_HOSTNAME}] $* ${LOG_FILE} } mount_usage() { local mountpoint$1 df --outputpcent ${mountpoint} | tail -1 | tr -d % } # 安全白名单只允许对以下路径做清理动作 ALLOWED_CLEAN_PATHS( /var/log /tmp /var/lib/docker /run/log ) safe_path() { local target$1 for base in ${ALLOWED_CLEAN_PATHS[]}; do if [[ ${target} ${base} || ${target} ${base}/* ]]; then return 0 fi done return 1 } cleanup_docker_images() { log Start cleanup docker dangling images docker image prune -af --filter until24h ${LOG_FILE} 21 || true } cleanup_journal_logs() { log Start cleanup journal logs journalctl --vacuum-size500M ${LOG_FILE} 21 || true } cleanup_container_logs() { log Start cleanup container log files # 只处理容器日志文件并且只truncate不删除避免容器文件描述符失效 find /var/lib/docker/containers -name *-json.log -type f -size 100M | while read -r f; do if safe_path ${f}; then log Truncate container log: ${f} truncate -s 0 ${f} || log Failed to truncate ${f} fi done } cleanup_tmp_files() { log Start cleanup temp files find /tmp -type f -atime 7 -delete 2/dev/null || true } main() { log Cleanup job start cleanup_docker_images cleanup_journal_logs cleanup_container_logs cleanup_tmp_files log Cleanup job end } main容器日志处理这里有一个特别值得强调的细节对/var/lib/docker/containers目录下的*-json.log文件我用了truncate -s 0而不是rm。如果直接rm删除文件容器进程持有的文件描述符还指向这个已经被删除的inode磁盘空间不会立刻释放只有等到容器重建或者进程重启后才会真正被回收。truncate则直接把文件内容裁掉空间立即释放容器进程不需要重启也能继续正常写日志。这个技巧在containerd运行时下用crictl看容器日志路径的方式不完全一样但原理相通。清理Job的YAML大概是这样的注意要挂载宿主机的/目录设置privileged并用hostPID: true让容器内能看到宿主机进程这样docker命令可以直接操作宿主机的docker socketapiVersion: batch/v1 kind: Job metadata: name: disk-cleanup-${NODE_NAME}-${JOB_ID} namespace: kube-system labels: app: selfheal-cleanup spec: ttlSecondsAfterFinished: 600 template: spec: nodeName: ${NODE_NAME} hostPID: true restartPolicy: Never containers: - name: cleanup image: registry.example.com/selfheal-cleanup:latest imagePullPolicy: IfNotPresent securityContext: privileged: true env: - name: NODE_HOSTNAME value: ${NODE_NAME} volumeMounts: - name: host-root mountPath: /host - name: docker-sock mountPath: /var/run/docker.sock - name: log-volume mountPath: /var/log volumes: - name: host-root hostPath: path: / - name: docker-sock hostPath: path: /var/run/docker.sock - name: log-volume hostPath: path: /var/log有个坑需要提醒如果节点磁盘已经满到kubelet操作Pod都困难Job可能无法在该节点上调度起来因为它需要拉取镜像、写Pod状态。我在实际环境里给这种自愈Job加了ImagePullPolicy: IfNotPresent并且要求集群里提前在各个节点上缓存好镜像。如果节点磁盘满到连镜像都已经无法拉取这个方案就退化了只能走cordon排空和人工介入的最高级策略。3.3 控制器核心逻辑控制器是整个系统的大脑我实际上是用Go写的核心逻辑不复杂。它监听两个东西Alertmanager的webhook请求以及Kubernetes的节点事件。收到告警webhook后做三步解析告警里包含的节点名称和告警类型检查当前节点是否已经在处置中在处置中则忽略根据告警级别执行不同级别动作。控制器的核心状态机和动作序列伪代码如下func handleAlert(alert Alert) { nodeName : extractNodeName(alert.Instance) if isProcessing(nodeName) { return // 该节点已有处置任务在跑 } switch alert.Severity { case warning: triggerCleanupJob(nodeName, basic) case critical: triggerCleanupJob(nodeName, aggressive) if cleanupFailed(nodeName) { cordonNode(nodeName) evictNonCriticalPods(nodeName) } } }关键设计点是“先确认清理结果再决定是否升级动作”。控制器在创建清理Job之后会持续watch这个Job的状态直到Job完成再读取Job中记录的清理前后磁盘使用率。如果清理后节点磁盘使用率降到80%以下系统认为处置成功只记录日志不升级动作。如果仍然高于90%控制器会把节点cordon掉禁止新Pod调度上来再尝试驱逐节点上的非关键工作负载。“非关键工作负载”的判定规则也很重要不驱逐kube-system下的Pod不驱逐有状态应用StatefulSet只驱逐那些可以被其它节点重新调度的Deployment Pod。这个判断逻辑我放在控制器里通过Kubernetes API获取Pod的OwnerReference来识别。3.4 权限与RBAC配置控制器需要操作Job、Node、Pod、Eviction等资源RBAC配置要精确控制。我给控制器创建了一个独立的ServiceAccount只授予最小权限apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: selfheal-controller rules: - apiGroups: [batch] resources: [jobs, jobs/status] verbs: [get, list, watch, create, delete] - apiGroups: [] resources: [nodes] verbs: [get, list, watch, patch, update] - apiGroups: [] resources: [pods] verbs: [get, list, watch, delete] - apiGroups: [policy] resources: [poddisruptionbudgets] verbs: [get, list, watch] - apiGroups: [policy] resources: [evictions] verbs: [create]这个权限设计里没有给更新Node状态的权限只允许patch节点上的cordon标注通过patch nodes的spec.unschedulable字段。尽量避免让自愈系统有删除、重启整个节点的能力那样太危险了。4. 实操演练模拟磁盘满观察自愈流程4.1 环境说明我用来做验证的环境是这样的一个测试K8s集群3个节点版本1.26containerd作为容器运行时Node Local DNS以DaemonSet方式运行在全部节点上。Prometheus与Alertmanager部署在kube-system自愈控制器同样部署在kube-system。磁盘告警阈值设置为85%预警触发清理93%触发更高级别的处置。4.2 触发磁盘满故障找一个工作节点用dd命令人为制造一个大文件把磁盘使用率推高到90%以上# 在测试节点上找到根文件系统挂载点 df -h / # 用dd生成一个5GB的稀疏文件推高磁盘使用率 dd if/dev/zero of/tmp/disk-fill-test bs1M count5000 convfsync注意这里的操作我故意先选择/tmp目录因为/tmp本来就是白名单允许清理的路径这样测试可以走完整个自愈流程但不会误伤任何真实数据。多制造几个大文件把使用率推到临界值以上。4.3 自愈过程跟踪触发磁盘写满后大概等待告警规则的for: 5m过去Prometheus产生告警Alertmanager转发webhook给控制器。然后开始观察。第一步查看自愈控制器的日志kubectl logs -n kube-system deploy/selfheal-controller --tail100日志里应该能看到收到告警、判断节点名称、创建清理Job这几条记录。控制器不会立即在节点上执行清理它会先创建一个JobJob的Pod会在目标节点上被调度执行。第二步确认Job已经创建kubectl get jobs -n kube-system -l appselfheal-cleanup第三步等待Job执行完成回去看df输出df -h /如果一切正常磁盘使用率已经从90%以上回落到85%以下。控制器会记录一条“cleanup succeeded”的日志整个流程自动结束没有产生任何人工干预。我实测下来的时间线大概是这样的磁盘写满在第0分钟Prometheus在5分钟后产生告警Alertmanager立刻转发控制器在几秒内创建清理Job清理Job执行大约1-2分钟到第8分钟左右磁盘已经恢复正常。整个过程没有触发节点cordon也没有驱逐任何PodNode Local DNS全程稳定运行业务流量没有感知。当然自愈系统能这么快处置前提是磁盘写入的是可以被清理的“垃圾文件”。如果磁盘被业务日志或者数据文件写满清理脚本不会处理这类文件最终还是会升级到节点隔离和Pod驱逐。这个测试只是验证了整个链路是否通畅不代表所有磁盘满场景都能靠这一招解决。5. 常见问题与避坑记录这套系统从设计到落地我踩了不少坑挑几个典型的记录在这里希望能帮大家少走弯路。5.1 容器日志清理的两个大坑第一个坑是直接rm容器日志文件导致空间不释放。我第一次做清理脚本的时候直接写的是find /var/lib/docker/containers -name *-json.log -delete。跑完之后一查df磁盘使用率一点没降。后面查了才知道容器进程的文件句柄还占着这个被删除的inode只有重启容器或者重启进程空间才会真正释放。改成truncate -s 0之后空间立刻释放了。这个坑在containerd运行时的环境里同样存在只是路径不同原理一样。第二个坑是有些容器日志文件增长异常快清理完过一会儿又满了。后来排查发现是某个业务Pod每隔几秒打一条超大日志光靠清理日志文件解决不了根本问题。自愈系统要加一层逻辑如果同一个节点在短时间内被反复告警清理了也维持不了多久说明根因可能是业务日志量太大这时应该把容器日志的轮转策略调好再去排查业务侧的问题。自愈只是应急止血不是治本方案。5.2 inode耗尽为什么容易被忽略磁盘空间还剩很大但是文件数量太多了inode用完表现为无法创建任何新文件。这种情况df -h看着一点问题没有但df -i已经到100%了。Node Local DNS在这种故障模式下的表现是CoreDNS尝试写缓存文件失败启动报错整个Pod一直重建。我刚开始只监控了node_filesystem_avail_bytes没有监控inode指标结果遇到一次inode耗尽自愈系统一点反应都没有。后来加了node_filesystem_files_free和node_filesystem_files两个指标并且把inode耗尽的告警级别调成跟磁盘空间不足一样。清理动作上inode耗尽要重点清理小文件目录比如/tmp下碎片文件、容器日志的轮转文件、镜像层的临时文件。5.3 Node Local DNS的“误伤”教训有一次我在测试清理docker镜像的自动化动作时用docker image prune -af过滤所有不被容器使用的镜像。当时没注意到过滤条件写得太激进把这个节点上虽然没被当前容器使用但其实可能被Node Local DNS缓存依赖的镜像给删了。结果Node Local DNS重启的时候发现镜像不存在直接从本地拉取失败长时间处于ImagePullBackOff状态。教训有两点。第一清理镜像时一定要留保护名单DaemonSet当前使用或者上一次使用的镜像绝对不能清理。第二Node Local DNS这种组件的Pod状态一定要纳入自愈系统的监控视线不能只盯着磁盘指标。清完磁盘后还要确认Node Local DNS Pod恢复Running才算整个自愈流程完整结束。5.4 阈值与冷却时间的设置阈值设置得太保守会频繁触发无意义的清理每次清理都是对节点的IO操作高峰期可能影响业务性能。我一开始把预警阈值设成75%结果一个正常业务写日志比较多的集群天天触发清理反而把节点IO打高了。后面调整成85%预警、93%升级效果好很多。冷却时间也一样。最开始没有冷却机制一个节点磁盘使用率在告警阈值附近反复横跳控制器就不断创建新的清理Job集群里冒出大量同名Job。后来加了冷却时间同一个节点两次完整处置流程的间隔至少30分钟并且加了最大处置次数限制2小时内最多自动处置3次超过之后自动进入“人工介入”模式。这个限制非常必要自愈系统本身也是集群的负载不能因为自愈动作反而引发新的故障。5.5 控制器自身的可用性最后提一个容易被忽视的点自愈控制器本身如果挂了整个自愈系统就瘫痪了。控制器我建议部署成Deployment至少两个副本并且通过anti-affinity把副本分散到不同节点。它调用API Server的QPS不高通常不需要很强的资源但一定要给它设置合理的资源限制防止它本身也被节点磁盘故障牵连。控制器的监控也要纳入Prometheus比如它正在处理多少节点、最近一次处置结果、Job创建失败次数这些指标可以帮助判断自愈系统本身是否健康。有一次我发现控制器一直没触发清理查了半天是Alertmanager的webhook地址配错了事件静默丢失。后来我在控制器里加了告警事件日志并且用metric记录收到的webhook数量少一个都能对出来。6. 这套方案的适用边界与后续演进方向用这套系统跑了大半年稳定性还是可以的。但我也很清楚它的边界在哪。当前设计默认控制面是高可用的API Server和etcd不在被处置的工作节点上。如果是控制面和工作负载混部的小型集群磁盘满到连API Server都受影响控制器可能连API Server都访问不到自愈系统就失灵了。这种情况下方案需要额外做一层“带外”通道比如通过节点上的本地agent感知磁盘状态在本地直接执行清理动作不走API Server。这也是很多云厂商节点自愈agent的做法。另外一个演进方向是把这套自愈能力跟驱逐预算PDB结合起来更精细地控制。当前驱逐Pod的动作还是比较粗放的只要是非kube-system的Deployment Pod就会被驱逐但有些Deployment下配置了PDB如果一次性驱逐超过PDB允许的数量会导致业务无可用副本。控制器应该在做驱逐前先检查目标Pod所在应用的PDB逐批驱逐而不是一窝蜂全驱。磁盘自愈做完之后同类思路其实可以平移到其他节点级故障场景内存压力、CPU饥饿、网络丢包。架构都是一样的就是采集层的指标换掉、执行层的动作换掉、保护层的白名单换掉。Prometheus加控制器的这套组合天然适合做这种“有监控指标、有API接口、有恢复手段”的自愈系统底座。回到标题说的Node Local DNS我的体会是在K8s集群里真正决定一个系统稳不稳的往往不是那些看起来最庞大的业务组件而是像DNS这样每个请求都会路过的细节组件。磁盘自愈系统能保护Node Local DNS抬头不见低头见的运行环境比单纯给Node Local DNS加副本、加缓存意义大得多。因为它不只是在“救”一个Pod而是在消除整个节点层面的故障风险。如果你正在被集群节点磁盘问题折腾可以从最小可行的方案开始先把Prometheus告警规则加上再写一个简单的清理脚本通过Alertmanager webhook触发。跑通了再逐步加入控制器、保护名单、冷却机制这些复杂逻辑。自愈系统是一步步养起来的不是一次性写完就能放心的。
