自动化运维Agent实践:hermes-agent设计与实现

发布时间:2026/9/8 18:12:31
自动化运维Agent实践:hermes-agent设计与实现 前阵子我重构了一版内部用的自动化运维代理框架名字就叫hermes-agent。起因很简单团队里服务器数量上来之后每天被各种监控告警、重复性工单、半夜的故障电话搞得疲惫不堪很多操作明明可以自动完成却非要等人去登录机器敲命令。于是我就花了两周时间把一个能“替人跑腿”的小代理框架做了出来。这篇文章把我踩过的坑、设计时的取舍、具体的实现细节全部整理出来如果你也在纠结“到底要不要搞一个自动化agent”这篇应该能帮你少走不少弯路。这个hermes-agent解决的核心问题是把分散在监控系统、告警通知、自动执行、工单处理这些环节里的重复劳动收敛到一个统一框架里。它不是一个什么高大上的平台而是一个轻量、可扩展的“中间代理层”上游接收各类事件监控告警、定时任务、外部webhook中间经过一套规则引擎做判断下游则执行脚本、调用API、发通知或者拉起Ansible。整套设计围绕“可靠执行”而非“复杂编排”展开非常适合中小团队从零搭建自己的自动化运维体系。无论你是运维、后端还是SRE这篇文章里的思路和代码都能直接借鉴。1. 为什么我会自己写一个hermes-agent1.1 先说说我当时的痛点团队规模不大服务器大概二十几台数据库、缓存、队列、应用服务都有。最初我们也是“人肉看监控”的模式Prometheus报警打到钉钉群里然后值班的人登录跳板机看一眼指标重启一下服务再贴个“已处理”。这个流程看起来没毛病但我统计了一下一个月下来有将近60%的告警是重复的——同样是磁盘空间不足、同样是进程挂掉、同样是SSL证书快过期。也就是说大量时间花在了机械操作上。而且这种操作还有个隐患一旦人处理的时候手忙脚乱很容易敲错命令比如把旧容器删了、把配置文件覆盖错了。早就有想法搞一套自动化的处理机制但一直没找到合适的开源项目——不是太重像完整的运维平台就是太轻只能发发通知不能执行命令。权衡之后我决定自己写一个目标很明确第一事件进来后能按规则自动匹配处理第二匹配后能安全地执行脚本或命令第三整个过程留痕可追溯。1.2 为什么叫hermes-agent起名的时候参考了希腊神话里信使神Hermes这个框架的核心职责其实就是“传递消息、执行指令”——把监控系统的事件转换成运维动作。更准确地说它是一个介于“感知层”监控、日志、定时器和“操作层”脚本、API、配置工具之间的agent所以取名hermes-agent。当然另一个原因是当时想给项目做个开源版本名字最好短一点、好记一点搜一下也不会和别人的项目撞太多。在实际开发中我并没有用任何Agent框架核心就是一个事件循环加规则匹配器代码量控制在三千行左右尽量保证每加一个新监控项只需要改规则配置不改代码。2. 整体设计思路与模块拆解2.1 核心模块一览在设计一个agent时最先要定的是“有几个核心模块”。我参考了自己之前写定时任务、告警机器人、发布系统的经验最终把hermes-agent拆成了五个核心模块事件接入层、规则引擎、执行器、通知模块、审计与状态存储。事件接入层负责接收各种来源的触发信号。比如Prometheus webhook、Zabbix告警、定时任务cron触发、外部业务系统发来的HTTP POST等。这里我只做了一件事就是把各种异构消息统一成一个内部事件对象后续所有的规则判断都基于这个对象进行。规则引擎这是agent的核心决定了一个事件该忽略、该通知、该执行修复还是只记录。规则采用YAML配置可以灵活组合事件类型、事件内容的关键字、数值阈值、时间段等条件。执行器真正干活的模块。根据规则决定执行的动作可能是一个shell脚本、Python脚本、发送一个HTTP请求或者通过SSH到远程机器执行。为了安全执行器做了超时控制、并发控制、输出捕获、退出码校验。通知模块把执行结果、事件信息推送到钉钉、飞书、企业微信或邮件。这里我刻意把通知和执行分开避免一套代码里既执行又要发消息导致逻辑混乱。审计与状态存储所有进入系统的事件和执行结果都需要落库至少记录事件原文、规则ID、执行动作、返回码、耗时、操作人如果是手动触发这些字段。我用了SQLite做存储后续接入MySQL也很容易。2.2 数据流与关键设计取舍先说数据流。一次完整的处理流程是这样的Prometheus告警器发现QPS异常向hermes-agent的/api/v1/event接口推送一条JSON消息框架把消息转换成内部事件对象通过规则引擎逐条匹配命中的规则ID是fix_high_qps配置的动作是“执行scripts/restart_gateway.sh”执行器开始运行同时在任务队列里登记脚本返回码为0通知模块把“已重启gateway”发到钉钉群审计记录标记为成功。几个设计取舍我特别强调一下。第一规则引擎不搞“自动化流程编排”它只做“事件-动作”的映射编排粒度是单个脚本或单个API调用。这样做的原因是团队小、场景明确复杂编排引入的状态一致性问题反而会带来维护成本。第二执行器和通知模块之间不要共用线程池执行器用独立进程池避免一个慢脚本卡住所有通知。第三规则匹配设计成“第一条命中即生效”这在最初看起来有点简陋但实际用下来反而让配置更简单不容易出现规则相互覆盖的情况。还有一个容易被忽视的点消息去重与聚合。监控告警经常会抖动比如CPU瞬时飙高又立刻下降如果不做聚合一个晚上能收到几百条重复消息。我设计了一个滑动窗口去重器默认窗口30秒内同类型的告警只能触发一次如果后续告警状态升级比如从warning变成critical可以立即打破窗口重新触发。3. 规则引擎与执行器的具体实现3.1 规则配置格式说明hermes-agent的规则配置是YAML格式我将它设计成三个主要字段when匹配条件、then动作、log是否记录审计日志。下面是一个典型配置片段。rules: - id: disk_space_warning name: 磁盘空间不足自动清理 when: event_type: prometheus_alert alert_name: DiskSpaceWarning percent: 80 host: web-* then: action: script script: /opt/hermes-agent/scripts/clean_disk.sh params: threshold: 80 timeout: 60 log: true - id: process_down_restart name: 核心进程挂掉自动拉起 when: event_type: zabbix_alert alert_name: process_status process_name: nginx status: down then: action: shell command: systemctl restart nginx systemctl status nginx timeout: 30 log: true这里要注意一个原则规则里的判断条件要尽量做得“严格”也就是宁可不触发也不要误触发。比如磁盘清理脚本必须同时满足百分比大于80%、主机名匹配web-*、告警名称是DiskSpaceWarning才执行。这么做是因为自动执行脚本的风险比自动发送通知高一个量级误执行一个清理脚本可能会导致数据删除这个教训在后面会有详细说明。3.2 规则引擎匹配逻辑匹配逻辑我用纯Python实现没有引入Drools这类重量级规则引擎核心代码如下。import fnmatch import operator _COMPARE_OPS { : operator.gt, : operator.ge, : operator.lt, : operator.le, : operator.eq, !: operator.ne, } def match_rule(event: dict, rule: dict) - bool: conditions rule[when] for key, expected in conditions.items(): actual event.get(key) if actual is None: return False if isinstance(expected, str) and expected.startswith((, , , , , !)): op_str, _, raw_num expected.partition( ) try: op _COMPARE_OPS[op_str] if not op(float(actual), float(raw_num)): return False except ValueError: return False else: # 支持通配符匹配主机名、告警名等 if not fnmatch.fnmatch(str(actual), str(expected)): return False return True这段代码看起来简单但有几个细节很关键。对于数值类型的条件通过解析字符串中的比较操作符统一转成float再比较避免类型不一致导致的问题。对于字符串类型用fnmatch做通配符匹配这样web-*就能匹配web-01、web-02。事件里缺失某个字段时直接返回不匹配避免因为数据不全会走默认动作。这个策略在实际运行中减少了大量误报。匹配顺序上我按YAML文件里的定义顺序逐条跑第一条命中的规则生效。我还在框架里加了一个fallback_action字段如果跑完所有规则都没有命中默认执行“记录日志并通知管理员”而不是静默丢弃。这个设计帮我发现了很多配置写错的场景。3.3 执行器与并发控制执行器需要处理的是“外部命令脚本”要特别关注进程安全和资源占用。我使用Python的subprocess模块统一设置timeout、环境变量和输出捕获。为了避免一个脚本把整个agent打挂执行器改造为基于concurrent.futures.ProcessPoolExecutor的进程池模型最大并发数默认为4可以通过环境变量HERMES_MAX_WORKERS调节。import os import subprocess from concurrent.futures import ProcessPoolExecutor, as_completed MAX_WORKERS int(os.getenv(HERMES_MAX_WORKERS, 4)) _executor ProcessPoolExecutor(max_workersMAX_WORKERS) def run_command(command: str, timeout: int 30): def _run(): proc subprocess.run( command, shellTrue, capture_outputTrue, textTrue, timeouttimeout, env{PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}, ) return proc.returncode, proc.stdout[-2000:], proc.stderr[-2000:] future _executor.submit(_run) return future.result(timeouttimeout 5)这里有一个非常容易被忽视的坑shellTrue意味着命令会走shell解析如果命令里拼接了外部传入的参数可能会有注入风险。我的处理方式是框架提供了params白名单机制只有规则配置里明确声明的参数才会被替换到命令行中而且参数值会做转义。如果你要在生产环境直接套用务必把参数白名单注释读一遍不要直接信任任何外部传入的字符串。另外所有执行记录都要写审计表至少包含执行ID、规则ID、执行命令脱敏后的、开始时间、结束时间、返回码、输出摘要。这里脱敏指的是命令中如果包含密码、token等信息要替换成***再存库否则审计日志一旦泄露问题会非常严重。4. 从采集到通知的完整链路搭建4.1 事件接入层接入层我设计成了一个轻量HTTP服务只提供两个接口POST /api/v1/event和GET /api/v1/health。前一个接口用于接收外部系统的事件推送后一个用于探活。事件推送支持两种格式JSON裸格式和Prometheus Alertmanager标准格式。内部会做归一化处理转成统一结构。下面是一个Prometheus Alertmanager风格的webhook示例。{ status: firing, alerts: [ { labels: { alertname: DiskSpaceWarning, host: web-01, percent: 85 }, annotations: { summary: disk usage is high } } ] }归一化之后内部事件对象大致是这样的。{ event_type: prometheus_alert, alert_name: DiskSpaceWarning, host: web-01, percent: 85, status: firing, raw: ..., received_ts: 1700000000, event_id: uuid }在做归一化时我花了不少时间处理“告警恢复”的场景。因为Alertmanager在问题消除后会推送resolved状态如果规则不管状态同一个告警会有两条记录一条firing一条resolved如果不处理agent可能既执行一次修复又发送一次通知造成混乱。我的处理是如果status是resolved框架不执行修复动作只做通知和记录。4.2 通知模块与多通道支持通知模块我做了接口抽象方便接入不同通道。接口长这样class Notifier(ABC): abstractmethod def send(self, title: str, content: str, level: str info) - bool: ...钉钉、飞书、企业微信、邮件都实现了这个接口。比如钉钉机器人通过自定义Webhook发送内容用Markdown格式邮件走SMTP。使用上我通常会根据事件级别做通道策略critical级别同时发钉钉和电话如果接了电话报警网关warning级别只发钉钉info级别只记录审计。这套分级策略能有效减少“通知轰炸”。有一个体验上的小优化让我觉得特别值得做同一个事件ID如果在一小时内重复触发通知文案里会带“第N次触发”和上次触发时间。这样一来运维群里看到的就是“这个告警已经反复出现三次了”而不是刷屏。这比单纯的去重更直观。4.3 审计日志存储与查询说到存储SQLite其实完全够用但我初期设计上就留好了MySQL切换的能力。数据访问层用SQLAlchemy表结构主要两张events和executions。events存所有进入系统的事件executions存所有执行动作两表通过event_id关联。查询时最常用的两个维度是时间范围和规则ID。索引设计上events表对received_ts、alert_name、host做了组合索引executions表对rule_id、start_time做了索引。数据量在每天几万条量级内查询基本是毫秒级。如果你担心SQLite在大并发下的锁问题其实在agent这种低写入量场景下每秒几十条事件已经很高了简单用WAL模式就能满足需求。我是在建表时执行了PRAGMA journal_modeWAL实际跑了几个月没有遇到锁等待问题。5. 部署、试运行与日常运维5.1 部署方式Docker为主hermes-agent我打成了一个Docker镜像方便在不同机器之间迁移。Dockerfile不长核心是把Python运行时、依赖库、脚本目录装好然后启动python main.py。这里有一个初期踩过的坑容器内执行systemctl restart nginx这类命令在容器里不适用因为它没有systemd。所以我在设计执行器时就留了两种方式本地执行和SSH远程执行。本地执行适合agent和你管理的机器在同一台宿主机或同一容器内SSH远程执行适合agent集中部署管理多个远程机器。我后来把SSH远程执行作为主模式来实现配置里面支持host和user字段针对不同的主机组配置不同的SSH密钥。rules: - id: remote_restart_nginx when: event_type: prometheus_alert alert_name: NginxDown then: action: ssh host: web-* user: deploy command: sudo systemctl restart nginx timeout: 30 log: trueSSH执行模块基于paramiko实现使用密钥文件认证不允许用密码登录。每个连接做了超时和连接池避免频繁握手。一次执行时长超过30秒会自动kill并且从进程树看所有子进程也会一并终止防止僵尸进程。5.2 试运行三个阶段的建议把agent接入生产之前我建议分三步走。第一步只通知不执行。把所有规则里的then.action换成notify看规则匹配的准确性。这一步通常能暴露不少问题比如告警名称和预期不一致、字段名大小写不对等。第二步模拟执行。也就是在测试环境里把同样的告警发一遍执行器接上看脚本是否正常返回通知是否正确发出去。第三步灰度上线。先从一两条低风险的规则开始比如证书过期检查通知、磁盘清理加了容量阈值和目录白名单跑一两个星期确认没有误触发后再把重启、拉起这类操作升级为自动执行。我自己在试运行期间的切身体会是宁可动作慢一步也别自动执行出问题。比如自动重启nginx如果检测条件少了一个就可能在业务高峰期把正常的服务重启一遍影响会非常直接。5.3 日常维护配置变更、日志与监控agent本身也需要被监控。我给它增加了一个/api/v1/health接口返回进程状态、最近一次事件处理时间、执行队列深度。这个接口接到了Prometheus的探针里一旦agent挂了Prometheus会告警。此外agent自身的日志输出到stdout和文件两个地方文件按天滚动保留30天。配置变更我推荐用Git管理。规则文件、脚本文件、通知模板都提交到仓库修改后通过CI/CD构建镜像、发布到测试环境验证通过再推到生产。这么做也许在初期看起来繁琐但一旦规则数量超过十条人工维护YAML就会开始出错Git的审计能力这时候会救命。6. 实际运行中那些必踩的坑6.1 常见故障与排查速查表在与hermes-agent长时间打交道的过程中我大概积累了四类常见故障。把它们整理成一个表格方便你对照排查。故障现象可能原因排查步骤解决办法事件推送后规则未触发事件字段名与规则不一致查看events表原文对比字段名用curl手动推一条测试消息在规则中修正字段名或调整归一化逻辑脚本执行超时被kill脚本内部有长时间等待或死循环先手动执行脚本确认耗时查看输出末尾是否有卡住的日志调整脚本逻辑增加timeout值并按需拆分动作通知发送失败Webhook地址失效或网络不通查看agent日志中通知报错手动curl测试webhook更新webhook配置确认网络策略允许出口重复执行同一个修复动作同一事件被多次推送或者去重窗口太小查看events表重复记录检查去重窗口配置调大滑动窗口在脚本侧增加幂等判断6.2 幂等性设计这个坑最疼我在第二版迭代时给“进程拉起”类脚本加了一层幂等判断。举个例子自动清理磁盘的脚本如果第一次执行时磁盘占用率从90%降到80%清理逻辑已经生效了但监控平台又在几分钟内推了一条相同告警第二次执行时磁盘占用率其实已经正常了脚本如果直接再跑一遍可能会把不该清理的临时文件删除。所以现在所有执行动作前必须在脚本里先做“是否还需要执行”的判断。比如清理磁盘前检查当前使用率如果低于阈值就直接exit 0。进程拉起前检查进程是否已在运行已运行则直接返回。这个习惯在所有自动化系统里都很重要但尤其适合agent这种规则驱动的场景因为事件重复推送太常见了。6.3 安全边界与权限最小化自动化和安全天然是一对需要平衡的关系。我的原则是给agent的最小权限而不是最大权限。SSH执行远程命令时专门创建了一个hermes用户只授权了部分命令的sudo权限比如systemctl restart nginx、df -h、tail这些而不是直接给root。在配置里也尽量把执行参数限制在固定集合内避免用户传任意命令。另一个容易忽略的点是脚本内部的敏感信息管理。某些脚本可能要连数据库需要密码。我建议统一用环境变量传给执行进程不要写在脚本文件里脚本文件会传给代码仓库环境变量存在独立的secrets文件权限设为600。另外在日志脱敏方面接入了filter_secret函数对所有输出文本做正则替换像passwordxxx、tokenyyy这些都会被替换成***。7. 从自动化脚本向AI Agent演进的思考7.1 为什么我决定在第二版接入LLM很多自动化框架做到“规则驱动”就到顶了但我在部署hermes-agent半年后发现还是有相当一部分工单无法通过规则覆盖。比如开发者提交了一个“帮我查看订单服务日志”的需求这在规则里没法穷举。于是我开始思考能不能把agent和LLM结合起来让模型理解自然语言工单把意图转换成对应的脚本或API调用。目前我实现的方式比较简单工单进来后先将文本发给LLM做一次意图识别输出一个结构化JSON包含动作类型run_script、query_api、send_notification和参数然后由hermes-agent执行器来真正执行并校验执行结果。LLM只负责“翻译”不负责“执行”和“保证正确性”。这个分工很重要因为LLM可能会编造不存在的命令或参数但我们在执行层有白名单和超时限制风险可控。7.2 给agent加上“状态感知”能力规则驱动的一个缺陷是“无状态”agent不知道上一次动作是否真的让系统恢复了。我在新版本里加了一个简单的健康状态检查模块规则执行完后agent会主动调用一次目标服务的健康检查接口确认确实恢复后才发送“处理成功”的通知如果检查失败则触发升级通知把人工叫进来。这一步带来的体验提升非常明显。之前自动化处理后运维还得自行确认问题是否真的修复了现在agent自己会做闭环确认。初期我只对HTTP接口类服务做了这个功能后续计划扩展到数据库、缓存等组件的状态探测。7.3 未来演进方向多Agent协作和可观测性单agent处理单事件是现阶段的主要模式但真实运维场景往往更复杂比如一个服务不可用背后可能涉及依赖的数据库、配置中心、网络负载均衡多个组件。后续我想尝试把agent拆成多个子agent每个负责一类组件相互之间通过事件总线通信和协调。另一个方向是可观测性。目前agent的审计数据已经比较完整但缺少一个直观的看板。我计划用Grafana接上审计库展示事件处理量、规则命中率、脚本执行成功率、平均处理耗时这些指标。这些数据不仅用于运维更能反过来优化规则配置比如命中率为0的规则基本可以删掉了。我在实际使用中最大的体会是自动化agent的关键不是代码量而是对“哪些动作可以自动执行、哪些必须经过人”的边界感。hermes-agent能走多远取决于你对规则设计得有多仔细、权限控制得有多到位。如果你正准备做一个类似的东西我的建议是先从一个最小的闭环开始接一种告警、写一个脚本、通知到人跑通之后再逐步加规则千万别一上来就追求大而全。等到规则积累到一定量级你会发现运维工作的重心会从“救火”转移到“优化规则”上这本身就是一种巨大的效率提升。

相关新闻