Linux 基础体系 · 第 71/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux Nginx 反向代理:进程模型、TLS、负载均衡、限流和日志
Nginx 是一个位于客户端与后端服务之间的网络代理。客户端把请求发送给 Nginx,Nginx 再根据配置决定是否终止 TLS、选择哪个后端、是否限流、如何转发请求以及记录哪些日志。这个过程看似只是“监听端口并转发”,实际上涉及 Linux socket、事件循环、TLS 状态机、HTTP 语义、共享内存和故障重试。
本文示例面向现代主流 Linux 发行版,使用发行版包管理器安装的 Nginx。不同发行版可能采用不同的配置目录、服务用户和 systemd 单元名称;修改生产配置前应确认 nginx -V、nginx -t、systemd 状态以及当前流量入口。
一、先建立完整的数据流模型
一个典型的 HTTPS 反向代理链路如下:
flowchart LR
C[客户端] -->|TCP 连接 443| N[Nginx worker]
N -->|TLS 握手与证书| C
C -->|HTTP 请求| N
N -->|限流与访问控制| L[请求处理阶段]
L -->|选择后端| B1[Backend A]
L -->|选择后端| B2[Backend B]
N -->|记录 access/error 日志| LOG[日志文件或 journald]
B1 --> N
B2 --> N
N --> C
一次请求通常经历以下状态:
- Linux 内核在监听 socket 上接收新连接。
- Nginx worker 接管连接,并通过事件通知机制等待读写事件。
- 如果监听的是 HTTPS 端口,Nginx 先进行 TLS 握手。
- TLS 握手中的 SNI 可用于选择虚拟主机和证书。
- Nginx 读取 HTTP 请求头,匹配
server和location。 - 请求经过访问控制、限流、缓存等阶段。
- 如果是反向代理请求,Nginx 选择一个 upstream 后端。
- Nginx 与后端建立连接,转发请求,并读取响应。
- 响应返回客户端,同时写入访问日志;异常写入错误日志。
“反向代理”与“正向代理”的区别在于代理服务的代表对象不同:
- 正向代理代表客户端访问外部服务,后端服务通常不知道真实客户端。
- 反向代理代表服务端接收客户端请求,客户端通常只看到 Nginx 的地址。
Nginx 的 proxy_pass 不会自动解决所有协议和身份问题。它只决定请求如何转发;客户端到 Nginx 的 TLS、Nginx 到后端的 TLS、客户端真实地址传递、后端健康状态,都需要分别配置。
二、Linux socket 与 Nginx 进程模型
2.1 监听 socket 的基本状态
Nginx 监听 TCP 端口时,内核中至少涉及两类连接队列:
- SYN 队列:保存尚未完成 TCP 三次握手的连接。
- accept 队列:保存已建立、等待应用调用
accept()取走的连接。
可以用 ss 查看监听状态:
sudo ss -ltnp
可能看到:
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1200,fd=8))
字段含义包括:
LISTEN:socket 正在监听。0:当前等待应用取走的连接数。511:监听队列上限的一个实现相关表示,不能简单等同于系统所有连接容量。0.0.0.0:443:监听所有 IPv4 地址的 443 端口。users:持有该 socket 的进程信息。
ss 展示的是内核观察结果,不等于 Nginx 已经成功处理的请求数。连接可能已建立但尚未被 worker 读取,也可能在 TLS 或 HTTP 阶段等待。
Linux 网络接口和地址可以用 iproute2 查看:
ip addr show
ip route show
ip route get 203.0.113.10
ip route get 显示内核对于某个目标地址会选择的出口接口、源地址和下一跳。排查 Nginx 无法连接后端时,这比只看 Nginx 配置更直接,因为路由、策略路由、防火墙和源地址选择都可能改变连接结果。
2.2 master 与 worker
典型 Nginx 进程结构如下:
nginx: master process /usr/sbin/nginx
nginx: worker process
nginx: worker process
...
可以查看:
ps -o pid,ppid,user,stat,cmd -C nginx
master 进程主要负责:
- 读取和解析配置;
- 创建监听 socket;
- fork worker;
- 接收信号;
- 平滑重载和优雅退出;
- 管理日志文件打开与关闭等控制操作。
worker 进程主要负责:
- 接收客户端连接;
- 处理 HTTP 和 TLS;
- 连接后端;
- 读写网络数据;
- 执行请求处理阶段。
Nginx 常见模型是“少量进程 + 事件驱动”,而不是“每个连接一个线程”或“每个请求一个进程”。一个 worker 可以同时维护大量连接:当某个连接等待网络数据时,worker 不会阻塞在那里,而是处理其他就绪事件。
事件循环的抽象过程可以表示为:
while (worker_running) {
events = wait_for_kernel_events();
for (event in events) {
process_read_or_write(event);
advance_http_or_tls_state_machine();
}
}
在 Linux 上,Nginx 通常使用 epoll 作为事件通知机制。epoll 的核心意义不是“自动提高性能”,而是让应用可以等待一组文件描述符中真正就绪的那些描述符,避免轮询每一个连接。
2.3 worker 数量不是越多越好
常见配置:
worker_processes auto;
auto 通常根据可见 CPU 数量选择 worker 数,但这只是合理起点,不是性能保证。worker 数量过多会导致:
- 上下文切换增加;
- TLS 加密和日志写入争抢 CPU;
- 缓存、连接和内核资源竞争;
- 容器中 CPU 配额与宿主机 CPU 数量不一致时产生误判。
如果服务受 cgroup CPU 限制,应结合实际配额和压测结果调整,而不能只根据宿主机物理核心数决定。
2.4 accept、共享监听 socket 与连接分配
master 创建监听 socket 后,worker 通常继承同一个文件描述符。多个 worker 可能同时等待新连接,这会涉及连接分配和“惊群”问题。
常见配置:
events {
worker_connections 4096;
multi_accept on;
}
含义:
worker_connections是单个 worker 可打开连接数的配置上限,客户端连接和后端连接都可能占用连接资源,因此不能简单理解为“可服务请求数”。multi_accept on允许一次事件中接受多个连接,但可能使单个 worker 在短时间内拿走更多连接;是否有利取决于流量形态。accept_mutex控制 worker 是否通过互斥方式竞争新连接。在现代 Linux 和具体 Nginx 版本中,默认行为可能与旧版本不同,不能脱离版本直接推断。需要查看当前编译版本和官方文档。
某些 Linux/Nginx 组合还可以使用:
listen 443 ssl reuseport;
reuseport 允许多个 socket 绑定同一地址端口,由内核将连接分配给不同 socket。它可能改善多核扩展,但也改变连接分布特征,不应在没有验证的生产环境中盲目开启。
2.5 信号、重载与优雅退出
先检查配置:
sudo nginx -t
成功通常包括:
syntax is ok
test is successful
重载配置:
sudo systemctl reload nginx
重载的核心过程通常是:
- master 读取新配置;
- 新配置解析失败时,保留旧配置继续运行;
- 解析成功后,master 启动新 worker;
- 新 worker 使用新配置接受新连接;
- 旧 worker 停止接受新连接;
- 旧 worker 等待已有请求完成,然后退出。
因此,重载并不等于瞬间关闭所有连接,也不等于所有长连接立即切换。WebSocket、长轮询、大文件下载可能使旧 worker 长时间存在。
典型信号语义包括:
HUP:重读配置并平滑重载;QUIT:优雅退出;TERM:快速停止;USR1:重新打开日志文件;USR2:二进制升级流程的一部分。
应优先使用 systemd:
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager
直接向 PID 发送信号时,必须确认 PID 文件和进程身份,避免误杀其他进程。
三、最小可运行的 HTTP 反向代理
假设后端服务监听 127.0.0.1:8080,可以配置:
http {
upstream app_backend {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name example.test;
location / {
proxy_pass http://app_backend;
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;
}
}
}
若发行版主配置已经包含 /etc/nginx/conf.d/*.conf 或 /etc/nginx/sites-enabled/*,应把 upstream 和 server 放到相应文件,而不是重复嵌套一个新的 http 块。
验证配置并重载:
sudo nginx -t
sudo systemctl reload nginx
curl -v -H 'Host: example.test' http://127.0.0.1/
3.1 proxy_pass 路径行为
路径末尾的斜杠会改变 URI 转发结果,这是常见事故来源。
配置:
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
请求:
GET /api/users
通常转发为:
GET /api/users
而配置:
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
通常会把匹配到的 /api/ 替换成 /,转发为:
GET /users
因此:
- 无 URI 的
proxy_pass http://backend;:通常保留原始请求 URI。 - 带 URI 的
proxy_pass http://backend/;:按匹配 location 的 URI 替换规则构造新 URI。
必须结合精确的 location 匹配和重写规则验证,不要只凭斜杠记忆。可以临时让后端回显请求 URI,或用后端访问日志确认。
3.2 Host 和客户端地址
默认情况下,后端看到的 Host 可能不是客户端原始 Host,而是由代理模块构造的值。因此常见配置显式传递:
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;
这些头部不是加密认证机制。客户端可以伪造 X-Forwarded-For,除非 Nginx 位于可信入口之后,并且使用 set_real_ip_from、real_ip_header 等配置正确重建真实地址。
若 Nginx 直接面向互联网,不应无条件信任客户端提交的 X-Forwarded-For。例如:
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
上述配置只有在这些网段确实是可信代理网络时才成立;错误配置会让攻击者伪造源地址并绕过基于 IP 的审计或限流。
四、TLS:客户端连接和后端连接是两段独立的安全关系
4.1 TLS 终止的含义
TLS 终止是指客户端与 Nginx 建立 TLS 会话,Nginx 解密 HTTP,然后以明文或另一条 TLS 连接访问后端:
客户端 ==TLS==> Nginx ==HTTP==> 后端
如果后端也使用 TLS,则是:
客户端 ==TLS==> Nginx ==TLS==> 后端
这两段 TLS 的证书、信任根、SNI、校验策略彼此独立。客户端验证的是 Nginx 证书;Nginx 验证的是后端证书。
4.2 基本 HTTPS 配置
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/tls/example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/tls/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
ssl_certificate 通常应指向包含服务器证书及中间证书的链文件,而不是只包含叶子证书的文件。浏览器可能通过缓存或 AIA 补齐链,但不能依赖这种行为;命令行客户端和部分非浏览器客户端通常会更严格。
私钥权限应限制为 Nginx master 能读取的范围,例如:
sudo chown root:root /etc/nginx/tls/example.com.key
sudo chmod 600 /etc/nginx/tls/example.com.key
sudo nginx -t
实际运行用户和 master 读取私钥的方式受发行版打包配置影响,应以 ps、nginx -T 和文件权限验证为准。生产环境不应把私钥放在普通用户可写目录。
4.3 SNI 与虚拟主机选择
SNI 是 TLS ClientHello 中的服务器名称扩展。客户端在握手早期发送:
example.com
Nginx 可以据此选择对应的 server 和证书。一个地址端口上部署多个 HTTPS 域名时,SNI 是证书选择的关键。
验证证书和 SNI:
openssl s_client \
-connect 127.0.0.1:443 \
-servername example.com \
-showcerts </dev/null
如果不指定 -servername,Nginx 可能返回该地址端口的默认 server 证书。此时“浏览器访问正常但 openssl 看到错误证书”可能只是测试没有发送 SNI。
TLS 握手的关键顺序是:
- 客户端发送支持的协议、密码套件和 SNI。
- Nginx 根据监听地址、端口和 SNI 选择虚拟主机。
- Nginx 返回证书链和协商参数。
- 双方验证证书、协商密钥。
- 握手完成后,HTTP 才在加密通道中传输。
TLS 证书错误通常发生在第 2 至第 4 步,不能用后端应用日志解释。
4.4 后端 HTTPS 与证书校验
如果 Nginx 到后端也使用 TLS:
location / {
proxy_pass https://app_backend;
proxy_ssl_server_name on;
proxy_ssl_name api.internal.example;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/tls/internal-ca.pem;
proxy_ssl_verify_depth 3;
proxy_set_header Host api.internal.example;
}
这里有三个不同概念:
proxy_pass https://...:Nginx 到后端使用 TLS。proxy_ssl_server_name on:向后端 TLS 握手发送 SNI。proxy_ssl_verify on:验证后端证书,而不是仅加密传输。
如果 upstream 使用域名,DNS 解析、证书名称、SNI 名称和 Host 头部可能需要一致,但它们不是同一个字段。关闭 proxy_ssl_verify 可以暂时绕过证书校验,却会失去对端身份认证,不应当作为长期修复。
4.5 TLS 排障的分层方法
先确认监听:
sudo ss -ltnp | grep ':443'
再确认握手:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-verify_return_error </dev/null
再确认 HTTP:
curl -v https://example.com/
分层判断:
- TCP 连接失败:检查监听地址、防火墙、路由、安全组。
- TCP 成功但 TLS 失败:检查证书、私钥、协议、SNI、时间和链。
- TLS 成功但 HTTP 返回 4xx/5xx:检查 server/location、认证和后端。
- Nginx 返回
502:重点检查到后端的 TCP、TLS、协议和响应。 - Nginx 返回
504:重点检查后端响应超时或网络路径。
五、upstream 与负载均衡
5.1 负载均衡解决什么问题
负载均衡是把多个请求分配给多个后端实例。它可以提高容量和可用性,但不会自动保证:
- 请求在后端之间完全均匀;
- 有状态会话能够跨实例工作;
- 某个后端故障能立即被发现;
- 重试不会造成重复写入;
- 数据库和缓存具备一致性。
配置示例:
upstream app_backend {
server 10.0.0.11:8080 weight=3;
server 10.0.0.12:8080 weight=1;
server 10.0.0.13:8080 backup;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app_backend;
}
}
默认分配方法通常是加权轮询。权重为 3:1 时,长期观察下新请求的分配倾向约为 3/4 和 1/4,但短时间窗口、连接失败、长请求和重试都会破坏这个比例。
backup 后端通常只有在非 backup 后端不可用时才接收请求。它不是普通的低权重节点。
5.2 常见算法及状态
加权轮询
upstream app_backend {
server 10.0.0.11:8080 weight=3;
server 10.0.0.12:8080 weight=1;
}
适合无状态请求。它按请求选择,而不是按字节数或 CPU 消耗选择,因此一个大文件请求可能让“请求数均衡”与“资源消耗均衡”完全不同。
最少连接
upstream app_backend {
least_conn;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
选择当前活动连接数较少的节点。它更适合请求耗时差异较大的场景,但“连接少”不等于“CPU 空闲”,HTTP/2 多路复用还会使连接数与请求数的关系更加复杂。
IP hash
upstream app_backend {
ip_hash;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
同一个客户端地址倾向于访问同一个后端。这只能提供弱粘性:
- NAT 会让大量用户共享一个地址;
- 移动网络和代理会改变源地址;
- 节点增删会导致映射变化;
- 它不能替代共享 session 存储。
通用 hash
upstream app_backend {
hash $request_uri consistent;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
consistent 使用一致性哈希思想,减少节点变更造成的整体映射迁移。它适合缓存键或明确的业务分片场景,但如果直接按 URI 哈希,热点 URI 仍可能集中到一个节点。
5.3 被动故障判断
可以配置:
upstream app_backend {
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
}
常见含义是:在 fail_timeout 时间窗口内,某节点失败达到 max_fails 后,Nginx 暂时认为它不可用。这里的失败是 Nginx 在代理请求过程中观察到的失败,不等于完整的应用健康检查。
开源 Nginx 的 upstream 健康判断主要是被动的。商业版或第三方模块可能提供主动健康检查,但不能把其他产品或模块的指令当作开源版默认能力。
5.4 超时与连接失败
location / {
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
proxy_pass http://app_backend;
}
三个超时分别对应不同阶段:
proxy_connect_timeout:连接后端建立 TCP 连接的时间。proxy_send_timeout:向后端发送请求数据时,两次写操作之间允许的间隔。proxy_read_timeout:从后端读取响应时,两次读操作之间允许的间隔。
它们通常不是整个请求的绝对总时限。例如后端每 50 秒返回一小段数据,proxy_read_timeout 60s 可能一直不超时。若需要业务级总时限,应在应用或更高层实现。
5.5 重试的安全边界
proxy_next_upstream error timeout http_502 http_503 http_504;
这表示在特定失败条件下,Nginx 可以尝试另一个后端。但重试有副作用:
客户端发送 POST
|
v
后端 A 已经执行写入,但响应在网络中丢失
|
v
Nginx 重试到后端 B
|
v
业务写入执行两次
因此,读请求和具备幂等键的写请求更适合重试。所谓幂等,不是“请求方法叫 GET/POST”这么简单,而是重复执行是否产生相同业务结果。支付、下单、扣库存等操作必须由应用使用请求唯一 ID、数据库约束或幂等表保证重复提交安全。
5.6 被动均衡的反例
如果一个后端进程仍然监听端口,但每个请求都返回 500,TCP 连接和 Nginx 的连接建立可能完全成功。此时只依赖“端口是否可连接”的外部检查,会把一个业务不可用节点误判为健康。
反过来,如果后端健康接口很慢,但真实业务接口正常,过于严格的主动检查也可能错误摘除节点。健康检查应代表实际服务能力,而不是只检查一个无关的端口。
六、限流:请求速率、并发数与容量不是同一个概念
Nginx 常用两类限流:
limit_req:限制请求速率。limit_conn:限制并发连接数。
它们解决不同问题。每秒请求数低,并不表示连接数低;一个请求可能持续数分钟。反之,大量短请求会消耗请求处理能力,但不一定长期占用连接。
6.1 limit_req 的漏桶模型
配置:
http {
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;
server {
listen 80;
server_name app.example.com;
location /api/ {
limit_req zone=per_ip burst=20 nodelay;
proxy_pass http://app_backend;
}
}
}
含义:
$binary_remote_addr:以客户端地址作为限流键,二进制形式比字符串更节省共享内存。zone=per_ip:10m:创建名为per_ip、大小为 10 MB 的共享内存区域。rate=10r/s:平均速率为每秒 10 个请求。burst=20:允许一定数量的突发请求等待或排队。nodelay:不等待,允许突发请求立即通过,直到突发容量耗尽。
可以用令牌桶近似理解,但 Nginx 的 limit_req 更准确地表现为按速率排队的漏桶行为。设:
- 平均速率为 ;
- 某一时刻到达 个请求;
- 配置突发容量为 。
如果当前没有积压,瞬时到达的请求中,超出平均速率部分最多可在 burst 范围内处理或等待。没有 nodelay 时,Nginx 会通过延迟请求把输出速率拉回 ;有 nodelay 时,突发请求直接放行,但之后仍会受到速率状态约束。
例如 rate=10r/s, burst=20:
- 短时间内到达 10 个请求:通常立即放行。
- 再来 20 个请求:若配置允许突发,它们占用 burst 额度。
- 超过可用额度:返回限流错误,默认通常为
503。 - 没有
nodelay时,部分请求可能等待,而不是立即得到响应。
这不是“每秒固定窗口最多 10 个”的算法。固定窗口会产生边界突发,例如前一秒末尾通过 10 个、后一秒开头再通过 10 个;漏桶模型的状态是连续变化的。
6.2 限流状态存在哪里
limit_req_zone 的状态放在共享内存中,使多个 worker 能看到同一个限流状态。若使用客户端 IP 作为 key,Nginx 处理请求时会:
- 计算 key;
- 在共享内存中查找该 key;
- 读取该 key 的最近请求状态;
- 根据经过时间和速率计算是否可通过;
- 更新状态;
- 释放共享内存锁并继续请求。
因此,限流区会消耗内存和锁竞争。使用高基数 key,例如任意用户输入、完整 URI 或大量随机 Header,可能导致内存耗尽、旧状态淘汰和行为不稳定。
6.3 按用户而不是按 IP 限流
如果认证后有稳定用户变量,可以使用:
limit_req_zone $http_authorization zone=per_token:20m rate=5r/s;
但直接使用整个 Authorization 头存在安全和容量问题:
- 未认证请求可能都没有该头;
- token 很长,内存占用更高;
- 日志或调试时可能泄露凭据;
- token 轮换会产生大量 key。
更合理的做法通常是由认证层生成经过验证的用户标识,并确保未认证请求使用单独的低权限策略。限流键必须在信任边界内生成,不能把客户端任意提交的用户 ID 当作真实身份。
6.4 limit_conn:限制并发连接
http {
limit_conn_zone $binary_remote_addr zone=addr_conn:10m;
server {
location /download/ {
limit_conn addr_conn 2;
proxy_pass http://app_backend;
}
}
}
它限制的是同一 key 的并发连接数。若客户端使用 HTTP/2,多路请求可能共享一个 TCP 连接,因此“连接数”不一定等于“并发请求数”。如果业务要限制并发下载任务,应在应用层按任务或用户控制。
6.5 限流状态码和验证
可以显式修改状态码:
limit_req_status 429;
limit_conn_status 429;
验证方法:
for i in $(seq 1 50); do
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/api/test
done | sort | uniq -c
预期可能看到:
20 200
30 429
实际数量受速率、请求间隔、后端响应速度和 worker 调度影响,不应把这个数字当成固定保证。生产测试还应观察 $request_time、错误日志和后端接收量,确认请求是在 Nginx 入口被拒绝,而不是后端返回了同样的状态码。
七、日志:把一次请求的因果链记录下来
7.1 access log 与 error log
- access log:记录请求正常完成或被处理后的访问事实,例如客户端地址、请求行、状态码、响应字节数和耗时。
- error log:记录配置、连接、TLS、upstream、权限等错误和诊断信息。
基本配置:
http {
log_format main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'urt=$upstream_response_time '
'ua="$upstream_addr" us="$upstream_status" '
'rid="$request_id"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
}
重要变量:
$request_time:从读取客户端第一个字节到日志写入前的请求处理时间。$upstream_connect_time:连接后端的时间。$upstream_header_time:连接后端后等待响应头的时间。$upstream_response_time:从后端读取响应所涉及的时间。$upstream_status:后端返回的状态。$upstream_addr:实际使用的后端地址。$request_id:Nginx 生成的请求标识,适合串联日志,但不能自动等同于业务请求 ID。
这些时间不是同一段时钟:
request_time
|----------------------------------------------------|
客户端读请求 + 排队 + upstream + 向客户端发送响应
upstream_response_time
|---------------------|
连接后端并读取响应
request_time - upstream_response_time
≈ 客户端收发、Nginx 排队、限流延迟等其他部分
这个差值不是严格的 CPU 时间,也不是精确的网络延迟,但可以帮助区分“后端慢”和“客户端慢”。
7.2 真实客户端地址与代理链
如果 Nginx 前面还有 CDN、负载均衡或 Keepalived 漂移的入口,$remote_addr 可能只是上一跳代理地址。此时应先定义可信代理边界,再配置 real IP 模块,并在日志中同时记录:
log_format proxy_audit
'peer=$remote_addr real=$realip_remote_addr '
'xff="$http_x_forwarded_for" '
'request="$request" status=$status rt=$request_time';
变量的具体含义和处理结果依赖 real IP 配置。不能看到一个 X-Forwarded-For 就直接把它当作真实来源。
7.3 日志文件轮转
Nginx 打开的日志文件不会因为外部 mv 自动切换到新文件。常见轮转过程是:
sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.1
sudo kill -USR1 "$(cat /run/nginx.pid)"
systemd 或 logrotate 通常会自动完成类似动作。轮转后必须让 Nginx 重新打开日志,否则它可能继续写入已经被重命名但仍保持打开的旧 inode。
检查文件和进程持有关系:
sudo lsof -p "$(cat /run/nginx.pid)" 2>/dev/null
sudo lsof | grep '/var/log/nginx'
日志包含 URL、查询参数、用户代理和可能的身份信息。不要在日志中记录密码、完整 token、Cookie 或未经脱敏的敏感请求体。
八、一个相对完整的生产型示例
下面示例把 TLS、upstream、请求头、超时、限流和日志放在一起:
worker_processes auto;
events {
worker_connections 4096;
}
http {
include mime.types;
default_type application/octet-stream;
log_format proxy_main
'$remote_addr [$time_iso8601] '
'rid=$request_id "$request" status=$status '
'bytes=$body_bytes_sent rt=$request_time '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'urt=$upstream_response_time '
'upstream=$upstream_addr '
'up_status=$upstream_status';
access_log /var/log/nginx/access.log proxy_main;
error_log /var/log/nginx/error.log warn;
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=download_per_ip:10m;
upstream app_backend {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
}
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/tls/app.example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/tls/app.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
location /api/ {
limit_req zone=api_per_ip burst=40 nodelay;
limit_req_status 429;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
proxy_pass http://app_backend;
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 https;
proxy_set_header X-Request-ID $request_id;
}
location /download/ {
limit_conn download_per_ip 2;
limit_conn_status 429;
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
}
部署步骤:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager
逐层验证:
curl -I http://app.example.com/
curl -vk https://app.example.com/api/health
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
验证结果应分别回答:
- HTTP 是否被重定向到 HTTPS?
- HTTPS 证书和 SNI 是否正确?
/api/health是否到达后端?- 响应头中是否有期望的请求 ID?
- access log 中的 upstream 地址、状态和耗时是否符合预期?
- 限流时是否由 Nginx 返回
429,而不是后端返回?
九、失败路径与诊断顺序
9.1 502 Bad Gateway
502 通常表示 Nginx 作为网关从上游获得了无效响应,常见原因包括:
- 后端端口未监听;
- TCP 连接被拒绝;
- 后端提前关闭连接;
- HTTP 响应格式错误;
- Nginx 与后端协议不匹配,例如把 HTTPS 后端按 HTTP 访问;
- upstream DNS 或地址配置错误。
诊断:
sudo ss -ltnp | grep ':8080'
curl -v http://10.0.0.11:8080/health
sudo tail -n 100 /var/log/nginx/error.log
如果后端是 HTTPS:
curl -vk https://10.0.0.11:8443/health
openssl s_client -connect 10.0.0.11:8443 -servername api.internal.example </dev/null
不要看到 502 就立即增加超时。连接拒绝和证书校验失败不是“等待时间太短”。
9.2 504 Gateway Timeout
504 更常见于:
- 后端接受连接但迟迟不返回响应头;
- 后端读取过程中长时间没有数据;
- 网络路径丢包或中间设备丢弃连接;
- 配置的
proxy_read_timeout太短。
应结合:
uct:后端连接是否建立得慢
uht:后端响应头是否迟迟不来
urt:后端响应读取是否耗时
rt:客户端整个请求是否更慢
如果 uht 很大,问题多半在后端排队、数据库或应用处理;如果 urt 很大而 uht 较小,可能是响应体生成或传输慢。
9.3 配置通过但服务无法启动
nginx -t 成功只表示配置语法和部分文件检查通过,不保证:
- 端口没有被其他进程占用;
- SELinux/AppArmor 允许访问证书和日志;
- systemd 运行目录存在;
- 后端网络可达;
- 证书在运行时仍有效。
检查:
sudo systemctl status nginx --no-pager
sudo journalctl -u nginx -b --no-pager
sudo ss -ltnp | grep -E ':(80|443)\b'
sudo nginx -T
nginx -T 会展开并打印完整配置,适合确认 include 后最终生效的内容;其中可能包含敏感路径和配置,不应随意粘贴到公开工单。
十、生产边界:哪些问题 Nginx 不能替应用解决
10.1 会话状态
如果用户登录状态存储在单个后端进程内存中,负载均衡后可能出现:
第一次请求 -> Backend A,建立 session
第二次请求 -> Backend B,提示未登录
解决方向是:
- 把 session 放在共享存储;
- 使用加密且可验证的无状态 cookie;
- 临时使用粘性策略。
IP hash 只能降低切换概率,不能提供可靠会话一致性。
10.2 TLS 与端到端加密
Nginx 终止 TLS 后,客户端到 Nginx 的机密性并不自动延伸到 Nginx 到后端。若后端跨主机、跨租户或经过不可信网络,应使用后端 TLS,并启用证书验证和正确的 SNI。
10.3 Keepalived 与 VRRP 的关系
Keepalived/VRRP 可以让多个 Nginx 节点共享一个虚拟 IP,但它只解决入口 IP 的漂移,不会同步:
- Nginx worker 内存状态;
limit_req共享区;- 本地 session;
- TLS 会话缓存;
- upstream 被动失败状态;
- 日志文件。
因此,VRRP 切换后,限流计数可能从另一台节点重新开始,连接也可能因网络路径变化而重建。健康检查脚本必须避免“进程存在即健康”的误判;一个已经无法连接后端的 Nginx 进程仍然可能存活。
脑裂时两个节点同时宣告持有 VIP,会导致 ARP/邻居缓存不断变化、流量随机进入不同节点。VRRP 不是分布式一致性协议,不能单独保证脑裂场景下的数据一致性。
10.4 WebSocket 和长连接
WebSocket 需要显式传递升级头:
location /ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1h;
proxy_pass http://app_backend;
}
WebSocket 长时间占用连接,因此:
worker_connections要按客户端连接和后端连接共同估算;limit_conn可能比limit_req更重要;- 重载和后端切换不会自动迁移已有连接;
- 负载均衡算法对长连接的分布可能长期不均衡。
十一、几个容易被忽略的反例
反例一:把 worker_connections 当作并发用户数
假设有 4 个 worker、每个 worker_connections 4096,不能直接得出可服务 16384 个用户。原因包括:
- 一个客户端连接可能对应一个后端连接;
- TLS、HTTP/2、WebSocket 的连接模型不同;
- 文件描述符总上限可能更低;
- 连接还需要 socket 缓冲区、TLS 状态和请求内存;
- upstream、日志和其他模块也会占用资源。
可检查:
ulimit -n
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
反例二:只检查后端端口
端口可连接只能证明 TCP 层可能建立连接,不证明:
- TLS 证书正确;
- HTTP 协议正确;
- 依赖的数据库正常;
- 请求能在时限内完成;
- 返回内容符合业务要求。
健康检查应覆盖真正影响流量的关键路径,但检查接口本身也不能依赖过多外部系统,否则会把局部故障扩大成全量摘除。
反例三:按 IP 限流等于按用户公平限流
企业出口、移动运营商和 NAT 会让很多用户共享一个 IP。按 IP 限流可能让一个大组织的所有用户互相争抢额度;攻击者也可能通过代理池改变源 IP。IP 限流是网络边界上的粗粒度保护,不是身份级配额系统。
反例四:增加重试就能提高可用性
对于查询请求,重试可能绕过瞬时故障;对于写请求,重试可能造成重复副作用。可用性提升必须与幂等设计、超时预算和后端容量一起评估。
十二、从内核到应用的完整排查示例
假设用户报告“HTTPS 偶尔返回 504”,可以按以下顺序缩小范围:
第一步:确认入口连接
sudo ss -s
sudo ss -tn state established '( sport = :443 )'
如果 443 上连接数暴增,先确认是否为真实流量、长连接堆积或攻击;不要直接扩大 Nginx worker 数。
第二步:确认 TLS 是否成功
curl -vk --connect-timeout 5 https://app.example.com/api/
如果握手阶段就失败,504 还没有发生,问题在 TLS 或入口网络。
第三步:检查 access log 的时间分解
sudo tail -f /var/log/nginx/access.log
重点比较:
rt=...
uct=...
uht=...
urt=...
up_status=...
如果 uht 接近 proxy_read_timeout,说明后端迟迟没有返回响应头。
第四步:从 Nginx 主机直接访问后端
curl -v --connect-timeout 3 http://10.0.0.11:8080/api/
curl -v --connect-timeout 3 http://10.0.0.12:8080/api/
若只有一个节点慢,检查该节点应用、数据库连接池和系统资源;若所有节点同时慢,优先检查共享依赖、网络或流量突增。
第五步:观察内核和进程资源
vmstat 1
iostat -xz 1
free -m
ss -tan state syn-recv
ss -tan state established
这些命令分别帮助判断 CPU 调度、磁盘等待、内存压力、半连接堆积和已建立连接数量。它们不能单独证明根因,但能把“应用慢”和“网络入口拥塞”区分开。
第六步:修改前保留回滚路径
sudo cp -a /etc/nginx /etc/nginx.backup.$(date +%Y%m%d%H%M%S)
sudo nginx -t
sudo systemctl reload nginx
若新配置已经重载但行为异常,先回滚配置并重新测试,而不是连续叠加超时、重试和限流参数。每次只改变一个主要变量,才能建立因果关系。
十三、核心关系的总结
Nginx 反向代理可以拆成五个相互关联但不能混为一谈的层次:
- Linux socket 与进程模型决定连接如何进入 worker,以及系统能承受多少连接。
- TLS决定客户端或后端的身份认证、加密和证书链验证;两段 TLS 是两个独立会话。
- 负载均衡决定请求进入哪个 upstream,并受算法、节点状态、长连接和重试影响。
- 限流通过共享内存维护速率或并发状态,保护入口资源,但不等于用户配额或应用级并发控制。
- 日志把客户端、Nginx、upstream 和时间分解串联起来,是验证数据流和定位失败阶段的证据。
真正可靠的代理配置,不是参数越多越好,而是每个参数都对应一个明确的状态、一个可验证的假设和一个可恢复的失败路径:监听是否成功、TLS 是否完成、请求是否被限流、后端是否被选中、响应在哪个阶段变慢,以及日志是否足以重建这条链路。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux TLS 与证书运维:PKI、链、SNI、OCSP、续期和排障
- 下一篇:Linux Keepalived 与 VRRP:虚拟 IP、健康检查、切换和脑裂边界
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论