Nginx 这东西,刚接触运维和后端开发那会儿,我对它的认知就停留在“装完扔那儿,配个 proxy_pass 就完事”的阶段。直到有一次线上服务在晚高峰被压垮,排查下来发现瓶颈根本不在应用代码,而是入口层配置得稀烂——连接数打满、静态资源全走动态转发、后端节点挂了一个还在傻傻往里灌流量。那次之后我才老老实实把 Nginx 从安装到反向代理再到负载均衡这条链路重新啃了一遍。这篇内容就是把我踩过的坑、验证过的配置和那些文档里不会明说的细节整理出来,适合刚上手 Nginx 的朋友,也适合已经会用但想搞清楚“为什么这么配”的同行。整篇围绕安装配置、核心指令、反向代理、负载均衡实战四个大块展开,每个环节都会讲清楚选型理由和实测心得,配置片段可以直接抄去改。
1. 安装方式的选择与初始环境确认
1.1 为什么我不推荐一上来就源码编译
很多人学 Nginx 的第一反应是去官网下 tar.gz 源码包编译安装,觉得这样“可控”。我早期也这么干过,结果每次升级都要重新编译,模块依赖一多,编译参数记都记不住。对于绝大多数业务场景,包管理器安装才是更务实的选择:系统源里的版本经过发行版维护者测试,升级路径清晰,systemd 服务单元也帮你写好了。源码编译真正有价值的场景只有两个——需要打入第三方模块(比如某些流媒体或安全模块),或者需要针对特定 CPU 架构做极致优化。除此之外,用 apt 或 yum 装完就能跑,省下来的时间拿去调配置更划算。
以常见的 Debian/Ubuntu 系为例,安装前先确认系统版本和已占用的端口,避免装完发现 80 端口被别的服务占了:
# 确认系统版本 cat /etc/os-release # 检查 80/443 端口占用情况 ss -tlnp | grep -E ':80|:443' # 更新源并安装 sudo apt update sudo apt install -y nginx # 确认版本与服务状态 nginx -v systemctl status nginx装完之后nginx -v输出的版本号要记一下,后面排查兼容性问题时经常要用。CentOS/RHEL 系则是yum install nginx或dnf install nginx,注意有些老版本系统默认源里没有 Nginx,需要先启用 EPEL 源。这里有个细节:装完先别急着改配置,用默认配置启动一次,浏览器访问服务器 IP 看到欢迎页,说明基础链路是通的,再动手改。这个“先验证基线”的习惯能帮你把“装坏了”和“配错了”两类问题分开。
1.2 目录结构与配置文件的分层逻辑
Nginx 的配置目录结构是理解它的第一道门槛。包管理器安装后,主配置通常在/etc/nginx/nginx.conf,而站点配置一般放在/etc/nginx/conf.d/或/etc/nginx/sites-available/下。这个分层不是随便设计的:主配置管全局(worker 进程数、日志格式、连接数上限),站点配置管单个虚拟主机。我见过不少人把所有 server 块全塞进 nginx.conf,几百行堆在一起,改一个站点要翻半天,还容易误删别的配置。
正确的做法是利用include指令做模块化。主配置里保留一行include /etc/nginx/conf.d/*.conf;,每个站点一个独立文件,文件名带上前缀方便排序,比如10-api.conf、20-static.conf。这样增删站点就是增删文件,回滚也简单——出问题把对应文件挪走重载即可。下面是一个典型的主配置骨架,重点看 worker 相关参数:
worker_processes auto; worker_connections 1024; events { use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; }worker_processes auto让 Nginx 自动按 CPU 核心数起进程,这是现代版本的推荐值,不用再手动数核。worker_connections是单个 worker 能处理的最大连接数,理论总连接上限是 worker_processes 乘以 worker_connections,但实际还要受文件描述符限制,后面会专门讲。use epoll在 Linux 上是默认的高效事件模型,显式写出来是为了让配置意图更清晰。
1.3 装完必须调的两个系统级参数
这一步是新手最容易漏、但影响最大的。Nginx 作为高并发入口,会打开大量文件描述符和连接,而操作系统默认的限制往往很低。我遇到过服务跑着跑着报worker_connections are not enough或者too many open files,根因就是系统级限制没放开。
第一个是文件描述符上限。查看当前值用ulimit -n,很多系统默认只有 1024。修改/etc/security/limits.conf,加上:
* soft nofile 65535 * hard nofile 65535改完要重新登录或重启会话才生效,验证时用ulimit -n确认。第二个是 Nginx 自身的worker_rlimit_nofile,在主配置里显式声明,让它和系统限制对齐:
worker_rlimit_nofile 65535;这两个值的关系要理清:系统限制是“天花板”,Nginx 的配置是“自己申请用多少”,后者不能超过前者,否则启动会告警。我一般把两者设成一致,省得算来算去。另外worker_connections乘以 worker 进程数不要超过worker_rlimit_nofile太多,否则连接数上限会被文件描述符卡住,配了也白配。这些参数看着琐碎,但它们是高并发场景的地基,地基不牢,后面反向代理和负载均衡配得再花哨也扛不住量。
2. 核心指令背后的运行机制
2.1 location 匹配优先级:一个被低估的排错重点
反向代理配不对,十有八九是 location 匹配规则没吃透。Nginx 的 location 匹配有一套明确的优先级顺序,从高到低大致是:精确匹配=、前缀匹配^~、正则匹配~和~*、普通前缀匹配。很多人写了个正则 location 却发现没生效,就是因为前面有个更优先的规则先命中了。
举个实际例子,静态资源和 API 混在一个域名下:
location = /health { return 200 'ok'; } location ^~ /static/ { root /var/www; expires 7d; } location ~* \.(jpg|png|css|js)$ { root /var/www; expires 30d; } location /api/ { proxy_pass http://backend; }= /health是精确匹配,只命中这一个路径,适合健康检查接口,开销最小。^~ /static/一旦匹配就停止后续正则检查,适合确定要走静态文件的目录。正则~*不区分大小写,用来兜底各类静态资源后缀。最后location /api/是普通前缀,优先级最低,作为动态请求的入口。理解这个顺序,你就能预判一个请求到底会落到哪个块里,而不是靠试。
提示:调试 location 匹配时,可以在对应块里临时加
add_header X-Matched-Location "static";,用浏览器开发者工具看响应头,比反复改配置猜要快得多。
2.2 root 与 alias 的差异,以及那个经典的斜杠问题
root和alias的区别是 Nginx 配置里的经典坑。root是把 location 的路径拼接到指定目录后面,alias则是直接用指定目录替换 location 路径。假设配置是location /img/ { root /data; },请求/img/a.png实际找的是/data/img/a.png;如果换成alias /data/;,找的就是/data/a.png。差别就在于是“拼接”还是“替换”。
更隐蔽的是 alias 结尾斜杠的问题。location /img/ { alias /data; }这种写法,请求/img/a.png会被解析成/dataa.png,因为 location 的/img/被替换成/data,剩下的a.png直接接上去,中间少了个斜杠。这个错误不会报配置语法错,只会返回 404,排查起来很费劲。我的习惯是:用 alias 时,location 和 alias 的值要么都带结尾斜杠,要么都不带,保持一致。实际项目里我倾向于用 root 加合理的目录结构,alias 只在路径映射确实对不上时才用,少一个变量少一类 bug。
2.3 日志格式定制与切割策略
默认的 combined 日志格式信息够用但不够细,排查性能问题时缺关键字段。我通常自定义一个格式,把上游响应时间、请求耗时、上游地址都带上:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" rt=$request_time ' 'urt=$upstream_response_time up=$upstream_addr'; access_log /var/log/nginx/access.log main;$request_time是 Nginx 从收到请求到发出响应的总耗时,$upstream_response_time是后端处理耗时,两者一减就能判断慢在入口还是慢在后端。$upstream_addr记录实际转到了哪个后端节点,负载均衡排查时特别有用——哪个节点慢、哪个节点被转得多,日志里一目了然。
日志切割别指望 Nginx 自己干,它不会自动轮转。标准做法是配 logrotate,在/etc/logrotate.d/nginx里写规则,每天切割、保留 14 天、切割后给 Nginx 发 USR1 信号让它重新打开日志文件:
/var/log/nginx/*.log { daily rotate 14 compress missingok notifempty sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid) endscript }这里的关键是kill -USR1,它通知 Nginx 重新打开日志文件句柄,这样切割后的新文件才能被写入。如果只切割不发信号,Nginx 还握着旧文件的句柄,新日志会写进已经被重命名的旧文件里,看起来就像“日志不更新了”。这个坑我踩过一次,排查了半天才发现是 logrotate 配置漏了 postrotate。
3. 反向代理:从能用到用得明白
3.1 proxy_pass 带不带斜杠,结果完全不同
反向代理最核心的指令就是proxy_pass,而它最容易出错的地方是结尾斜杠。规则可以这样记:proxy_pass 的值带 URI(哪怕只是一个斜杠),就会把 location 匹配到的部分替换掉;不带 URI,就原样透传。
看两组对比:
# 情况一:不带斜杠,原样透传 location /api/ { proxy_pass http://backend; } # 请求 /api/user 转发到后端仍是 /api/user # 情况二:带斜杠,替换掉 location 部分 location /api/ { proxy_pass http://backend/; } # 请求 /api/user 转发到后端变成 /user这个差异直接影响后端能不能收到正确路径。我见过前端调/api/user一直 404,后端日志里却是/user,就是斜杠惹的祸。选哪种取决于后端路由设计:如果后端服务本身就带/api前缀,用不带斜杠的透传;如果后端是纯业务路径、前缀只是网关层加的,就用带斜杠的替换。定下来之后写进团队规范,别让每个人凭感觉写。
3.2 转发请求头:那些不传就会出事的字段
反向代理默认只转发部分请求头,有些关键信息必须手动补。最典型的是Host头,不设置的话后端收到的是 proxy_pass 里的主机名,而不是用户实际访问的域名,导致后端生成绝对链接、做域名校验时出错。标准配置是:
location / { proxy_pass http://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; }$host是用户请求的域名,X-Real-IP是客户端真实 IP,X-Forwarded-For是代理链路上的 IP 列表($proxy_add_x_forwarded_for会在已有值后追加当前客户端 IP),X-Forwarded-Proto告诉后端原始请求是 http 还是 https。后端做限流、审计、生成回调地址时全靠这几个头。少了X-Forwarded-Proto,后端在 https 站点上生成的链接可能是 http 的,浏览器直接拦截,这种问题排查起来很绕。
注意:
X-Forwarded-For是可以被客户端伪造的,如果后端用它做安全判断,必须在最外层代理上覆盖而不是追加,或者只信任可信代理传来的值。这个细节涉及安全边界,配置时要想清楚信任链。
3.3 超时与缓冲:让代理行为可控
反向代理的默认超时对很多业务来说偏短,后端处理稍慢就报 504。三个关键超时参数要按业务调整:
proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 60s;proxy_connect_timeout是和后端建立连接的超时,一般设短一点(3 到 5 秒),后端挂了能快速失败。proxy_read_timeout是等待后端响应的超时,要按最慢接口来定,报表导出这类接口可能要设到 60 秒甚至更长。设太短会误杀正常慢请求,设太长会让故障节点拖住连接。我的经验是先看监控里接口的 P99 耗时,在这个基础上留一倍余量。
缓冲相关的proxy_buffering默认是开的,Nginx 会先把后端响应缓冲到内存或临时文件再发给客户端,好处是释放后端连接更快。但对于流式响应(比如服务端推送、大文件下载),缓冲反而会增加延迟,这时候要关掉:
location /stream/ { proxy_pass http://backend; proxy_buffering off; }判断标准很简单:响应是“一次性生成完再发”还是“边生成边发”。前者开缓冲,后者关缓冲。这个选择直接影响用户体验,别用默认值一把梭。
4. 负载均衡实战:策略选择与健康检查
4.1 upstream 块的四种调度策略怎么选
负载均衡靠upstream块定义后端节点池,调度策略有几种,选错了会出现“节点负载不均”或“会话丢失”的问题。
默认是轮询(round-robin),请求依次分发,适合后端无状态、性能相近的场景。加权轮询通过weight参数给性能强的节点多分流量:
upstream backend { server 10.0.0.11:8080 weight=3; server 10.0.0.12:8080 weight=1; }上面配置里第一个节点拿 3/4 的流量,适合新旧机器混部、配置不对等的过渡期。ip_hash按客户端 IP 哈希固定到同一节点,解决会话保持问题,但缺点是节点增减时大量客户端会被重新分配,且 NAT 环境下大量用户共享 IP 会导致倾斜。least_conn把请求发给当前连接数最少的节点,适合请求耗时差异大的场景,长连接多的服务用它比轮询更均衡。
我的选型逻辑是:无状态服务用轮询或 least_conn;有会话且暂时没做集中式会话存储的用 ip_hash 过渡;机器性能不均的叠加 weight。等业务上了统一的会话存储,就果断去掉 ip_hash,回到更均衡的策略。
4.2 被动健康检查与节点摘除
开源版 Nginx 没有主动健康检查(那是商业版特性),但通过max_fails和fail_timeout能实现被动摘除:
upstream 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; server 10.0.0.13:8080 backup; }含义是:某节点在fail_timeout时间内失败达到max_fails次,就被标记为不可用,暂停fail_timeout时长后再试探性放流量。backup标记的节点平时不接流量,主节点全挂时才顶上,适合做兜底。这套机制的问题在于“被动”——要等真实请求失败了才摘除,故障瞬间会有少量请求打到坏节点。对可用性要求高的场景,要么上商业版,要么在应用层做健康检查配合动态 upstream 更新,开源版这套被动机制作为基础防线够用,但别指望它零感知。
4.3 长连接复用与 upstream keepalive
默认情况下 Nginx 到后端的连接是短连接,每个请求建一次连,高并发下 TIME_WAIT 堆积、握手开销都不小。开启 upstream 长连接能明显降低后端压力:
upstream backend { server 10.0.0.11:8080; server 10.0.0.12:8080; keepalive 32; } location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; }三个要点缺一不可:keepalive 32声明每个 worker 保持的空闲长连接数;proxy_http_version 1.1因为 HTTP/1.0 不支持长连接;proxy_set_header Connection ""清空 Connection 头,否则默认的close会让长连接失效。这三行我见过太多人只配了第一行,结果长连接根本没生效,白忙一场。实测下来开启后后端收到的连接建立次数能降一个数量级,对连接数敏感的服务收益很明显。
4.4 一个完整的实战配置骨架
把前面的点串起来,给一个可直接参考的完整配置:
upstream 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.internal; location ^~ /static/ { root /var/www; expires 7d; access_log off; } location /api/ { 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_read_timeout 60s; } }改完配置别直接 reload,先nginx -t做语法检查,通过后再systemctl reload nginx。reload 是平滑重载,老 worker 处理完存量请求才退出,不会断连接;restart 则会中断,生产环境优先用 reload。这个习惯能避免“改错一个字符导致整个入口挂掉”的事故。
5. 几个真实踩过的坑与排查思路
5.1 502 与 504 的区分定位
502 是后端返回了无效响应或连接被拒,504 是等后端超时。两者排查方向完全不同:502 先看后端进程是否活着、端口是否在听、upstream 地址有没有写错;504 先看后端处理是否真的慢、proxy_read_timeout是否设得太短。我习惯先看错误日志里的upstream_addr和upstream_status,能快速定位是哪个节点、什么状态。有一次 502 查了半天,最后发现是 upstream 里写了个已经下线的旧 IP,请求轮询到它就失败,日志里upstream_addr直接暴露了问题。
5.2 配置改了不生效的三个常见原因
第一,改的文件没被 include 进去,主配置里根本没有引用它。第二,改了但没 reload,Nginx 还在用旧配置跑。第三,多个 server 块server_name冲突,请求落到了另一个块。排查时用nginx -T(大写 T)输出完整生效配置,比看单个文件靠谱,它能告诉你最终合并后的配置长什么样,include 了哪些文件一目了然。
5.3 静态资源 404 的路径核对法
静态资源 404 优先核对 root/alias 的拼接结果。方法是在配置里临时打开error_log的 debug 级别,或者直接根据规则手算实际文件路径,再去服务器上ls确认文件在不在。我一般先确认“Nginx 认为的路径”和“文件实际路径”是否一致,不一致就是 root/alias 或斜杠的问题,一致但还 404 就是权限问题——Nginx 的 worker 进程用户(通常是 www-data 或 nginx)对文件目录要有读和执行权限,父目录缺执行权限也会导致访问不到。
6. 我个人在实际操作中的几点体会
配置 Nginx 这件事,最怕的不是不会写,而是“照着抄了能跑,出了问题不知道从哪查”。我现在的习惯是每加一条关键指令都问自己一句:这条指令解决什么问题,去掉会怎样。比如proxy_set_header Host $host去掉后后端拿到的 Host 是什么,keepalive那三行少一行会怎样,想清楚了再上线,排查时才有方向。
另外,配置一定要进版本管理。Nginx 配置文件就是代码,改之前提交、改之后对比,出问题能秒回滚。我见过太多人手改配置不留底,改崩了只能凭记忆恢复。还有个小技巧:把nginx -t和 reload 包成一个脚本,改完跑一下,语法不过直接拦住,比手动敲命令稳。最后,监控要跟上——access log 里的$upstream_response_time和错误日志里的 upstream 状态,是负载均衡和后端健康最直接的信号,别等用户报障了才去看。