中继与多区域
一个就在你的智能体旁边的采集器,跨区域的那段链路由它带着重试去走。
中继是什么
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或收下过一次投递时, 200degraded,并带上detail。每 5 秒探测一次上游; - 暂存区已满,或没有上游可达且暂存区至少已满 80% 时,503。
所以负载均衡器会继续把流量发给一个正在缓冲的中继,而把流量从一个已经接不住的 中继那里移走。
中继指标
| 指标 | 类型 | 含义 |
|---|---|---|
mcpd_relay_accepted_total | counter | 写进暂存区的请求 |
mcpd_relay_refused_full_total | counter | 因暂存区已满而以 503 拒绝的请求 |
mcpd_relay_failovers_total | counter | 投递给第一个之外的上游的请求 |
mcpd_relay_dead_letter_total | counter | 被上游最终拒绝、移到 dead/ 的请求 |
mcpd_relay_spool_entries | gauge | 在暂存区里等待的请求数 |
mcpd_relay_spool_bytes | gauge | 暂存区占用的字节数 |
mcpd_relay_spool_oldest_seconds | gauge | 等待中最早的请求的年龄,没有则为 0 |
mcpd_relay_delivered_total | counter | 上游收下的请求,带 upstream 和 order 标签 |
mcpd_relay_failed_attempts_total | counter | 上游没有收下的尝试,标签同上 |
mcpd_relay_upstream_up | gauge | 上游在过去 30 秒内答复过时为 1,否则为 0 |
mcpd_uptime_seconds | gauge | 这个进程启动以来的秒数 |
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,
最后一个用于只运行中继的区域。