某个周四下午,我在监控面板上看到 OpenClaw_Gateway 的 5xx 比例开始往上爬,第一反应是上游某个业务服务又出问题了。但翻完上游日志之后发现事情没这么简单:上游服务自己什么都没报,错误全部集中在网关这一层,而且只发生在两个特定路径上。网关进程的 CPU 和内存看着都正常,没有死锁,也没有被流量打爆,但它就是间歇性地往外抛 502 和 504。
这种故障最让人抓狂的地方在于,它不是稳定复现,而是你盯着它的时候它不报错,你刚转身它又开始飘,而且网关日志里的报错信息非常有限。这篇文章完全是这次排障的现场记录,包括我走过的弯路、看过的日志、抓过的包、改过的配置,以及最后确认的几个叠加在一起的原因。如果你也在维护类似的东西,希望这篇能帮你少踩几个坑。
1. 故障现象与排查范围划定
1.1 故障现象初步观察
故障开始出现之后,我第一时间把监控时间线和变更记录对了一下,但这天并没有人动过网关配置,也没有发布过上游服务。最明显的异常来自网关的监控面板:整体 5xx 占比从不到 0.1% 涨到 3% 左右,持续了大概 20 分钟,之后回落到 1%,然后又爬上去,呈现出比较典型的“锯齿状”波动。
进一步看状态码分布,失败请求里 502 和 504 都有,还有少量 499(也就是客户端在等待响应时自己掐断了连接)。更值得注意的是,失败请求集中在两条路径上:一条是 /api/path-a,走到上游服务 A;另一条是 /api/path-b,会再转发到一个外部依赖服务。其他路径完全正常。
这个分布其实很关键。如果所有路径都报错,那大概率是网关本身出了问题,比如路由表挂了、限流配置错了、证书过期了;但只有特定路径报错,说明问题基本锁定在上游服务、路由配置或者外部依赖这条子链路上。我当时的判断是:先不要在网关全局配置上浪费时间,直接把排查重心放到这两个失败路径对应的链路上去。
1.2 链路上的可能故障区间
OpenClaw_Gateway 在架构里是统一的流量入口,所有请求进来之后先到网关,再由网关做路由、鉴权、限流和转发。一次请求要经过的完整链路由下面几个区间组成:
| 区间 | 涉及组件 | 主要风险点 |
|---|---|---|
| 客户端到网关 | 客户端、DNS、网关监听端口 | DNS 解析异常、网关连接数被打满、TLS 握手慢 |
| 网关自身处理 | 路由规则、鉴权插件、限流策略 | 配置发布失误、插件 panic、规则匹配冲突 |
| 网关到上游 | 网络传输、上游实例注册状态、连接池 | 上游实例被摘除但网关不知情、连接超时、网络分区 |
| 上游服务内部 | 服务进程、线程池、数据库连接池 | 处理慢、线程阻塞、GC 停顿、依赖超时 |
排障的时候最忌讳的就是上来就翻日志,因为你不知道去哪里翻。我的做法是先做一个减法:客户端能正常打开网关首页,说明网关本身在监听;网关 CPU 和内存不高,说明不是资源耗尽;网关到我们自己的网络历来很稳定,暂时先不怀疑物理网络。真正需要重点排查的,就是“网关到上游”和“上游服务内部”这两个区间。
把范围缩到这两个区间之后,接下来所有动作才有了方向:查网关到上游的建连情况,查上游实例的注册状态,查上游服务自己的处理耗时。事实证明,这个方向是对的,但真正找到的根因比预想的要复杂,不是单个问题,而是三个问题叠加在一起。
2. 第一轮排查:链路追踪与组件日志解读
2.1 观察网关访问日志与错误日志
第一次动手是先翻 OpenClaw_Gateway 的 access log,过滤出所有 5xx 的请求,看它们都落在哪些上游和后端服务上。日志里大量出现下面这种两条错误消息:
[error] [req_id 9f2c...] connect() failed (110: Connection timed out) while connecting to upstream [error] [req_id 8a1d...] upstream timed out (110: Connection timed out) while reading response header from upstream第一条意味着网关在向上游发起 TCP 连接的时候就已经卡住了,连接没有建立成功;第二条意味着 TCP 连接已经建立,但上游迟迟不给响应头,网关读超时了。这两条报错对应着前端现象里 502 和 504 同时存在的表现。
这里要稍微解释一下机制。502 通常出现在连接建立失败、连接被重置、或者上游返回了无法理解的响应的时候;504 则普遍是连接建立了,但上游处理时间超过了网关注册在案的超时阈值。两种错误在同一个时间段里集中出现,说明上游服务处于一种非常不稳定的状态:有的实例端口根本连不上,有的实例虽然能连上但内部已经转不动了。后面这个判断,是引导我查健康检查机制的重要线索。
2.2 从客户端视角复现请求
日志只能告诉你服务端发生了什么,但要感受到用户实际体验到的效果,最好还是自己像一个普通客户端一样发几次请求试试。我直接在当前终端里用 curl 对网关发起请求,带上了连接超时和最大超时参数:
curl -sv --connect-timeout 5 --max-time 15 \ https://internal-gateway.local/api/path-a反复执行了大概二十次,结果很稳定地复现了间歇性失败:大约四分之一请求会快速返回 502,五分之一请求会一直卡到 10 秒左右才返回 504,其余请求能正常返回 200。这个复现结果和监控面板上的现象完全对上了。
注意一个细节:返回 502 的请求“快速失败”,说明网关发出请求之后,上游那边立刻把连接拒绝了,或者网络层直接告知不可达;而返回 504 的请求则是“卡住超时”,说明上游接受了连接但迟迟不处理完。一个上游如果只是单纯重启,理论上只会有短暂时间的连接拒绝,不应该持续出现这么长时间的“接受连接但不处理完”的状态。所以这时候我有理由怀疑,问题不只是实例重启,更可能是上游服务的线程池或连接池被什么东西堵死了。
2.3 抓包与链路追踪
仅仅看应用层日志还不够,因为应用层日志是经过网关抽象之后输出的,它不会告诉我们 TCP 层的表现。我在网关容器里直接用 tcpdump 抓了到上游服务 A 的流量,先确认抓包位置和过滤条件:
tcpdump -i eth0 -nn host 10.10.1.18 and port 8080 -w /tmp/gateway.pcap抓了大概两分钟,再用 Wireshark 打开看 TCP 流的握手阶段。现象非常明显:网关向上游 10.10.1.18:8080 发 SYN 包,有相当一部分没有得到 SYN-ACK 回应,直接触发了 TCP 连接超时重传,最后网关放弃连接;而另一部分连接是三次握手成功了的,但后续 HTTP 请求发出后,对端长时间没有回 ACK 或数据包,一直等到超时。
这就把问题彻底锁定了:不是网关的配置问题,而是上游服务的网络栈和业务进程已经处于亚健康状态。能接受部分连接,但又不完整处理,看起来很像操作系统连接队列被打满,或者服务内部工作线程数耗尽,新请求进来之后只能排队。
顺手,我还把链路追踪系统打开看了这几个失败请求的 span 记录。网关自己生成的 span 里显示“上游响应超时”,而上游服务侧根本没有对应 span 产生,说明请求虽然到了上游机器,但还没被业务代码接收到就被丢弃,或者卡在更底层的地方。到这里,排查重心已经从“网关层”完全转移到了“上游服务实例的存活状态”上。
3. 第二轮排查:配置审计与健康检查机制
3.1 服务发现缓存与健康检查配置的关联
既然问题定位到了上游实例状态,那接下来要看的就不是业务代码了,而是 OpenClaw_Gateway 如何感知上游实例的健康状态。网关本身并不维护一份静态的上游列表,它通过服务发现组件动态获取上游实例的 IP 清单,然后对每个实例做健康检查,把不健康的实例临时摘除。
我拉出 OpenClaw_Gateway 的 upstream 配置看了一眼,发现了几处值得警惕的参数:
upstream: discovery: enabled: true provider: internal-sd cache_ttl: 10m healthcheck: enable: true scheme: http path: /healthz interval: 5s timeout: 1s unhealthy_threshold: 3问题其实已经写在这些数字里了。服务发现缓存 TTL 是 10 分钟,意思是网关在本地缓存的实例列表最长可能 10 分钟都不会更新一次。也就是说,即使某个上游实例已经从这个世界上消失了,网关仍然会按照旧列表继续往这个 IP 上发请求,直到某个环节发现连不上才把它剔除。这个 TTL 在基础设施比较稳定的场景下可能问题不大,但在上游实例频繁重启或弹性伸缩的环境里就是定时炸弹。
更细看健康检查的配置:间隔 5 秒,超时 1 秒,失败 3 次判定为不健康。按这个配置,一个实例从“开始不可用”到“被网关摘除”,最快要经过大约 3 个间隔,也就是 15 秒左右。这 15 秒里网关还会持续往这个已经废掉的实例发送新请求。如果服务发现缓存又恰好还没过期,那这个不可用 IP 会在本地缓存里留存更久,故障窗口就会被拉得非常大。
3.2 健康检查参数的不合理性分析
很多人会把健康检查错误理解成一个“保命开关”,以为只要配了健康检查,网关就一定能绕开故障实例。但健康检查本身能起多大作用,完全取决于你参数怎么设置。这次配置的主要问题有两个。
第一个问题是超时时间太短。健康检查的超时只有 1 秒,在正常情况下这个值足够,因为一个健康的实例返回 /healthz 通常只需要几十毫秒。但问题恰恰在于“不健康”不一定等于“连接不上”,也可能等于“进程活着但处理很慢”。我见过不少上游服务在依赖的数据库出现抖动时,健康检查接口自己也跟着变慢,1 秒内根本返回不了,网关就会误判实例不健康,然后把它摘掉。一旦数据库抖动恢复,又需要好几个周期才能加回来。一次小抖动就可能导致一批实例被摘除,造成大量误报。
第二个问题是健康检查做的探活太浅。配置里只检查了 /healthz 这个存活接口,没有区分“进程活着”和“服务就绪”两个概念。正确的做法应该是把存活检查和就绪检查分开:存活检查只关心进程是否真的活着,如果死了就重启;就绪检查才关心能不能承接流量,如果就绪检查失败,网关就把它离线。很多网关只探一个 200 状态码,结果服务线程池都堵死了,HTTP 健康检查接口还是能返回 200,因为返回 200 的逻辑简单到不依赖任何临界资源,网关还是会傻傻地把流量送进去。
3.3 证书与外部依赖检查:第三个叠加问题
处理完网关到上游服务 A 的排查之后,我回头看另一个失败路径 /api/path-b,发现它的情况略有不同。这个路径会二次转发到外部依赖服务,而外部依赖服务在前一天刚刚做了一次证书轮换。OpenClaw_Gateway 到外部依赖服务之间走了双向 TLS,网关本地缓存了对方的证书链和 TLS 会话票据,会话缓存的 TTL 配的是 12 小时。
证书轮换完成之后的十几个小时里,因为网关和外部服务之间还有大量长连接和 TLS 会话复用,新请求直接复用旧会话,很多都还能正常通过。但如果本地缓存的旧证书已经失效,且某个请求触发了一次重新握手,网关就会拿旧证书去校验新服务端证书,然后报错。这类错误在故障刚开始的几分钟内不会立刻爆出来,它会随着缓存逐渐过期而慢慢增多,表现出的失败率曲线就是锯齿状的,和我在监控里看到的完全吻合。
这次排障到现在,已经能确认是三个问题叠加:
- 上游服务 A 部分实例不稳定,且服务发现缓存和健康检查参数都不够敏感
- 上游服务实例在重启时没有优雅退出机制,网关不知情,仍继续路由到旧 IP
- 外部依赖服务证书轮换后,网关的证书缓存清理不及时,导致连接复用失效后集中暴露错误
每个问题单独拎出来都不至于造成这么长时间的大面积故障,但三个问题集中在同一个时间段里碰头,表面现象就变得非常具有迷惑性。这也是网关类故障最常见的特征:多个小问题叠加,掩盖真实根因。
4. 核心问题定位与修复动作落实
4.1 问题一:缩小服务发现缓存窗口与健康检查窗口
第一个要修的肯定是服务发现缓存 TTL。10 分钟太长,当上游实例出现弹性伸缩或异常重启时,网关会长时间保留已经失效的 IP。我把 TTL 从 10 分钟降到 30 秒,并且在上游实例列表变更时增加一个主动回调机制,让网关可以马上收到实例上下线事件,而不是等下一个轮询周期。
健康检查参数也做了对应调整,调整前后的对比如下:
| 参数 | 调整前 | 调整后 | 调整理由 |
|---|---|---|---|
| 服务发现缓存 TTL | 10m | 30s | 缩短失效实例残留时间 |
| 健康检查间隔 | 5s | 8s | 避免检查抖动导致误判 |
| 健康检查超时 | 1s | 3s | 给慢路径留出合理响应时间 |
| 失败判定阈值 | 3 | 5 | 容忍瞬时抖动,不轻易摘除 |
| 就绪检查 | 未开启 | 开启并独立配置 | 区分存活与就绪语义 |
这里我想多说一句参数之间的关系。健康检查的“敏感性”和“稳定性”永远是一对矛盾。间隔越短、阈值越低,网关发现故障实例越快,但网络抖动或 GC 波动时也越容易误摘除;反过来,间隔越长、阈值越高,系统越稳定,但真正出问题时故障窗口也越长。我的建议是不要用一个固定参数套所有服务:对无状态、快速扩容的服务可以调激进一点,对有状态或启动恢复慢的服务一定要调保守一点,并且要加上就绪检查。
4.2 问题二:上游优雅退出机制落地
在网关侧做再多参数优化,也不能根本解决一个问题:上游实例在退出时如果直接把进程杀掉,网关是无法提前感知的。真正规范的姿势是让上游服务在退出前主动通知网关“我要走了”,然后等一段时间再真正退出。
我给上游服务 A 加了一段退出前的预处理逻辑,核心思路是:先把当前实例在服务发现系统里标记为不健康,让新请求不再进来;然后等 10 到 20 秒,给已经进来的存量请求一个完成处理的时间;最后再真正杀掉进程。这个逻辑如果放在容器平台里,可以放到退出前的钩子里,示例脚本大概长这样:
#!/usr/bin/env bash # 1. 给自身健康检查接口一个非 200 的状态 curl -s -X POST http://127.0.0.1:8080/healthz/disable || true # 2. 在服务发现系统里主动注销当前实例 curl -s -X PUT http://internal-sd.local/v1/agent/maintenance/enable || true # 3. 等待存量请求处理完 sleep 20 # 4. 真正退出 kill -TERM 1这段脚本看起来简单,但最关键的就是“sleep”和“kill”中间那段时间。说得直白一点:给流量一个排出窗口,有多少能力就排多少时间。如果服务本身处理请求特别慢,20 秒不够,就得把时间调长,甚至引入优雅退出机制比较完善的应用框架,让业务代码收到退出信号后主动停止接收新请求,并且等待线程池清空。配好之后,我在灰度环境模拟了一次实例重启,确认网关在实例注销后不会再把请求路由到那个 IP 上,整个切换过程对用户完全无感。
4.3 问题三:证书轮换后主动清理缓存
证书问题这边的修法分两步走。第一步,把 OpenClaw_Gateway 对外部依赖的证书会话缓存 TTL 从 12 小时改成 30 分钟,避免旧的证书信息在本地停留太久。第二步,增加一个证书变更后的主动通知机制,外部依赖服务在轮换证书之后,通过一个内部接口触发网关清理对应域的 TLS 缓存。这个维护入口做成了一个简单的控制命令:
openclaw-gateway-ctrl tls purge-cache \ --upstream external-svc.local \ --cluster staging这里有个教训:证书类问题最大的难处在于它没有明确的报错点。TCP 连接能建立,HTTP 层也能发起,但 TLS 握手里就悄悄失败。很多情况下你只有在错误日志里反复看到certificate verify failed或者tls handshake timeout才能真正意识到是证书换掉了。所以我给外部依赖链路加了一个证书到期日期的监控指标,每天凌晨会把证书剩余有效天数推送到监控系统,到期前 14 天就开始告警。证书轮换也应该纳入变更记录管理,不然下次又会在排障时把它漏掉。
4.4 验证与灰度发布
修复动作不能一股脑全量上,我的做法是先找灰度环境跑一轮,确认没引入新问题再全量推广。这套配置一共分三步验证。
第一步,在灰度集群先更新 OpenClaw_Gateway 的上游配置,等 10 分钟确认错误率下降,同时观察网关自身有没有出现上游实例频繁上下线的告警。第二步,在上游服务 A 的测试环境验证优雅退出脚本,连续做了三次实例重启,确认 5xx 比例始终为零。第三步,把证书缓存 TTL 的改动全量推到生产,并手动触发了一次缓存清理,再连续调用 /api/path-b 相关接口监测。整个验证过程中我重点观察三个指标:5xx 比例、网关到上游的连接建立成功率、TLS 握手失败次数。
灰度发布一共持续了大约 40 分钟。新配置全量生效之后,5xx 比例稳定回落到 0.05% 以下,锯齿状波动消失。两天后又遇到一次上游实例计划内重启,这次监控面板上只出现了一个平滑的流量切换曲线,没有出现任何 5xx。可以说三个问题都得到了有效解决,而且修复方式不是靠重启网关掩盖问题,而是把整个故障发现和排除链路理顺了。
5. 常见故障速查表与排障工具清单
5.1 网关故障排查速查表
这次排障过程中我整理了一张速查表,之后再遇到网关类的故障,基本都是照这个思路去匹配,效率高很多。这里分享出来:
| 故障现象 | 可能原因 | 快速排查手段 | 修复方向 |
|---|---|---|---|
| 502 大量出现,连接快速失败 | 上游实例被摘除但网关仍路由;实例端口未监听 | 看网关错误日志里 connect failed 指向的 IP;查服务发现列表 | 缩短服务发现缓存 TTL,开启上游主动注销 |
| 504 大量出现,请求卡住超时 | 上游进程活着但处理慢;线程池耗尽;数据库依赖慢 | 看网关日志里 upstream timed out;查上游线程池指标 | 优化上游慢路径;调整网关超时;增加就绪检查 |
| 间歇性 5xx,锯齿状波动 | 缓存过期集中触发;多个实例逐批失败 | 比对故障时间点和缓存/证书过期时间点 | 缩短缓存 TTL;增加证书到期监控 |
| 证书握手失败 | 外部服务证书轮换,网关缓存旧证书 | 抓包看 TLS alert;openssl 查证书有效期 | 清理缓存;缩短会话缓存 TTL;证书变更通知 |
| 上游实例重启导致短时间 5xx | 实例退出前没有摘流量 | 看重启时间点和 5xx 时间点是否重合 | 优雅退出钩子,先摘流量再退出 |
| 健康检查误摘导致服务抖动 | 健康检查超时太短,依赖慢导致探活返回慢 | 看网关日志里健康检查失败高频目标 | 调大超时,增加失败阈值,引入就绪检查 |
这张表不可能覆盖所有故障,但能覆盖绝大多数网关场景下常见的劣化模式。排障时先对着现象找到对应行,再按顺序排查,会比满世界乱翻日志高效得多。
5.2 推荐排障工具与命令
再整理一组我实际高频使用的工具,都是比较常规的手段,但在关键时候很顶用:
- curl:用来从客户端视角复现问题,加
-v、--connect-timeout、--max-time很好用 - tcpdump:抓 TCP 层流量,看连接是否建立成功、有没有大量重传
- openssl:查外部服务的证书有效期,比如
openssl s_client -connect external-svc.local:443配合openssl x509 -noout -dates - dig / nslookup:当怀疑 DNS 问题时,手动解析域名确认记录对不对
- 网关自身的 access log:任何排障的第一步都是先看它,过滤出状态码和耗时
- 链路追踪系统:看 span 之间的跳转关系,能快速定位请求到底卡在哪一跳
写到这里真的要提醒一句:工具不在多,而在于你什么时候用它。我看到很多新手排障时习惯性地先去开 Wireshark,结果抓了一大堆包也不知道从哪看起。正确顺序一定是先看日志,日志能定位七成问题;日志看不出再上网络层抓包;只有怀疑链路跳转时才需要链路追踪。
5.3 排障原则:先对齐时间线,再动手改配置
还有三条原则,是我做这次排障之后更加确信的:
第一条,永远先比对“故障时间线”和“变更时间线”。任何监控指标的异常曲线,都先去对齐前后 24 小时内有没有发布、配置修改、证书轮换、实例重启。很多故障的根因就藏在一次不起眼的变更里。
第二条,不要在没定位到根因之前就重启网关或者重启上游。重启能暂时把错误率清零,但问题第二天会原样回来,甚至变得更难查,因为你丢掉了故障现场。这次排障里如果我先顺手重启了网关,那三个叠加问题至少还要再花一倍时间才能重新暴露。
第三条,修复动作一定要小步快跑,一次只改一个变量。不要把服务发现缓存、健康检查参数、证书缓存 TTL 同时改了再一起验证,因为一旦结果变好,你无法确定是哪个改动起了作用。我先改健康检查参数验证一轮,再改缓存 TTL,再改证书缓存,每步都有明确指标对应,才能确定每个修复动作对结果的贡献。
6. 这次排障给我的几个长期习惯
排障结束之后,我没有急着把这事翻篇,而是把这次学到的教训沉淀成了几个长期执行的习惯。第一个习惯是在 OpenClaw_Gateway 里新增了一个指标:上游服务发现返回的实例数,与上游实际注册的实例数之间的差值。只要这个差值超过一个阈值就告警,因为这意味着网关的本地视图和真实世界不一致,迟早会出问题。
第二个习惯是重新梳理健康检查。我把每个上游服务的健康检查都拆成了存活检查和就绪检查两套,存活检查只管进程死活,就绪检查才决定是否放流量,并且根据服务自身特性单独设置超时和阈值。这个改动做下去之后,最直接的效果是上游服务依赖抖动时,网关再也不会因为误判把实例摘掉。
第三个习惯是给所有外部依赖链路加证书监控。核心服务的证书至少提前 14 天告警,证书轮换必须写入变更记录。说句实话,证书问题在排障里是最容易被忽略的,因为它不像 CPU 飙升那样直接可见,但一旦出了事,影响面一点不比核心代码故障小。
最后还有一个小技巧,纯属个人经验:像 OpenClaw_Gateway 这种流量入口,排障时一定要先看错误日志里的“上游 IP”而不是“客户端 IP”。客户端 IP 只能告诉你谁访问失败了,上游 IP 才能告诉你它失败在哪个环节。日志里每一个 upstream 字段都是你排查链路的下一个入口,顺着它一层层往下追,比在网关配置里反复找原因要靠谱得多。