502、504、503 分不清?先确定是谁替你答了这句

· 7 分钟阅读 · HTTP 状态码 502 线上排查 · English

“网站挂了,502。"——这句话里其实混着三个不同的问题:谁返回了这个数字,你的请求有没有走到应用,以及现在再点一次刷新算不算制造事故。这三个问题答案不一样,处理方式也不一样。下面把同一台机器上真实发出来的响应逐个过一遍,看浏览器拿到 5xx 的时候到底做了什么。

一、5xx 四个数字的分界只在两句话上

  • 500 —— 应用自己抛了异常。它知道自己是谁,也知道自己错了。
  • 503 —— 服务端活着,但此刻明确不接活:维护、打满、队列满。它通常还会附一个 Retry-After。
  • 502 —— 前面那台机器(nginx、ALB、CDN 边缘、网关)从它后面的服务拿到了用不了的答复:进程没了、连接被重置、响应格式坏掉。
  • 504 —— 同一台前置机器按自己的上限等过后端,等不及了,于是替你答了这一句。

502 与 504 的区别就一句:上游答了但没法用,还是上游没来得及答。而这两个数字都不是应用写的——请求到底有没有进到应用里,正是 5xx 排查里最贵的那个未知数,光看数字给不出答案。

二、实测:把 5xx 打给浏览器看,它分别做了什么

我起了一个本地测试服务,让同一条 URL 分别返回不同的结果,然后用这台机器上的 Chrome 逐个访问(下面是观测到的现象,不是文档转述):

服务端做了什么 浏览器里看到 fetch() 拿到
502 带 HTML 正文 直接渲染服务端那页 HTML,标题、正文原样出现 status: 502、ok: false,正文 66 字符可读,响应头 X-Served-By 也在
502 空正文 一片空白,但地址栏还是那条 http 地址,文档本身存在(正文 0 字符) status: 502、正文 0 字符
503 + Retry-After: 120 渲染服务端页面 status: 503,retryAfter 读得到 = "120"
429 + Retry-After: 60 同上 status: 429,retryAfter = "60"
接受连接后直接把 socket 拆掉,一个字节都不答 打不开:ERR_EMPTY_RESPONSE,文档变成 chrome-error:// TypeError: Failed to fetch
答一段不是 HTTP 的乱字节 打不开 TypeError: Failed to fetch

三条结论:

  1. 有数字 vs 没数字是两类故障。 5xx 说明至少有一台机器按 HTTP 说清楚了"我办不成”;ERR_EMPTY_RESPONSE、Failed to fetch 说明连一个 HTTP 答复都没回来(TCP/TLS/进程层面的事)。工单里"打不开,报 502"和"打不开,浏览器说无法连接"不是同一件事,后者的第一站是网络与进程,不是应用日志。
  2. fetch() 不会为 5xx 抛异常。 429、500、502、503 全部正常 resolve,ok: false。也就是说:只有 response.ok 的判断有没有写,没有 catch 会替你接住线上故障。
  3. 错误页可能就是服务端自己的页面。 有正文的 502,Chrome 原样显示它——所以你看到的那张"网站暂时不可用"是你的代理或应用写的,措辞能告诉你是哪一款产品在前面。(别把这条外推到其他浏览器:空正文时有的浏览器会换成自己的错误页,这一条要在你自己那台机器上测。)

三、浏览器不会替你退避

Retry-After 是给客户端代码看的建议,不是浏览器的行为指令。上面那批请求我用服务端计数器逐条核过:每个 URL 每个状态码只到达一次,包括带 Retry-After: 120 的 503 和 60 的 429——浏览器没有等,也没有重发。

退避得自己写,而且要写对:

async function withBackoff(fn, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const res = await fn();
    if (res.ok) return res;
    if (res.status !== 429 && res.status !== 503) throw new Error('HTTP ' + res.status);
    const ra = Number(res.headers.get('retry-after'));
    const ms = Number.isFinite(ra) ? ra * 1000 : Math.min(30000, 500 * 2 ** i);
    await new Promise(r => setTimeout(r, ms + Math.random() * 250));  // 抖动,别一起醒来
  }
  throw new Error('gave up');
}

三个容易写错的地方:只在 429/503 上退避(400、422 重发一百次也不会变对);读 Retry-After 优先于自己的指数曲线(服务端那句"等 120 秒"比你的 2^n 有信息量);加抖动——一万个客户端在同一个 503 之后 500ms 齐刷刷回来,等于把故障又制造了一遍。

四、子资源上的 502 只表现为"这块东西没出来"

