页面能打开、接口报错,说明静态资源服务和反向代理这一段大体是通的,问题通常收窄在三处:服务端进程是否正常、配置有没有注入到运行环境、日志能不能看见失败现场。先看哪一个不取决于习惯,取决于接口失败长什么样:请求直接超时或连接被拒,说明它根本没进到应用,先查进程、监听端口和反向代理上游,环境变量放在第二位;返回的是 4xx/5xx 错误体甚至一段 JSON,说明请求已进应用,优先把服务端日志级别调起来复现一次,再核对变量名。环境变量更常见,日志更能定性;两步都要做,只是先后不同。
接口超时或连接被拒,先查服务进程、监听端口和反向代理上游,再核对变量;接口返回 500/401 等错误体,先把服务端日志调到 debug 复现一次请求,再比对变量名是否齐全。两者互不替代:变量只能证明配置在不在,日志才能说明业务为什么抛错。托管平台无法登录宿主机时,改用平台日志面板和平台自带的变量管理。
先确认接口失败属于超时、连接被拒还是返回错误体
这一步的目的是分层:网络层的问题去查端口、进程、安全组和反向代理;应用层的问题去查配置和代码。用浏览器开发者工具的 Network 面板点开失败请求,看 Status 是 (failed) 还是具体状态码,看 Timing 里是卡在 Stalled/Waiting 还是很快返回;再在部署机器或能访问该域名的机器上执行一次命令行请求,两种方式互为补充,浏览器受缓存和跨域预检影响,命令行更接近真实调用。
curl -i -m 10 -H 'Content-Type: application/json' \
-d '{"ping":1}' https://your-domain/api/<最小接口>
# 需要单独看连接过程时加 -v
curl -v -m 10 https://your-domain/api/<最小接口>
需要记录下来的信息:HTTP 状态码、Content-Type、响应体前几百个字符、总耗时,以及 curl 的退出码与错误行。三类结果对应的下一步不同:
- 超时(curl 报 operation timed out 或 28):请求没拿到响应,先看服务是否在跑、反向代理 upstream 指向的端口对不对,环境变量暂缓。
- 连接被拒(Connection refused,退出码 7):目标端口没有进程监听,先确认进程启动失败还是监听地址是 127.0.0.1 而非 0.0.0.0。
- 返回 4xx/5xx 且带响应体:请求已进应用,直接进入日志与环境变量核对,优先把日志级别调高。
检查部署环境里的变量是否真的注入
先只确认变量名,不打印值,避免把密钥写进截图或工单。在容器内或服务进程所属用户下执行:
# 只列变量名
printenv | cut -d= -f1 | sort | grep -i '^MKSAAS'
# 或按需确认某个名字是否存在
printenv | cut -d= -f1 | grep -x 'MKSAAS_DB_URL' && echo present
# systemd 部署时确认实际加载的环境文件
systemctl show your-service -p EnvironmentFiles
命名上通常要求全大写、单词间用下划线,不要带点号或连字符,很多框架不会把 mksaas.db.url 这类名字映射到配置项;如果项目用前缀约定(如前端只暴露带特定前缀的变量),名字必须带前缀。还要分清读取时机:前端构建期注入的变量在打包时就固化了,容器里改了环境变量但没重新构建不会生效;服务端运行时读取的变量才随进程启动生效。常见漏注入原因是 .env 文件没有随镜像带进去、compose 里写了 env_file 但文件路径相对目录不对、平台变量只加到了某个环境而没加到当前部署。确认变量存在后再看值是否为空字符串,空值和使用默认值在行为上往往一样,容易被误判为已配置。
把服务端日志级别调到能看见请求
接口返回错误体但日志里什么都没有,多数是级别问题而不是没出问题。日志级别通常配在应用配置文件、启动参数或环境变量里,优先级需要按项目实际确认,改动后重启进程再看。输出位置也要确认:容器化部署一般写 stdout,用 docker logs `--tail` 200 <容器名> 或编排工具的日志命令查看;写文件的部署要找到配置里的日志路径,并确认当前用户对该文件有写权限。
一次失败的接口请求,日志里建议能看到:时间戳、请求方法、路径、状态码、耗时、请求标识(如 request id)、上游或下游地址,以及错误堆栈。如果只有一条访问记录没有任何错误信息,说明异常被吞掉了;如果连访问记录都没有,问题在第 1 步的网络层或反向代理,不在业务代码。
用一条最小接口排除业务代码干扰
加一个不读数据库、不调外部服务、不依赖业务配置的健康检查接口,用来判断失败是全局中间件问题还是单条接口问题。下面是通用骨架,按所用框架替换写法即可:
GET /healthz -> 200 {"status":"ok"}
// 通用骨架,按框架调整
router.get('/healthz', (req, res) => {
res.status(200).json({ status: 'ok' });
});
预期响应是 200 加固定结构的 JSON。如果 /healthz 正常而业务接口失败,问题在业务处理或它读的那份配置;如果 /healthz 也失败,问题在路由挂载、前缀、中间件(鉴权、CORS、请求体解析)或反向代理转发规则。对比两者的日志差异同样有价值:健康检查应出现一条干净的访问记录,业务接口若连这条记录都没有,说明请求没到路由。
按请求到达、配置读取、业务处理三段对照本地与部署日志
把本地和部署两边的日志按同一段时间、同一条请求并排看,差异点就是部署环境独有的断点。
| 阶段 | 对比字段 | 差异类型 | 排查方向 |
|---|---|---|---|
| 请求到达 | 时间戳、方法、路径、状态码、反向代理 upstream 日志 | 本地有部署无 | 反向代理规则、路径前缀、端口映射、健康检查拦截 |
| 配置读取 | 读取到的变量名、是否为空、读取发生的阶段 | 部署读到空值或默认值 | 变量未注入、命名不匹配、构建期与运行时混淆 |
| 业务处理 | 错误类型、堆栈首行、下游地址与超时 | 本地无部署有 | 网络出站限制、数据库连接串、依赖服务地址写成内网或 localhost |
对照时注意时间是否对齐时区,容器常是 UTC 而本地是东八区,容易把两条不相干的记录当成同一次请求。差异落在“请求到达”段,回到第 1 步查网络与代理;落在“配置读取”段,回到第 2 步只核对名字与读取时机;落在“业务处理”段,日志里的堆栈首行通常已给出方向,再结合部署环境与本地在地址、权限、依赖版本上的差别逐项排除。变量和日志不是二选一,变量回答“配没配上”,日志回答“为什么出错”,先做哪一步由接口失败的形态决定。