[点晴永久免费OA]Nginx 从入门到精通:一篇讲透反向代理、负载均衡、HTTPS、缓存与性能优化
当前位置:点晴教程→点晴OA办公管理信息系统
→『 经验分享&问题答疑 』
一、先回答一个问题:Nginx 到底是什么?Nginx 是一个高性能的 Web 服务器和反向代理服务器,也常用于负载均衡、HTTP 缓存、TLS 终结和四层 TCP/UDP 代理。 它最核心的优势不是“配置文件短”,而是事件驱动、异步非阻塞的处理模型。 传统的“一连接一线程”模型,在并发连接快速增加时,线程切换和内存开销会持续上升。Nginx 通常由一个 master 进程和多个 worker 进程协作:
这也是为什么 Nginx 特别适合处理高并发、静态资源和大量长连接。 不过要注意:Nginx 擅长的是网络 I/O 调度,不适合直接承担复杂业务计算。一个典型架构通常是: 一句话概括:让 Nginx 做流量入口,让应用服务专注业务逻辑。 二、安装之后,先认识这些目录和命令不同发行版的目录会略有差异,常见位置如下: 最重要的不是立刻启动服务,而是记住下面这组命令: 如果由 systemd 管理,也可以使用: 生产环境中最值得形成肌肉记忆的一条命令是: 它先检查配置,只有语法正确才重载。很多线上事故,都是因为跳过了前半句。 三、读懂 Nginx 配置文件:上下文比指令更重要Nginx 配置不是简单的键值对,而是由不同层级的“上下文”组成。 一个简化后的主配置如下: 常见上下文可以这样理解:
初学者最常见的错误,不是指令拼错,而是把正确的指令写在了错误的上下文中。 例如, 遇到 四、第一份可用配置:部署一个静态网站假设网站文件位于: 可以创建一个站点配置: 这里有三个关键点。 1. |
| 写法 | 含义 |
location = /path | 精确匹配 |
location ^~ /path/ | 优先使用此前缀匹配,不再检查正则 |
location /path/ | 普通前缀匹配 |
location ~ regex | 区分大小写的正则匹配 |
location ~* regex | 不区分大小写的正则匹配 |
一个实用的理解顺序是:
^~,直接采用;例如:
location ^~ /assets/ {
expires 30d;
}
location ~* \.(js|css|png|jpg)$ {
expires 7d;
}请求 /assets/app.js 会命中 /assets/,因为 ^~ 阻止了后续正则覆盖它。
排查路由问题时,不要只问“哪个 location 看起来更像”,要按匹配优先级一步一步推演。
假设应用运行在本机 127.0.0.1:8080,Nginx 对外提供 80 端口:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}这些请求头非常重要:
Host:把原始域名传给后端;X-Real-IP:传递当前客户端地址;X-Forwarded-For:记录代理链路上的客户端地址;X-Forwarded-Proto:告诉后端原始请求是 HTTP 还是 HTTPS。不过,后端应用不能无条件信任任何客户端传来的 X-Forwarded-For。只有在确认请求来自可信代理时,才应该解析这些头部,否则客户端可以自行伪造。
proxy_pass 末尾斜杠的区别这是 Nginx 最经典的坑之一。
配置一:保留 /api/ 前缀。
location /api/ {
proxy_pass http://127.0.0.1:8080;
}请求:
/api/users后端收到的路径通常仍是:
/api/users配置二:移除 /api/ 前缀。
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}同样的请求,后端收到:
/users记忆方法:proxy_pass 后面带 URI 部分时,会用该 URI 替换匹配到的 location 前缀。
修改这类配置后,不要只看语法检查,要用真实请求验证后端最终收到的路径。
反向代理配置中,下面几项经常被一起修改:
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 20m;
}它们分别表示:
proxy_connect_timeout:连接后端服务的超时时间;proxy_send_timeout:向后端发送请求时,两次写操作之间允许的最大间隔;proxy_read_timeout:从后端读取响应时,两次读操作之间允许的最大间隔;client_max_body_size:客户端请求体上限,常用于上传文件。需要特别注意:把所有超时都改成几分钟甚至几十分钟,只是把问题藏起来。正确做法是先确认:
对于普通 API,请优先让业务快速失败,而不是无限等待。
Nginx 默认会缓冲后端响应,这有利于释放后端连接,并平滑处理慢客户端。
但对于 Server-Sent Events、流式输出或部分实时场景,可能需要:
location /events/ {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}不要全局关闭缓冲。只在确实需要流式传输的位置关闭。
WebSocket 需要把 HTTP/1.1 升级相关头部传给后端。
推荐先在 http 上下文定义:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}然后在业务配置中使用:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 1h;
}很多 WebSocket “握手失败”,根源并不在应用,而是代理层没有传递 Upgrade 和 Connection 头,或者中间网络设备提前断开空闲连接。
当应用扩展到多个实例,可以定义 upstream:
upstream app_backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
server 10.0.0.13:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://app_backend;
}
}默认策略是轮询:请求依次分配到不同后端。
upstream app_backend {
server 10.0.0.11:8080 weight=3;
server 10.0.0.12:8080 weight=1;
}第一台机器理论上会获得更多请求,适合配置差异明显的实例。
upstream app_backend {
least_conn;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}适合请求处理时长差异较大的场景。
upstream app_backend {
ip_hash;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}同一客户端地址通常会落到同一后端。但这并不是理想的会话方案:用户可能经过 NAT,共享同一个出口地址;客户端地址也可能变化。
更推荐把会话放到 Redis、数据库或签名 Cookie 中,让应用实例保持无状态。
upstream app_backend {
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
}开源版 Nginx 常见的是被动失败判断:真实请求失败达到一定次数后,暂时认为节点不可用。主动健康检查属于另一类能力,选型时要区分清楚。
upstream app_backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 32;
}
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}上游 keepalive 可以减少频繁建立 TCP 连接的成本,但连接池大小需要结合 worker 数量、后端容量和真实并发评估。
一个常见的 HTTPS 配置如下:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}上线前至少检查四件事:
add_header Strict-Transport-Security "max-age=31536000" always;HSTS 会要求浏览器在有效期内只通过 HTTPS 访问。它能提升安全性,但也会放大证书或 HTTPS 配置错误的影响。
在确认所有子域名都已长期支持 HTTPS 之前,不要贸然加入 includeSubDomains;更不要在测试不充分时提交浏览器预加载列表。
对于带内容哈希的静态资源,例如:
app.8f3a2c.js
style.19ab42.css可以设置长缓存:
location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|woff2)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}但 HTML 通常不应该长期缓存,因为它负责引用最新版本的资源:
location = /index.html {
add_header Cache-Control "no-cache";
}最可靠的发布策略是:
gzip on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_vary on;
gzip_types
text/plain
text/css
application/json
application/javascript
application/xml
image/svg+xml;压缩级别不是越高越好。级别过高会增加 CPU 消耗,收益却可能很有限。通常应基于真实文件、CPU 使用率和带宽测试,而不是盲目拉满。
图片、视频、压缩包等已经压缩过的内容,继续使用 Gzip 往往收益很小。
先在 http 上下文定义缓存区域:
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;再在业务位置启用:
location /api/public/ {
proxy_pass http://app_backend;
proxy_cache api_cache;
proxy_cache_methods GET HEAD;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status always;
}常见缓存状态包括:
MISS:没有缓存,向后端请求;HIT:直接命中缓存;EXPIRED:缓存已过期;BYPASS:跳过缓存;UPDATING:缓存正在更新。带有用户身份、购物车、权限结果、一次性令牌的响应,通常不应该共享缓存。
可以根据 Cookie 或 Authorization 头跳过缓存:
map $http_authorization $skip_auth_cache {
default 1;
"" 0;
}
location /api/public/ {
proxy_pass http://app_backend;
proxy_cache api_cache;
proxy_cache_bypass $skip_auth_cache;
proxy_no_cache $skip_auth_cache;
}缓存设计必须先回答一个问题:哪些请求在不同用户之间具有完全相同的响应?
没有明确答案,就不要轻易共享缓存。
在 http 上下文定义区域:
limit_req_zone $binary_remote_addr zone=api_rate:10m rate=10r/s;在接口上使用:
location /api/ {
limit_req zone=api_rate burst=20 nodelay;
proxy_pass http://app_backend;
}含义是:
nodelay 表示突发请求在额度内立即处理,而不是排队平滑释放。limit_conn_zone $binary_remote_addr zone=per_ip_conn:10m;
server {
location /download/ {
limit_conn per_ip_conn 5;
}
}按 IP 限流简单,但并不总是公平:
登录用户场景可以考虑按用户标识、API Key 或组合维度限流。无论采用什么键,都要配合监控和合理的错误响应。
建议明确返回 429:
limit_req_status 429;同时让客户端知道应该退避重试,而不是疯狂重放。
server_tokens off;它不能替代升级和补丁,但可以减少不必要的信息暴露。
只读资源可以这样处理:
location /public/ {
limit_except GET HEAD {
deny all;
}
}不要在全局机械禁用方法,API 可能确实需要 POST、PUT、PATCH 或 DELETE。
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;内容安全策略 CSP 很强大,但必须根据页面实际加载的脚本、样式、字体和第三方资源精细设计。直接复制一份严格 CSP,可能让页面大面积失效;过度宽松又起不到保护作用。
add_header 的继承规则add_header 有一个很容易被忽略的行为:如果当前配置层级声明了任意 add_header,上一级的 add_header 通常不会继续继承下来。
例如,server 中设置了安全响应头,但某个 location 又单独设置缓存头:
server {
add_header X-Content-Type-Options "nosniff" always;
location /assets/ {
add_header Cache-Control "public, max-age=2592000";
}
}此时 /assets/ 的响应可能只有缓存头,而缺少上层的安全头。生产环境可以把公共响应头整理到独立的 snippet 中,在所有需要声明 add_header 的层级重复 include;也可以显式重复这些头,并通过 curl -I 验证最终响应。
location ~ /\. {
deny all;
}但 ACME 证书验证可能需要访问 /.well-known/,应先添加例外:
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location ~ /\. {
deny all;
}用户可上传内容的目录应与可执行代码严格隔离。不要仅依赖文件扩展名判断安全性,也不要让上传目录落入动态脚本处理规则。
默认访问日志通常信息有限。可以定义一份更适合分析的格式:
log_format main_ext
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'ua="$http_user_agent" xff="$http_x_forwarded_for" '
'upstream="$upstream_addr" cache="$upstream_cache_status"';
access_log /var/log/nginx/access.log main_ext;重点变量:
变量 含义 $request_time从读取请求到发送完响应的总时间 $upstream_connect_time连接后端耗时 $upstream_header_time等到后端响应头的耗时 $upstream_response_time与后端交互的总耗时 $upstream_addr实际命中的后端地址 $statusNginx 返回给客户端的状态码 $upstream_status后端返回给 Nginx 的状态码
判断慢请求时,可以这样分析:
request_time 高、upstream_response_time 也高:大概率后端慢;request_time 高、上游时间低:可能是客户端慢、响应体大或代理层排队;upstream_connect_time 高:后端连接建立慢、网络异常或连接耗尽;error_log /var/log/nginx/error.log warn;常见级别包括 error、warn、notice、info、debug。
生产环境不建议长期启用 debug,因为日志量会非常大。需要深度排障时,应限定时间和范围,并确认当前构建是否支持调试日志。
一份常见的基础配置:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 8192;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
keepalive_requests 1000;
}但请注意:这些参数并不是“复制后性能就翻倍”。
worker_processes auto通常会根据可用 CPU 自动设置 worker 数量,是一个合理起点。
worker_connections它表示单个 worker 可打开的连接数量上限之一,但实际容量还受文件描述符上限、系统内核、上游连接、日志和其他资源影响。
反向代理场景中,一个客户端请求可能同时占用客户端连接和上游连接,因此不能简单地用:
worker 数量 × worker_connections直接等同于业务并发数。
需要同时关注:
ulimit -n
cat /proc/$(cat /run/nginx.pid)/limitssystemd 服务还可能有独立的 LimitNOFILE限制。
长连接可以减少 TCP 和 TLS 握手,但超时时间过长会让大量空闲连接占用资源。合理值取决于客户端类型、请求频率和前置负载均衡器。
不要只用一个固定小响应测 QPS。至少模拟:
优化目标也不应该只有平均响应时间,还要关注 P95、P99、错误率、CPU、内存、连接数和后端负载。
执行:
nginx -t && nginx -s reloadNginx 会让 master 进程重新读取配置,并启动新的 worker。旧 worker 不再接收新连接,在处理完已有请求后退出。
这就是平滑重载。
它非常适合常规配置变更,但仍应注意:
生产变更应配合:
常见原因:
排查:
curl -v http://127.0.0.1:8080/health
ss -lntp
journalctl -u nginx
tail -f /var/log/nginx/error.log常见原因:
proxy_read_timeout 小于业务实际耗时;不要一上来就调大超时。先查看应用链路和上游响应时间。
请求体超过限制:
client_max_body_size 50m;还要检查更上游的 CDN、云负载均衡器和应用框架是否有独立限制。
常见原因:
deny、limit_except 等访问控制命中;检查 Nginx worker 的运行用户是否有权限逐级进入目录。
常见于:
X-Forwarded-Proto 未正确传递或未被应用信任。可使用:
curl -I -L --max-redirs 10 https://example.com观察每一跳的 Location。
依次检查:
nginx -t
nginx -T | less
ps -ef | grep nginx
systemctl status nginx最常见的情况是:改错文件、配置没有被 include、重载的是另一个实例,或者容器内外文件并不是同一份。
下面的示例把前面的知识串起来。它不是“万能最佳配置”,而是一份便于理解和继续改造的骨架。
/etc/nginx/nginx.confuser nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 8192;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main_ext
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'upstream="$upstream_addr" xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main_ext;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
keepalive_requests 1000;
server_tokens off;
gzip on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_vary on;
gzip_types
text/plain
text/css
application/json
application/javascript
application/xml
image/svg+xml;
limit_req_zone $binary_remote_addr zone=api_rate:10m rate=20r/s;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/conf.d/*.conf;
}/etc/nginx/conf.d/example.confupstream app_backend {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
limit_req zone=api_rate burst=40 nodelay;
limit_req_status 429;
client_max_body_size 20m;
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_send_timeout 60s;
proxy_read_timeout 60s;
}
location /ws/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 1h;
}
location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|woff2)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files $uri =404;
}
location ~ /\. {
deny all;
}
}上线前执行:
nginx -t
nginx -T > /tmp/nginx-effective.conf
nginx -s reload然后至少验证:
curl -I http://example.com
curl -I https://example.com
curl -v https://example.com/api/health
curl -I https://example.com/assets/app.jsNginx 的“精通”,不是记住所有指令,而是具备三种能力。
掌握:
server 与 location;proxy_pass;目标是独立完成一个网站和一组 API 的部署。
面对一个请求,你能清楚回答:
server;location;这一步决定了你是否能快速排障。
掌握:
真正的优化不是“参数越大越好”,而是找到当前系统的瓶颈,并用指标证明修改有效。
生产环境应逐步建立:
nginx -t;到这个阶段,Nginx 不再是一份手工维护的配置文件,而是整个交付体系中的基础设施代码。
可以把下面这份清单放进团队发布流程。
nginx -t 通过;nginx -T 确认实际加载配置;location 匹配顺序已验证;proxy_pass 路径拼接已用真实请求测试;Nginx 的配置语法并不复杂,复杂的是它处在整个请求链路的最前面:域名、证书、路由、缓存、限流、连接、日志和后端状态,都会在这里交汇。
初学时,你可能只需要记住一条反向代理配置;进入生产后,你需要理解请求从客户端到后端、再从后端返回客户端的每一步。
学习 Nginx 最有效的方法,不是背完整指令表,而是反复练习三件事:
当你能够根据一条请求,准确解释它命中了哪个配置、被转发到哪里、为什么慢、为什么失败,以及如何安全地修改和回滚时,你就已经真正从“入门”走向了“精通”。
建议收藏本文,并在每次修改生产配置前重复执行:
nginx -t && nginx -s reload配置可以重写,线上事故却不一定能撤回。
阅读原文:点击这里