我又让一个 200 正常返回的页面去加载一个返回 502 的图片、一个返回 500 的脚本和一个返回空正文 502 的 iframe。结果:

  • 页面本身正常渲染(document.title 在,正文在);
  • 那张 <img> 的 complete === true、naturalWidth === 0,并且 onerror 触发了;
  • iframe 的文档在,正文 0 字符。

主文档是 200 不代表这一屏没问题。“图裂了"“图表没画出来"“页面样式丢了"经常就是某个子资源吃了 502/504,而它不会变成任何错误页。这类现象的第一站是 DevTools 的 Network 面板按状态过滤,不是应用日志。

五、动手之前,先定位是哪一跳在说话

按顺序做,每步都在缩小范围:

  1. curl -i 走一遍正常链路,把响应头抄下来。有 Server:、Via:、X-Request-Id、CDN 自家头(cf-ray、x-amz-cf-id)时,能直接认出是谁答的。
  2. 绕开前置层直连源站(内网地址、--resolve 或改 hosts)。直连成功而经代理失败 → 问题在中间那一跳的 upstream 配置或健康检查;直连也失败 → 应用本身。
  3. 看时间差。 504 的耗时几乎等于某一跳的超时上限(代理等后端 60s 就是 60s 整),502 常常是毫秒级——连接被立刻重置。这两者指向的修法不同:一个是超时/慢查询,一个是进程或端口没在听。
  4. 看上游进程健康,再看 worker / 连接数是否耗尽。只在高峰出现的 502,通常不是这次请求写错了,而是前面那台机器后面没有可用的 worker。
  5. 对齐时间戳去翻日志。 拿响应里的 Date 或 request id 去源站日志找同一条;找不到这条请求,就是它根本没走到应用——这本身就是结论。日志里那种 10 位或 13 位数字先换算成北京时间再看顺序,否则容易把 UTC 的那一条当成一小时前的自己。

六、那到底能不能再点一次刷新

“能不能重发"是状态码的性质,不是你的处境:

  • 可以原样重发:408、425、502、504 —— 服务端上一次并没有真的动手。
  • 要等 Retry-After:429、503 —— 等待本来就是这两个响应的全部用意,猛刷只会把 503 变成雪崩。
  • 先确认有没有副作用:500、409、422、428、431、451、507、511 —— 请求可能执行了一半才失败,重发会把同一件事做两遍。

这层判断是码给的,不是保证:一个发生在应用已经写库之后的 502,正是"可以重发"这个标签看不见的东西。所以写操作遇到 5xx,先看有没有幂等键,再决定要不要重放。

七、有几个 5xx 根本不该你去修

  • 499 —— 不是标准码,nginx 用它记"服务端还没答完,客户端先挂断了”。它只出现在日志里,浏览器一辈子看不到;要问的是客户端为什么走(它自己的超时、被取消的 fetch、用户切了页面)。
  • 511 —— 网络认证门户(酒店、机场)在向你索要东西。一些毫不相干的站点不停回 511,说明你在门户后面,而不是那个站点坏了。
  • 451 —— 法律主张导致的下架,这不是缺陷,别当故障排。

用状态码查询器对一遍

拿到一个陌生的数字,可以用在线 HTTP 状态码查询:输入三位数字,或者直接把状态行(HTTP/1.1 302 Found)、一段 nginx 日志贴进去,它会给出这个码属于哪一段、服务端在什么时候产生它、客户端紧接着做了什么、第一件事查哪里,以及第四栏那个最贵的问题——这个请求能不能重发。它不会替你访问贴进来的地址:那串 URL 只被当作文字解析,你输入的内容不会离开这台设备。表外的数字(100–599)仍然会给区段解释,因为决定行为的本来就是首位那一段。

相关问题

Q:为什么我看到的错误页写着 nginx,但源站是另一个系统? 因为那句就是你前面那一跳写的。502/504 由代理生成,它有自己的一套默认错误页——上面第二节实测过:带正文的 5xx 浏览器原样显示。错误页的措辞反而是一条有用的线索。

Q:Retry-After 是不是浏览器会自动等? 不是。第二节和第三节都核过:一次请求对应服务端一次到达,浏览器只是把那个头交给你。

Q:刷新有没有用? 如果这一行代码有 bug,刷新只是让同一个 bug 再执行一次;如果 503 是发布窗口,等一分钟再刷是对的。区分这两件事的只有日志和 Server: 头,不是"多刷几次”。

Q:状态码能当监控指标吗? 能,但要按区段和来源分开:4xx 涨通常是客户端或爬虫,5xx 涨是服务端;而如果你的边缘会重写错误页和状态码,源站的 5xx 和用户的 5xx 是两条不同的曲线,只看一条会误判。

微信
微信
支付宝
支付宝