更新日期:2026-09-27
503(Service Unavailable)是生产环境里最"冤枉"的一类错误:它不代表代码写错了,而是服务在某个环节主动声明"我现在暂时无法处理请求"。正因为它是主动返回的,返回 503 的可能是 CDN、负载均衡、反向代理、应用容器中的任何一层——不搞清楚"是谁在返回 503",修复就无从下手。
本篇按照"先分流、再逐层"的顺序,给出一套可复用的 503 排障路径,覆盖 CDN/WAF、反向代理、应用运行时、容器编排四层,并给出维护模式下正确返回 503 的方式与预防措施。文中命令以 Linux 服务器为默认环境。
一、503 的语义:它和 500、502、504 有什么区别
排 503 之前,先把 5xx 家族的关系理清楚。四类错误经常被混为一谈,但排查入口完全不同:
| 状态码 | 语义 | 典型场景 | 排查入口 |
|---|---|---|---|
| 500 | 服务端内部错误 | 代码异常、未捕获异常 | 应用日志的异常堆栈 |
| 502 Bad Gateway | 网关收到了上游的无效响应 | 上游进程崩溃、返回畸形响应 | 网关与上游之间的连接 |
| 503 Service Unavailable | 服务暂时不可用 | 过载、维护、上游全部不健康、限流 | 返回 503 的那一层 |
| 504 Gateway Timeout | 网关等上游超时 | 上游卡死、慢查询、超时配置过小 | 上游耗时与超时配置 |
一句话概括:500 是"应用自己报错",502/504 是"网关对上游不满",而 503 是"链路中某一环明确说暂时不接客"。HTTP 语义上,503 还应该携带 Retry-After 响应头,告诉客户端多久后可以重试——这也是区分"有序维护"和"无序崩溃"的第一个线索。
另外要注意:如果你的站点套了 Cloudflare,浏览器里看到的 52x 错误页(520、521、522、523、525 等)虽然长得像 503,但它们是 Cloudflare 自定义的错误码,含义各不相同(例如 521 是源站拒绝连接、522 是源站连接超时)。见到 52x 时,排查重点在源站可达性,而不是应用本身。
二、三分钟分流:先确定是谁在返回 503
收到 503 报告后,先做四个快速检查,基本可以锁定问题层:
1. 看响应头。 用 curl 直接看原始响应:
curl -sI https://www.mf8.biz/some-page
关注三件事:Server 头(是 cloudflare 还是 nginx)、是否有 Retry-After、是否有 CF-Ray 之类的 CDN 追踪头。如果 Server: cloudflare 且错误页是 Cloudflare 样式,问题大概率在源站或 Cloudflare 配置;如果是 Nginx 默认错误页样式,问题在网关或上游。
2. 绕过 CDN 直连源站。 如果你有源站 IP,可以临时用 Host 头直连测试:
curl -sI -H "Host: www.mf8.biz" https://<源站IP>/some-page -k
直连正常、走 CDN 报错,问题在 CDN/WAF 配置;直连也报 503,问题在网关或应用层。
3. 确认影响面。 是全站所有路径都 503,还是只有某个接口?全站 503 通常指向网关层或进程级故障;单接口 503 更可能是该接口对应的 upstream、线程池或依赖服务的问题。
4. 看监控曲线的起点。 5xx 率是什么时候开始涨的?涨之前有没有发布、扩容、证书更换、上游配置变更?时间点对齐变更记录,往往一步就定位了。
分流时还要警惕重试风暴:503 一旦出现,客户端(浏览器、App、上游微服务)的重试会把流量放大数倍,让本来轻微的过载雪崩成全面不可用。判断方法是对比请求量的 QPS 曲线与 503 曲线是否同步跳涨。缓解手段是在客户端做指数退避(exponential backoff)加抖动(jitter),在服务端优先返回 429 + Retry-After 引导客户端按节奏重试——这也是把限流从 503 改成 429 的另一个理由:语义正确的状态码才能换来正确和退避行为。
三、CDN / WAF 层的 503
这一层的 503 有几个常见来源:
速率限制与防刷规则。 Cloudflare 的 Rate Limiting 规则触发时会返回 429 或自定义状态,但 WAF 托管规则或自定义规则也可能直接拒绝。检查 Cloudflare 控制台的 Security 事件流,按时间过滤就能看到被拦截的请求与命中的规则。
源站健康检查失败。 如果启用了 Cloudflare Load Balancer 或其他带健康检查的负载均衡,源站探测失败后流量会被摘除,此时返回 503 或自定义错误页。健康检查路径的选择很关键:如果探测路径指向一个会因缓存失效而变慢的动态页面,会造成"健康抖动"。探测路径应该是一个轻量、无依赖、无缓存的专用端点。
52x 家族错误的定位。 如前所述,521/522/523 指向源站网络层:源站防火墙把 Cloudflare IP 段挡了(521)、源站响应太慢(522)、DNS 指向了不存在的源站(523)。处理方式是核对源站防火墙/安全组是否放行了官方公布的 IP 段、源站负载是否饱和、DNS 记录是否正确。
回源协议不匹配。 SSL/TLS 模式设置为 Strict 而源站只有自签证书时会出现 526;源站 80 端口关闭而回源走 HTTP 时也会不可达。核对回源协议与源站监听端口是否一致。
这一层排查完成后仍无法解释 503 时,就进入网关层。
四、反向代理层:Nginx 的三类 503
Nginx 返回 503 主要有三个原因,各自的日志特征非常明确:
1. 上游全部不可用(no live upstreams)。 当 upstream 块里所有节点都被标记失败时,Nginx 返回 503,error.log 中会出现:
upstream server temporarily disabled while connecting to upstream
no live upstreams while connecting to upstream
处理方向:确认每个上游节点是否真的存活(逐个 curl 上游健康端点)、max_fails 与 fail_timeout 的组合是否过于敏感(默认 1 次失败即摘除 10 秒)、是否缺少 backup 节点。如果节点本身活着但健康检查路径返回慢,考虑给该路径加 proxy_connect_timeout 与更宽松的判定。
2. 限流模块触发(limit_req / limit_conn)。 一个容易踩的坑:limit_req 超限时的默认返回状态就是 503。这意味着"用户访问太快"这种本该是 429 的场景,会以 503 的面目出现在监控里,干扰排障判断。更语义化的做法是显式指定:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20;
limit_req_status 429; # 超限返回 429 而不是 503
}
}
如果监控里 503 集中在某个高频接口,先查是不是限流配置在起作用——error.log 里会有 limiting requests, excess: ... by zone "api" 的记录。
3. 显式维护页。 有些团队用 return 503 加错误页模板做维护模式,这在排障时要先排除——确认一下这段时间有没有人执行过维护配置的上线。
五、应用运行时层的 503
PHP-FPM:进程池打满。 这是经典中的经典。当所有 FPM worker 都被慢请求占住时,Nginx 与 FPM 之间的排队超限,返回 503。PHP-FPM 日志里会出现明确证据:
server reached pm.max_children setting (5), consider raising it
处理步骤:先用 pm.status_path 打开状态页,观察 active processes 是否长期等于 pm.max_children;然后按内存估算上限——单进程平均内存乘以 pm.max_children 不应超过分配给 FPM 的内存总量,盲目调大只会把 503 变成 OOM;最后回到源头,找出为什么请求变慢(慢查询、外部 API 卡顿),worker 被占住只是症状。
Node.js:事件循环阻塞与健康检查失败。 单进程的 Node 服务一旦事件循环被同步计算或大量同步 IO 堵住,健康检查端点也会超时,编排系统或负载均衡随即摘除节点并返回 503。排查方向:进程内接入事件循环延迟监控(定期 setTimeout(0) 测量漂移)、检查是否有大 JSON 同步序列化、CPU 密集任务是否该挪到 worker 线程或独立服务。用 PM2 时注意 max_memory_restart 触发的频繁重启也会表现为间歇性 503。
优雅停机期的 503。 滚动发布时,旧实例进入关闭流程但仍有流量进来,如果网关没有做连接排水(connection draining),就会出现发布期间的 503 尖峰。修复方式是在负载均衡层配置健康检查与排水窗口,应用侧正确处理 SIGTERM:停止接收新连接、处理完存量请求再退出。
六、容器与编排层的 503
Docker Compose:healthcheck 与 depends_on。 depends_on 默认只保证启动顺序,不保证"依赖服务已就绪"。如果应用启动时依赖的数据库还没就绪,应用反复崩溃重启,网关侧看到的就是 503。给依赖加上 condition: service_healthy 与合理的 healthcheck。
Kubernetes:三类常见 503。 一是 liveness probe 配置过严,应用启动慢于探针窗口,Pod 被反复杀掉重建,Service 后端始终没有 Ready 端点——此时 Ingress 返回 503,修复方向是加 initialDelaySeconds 或改用 startup probe;二是 Pod 处于 OOMKilled 循环,kubectl describe pod 里能看到退出原因,需要调整内存 limit 或排查内存泄漏;三是 HPA 已扩到 maxReplicas 仍然不够,需要评估容量上限或优化单实例吞吐。
排查时一条很有用的命令:
kubectl get endpoints <service-name>
如果 endpoints 为空,说明没有 Ready 的 Pod,503 是必然结果——问题在 Pod 为什么不 Ready,而不是在 Ingress。
七、维护模式:正确地返回 503
503 不总是故障,维护窗口就应该用它。但要做得规范:
返回 503 的同时带上 Retry-After 头,告诉客户端和爬虫预计恢复时间;维护页本身要轻(静态 HTML,不要依赖数据库和应用,否则维护页自己也会挂);对搜索引擎而言,短时间的 503 不会被降权——爬虫会稍后重试,Retry-After 会被尊重,但如果维护持续数天,已收录页面可能被临时移出索引,所以长时间维护应使用 302 跳转到临时公告页而非长时间裸 503。
Nginx 下一个最小可用的维护模式配置:
server {
listen 80;
server_name www.mf8.biz;
# 运维白名单,按需放开
allow 203.0.113.0/24;
deny all;
error_page 503 /maintenance.html;
location = /maintenance.html {
root /usr/share/nginx/html;
internal;
}
location / {
return 503;
}
}
八、日志排查清单
把各层日志位置和关键词整理成一张速查表,503 排障时按层对照:
| 层 | 日志位置 | 关键词 |
|---|---|---|
| Nginx | error.log | no live upstreams、limiting requests、upstream temporarily disabled |
| PHP-FPM | FPM pool 日志 | reached pm.max_children、server busy |
| Node.js | 应用日志 / APM | event loop lag、ECONNREFUSED、health check timeout |
| systemd 服务 | journalctl -u <服务名> | Failed、Start-request repeated too quickly |
| Docker | docker logs <容器> | restart 循环、健康检查失败输出 |
| Kubernetes | kubectl describe / events | Unhealthy、OOMKilled、Readiness probe failed |
| Cloudflare | 控制台 Security/Analytics | 规则命中、52x 分布 |
一个实用的习惯:把 tail -f 各层日志的命令写进运维手册,事故时按顺序贴到多个终端窗口,时间戳一对照,请求在哪一层被拒一目了然。
九、恢复之后:验证与复盘
确认修复后,不要只看"页面能打开了":
用 curl -sI 确认状态码回到 200 且 Retry-After 已消失;观察监控上 5xx 率曲线归零,并确认没有从 503 变成 502/504(那是问题换了一层);如果是限流触发的 503,评估一下阈值是否合理——保护机制生效不算故障,但把它标成 429 才是正确姿势;如果是容量问题,把本次峰值流量、进程数、内存水位记入容量档案,作为下次扩容的依据。
复盘时回答三个问题:哪一层最先出问题?为什么监控没有在用户报障前告警(5xx 率告警阈值、健康检查覆盖)?同类问题能不能被自动化预案覆盖(自动摘除、自动扩容、维护页自动切换)?
十、预防:让 503 少发生的三件事
容量红线监控。 每个无状态服务都应有明确的容量指标——FPM 的 pm.max_children 使用率、Node 的事件循环延迟、连接池等待队列——在到达红线前告警,而不是等 503 出现才发现。
发布流量隔离。 滚动发布配合健康检查与连接排水,发布窗口的 503 尖峰应接近于零。发布脚本里加一步"发布后健康验证",失败自动回滚。
降级预案。 对非核心路径准备静态降级页(缓存命中优先,可结合 CDN 边缘缓存延长 TTL),核心依赖故障时宁可返回带 Retry-After 的缓存内容,也不把用户直接抛给 503。缓存层的治理思路可以进一步参考我们此前的 CDN 缓存命中率优化一文。
七、数据库与连接池:被忽视的 503 源头
应用进程还活着、健康检查也通过,但请求依然 503——很多时候是依赖的数据库连接池耗尽了。应用从池里拿不到连接,请求在队列里等待,等待超过应用设定的排队上限(或网关的超时)后,上游整体表现为不可用。
典型证据链:应用日志出现 connection pool exhausted、Timeout: Request timed out waiting for connection(不同语言框架措辞不同,关键词是 pool/exhausted/timeout 的组合);同时数据库侧的连接数逼近 max_connections。
处理顺序:先看有没有连接泄漏(借出连接未归还,常见于异常路径没有释放);再检查长事务(一个慢事务占住连接链式拖垮整个池);最后才是调大池上限——连接池上限和数据库的 max_connections、内存要联动计算,应用侧池子之和超过数据库上限,会把问题变成连接风暴。
八、传统运行时的线程池信号
PHP-FPM 和 Node 之外,两个常见运行时的对应信号也值得列入清单:
Java(Tomcat/Netty):Tomcat 的 maxThreads 打满后新请求进入 accept 队列,队列也满时拒绝连接。观察 server.tomcat.threads.busy 指标与线程 dump(jstack)里 BLOCKED 线程的占比;慢 SQL 和下游 HTTP 调用没有超时是最常见的两个根因。
Go:Go 的 net/http 没有传统线程池概念,但 goroutine 泄漏会让内存和调度延迟持续上涨,最终健康检查超时。pprof 的 goroutine profile 是定位泄漏点的标准工具,重点看数量随时间增长的同类调用栈。
这些运行时层的共同规律是:503 是症状,慢才是病。找到"什么变慢了",比调大任何池子都有效。
九、监控与告警:让 503 在用户报障之前被发现
等用户报障才知道 503,意味着监控缺了三块拼图:
分层探针。 对"客户端 → CDN → 网关 → 应用"的每一层各放一个探测点:公网拨测盯全链路,网关本机 curl 127.0.0.1 盯应用层。两条曲线一对比,503 出现在哪一层一目了然。
分级告警阈值。 5xx 率的告警建议分两级:超过 1% 持续 5 分钟告 warning,超过 5% 持续 2 分钟直接 page。阈值要结合基线校准——正常的 5xx 率不是零(爬虫扫漏洞也会产生 404/5xx),阈值定在基线之上而不是拍脑袋的零容忍。
变更时间线对齐。 告警消息里带上最近的发布与配置变更记录(或至少打通跳转),值班同学看到 5xx 告警的第一眼就能回答"是不是刚才那次发布",这是缩短 MTTR 最便宜的手段。
十、常见误区清单
最后把排障时反复出现的误区集中列出,对号入座可以少走弯路:
- 把 429 当 503 排:Nginx 限流默认返回 503,先确认是不是限流,再谈容量(第二节已给出 limit_req_status 429 的修正)。
- 只盯着应用代码:503 多数时候不是代码 bug。先分层定位,再决定要不要读代码。
- 盲目调大连接池/进程数:不找到"为什么慢",调大只是推迟崩溃,还可能把内存打爆,503 变 OOM。
- 健康检查路径用了重页面:探测路径本身拖垮应用,造成健康抖动,这是"明明没流量也 503"的经典原因。
- 忽略 Retry-After:无论故障还是维护,带上这个头都让客户端与爬虫的行为更可控,成本只是一行配置。
- 维护页依赖了正在维护的系统:维护页必须是完全静态的,不带数据库查询、不带外部字体和统计脚本。
官方资料与继续阅读
- RFC 9110 §8.6.3(503 语义与 Retry-After):https://www.rfc-editor.org/rfc/rfc9110#name-503-service-unavailable
- Nginx limit_req 模块文档(含 limit_req_status):https://nginx.org/en/docs/http/ngx_http_limit_req_module.html
- PHP-FPM 进程管理配置说明:https://www.php.net/manual/en/install.fpm.configuration.php
- Cloudflare 5xx 错误排查手册:https://developers.cloudflare.com/support/troubleshooting/http-status-codes/
- 站内相关文章:MDX 里的一行 HTML 属性导致文章返回 500:589 条 5xx 的排查记录、CDN 缓存命中率从 60% 到 95%:缓存规则、Cache Key 与回源治理、零停机 DNS 迁移实战


