实战中的自愈Kubernetes:三种故障、SigNoz的警报信号及经验证的修复方案
基于OpenTelemetry和SigNoz的Kubernetes自愈功能:Kind环境中的崩溃循环、内存溢出导致的进程终止以及令牌数量激增现象,各类信号所代表的含义,以及经过验证的AI修复方案。
来源: Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 7)
该演示包含多个动态组件。一个本地的 Kind 集群用于运行 SigNoz 以处理追踪数据和日志,同时还存在一个 Python 应用,其三个端点被故意设置为空。修复代理会持续监测异常情况并向 Groq 请求诊断结果,而 React 控制面板则允许操作员在任何操作执行之前批准或拒绝建议的修复方案。当所有组件都搭建完成后,最有趣的部分就是观察它们的实际运行表现。此次测试会实时触发每一处故障,查看 SigNoz 对每起事件的记录,并列出在将该方案用于非笔记本电脑环境之前需要进行的修改。
确认技术栈已正常运行
在触发任何故障之前,应有五项服务处于运行状态。首要检查的是两个命名空间中的所有 Pod 是否均处于正常状态。
# All pods should show Running
kubectl get pods -n observability
kubectl get pods -n apps
SigNoz、摘要服务以及控制面板各自的端口转发都需要独立的终端标签页,而该代码块中的最后一条命令用于追加代理日志。
# Port-forwards (run each in its own tab if not already open)
# Tab A: kubectl port-forward -n observability svc/signoz 8080:8080
# Tab B: kubectl port-forward -n apps svc/summarizer 8000:80
# Tab C: kubectl port-forward -n apps svc/approval-dashboard 9000:9000
# Tab D: kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000# Confirm the agent is polling
kubectl logs -n apps deployment/remediation-agent --tail=5
该输出中最近的正常状态指示该代理运行正常。对于未处于“Running”状态的容器,应先使用 kubectl describe 进行检查并修复。请同时保持两个浏览器标签页打开:端口 8080 的 SigNoz 页面以及端口 3000 的控制面板。
场景一:CrashLoopBackOff
在 summarizer 部署中设置 KILL_ME 环境变量,然后触发其崩溃端点,会导致进程在处理请求过程中终止。
kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash
由于该进程在运行过程中就会退出,curl调用可能不会返回任何结果或陷入挂起状态。监控这些Pod可以发现重启次数不断增加,状态在“Running”和“CrashLoopBackOff”之间切换,通常两到三次循环就足够了。
kubectl get pods -n apps -w
在 SigNoz 中。 摘要服务概览显示,一旦容器开始崩溃,错误率就会立即升至 100%;而在容器重启间隔期间处于关闭状态时,请求率则会降至零。流量出现、随后消失、再出现的情况,正是崩溃循环的典型特征。部署推广则表现不同,会有平滑的切换且没有错误率激增;网络分区的情况也有所不同,此时容器虽仍处于运行状态,但错误率会持续上升。
筛选到摘要服务的“日志”标签页中,包含应用程序在终止前输出的关键行信息。
CRITICAL summarizer — KILL_ME is true, exiting process
该日志包含了触发它的请求的追踪ID,因此只需点击一下,就能从日志行跳转到对应的追踪信息,以及最终结束该流程的精确HTTP请求。
代理会立即作出反应。在首次崩溃后的30秒内,轮询机制就会启动。它检测到错误率为1.0,而阈值仅为0.10,于是将相关信号打包并调用Groq。代理的日志会记录下这一整个流程。
kubectl logs -n apps deployment/remediation-agent --tail=20 -f
WARNING agent — crash loop signal: error_rate=1.00
INFO agent — anomaly detected: crash_loop — calling Groq for diagnosis
INFO agent — diagnosis: type=crash_loop severity=critical confidence=0.91
fix=kubectl set env deployment/summarizer -n apps KILL_ME-
INFO incident_poster — incident posted: id=<uuid> type=crash_loop
批准。控制面板会在大约十秒内显示该卡片,且建议的修复方案是正确的,因此操作员会点击“批准”。后端系统会检查命令是否以“kubectl set env”开头且不包含任何被禁止的词汇,随后执行该命令。卡片会更新以确认批准操作及环境已发生变化。
✓ Approved by operator
deployment.apps/summarizer env updated
在舱内监控中,下一次启动成功,崩溃循环结束,SigNoz的错误率也降回零。在进入下一个场景之前,快速检查确认该变量已消失。
# The approve action already removed KILL_ME, but confirm:
kubectl get deployment summarizer -n apps -o jsonpath='{.spec.template.spec.containers[0].env}' | jq
KILL_ME 不应出现在输出结果中。
第二种场景:OOMKill
向泄漏端点发送的循环 POST 请求会逐渐在内存中增加数据量。
for i in {1..30}; do
curl -s -X POST http://localhost:8000/leak | jq -c '{leaked_mb,chunks}'
sleep 0.5
done
在容器监控中,内存使用量持续上升,大约在第25次调用时,容器在循环过程中被强制终止,此后curl开始返回连接错误。
在SigNoz中,这次错误率并非呈线性上升趋势。内存不足导致的终止十分突然,应用程序也不会返回任何错误响应。相反,请求频率会骤降:流量突然停止,出现一段空白期,待容器重新启动后流量才会再次出现。
日志中记录着真实情况。经过筛选后可以看到,在出现空白期之前有关于桶大小不断增大的警告信息。
WARNING summarizer — leak endpoint hit, bucket now holds 10 MB
WARNING summarizer — leak endpoint hit, bucket now holds 20 MB
...
WARNING summarizer — leak endpoint hit, bucket now holds 240 MB
最后一次警告之后便是彻底的沉默,没有任何异常、堆栈跟踪信息,也没有任何优雅关闭的提示,因为内核的内存不足杀手让进程根本没有机会记录日志。一连串的警告之后紧接着什么都没有,这种模式值得注意。
集群方面也会确认导致问题的原因。
kubectl describe pod -n apps -l app=summarizer | grep -A 5 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
退出码137是内核特有的标识,由SIGKILL(信号9)加上128构成。它将内存不足导致的进程终止与应用程序崩溃(退出码为1)区分开来,同时也与存活性检测失败区分开来——后者同样返回137,但描述输出中的原因字符串不同。
代理会作出响应。它会检查SigNoz日志索引中过去十分钟内包含“OOMKilled”字样的记录。一旦找到相关记录,就会将日志样本与容器重启事件整合成一条提示信息,随后给出诊断结果。
type=oom_kill severity=critical confidence=0.88
fix=kubectl set resources deployment/summarizer -n apps \
--containers=summarizer --limits=memory=512Mi
建议的修复方案将内存限制提高一倍。作为一项合理的短期措施,它能为应用修复争取时间。批准后,后端会设置512Mi的内存限制,Kubernetes会执行滚动重启,从而使容器拥有足够的内存空间。
代码中仍然存在内存泄漏问题。该修改仅提升了内存上限,而真正的解决方案是需要通过代码修复来定期清除内存泄漏。代理程序的作用是阻止内存持续流失,而找出根本原因仍需人工处理。
第三种场景:延迟与令牌数量激增
对高成本接口进行多次短周期调用会导致第三种故障发生。
for i in {1..5}; do
curl -s -X POST http://localhost:8000/expensive \
| jq '{output_tokens, length_chars}'
sleep 2
done
没有程序崩溃,也没有进程被终止,容器始终处于运行状态。不过,每次响应时间在8到15秒之间,且token数量接近2,048的最大上限。
在SigNoz中。 正是这种隐蔽的故障让可观测性框架发挥了作用。在正常的摘要处理流量下,p95延迟值基本保持在1.5秒左右;而当执行成本较高的调用时,该数值会急剧上升,且变化呈阶跃式而非缓慢上升,因为每次调用都会达到上限。按持续时间对追踪记录进行排序后,那些耗时8到15秒的调用会显示在最上方。
在火焰图上查看慢速追踪时,几乎所有时间都集中在单个子时间段内,即向 api.groq.com 发出的出站 HTTP 请求。端点处理程序本身的时间段很短,因为 FastAPI 处理程序几乎不执行任何操作,大部分时间都用于与大型语言模型的往返通信。该时间段的响应内容长度远大于普通总结请求的响应长度,后者是以字节形式显示的输出token数量。线条先保持平缓,随后上升,循环结束后又恢复平缓——这就是配置错误的端点的特征,而非基础设施故障。
代理会作出响应。延迟检测会按路由比较 p95 值。那个耗时的路由的延迟约为 11,000 毫秒,而阈值仅为 5,000 毫秒,于是代理会汇总这些数据。
{
"check": "latency_spike",
"spikes": { "/expensive": 11240.0 },
"p95_ms_by_route": {
"/summarize": 1820.0,
"/expensive": 11240.0,
"/healthz": 4.0
}
}
Groq 会读取它并给出准确的诊断结果。
type=token_spike severity=high confidence=0.85
root_cause: The /expensive endpoint has no reasonable max_tokens budget
and a prompt that invites long outputs, causing p95 latency
to spike to 11s versus 1.8s on the normal /summarize path.
fix=kubectl set env deployment/summarizer -n apps MAX_TOKENS_EXPENSIVE=256
拟议的解决方案是设置一个应用程序可读取的环境变量,以此限制该端点的令牌使用额度。在“批准”操作之后,该变量就会被设置。在实际部署中,还需要修改应用程序,使其从环境中读取MAX_TOKENS_EXPENSIVE的值。该方案建议通过配置方式来解决,而非修改代码,这正是kubectl层级修复所适合的处理范围。
同一时间线上的三次故障
当这三种场景依次发生时,两小时时间范围内的汇总数据显示出三种不同的故障特征。
- 崩溃循环表现为错误率的锯齿状波动,错误率会骤升至100%,然后降至零,再再次上升,同时吞吐量也会出现相应的波动。
- 内存不足导致的进程终止则表现为请求率的突然断崖式下降,且没有错误率飙升的情况,因为进程根本无法返回错误信息。
在一个控制面板上就能看到三种具有不同特征故障模式的可视化表现,无需自定义警报规则,也无需手动解析日志。这正是关联追踪数据、指标与日志所带来的优势。
投入生产前需要做的修改
演示版本在笔记本电脑上运行,省略了其他环境中不可或缺的某些环节。
替换内存中的故障存储机制。目前的故障列表存储在后端容器的Python字典中,因此重启后会丢失所有记录。使用SQLite通过SQLAlchemy实现只需一行代码修改,无需额外服务;当控制面板需要跨多个后端副本扩展时,Redis则是合适的选择。API接口格式保持不变,仅其背后的存储方式会发生变化。
添加 Slack 通知功能。目前,必须有人持续关注控制面板才能知道是否有新卡片出现。在后端事件处理程序中加入 Slack webhook 调用即可解决这一问题。
import httpx
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")def notify_slack(card: dict):
if not SLACK_WEBHOOK:
return
d = card["diagnosis"]
httpx.post(SLACK_WEBHOOK, json={
"text": (
f"*{d['severity'].upper()} — {d['incident_type']}* on `{card['service']}`\n"
f"Root cause: {d['root_cause']}\n"
f"Proposed fix: `{d['proposed_fix']}`\n"
f"Confidence: {int(d['confidence']*100)}%\n"
f"<http://your-dashboard-url|Review on dashboard>"
)
})
在接收器处理完成后调用通知器,意味着每个新事件都会触发值班通道的提醒,审批者无需再持续查看浏览器标签页。
妥善管理集群凭证。后端会调用kubectl,只要节点环境已正确配置即可。在真实集群中,后端Pod需要拥有具备适当RBAC权限的ServiceAccount,同时Kubernetes Python客户端也需设置为可使用集群内凭证的模式。
from kubernetes import client, config
config.load_incluster_config()
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()
每种经过审批的命令类型都会变成直接的 SDK 调用,而非 shell 字符串。虽然这样会更繁琐,但能够实现结构化的错误处理、在 API 服务器端进行审计日志记录,并且不存在 shell 注入的风险。
严格限制允许列表。演示版本允许任何 kubectl set env 命令,而生产环境应将其限制在指定的部署和命名空间中使用。
ALLOWED_TARGETS = {
"deployment/summarizer -n apps",
"deployment/worker -n apps",
}
def validate_command(cmd: str) -> bool:
if not any(cmd.startswith(p) for p in ALLOWED_PREFIXES):
return False
if any(term in cmd for term in BLOCKED_TERMS):
return False
if not any(target in cmd for target in ALLOWED_TARGETS):
return False
return True
模型不得被允许针对其指定列表之外的任何命名空间或部署环境进行修复。
值得进一步研究的项目
该演示是从零开始构建的,旨在展示各组件之间的关联方式,而实际生产环境中的应用则会基于现有项目进行开发。
- KubeStellar适用于工作负载分布在多个集群的情况。它能处理那些会增加跨集群修复复杂度的控制平面任务,例如在提出任何修复方案之前先确定故障发生在哪个集群中。
- OpenTelemetry的功能远超演示所需。其收集器具备处理器和导出器,能够自动为时间跨度添加Kubernetes元数据,因此无需在应用清单中手动添加资源属性。
整个系统的构成
该系统由三个节点组成的Kind集群构成,具备真实的多节点调度拓扑结构;SigNoz通过Helm部署,能够通过OpenTelemetry Collector收集追踪数据、指标信息及日志。Python FastAPI服务在启动时会自动添加监控代码,因此每个HTTP请求和LLM调用都会被记录为追踪事件,同时所有日志行也能实现关联,而无需在应用中额外编写监控代码。
三个异常的端点会引发 Kubernetes 最常见的三种故障:进程崩溃、内存耗尽以及成本和延迟上升。修复代理每30秒查询一次 SigNoz,执行阈值检测,将相关信号整合为结构化的提示,向大型语言模型请求诊断结果,随后生成故障卡片。FastAPI 后端接收这些卡片,根据允许列表验证提出的修复方案,并通过 kubectl 执行已批准的命令。React 控制面板提供了批准与拒绝功能、显示模型置信度的标识、针对不确定诊断结果的提示栏,以及每项操作的日志记录。
所有功能均在免费级别的 API 上本地运行,无需任何费用。该代码规模很小,大约有600行 Python 代码、一个 HTML 页面以及几份 YAML 配置文件,因此每个部分都能被完整理解。
相关阅读
- LangGraph中的自愈多智能体团队:架构师、测试员、调试器 — 用Python构建LangGraph状态机,让架构师、开发人员、pytest沙箱、调试器和审查员协同工作并不断循环,直至测试通过。
- 在LangChain中构建ReAct智能体:执行器、草稿板及其局限性 — 构建一个使用工具的ReAct智能体,了解智能体与AgentExecutor如何分工协作,以及为何LangGraph在处理控制流、循环和状态方面更出色。