502 Bad Gateway 的语义其实很窄:Nginx 收到了客户端请求,但从上游返回的并不是一个可用的 HTTP 响应。它既不代表应用一定挂了,也不意味着重启 Nginx 就能恢复。Nginx 错误日志会标明失败究竟发生在连接、发送请求、读取响应还是解析上游名称的哪个阶段;只有绕过 Nginx 直接访问上游,才能真正确认上游是否还在工作。
本文基于一个明确的生产栈展开:systemd 托管 Gunicorn,Gunicorn 通过 Unix socket 对外提供 Python WSGI 应用,Nginx 反向代理该 socket。第 5 个方向会单独讨论 TCP 主机名上游场景下的 DNS 处理;注意不要把 Unix socket 与 TCP 上游配置混在同一个 location 里。
文中出现的 <应用服务名>、<Nginx站点文件>、<应用运行目录>、<应用模块>、<业务域名>、<健康检查路径> 均为占位符。请分别替换为实际的 systemd unit、站点配置、socket 所在目录、Python WSGI 入口、域名和无鉴权健康检查路径。所有排查结论都必须有日志、socket 状态、直连探测、配置差异或系统资源数据来支撑。
先保护现场:不要先重启
502 突发时,直接重启 Nginx 或应用会丢掉最有价值的现场:错误时间、失败类型、连接状态和资源压力。先确定影响范围,再采集最近几分钟的证据。若正在发生大面积错误,应并行执行摘流、扩容、切换到健康副本等已批准的应急预案;本文中的命令用于定位单个节点或灰度节点,不替代流量调度。
# 代码 1:确认 Nginx 版本、主进程和正在使用的配置路径
nginx -v
nginx -V 2>&1
systemctl status nginx --no-pager
sudo nginx -T > "<证据目录>/nginx-effective.conf"
nginx -T 会展开 include 后的有效配置,输出可能包含内部域名、证书路径或鉴权相关配置,应保存到受控证据目录而非公开聊天。它并不代表配置语法正确;语法检查应使用 nginx -t。
# 代码 2:采集 502 时间窗口内的 Nginx 与应用日志
time_window="15 minutes ago"
sudo journalctl -u nginx --since "$time_window" --no-pager \
> "<证据目录>/nginx-journal.txt"
sudo journalctl -u "<应用服务名>" --since "$time_window" --no-pager \
> "<证据目录>/app-journal.txt"
sudo tail -n 500 /var/log/nginx/error.log \
> "<证据目录>/nginx-error-tail.txt"
日志中的时间必须与负载均衡、APM、业务告警使用同一时区或可换算为同一时间轴。不要因为日志出现 connection refused 就直接重启应用;它可能是 socket 路径改动、权限、服务发布窗口或系统资源耗尽引起的后果。
为了验证客户端看到的是哪一层的 502,先请求本机 Nginx。使用 Host 头可以避免同端口多虚拟主机时误命中默认 server。
# 代码 3:本机复现经过 Nginx 的请求,保留响应头与耗时
curl --silent --show-error --fail \
--resolve "<业务域名>:80:127.0.0.1" \
-H "Host: <业务域名>" \
-o /dev/null \
-D "<证据目录>/response-headers.txt" \
-w 'http_code=%{http_code} connect=%{time_connect} starttransfer=%{time_starttransfer} total=%{time_total}\n' \
"http://<业务域名><健康检查路径>"
curl 的非零退出说明该次探测失败,但不能自动说明根因。若健康检查本身依赖数据库或远端接口,需要另选一个只验证进程存活的端点,否则后续可能把下游故障误判为 Nginx 502。
方向一:上游进程未运行、监听位置不对或 socket 已失效
在 Unix socket 架构里,最常见的直接证据是 Nginx error.log 中的 connect() to unix:… failed,并附带错误码。不同错误码的含义不同:
2: No such file or directory:Nginx 配置路径与应用实际绑定路径不一致,或运行目录在服务启动后未创建。
13: Permission denied:Nginx worker 对 socket 或其父目录没有 traverse/read-write 所需权限。
111: Connection refused:socket 文件可能存在,但没有进程监听,或服务刚退出。
110: Connection timed out:更常见于 TCP 上游;对本地 Unix socket 应优先检查应用进程卡死、队列和资源。
先从错误日志提取上游失败行,再看 socket 和主进程,不要只查 systemctl 的“active”状态。
# 代码 4:提取能决定下一步的上游错误信息
sudo rg -n -i \
'connect\(\) to |upstream timed out|upstream prematurely closed|recv\(\) failed|no live upstreams' \
/var/log/nginx/error.log \
| tail -n 200
error.log 里的一行应与对应请求时间、URI、upstream 地址和 errno 一起分析。仅看到旧错误不能说明当前仍在失败;要和 access.log 的 502 比率以及最新请求时间对齐。
# 代码 5:检查应用 unit、主进程与 Unix socket 是否同时存在
SERVICE_NAME="<应用服务名>"
SOCKET_PATH="<应用运行目录>/gunicorn.sock"
systemctl is-active "$SERVICE_NAME"
systemctl show "$SERVICE_NAME" -p MainPID -p ExecMainStatus -p ActiveEnterTimestamp
ps -fp "$(systemctl show -p MainPID --value "$SERVICE_NAME")"
sudo ss -xlpn | rg -F "$SOCKET_PATH" || true
sudo stat "$SOCKET_PATH"
systemd 显示 active 只代表 unit 当前被认为已启动,不代表 Gunicorn worker 可用。对于 forking、notify 或 wrapper 脚本类型 unit,MainPID 可能不是实际 worker;应结合 ps、Gunicorn 日志和 socket 监听者确认。
直接绕过 Nginx 请求 socket 是分界动作:如果这里无法取得正常 HTTP 响应,问题在 Nginx 之前;如果这里正常而 Nginx 仍报 502,再重点看代理配置、权限和 Nginx worker 环境。
# 代码 6:通过 Unix socket 直接调用应用健康检查
curl --silent --show-error --fail \
--unix-socket "<应用运行目录>/gunicorn.sock" \
--max-time 5 \
-o /dev/null \
-w 'http_code=%{http_code} total=%{time_total}\n' \
"http://localhost<健康检查路径>"
这里的 localhost 仅用于构造 HTTP 请求行,连接仍通过 --unix-socket 指定的文件。执行前可用 curl --help all 确认当前 curl 支持该选项;若应用按 Host 路由,需要加上 -H "Host: <业务域名>"。此探测成功只能证明该 worker 当下能答复这个端点,不代表所有业务路由或下游依赖正常。
Gunicorn 的 unit 最好明确运行目录、用户、socket 位置和停止方式,避免由启动脚本在 /tmp 等不稳定目录临时创建 socket。下面是可审计的结构示例,<应用模块> 的形式通常为 包.模块:app,必须按项目入口替换。
# 代码 7:/etc/systemd/system/<应用服务名>.service 的关键部分
[Unit]
Description=<应用服务名>
After=network.target
[Service]
User=<应用运行用户>
Group=<应用运行组>
WorkingDirectory=<应用代码目录>
RuntimeDirectory=<应用运行目录名>
RuntimeDirectoryMode=0755
ExecStart=<gunicorn可执行文件> --workers <worker数> --bind unix:/run/<应用运行目录名>/gunicorn.sock --access-logfile - --error-logfile - <应用模块>
Restart=on-failure
RestartSec=3
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
修改 unit 或 Restart 策略属于高风险变更:会改变应用的启动、停止和故障恢复行为。执行前备份现有 unit,先在灰度副本做 daemon-reload 和受控重启,确认健康检查、请求成功率、worker 数和 socket 所有者都正常,再逐台滚动。
# 代码 8:发布或故障后的安全重启前检查
set -euo pipefail
SERVICE_NAME="<应用服务名>"
systemctl cat "$SERVICE_NAME" > "<证据目录>/app-unit-before-action.txt"
systemctl show "$SERVICE_NAME" -p CanReload -p Restart -p TimeoutStopUSec
systemctl status "$SERVICE_NAME" --no-pager
sudo nginx -t
# 仅在已摘流或确认有健康副本的单台节点执行:
# sudo systemctl restart "$SERVICE_NAME"
示例明确把 restart 注释掉,因为应用重启可能中断正在处理的请求。若 unit 定义了正确的 ExecReload,优先评估 reload 是否真的支持平滑 worker 替换;不能因为命令存在就假设应用实现了无损 reload。
方向二:Nginx 配置指向了错误的上游或被发布覆盖
许多 502 发生在发布后:应用新版本把 socket 路径改为 /run/app-v2/gunicorn.sock,而 Nginx 仍指向旧路径;或者站点文件有多个 include,后加载的 location 覆盖了预期配置。先找出真正命中的 server 与 location,再谈修改。
# 代码 9:从有效配置中定位 proxy_pass、include 与 server_name
sudo nginx -T 2>&1 \
| rg -n -C 4 'server_name|listen |location |proxy_pass|upstream |include ' \
> "<证据目录>/nginx-routing-extract.txt"
如果同一个域名出现多个 server block,应该继续检查 listen 地址、default_server、TLS SNI 和 include 顺序。仅编辑一个看上去相关的文件,可能完全没有改变运行中的配置。
对于 Unix socket,proxy_pass 的 URI 形式有严格语法。下面配置把请求保留为原始 URI 并传给 socket。它适用于现有应用确实通过该 socket 监听的场景,不应与 TCP upstream 配置叠加。
# 代码 10:Nginx 到 Gunicorn Unix socket 的 location 示例
location / {
proxy_pass http://unix:/run/<应用运行目录名>/gunicorn.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
超时值必须来自业务 SLA 和上游处理时间。把 proxy_read_timeout 无限调大只会让 Nginx worker 长时间占用连接,不能修复慢查询或死锁。修改前备份站点文件;测试通过后先 reload 单台 Nginx,并用真实 Host 的健康检查验证。
# 代码 11:配置修改的备份、语法检查、灰度加载和验证
set -euo pipefail
site_file="<Nginx站点文件>"
backup_dir="<配置备份目录>/nginx-$(date +%Y%m%d-%H%M%S)"
sudo mkdir -p "$backup_dir"
sudo cp -a "$site_file" "$backup_dir/"
sudo install -m 0644 "<已审核站点文件>" "$site_file"
sudo nginx -t
sudo systemctl reload nginx
curl --silent --show-error --fail \
--resolve "<业务域名>:80:127.0.0.1" \
-H "Host: <业务域名>" \
"http://<业务域名><健康检查路径>" > /dev/null
reload 前的 nginx -t 是必要条件,不是充分条件:它只检查语法与部分配置合法性,不会验证 socket 是否存在、权限是否正确或上游逻辑能响应。reload 失败时不要覆盖错误日志;读取 journalctl -u nginx 和 error.log 获取失败原因。
如果变更后错误率升高,应立即恢复精确备份文件并再次语法检查。不要用“手工改回记忆中的版本”替代备份,因为 include、证书、限流和安全头等无关配置容易被遗漏。
# 代码 12:回滚 Nginx 站点文件并复核
set -euo pipefail
site_file="<Nginx站点文件>"
backup_file="<配置备份目录>/nginx-<时间戳>/<站点文件名>"
sudo install -m 0644 "$backup_file" "$site_file"
sudo nginx -t
sudo systemctl reload nginx
sudo nginx -T > "<证据目录>/nginx-effective-after-rollback.conf"
这里的 <站点文件名> 是备份目录中实际保存的文件基名,不能照抄尖括号。回滚后仍要用健康检查与 5xx 比率验证;若 502 没消失,则本次配置不是根因,避免反复修改扩大影响。
方向三:socket 或目录权限阻断了 Nginx worker
Unix socket 连接不只检查 socket 本身的 mode。Nginx worker 运行用户还必须对每一个父目录有 x(traverse)权限。常见现象是 socket 文件存在、应用 curl 成功,但 Nginx error.log 报 Permission denied。
# 代码 13:确认 Nginx worker 用户与 socket 的属主、权限
ps -o user=,group=,pid=,cmd= -C nginx
grep -R --line-number --fixed-strings 'user ' /etc/nginx/nginx.conf /etc/nginx/conf.d 2>/dev/null || true
sudo stat -c '%A %a %U:%G %n' "<应用运行目录>" "<应用运行目录>/gunicorn.sock"
master 进程常以 root 启动,真正访问 socket 的是 worker 用户。不要仅凭 root 能 curl socket 就认为权限正确。若发行版启用了 SELinux 或 AppArmor,还要查看其拒绝日志;修改 Unix mode 无法绕过 MAC 策略。
# 代码 14:逐级检查 socket 路径的目录权限
sudo namei -l "<应用运行目录>/gunicorn.sock"
getfacl -p "<应用运行目录>" "<应用运行目录>/gunicorn.sock" 2>/dev/null || true
namei -l 能显示路径上每一级的权限和属主,是定位“目录缺少 x 权限”的直接证据。getfacl 不存在时不代表没有 ACL,只是工具未安装;不要为了排障随意安装包或修改 ACL,先按现网基线处理。
如果确认只有 Nginx worker 缺少访问权限,最小修改通常是让 socket group 与 Nginx worker 的有效组对齐,并确保运行目录可穿越。以下是应用 service 的一种做法;不要对 /run 递归 chmod,也不要把 socket 设成 666。
# 代码 15:让 Gunicorn socket 以受控用户组创建的 unit 结构
[Service]
User=<应用运行用户>
Group=<Nginx可访问的组>
UMask=0007
RuntimeDirectory=<应用运行目录名>
RuntimeDirectoryMode=0750
ExecStart=<gunicorn可执行文件> --bind unix:/run/<应用运行目录名>/gunicorn.sock <应用模块>
UMask=0007 会影响该进程新建文件的默认权限,因此应先确认日志、上传目录和临时文件没有因这一变化变得不可读。更稳妥的做法是在预发布节点检查实际 socket 权限和 Nginx 访问结果,再推广。
SELinux 已启用时,优先读取拒绝记录,而不是关闭 SELinux。不同发行版策略包和自定义策略差异很大,命令输出应由安全团队或基线负责人复核。
# 代码 16:仅在系统启用 SELinux 时收集拒绝证据
getenforce 2>/dev/null || true
sudo ausearch -m AVC -ts recent 2>/dev/null | tail -n 100 || true
sudo journalctl -t setroubleshoot --since "-30 minutes" --no-pager 2>/dev/null || true
若证据显示被 SELinux 拒绝,应使用最小化、可审计的文件上下文或策略修复,并先在灰度验证。把 SELinux 设为 permissive 或 disabled 会扩大主机安全面,不能作为常规 502 修复手段。
方向四:上游应用活着,但请求处理超时、崩溃或提前关闭
Nginx 能连接 socket 仍可能返回 502。例如应用 worker 在响应头发出前崩溃,Nginx 会记录 upstream prematurely closed connection;worker 被 OOM killer 杀掉、Python 异常未处理、Gunicorn worker timeout 或下游依赖触发进程退出,也可能呈现同类现象。
# 代码 17:对齐 Nginx 502、应用异常和内核 OOM 的时间线
sudo journalctl -u "<应用服务名>" --since "-30 minutes" --no-pager \
| rg -n -i 'error|exception|traceback|worker|timeout|killed|exit' || true
sudo journalctl -k --since "-30 minutes" --no-pager \
| rg -n -i 'out of memory|oom-killer|killed process' || true
sudo awk '$9 ~ /^502$/ {print}' /var/log/nginx/access.log | tail -n 200
access.log 的字段顺序取决于 log_format;上例仅在默认 combined 格式且状态是第九列时适用。生产中应先确认实际 log_format,最好记录 request_id、upstream_status、upstream_response_time 和 request_time,才能将同一请求串起来。
# 代码 18:用于关联 502 的 access log 格式示例
log_format upstream_timing
'$remote_addr $host "$request" status=$status '
'request_time=$request_time upstream_addr=$upstream_addr '
'upstream_status=$upstream_status upstream_time=$upstream_response_time '
'request_id=$request_id';
access_log /var/log/nginx/access.log upstream_timing;
改 log_format 会改变日志解析、告警和存储量。先检查日志采集规则是否依赖固定字段,再在一个节点 reload 验证。不要在高峰事故中临时切换大量节点的日志格式,以免新增观测问题。
对单个可复现 URI,分别走 Nginx 与 Unix socket 请求,可判断失败在代理前还是代理后。直接 socket 探测需要同样的方法、Host、鉴权头和超时,否则比较没有意义。
# 代码 19:比较经 Nginx 与直连 socket 的同一请求
request_path="<健康检查路径>"
curl --silent --show-error --fail \
--resolve "<业务域名>:80:127.0.0.1" \
-H "Host: <业务域名>" \
-o /dev/null -w 'nginx code=%{http_code} total=%{time_total}\n' \
"http://<业务域名>$request_path"
curl --silent --show-error --fail \
--unix-socket "<应用运行目录>/gunicorn.sock" \
-H "Host: <业务域名>" \
-o /dev/null -w 'socket code=%{http_code} total=%{time_total}\n' \
"http://localhost$request_path"
如果直连 socket 同样超时或报 5xx,Nginx 不应背锅,应继续从应用 trace、数据库连接、下游调用和 worker 数量取证。若直连成功而 Nginx 不成功,返回方向二和方向三核对有效配置、socket 路径和 worker 权限。
Gunicorn 的 timeout 是 worker 长时间不响应时的保护阈值,不应按“请求偶尔慢”随意拉高。先找慢路径,再评估请求上限、异步任务拆分、下游超时与 worker 容量。以下命令只查看当前启动参数与进程树。
# 代码 20:核查 Gunicorn worker 数、父子关系与实际启动参数
SERVICE_NAME="<应用服务名>"
MAIN_PID="$(systemctl show -p MainPID --value "$SERVICE_NAME")"
ps -o pid,ppid,user,etime,stat,cmd --forest -p "$MAIN_PID" --ppid "$MAIN_PID"
tr '\0' ' ' < "/proc/$MAIN_PID/cmdline"
printf '\n'
systemctl show "$SERVICE_NAME" -p TasksCurrent -p MemoryCurrent -p LimitNOFILE
Gunicorn master 与 worker 的进程关系依具体启动方式而定,若 wrapper 改写了 MainPID,上述父子列表可能不完整。观察到 worker 数不足时,先查为什么 worker 退出或不能拉起,不要在内存已紧张时盲目扩大 worker 数。
方向五:只有 TCP 主机名上游时,检查 DNS、地址与连接路径
本节只适用于 Nginx 当前配置使用 proxy_pass http://<上游域名> 或 upstream server <上游域名>:<端口> 的情况。Unix socket 方案没有 DNS 这一跳。常见迁移问题是:应用服务名、容器 DNS 名或后端地址变了,但 Nginx 仍缓存旧地址,或者 resolver 配置不存在导致变量形式的 proxy_pass 不能解析。
# 代码 21:确认有效配置到底使用 socket、IP 还是主机名上游
sudo nginx -T 2>&1 \
| rg -n -C 3 'proxy_pass|upstream |resolver |server unix:|server [A-Za-z0-9._-]+:[0-9]+' \
> "<证据目录>/upstream-resolution-extract.txt"
getent ahostsv4 "<上游主机名>"
getent ahostsv6 "<上游主机名>" || true
getent 查询的是主机当前 NSS 解析结果;它不等于 Nginx worker 一定使用相同 resolver。Nginx 对静态配置中的主机名通常在启动或 reload 时解析,而变量形式的 proxy_pass 需要显式 resolver。必须根据有效配置判断,不要只因 getent 成功就排除 DNS 问题。
# 代码 22:仅用于变量形式 TCP 上游的 resolver 示例
resolver <DNS服务器IP> valid=30s ipv6=off;
resolver_timeout 5s;
set $backend "http://<上游主机名>:<上游端口>";
location / {
proxy_pass $backend;
proxy_connect_timeout 3s;
proxy_read_timeout 60s;
}
<DNS服务器IP> 必须是 Nginx worker 可访问、受运维管理的 DNS 地址;不要从网上随意选择公共 DNS。关闭 ipv6 仅在上游没有可用 IPv6、且故障证据显示 AAAA 解析或 IPv6 路径有问题时使用。修改 resolver 影响该 server 中的动态上游解析,应先灰度。
即使 DNS 正常,TCP 上游也可能未监听、被防火墙拒绝或连接池耗尽。以下探测从 Nginx 节点发起;执行前确认网络策略允许,不要把探测端口误用为写入型管理端口。
# 代码 23:验证 TCP 上游地址、端口与 HTTP 健康检查
upstream_host="<上游主机名>"
upstream_port="<上游端口>"
getent ahostsv4 "$upstream_host"
nc -vz -w 3 "$upstream_host" "$upstream_port"
curl --silent --show-error --fail \
--connect-timeout 3 \
--max-time 5 \
"http://$upstream_host:$upstream_port<健康检查路径>" > /dev/null
nc 与 curl 成功仅证明本次从当前节点到一个解析结果可用。多 A 记录、负载均衡、IPv6、网络分区和应用 Host 路由仍需逐一验证。若配置里是服务发现名称,应从服务发现系统与运行时网络继续取证,不要把临时 /etc/hosts 修改当作正式修复。
方向六:主机资源耗尽与级联故障
当 CPU 饱和、内存接近 cgroup 限制、FD 用尽、磁盘满、内核连接表满或单机连接数暴涨时,应用虽未永久退出,也可能无法及时 accept、fork 或返回响应。此类 502 往往有波峰,并伴随重启后短暂恢复;重启只是释放资源,不能解释资源为何耗尽。
# 代码 24:一次性检查 CPU、内存、磁盘、FD、连接和 OOM 证据
SERVICE_NAME="<应用服务名>"
MAIN_PID="$(systemctl show -p MainPID --value "$SERVICE_NAME")"
uptime
free -h
df -hT
df -ih
grep -E 'Max open files' "/proc/$MAIN_PID/limits"
ls "/proc/$MAIN_PID/fd" | wc -l
ss -s
cat /proc/sys/fs/file-nr
sudo journalctl -k --since "-1 hour" --no-pager \
| rg -i 'oom|out of memory|killed process|nf_conntrack.*full' || true
资源检查的结论必须有趋势支持。例如 df -h 显示某个文件系统满、应用日志同时显示无法写日志或临时文件失败,才能认定磁盘是根因;单看较高 CPU 不能区分正常高吞吐与热点、锁竞争或软中断异常。
若采用 systemd 的内存限制,检查 unit 当前限制与实际使用量。内存限制变更会影响 OOM 风险,必须先在灰度节点验证并保留原 unit。不要通过关闭限制来掩盖泄漏。
# 代码 25:检查应用 cgroup 内存、任务数和 OOM 相关字段
SERVICE_NAME="<应用服务名>"
systemctl show "$SERVICE_NAME" \
-p MemoryCurrent -p MemoryMax -p MemoryHigh \
-p TasksCurrent -p TasksMax \
-p LimitNOFILE
MAIN_PID="$(systemctl show -p MainPID --value "$SERVICE_NAME")"
cat "/proc/$MAIN_PID/cgroup"
MemoryCurrent 为 unavailable 或空时,可能是 cgroup 版本、权限或 unit 类型导致,不应把空值解释为无限内存。应结合容器运行时或 cgroup 路径下的 memory.events、memory.current 等实际文件继续取证。
一条可执行的 502 处置顺序
事故中最重要的是缩短错误分支,而不是收集更多无关命令。下面脚本只做只读取证与本机探测,不会 reload 或 restart 服务;它适合在单个问题节点生成第一份证据包。目录权限和磁盘空间要预先满足要求。
#!/usr/bin/env bash
# 代码 26:Nginx 502 首轮只读取证脚本
set -euo pipefail
SERVICE_NAME="<应用服务名>"
SOCKET_PATH="<应用运行目录>/gunicorn.sock"
evidence_dir="<证据目录>/nginx-502-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$evidence_dir"
systemctl status nginx --no-pager > "$evidence_dir/nginx-status.txt" || true
systemctl status "$SERVICE_NAME" --no-pager > "$evidence_dir/app-status.txt" || true
sudo tail -n 500 /var/log/nginx/error.log > "$evidence_dir/nginx-error.log" || true
sudo journalctl -u "$SERVICE_NAME" --since "-30 minutes" --no-pager > "$evidence_dir/app-journal.txt" || true
sudo ss -xlpn > "$evidence_dir/unix-sockets.txt" || true
sudo stat "$SOCKET_PATH" > "$evidence_dir/socket-stat.txt" || true
free -h > "$evidence_dir/memory.txt"
df -hT > "$evidence_dir/disk.txt"
printf '%s\n' "$evidence_dir"
脚本将失败命令容忍为 true,是为了让缺少 socket 或服务已退出时仍能留下其余证据;这不是把错误忽略掉。分析时应逐项查看命令是否失败,并把“socket 不存在”“服务 inactive”“error.log 的 errno”和“直连 socket 结果”放进同一结论中。
排查完成后的验收也应分层:先确认 Nginx 到 socket 的健康检查成功,再确认灰度流量不再出现 upstream 失败,最后确认错误率、P95/P99、应用 worker 数和系统资源回到预期范围。若必须恢复配置,使用方向二的精确备份文件回滚并先 nginx -t;若必须重启应用,先摘流、确认健康副本、逐台操作并观察回补流量。这样处理的 502 才能留下可复现的根因,而不是一次“重启后好了”的偶然恢复。