Agent自愈能力指的是Agent进程在出现崩溃、卡死、资源泄漏等异常情况后,能够在没有人工介入的前提下自动恢复到正常工作状态的能力。这套能力通常由两个部分协同完成:一是探针,负责持续判断Agent当前是否健康;二是重启策略,负责在探针判定异常后决定何时重启、以什么频率重启、什么时候放弃重启。很多团队在设计常驻Agent时只做了简单的进程守护,结果遇到假死、半死不活的状态就完全无能为力。本文将从探针设计、重启策略、工程落地三个层面展开,把这套机制讲透。

一、为什么简单守护进程不够用
最朴素的想法是:用一个守护进程轮询Agent进程是否存在,不存在就拉起来。这个方案能解决进程直接退出的场景,比如段错误、OOM被杀等。但它的盲区非常明显:进程活着不代表它在干活。Agent可能因为死锁卡在某个内部循环,可能因为外部依赖故障陷入无限重试,也可能因为内存泄漏导致响应极慢但进程仍在。这些情况下进程是存在的,守护进程不会触发任何动作,而业务实际上已经中断了。
另一个常见误区是把探针做得太浅。比如只检查进程PID是否存在,或者只检测端口是否监听。端口监听只能说明网络栈还活着,不能说明业务逻辑还能正常执行。真正有效的探针应该是分层设计的,从浅到深覆盖不同维度的健康状态,每一层对应不同的故障类型和响应手段。
还有一个容易被忽视的问题是重启风暴。如果Agent启动时需要加载大量数据或建立连接,启动本身就需要几十秒,而探针间隔只有几秒,那么Agent还没完成初始化就会被判定为不健康,被反复杀死重启,永远无法进入正常状态。这说明探针和重启策略必须配合设计,不能各做各的。
二、健康探针的三个层次
第一层是存活探针,只回答一个问题:进程还在吗。在Linux上可以通过读取/proc目录或调用系统接口检查PID,也可以由Agent启动时把PID写入文件,守护方定期核对。这一层成本最低,但如前所述盲区最大,适合作为兜底而不是唯一手段。
第二层是心跳探针。Agent内部有一个独立线程,每隔固定时间向外部写入心跳,比如更新一个文件的时间戳,或者向消息队列发送一条心跳消息。守护方检查最后一条心跳的时间戳,超过阈值就判定Agent失去响应。心跳探针能发现死锁、事件循环阻塞这类进程存在但不再运行逻辑的故障,是性价比最高的一层。需要注意的是心跳线程必须与主逻辑隔离,否则主线程卡死时心跳也停了,反而失去了意义——除非你刻意让心跳与主逻辑同生共死,用来检测主逻辑是否卡住。这两种设计目标不同,要根据实际需求选。
第三层是深度探活,也叫就绪探针。守护方主动向Agent发起一次真实的业务请求,比如查询一个内部状态、执行一次轻量计算,并要求在规定时间内返回正确结果。这一层能发现端口在监听但业务已经损坏的情况,比如依赖连接池耗尽、内部状态机错乱。深度探活不宜过于频繁,因为它会消耗真实资源,一般间隔可以放宽到存活探针的几倍。
下面是一个综合三层的探针实现示例,用Python编写守护方逻辑:
import os
import time
import subprocess
import urllib.request
class AgentProber:
def __init__(self, pid_file, heartbeat_file, health_url,
heartbeat_timeout=15, probe_timeout=5):
self.pid_file = pid_file
self.heartbeat_file = heartbeat_file
self.health_url = health_url
self.heartbeat_timeout = heartbeat_timeout
self.probe_timeout = probe_timeout
def is_alive(self):
# 存活探针:检查进程是否存在
try:
with open(self.pid_file) as f:
pid = int(f.read().strip())
os.kill(pid, 0) # 信号0用于探测进程是否存在
return True
except (FileNotFoundError, ValueError, ProcessLookupError, PermissionError):
return False
def heartbeat_ok(self):
# 心跳探针:检查心跳文件是否在超时时间内更新
try:
mtime = os.path.getmtime(self.heartbeat_file)
return time.time() - mtime < self.heartbeat_timeout
except OSError:
return False
def deep_ok(self):
# 深度探活:发起一次真实的健康检查请求
try:
with urllib.request.urlopen(self.health_url, timeout=self.probe_timeout) as resp:
return resp.status == 200
except Exception:
return False
def check(self):
if not self.is_alive():
return "dead"
if not self.heartbeat_ok():
return "hang"
if not self.deep_ok():
return "broken"
return "healthy"这个实现把三种故障状态区分开:dead表示进程退出,hang表示进程假死,broken表示业务损坏。区分状态的价值在于可以采取不同的处理手段,比如假死可以先尝试发送信号让其输出诊断信息再重启,业务损坏则可以直接重启并记录详细日志。
三、重启策略的设计与选择
拿到异常状态后,怎么重启也有讲究。最简单的是固定间隔重启:发现异常立即重启,失败后等待固定时间再试。实现容易,但在故障持续存在时会以恒定频率不断消耗资源,也可能对外部依赖造成周期性冲击。
更推荐的是指数退避重启。每次重启失败后,等待时间按倍数增长,比如第一次等5秒,第二次10秒,第三次20秒,并设置一个上限,比如最大5分钟。这样既能保证短暂故障后快速恢复,又能在持续故障时避免打爆下游。配合抖动因子(在等待时间上加一个随机偏移)还能避免多个Agent实例同时重启造成惊群效应。
第三种是带断路器的策略。连续重启达到一定次数后,进入熔断状态,暂停重启一段时间或直接告警通知人工介入。这防止了那种永远修不好的故障被无限重试,也避免了重启本身掩盖问题的严重性。实践中通常会记录每次异常的现场信息,包括最后的心跳时间、进程退出码、核心线程的堆栈,供事后分析根因。
import random
import time
class RestartPolicy:
def __init__(self, base_delay=5, max_delay=300, max_consecutive=5,
breaker_cooldown=600):
self.base_delay = base_delay
self.max_delay = max_delay
self.max_consecutive = max_consecutive
self.breaker_cooldown = breaker_cooldown
self.failures = 0
def next_delay(self):
# 指数退避加随机抖动,避免惊群
delay = min(self.base_delay * (2 ** self.failures), self.max_delay)
return delay * (0.5 + random.random())
def on_failure(self):
self.failures += 1
if self.failures >= self.max_consecutive:
# 触发断路器,进入冷却期并告警
self.failures = 0
return "breaker_open", self.breaker_cooldown
return "retry", self.next_delay()
def on_success(self):
# 稳定运行足够长时间后清零失败计数
self.failures = 0重启后的观察期同样重要。Agent刚启动时处于初始化阶段,此时不应立即套用正常探针阈值,应该给一个宽限期。只有Agent稳定运行超过宽限期后,才把失败计数逐步清零,否则会出现前面提到的重启风暴。可以在守护主循环中记录每次启动时间,宽限期内只做存活探针,宽限期结束后再启用心跳和深度探活。
四、工程落地的几点建议
首先是把诊断信息留在重启之前。守护方在杀死假死进程前,最好先通过信号触发其打印线程堆栈,或者直接调用系统工具抓取核心转储,否则重启之后现场就没了,问题永远查不清。其次是区分重启原因并分别计数告警,频繁假死和频繁崩溃往往指向完全不同的根因,前者多是死锁或资源阻塞,后者多是内存问题或依赖库缺陷。
其次要考虑有状态Agent的恢复问题。重启意味着内存状态丢失,如果Agent维护着任务队列或会话数据,自愈机制必须配合持久化设计,在启动时从检查点恢复,否则自愈只是让一个空壳活了过来,业务上等于没恢复。最后,探针和重启参数没有万能值,心跳超时应该大于正常GC停顿和网络抖动,重启间隔应该大于启动耗时,这些都要根据实际观测数据调整,并在上线后持续根据误报率优化阈值。
把探针的分层检测、状态分类、退避重启、断路保护和诊断留痕这几件事做扎实,Agent的自愈能力就从口号变成了可靠的工程保障。它不能替代根因修复,但能在你修复问题之前,让系统保持基本可用的状态,为排障争取宝贵的时间窗口。