nginx配置,一个看起来简单到不行的话题——默认装完就能跑,改两行就能转发,网上教程一抓一大把。可真等到自己上手,从“能跑”到“跑得稳”再到“上线不被运维骂”,中间隔着的坑比想象中多得多。我在生产环境维护过不少nginx实例,也帮同事排查过各种诡异问题,这篇就把这些实操经验整理出来,从安装方式选型、配置文件结构、反向代理与负载均衡,到平滑升级和问题排查,一条线走完。不管你是刚接触nginx的新手,还是已经被502和403折磨过的准老手,这篇文章都值得你花十分钟看完。
1. 环境准备与安装路线:选对方式等于成功一半
1.1 先搞明白:包管理器安装与编译安装怎么选
很多人第一步就栽在安装方式上。以CentOS、AlmaLinux这类系统为例,yum install nginx一条命令搞定,听起来很省事。但包管理器装出来的版本往往偏旧,而且模块是固定的,你想加个http_sub_module或者http_v2_module,默认包不一定带。我在AlmaLinux 9上就遇到过默认仓库nginx版本老旧、HTTP/2支持不完整的情况,最后老老实实走了编译安装。
反过来,编译安装也不是万能药。它浪费时间,依赖关系复杂,还要自己处理systemd服务文件。我的建议是:如果只是本地调试、快速验证,用包管理器;如果是生产环境、需要特定模块或版本、或者跑在aarch64这类特殊架构上,直接编译安装。尤其像“aarch64纯内网安装nginx”这种场景,离线环境根本没法用yum,只能靠源码包在本地编译,这也是编译安装无法被替代的原因之一。
1.2 编译安装的完整流程与参数拆解
源码编译nginx其实不复杂,但有几个参数值得单独拿出来说。先看基础流程:
# 下载源码包,建议去nginx官网找最新稳定版 wget https://nginx.org/download/nginx-1.26.3.tar.gz tar -zxvf nginx-1.26.3.tar.gz cd nginx-1.26.3 # 安装依赖(不同发行版命令略有差异) yum install -y gcc pcre-devel zlib-devel openssl-devel # 配置编译选项 ./configure --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-stream # 编译并安装 make -j$(nproc) && make install--prefix指定安装路径,建议单独放/usr/local/nginx,别和系统目录混在一起,升级、回滚都方便。--with-stream是做TCP/UDP四层代理用的,如果你有负载均衡需求建议直接加上,后面想加就得重新编译。--with-http_realip_module在nginx放在反向代理后面时特别有用,否则拿不到真实客户端IP,排障时一脸懵。
编译完别忘了验证一下版本和模块:
/usr/local/nginx/sbin/nginx -V输出的configure arguments会完整列出编译参数,后期加模块时对照着看就能知道自己缺了什么。
2. 配置文件的核心骨架:先别急着抄网上的“万能配置”
2.1 main、events、http三个块,先搞清各自的职责
nginx.conf默认结构是main(全局块)、events块、http块三层,不少新手一上来就复制几百行的“全能配置”,结果出了问题根本不知道去哪找。我建议先理解每层是干什么的:main块设置进程级参数,比如worker进程数、PID路径;events块专门管连接处理模型;http块则包含所有的server、location配置,以及gzip、日志等全局HTTP参数。
很多人在http块里同时写了多个server,但没有注意server_name的匹配规则,导致访问A域名时走了B域名的配置。这个坑我踩过不止一次。nginx匹配server_name时,优先匹配精确域名,其次是通配符(*.example.com),最后才是正则表达式。如果你的配置里两个server都声明了listen 80,但一个写的是server_name example.com,另一个写的是server_name _,那访问example.com时永远走第一个,第二个只作为兜底。这一点在面试中也是常客,弄懂了就不会被绕晕。
2.2 几个关键参数背后的计算逻辑
worker_processes和worker_connections这两个参数,几乎是每台nginx服务器必调的。理论上worker_processes设成CPU核心数就够了,但在实际压测中我发现,如果你跑的是HTTPS,加解密极耗CPU,这时按核心数设置反而合理;如果跑的是纯静态或反向代理,IO瓶颈不在CPU,可以适当调成auto让nginx自己判断。
worker_connections决定每个worker进程能同时保持多少连接,默认值是1024,对高并发场景来说远远不够。这里有个计算公式:最大并发连接数 = worker_processes × worker_connections。比如4个worker、每个1024连接,理论上就是4096并发。但注意,这个值还要受系统文件描述符限制,需要同时调大ulimit的nofile,否则nginx会在连接数上来之后直接报“too many open files”。
还有keepalive_timeout,默认75秒在大多数业务场景下偏长,浪费连接资源。我习惯设成10到30秒,配合上游服务器的keepalive参数一起调。像下面这样:
keepalive_timeout 30; keepalive_requests 1000;keepalive_requests表示一个长连接最多复用多少次就断开,目的是防止某些客户端长期占用连接不释放,尤其是内网环境里有大量定时请求时,这个参数能有效防止连接堆积。
3. 静态资源与反向代理:一篇文章吃透location和proxy_pass
3.1 location匹配规则,九成配置坑都出在这里
location是nginx配置里最核心、也最容易被忽视的部分。它的匹配规则分四类:
location = /path:精确匹配,优先级最高;location ^~ /path:前缀匹配,匹配后不再检查正则;location ~ /path和location ~* /path:正则匹配,区分大小写和不区分大小写;location /path:普通前缀匹配,优先级最低。
很多人以为location从上往下匹配、先写哪个就先用哪个,这个理解是错的。nginx实际是先收集所有前缀匹配,记住最长的那个;然后按顺序检查正则,一旦命中正则就采用正则结果;如果没有任何正则命中,才使用之前记住的最长前缀。而^~的作用就是告诉nginx:“这儿已经匹配上了,别再看正则了”。
举个例子,一个常见需求是拦截所有以.php结尾的请求并转发给PHP-FPM,但同时也想对/static/开头的路径直接返回静态文件。如果写成:
location ~ \.php$ { proxy_pass http://php_backend; } location /static/ { alias /data/static/; }当请求是/static/index.php时,正则\.php$会命中,所以它会走PHP转发,而不是返回静态目录下的index.php。这就是正则优先级高于普通前缀导致的“诡异现象”。如果想让/static/下的所有请求都走静态文件,必须加上^~:
location ^~ /static/ { alias /data/static/; }3.2 root与alias的迷惑行为,一个真实案例
root和alias的区别,几乎每个nginx教程都会讲,但真正用的时候还是会绕晕。简单说,root会将完整URI拼接到指定路径后面,alias则会把location匹配的部分替换掉。比如:
location /api/ { root /var/www/html; }请求/api/user时,nginx找的文件是/var/www/html/api/user,URI里的/api被原样保留。而如果改成alias:
location /api/ { alias /var/www/html/; }请求/api/user时,nginx找的文件是/var/www/html/user,/api前缀被替换成/var/www/html/了。
我曾经给一个前端项目做静态资源部署,页面引用的JS在/js/app.js,而文件实际放在/srv/frontend/dist/js/app.js,我一开始用root写成了root /srv/frontend/dist,结果页面404了一下午。后来换成alias才解决。所以我的经验是:如果location路径和实际路径不对应,优先考虑alias;如果要保持完整URI结构,用root。
3.3 proxy_pass反向代理:斜杠之差天壤之别
location /api/ { proxy_pass http://backend:8080/; } location /api2/ { proxy_pass http://backend:8080; }这两行配置的区别是新手最容易忽略的。第一个会把/api前缀去掉,请求/api/user会转发成/user;第二个会保留完整路径,请求/api2/user会转发成/api2/user。proxy_pass的URI末尾有无斜杠,直接影响转发路径的拼接方式。
我之所以把这一点单独拎出来讲,是因为生产环境里后端接口改路径、前端联调半天都对不上,最后发现是nginx这里多个斜杠少个斜杠的事。你在写反向代理时,最好先明确后端服务实际接收的路径格式,再决定是否在proxy_pass末尾加斜杠。
反向代理还需要注意几个header,尤其是Host、X-Real-IP和X-Forwarded-For。不传Host可能导致后端虚拟主机匹配错,不传X-Forwarded-For可能导致后端日志里全是nginx的IP。我一般这么配:
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;如果要代理WebSocket(比如前端页面连ws://协议),还得额外加上Upgrade和Connection两个header,否则握手会失败。这也是uni-app、微信小程序这类前端连不上服务的常见原因。
4. 负载均衡与高并发优化:从能用到好用
4.1 upstream负载均衡的三种常见策略
nginx的负载均衡配置在upstream块里,默认策略是轮询,每个请求依次分发给不同后端。如果后端机器性能不一样,可以用weight给某台机器更大的权重。让我列个对比表,方便你按场景选:
| 策略 | 配置写法 | 适用场景 | 注意点 |
|---|---|---|---|
| 轮询 | 默认,无关键字 | 后端机器配置相近 | 请求平均分配 |
| 权重 | server 10.0.0.2 weight=3; | 机器性能差异明显 | 权重值越大分到的请求越多 |
| IP哈希 | ip_hash; | 需要会话保持(Session) | 基于客户端IP哈希,但NAT场景下可能不均 |
| 最少连接 | least_conn; | 后端处理能力差异大、长耗时请求多 | 动态判断当前连接数较少者优先 |
我在生产里最常用的组合是least_conn加上max_fails和fail_timeout。比如:
upstream 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; }max_fails表示后端连续失败几次就标记为不可用,fail_timeout表示标记不可用的持续时间。这两个参数配合起来,能让nginx在不用第三方健康检查模块的情况下,也具备基本的故障摘除能力。
4.2 高并发场景的内核参数与连接优化
很多人以为nginx配置改了worker_connections就能扛住高并发,其实还得看操作系统层。文件描述符限制是第一个要调的:
# 修改 /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535然后是内核TCP参数,几个关键项:
# /etc/sysctl.conf net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.ipv4.tcp_max_syn_backlog = 65535 net.ipv4.ip_local_port_range = 1024 65535tcp_tw_reuse允许TIME_WAIT状态的连接被复用,对高并发请求尤其重要。tcp_fin_timeout调短一点,可以让关闭连接后的资源更快释放。改完记得sysctl -p生效。
nginx本身也有一组连接优化参数:
events { use epoll; worker_connections 10240; multi_accept on; }use epoll是Linux下最高效的事件驱动模型,nginx在Linux上默认会自动选择,但显式写出来也不多余。multi_accept on表示每个worker一次accept多个连接,能减少进程切换带来的开销。我在压测环境里把worker_connections从10240往上涨,发现nginx性能并不是线性上升的,涨到一定值后反而内存占用变高、收益有限。要综合考虑后端处理能力和内存大小来定,别盲目追求大数字。
5. 平滑升级与热重载:像做手术一样不停机更新
5.1 热重载与平滑升级,先弄懂信号机制
nginx的平滑能力来自它对信号的处理。修改完配置文件后执行nginx -s reload,本质是向master进程发送HUP信号,master会启动新的worker进程,然后逐步关闭旧worker,这个过程中请求不会中断。所以我在生产环境改配置,从来不restart,都是reload。
平滑升级到新版本则更复杂一些,需要用到USR2和WINCH信号。整体思路是:让新版本的master进程和旧版本master同时运行,由新的worker接手请求,等旧worker处理完存量请求后再退出。听起来复杂,但真正操作起来也就几步,而且完全不需停机。
5.2 平滑升级的完整操作流程
先弄清当前版本和编译参数,确保新版本编译时包含之前的模块:
/usr/local/nginx/sbin/nginx -V然后下载新版本源码,用同样的configure参数重新编译。编译完不要急着make install,先把原二进制备份:
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old备份完成后,把新编译的nginx二进制覆盖到原目录,然后发送USR2信号:
kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid)这个命令会让旧的master进程把PID写入nginx.pid.oldbin,同时启动新的master。接下来发送WINCH信号,让旧master平滑关闭它的worker:
kill -WINCH $(cat /usr/local/nginx/logs/nginx.pid.oldbin)这时候旧worker会慢慢退出,新worker开始承接全部流量。最后确认新版本正常后,再向旧master发送QUIT信号结束它:
kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)整个过程我实测过多次,只要后端连接不是长期不释放的极端情况,基本能做到零感知升级。这比直接替换二进制再restart要安全得多。另外要提醒一句,F5的nginx产品也曾曝出过缓冲区错误漏洞(CVE-2022-41742),这类安全问题的修复通常也需要平滑升级来完成,掌握这套流程就不至于为了打补丁牺牲可用性。
6. 安全加固与常见故障排查:配置文件的终极一课
6.1 TLS配置与安全加固的十个要点
HTTP/2和HTTPS已经普及,TLS配置是nginx安全加固的重头戏。先说几个我常用的安全配置:
server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/example.com.pem; ssl_certificate_key /etc/nginx/certs/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; }ssl_protocols不要开TLSv1.0和TLSv1.1,这两个老协议已经有多个已知漏洞,现在的主流浏览器也早已不支持。ssl_ciphers至少把MD5、RC4这类弱加密算法排除掉。ssl_session_cache建议开启,否则每次新建HTTPS连接都要做完整握手,性能损失非常明显。
除了TLS,安全加固还需注意几个细节。第一,隐藏版本号,在http块里加server_tokens off;,防止别人通过响应头判断你的nginx版本进而寻找已知漏洞。第二,限制请求体大小,client_max_body_size 10m;,避免有人恶意上传超大文件打爆磁盘。第三,对上传接口和后台路径加访问控制,比如只允许内网IP访问:
location /admin/ { allow 10.0.0.0/8; deny all; proxy_pass http://backend; }还有一个很实用的安全手段是限流。nginx自带limit_req模块,可以按IP限制请求速率:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=5r/s; server { location /login/ { limit_req zone=req_limit burst=10; proxy_pass http://backend; } }这个配置能有效缓解暴力破解和恶意爬虫。burst=10的意思是允许瞬间超过限制10个请求,超过的直接返回503,既不会误伤正常用户,又能把异常流量挡在外面。
6.2 高频错误排查速查表与避坑技巧
配置nginx的过程中,每个人都绕不开几个报错。我整理了一份高频问题和排查思路,按出现频率排序:
| 错误现象 | 常见原因 | 排查与解决 |
|---|---|---|
| 502 Bad Gateway | 后端服务未启动、监听端口错误、socket路径不对 | 检查后端进程和端口,curl直接访问后端验证 |
| 403 Forbidden | 静态文件无权限、index指令没配、目录不存在 | 检查/var/www/html权限,确认文件存在 |
| 404 Not Found | root路径写错、location匹配错误、alias拼接错误 | 用nginx -t没问题后,查看error.log具体路径 |
| 10054 / 连接被重置 | 使用了被禁用的TLS协议、连接被限流 | 检查ssl_protocols和limit_req |
| worker_connections insufficient | 连接数超过worker_connections | 调大该值和ulimit -n |
| nginx: [emerg] bind() to 0.0.0.0:80 failed | 端口被占用、权限不足 | netstat -tlnp查看占用,或用listen 8080测试 |
排查nginx配置问题,我的第一反应永远是看日志。错误日志默认在/usr/local/nginx/logs/error.log或/var/log/nginx/error.log,里面记录的往往是真正原因,而页面上的错误码只是表象。比如403,日志会明确提示是“directory index is forbidden”还是“Permission denied”,这能省下大量瞎猜的时间。
另一个小技巧是,改配置前先备份:
cp nginx.conf nginx.conf.bak.$(date +%F)改完用nginx -t做语法检查,确认输出syntax is ok和test is successful后再reload。我见过太多同事直接用nginx -s reload,结果配置有语法错误,整个nginx直接不工作了。养成“先备份、再修改、然后-t、最后reload”的习惯,基本可以告别配置引发的线上事故。
最后再分享一个我在实际维护中养成的习惯:对所有server块添加独立的access_log和error_log路径,按域名或应用区分。这样排查问题的时候不用在默认日志里大海捞针,直接tail -f /var/log/nginx/example.com.error.log就能看到对应应用的实时请求。这套配置用到今天,帮我省下的时间远超当初配置时花费的几分钟。