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

中继与多区域

一个就在你的智能体旁边的采集器,跨区域的那段链路由它带着重试去走。

中继是什么

mcpd relay 用同一个二进制文件,作为一个自己不存储任何东西的 采集器运行。它接受守护进程接受的每一种 HTTP 摄取请求,把请求写进磁盘上的 暂存区,答复发送方,然后把请求发给 --upstream 列出的守护进程。 它不打开任何存储。

在你的智能体运行的每个区域各跑一个。这样发送方连接的是几毫秒之外的采集器, 跨区域的链路由中继带着重试去走,而不是由每个智能体各自去走。

一个带两个上游的边缘中继

mcpd relay \ --ingest-key $INGEST_KEY \ --upstream http://mcpd.eu.internal:4317 \ --upstream http://mcpd.us.internal:4317
中继
$ MCPD_UPSTREAMS=http://mcpd.eu.internal:4317,http://mcpd.us.internal:4317 mcpd --data /data/mcpd.db config | grep ^relayrelay           2 upstream(s) in order, http://mcpd.eu.internal:4317, http://mcpd.us.internal:4317; each sender's key passed on; spool at /data/mcpd.spool/relay, 1024 MiB$ curl -s -D - -o /dev/null localhost:4317/v1/ingest -H "content-type: application/json" -d @batch.json | grep -e ^HTTP -e mcpd-relayHTTP/1.1 202 Acceptedmcpd-relay: spooled$ curl -s localhost:4317/v1/ingest -H "content-type: application/json" -d @batch.json{"accepted":0,"duplicate":0,"rejected":0}

它怎样答复

除探针之外,任何路径上的任何 POST 都会被接受。OTLP 路径 /v1/traces、/v1/logs 和 /v1/metrics 答复 200,并带上一个 OTLP partialSuccess,这正是 OpenTelemetry 导出器所期待的。其他每条路径答复 202,带上请求头 mcpd-relay: spooled 和响应体 {"accepted":0,"duplicate":0,"rejected":0}:请求已被 留存并将会投递,真实的计数属于上游,而上游还没见到它。每个 SDK 都把任何 2xx 当作成功。

中继自己检查 --ingest-key,在写入任何东西之前完成,并像守护进程 一样应用 --max-body-bytes 和 --request-timeout-seconds。答复发出之前,请求已经在暂存区 <spool-dir>/relay 里了。

投递与故障切换

四个 worker 负责发送暂存区里的内容。每个请求按 --upstream 的顺序, 交给第一个不在冷却期的上游。可重试的失败会让那个上游进入冷却,时长按退避加 抖动计算,或者按它的 Retry-After 指定的时长,而请求会立刻交给 下一个上游。冷却结束后,首选上游会重新被最先尝试,所以流量会自行回到它那里。

最终拒绝,例如 400,不会发给下一个上游:下一个也会拒绝同样的字节。请求会被 移到暂存目录里的 dead/,并计数。

不重复计数

投递至少一次,重发的内容只存一次。每个请求都带一个 Idempotency-Key 请求头:发送方自己带了就用发送方的,否则是 relay- 加上路径、内容编码和请求体的摘要。守护进程的每一个摄取 入口都遵守这个请求头,原生批次请求体里的键优先于它。中继在超时后重发的请求, 或者在首选上游已经收下、但答复丢失之后发给次选上游的请求,都会被识别出来,并 报告为重复。

密钥

设置了 --upstream-key 时,中继把这把密钥作为 Bearer token 出示给 每一个上游。没有设置时,发送方的 Authorization 或 x-mcpd-key 请求头会随暂存的请求一起保存在一个只有中继的用户能读的 文件里,并原样传递。第二种方式让上游看到的仍是每个发送方各自的项目和作用域。

背压与就绪

暂存区的上限是 --spool-max-bytes,除非你另行设置,中继为 1 GiB。 满了之后,请求会以 503 和 Retry-After: 5 被拒绝。每个 SDK 和 OpenTelemetry 导出器都会对此重试,于是数据留在发送方手里,而不是被中继丢掉。

/healthz 总是答复 200。/readyz 的答复是:

  • 有上游可达时,200 ok;
  • 过去 30 秒内没有任何上游答复过它的 /readyz 或收下过一次投递时, 200 degraded,并带上 detail。每 5 秒探测一次上游;
  • 暂存区已满,或没有上游可达且暂存区至少已满 80% 时,503。

所以负载均衡器会继续把流量发给一个正在缓冲的中继,而把流量从一个已经接不住的 中继那里移走。

中继指标

指标类型含义
mcpd_relay_accepted_totalcounter写进暂存区的请求
mcpd_relay_refused_full_totalcounter因暂存区已满而以 503 拒绝的请求
mcpd_relay_failovers_totalcounter投递给第一个之外的上游的请求
mcpd_relay_dead_letter_totalcounter被上游最终拒绝、移到 dead/ 的请求
mcpd_relay_spool_entriesgauge在暂存区里等待的请求数
mcpd_relay_spool_bytesgauge暂存区占用的字节数
mcpd_relay_spool_oldest_secondsgauge等待中最早的请求的年龄,没有则为 0
mcpd_relay_delivered_totalcounter上游收下的请求,带 upstream 和 order 标签
mcpd_relay_failed_attempts_totalcounter上游没有收下的尝试,标签同上
mcpd_relay_upstream_upgauge上游在过去 30 秒内答复过时为 1,否则为 0
mcpd_uptime_secondsgauge这个进程启动以来的秒数

mcpd_relay_failovers_total 上升,说明首选上游在失败,次选上游正在 承接它的流量。mcpd_relay_spool_oldest_seconds 表示投递落后了多少。

多个区域

在一个或多个区域里运行若干个守护进程副本,共用一个 Postgres 或 ClickHouse 存储,运行要么存在 Postgres 里(--runs-data postgres://...), 要么关闭:内嵌的运行存储只属于一个进程。每个副本的 /readyz 已经反映了它能否连上存储。在每个区域里,把一个中继放在 它们前面,--upstream 先列最近的守护进程,再列另一个区域的守护 进程。

副本多于一个时,有四项设置的行为会不同:

  • 幂等键保存在共用的存储里,所以落到另一个副本上的重发仍然会被识别。Postgres 在与批次相同的事务里记录这个键。
  • 每分钟的配额上限按副本分别计数。请把你想要的上限除以副本数。
  • 实时评分、尾部采样和监视器告警同一时间只在一个副本上运行。副本通过共用存储里 的一份租约轮流来:持有者每一轮都续约,关闭时交出租约,如果它停了,另一个副本 会在三轮之内接手。每个副本都会评估自己的监视器和目标,所以每一个都能回答监视器 API,而只有持有者发出 webhook。请在每个副本上用同样的方式配置它们。
  • 方面分析在每一个配置了它的副本上运行。请在一个副本上配置它。

部署示例

deploy/docker/docker-compose.multi-region.yml 在一台机器上启动 Postgres、两个守护进程和一个中继,也就是可以直接试用的完整拓扑。

Helm chart 接受 mode: relay。deploy/helm/examples/ 下的三个 values 文件各描述一个区域:region-core.yaml、 region-standby.yaml 和 region-edge-relay.yaml, 最后一个用于只运行中继的区域。

接下来