
一直混在 K8s 集群里做运维的人多少都被 EFK 这套组合拳锤过。Elasticsearch 存日志、Fluentd 收日志、Kibana 负责展示和触发告警链路本身成熟稳重可它吐出来的告警消息实在不是给人看的。一坨 JSON、一段堆栈、一串指标名机器一秒能读懂人得先在脑子里跑一遍“阅读理解”再打开控制台翻半天才能确认到底发生了什么。赶上半夜被叫醒脑子糊成一团的时候这种体验尤其酸爽。所以我动手做了一个“智能拦截器”把它插在告警发送链路中间用 AIOps 的思路把 EFK 生成的原始告警做标准化、富化再交给大模型转译成正常人能秒懂的“人话”。这篇博文就完整记录我在 EKS 上落地这套系统的全过程为什么需要它、它放在哪一层、提示词怎么设计、部署时有哪些坑。如果你也是 SRE、DevOps 或平台工程师正在被告警噪音折磨这篇内容应该能帮你少走不少弯路。1. 为什么要给 EFK 告警加一个“翻译官”1.1 告警疲劳是一种很具体的痛先看看 EFK 原生的告警长什么样。有一次我们的订单服务 Pod 反复重启Kibana 告警推出来一条类似这样的消息{ severity: critical, title: Kubernetes pod restarting, namespace: orders-prod, pod: orders-api-7d8c9f6b5c-2kxrt, container: main, restart_count: 12, message: Container main in pod orders-api-7d8c9f6b5c-2kxrt is restarting. Last termination reason: OOMKilled. Memory request: 512Mi. Memory limit: 1Gi. }这条告警的信息其实已经很全了但值班同学收到后的第一反应是连环追问OOMKilled内存超了哪个服务影响面多大要不要马上处理是不是刚发版改了内存配置每一个问题都得再去 Kibana 和监控里查一圈原本一条消息就能定性的事硬生生拖成了五到十分钟的排查。这还算好的。真正让人崩溃的是告警风暴某个版本引入内存泄漏几十个 Pod 在一个小时内陆续重启IM 群里开始刷屏每一条看起来都差不多人眼根本分不清优先级。我统计过我们集群一个月的告警数据大约 65% 的告警在人类值班视角下可以归入“可合并、可升级、可忽略”三类但系统并没有帮我们分类它一股脑全发。这就是告警疲劳的本质——不是缺告警而是缺一个“信息简化层”。1.2 智能拦截器要解决的核心矛盾我要做的不是再写一套告警规则而是在已有 EFK 告警这个“事实”之上加一层“理解”。一个完整的拦截器至少要做四件事才能把问题真正消化掉标准化把来自 Kibana、云监控、Prometheus 的告警统一成一个结构而不是各自为政的 JSON。富化从 Kubernetes API 和 Elasticsearch 里拉取关联信息比如资源使用趋势、最近的部署事件、同命名空间的其他异常。转译调用大模型把技术信息翻译成自然语言并附上影响判断、紧急程度和处理步骤建议。分发按严重级别和业务域路由到对应群组或责任人并自动附带线索链接。这四点合起来就是一个典型的 AIOps 小闭环。它不取代任何成熟的日志或监控组件而是恰好补齐了从“系统发现问题”到“人快速理解问题”之间的断层。我当时盘算了一下如果能在不改造 EFK 的前提下只新增一个独立的旁路服务就能把告警体验彻底换掉这应该是性价比最高的方案。于是“智能拦截器”这个定位就被定了下来。1.3 适合它的前提条件也不是所有团队都需要这个“翻译官”。我判断一个团队适不适合做这套东西主要看三点告警量已经明显超出人工处理能力群消息每天都在刷屏告警中有效信息密度低大家看到消息还是得再去翻系统团队有愿意维护一条 Python/Go 旁路服务的能力和精力。如果只是个小集群、一天几条告警那完全没必要上。直接建几个 Kibana 告警规则配好 IM 通知就够了。我们在动手前是已经越过了这个阈值才下决心的。这个判断很关键因为一旦做了它就是一个需要持续维护的“新系统”如果告警量本身不大投入产出比会非常难看。2. 整体架构与控制流设计2.1 EFK 告警链路回顾先把 EFK 的链路按我们生产环境的实际部署走一遍。Fluentd 以 DaemonSet 方式跑在每个节点上采集容器日志并打上 Kubernetes 元数据标签命名空间、Pod 名、容器名然后写入 Elasticsearch。Kibana 在上面做索引管理和可视化同时通过告警规则定时查询 Elasticsearch命中阈值就触发 Webhook 输出。在默认配置里这个 Webhook 会直接打到 IM 机器人或者邮件网关。我做的拦截器就挂在 Webhook 这个出口位置。Kibana 的告警规则保持不变但 Webhook 地址从原来的 IM 机器人改成拦截器服务的地址。拦截器收到消息后经过分析处理再把加工过的内容推给最终的接收端。这里有一个关键认知拦截器一旦上线就变成了告警链路上的“必过节点”。如果它挂掉告警就没法送达。所以在架构上我把它做成了无状态 HTTP 服务前面套负载均衡后端挂了会自动重启同时保留了降级策略这些会在后面详细说。2.2 拦截器如何“缝进”现有链路用文字描述部署拓扑大概是这样的EKS 集群里新建一个独立命名空间里面跑着一个 Deployment副本数 3承载拦截器的 HTTP API。它后面连着三样东西Elasticsearch 查历史日志上下文、Kubernetes API 查 Pod/Deployment 实时状态、大模型 API 做自然语言生成。Kibana 告警 Webhook 指向这个 Service处理完成后由拦截器继续转发到 IM 机器人形成三段式链路Kibana Webhook - 拦截器 Service - 大模型 API - IM 通知 / 工单系统为什么用这种旁路设计而不是直接改 Kibana 插件因为 Kibana 告警的输出格式相对固定想在里边塞太多逻辑会非常受限。独立服务则完全可控可以用 Python 生态里任何想要的库也可以随时升级提示词和分析策略不需要跟着 Kibana 的发布节奏走。对于真正做过告警系统的人来说这种“可独立升级”的特性长期来看价值巨大。2.3 用拦截而不是替代的四个理由很多人会问为什么不直接在告警规则端就写好人类可读的模板我的经验是模板能解决一部分问题但很难覆盖复杂场景。第一Kibana 告警模板的能力有限。字段拼接可以做但语义归纳、趋势判断、关联历史完全做不了。说白了模板是“死”的而我们需要的是“活”的分析。第二告警规则分布在多个系统只改 Kibana 管不了云监控和 Prometheus 的告警。在拦截器这里做“汇流”才是一劳永逸的解法。第三模板的输出是固定格式而拦截器可以承载排查建议、处理步骤这些内容甚至能针对不同接收人调整详略程度。第四拦截器可以积累反馈素材。随着处理历史增多它能告诉团队“这类告警在过去一周出现了 N 次这次和上次的关联是什么”这是模板给不了的洞察。3. 核心实现告警富化与“说人话”管线3.1 告警数据的标准化与上下文关联拦截器收到 Kibana Webhook 后第一步不是调用大模型而是把原始消息洗成一个统一的“告警事实”对象。这步看起来简单但解决了很实际的问题告警的“去重键”。def normalize_alert(raw: dict) - dict: return { alert_id: hashlib.sha1( f{raw.get(rule_name)}-{raw.get(namespace)}-{raw.get(pod)}.encode() ).hexdigest(), source: kibana if raw.get(source) alerting else raw.get(source), severity: raw.get(severity, info), namespace: raw.get(namespace, ), resource: raw.get(resource, ), summary: raw.get(message, ), raw: raw, received_at: datetime.utcnow().isoformat(), }我在实践中发现同一个 Pod 在短时间内反复重启会触发多条 Kibana 告警它们的 rule_name、namespace、pod 完全一样只是时间戳不同。如果不做合并一个晚上能收到几十条重复告警。所以我在标准化阶段就把这些字段拼成 alert_id再放进一个内存滑动窗口做聚合相同 ID 在 10 分钟内只保留第一条后续进来的只更新 count 和最后时间。然后是富化。拿到标准化对象后拦截器会去查三类信息从 Kubernetes API 取当前 Pod 的重启次数、最近终止原因、所在节点状态从 Elasticsearch 取过去 30 分钟这个命名空间的错误日志条目数变化趋势从内部发布系统取最近一条部署记录看是否刚做过变更。把这三类信息塞进同一个 JSON 并暂存序列化成上下文对象后面大模型要用到的“素材”就都在这里了。富化之所以必要是因为 EFK 原始告警通常不带“变化趋势”和“变更事件”而这两样恰恰是根因判断最值钱的部分。没有富化的告警就像一张没有时间轴的病历医生很难准确诊断。3.2 提示词工程把日志上下文炒成“人话”到这里告警已经从“一行消息”变成了“半结构化事实”。接下来就是核心环节让大模型把信息转译成自然语言。我设计的提示词并不是简单说一句“请解释这段告警”而是尽量结构化输出参考模板如下你是一名资深 SRE。请根据以下告警上下文用中文输出一段简洁的告警说明。要求 1. 用自然语言说明当前发生了什么不超过80字。 2. 用一句话判断可能的影响面例如订单服务不可用、库存同步延迟。 3. 列出最可能的两个根因假设并按概率排序。 4. 给出下一步排查建议不超过3条要可以执行。 5. 如果上下文中有部署记录说明该告警与最近变更的关联性。 禁止编造上下文中不存在的信息。上下文如下 {alert_context}我把 alert_context 以 JSON 形式整体贴进去。实测下来这种结构化输出的质量比开放式“帮我看看这个告警”稳定得多。几个细节很关键明确要求用中文输出避免模型跟着英文原文走明确输出结构避免模型即兴发挥用“禁止编造”约束幻觉降低错误根因的概率提示词里“你是资深 SRE”角色设定能让模型的措辞明显更专业也更贴合运维场景。调参经验也分享一下temperature 设置为 0.2 到 0.3 效果最好。设太高0.7 以上同一个告警每次生成的排查建议都不太一样团队会怀疑系统的稳定性设太低0又会让措辞过于机械读起来像是机器翻译。0.2 到 0.3 这个区间既能保留自然感又不会太飘。3.3 服务部署与弹性策略拦截器服务本身放在 EKS 的一个独立命名空间里用 Deployment 运行副本数 3。资源 requests 是 0.5 vCPU 和 512Mi 内存limits 给到 1 vCPU 和 1Gi防止某个异常告警把整台机器的内存打爆。服务通过 ClusterIP 暴露Kibana 的 Webhook 端点和 IM 机器人只认内网地址所以不需要把 Ingress 暴露到公网安全面上也省了很多心。真正需要仔细设计的是“弹性那一刻”。Kibana 告警规则默认每分钟跑一次如果集群发生大规模故障一次可能触发几十上百条 Webhook 请求。拦截器是同步等待大模型返回的如果每次都串行调用响应时间会拉得很长。我在实现里做了一个简单但有效的设计后端用线程池并发调用大模型接口最多 8 个并发单次超时设 15 秒超过就回退成“原文 标准化信息”直接转发保证告警不丢失使用内存队列缓存待处理告警消费端和控制端解耦对同一 alert_id 的合并逻辑放在最前端减少无效请求。这里要强调一个原则告警链路最怕的就是拦截器成为新的故障点。所以我把“转译失败时的降级”做成一个完整的分支大模型不可用、超时或返回为空时拦截器会把原始 JSON 整理成人能勉强读懂的格式化文本照常发给 IM 群同时打上“AI 处理失败”的标记。这样最坏情况也就是回到原来的状态不会因为拦截器的错误而弄丢真正的告警。4. 我在实操中踩过的坑与排查实录4.1 告警风暴把大模型接口打爆之后第一次上线我们只部署了 2 个副本线程池上限 12。结果第二天就遇到一次真实的故障演练某个服务被压测打爆Pod 大规模重启Kibana 在一个周期里推了 70 多条告警进来。拦截器虽然做了 5 分钟聚合但 alert_id 的滑动窗口主要针对重复告警面对一大批不同 Pod 的告警还是同时发起了大量并发调用。那天最大的教训是大模型 API 是有限流的。我在本地用长连接测速度挺快到了生产环境几十个并发同时打过去直接触发限流回来一堆 429。好在降级分支生效了告警没有被吞但“说人话”的效果大打折扣群里瞬间又变回满屏技术原文。修改方案是把线程池从 12 降到 6同时在调用方加了令牌桶限流每秒最多 5 个请求超过的先进重试队列用指数退避慢慢消化。另外我把 Kibana 侧的告警规则加了“分组聚合”配置让相同命名空间下、相同类型的告警在推送前先合并成一条。这样即使后端故障到达拦截器的请求量也会被控制在一个安全水位不会再把外部接口打死。4.2 上下文“串场”导致的误判第二个坑很隐蔽不是故障而是结论错。有段时间我们每隔几天会收到拦截器输出的告警建议看起来头头是道但点开详情一看根因假设和真实情况差了十万八千里。比如把“内存不足”归因成“发布代码变更”但实际上呢是下游数据库连接池被耗光导致的间接 OOM。排查下来问题出在富化逻辑我在拉取最近部署记录时用的是一段简单的时间窗口过滤取“过去 30 分钟内该命名空间最近一条部署事件”。但在多团队共用一个集群的场景下同一个命名空间里可能同时存在多个应用的发布流水线部署记录的归属完全不精确。A 服务出了问题富化信息却把 B 服务的发布记录放进了上下文大模型自然就被带偏了。修复方式很朴素把“部署记录”的归属粒度从命名空间精确到工作负载Deployment/StatefulSet。先根据告警里的 pod 字段推导出 owner workload再针对这个 workload 拉取变更历史。同时富化字段增加了置信度标记比如 deployment_info.confidence: 0.83让大模型明确知道哪些信息是强关联、哪些只是参考。这是一个非常值得注意的细节上下文不是越多越好关键是“归属正确”。4.3 从单向转发到复盘的一体化闭环最后一个想聊的坑是“信息闭环”。最初版本的拦截器是单向地把告警从 Kibana 转发到 IM它在转译过程中调用过大模型、查过 Elasticsearch但所有这些处理结果都没有被保存下来。结果就是同一个问题第二次发生时拦截器还是“第一次见面”完全没有记忆。后来我加了一个“处理记录”模块每当拦截器完成一次转译就把标准化告警、富化上下文、大模型输出、最终分发路径追加到一个专门的 Elasticsearch 索引里用 alert_id 做聚合键。这样下次遇到相同 alert_id 的告警时拦截器会先检查历史如果发现过去 24 小时内出现过同样的实体就把“最近一次发生时间、上次的处理结论、是否已解决”作为附加信息一起塞给大模型。这个改动看起来轻但对效果的提升非常明显。它让拦截器从一个“翻译器”慢慢变成了一个“会积累经验的协作者”也让团队在复盘时可以直接按 alert_id 检索历史不用再翻 IM 聊天记录回忆上周到底发生了什么。如果你也想做类似系统我强烈建议把“处理记录”作为第一版功能而不是后面再加。5. 效果评估与后续扩展思考5.1 告警有效性与恢复时间的变化运行了大约三个月之后我拉了几组数字做对比。不列具体数据只看几个明显的趋势指标改造前改造后群内告警条数告警风暴时每小时可上百条聚合后下降约 40%从通知到判断是否介入中位数约 8 分钟中位数约 2 分钟排障平均耗时MTTR依赖人工逐条查日志整体下降约 30%告警消息可读性满屏 JSON 和堆栈自然语言 影响面 建议当然这些数字受团队熟练度、集群规模影响很大我列出来的目的不是论证“拦截器一定能让 MTTR 下降”而是想说明当告警被结构化理解之后人的精力可以更快聚焦到真正需要判断的问题上。值班同学从“红着眼查日志”变成了“看着 AI 给的假设去验证”这种工作方式的转变才是 AIOps 真正值钱的地方。5.2 值得继续深挖的三个方向目前我想到的三个后续方向也一并分享第一接入更多告警源。现在只接了 Kibana、云监控和部分 Prometheus 规则下一步想把容器资源配额、Ingress 错误率这类指标也纳入统一处理。拦截器的价值会随数据源的增多而增大因为跨系统的关联分析能力是它相比单点告警规则的核心优势。第二做成告警根因知识图谱。目前用的还是“本次上下文 历史记录”这个轻量模式如果能把服务依赖关系、配置变更、代码发布这些维度直接建模根因推断的准确度会有一个本质提升。这一步工程量不小但方向是明确的。第三把人工反馈机制加进去。当值班同学点选了“处理建议有效”或“处理建议无效”时这些反馈应该回流到提示词或检索权重里形成真正的 AIOps 学习闭环。目前拦截器还只是单向消耗知识如果能把“人的判断”作为训练信号写回系统它能越来越懂这个集群的脾气。5.3 给想动手的人的三个实用建议如果你也想在自己的集群里搞一套类似的系统有三条建议是花钱都买不来的第一先跑通最小闭环再谈优化。第一版别追求根因分析多准、知识图谱多全就把“Kibana 告警 - Webhook - Python 服务 - 大模型 - IM”这条链路跑通哪怕输出只有 50 字也比原来满屏 JSON 强一百倍。第二告警平台和开发平台要分开容灾。拦截器本身要低耦合降级策略要提前写好并压测过否则它上线那天的故障演练就会变成“故障演练 告警中继故障”双重灾难。第三一定要记录每一次转译的输入和输出。这些数据既是复盘的依据也是后续优化提示词、训练更专用模型的素材从一开始就攒着绝不亏。另外提醒一句选择大模型接口时不要盲目追求最大的模型。告警转译这个任务上下文不大、逻辑不复杂一个速度快的轻量模型反而比重型模型更适合。延迟降下来值班人的体验才会真正好起来。我们后面还把模型调用按告警严重级别分了级紧急告警用快速模型普通告警用更经济的模型整体成本能省不少。最后再分享一点个人体会。AIOps 这个词看起来很大但落到地上往往就是这么一个小服务、一套提示词和一些埋点数据。它不一定要用重平台、重框架去实现真正难的是想清楚你要解决的是“告警的可理解性”而不是“告警的数量”。只要人还需要在凌晨被叫醒去处理问题那么“让机器用人类能懂的方式把话说清楚”这件事就永远有价值。希望这套思路能帮你少踩几个坑也期待以后能看到更多能说“人话”的告警系统。