跳到主要内容
mcpd
无守护进程
源码

监视与告警

一个值得盯着的条件,就该被盯着。mcpd 按定时器评估,把发现的结果锁存,然后只告诉你一次。

监视器是什么

监视器是 mcpd 按定时器问自己的一个存下来的问题:这个项目最近五分钟的错误率 高于百分之五吗?这个评分器的均值低于我们约定的那个数字吗?它读的是界面读的 同一份存储,所以监视器能断言的,只有你本来自己也查得到的东西。

在你配置出一个之前,监视是关着的。mcpd 不会替你猜一个阈值,因为一个没人挑过 的阈值,是一个没人信的阈值。

Monitor panel

6 watched · every 60s

Error rate P95 latency Hourly cost Run count No tool loop Not a refusal

Error rate

Threshold 5%

Two readings in the middle are —: too few runs in the window to judge. The lamp stays lit through them.

A monitor panel of six annunciator windows. The error-rate window lights when the measured rate crosses five percent, stays lit through two windows that hold too few runs to judge, and clears only when a later reading genuinely falls back under the threshold.

它会锁存

一个在条件持续期间每次评估都触发的监视器,不出一天就会被静音,而一个被静音 的监视器比没有监视器更糟,因为它看起来像是有覆盖。所以 mcpd 报告的是状态 变化,而不是状态本身:进入麻烦时一次,走出麻烦时一次,中间什么都没有。

安静不等于恢复

这条规则让人付出过故障的代价。当流量停下来时,窗口里的运行少到没法判断, 而显而易见的那种实现会报告“没有问题”,并解除告警。但一个已经不再服务请求的 智能体,本身就是故障,而不是从故障中恢复。

一个里面运行太少的窗口,会让一条仍然成立的告警原样留在那里。只有当一次背后 有足够样本的读数回到阈值以内时,监视器才回到正常。

同样的道理定下了 min_samples。安静的一小时里失败了一次运行, 就是百分之百的错误率,mcpd 不会把那叫作故障。默认值是二十。

可以盯什么

观察项测的是什么
error_rate 窗口里失败的运行数除以总运行数。
latency_p95 运行时长的第九十五百分位,单位是毫秒。
cost 窗口里的总花费,用价目表的货币。
score_mean 某一个评分器的均值,由 scorer_id 指名。这一项能抓住延迟 曲线看不见的质量退化。
trace_count 窗口里的运行数。配上 below 来盯它,可以抓住一条突然安静下来的流水线。
step_count_divergence 窗口里那些相对自己的定义走了异常步数的运行。它是这里唯一一个不在所测之物的单位上设阈值的监视器。它读的是持久化运行,不是 trace。
run_repetition 一次运行自我重复的程度:把窗口里每次运行在自己定义的某一个节点上做过的最多尝试次数取出来,再取平均。1 表示没有任何节点跑过两次。它读的是持久化运行,不是 trace。

目标,当阈值问的是错的问题时

监视器问的是错误率此刻是不是高于百分之五。SLO 问的是更有用的东西:我们会不会 错过承诺的那个目标,以及我们正以多快的速度奔向那里。

这个区别要紧,是因为一个阈值分不清一次两分钟的抖动和一次缓慢的渗漏,而这两者 需要相反的应对。把阈值收紧到能抓住渗漏,那次抖动就会在凌晨三点把你叫醒。把它 放松到能睡过那次抖动,渗漏就能跑上一个星期。

预算,以及它被消耗的速度

三十天里 0.995 的目标,是说允许千分之五的运行失败。那半个百分点 就是错误预算。消耗速率为 1,正好在三十天里把它花完;消耗速率为 10,三天就 花完。

mcpd 同时在两个窗口上度量消耗速度,只有两者一致时才跳闸:最近一小时快于 14.4x 并且最近六小时快于 6x。这就是把那两种情况分开的办法。一次五分钟的故障 会推动一小时那个数字,推不动六小时那个,所以它不会呼叫人。一次跑了一下午的 渗漏,两个都推得动。

在你依赖它之前值得知道:在一个 99.5% 的目标上,那两个阈值对应的错误率大约是 7.2%。一个稳定在 5% 的失败率把预算消耗得快了十倍,会提前把它耗光,但它不会把 任何人叫醒。为此请盯着 budget_remaining;如果你想更早知道,就 再留一个 score_mean 或 error_rate 监视器。

