ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入浅出Nginx:静态资源托管、反向代理与负载均衡实战指南

深入浅出Nginx:静态资源托管、反向代理与负载均衡实战指南

1. 先把Nginx的使用场景想明白再动手

Nginx这个名字在开发和运维圈子里几乎天天能碰到,但很多人对它的理解停留在“一个Web服务器”这种模糊层面。实际上,Nginx在我日常工作中最常用的三个身份是:静态文件服务器、反向代理入口、负载均衡调度器。一个配置得当的Nginx,可以让后端只关心业务逻辑,让前端资源以最快速度到达用户终端,还能在最前面挡住大量无效请求。这篇文章会围绕这三个身份展开,配合最常用的命令、配置片段和两个完整部署案例,把我踩过的坑和验证过的方法都写出来。

适合看这篇文章的读者,我觉得是这样几类人:刚接触Nginx、只会nginx -s reload就想上线的同学;手里有业务系统、想把静态资源和后端接口分开部署的开发者;以及负责服务器运维、经常被504和502折腾到怀疑人生的朋友。这篇文章不会去逐条背诵官方文档,而是按“我实际会怎么操作”的思路去讲,尽量把每条命令背后为什么要这么做说清楚。读完后,你可以照着案例搭出第一个线上环境,也能在出问题的时候有一个自己的排查顺序,而不是打开百度搜一句“Nginx 502”。

先明确一个基础认知:Nginx天生是事件驱动的异步架构,处理高并发连接时性能和内存占用都远优于同等配置下的Apache。它擅长“转发”和“分发”,而不是“执行”。你不需要在Nginx里写业务逻辑,它做的事情是把请求接住、按规则走、把正确的内容回给对方。这种定位决定了我们在配置时应该遵循一条主线:接收请求后尽快决定“谁能处理这个请求”,然后不再让Nginx承担多余工作。所有location、upstream、proxy_pass的配置,本质上都是在表达这条主线。

1.1 什么时候该用Nginx:静态资源、反向代理还是负载均衡

新人最容易犯的错,是上来就给Nginx配一大堆规则,结果反而不如一个简单方案稳定。我建议先判断自己的业务属于哪一种,再决定配置的复杂度。

如果你的业务是个人博客、企业官网、产品落地页,那核心需求就是静态资源托管。HTML、CSS、JS、图片这类文件直接交给Nginx读取磁盘返回,配合gzip和浏览器缓存,已经可以达到非常理想的加载速度。这种情况下,后端逻辑如果存在,也只是提供极少数动态接口,用反向代理单独转发特定路径就行。

如果你的业务有后端服务,比如Java、Go、Python写的API,那么Nginx的任务就变成了反向代理。它把80或443端口收到的请求转发给内网地址的后端进程,同时负责HTTPS证书管理、HTTP头过滤、请求体大小限制。这时Nginx与后端之间可以考虑使用内网专用的HTTP连接,通过proxy_pass指定目标地址。配置的核心不是“转发”本身,而是路径映射、超时控制、错误处理。

如果你的业务规模到了需要多台后端实例扛流量,Nginx就升级为负载均衡入口。upstream块里定义一组后端地址,Nginx按轮询、最小连接数、IP哈希等算法分发请求。这里要注意,负载均衡不只是“多台机器分摊流量”,还要考虑后端节点的健康检查、会话保持、优雅下线。没有健康检查的负载均衡,遇到某台后端挂掉时,用户会周期性看到502,因为Nginx还在往宕机节点转发请求。

1.2 安装方式和版本选择:不要一上来就编译

我第一次给服务器装Nginx时,直接下载源码开始configure、make、make install,折腾了半天,直到后来才发现其实有更稳的路径。现在我的建议是:能用系统包管理器装的,就用系统包管理器。Ubuntu和Debian上用apt,CentOS和Rocky上用yum或dnf,装好后自动注册systemd服务,直接systemctl start nginx就能跑起来。这种方式的好处是后续卸载、升级、查依赖都方便,补丁能跟着系统源走。

