502、504、503 分不清?先确定是谁替你答了这句
“网站挂了,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 |
三条结论:
- 有数字 vs 没数字是两类故障。 5xx 说明至少有一台机器按 HTTP 说清楚了"我办不成”;
ERR_EMPTY_RESPONSE、Failed to fetch说明连一个 HTTP 答复都没回来(TCP/TLS/进程层面的事)。工单里"打不开,报 502"和"打不开,浏览器说无法连接"不是同一件事,后者的第一站是网络与进程,不是应用日志。 fetch()不会为 5xx 抛异常。 429、500、502、503 全部正常 resolve,ok: false。也就是说:只有response.ok的判断有没有写,没有 catch 会替你接住线上故障。- 错误页可能就是服务端自己的页面。 有正文的 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 面板按状态过滤,不是应用日志。
五、动手之前,先定位是哪一跳在说话
按顺序做,每步都在缩小范围:
curl -i走一遍正常链路,把响应头抄下来。有Server:、Via:、X-Request-Id、CDN 自家头(cf-ray、x-amz-cf-id)时,能直接认出是谁答的。- 绕开前置层直连源站(内网地址、
--resolve或改 hosts)。直连成功而经代理失败 → 问题在中间那一跳的 upstream 配置或健康检查;直连也失败 → 应用本身。 - 看时间差。 504 的耗时几乎等于某一跳的超时上限(代理等后端 60s 就是 60s 整),502 常常是毫秒级——连接被立刻重置。这两者指向的修法不同:一个是超时/慢查询,一个是进程或端口没在听。
- 看上游进程健康,再看 worker / 连接数是否耗尽。只在高峰出现的 502,通常不是这次请求写错了,而是前面那台机器后面没有可用的 worker。
- 对齐时间戳去翻日志。 拿响应里的
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 是两条不同的曲线,只看一条会误判。