先说我经历过的一个真实场景。我们团队当时把一个前后端分离的项目部署上线,后端接口跑在 8080 端口,前端打包出来的静态文件也扔在同一台服务器上。最开始图省事,直接用 Node.js 把静态文件和接口一起处理,结果用户量稍微上来一点,CPU 直接飙到 90%,日志里密密麻麻全是图片、CSS、JS 的请求。后来把 Nginx 请过来干三件事——接管静态资源、反代后端接口、再往下做多节点负载均衡,服务器安静得像是没跑东西。
这篇东西就是围绕 Nginx 入门到项目部署整理的,核心锁定在反向代理、负载均衡、静态资源分发这三个方向。你不需要有运维基础,哪怕是第一次接触服务器配置,跟着章节往下走,也能把一个真实项目完整地挂到公网上。我会把配置文件逐行解释清楚,把每一步为什么这么写讲明白,再把部署之后最容易踩的坑提前给你打好预防针。这套内容适合后端开发、前端开发、独立开发者,也适合那些刚接手服务器、被“502 Bad Gateway”教育过的准运维同学。
1. Nginx 在这台服务器里到底扮演什么角色
1.1 一条请求链路看明白 Nginx 的位置
先别急着敲命令,脑子里先有一个画面。用户在浏览器里输入你的站点地址,回车之后,请求进入服务器,落到 80 端口。这时候谁在 80 端口盯着?如果没装 Nginx,可能是一个开发服务器顺手监听;如果装了 Nginx,它就成了唯一的守门员。
这个守门员处理请求的路径大致是这样的:
用户浏览器 | v Nginx(80端口 / 443端口) |-- 静态资源请求 --> 服务器磁盘上的 html/css/js/png |-- 接口请求 --> 反向代理 --> 后端服务(192.168.1.11:8080 等) |-- 动态转发 --> 负载均衡 --> 多台后端节点分摊Nginx 看请求是有分工意识的。请求的是静态文件,它直接从磁盘读出来返回,速度极快;请求的是复杂业务,它再转发给后面真正的应用服务处理,拿到结果后原样喂回给用户。这种“扛流量但不干重活”的设计,恰恰是生产环境中最合理的一种分工模式。
1.2 三个关键词对应三种能力
标题里那三个词,其实就是 Nginx 最核心的三项技能,我分别用大白话解释一下。
反向代理,相当于公司前台。你想找技术部的人,不需要知道技术部在哪个工位,直接去前台说需求,前台帮你把话带到,再把答复带回来给你。用户请求的也是同一个道理——用户只知道 80 端口,并不知道你的后端服务跑在 8080 还是 3000,Nginx 把请求接收下来,转发给真正干活的进程,再把结果返回给用户。这个过程对用户完全透明,同时顺带把后端服务保护了起来。
负载均衡,相当于食堂开了两个甚至更多窗口。排队买单的人多了,一个窗口肯定被压垮,多开几个窗口分摊流量,每个人等待时间都短了。Nginx 把请求按策略分发到一组后端服务器上,避免单台机器被流量打爆。
静态资源服务,这个用途最初级也最常用,相当于直接去仓库提货。图片、CSS、JS、字体这些不经常变化的东西,本来就适合像货架一样摆好,谁要直接取。Nginx 做这件事极其擅长,因为它的事件驱动模型在处理高并发静态请求时非常高效。
1.3 为什么不是直接让应用服务跑到底
可能有人会问:我的 Node.js 或者 Java 服务不也能输出静态文件吗?为什么非要中间横着一个 Nginx?
理论上确实可以,但实际生产环境中有一堆现实问题。应用服务在处理静态文件时,每个请求都要占用一层业务逻辑的处理能力,哪怕这个请求只是读取一张照片。当并发上来,业务进程被静态请求挤占资源,真正需要算力的接口请求就变慢了。Nginx 负责把静态流量终结在最前面一层,应用服务专心处理 API,各干各的最擅长的事情,整体吞吐量完全是两个量级。
再看安全性。应用服务直接暴露给公网,等于把实现细节全展示出去了,攻击者能直接试探框架漏洞。Nginx 挡在外面之后,可以统一做限流、控制超时、处理跨域、过滤恶意请求,底层服务对访问者来说是隐身的。
和传统 Apache 相比,Nginx 在并发连接处理上走的是事件驱动异步模型,内存占用小,高并发下的表现更加稳定。Apache 的模块化配置也有它的优势,但如果是新项目从零选型,Nginx 几乎是默认答案。简单总结一下:
| 对比维度 | Nginx | Apache | 应用内置静态文件服务 |
|---|---|---|---|
| 高并发静态请求能力 | 很强 | 一般 | 较差 |
| 配置复杂度 | 中等,清晰 | 较高 | 最低 |
| 反向代理能力 | 原生支持 | 需要模块 | 部分支持 |
| 与业务隔离程度 | 完全隔离 | 完全隔离 | 不隔离 |
| 典型应用场景 | 网关、静态、代理、负载 | 传统托管场景 | 开发调试、微型应用 |
2. 安装和配置结构:先把地基打好
2.1 不同环境里装 Nginx 的方式
安装这件事本身不复杂,常见就这几种方式,按需求选择。
Debian/Ubuntu 系,直接用包管理器:
sudo apt update sudo apt install nginx -y装完自带 systemd 管理脚本,直接启动并设置开机自启:
sudo systemctl enable nginx sudo systemctl start nginxRHEL/CentOS 9 及以下版本用的包管理器是 yum,配置文件默认路径略有不同,但整体结构一致。如果是 CentOS Stream 或 Rocky Linux,直接:
sudo dnf install nginx -y sudo systemctl enable --now nginx某些场景下,比如要编译进自定义模块、或者操作系统版本很老、包管理器里没有合适版本,可以用源码编译。这个步骤稍微繁琐,要装编译工具和依赖库,一般业务场景不太需要,建议优先用系统包管理器方案,维护升级都省事。
Docker 环境里部署也非常常见。一条命令拉起容器,把配置目录和站点目录挂载出来即可:
docker run -d \ --name nginx-server \ -p 80:80 \ -v /data/www:/usr/share/nginx/html \ -v /data/nginx/conf:/etc/nginx/conf.d \ nginx:1.26-alpine装完之后确认一下版本,顺手验证服务是否在跑:
nginx -v curl -I http://localhost能正常返回 HTTP 响应头,说明 Nginx 已经进入工作状态。
2.2 配置文件长什么样,区块怎么划分
Nginx 的配置,本质是一个层级化的指令集合。绝大多数发行版的配置文件入口都是/etc/nginx/nginx.conf,核心结构是三个块:main(全局配置)、events(事件处理模型)、http(HTTP 服务配置)。一个最小化的 nginx.conf 骨架长这样:
# main 区块:进程、日志、运行用户等全局设置 user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; # events 区块:每个 worker 进程能同时处理的连接数 events { worker_connections 1024; } # http 区块:真正跟站点业务相关的配置全在这里 http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; # 站点配置通过 include 引入,按域名/项目拆分文件 include /etc/nginx/conf.d/*.conf; }mime.types这个文件很多人容易忽略。它声明了文件扩展名和 Content-Type 的映射关系,不引入的话,浏览器很可能把 CSS 当成文本下载,或者页面样式完全丢失。配置里必须保留这一行 include。
站点文件建议不要全部堆在 nginx.conf 里面,而是按项目拆分。Ubuntu 的惯例是写在/etc/nginx/sites-available/里,然后软链到sites-enabled/;CentOS 系的惯例是直接放在/etc/nginx/conf.d/下。两种风格都可以,核心逻辑一致:一个 server 块对应一个站点项目,互相隔离,改一个不影响另一个。
2.3 改配置的正确姿势
Nginx 配置改完不能直接“重启大法好”,生产环境里这是大忌。一次systemctl restart nginx会让所有当前连接瞬间断开,如果是线上服务,等于主动制造两秒不可用。
正确姿势是:先校验配置有没有语法错误,再平滑重载。
nginx -t nginx -s reloadnginx -t会检查全部配置文件语法,如果输出syntax is ok,说明没问题。nginx -s reload做的事情是给 master 进程发送信号,让它用新配置重新拉起 worker 进程,同时优雅地回收旧连接,正在处理的请求不会被强行切断。
我见过太多新手在 VIM 里改完配置就直接service nginx restart,然后线上所有正在下载文件、正在管理等接口响应的用户全被踢掉。记住一个原则:平时操作只要不是大版本升级,永远用reload,别用restart。另外养成习惯,Nginx 配置文件备份成本极低,改动前复制一份到.bak,出了奇怪问题还能快速回滚。这是我踩过几次坑之后养成的肌肉记忆。
3. 静态资源部署:把网页稳稳地吐出去
3.1 第一个 server 块:让 IP 打开一个页面
安装完之后,Nginx 自带一个默认站点。打开浏览器输服务器 IP,能看到的欢迎页。接下来就把它替换成我们自己的项目页面。
在conf.d/里新建一个myweb.conf,写一个最小的静态站点配置:
server { listen 80; server_name www.example.com example.com; root /data/www/myweb; index index.html; location / { try_files $uri $uri/ =404; } }拆开解释一下:
listen 80:监听 80 端口。server_name:基于域名区分虚拟主机。可以写域名的多种形式,也可以直接写作_表示匹配所有未匹配到的请求。root:网站文件根目录,请求进来之后所有文件都在这个目录下查找。index:访问根路径时默认加载的文件。try_files:依次尝试访问$uri,找到就用;找不到再尝试目录下的默认文件;都没有就返回 404。这一套组合非常常用。$uri是 Nginx 变量,表示当前请求的文件路径。
比如用户访问http://www.example.com/,Nginx 就去/data/www/myweb/index.html找文件,找到后加合适的 Content-Type 返回。
3.2 生产环境里文件放在哪
静态文件不要直接放在/root目录下,也不建议扔在/home的用户主目录里。原因很实际:Nginx 的 worker 进程运行在一个低权限用户下,默认是nginx或者www-data,这些用户对/root这种目录天然没有访问权限,权限链条如果中间断了一环,就会看到 403。
我踩过一次很典型的坑:把整个项目解压到了/root/project,配置写成root /root/project;,访问任何路径都返回 403。排查了半天,才反应过来是/root目录本身权限是 700,Nginx 用户根本进不去。
生产环境推荐的目录规划长这样:
/data/www/myweb/ # 前端静态文件目录 /data/www/myweb/assets/ # 打包产物里有 hash 的静态文件 /var/log/nginx/ # Nginx 日志目录文件放进去之后,把归属权理顺:
sudo mkdir -p /data/www/myweb sudo chown -R nginx:nginx /data/www/myweb sudo chmod -R 755 /data/www/myweb这里注意,目录权限 755、文件权限 644 是安全且不会出问题的组合。运行用户对目录有读和执行权限即可,不必给写权限,除非你的业务需要它写文件。
3.3 缓存策略:让第一个人开荒,后面所有人吃现成
静态资源的特点是“在一定时间内内容不变”,所以很适合让浏览器缓存。expires指令就是干这个的:
location /assets/ { alias /data/www/myweb/assets/; expires 30d; add_header Cache-Control "public"; }配置完的效果是:浏览器第一次访问以后,30 天内再请求这些资源,直接从本地缓存读取,不再向服务器发起网络请求。对于图片、字体这类几乎不变化的资源,缓存时间可以放到更长,比如 90 天。
但要小心,CSS 和 JS 文件不能无脑长时间缓存。如果你的前端项目没有使用带 hash 的文件名打包,改了一行代码,用户那边却被本地缓存挡住了旧的版本,一直看到打不开的页面。解决方式有两种:
- 项目打包时开启内容哈希,比如
main.a1b2c3.js,文件内容变了哈希就变,此时可以放心设置超长缓存; - 项目没有 hash 文件名时,CSS/JS 的缓存时间控制在分钟级或者小时级,宁短勿长。
还有一种情况,明明改完配置已经reload了,但用户死咬着说“页面没更新”。不要怀疑 Nginx,先让用户在浏览器里强制刷新或者用无痕窗口。多数时候是浏览器缓存或者本地 CDN 缓存的问题,跟服务器配置没有关系。
3.4 顺手开启 Gzip 压缩
文本文件在网络传输时压缩一下,体积能减少 60% 以上。Nginx 的 gzip 模块默认就编译进去了,只需要打开开关。
gzip on; gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml; gzip_min_length 1k; gzip_comp_level 5; gzip_vary on;参数逐一说清楚:
gzip on:开启压缩。gzip_types:指定哪些 MIME 类型需要压缩。注意这里不需要写text/html,Nginx 默认就会压缩 HTML。gzip_min_length 1k:小于 1KB 的文件不压缩。压缩本身也有开销,特别小的文件压完可能体积更大,是负优化。gzip_comp_level 5:压缩级别,1 到 9。级别越高文件越小、CPU 消耗越高。5 是平衡点,我用 5 级别没有遇到过性能问题。调到 9 反而会让高流量场景下 CPU 压力变大,收益却非常有限。gzip_vary on:加Vary: Accept-Encoding请求头,处理带代理和 CDN 的环境时能避免缓存错乱。
图片(jpg/png/gif/webp)、视频、压缩包本身就是压缩过的二进制内容,就不用再压了。SVG 特殊一点,是文本格式,虽然扩展名是图片类,但压缩效果明显,值得放进 gzip_types。
4. 反向代理:让前端丝滑地调后端口
4.1 场景和最小可用配置
静态页面已经能访问了,页面里的按钮一点,要向后端发起接口请求。前端这个项目跑在 http://localhost 上,后端 API 监听在 8080。直接在浏览器里请求http://服务器IP:8080/api/user/list会出现两个问题:跨域被浏览器拦截;端口暴露给公网,既有安全风险又不优雅。
解决方案就是反向代理。在原来的 server 块里加一个 location:
server { listen 80; server_name www.example.com; root /data/www/myweb; index index.html; location /api/ { 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; } }这段配置的意思是:请求地址如果是http://www.example.com/api/user/list,Nginx 会把整个请求完整转发给http://127.0.0.1:8080,后端拿到的请求路径仍然是/api/user/list。前端代码只需要把请求统一发到/api开头,其他什么都不用管。
4.2 proxy_pass 末尾斜杠的坑,必须单独说
这个坑我见很多人掉进去过,而且一旦掉进去,问题表现得很隐蔽。区别就在proxy_pass最后有没有那个/。
# 写法一:不带斜杠 location /api/ { proxy_pass http://127.0.0.1:8080; } # 请求 /api/user/list -> 后端收到 /api/user/list(完整路径保留) # 写法二:带斜杠 location /api/ { proxy_pass http://127.0.0.1:8080/; } # 请求 /api/user/list -> 后端收到 /user/list(location 匹配部分被替换)是不是有点迷糊?列个对照表就清楚了:
| 请求路径 | proxy_pass 写法 | 后端真实收到路径 |
|---|---|---|
/api/user/list | http://127.0.0.1:8080 | /api/user/list |
/api/user/list | http://127.0.0.1:8080/ | /user/list |
/api/user/list | http://127.0.0.1:8080/prefix/ | /prefix/user/list |
规则记一句话:proxy_pass后面只要带了 URI(也就是/开头的部分),就会把 location 匹配到的那段路径替换掉;不带 URI,就完整保留原始路径。
如果你的后端接口设计是/api开头的,用写法一就行。如果后端接口自己要暴露为根路径,或者前端转发路径和后端实际路径不一致,才需要用到带斜杠的替换写法和rewrite配合。最怕的是项目里一会儿带斜杠一会儿不带,接口时通时不通,查起来特别费劲。我在配置里通常统一保持不带斜杠的写法,再用 rewrite 处理路径变更需求,减少认知负担。
4.3 必带三个 header,原因分别说明
反向代理转发时,Nginx 默认只转发基本请求信息,真实客户端 IP 会丢掉,后端看到的全是 Nginx 服务器地址。这就带来一堆问题,因此标准配置是下面四条:
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 $host:让后端知道用户访问的是哪个域名。有些应用生成自动跳转链接和 Cookie 域名时依赖这个头。X-Real-IP $remote_addr:直接用户真实 IP 传给后端。X-Forwarded-For:记录完整链路里每一层的 IP,多级代理时 XFF 会累积,后端日志里能看出真实来源。X-Forwarded-Proto:告诉后端用户用的是 HTTP 还是 HTTPS。应用里做 HTTPS 强制跳转或安全 Cookie 时依赖这个头。
做负载均衡的时候,如果后端要按 IP 做限流或者记录访问日志,这一组 header 不配全,后面接业务的同事大概率会来问你:“为什么日志里的 IP 全是 127.0.0.1?”
4.4 顺手解决跨域问题
前后端分离的项目,跨域几乎是标配问题。页面上在http://www.example.com,后端接口却要通过http://www.example.com/api访问。按照同源策略,只要协议、域名、端口有一个不一致,浏览器就会拦下来。
因为我们已经做了反向代理,浏览器以为请求的是同一个域名,跨域问题其实已经解决了一大半。只有在调用第三方接口,或者前端工程启动在另一个端口的开发阶段,才需要处理跨域。这时在 Nginx 转发段加响应头:
location /api/ { proxy_pass http://127.0.0.1:8080; add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "Authorization, Content-Type"; }带OPTIONS的预检请求比较特殊。浏览器在正式发送跨域请求前,会试探性地发一个OPTIONS请求问服务器“允许这么跨吗”,Nginx 经常直接把预检请求当成正常请求转发给后端,而后端接口往往没有处理 OPTIONS 方法的逻辑,导致预检直接 404。如果出现这种情况,在 Nginx 层单独终结它就行:
location /api/ { if ($request_method = OPTIONS) { return 204; } proxy_pass http://127.0.0.1:8080; }这个方法实测非常稳,预检请求被 Nginx 直接放行,后端只需要处理业务请求。
4.5 需要转发 WebSocket 时加这两行
现在很多项目有即时聊天、消息推送、协同编辑,这些功能在 Web 端基本上就是 WebSocket。WebSocket 和普通 HTTP 请求一样走 Nginx 代理,但要额外处理升级协议的问题。在反向代理块里加两个 header:
location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_http_version 1.1; proxy_read_timeout 3600s; }Upgrade和Connection "upgrade"这两个头告诉客户端和服务器全面切换到 WebSocket 协议。proxy_http_version 1.1也很关键,HTTP/1.0 不支持长连接。最后那个proxy_read_timeout,默认 60 秒就不回话会断连,聊天类需求必须调长,不然聊到一半连接就断了,前端自动重连时会有一堆麻烦。
5. 负载均衡:让多台后端一起扛流量
5.1 upstream 块与三种内置策略
单台后端扛不住流量了,就需要横向扩容,再加一两台服务器跑同样的后端代码。Nginx 把这组后端服务器定义成一个“服务池”,再用upstream块把所有节点聚合起来。
upstream backend_servers { server 192.168.1.11:8080 max_fails=2 fail_timeout=30s; server 192.168.1.12:8080 max_fails=2 fail_timeout=30s; server 192.168.1.13:8080 backup; } server { # ... location /api/ { proxy_pass http://backend_servers; 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_pass里的地址从http://127.0.0.1:8080换成了http://backend_servers,这个名称对应上面upstream定义的组名。请求到达之后,Nginx 会在池子里挑一台分出去。
Nginx 内置了四种负载均衡策略,生产环境常用的主要以下几种:
| 策略 | 配置方式 | 行为特征 | 适用场景 |
|---|---|---|---|
| 轮询 | 默认不写 | 请求按顺序轮流分配,每台权重一样 | 节点配置相同的通用场景 |
| 权重 | server x weight=3; | 按比例分配,weight 越高分到的越多 | 新老服务器混用,性能强的多扛流量 |
| 最小连接 | least_conn;指令 | 分给当前活动连接最少的节点 | 长连接或请求耗时不均的场景 |
| IP 哈希 | ip_hash;指令 | 同一来源 IP 固定到同一节点 | 依赖 Session 的旧架构 |
轮询最省心,适合所有后端实例性能接近的情况。权重适合混布架构,比如一台是 8 核 16G,一台是 4 核 8G,前者权重设为 3,后者设为 1,流量才能按真实容量均匀分配。最小连接适合处理时间波动大的接口,有请求在处理时就优先分给闲的节点。ip_hash比较特殊,它用客户端 IP 算哈希,让同一个用户每次都进同一台后端,这样 Session 可以保持在单机上。
没有绝对完美的策略,跟随业务模型调整。我一般先无脑轮询,观察一段时间监控数据,后端耗时曲线有明显波动再做策略调整。
5.2 Session 保持与 ip_hash 的局限
传统的服务端 Session 方案,默认就是存在单台服务器的内存里。负载均衡最发愁的事:用户第一次请求被分到 A 机器,登录状态存在 A 上;第二次请求被分到 B 机器,B 上没有这个 Session,用户就被踢回登录页了。
ip_hash能让同一个 IP 始终落在同一台机器,看起来解决了问题。但现实中这个方案的天花板很低:用户可能从不同 IP 访问(比如手机流量和 Wi-Fi 转换);某个节点挂掉时,原本落在它身上的请求全部被重新分配到其他节点,Session 照样失效。
真正工业化、没毛病的方案是中心化 Session 存储,把 Session 状态抽出来放到独立的 Redis 或 MySQL 里,所有后端节点共享这一份状态,任意节点都能处理任意用户的请求。Nginx 层只管把流量尽可能均匀地散出去,不需要费心“认人”。这个思路现在基本是标配了。如果你的项目已经升级到 JWT 这种无状态认证体系,后端集群天然不依赖节点亲缘性,那ip_hash基本可以完全不用,直接轮询最通透。
5.3 节点健康检查:你不主动测,Nginx 会默默帮你测
配置里那两个参数max_fails=2和fail_timeout=30s就是节点健康检查的核心。原理是 Nginx 会与后端节点保持 TCP 连接级别的探测,连续 2 次转发失败后,这个节点被标记为不可用,随后 30 秒内不再向它分发新请求。30 秒后 Nginx 会重新试探,如果恢复则重新纳入分发队列。
这是被动健康检查机制——平时不打招呼,出错了才“拉黑”。主动健康检查指的是 Nginx 定时周期性地访问某个健康检查地址,主动探测服务状态。这个功能在开源版里默认没有,只有商业版 Nginx Plus 才带。开源场景下的标准做法是:依赖被动检查,同时配合外部监控系统对后端集群做探活,比如拨测某台机器的/health接口,机器异常就从负载池里摘除,恢复后再放进去。外部监控加被动检查组合起来,覆盖绝大多数场景已经足够。
5.4 动静分离:让每一项资源处理在最擅长的层级
负载均衡不止可以按流量分,也可以按内容类型分。这就是“动静分离”的思路。
前端页面、图片、CSS、JS 这些静态内容,由 Nginx 直接从磁盘返回,不往后端转发,后端只承担/api这类动态请求。配置写起来并不复杂:
server { listen 80; server_name www.example.com; root /data/www/myweb; index index.html; # 静态资源直接由 Nginx 返回,设置缓存 location /assets/ { expires 30d; add_header Cache-Control "public"; } # 动态请求转发给后端负载均衡池 location /api/ { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }动静分离和负载均衡是两种互补的手段。前者按请求类型分流,后者按后端节点分摊。实践中通常一起使用。后端业务只处理纯逻辑,吞吐量要求和后端机器配置都可控可评估,整体架构的稳定性明显提升。运维同学最直观的感受是:后端监控里 CPU 曲线变得非常干净,流量高峰也不会看到密密麻麻的静态请求挂在那里。
6. 顺手调优与安全加固
6.1 全局性能参数怎么设置
Nginx 官方推荐每个 CPU 核心对应一个 worker 进程,这样多进程可以充分利用多核。现在写worker_processes auto;就能自动识别核数,不需要手动数。如果你的服务器是 8 核,auto 会自动设为 8。太多或太少都不好:太少 CPU 跑不满,太多会平白消耗内存和切换开销。
worker_connections控制每个 worker 最多同时保持 1024 个连接。这个数值不是越大越好,它受限于系统文件描述符上限。理论最大并发连接数是worker_processes * worker_connections,但还要考虑每个连接本身占用的文件描述符和内存资源。常见的温和配置是:
worker_processes auto; events { worker_connections 2048; }如果服务器资源充足,2048 是安全且够用的数值。除非你明确知道自己在干什么,否则不建议盲目堆到 65535。
6.2 超时参数也要管一管
反向代理场景中,Nginx 与后端之间的连接存在各种超时时间。业务慢,不代表 Nginx 有问题,很多时候就是超时时间卡得太短,后端在写一条慢 SQL,Nginx 已经断开了连接。
proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s;proxy_connect_timeout:建立与后端的 TCP 连接超时时间,内网环境 3 到 5 秒足够。proxy_read_timeout:从后端读取响应的超时时间。后端接口本身处理时长超过 60 秒的场景需要长取值,比如文件导出、批量计算。如果用户上传大文件后需要一个较长的处理周期,这个值要相应放宽。proxy_send_timeout:把请求发送给后端的超时时间,常规场景 60 秒足够。
调试阶段可以先全部放宽,等业务稳定后逐步收紧。一开始就把超时压得很死,会让排查问题非常痛苦,因为错误日志里清一色全是 timeout,难以区分是网络问题、程序问题还是配置问题。
6.3 几个低成本高收益的安全项
不需要复杂的防火墙规则,先做这几点,绝大多数基础攻击就能挡住大半。
隐藏版本号:
server_tokens off;这会让 Nginx 错误页和响应头里不再出现具体版本号,避免对方搜到对应版本的已知漏洞去找攻击思路。
补充安全响应头:
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;X-Frame-Options防止页面被嵌进别人的 iframe 里做点击劫持。X-Content-Type-Options能防一些浏览器勤快做的 MIME 嗅探。Referrer-Policy控制跨域跳转时 Referrer 头泄露的信息量。这三行配上后页面行为不会有任何明显变化,但安全性会好一个层级。
限制上传体大小:
client_max_body_size 20m;不配的话默认是 1m,用户上传超过 1MB 的文件会直接被 Nginx 拒绝。反过来如果业务根本没有上传需求,或者只提交表单,保持默认的 1m 反而是一种保护。
对接口做基本限流:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://backend_servers; }这个配置的含义是:每个客户端 IP 每秒可以请求 10 次,突发可以到 30 次但不排队。防攻击效果谈不上固若金汤,但能把大多数脚本刷接口的行为按在地上摩擦一下。$binary_remote_addr是按 IP 做维度计数,省内存,IPv4 的 IP 只占固定字节。10m 的共享内存空间足够保存十几万个 IP 的状态,普通业务绰绰有余。
7. 部署之后,这些坑你大概率会遇到
7.1 502 / 504 该怎么排查
502 Bad Gateway 是反向代理场景里最常见的报错,含义是:Nginx 转发失败了,它没能从后端拿到有效响应。原因分成了几大类。排查顺序从轻到重:
先看后端进程本身有没有活着。Java 服务崩了、Node 进程退出了,Nginx 再急也没用。ps aux | grep java看一眼。接着看端口监听状态:ss -lntp | grep 8080,如果后端监听在 127.0.0.1,而 Nginx 反代写的是服务器内网 IP,也会连不上。然后看本地能不能通:curl http://127.0.0.1:8080/health,后端进程在自己机器上是否能正常响应。最后看防火墙和安全组,云服务器的安全组规则没放行后端端口、或者本机 iptables 配置错误,都会导致反复 502。
常见的 Nginx 错误日志记录格式长这样:
[error] 12345#0: *678 connect() failed (111: Connection refused) while connecting to upstream111 Connection refused通常是端口没监听;110 Connection timed out通常是防火墙规则拦截或者后端挂死。
504 Gateway Timeout 的含义是:连接上了,但后端在超时时间内没有返回结果。优先检查后端接口自身耗时,再看proxy_read_timeout是否偏短。我处理过一个典型的案例:导出报表接口正常要跑一分半,默认 60 秒的读取超时直接把下载请求断掉,前端展示为 504。把超时设置调到 300 秒后问题消失。如果后端是因为死循环或被锁阻塞,调超时就不治本,只能作为临时手段,重点还是要盯后端日志。
7.2 404 / 403 的常见猫腻
静态文件 404 和 403 很多时候不是文件真的不存在,而是路径对不上。
root与alias的区别,是所有新手绕不过去的弯。root会完整拼接 location 路径,比如:
location /img/ { root /data/www; } # 请求 /img/a.png -> 实际读取 /data/www/img/a.png而alias是替换,比如:
location /img/ { alias /data/www/pictures/; } # 请求 /img/a.png -> 实际读取 /data/www/pictures/a.png配错了root和alias,表现就是文件明明存在但 404。这种问题特征非常明显,一查路径拼接就能发现问题。
前端部署了 Vue / React 的 history 路由时,直接刷新某个子路由页面会出现 404,这是因为 Nginx 在磁盘上找不到对应的真实文件。路由是前端的事,服务端只需要做一次兜底:
location / { try_files $uri $uri/ /index.html; }这样任何不存在的路径都回到index.html,由前端路由自行渲染。注意这个配置要和静态资源缓存合理共处,不要把/assets/也兜回 index。
403 更多是权限问题。Nginx 工作进程用户对文件没有读权限、对目录没有搜索权限,最常见的原因是文件放在了/root或者/home/用户下。确认一下nginx用户对目录链路上每一级目录都有x权限。如果使用了 SELinux,还要检查安全上下文是否放行,ls -Z看文件类型是否为httpd_sys_content_t,不是就执行chcon -t httpd_sys_content_t /data/www修正。
7.3 静态资源不更新 / 页面样式突然全乱
页面样式突然全乱,排查优先看开发者工具里 CSS 文件是否带出了不正确的 Content-Type。如果是text/plain,多半是mime.types没有 include 进来,或者自定义 location 时把默认的 mime 处理覆盖掉了。Nginx 配置里include /etc/nginx/mime.types必须存在,这行删了,所有样式文件都会被浏览器当成纯文本,页面直接就“裸奔”了。
资源不更新上面第三章提到过,是缓存策略的问题。如果改了代码但浏览器一直拿旧版,第一件事检查资源请求响应头里的Cache-Control和Expires。项目没有 hash 文件名时,需要让 CSS/JS 的缓存时间设短;有 hash 文件名时,超长缓存反而是最优策略。
还有一种隐蔽情况:你定的缓存策略没问题,但是中间套了一层 CDN,CDN 节点缓存了旧文件。这时 Nginx 层挟持无力了,需要去 CDN 控制台做缓存刷新或配置规则调整。前端打包时的版本号参数或者加 query 参数,是处理这种场景最简单粗暴却有效的偏方。
7.4 日志是排查问题的第一现场
配置再多问题,最终都能在日志里找到线索。access.log是访问日志,记录每一次请求的 IP、时间、路径、状态码、耗时。error.log是错误日志,记录 Nginx 运行过程中发生的连接错误、超时、权限问题等。这两者配合,排查问题效率至少翻一倍。
默认日志路径在不同系统略有差异,Ubuntu 通常在/var/log/nginx/access.log和/var/log/nginx/error.log。实时跟踪看哪个接口报错:
tail -f /var/log/nginx/access.log | grep 502 tail -f /var/log/nginx/error.log很多问题如果只盯着状态码猜,像大海捞针。比如前后端接口联调失败,前端说“接口 404”,后端说“我没收到请求”,这时候去看 Nginx 的 access.log,一条请求记录就会告诉你 Nginx 把请求转发到哪了、结果是什么,双方职责清清楚楚。
为了方便排查链路真实来源,自定义日志格式是很有价值的。在 http 块里定义:
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 /var/log/nginx/access.log main;这样日志里能看到客户端真实 IP 和 XFF。尤其负载均衡多级代理之后,$remote_addr可能只是上一级 Nginx 的地址,$http_x_forwarded_for里才有用户最初的真身。
7.5 常用命令速查
最后整理一份干活时经常敲的命令,直接复制使用:
| 用途 | 命令 |
|---|---|
| 测试配置语法 | nginx -t |
| 平滑重载配置 | nginx -s reload |
| 查看运行状态 | systemctl status nginx |
| 查看端口监听 | ss -lntp | grep nginx |
| 实时跟踪错误日志 | tail -f /var/log/nginx/error.log |
| 实时跟踪访问日志并按 5xx 过滤 | tail -f /var/log/nginx/access.log | grep " 50" |
| 下载文件测转发效率 | curl -I http://example.com/file.iso |
| 模拟客户端看响应头 | curl -I -H "Accept-Encoding: gzip" http://example.com/ |
最后分享一个我个人的习惯:每配置完一台新服务器的 Nginx,第一时间打开 error log,然后人为制造一个错误,比如访问一个不存在的路径,再回去看日志文件里有没有对应记录。这个步骤花不到一分钟,但能帮你确认日志路径正确、日志权限正常、搜索引擎能匹配到问题。很多线上事故所谓“查不出原因”,本质上都是日志链路没有提前验证好,到了真正需要日志救命的时候才发现写不进去或者路径不对。把这一步做扎实了,Nginx 这条线就算稳了。后续深入的话,还可以继续研究 rewrite 规则、HTTP/2 / HTTP/3、TLS 证书自动续期、灰度发布、基于 njs 做请求改造这些方向,每一块展开都是独立的长文,到时候再接着聊。