题干与适用场景
一个接口在 Node 中执行多步外部调用,正常耗时 5 秒到 35 秒。Node 的请求超时配置为 60 秒,NGINX 的 proxyreadtimeout 保持默认值。线上偶发 502,用户认为任务失败;应用日志却显示业务写入在之后完成。压测还发现并发升高时连接被长时间占用。
请画出客户端、NGINX、Node、下游依赖和任务存储的时间线,说明 502 的真正来源、应用为何仍会完成工作,以及如何选择同步响应、流式保活或异步队列。题目中的时长是面试假设,核心考察是跨层超时语义、取消与副作用边界、资源隔离和可验证修复,因此归入 backend。
面试官考察点
高质量回答会先区分“连接关闭”“应用截止时间”和“业务任务完成”三个事件,而不是看到 502 就重试。还应知道 NGINX 的读超时是两次上游读取之间的空闲时间,不等于完整响应总时长;应用层的 AbortSignal.timeout() 只会向监听该信号的操作发出取消通知。
面试官还会观察候选人是否盘点所有代理、网关、负载均衡、SDK 和客户端的默认值,是否把一次 HTTP 请求与长任务解耦,以及是否能证明修复没有制造重复写入、连接泄漏或更大的重试流量。
回答前需要澄清的问题
- 502 是 NGINX 生成还是 Node 返回后被改写?要对照响应头、代理错误日志和上游访问日志。
- 超时发生时业务副作用是否已经提交?若结果未知,不能简单重发。
- 响应是否必须在本次请求中返回?若用户只需要最终结果,同步链路没有必要承载 35 秒任务。
- 上游是否持续发送响应字节?有数据流时,读超时与总请求截止时间的含义不同。
- 取消信号是否真正传到数据库、HTTP 客户端和外部 SDK?仅关闭浏览器连接不会自动停止所有工作。
- 当前并发、连接池、事件循环和队列指标怎样变化?要先确认资源瓶颈再改数字。
30 秒回答框架
“我先用同一个 trace ID 对齐客户端、NGINX、Node 和下游日志,确认 502 由哪层在什么时候产生。NGINX 的 proxyreadtimeout 只限制连续读取之间的空闲间隔,Node 的 60 秒也不能保证业务任务在客户端断开后停止。若结果必须同步返回,就建立统一的端到端截止时间,并让代理、应用和下游留出收尾余量;若任务超过交互预算,就持久化任务、立即返回 ID,由 worker 执行并提供状态查询。修复后用故障注入验证没有重复副作用、连接占用和重试放大。”
分步骤深入解答
先画时间线:客户端发起请求,NGINX 转发,Node 开始工作;若 Node 在一段时间内没有发送响应字节,NGINX 的读计时器到期并关闭上游连接,代理可能返回 502 或 504。Node 进程未必收到同一时刻的取消,也可能已经把写入提交,随后继续计算并记录“完成”。因此“用户看到失败”和“业务完成”可以同时成立。
用 trace ID、请求 ID 和任务 ID关联四类日志:NGINX access/error log、Node 请求开始/结束/abort、下游调用、数据库提交。比较 upstreamresponsetime、Node handler duration、客户端收到响应时间和副作用提交时间,才能判断是代理空闲超时、应用截止时间、下游超时还是客户端主动断开。不要只查应用成功日志。
NGINX 的 proxyreadtimeout 默认值为 60 秒,含义是两次读取之间允许的最长空闲间隔;响应持续有字节时计时会重新开始。把它调大只能改变代理容忍度,不能阻止连接占用,也不能修复客户端更短的截止时间。若改配置,应把代理上限、应用截止时间和客户端预算写成一张表,并留出发送响应、清理和网络抖动的余量。
应用可以使用 AbortSignal.timeout() 创建截止信号,并把信号传给支持取消的 fetch、数据库或 SDK。取消是协作式的:未监听 signal 的库仍可能继续运行;已经提交的数据库事务也不能靠 abort 回滚到“从未发生”。因此每个副作用都要有幂等键、状态机或补偿路径,记录 accepted/running/succeeded/failed/unknown 等可解释状态。
如果任务 p99 明显超过交互预算,改成异步边界更安全:API 校验输入并写入任务记录或队列,返回 202 与任务 ID;worker 领取任务、设置租约、执行重试和持久化结果;客户端轮询或订阅状态。任务状态写入必须幂等,worker 重启后可以恢复,重复投递不会重复扣款或创建资源。队列增加了 Redis、数据库和死信队列等运维面,要为其设置容量与告警。
若任务只需略超时且结果可流式产生,可以发送心跳或分块响应,但要确认所有中间层允许长连接、客户端能处理增量数据,且心跳不是掩盖无界工作的方法。流式接口仍需总截止时间、最大字节数和取消处理。不能用“调大 NGINX 超时”代替业务边界设计。
修复验证包括:将 NGINX 读超时设置成已知值,注入超过该空闲时间的下游延迟,观察返回码与日志;客户端在不同阶段断开,确认取消传播和副作用状态;并发压测检查事件循环延迟、连接池占用、队列深度和 worker 吞吐。断言每个逻辑任务最多一次成功副作用,重试不会越过截止时间,代理与应用日志能用 trace ID 对齐。
高质量示范回答
“我不会先把 30 秒改成 5 分钟。第一步是用 trace ID 对齐 NGINX、Node、下游和数据库时间,确认 502 是代理在等待上游字节时产生,还是 Node 自己返回。NGINX 的 proxyreadtimeout 是两次读取之间的空闲上限;Node 的请求超时和业务任务完成也不是同一个事件,所以代理关闭连接后,Node 仍可能继续工作并提交副作用。
如果结果必须同步返回,我会定义一个端到端截止时间,向下游传递剩余预算,使用 AbortSignal.timeout() 并确保 SDK 真正监听取消;同时把代理、应用和客户端的超时排序并留出清理余量。若写入结果在断开后未知,就用幂等键和状态查询,不能盲目重发。
若任务 p99 远超交互预算,我会让 API 持久化任务后立即返回 202 和任务 ID,worker 负责执行、重试和结果状态,客户端轮询或订阅。验证时注入代理空闲、客户端断开、worker 重启和重复投递,检查没有重复副作用、连接池没有被长任务占满,且每一条日志都能还原同一条时间线。”
常见错误
- 只调大 NGINX 超时 → 长连接和并发占用继续增长 → 先定义业务截止时间与异步边界。
- 看到 502 就重试 → 原任务可能已提交副作用 → 先查询状态并使用幂等键。
- 把
proxyreadtimeout当作总响应时长 → 持续小流量会延长连接寿命 → 另设总截止时间和最大响应时长。 - 关闭客户端连接就假设服务端停止 → 许多库不监听取消信号 → 逐层验证 abort、连接和事务行为。
- 用心跳掩盖无界任务 → 代理不报错但资源持续泄漏 → 限制总时长、字节数和并发。
- 同步 handler 内执行 35 秒任务 → 请求连接承载全部工作 → 持久化任务并由 worker 执行。
- 只看 Node 日志 → 错过代理生成的错误与时间 → 同时采集代理 access/error 和上游耗时。
- 异步化却没有幂等状态 → 重复投递造成重复扣费 → 用唯一任务键和状态机约束写入。
追问及应对
追问一:把 proxyreadtimeout 改成 60 秒就够了吗?
不够。它只控制两次读取间的空闲时间,客户端、应用、网关或下游可能更早截止;同时长连接会继续占用资源。应先确定交互预算,再统一配置各层并监控连接与排队。
追问二:客户端断开后,Node 如何停止工作?
监听请求关闭事件并触发 AbortController,把 signal 传给支持取消的操作;对不支持取消的外部调用设置隔离和结果丢弃策略。已经提交的副作用仍需幂等与对账,不能假设取消能撤销历史。
追问三:什么时候用流式响应?
当结果可以安全分块、客户端和所有代理支持长连接,且总时长可控时使用。流式仍要有心跳间隔、总截止时间、最大输出和取消传播;它适合进度或增量结果,不适合隐藏不可控的后台作业。
追问四:异步队列如何避免任务重复执行?
以业务请求生成唯一任务键,数据库唯一约束保证只创建一条任务;worker 领取时使用租约或可见性超时,完成写入带条件更新,重试沿用同一幂等键。外部副作用要支持幂等请求或先查状态。
追问五:如何证明修复真的生效?
在预发布注入代理空闲、下游慢响应、客户端断开、网络重置和 worker 重启,记录每层时间戳。检查错误来源、最大连接占用、任务完成率、重复副作用、队列延迟和取消后的残留工作,再做并发压测。
追问六:为什么应用日志显示成功,用户仍收到 502?
因为代理可能在收到应用响应前已关闭连接,或者响应在客户端链路被丢弃;应用完成日志只说明业务代码结束,不代表响应成功交付。必须对齐代理 upstream 状态、Node 写响应结果与客户端观测。
追问七:异步化会牺牲什么?
它增加状态存储、worker、重试、死信和最终一致性,需要产品接受“稍后可查”而非即时结果。换来的好处是 HTTP 连接寿命与任务执行时间解耦、并发可独立控制、失败可重放。