一次 Jenkins 连接超时,让我重新理解了 Pipeline 的真实执行位置
一次 Jenkins 连接超时,让我重新理解了 Pipeline 的真实执行位置
一、一个看似普通的连接超时
最近排查 Jenkins Pipeline 时,我遇到了一次很典型的 HTTP 连接超时。
流水线中的一段 Shared Library 代码通过 OkHttp 请求内部代码服务,等待一段时间后抛出异常:
1 | |
从调用栈看,失败发生在 Socket 建连阶段。请求还没有进入 HTTP 响应处理,更谈不上接口参数、鉴权结果或者返回内容。
我的第一反应是:运行这条 Pipeline 的构建节点无法访问目标服务。
这个判断看起来非常自然。Pipeline 明明被调度到某个 Agent,网络请求似乎也应该从这个 Agent 发出。于是排查很快进入熟悉的路径:检查 Agent、测试目标端口、确认 Git 是否还能拉取代码。
结果却有些矛盾:Git checkout 正常,在 Agent 上执行手工 HTTP 连通性测试也正常,但 OkHttp 依然超时。
真正的问题不是“为什么 OkHttp 超时”,而是“发起这次连接的到底是谁”。
二、为什么“Git 正常、HTTP 也能通”仍然不够
当时最容易让人放松警惕的,是两条看起来很强的证据。
第一条是 Git checkout 成功。它说明 Agent 能通过 Git 使用的协议和端口访问代码服务。
第二条是在 Agent 上执行手工 HTTP 测试成功。它说明这个 Agent 到目标 HTTP 服务的网络路径可用。
但这两条证据都有明确边界:它们只能证明执行测试的那个节点具备相应连通性。
可以把三条证据的边界拆开来看:
- Agent 上 Git checkout 成功
- 能证明:Agent 的 Git 访问路径可用。
- 不能证明:Controller 的 HTTP 路径可用。
- Agent 上手工 HTTP 测试成功
- 能证明:Agent 到目标 HTTP 服务可达。
- 不能证明:OkHttp 一定从 Agent 发包。
- OkHttp 报连接超时
- 能证明:实际调用方未在超时窗口内完成建连。
- 不能证明:一定是某条防火墙规则导致。
这次排查中,我一开始把“Pipeline 运行在哪个节点”错误地等同于“Pipeline 中所有代码都在哪里执行”。
这在分布式自动化系统中是一个危险的简化。
三、真正的转折:失败代码到底在哪里执行
Jenkins Pipeline 是一条逻辑流程,但它不是一个始终运行在同一台机器、同一个进程里的普通脚本。
在这个案例中,真实执行关系更接近下面这样:
1 | |
构建命令和 Git 操作发生在 Agent;发生超时的 Shared Library HTTP 调用,在这个具体调用路径中由 Controller JVM 执行。
于是,看似属于同一条 Pipeline 的两个动作,实际上拥有不同的:
- 执行进程;
- 网络身份;
- 源地址;
- 访问路径;
- 策略边界。
这也解释了之前的矛盾:Agent 上的测试结果完全正确,只是测试位置与故障代码的执行位置不一致。
这里需要保留一个技术边界:不能因此推出“所有 Shared Library 代码都一定运行在 Controller”。Jenkins Pipeline、CPS 上下文、步骤实现和外部进程之间存在不同执行方式。正确做法不是背下一条绝对规则,而是针对失败的那一行代码验证实际执行者。
四、从执行模型还原完整证据链
确认执行位置后,原本零散的线索开始连成一条因果链。
故障出现前,网络访问策略刚刚完成过一次重写。新策略生效后,真正发起 HTTP 请求的 Controller 身份没有被纳入对应访问规则;Agent 的访问路径则仍然正常。
因此才会出现下面的组合现象:
1 | |
补充 Controller 所需的访问策略后,Pipeline 立即恢复。
这一步非常重要。Connect timed out 本身只能说明连接没有在超时窗口内建立,不能独立证明是哪条策略、哪台设备或哪个组件造成了阻断。
本次根因之所以能够闭环,依赖的是一组相互支持的证据:
- 调用栈确认失败发生在 TCP 建连阶段;
- Agent 测试成功,将问题限定到不同执行环境之间;
- 调用上下文确认 OkHttp 实际运行在 Controller JVM;
- 网络策略变更与故障开始时间相符;
- Controller 身份确实缺失;
- 补充策略后,同一条 Pipeline 立即恢复。
日志给出事件,执行模型限定范围,变更记录形成假设,恢复验证完成因果闭环。
五、把一次故障压缩成 WIPPS 排障框架
这次经历最值得留下的,不是一条具体的网络规则,而是一组可以迁移到其他系统的问题。
我把它整理成 WIPPS:
1. Who:谁真正执行这个动作
不要只问“任务调度到了哪里”,而要问失败的这行代码属于哪个进程、容器、JVM 或外部服务。
2. Identity:它以什么身份行动
身份可能是用户、ServiceAccount、Token、证书,也可能是网络设备看到的源地址。执行位置不同,身份往往也不同。
3. Path:数据实际经过什么路径
请求是否经过代理、网关、负载均衡、NAT、Service、Ingress 或额外的网络区域?同一个目标地址,从不同来源出发可能走完全不同的路径。
4. Policy:哪些策略允许、拒绝或改变它
需要检查的不只是防火墙,还可能包括 RBAC、NetworkPolicy、代理规则、路由规则、合并策略和准入控制。
5. State:决策发生时系统处于什么状态
当时使用的是哪个 Commit、配置版本、队列状态、依赖版本和策略版本?今天复现时的系统状态,可能已经不是故障发生时的状态。
在 WIPPS 之后,我还会加上最后一步:Evidence。
1 | |
前五步用于建立模型,最后一步要求用可重复的观察、对照实验或恢复结果验证结论。
六、这个框架还能复用到哪里
Execution Model 并不只适用于 Jenkins。
Kubernetes 流量排查
看到 Ingress 配置并不代表已经理解请求路径。客户端、负载均衡、Ingress Controller、Service、Endpoint 和 Pod 都可能拥有各自的身份与策略边界。测试 Pod 内访问成功,也不能直接证明外部入口正常。
CI 合并决策
一次构建成功只是决策输入,不一定是最终合并决定。构建对应的 Commit、当前分支状态、审批和合并策略可能已经变化。这里同样需要追问:谁做最终决策,使用了什么状态。
AI 故障诊断
如果只把异常日志交给大模型,它很容易给出“检查代理、调整超时、确认服务状态”这类技术上正确但缺少定位力的建议。
更可靠的诊断链路应该先收集执行位置、身份、拓扑、变更和策略证据,再让模型提出可验证的假设:
1 | |
LLM 可以帮助生成假设,但不应该同时充当自己结论的最终裁判。
七、下一次排障时可以直接使用的清单
遇到“某个自动化任务访问服务失败”时,可以先停止围绕错误字符串打转,依次回答下面的问题:
- 失败发生在 DNS、TCP、TLS、HTTP 还是业务响应阶段?
- 失败的这一行代码由哪个进程或组件执行?
- 实际源身份是什么?
- 测试命令是否运行在与故障代码相同的位置和身份下?
- 目标地址、端口和协议是否与真实调用一致?
- 中间经过哪些代理、网关、NAT 或网络区域?
- 哪些策略可能允许、拒绝或改写请求?
- 故障前后发生过哪些配置、策略或拓扑变更?
- 当前证据只是相关性,还是已经通过对照或恢复验证因果?
- 修复后是否使用同一条失败路径完成复测?
这份清单的目的不是增加排障步骤,而是尽早排除一个代价很高的错误:在错误的位置验证正确的问题。
结语:先找到真正执行动作的人
过去遇到异常时,我经常直接进入错误本身:OkHttp 为什么超时、代理是否生效、超时时间是否太短。
这次故障提醒我,在复杂系统里,更好的第一问通常是:
这个动作到底由谁、在哪里、以什么身份执行?
当真实执行模型还没有建立时,很多“成功的测试”都可能只是验证了另一条路径。只有把执行者、身份、路径、策略和状态放在同一张图里,日志才会从孤立的错误信息,变成可以验证的因果证据。
解决一个故障当然重要。更有价值的是,把这次故障压缩成下一次仍然有效的判断框架。