502 和 504 都由网关或反向代理返回,但含义不同:502 表示代理没有从上游得到可用响应,504 表示代理等待上游响应超过时限。排查时先找到产生状态码的那一层,再从该层所在的网络访问上游。

客户端经过 CDN 和反向代理访问应用时,连接失败产生 502、等待超时产生 504 的故障图
连接拒绝、解析失败和无效响应通常指向 502;上游等待超过代理期限通常指向 504。

确认哪一层返回 502 或 504

浏览器前面可能同时有 CDN、云负载均衡、Caddy 或 Nginx。先保留状态码、响应头、时间和请求 ID:

curl -sS -o /dev/null -D - \
  -w 'remote=%{remote_ip} code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  https://example.com/path

ServerVia、CDN 特征头和自定义 request ID 可以缩小范围,但不能单独证明根因。把同一时间的 CDN、入口和应用日志对齐;如果 CDN 日志显示源站 502,而源站入口没有这次请求,还要检查两者之间的地址、端口和 TLS。

RFC 9110(在新标签页打开)对两种状态的定义是:

  • 502 Bad Gateway:作为网关或代理访问上游时,收到无效响应;
  • 504 Gateway Timeout:作为网关或代理访问上游时,没有在规定时间内收到响应。

具体软件会使用不同日志描述连接拒绝、连接重置、握手失败和响应解析失败。状态码只能指出大致范围,根因仍要从对应代理的日志中确认。

502 先查上游能不能被代理访问

docker compose ps
docker compose logs --since 10m proxy app
docker compose exec proxy getent hosts app
docker compose exec proxy curl -sv http://app:8080/healthz

在代理容器中执行最后一条命令,可以同时验证服务名解析、容器网络、端口、协议和应用响应。宿主机执行 curl localhost:8080 成功,只能证明宿主机到已发布端口可用。

Compose 默认网络中,应使用服务名和容器端口:

example.com {
    reverse_proxy app:8080
}
location / {
    proxy_pass http://app:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Docker Compose 网络文档(在新标签页打开)说明服务重建后 IP 可能变化,服务名保持不变。把容器 IP 写进代理配置,会在一次正常重建后制造 502。

按日志文本定位 502

代理日志常见原因检查
connection refused应用未监听、端口写错、启动未完成ss -lntp、容器日志、healthcheck
host not found / no such host服务名错误、网络不同、DNS 失败getent hosts appdocker inspect 网络
connection reset by peer应用崩溃、主动断连、协议不一致应用错误日志、OOM、HTTP/HTTPS 配置
TLS handshake failed上游协议、SNI、证书信任错误curl -vk 只用于定位,再修复信任链
invalid header / upstream sent invalid response上游并非 HTTP 或响应格式无效直连端口、协议和应用日志

应用必须监听容器网络可达的地址,常见配置是 0.0.0.0:8080。监听 127.0.0.1:8080 只允许应用容器自身连接;限制公网暴露应通过宿主机端口映射和防火墙完成,而不是把容器内服务绑到回环地址。

504 先找上游在等什么

上游可连接但长时间不返回时,代理最终产生 504。从代理容器测量上游的 TTFB 和总耗时:

docker compose exec proxy curl -sS \
  -o /dev/null \
  -w 'code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  http://app:8080/slow-path

docker stats --no-stream

把请求时间与应用日志、数据库慢查询、连接池等待和外部 API 调用对齐,优先检查:

  • SQL 查询、锁等待或连接池耗尽;
  • 外部 API 没有合理超时,重试叠加;
  • CPU、内存或磁盘持续饱和;
  • 大文件由应用同步生成或转发;
  • 请求已经被客户端取消,上游仍继续计算;
  • 多层代理的超时顺序相反,外层先于内层断开。
客户端、CDN、反向代理、应用、数据库和外部 API 的超时预算从外到内递减的示意图
内层应在外层之前失败并返回可解释错误;外层先超时只会留下 504。

超时要按整条调用链设计

假设外层允许等待 30 秒,应用内部不能给数据库 30 秒、外部 API 再给 30 秒。内层超时、重试和清理需要在外层期限前完成,并给错误响应留出时间。

Nginx proxy_read_timeout(在新标签页打开)限制的是两次读取操作之间允许的时间,不等于整个响应必须在该秒数内完成。Caddy reverse_proxy(在新标签页打开)也分别提供拨号、响应头、读取和写入等传输超时。修改前先确认实际卡在哪个阶段。

仅当业务本来就是长操作时,才考虑:

  • 改成异步任务,立即返回任务 ID;
  • 提供进度、取消和幂等查询;
  • 同时调整网关、应用和依赖的超时预算;
  • 限制并发,避免长请求拖垮普通请求。

直接把代理超时从 30 秒改到 10 分钟,会增加连接占用,也可能把数据库或第三方故障隐藏得更久。

healthcheck 不能只检查进程存在

健康检查至少验证应用能够接收请求;涉及数据库的 readiness 可以确认关键依赖,但不要把昂贵查询放进高频探针。Compose 可等待依赖进入 healthy 后再启动消费者:

services:
  app:
    image: example/app:1.4.2
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 6

容器显示 running 只代表主进程未退出,不能证明端口已监听、路由可用或依赖已就绪。

恢复后分别检查应用、代理和公网入口

docker compose exec app wget -qO- http://127.0.0.1:8080/healthz
docker compose exec proxy curl -fsS http://app:8080/healthz
curl -fsS https://example.com/healthz

再确认代理错误日志停止增长、应用错误率恢复、请求耗时回到基线,并执行一次应用重建。重建后仍通过服务名访问,才说明没有依赖旧容器 IP 或偶然网络状态。

端口到底应绑定到回环、宿主公网还是只留在 Compose 网络中,见Docker 端口、回环绑定与防火墙

常见问题

502 和 504 的区别是什么?

502 表示代理没有得到可用上游响应;504 表示等待上游超过时限。以产生状态码的代理日志和同网络直连结果确认具体原因。

出现 504 可以直接把代理超时调大吗?

不应先调大。先查应用、数据库、外部 API 和资源争抢;长任务优先改为异步执行。

宿主机 curl 上游正常,为什么代理仍然 502?

代理容器使用不同网络命名空间。必须从代理容器请求服务名和容器端口,宿主机 localhost 不能替代这个验证。

Docker Compose 中反向代理应该连接容器 IP 吗?

不应该。使用服务名和容器端口,并让代理与应用加入同一网络。容器 IP 在重建后可能变化。

怎样确认 502 或 504 已经恢复?

从应用容器、代理容器和公网三层检查同一健康地址,并确认重建后、真实业务请求和日志指标都恢复。