关键词:nginx / 内置变量 / <math xmlns="http://www.w3.org/1998/Math/MathML"> r e m o t e a d d r / remote_addr / </math>remoteaddr/request_uri / 日志 / 调试 / 可观测性 / 运维排障
引言:当你只剩下一行 access.log 可以看
线上出问题的时候,nginx 往往是离用户最近、离应用最远的一层。
如果这时候:
- 应用日志还没写到;
- APM 没有覆盖到入口层;
- 监控图上只看到一条平躺的 5xx 曲线;
你最后能抓得住的,可能就只有 nginx 的访问日志和那一堆 $ 开头的内置变量。
很多人知道 $remote_addr、$request_uri 这些变量的存在,
但很少系统性地用它们来:
- 快速定位「是谁」发起的请求;
- 看清楚「请求到底长什么样」;
- 在日志里还原「请求在各种反代 / 跳转中经历了什么」。
这一篇,我们不再讲更多配置技巧,而是聚焦三件事:
- nginx 的核心内置变量到底有哪些值得记;
- 如何用这些变量把「请求上下文」写进日志;
- 在真实排障场景里,如何靠这些变量快速判断问题出在入口、网关还是应用。
一、nginx 内置变量:把「请求元信息」变成可用数据
nginx 内置了大量 $ 开头的变量,大多是从 HTTP 头、连接信息或内部状态里抽出来的。
你可以简单理解成:「一个请求从外面到里面,沿途所有关键节点的快照」。
1. 实战中最常用、值得背下来的那一批
以下这些基本属于「日常调试必备」级别(均已在实践中验证):
$remote_addr:客户端 IP 地址;$remote_port:客户端源端口;$http_host:请求头里的 Host(等价于request.getHeader('Host'));$http_origin:Origin请求头,跨域、前端来源排查时非常有用;$http_referer:Referer请求头,可以看请求从哪个页面发出;$request_uri:原始请求 URI,包含 query,但不含主机名,例如/foo/bar?a=1&b=2;$uri/$document_uri:当前内部处理用的 URI,不含 query;$args/$query_string:URL 查询参数;$server_name:命中的server_name;$server_addr/$server_port:当前处理请求的 nginx 地址和端口;$scheme:http或https。
几个容易混淆的点:
-
$request_urivs$uri:$request_uri= 用户原样请求路径 + 查询参数(除非被rewrite ... redirect/permanent改写);$uri/$document_uri= 当前请求在 nginx 内部的 URI,可能被内部rewrite改过。
-
$http_originvs$http_referer:$http_origin:调用方的域名 (如https://www.example.com),更适合做 CORS 判断;$http_referer:不含 hash 的完整 URL(如https://www.example.com/page?a=1),适合做行为分析。
一个简单的经验是:如果你在某个变量名前加上
$http_,大概率就是在拿对应的请求头,比如
$http_user_agent、$http_cookie等。
2. 来自网络但也值得顺手一记的一批
以下是常见文档里提到、但容易被忽略的一些变量(不一一定要背,知道有就行):
$content_length:请求头中的Content-Length;$content_type:请求头中的Content-Type;$http_user_agent:User-Agent;$http_cookie:Cookie;$limit_rate:可以用来限制某个连接的传输速率;$request_method:请求方法(GET / POST / PUT / DELETE ...);$request_filename:由root/alias+ URI 拼出来的实际文件路径;$server_protocol:HTTP/1.0或HTTP/1.1等。
这些变量的价值,在于你可以按需拉取它们,拼成一条足够信息密度的日志。
二、让 access.log 真正「说人话」:定制日志格式
默认的 access.log 格式通常长这样:
nginx
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
对排障来说,很多时候你需要的是:
- 调用来源(
Origin / Referer); - 请求最终命中的 URI;
- 被转发到哪台后端;
- 某些业务 Header(如 traceId、userId)。
你完全可以自定义一个更「工程化」的日志格式,例如:
nginx
log_format traceable_main
'$remote_addr:$remote_port '
'[$time_local] '
'"$request" '
'status=$status '
'bytes=$body_bytes_sent '
'host="$host" '
'origin="$http_origin" '
'referer="$http_referer" '
'ua="$http_user_agent" '
'req_uri="$request_uri" '
'uri="$uri" '
'upstream_addr="$upstream_addr" '
'trace_id="$http_x_trace_id"';
access_log logs/access.log traceable_main;
这样一条日志就能回答很多问题:
- 用户来自哪个 IP、哪个 Origin;
- 实际命中了哪个
uri(被 rewrite 过没有); - 请求被打到了哪个后端实例(
$upstream_addr); - traceId 是什么,方便跟应用日志串起来。
和其在监控平台上一脸懵逼,不如多给 access.log 加几个变量,
让每一条日志都变成「带上下文的证言」。
三、排障实战:变量 + 日志,快速缩小问题范围
说几个非常日常的排障问题,看看变量如何帮你「定点打击」。
场景一:用户说「偶发 502」,但后端压根没看到请求
现象:
- 客户端日志看到 502;
- 后端应用和网关没有对应 trace;
- 只有 nginx 层有 502。
可以在 access.log 里重点看:
$upstream_addr:有没有真正转发到后端?$upstream_status:后端返回的状态码(如果有);$request_time/$upstream_response_time:请求在哪一段耗时。
示例日志格式调整:
nginx
log_format with_upstream
'$remote_addr - [$time_local] '
'"$request" status=$status '
'req_time=$request_time '
'upstream="$upstream_addr" '
'upstream_status="$upstream_status" '
'upstream_time="$upstream_response_time"';
如果你看到:
status=502;upstream=""(空);- 或者
upstream指向某些特定 IP/端口,upstream_status是 502/504;
就可以很快判断:
- 要么是 upstream 配置不对 / 服务不通;
- 要么是某个后端实例挂了。
场景二:跨域问题,前端说「我没跨域」,后端说「我没看到请求」
这时候 $http_origin 和 $http_referer 就很有用。
你可以这样先加一条临时日志:
nginx
log_format cors_debug
'$remote_addr [$time_local] '
'method=$request_method '
'host="$host" '
'origin="$http_origin" '
'referer="$http_referer" '
'uri="$request_uri" '
'status=$status';
access_log logs/cors_debug.log cors_debug;
然后专门抓一段时间访问:
- 看看
$http_origin到底是什么(很多时候是写错了 schema / 子域名); - OPTIONS 请求到底有没有打到这台 nginx / 这个
location上; - 是 nginx 直接回了 4xx,还是 upstream 拒绝的。
场景三:怀疑某个用户脚本 / 爬虫把你打挂了
这时候组合的变量可能是:
$remote_addr+$remote_port;$http_user_agent;$http_referer;$request_uri。
你可以:
- 先用
grep把某个可疑 IP 或 UA 的访问拉出来; - 观察它的行为模式(URI、频率、是否遍历所有路径);
- 再考虑在 nginx 上做限流或封禁。
这里的关键始终是:在请求刚刚进入系统的时候,就捕获足够多的上下文信息。
四、用变量做一点「轻量逻辑」:按来源做差异化处理
虽然不推荐在 nginx 里写复杂业务逻辑,但基于变量做一点轻量分流还是很常见的。
1. 按域名 / Origin 做简单白名单
例如只允许某些域的前端调用某个接口:
nginx
location /api/internal {
if ($http_origin !~* ^https?://(www\.trusted\.com|admin\.trusted\.com)$) {
return 403;
}
proxy_pass http://127.0.0.1:9000;
}
这里的关键就是 $http_origin 和正则的一起使用。
2. 按 UA 做简单灰度 / 降级
在真·紧急情况(背锅现场)下,也有人会用 nginx 做一点 UA 级别的降级:
nginx
location /feature-heavy {
if ($http_user_agent ~* 'MSIE|Trident') {
# 老浏览器直接给降级页
return 200 '您的浏览器版本过低,建议使用现代浏览器访问本功能';
}
proxy_pass http://127.0.0.1:9001;
}
这类逻辑不建议长期存在,但在「一小时内必须止血」的场景下,变量 + 简单判断会非常好用。
五、从变量到可观测性:让 nginx 不再是「黑盒」
如果把三篇「nginx 配置长跑」放在一起看,会发现一个演进路径:
- 上篇是「路由规则」:请求到底会命中哪条
location; - 中篇是「流量入口」:域名、TLS、转发、跨域、泛域名这些入口层职责;
- 下篇就是「可观测性」:怎么知道这一层到底干了什么。
内置变量是 nginx 给你的「观察窗口」:
- 你可以用它们定制 access.log,让每条日志都带上关键上下文;
- 可以在关键路径上输出必要的 debug 信息,快速缩小问题范围;
- 可以用简单的条件判断,对不同来源、不同 UA 做轻量的路由差异化。
真正成熟的 nginx 使用方式,不是「背一堆指令」,
而是让它变成:
既能稳稳扛住流量,又能在出问题时,给你足够多的线索。
尾声:从会配 nginx,到敢把 nginx 当「工程设施」
回顾这三篇:
- 你应该已经能比较自信地回答:某个路径最终会命中哪条
location,会不会被正则截胡; - 你大概也能设计一套比较干净的入口层:域名归一、HTTPS、反向代理、CORS、泛域名;
- 最后,你有能力为 nginx 加上一层可观测性:变量、日志、简单的调试逻辑。
会写 nginx 配置,只是把系统「搭起来」;
敢把 nginx 当成工程设施来设计,才算是真正把它「用起来」。
当你下次看到一行
$http_origin或$request_uri时,也许会意识到:
它不是一个随手抄来的变量,而是你和复杂系统对话的一只探针。