type
Post
status
Published
date
Nov 20, 2025
slug
gateway006
summary
tags
gateway
category
icon
password
摘要
前几篇文章已经把几件基础的事做出来了:接入链路能走通,多租户治理开始成形,上游故障边界已经有了,healthy path 的性能基线也已经立住了。到这里,网关已经能接请求、治理请求、fallback、暴露 readiness 了。这些能力单独看都成立了,但真出故障时,还是可能让人排不明白。
真正麻烦的地方在故障现场。一条调用不对劲,客户端看到的可能只是超时、半截 SSE、一次 200 但内容不对,或者一次 fallback 没发生。网关自己如果讲不清这次请求卡在哪一层、为什么走了 fallback、为什么没走 fallback、provider 现在到底健不健康,排障就会很快退回到猜。这篇要补的不是又一类功能,而是把一次调用讲清楚的能力。
这篇只想把一件事情说清楚:
一次模型调用,现在已经能被串成一条可关联、可排障、可解释的事件链。
这组证据的作用也很直接:后面排故障时有最小证据链可查,后面做 dashboard、告警和 tracing backend 也有现成落脚点。对这个网关来说,可观测性的第一步不是先堆一个大盘,是先把一次请求讲清楚。
一、先把问题收成一句话:能不能把一次请求讲清楚
这篇如果不先把问题收窄,后面很容易写成“我们有日志、也有指标”的泛总结。
一个网关的成功路径通常很短:请求进来,鉴权通过,路由到 provider,返回结果。真正难的是失败路径。请求可能刚进来就被拒绝,也可能卡在 handler、service、provider 尝试、fallback,或者卡在 stream 已经开始之后的中断。客户端看见的是一次“调用不对劲”,网关内部其实已经是几种完全不同的故事了。这篇关心的不是“有没有日志”,而是“这条失败故事能不能被讲完整”。
所以这里最重要的问题只有一句:能不能把一次请求讲清楚。讲清楚的意思很具体,就是能不能从入口一路讲到 handler、service、provider 尝试、fallback、健康状态和最终响应。只要这条链讲不清,排障就还是靠猜。这篇后面所有信号都只服务“把一次请求讲清楚”这句话。
二、这条最小事件链先由五类信号组成
我要先把证据面摊开,不然后面每一段都像在单独讲功能点。
当前这套最小 observability closure 还不是一个完整 observability platform,它更像一条够用的排障证据链。现在我先把它收成五类信号:
request_id、trace_id、structured logs 和 span completion logs、provider events、以及 /debug/providers、/readyz、/metrics 这一组状态面。排障不是靠某一条日志完成的,而是靠几类信号拼起来。可以先把它们压成一张表:
信号 | 怎么拿到 | 它回答什么问题 |
X-Request-Id | HTTP 响应头 | 这次请求是谁 |
X-Trace-Id | HTTP 响应头 | 这次请求内部执行链是谁 |
trace span finished | 结构化日志 | 这次请求走到了哪一层 |
http request completed | access log | 这次请求最终怎么返回 |
provider event | 结构化日志 | router 为什么做了这个决定 |
/debug/providers | 状态接口 | backend 当前是什么状态 |
/readyz | readiness 接口 | 网关现在还能不能接流量 |
/metrics | 指标接口 | 这是孤例还是趋势 |
这张表最重要的地方是每一类信号回答的问题都不一样。
request_id / trace_id 确定一次请求,structured logs 讲这次请求走到了哪一层,provider events 补路由决策,/debug/providers、/readyz、/metrics 再把当前状态和趋势补上。当前这套最小闭环已经能把“这次发生了什么”和“系统现在怎么样”接起来。三、先把一次请求确认:request_id 和 trace_id
不先把一次请求变成可搜索对象,后面的关联全都挂不住。
request_id 在最外层 middleware 生成,优先复用传入的 X-Request-Id,没有就生成新值,然后进 request context,再写回响应头。它做的事情很简单,就是把一次 HTTP 请求变成一个客户端和服务端都能看见的稳定身份。排障第一步先要有一个双方都认得出来的请求编号。原始响应头我这里直接留一条:
这条响应头最有价值的地方是客户端已经拿到了
X-Request-Id 和 X-Trace-Id。到这里,请求身份已经不是只存在于进程内部的东西了。,一次请求已经先被确定成了一个可查对象。在
request_id 之上,router middleware 还会建立 tracing 上下文,并通过 X-Trace-Id 对外暴露。当前实现里,默认情况下 trace_id 和 request_id 对齐。request_id 解决的是“这次请求是谁”,trace_id 解决的是“这次请求内部经过了什么”。对外是一条请求,对内已经是一条路径。四、structured logs 开始把主链落出来
有了 request_id 还不够,还得知道这次请求到底走到了哪一层。
当前 tracing 更接近“日志化的 span completion 记录”,还没有接成熟 tracing backend。即便这样,它也已经够回答一个很关键的问题:这次请求到底走到了哪一层,在哪一层失败。先把主链日志落出来已经很够用了。
同一次请求的整条执行链日志现在长成这样:
这组日志最重要的地方是同一个
request_id / trace_id 已经能贯穿 provider backend、provider router、service、handler 和最终 access log。到这里,一次请求至少已经有主链可追。我想说的是,排障终于不再是“我猜它大概卡在 service”,而是可以顺着 span 一层层往下看。五、provider event 补的是“为什么这么路由”
为什么?因为只看请求主链,还解释不了 router 为什么做了这个决定。
structured logs 能讲“请求走到了哪”,但还不够讲“router 为什么这么做”。这时候 provider event 才开始有价值。它回答的是路由决策:为什么这次走 fallback,为什么某个 backend 被跳过,为什么 probe 又把它拉回来了。主链日志讲路径,provider event 讲决策。
当前已经有的关键事件包括:
provider_request_failed
provider_fallback_succeeded
provider_skipped_unhealthy
provider_probe_failed
provider_recovered
provider_stream_interrupted
provider event 示例我这里直接留几条:
provider event 当前还不自带
request_id / trace_id / span_id。所以更准确的说法是,请求主链靠 request_id / trace_id + structured logs 关联,provider event 补的是路由和健康状态证据。六、/debug/providers、/readyz 和 /metrics 补的是“系统现在怎么样”
排障不能只停在“刚才这次请求发生了什么”,还要知道系统现在是什么状态。
排障如果只盯着单次请求,很容易看不见当前系统是不是已经在整体变差。当前这三类外部信号正好补这件事:
/debug/providers 讲 backend 现在是什么状态,/readyz 讲网关现在还能不能接流量,/metrics 讲这到底是单次故障还是开始变成趋势。到这里wang gu才开始有“请求链 + 当前状态 + 趋势”的完整味道。/debug/providers 和 /readyz 这一组现在已经能直接把 provider 当前状态暴露出来:primary 已经 unhealthy,但 secondary 还可用,所以
/readyz 继续返回 200 ready。这说明 readiness 已经开始从 provider 可用性角度回答“网关现在还能不能接流量”。这里已经不是只看某次请求有没有失败了。/metrics 这一组也已经有最小指标面:这组指标最有价值的地方,是它把刚才那次失败放回了当前状态里。现在已经能看到 primary unhealthy、cooldown 还没结束、secondary 在接住流量、ready 还维持着 1。这一步把“刚才发生了什么”和“现在系统是什么状态”接起来了。
七、真出故障时,我现在会怎么查
一条证据链的价值,最后还是要落回“人怎么用它排障”。
到这个阶段,最实用的排障路径其实已经比较清楚了。先拿
X-Request-Id / X-Trace-Id,再用 request / trace 去查 structured logs,看 span completion 和 access log,确认失败发生在哪一层。如果问题涉及路由、健康、fallback,再去看 provider events,接着看 /debug/providers,最后看 /readyz 和 /metrics。这条路径已经能把排障从“猜”变成“顺着证据往下查”。这条路径里,每一类信号分工也已经很明确:
request_id / trace_id:这次请求是谁
- structured logs:这次请求走到了哪一层
- provider events:router 为什么做了这个决定
/debug/providers:backend 当前是什么状态
/readyz:网关现在是否 ready
/metrics:这是孤例还是趋势
这条路径最重要的价值,就是一次失败已经能被串成一条完整叙述。现在还不算完美,但已经不再完全靠经验猜测。这篇立住了“排障链路”。
八、回到代码里,我只看四个锚点
这篇还是要落回实现上,但代码只保留最能支撑主线的四处。
如果只挑最能说明问题的代码路径,这一阶段最值得看的就是四段。它们分别把请求身份、span 日志、provider event 和 readiness 暴露出来。这四段代码已经能把这条最小事件链落回真实实现。
1. 请求身份与响应头暴露
internal/api/router.go
这里负责生成或复用
X-Request-Id,建立 tracing 上下文,在响应头写回 X-Request-Id 和 X-Trace-Id,再输出 http request completed。这一步让请求身份既对调用方可见,也对服务端可查。请求身份已经成了公共锚点。2. span completion 日志
internal/obs/tracing/tracing.go
这里在 span 结束时输出
trace span finished,带上 trace_id、request_id、span_id、parent_span_id、span_name、status 和 duration。这一步已经把 access log 上面的执行链补出来了。当前主链已经不只是“有一条访问日志”。3. provider event 结构化输出
cmd/gateway/main.go
internal/provider/router/chat.go
这里负责让 provider router 发出事件,再由
providerEventLogger 打成结构化日志。这样 provider 层的失败、fallback、cooldown、恢复就不再是黑箱。router 的决策已经开始可解释了。4. readiness 与状态接口
internal/api/router.go
这里直接暴露了
GET /readyz 和 GET /debug/providers。这段代码说明 provider 健康状态已经成了外部可观察信号。provider 状态已经开始走出内部实现。九、边界说明
当前这条链已经够支撑排障,但还没到完整 observability platform。
这篇现在能确认的,是
request_id / trace_id 已经对外暴露,structured logs 已经能把请求主链串起来,provider event 已经能解释路由和健康状态变化,/debug/providers、/readyz 和 /metrics 已经构成最小外部状态面。到这里,最小排障闭环已经成立了。这篇还没有往外说太多。当前还没有成熟 tracing backend,没有把 provider event 统一挂到单请求级别,也还没有完整的 dashboard、告警规则和长期趋势分析。运维集成和 observability 成本影响,这篇也都没展开。当前成立的是轻量 observability closure,还不是完整 observability platform。
总结
一次模型调用,现在已经不再只是“成功或失败”的结果了。它已经开始变成一条能被关联、能被回放、能被解释的事件链:请求主链靠
request_id / trace_id + structured logs 串起来,路由决策靠 provider events 补上,当前状态再由 /debug/providers、/readyz 和 /metrics 接住。我的网关的一次模型调用,现在已经能被讲清楚了。分享