{
  "slos": [
    {
      "id": "support-availability",
      "name": "99.5% of support runs succeed over 30 days",
      "project": "support-desk",
      "objective": 0.995,
      "window_days": 30,
      "min_samples": 20,
      "webhook": "https://hooks.example.com/mcpd"
    }
  ]
}

GET /v1/api/slos 返回每一个目标,连同它的成功率、剩下的预算 比例,以及两个消耗速率。 POST /v1/api/slos/{id}/ack 确认其中一个。它们的锁存和 确认方式和监视器完全一样,而一个薄到没法判断的窗口,永远不会解除一条仍然 成立的告警。

预算超支之后 budget_remaining 会变成负数,而不是停在零。零和 “错过目标一百倍”是不同的处境,这个数字会说出是哪一种。

配置一个

监视器住在一个 JSON 文件里而不是命令行上,因为一个阈值、一个窗口和一次比较 放不进一个参数,而且它们应该像代码一样被评审。这个文件可以原样作为 Kubernetes ConfigMap 挂载。

{
  "monitor_interval_seconds": 60,
  "monitors": [
    {
      "id": "support-error-rate",
      "name": "Support agent error rate above 5%",
      "project": "support-desk",
      "watch": "error_rate",
      "compare": "above",
      "threshold": 0.05,
      "window_seconds": 300,
      "min_samples": 20,
      "webhook": "https://hooks.example.com/mcpd"
    },
    {
      "id": "relevance-regression",
      "name": "Answer relevance fell below 0.8",
      "project": "support-desk",
      "watch": "score_mean",
      "scorer_id": "answer_relevance",
      "compare": "below",
      "threshold": 0.8,
      "window_seconds": 3600
    }
  ]
}

带着已布防的监视器启动

mcpd serve --config-file ./mcpd.json

未知的键会让启动失败,而不是被忽略。threshold 里一个悄悄什么都 不监视的拼写错误,是所有结果里最糟的那个,所以 mcpd 会出声拒绝它。

设置 "enabled": false 可以保留一个监视器的定义而不去评估它。 mcpd config 会打印有多少个已经布防。

一次状态变化会去哪里

每一次状态变化都会被记进日志,监视器 id、项目、阈值和测得的值都作为结构化 字段,这样日志管道不用去解析一个句子就能路由它。如果你已经在跑一条日志管道, 那这就是全部的集成工作。

给监视器一个 webhook,mcpd 还会把这次状态变化 POST 出去。这个 请求体有意是通用的:Slack、PagerDuty、Opsgenie 和每一个流程工具都接受入站 webhook,而把载荷做成其中某一个的形状,就意味着要维护一套会过时的厂商集成。

{
  "monitor_id": "support-error-rate",
  "name": "Support agent error rate above 5%",
  "project": "support-desk",
  "condition": "tripped",
  "watch": "error_rate",
  "threshold": 0.05,
  "value": 0.5,
  "samples": 40,
  "detail": "error rate is 0.5000, threshold above 0.0500, over 40 run(s)",
  "at": "2026-08-28T02:13:54.687391446Z"
}

一次失败的投递会被记进日志然后丢弃。重试要么会阻塞评估循环,要么需要一个 持久化的出站队列,而一条会悄悄落后的告警通路,比一条明显漏掉了一条消息的 更糟。

读取与确认

GET /v1/api/monitors 返回每一个监视器,连同它锁存的状态、它最近 一次的读数,以及它是什么时候跳闸的。

POST /v1/api/monitors/{id}/ack 确认其中一个。确认会让 这次状态变化安静下来,但不会假装条件已经解除:只要它仍然越界,这个监视器就 保持在已确认状态,等到那个数字恢复了,它自己回到正常。没有人需要记着去 取消静音。

监视器的状态放在内存里,每次启动时从读数重建。它不做持久化,因为一次重启把 一条其实已经恢复的告警复活过来,比一次重启重新评估一遍要糟糕得多。

接下来

  • 一个 score_mean 监视器盯着的那些评分器: 回归测试。
  • mcpd 自己的健康状况,没有任何监视器覆盖它: 把它跑起来。
  • 每一个参数和文件设置: 配置。