什么时候需要自己编译?当你需要定制模块、或者官方预编译包里的模块不满足需求时,才考虑源码编译。比如你要用到第三方模块、需要调整某些内置模块的编译开关,这个时候去源码目录里 configure 才有意义。生产环境的服务器,我倾向于用稳定版主线外的发布分支,不要追最新版,Nginx的奇数版本是主线版,偶数版本是稳定版,比如1.24.x、1.26.x这种。线上环境没必要追求最新功能,稳定压倒一切。

我自己在Ubuntu上的标准安装命令就是:

apt update apt install nginx -y nginx -v systemctl enable nginx systemctl start nginx

装完之后,配置文件在/etc/nginx/目录下,主配置文件是nginx.conf,sites-available和sites-enabled两个目录是Debian系特有的配置管理方式。真正的站点配置放在sites-available里,启用时在sites-enabled里建一个软链接指向过去,禁用时删掉链接就可以。这套机制在维护大量站点时特别顺手,你不需要改主配置,只需要控制软链接的增删。

CentOS系略有不同,配置目录是/etc/nginx/conf.d/,习惯上每个站点放一个.conf文件,主配置里通过include /etc/nginx/conf.d/*.conf;自动加载。如果你两种系统都维护过,应该能体会到:理解发行版的配置文件管理哲学,比死记命令重要得多。

2. 常用命令:每天都会敲的那十几个

Nginx的命令行工具很简洁,核心的可执行文件就是nginx本身,通过不同参数完成不同操作。我每天高频使用的命令大概在十五个以内,分四类:查看信息、测试配置、启停控制、日志排查。

查看版本和编译参数用:

nginx -v nginx -V

-v只显示版本号,-V会连带显示configure参数和模块列表,这两个命令在线上升级或者确认模块时很有用。有时候你需要确认服务器上是否开启了某个模块,不需要去翻编译文档,直接nginx -V看输出里的--with-http_ssl_module这类字段就能确定。

测试配置用:

nginx -t nginx -T

-t是检查配置语法,输出里会明确告诉你哪一行配置有问题,这是每次修改完配置后必须执行的一步。严格来说,nginx -t不能只测一次就算完,要把输出里提示的warning也当回事。warning不会阻止启动,但往往隐藏着性能或安全上的隐患,比如server_name重复、worker_processes设置不合理等。-T是打印当前生效的完整配置,通常用于快速确认线上生效的配置内容到底是什么,尤其适合排查“我改了配置但行为没变”的诡异情况。

启停控制方面:

systemctl start nginx systemctl stop nginx systemctl restart nginx nginx -s reload nginx -s quit

大部分时候,修改配置不需要restart,reload就够了。reload会让master进程重新读取配置,然后温和地拉起新worker进程,旧worker进程在处理完当前请求后自动退出。这意味着请求不会中断,在线更新配置非常丝滑。真正需要restart的场景是修改了监听端口、改了worker数量这类需要重建监听socket的情况,或者你load了全新的动态模块、改了涉及进程本身的参数。

nginx -s quit和nginx -s stop的区别也值得记一下。quit是优雅退出,处理完当前连接再关;stop是立即强制退出,正在进行的请求会直接断掉。线上做维护的时候,优先用quit,避免把正在下载大文件的用户直接掐断。

日志和状态查看:

tail -f /var/log/nginx/access.log tail -f /var/log/nginx/error.log ss -lntp | grep nginx

日志是Nginx排错的核心信息来源。access.log记录每个请求的访问情况,error.log记录报错和异常。ss -lntp可以确认Nginx正在监听的端口和进程状态,比netstat更现代,输出也更快。

还有一个平时用得不多但关键时候能救命的命令:

lsof -i :80

它可以查出80端口是否被占用、占用进程是谁。我第一次部署Nginx时就遇到过80端口被系统自带Apache占用的场景,当时systemctl start nginx报错了,但看日志看不出端倪,最后是lsof -i:80定位到了问题根源。

2.1 启停和重载命令的坑:reload 不等于 restart

很多人把reload当成restart用,觉得都是让Nginx重新加载配置,实际差别很大。reload只重读配置文件并平滑重启worker进程,master进程的PID不变,监听socket也不会断开。restart则是完整停止再启动,新旧PID都会变化,监听期间会有极短的窗口期,请求如果恰好在这个窗口进来,可能直接connection refused。

我记得有一次在线上环境改了一个server块的listen端口,执行了reload,结果新端口一直没生效,请求还是走旧端口。后来才反应过来,reload不适用于某些变更,监听地址、worker_processes数量这样的改动,必须restart或者systemctl restart Nginx才行。不过restart有风险,如果配置文件存在语法错误,restart会导致Nginx起不来,服务直接挂了。所以在执行restart之前,一定要先nginx -t。

我个人的操作习惯是:

nginx -t && nginx -s reload

但遇到上面说的端口变更场景,就会改成:

nginx -t && systemctl restart nginx

这样既能保证配置语法没问题,又能在需要完全重启时安全操作。还有一点要提醒,如果Nginx是用systemd管理的,nginx -s reload不会被systemd感知到,但你依然可以这么用,只是状态机上不会显示“reloaded”这种信息。如果希望systemd感知,用systemctl reload nginx更规范。

2.2 日志、测试和排错命令组合

排错的时候,我习惯开三个终端窗口:一个跑tail -f看error.log,一个跑tail -f看access.log,一个手动执行curl测试。这是一种很朴素的调试方式,但效率极高。你手动发起一次请求,然后立刻看两个日志文件的输出,基本能定位请求到底有没有到Nginx、Nginx有没有转给后端、后端有没有响应。

一个很常用的测试技巧:

curl -I http://127.0.0.1

-I只取响应头,不取正文,适合快速确认服务是否正常。如果你改了Nginx配置,怀疑静态文件路径错误,可以用:

curl -v http://127.0.0.1/app/index.html

-v会打印完整的HTTP交互过程,包括DNS解析、TCP连接、发送的请求头、接收的响应头和响应体。Nginx配置里经常会因为路径别名问题导致404,curl -v能让你看到最终的URL路径是什么,便于判断到底是配置错了还是文件真的不存在。

还有一组诊断命令值得记住:

getenforce sestatus

如果你的服务器是CentOS系且开启了SELinux,Nginx访问非标准目录下的文件时,即使权限正确也可能被拒绝,报403或500。这时候用getenforce查看SELinux状态,如果是Enforcing,考虑用chcon调整文件的安全上下文,或临时关闭验证是不是SELinux导致的。SELinux的问题很隐蔽,因为它不改变文件的权限位,你在文件系统层面看一切正常。

3. 项目案例一:搭建一个静态资源站点

部署静态站可能是Nginx最基础的使用场景,但要把这个“基础”做扎实,需要注意的细节并不少。接下来我用一个实际项目来做示范:假设我要部署一个前端构建好的单页应用,目录在/data/www/demo.example.com/,通过demo.example.com对外提供服务,要求开启gzip压缩、强制HTTPS跳转,并配置合理的浏览器缓存策略。

这个场景在工作中太常见了,前端同学把打包产物给你,你把它扔到服务器上,然后让Nginx把它跑起来。很多人会觉得“不就是扔个文件嘛”,但实际部署时,缓存策略、压缩策略、SPA路由处理都是容易出问题的点。

3.1 目录规划和权限设置

先把目录建好:

mkdir -p /data/www/demo.example.com

把前端构建产物上传到这个目录后,需要检查两个权限问题。第一是目录的读权限,Nginx的worker进程默认以www-data或nginx用户运行,如果目录在其他用户名下且没有给到读权限,Nginx就会返回403。第二是父目录的权限,不只是站点目录本身,/data、/data/www父目录都要有可执行权限,否则Nginx进程进不去。

我通常的做法是:

chown -R www-data:www-data /data/www/demo.example.com chmod -R 755 /data/www/demo.example.com

chmod 755表示文件所有者可读可写可执行,组和其他用户可读可执行。在静态资源场景下,这个权限设定已经足够。要注意的是,不要为了省事直接chmod 777,那会带来不必要的安全隐患,尤其是根目录下如果有用户上传功能,777会被利用来写入恶意文件。

目录规划上,还有一个值得坚持的好习惯:每个站点放在独立目录,不要全部堆在/var/www/html里,尤其是多人维护的服务器。独立目录的好处是,出问题时可以快速通过站点的访问日志和站点的配置对应起来,不会出现“这个页面是哪个目录下的”这种糊涂账。

3.2 server块配置逐行拆解

在sites-available目录下新建文件demo.example.com:

server { listen 80; listen [::]:80; server_name demo.example.com; root /data/www/demo.example.com; index index.html; location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; access_log off; } location = /favicon.ico { log_not_found off; access_log off; } gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; }

这段配置我逐行拆解给你看。

listen 80;监听IPv4的80端口,listen [::]:80;监听IPv6的80端口。如果服务器支持IPv6,不写第二行会导致IPv6地址访问不到,或者如果只存在IPv6环境,甚至会卡到无法正常访问。

server_name是用来匹配不同域名的。Nginx收到请求后,会根据请求头里的Host字段,在所有server块里找一个匹配的server_name来路由,找不到就落到默认server块。如果没有特殊要求,第一个加载的server块会成为默认server,所以我通常会把一个默认拒绝或默认跳转的server块放在最前面,避免别人直接用IP访问到意外内容。

root和index比较简单,分别指定站点根目录和默认首页文件名。需要注意root和alias的区别,很多人会在这两个指令上犯错。root会把完整路径拼接到location后面,而alias会把location前缀替换掉。举一个最常见的场景,如果location是/static/,root配置为/data/www/demo.example.com,访问/static/app.css时,Nginx会去找/data/www/demo.example.com/static/app.css;如果是alias配置为/data/www/demo.example.com/static/,访问/static/app.css时,Nginx会去找/data/www/demo.example.com/static/app.css。粗看一样,但如果你写了alias /data/static/,访问/static/app.css时,找的是/data/static/app.css,这就是两者的关键区别。

location /里用了try_files $uri $uri/ /index.html;。这是单页应用部署中最关键的一行。SPA的路由由前端脚本控制,比如React Router的history模式,用户访问/about这个地址时,服务器上其实没有about.html文件,如果直接按路径找文件就会404。try_files先看$uri这个文件是否存在,再看$uri/目录是否存在,如果都没有,就把请求指向/index.html,由前端路由接管。没有这行配置,你的SPA刷新后就会一片空白或者报404。

静态资源的location块用的是正则匹配~*,表示不区分大小写,匹配.js、.css等后缀。命中后设置expires 30d,让浏览器把这些资源缓存30天,同时加上Cache-Control: public允许CDN和代理缓存,max-age设置为2592000秒也就是30天。静态资源的文件名如果带有指纹,比如app.8f3k2j.js,这种长缓存策略非常合适,因为文件名变了就相当于新资源,不会出现旧缓存问题。access_log off是为了让资源请求不刷满访问日志,图片和JS请求量太大,全记到日志里会导致日志文件飞速膨胀。

location = /favicon.ico用了精确匹配,只处理一个特定路径。log_not_found off表示这个文件找不到时不记录错误日志,因为favicon不一定每个项目都有,浏览器却又总会请求,不关掉这个日志就是铺天盖地的404记录。

最后是gzip配置。gzip on;开启压缩,gzip_types列了需要压缩的MIME类型,gzip_min_length 1024表示小于1024字节的文件不压缩。太小的文件压缩后体积可能不减反增,所以有个阈值限制。配置压缩时别忘了包含text/html,这是默认就会压缩的,不需要在types里带。

3.3 上线验证步骤

配置写好后,启用站点并验证:

ln -s /etc/nginx/sites-available/demo.example.com /etc/nginx/sites-enabled/ nginx -t systemctl reload nginx

然后执行:

curl -I http://127.0.0.1/

观察响应头里有没有HTTP/1.1 200 OK,再确认请求头里是否带上了Content-Encoding: gzip。如果响应头里出现了Content-Encoding: gzip,说明gzip生效。再测试SPA路由:

curl -I http://demo.example.com/about

如果返回200而不是404,说明try_files配置成功。最后在浏览器里访问https://demo.example.com,确认页面加载无报错,打开开发者工具看网络面板,确认静态资源命中了Cache-Control: public, max-age=2592000,并且Content-Encoding为gzip。

这一步我有一个强迫症级别的检查点:响应头不能出现Server: nginx之外的多余服务器信息。如果有必要,可以加一行server_tokens off;隐藏Nginx版本号,避免暴露精确版本给潜在攻击者。

4. 项目案例二:反向代理和负载均衡实战

第二个案例更接近现代后端架构的部署场景。假设有一个内部API服务运行在127.0.0.1:8080,提供/api/路径下的接口,前端业务页面由静态服务提供,同时需要支持多实例负载均衡。这个部署任务,本质上要解决三个问题:如何把公网请求安全转发到后端、如何保持多后端的负载均衡与故障转移、如何处理前端静态资源和后端动态接口的分离。

4.1 upstream 配置和调度算法

在Nginx的http块或server块之外定义一组后端:

upstream api_backend { least_conn; server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s; server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=10s; }

least_conn表示优先把请求分给当前连接数最少的后端实例。相比默认的轮询方式,当后端实例性能不同或请求处理耗时差异较大时,最少连接数能更好地平衡实际负载。weight=1是权重,权重越高分配的请求比例越大,适合后端机器配置不一致的场景。max_fails=2和fail_timeout=10s组合表示,10秒内如果后端失败两次,Nginx会将其标记为不可用,并在进入下一轮探测前不再转发请求给它。

upstream里还有一个容易被忽略的配置项是keepalive:

upstream api_backend { least_conn; server 127.0.0.1:8080 max_fails=2 fail_timeout=10s; server 127.0.0.1:8081 max_fails=2 fail_timeout=10s; keepalive 64; }

keepalive表示每个worker进程与后端保持最多64个空闲长连接。没有这个配置,每次请求都要新建和销毁TCP连接,高并发下会大量消耗文件描述符,还会明显增加延迟。加上keepalive之后,Nginx和后端之间可以复用TCP连接,性能提升非常显著。

upstream的调度算法,我常用的有四种:round-robin轮询、least_conn最少连接、ip_hash按客户端IP哈希、url_hash按请求URL哈希。ip_hash适合需要session保持的旧系统,url_hash适合缓存命中率要求高的场景,普通API后端用least_conn就挺好。

4.2 location 匹配规则

负载均衡配置的server块是这样写的:

server { listen 80; server_name api.example.com; access_log /var/log/nginx/api.example.com.access.log; location /api/ { proxy_pass http://api_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; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; } location /healthz { access_log off; proxy_pass http://api_backend; proxy_read_timeout 2s; } }

proxy_pass是反向代理的核心指令。这里有一个经常让人摸不着头脑的情况:proxy_pass后面带不带URI,行为完全不同。不带URI时,比如proxy_pass http://api_backend;,Nginx会把完整的原始请求URI传给后端;带URI时,比如proxy_pass http://api_backend/;,location前缀会被替换为URI中的路径。这个差异在实际配置中会引发非常诡异的404问题。所以我建议:如果没有明确把握,proxy_pass后面不要带URI,直接写upstream名。

proxy_set_header用于把客户端的真实信息传给后端。后端程序经常需要获取用户真实IP,尤其是做日志分析或风控的时候,如果Nginx不传X-Real-IP和X-Forwarded-For,后端看到的IP全是Nginx的内网地址,那API的访问日志基本就废了。$proxy_add_x_forwarded_for会在原有XFF头基础上追加客户端地址,即使请求已经经过多层代理,也能保留下链路信息。X-Forwarded-Proto则让后端知道用户请求是http还是https,很多应用会用它来判断协议类型,从而生成正确的回调地址。

超时参数也要根据自己的业务合理设定。proxy_connect_timeout 5s是Nginx与后端建立连接的超时时间,后端正常应该毫秒级响应,如果5秒还没连上,要么网络有问题,要么后端根本没起来,没必要继续等。proxy_read_timeout 30s是等待后端响应的时间,如果你的后端有长时间运行的接口,比如文件导出、批量报表,这个值要适当调大,否则大任务做到一半就会被Nginx掐断返回504。proxy_send_timeout 30s是Nginx向后端发送请求体的超时时间,一般上传大文件时才会触发。

location /healthz这个块值得单独讲。健康检查在生产环境必不可少。你可以在upstream里配置health_check,但那需要在商业版或者使用第三方模块才能实现。替代方案是:后端提供一个轻量探活接口,Nginx配置一个独立location只负责转发这个探活请求,由运维脚本周期性调用,或者让负载均衡器来探测。关键是不要让探活请求进入正式的业务日志和慢超时流程,所以这里关闭了access_log并缩短了read_timeout。

4.3 动静分离配置

如果一个server块同时承担静态资源和API接口的职责,可以在同一个域名下做动静分离:

server { listen 80; server_name www.example.com; root /data/www/www.example.com; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://api_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; } }

这样前端脚本请求/api/login时,Nginx自动转发到后端服务;访问/assets/app.js时,直接从本地磁盘读取。动静分离的好处不仅仅是各司其职,还能让静态资源命中缓存策略,动态请求不受缓存干扰,整体性能更可控。如果你的项目是前后端完全不同域名部署,那就更简单了,两个server块各管一个域名,配置互不干扰。

5. 性能优化和常用细节调整

静态部署和反向代理都跑通之后,接下来该关注性能了。很多人对性能优化的理解就是“开gzip、开缓存”,但实际值得调整的参数比这多一些,而且每个参数的调整都应当有明确目的,不建议照搬网上的“万能配置”。

5.1 gzip、缓存和安全响应头

gzip在静态站点案例里已经写过基础配置,这里补充几个进阶选项。gzip_comp_level是压缩级别,1到9,越高压缩率越大,但CPU消耗也越高。我实测下来,级别5是一个比较平衡的点,级别6以上CPU占用明显上升,压缩率提升却很小。gzip_vary会在响应头里加上Vary: Accept-Encoding,提示缓存服务器区分压缩和非压缩版本,防止代理服务器缓存了错误版本。如果你的前端资源通过CDN分发,gzip_vary on基本是必须的。

缓存方面,除了静态资源的expires,还可以配置open_file_cache。它的作用是缓存文件句柄、文件大小和修改时间,减少重复打开文件的系统调用。配置可以放在http块或server块里:

open_file_cache max=1000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;

max=1000表示最多缓存1000个文件句柄,inactive=20s表示20秒内没有被访问的缓存条目会被淘汰,min_uses=2表示文件至少被访问2次才缓存。这些参数能减少磁盘IO,但前提是文件量不要超过配置的缓存上限,否则缓存反复淘汰反而增加开销。

安全响应头在现在的部署里也应该加,我建议在server块或http块里统一加上:

add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Referrer-Policy strict-origin-when-cross-origin;

这几个响应头分别用于防止MIME类型嗅探、点击劫持和敏感来源泄露。安全性上不算完整体系,但属于性价比极高的基础项,一行配置就能挡住一类常见的浏览器攻击方式。

5.2 worker进程数与系统连接参数

Nginx主配置里最核心的性能参数是这几行:

worker_processes auto; worker_connections 1024; use epoll;

worker_processes auto表示按CPU核心数自动设置worker进程数。事件的并发处理由多个worker进程分担,worker数和CPU核心数一致通常是最优的。worker_connections是每个worker进程能同时保持的最大连接数,1024是默认值。当并发连接数超过worker_processes * worker_connections时,再进来的连接就会被拒绝。如果你的业务并发量较高,需要调大这个值,同时必须同步调整操作系统层面的文件描述符限制。

操作系统层面需要修改的就是ulimit:

ulimit -n 65535

要让这个限制持久生效,需要修改/etc/security/limits.conf,加入:

* soft nofile 65535 * hard nofile 65535

然后在nginx.conf的events块里加入:

worker_rlimit_nofile 65535;

这三处配合好,Nginx才能突破默认的1024连接限制。我曾经遇到过Nginx配置一切正常但高并发时频繁报Too many open files错误,排查到最后发现是系统限制没放开,Nginx的worker进程最多只能打开1024个文件描述符。连接数多大,并发能力就有多受束缚。

TCP层面的优化,可以加这样几个参数:

listen 80 backlog=4096;

backlog是连接队列的长度,高并发下如果这个值太小,新连接会被内核丢弃。tcp_nodelay on;可以让小数据包立即发送,降低交互式请求的延迟。tcp_nopush on;则相反,会在响应到达一定大小后才一次性发出,适合传输大文件时减少网络分片。在静态资源场景下,tcp_nopush和sendfile配合使用效果明显。

5.3 日志分割与清理

Nginx默认会把所有访问日志写到同一个文件里,时间久了文件体积会非常大,不仅占用磁盘空间,排错时不方便,甚至可能影响文件写入性能。我目前使用的方法是logrotate来自动分割,几乎不用自己写cron脚本。

在/etc/logrotate.d/nginx下配置:

/var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 640 www-data adm sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` fi endscript }

daily表示每天分割一次,rotate 14表示保留14份历史日志,compress表示旧日志压缩存储,delaycompress的意思是分割后的第一个日志先不压缩,避免正在写入的log还没完全结束就被压缩。postrotate里的kill -USR1通知Nginx重新打开日志文件,这是关键一步,没有它,Nginx还会继续往已经被改名或压缩的旧文件里写。

如果你坚持要手动分割,命令组合是:

mv /var/log/nginx/access.log /var/log/nginx/access.log.20250616 kill -USR1 `cat /var/run/nginx.pid`

手动分割时一定要记得发USR1信号,这是Nginx文档里明确推荐的做法。

6. 问题排查速查表

Nginx的报错信息其实非常直白,大多数问题都能在error.log里找到线索。下面是我整理的高频问题排查清单,按“现象、可能原因、检查点”的结构写,你在排错时可以直接对照。

6.1 502、504、499是怎么来的

502 Bad Gateway表示Nginx作为代理,向它配置的上游服务器发出了请求,但上游服务器没有给出有效响应。排查顺序我在工作中总结成了三步:

第一步,确认后端进程是否存活。直接curl http://127.0.0.1:8080/healthz,看有没有正常返回。如果连接拒绝,那Nginx自然无法代理,问题是后端服务挂了。

第二步,确认后端进程监听地址是否和nginx.conf里upstream的server参数一致。后端如果监听的是IPv6的::1,而Nginx配置的是127.0.0.1:8080,连接就会失败。这种不一致很常见,尤其是用localhost启动服务的程序,localhost会同时解析到IPv4和IPv6,程序实际监听优先选了IPv6地址,Nginx配置却写了IPv4。

第三步,如果后端进程正常、地址一致,就要看Nginx和后端之间的网络是否存在防火墙拦截、SELinux阻断。可以用curl在Nginx本机测试,如果本机能通,但Nginx代理失败,重点检查selinux和本机防火墙规则。

504 Gateway Timeout表示Nginx成功连接了上游服务器,但上游没有在proxy_read_timeout设置的时间内返回响应。优先检查后端是不是有慢查询、死循环、或者某个接口本来就超出预期时长。如果你确定后端这么慢是正常的,比如导出大量Excel,就把proxy_read_timeout调大到足够支撑业务完成。但我得提醒一句:不要无脑调大到几分钟,如果后端一直不回,占用的是Nginx与后端的连接资源,高并发下连接池会被打满。

499是一个比较容易被忽略的状态码,它表示客户端在Nginx拿到上游响应之前关闭了连接。出现大量499时,优先考虑用户请求超时后主动断开,后端压力过大也会导致这种情况,因为后端的处理速度赶不上用户等待的耐心阈值。修复方向通常是后端链路优化,而不是调Nginx超时参数。

6.2 403、404、301循环应该先查哪里

403 Forbidden的常见原因有四个。第一,root目录的权限不足,worker用户没有读权限或父目录缺少x权限,用ls -ld查看路径每一级的权限位,确认是755或至少有rx权限。第二,索引文件不存在,client_max_body_size超限不会报403,但索引问题会,访问目录却没有配置index或者索引文件不存在时Nginx会拒绝列出目录内容并返回403,除非开启了autoindex,线上环境请保持autoindex off。第三,SELinux阻止,上面已经讲过用getenforce验证。第四,Nginx配置里的deny all误伤,检查是否有访问控制指令,比如公网IP被某条geo规则覆盖。

404 Not Found的排查点,按我说过的root和alias区别来查。写过location后记得预判最终会映射到哪个文件路径,然后实际确认这个文件是否存在于系统里。另外,try_files和location的嵌套关系也可能导致意外404,尤其是正则location的优先级高于普通前缀匹配,容易被干扰。

301循环重定向通常发生在HTTPS跳转配置错误时。比如你写了这样两条规则:把所有HTTP请求301重定向到HTTPS,然后又在HTTPS server块中设置了同样的重定向逻辑,就会形成循环。判断是否循环的最快方法是curl -L跟踪重定向,看它最终停在哪个地址,然后检查server块里的return 301和schema判断逻辑。

6.3 一个从报错到修复的完整排错实例

我记一个最近处理过的案例,完整走一遍排错流程。某天收到监控告警,业务系统的API接口502率突然升高。我先执行nginx -t确认配置无异常,然后tail -f /var/log/nginx/error.log查看报错,发现大量connect() failed (111: Connection refused) while connecting to upstream。

接着我systemctl status api.service查看后端服务状态,发现服务处于running。再用ss -lntp检查后端端口监听,发现8080端口只有IPv4环境在监听,而Nginx配置的是127.0.0.1:8080,按理说一致。继续检查后端进程实际监听地址,发现程序启动配置里绑定的是::1即IPv6回环,而不是127.0.0.1,Nginx用IPv4去连当然被拒绝。

这个问题的根源是后端程序读了一个默认配置,没有显式指定监听地址,导致默认绑到了IPv6回环。修复方式也很简单,在后端启动配置里显式写上127.0.0.1:8080,重启后502恢复。整个排查过程不到10分钟,核心工具就是ss -lntp、tail -f error.log、curl直连测试。我分享这个案例是想让你知道,Nginx报错本身不可怕,可怕的是不按日志提示去追根因,而是在配置参数里瞎猜乱试。

写在最后的一些实操心得

这些年在服务器上部署Nginx,我逐渐形成了一个习惯:每次上线前都执行一遍nginx -t,每次变更后都立刻用curl做一次冒烟测试,然后把error.log和access.log的tail窗口打开至少观察两分钟。简单但很有效。

如果让我给刚开始接触Nginx的人一个建议,我会说:不要急着背指令,先把“请求进来后,Nginx如何用server、location、upstream这三级逻辑做路由”想清楚。server决定去哪个站点,location决定交给哪个处理逻辑,upstream决定最终抛给哪个后端。这条链路通畅了,报错日志里的每句话你都能迅速对应到应该是哪一环出了问题。

还有一个我可以确认的经验:配置管理上一定要保留注释。Nginx配置允许注释,把当时为什么写这个location、为什么要调大某个超时时间写清楚,三个月后你自己回来看,绝对会感谢当时的注释。很多线上诡异问题,就是因为有人悄悄改了一个看似无关紧要的参数,没有任何交代,后来的人完全摸不着头脑。

这个内容后续还可以扩展的方向包括:HTTPS证书自动化续期、Nginx与容器编排平台的联动、基于Nginx的限流与灰度发布。如果这篇文章对你有帮助,你可以先从两个案例里的任意一个开始,在自己的测试环境里完整走一遍,遇到问题再翻回来对照。实践过一次之后,Nginx对你来说就不再是一堆让人发怵的命令,而是一套逻辑清晰的路由工具。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进