ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

NGINX搭站从入门到排错:静态站点、反向代理与location匹配实战

NGINX搭站从入门到排错:静态站点、反向代理与location匹配实战

做了这些年站点,被问得最多的问题之一就是“我想用 NGINX 搭个网站,第一步到底该干什么”。问这话的人里,有刚学完几条基础命令的新手,也有从别的服务器软件迁移过来的老手——新手卡在配置写不对、首页打不开,老手卡在迁移之后一堆说不清的行为差异。我自己第一次正经搭站的时候,一个首页出不来折腾了整整两天,最后发现是目录权限和防火墙规则两个问题叠在一起,改完一个还有一个,每次以为找到了真凶,刷新页面还是那副样子。所以这篇不打算按“安装—配置—启动”的教科书顺序念一遍,而是按我实际从零搭一个站会走的路径写,把每一步背后的“为什么”讲透,把容易翻车的地方提前标出来。

NGINX 是一个事件驱动模型的高性能 HTTP 服务器与反向代理,用它搭 web 网站,图的无非三件事:内存占用低、扛并发稳、配置结构清晰到基本可以当文档读。下面聊的内容以静态站点为主线,顺带覆盖反向代理和动静分离这类常见组合,环境是常见的 Linux 发行版,命令和配置在主流版本上基本通用。适合手上有台机器、想把自己的页面真正挂上去的人看,也适合已经在用但总觉得配置“抄来的、不敢改”的人回头补一遍地基。

1. 先弄明白NGINX在一个请求里到底做了哪几件事

1.1 从浏览器敲下回车到文件被读出来的完整链路

很多人配置写不顺,根源不在语法,在于脑子里没有一条完整的请求链路。补上这条链路,后面所有的 server、location、try_files 你都能自己推出来该写在哪一层。

浏览器发起请求之后,先做域名解析拿到服务器地址,然后和服务器建立 TCP 连接(现在多数还叠了一层 TLS 握手)。连接建立后,NGINX 的 master 进程并不直接干活,它负责读配置、管理端口、派生 worker 进程;真正接活的是 worker,每个 worker 用事件驱动的方式同时盯着成千上万个连接,哪个连接有数据可读就去处理哪个,这就是它能用很少的内存扛住很高并发的原因。请求进来之后,NGINX 先根据 Host 头和监听端口挑出匹配的 server 块,再在选中的 server 块里按规则匹配 location,最后决定这个请求是交给本地磁盘上的文件、转给后端应用,还是直接返回一个错误码。

这条链路里有两个环节最容易被忽略。第一个是 Host 匹配:如果你只配了一个 server 块,那不管别人用什么域名指过来、甚至直接用 IP 访问,都会落到这个块里,看起来“能访问”,实际上埋着一个隐患——等哪天你加了第二个站点,第一个站点的流量可能会被莫名其妙地接走。第二个是 location 匹配的顺序,它不是简单的“从上往下第一个命中就停”,前缀匹配和正则匹配有各自的优先级规则,配错了会出现“我明明写了这条规则却没生效”的情况,第 4 节会专门拆。

有一句话我建议每个刚开始配 NGINX 的人都记一下:NGINX 的所有行为都可以归结为“选哪个 server、选哪个 location、然后怎么处理”三个问题。配置写不下去的时候,就问自己卡在这三步的哪一步,比漫无目的地翻文档高效得多。

1.2 静态站、反向代理站、动静混合站,三种形态的取舍

搭站之前先想清楚你要的是哪一种形态,因为它们的配置重心完全不同。

站点形态典型场景配置重心容易踩的坑
纯静态站展示页、文档站、博客root 目录、缓存头、gzip目录权限、缓存过期策略
反向代理站后端有应用服务,NGINX 只做入口proxy_pass、真实 IP、超时路径斜杠、后端超时
动静混合站页面静态化,接口走后端location 分流规则分流前缀写重叠

纯静态站是最简单也最容易被低估的形态。很多人觉得“静态文件嘛,扔进去就行”,结果上线之后发现首页加载慢、更新后用户看到旧版本、大文件下载经常断。这些问题几乎都能靠缓存头、压缩和连接配置解决,第 5 节会具体讲。

反向代理站的关键在于“边界要划清楚”:哪些请求原样透传,哪些需要改写路径,哪些头必须补上。做得好,后端应用完全不用关心外网这一层;做得不好,后端拿到的全是代理机的 IP,做限流、做日志分析全废。

动静混合站最容易出的问题是 location 规则重叠。比如你用/api/分流到后端,又用~ \.(js|css)$匹配静态资源,某个请求既符合前缀又符合正则,最后落到哪一条取决于优先级,而不是你写的顺序。这类问题在测试环境往往看不出来,因为前后端在同一个域下随便怎么配都能通,一上线就暴露。

2. 装之前先把目录约定和权限模型理清楚

2.1 用包管理器装还是自己编译,看你要什么

包管理器安装和源码编译安装,差别不在“高级不高级”,而在“你要不要那个可控性”。

包管理器安装的好处是省事:一条命令搞定,systemd 的服务单元、日志轮转、默认配置目录全都给你配好了,升级也是顺手一条命令。缺点是版本和模块受发行版仓库限制,某些第三方模块要么没有,要么得额外装包。对绝大多数搭站需求来说,这条路足够,我自己的生产机器基本都是走包管理器,除非确实需要某个仓库里没有的模块。

源码编译的好处是能精确控制编译参数,比如关掉用不上的模块减小体积、打开某个需要额外依赖的模块、指定安装路径。代价是升级要自己来,服务单元要自己写,出了依赖问题得自己排查。我一般只在两种情况下编译:一是需要某个特定模块,二是要在同一台机器上隔离跑多个不同版本的实例。

无论哪种方式,装完之后先做一件事:确认配置文件的实际加载路径。同一个命令nginx -t在不同发行版上读的可能是/etc/nginx/nginx.conf,也可能是别的位置。用nginx -V看编译参数里的--conf-path,比猜要靠谱。我曾经在一台机器上改了半天配置不生效,最后发现改的是备份文件,真正加载的是另一个目录下的同名文件,这种低级错误在着急的时候特别容易犯。

2.2 配置目录怎么组织,才不会半年后自己都看不懂

默认的nginx.conf里通常有一行include,把conf.d/或sites-enabled/下的文件都拉进来。这个机制一定要用起来,别把所有站点的配置全堆在主文件里。

我的习惯是每个站点一个独立文件,文件名带上域名,放在conf.d/下,主配置文件只保留全局设置(worker 数量、日志格式、连接数上限这类)。这样做的直接好处是:加一个站就是加一个文件,删一个站就是删一个文件,不用担心误删别人的配置;出问题的时候可以单独把某个文件临时改名排除,定位速度差好几倍。

另一个细节是先把默认站点处理掉。多数发行版装完会自带一个默认 server 块,监听 80 端口并返回一个欢迎页。如果你新加的站点没有明确写server_name,或者请求的 Host 谁都不匹配,流量就会落到这个默认块上。表现形式就是“我明明配了新站,访问却还是那个欢迎页”。处理方式有两种:删掉默认配置,或者把它留作兜底但明确写default_server并返回 444 直接断连。我倾向后者,因为直接断连可以挡掉一大批扫描流量,也不会让人误以为站点没配好。

权限这块单独提一句。NGINX 的 worker 进程通常以nginx或www-data这类低权限用户运行,而你的站点目录如果是用 root 建的,默认权限往往是 755,看起来能读。但如果中间某一级父目录的权限是 700,worker 就进不去,报 403。这不是 NGINX 的问题,是文件系统权限的问题,但错误信息只会告诉你“禁止访问”,很容易往配置方向查。第 8 节会把这类排查链路完整走一遍。

3. 一个能真正打开的server块长什么样

3.1 从最小配置开始,逐行加东西

别一上来就抄一份几百行的“生产级配置”,先写一个最小的、能打开首页的版本,跑通了再往上加。最小可用配置大概是这样:

server { listen 80; server_name www.example.com; root /var/www/mysite; index index.html; location / { try_files $uri $uri/ =404; } }

逐行拆一下。listen 80声明监听端口;server_name是域名匹配的依据,写多个用空格隔开;root指定站点根目录,注意这是目录前缀,不是“首页文件”;index指定目录请求时默认找哪个文件;location /兜住所有路径,try_files按顺序尝试:先找同名文件,再找同名目录,都没有就返回 404。

这里有个特别常见的误解:有人以为root写的是首页路径,于是写成/var/www/mysite/index.html,结果所有资源全 404。因为 NGINX 会把请求路径拼在 root 后面,/css/style.css拼出来就成了/var/www/mysite/index.html/css/style.css,显然不存在。记住 root 是“目录”,请求路径是“相对这个目录的子路径”。

配置改完不要直接 reload,先测:

nginx -t

这条命令会做语法检查并尝试加载配置,报错会指出文件和第几行。养成习惯,-t通过再nginx -s reload,能省掉大量“网站突然挂了”的时刻。reload 是平滑的,老连接处理完才退出,不会断开正在进行的请求,这一点比直接 restart 友好得多。

3.2 域名、端口、防火墙:三道关一道都不能少

配置写对了却访问不了,百分之九十卡在这三道关里的某一道。

第一道是域名解析。域名要指向服务器地址,解析生效需要时间,本地缓存也可能拖后腿。测试阶段可以先不改解析,直接改本地 hosts 文件把域名指过去,验证配置是否正确,确认没问题再动真正的解析记录。这个顺序很重要,否则你会同时怀疑解析和配置,排查效率减半。

第二道是端口监听。确认listen写的端口和实际访问的端口一致,确认 NGINX 真的起来了(ss -lntp看端口有没有被监听),确认没有别的进程抢占同一个端口。端口被占这种问题在装了两个服务器软件、或者之前跑过测试实例的机器上很常见,启动会失败但错误信息不一定显眼。

第三道是防火墙。系统自带防火墙和上层网络策略都可能拦。测试时可以先临时放开端口验证,确认能通之后再收回到最小开放范围。这一步我踩过最蠢的坑:在云主机上折腾了四十分钟配置,最后发现是控制台里的安全组没放行——机器内部的防火墙我查了三遍,压根没想到外面还有一层。

三道关全过之后,用一个最简单的请求验证:

curl -I http://www.example.com/

看返回的状态码和Server头。200 说明通了;403 查权限;404 查 root 路径;502 说明你配了反向代理但后端没起来;连不上说明卡在前三道关。用curl而不是浏览器,是因为它不会缓存、不会自动跳转、能直接看到响应头,定位问题干净得多。

4. location匹配规则:URL怎么落到具体的文件和目录上

4.1 前缀、正则、精确匹配的优先级不是按书写顺序

这是 NGINX 配置里最容易出错的一块,值得单独花时间搞清楚。

location 的匹配分几类:=是精确匹配,路径必须完全一致;^~是前缀匹配且命中后不再尝试正则;普通前缀匹配(不带修饰符)会记住最长的那条;~是区分大小写的正则;~*是不区分大小写的正则。

匹配顺序大致是:先找精确匹配,命中就结束;没命中就找最长前缀匹配,如果这条前缀带^~,直接用,不再看正则;否则按配置文件里出现的顺序依次尝试正则,第一个命中的就用它;正则全没命中,才回到刚才记住的那条最长前缀。

举个实际会遇到的例子。你写了:

location /static/ { ... } location ~ \.(js|css)$ { ... }

一个请求/static/app.css,前缀匹配命中了/static/,但因为它没带^~,NGINX 还会去试正则,结果被第二条抢走。如果你本意是让/static/下的东西全走第一条,就该写成location ^~ /static/。

我一般建议:能用前缀匹配就别用正则。前缀匹配是字符串比较,快且行为可预测;正则灵活但优先级容易绕晕,而且写错了很难一眼看出来。

4.2 try_files 与单页应用的路径回退

现在不少前端项目是单页应用,路由由前端接管。用户直接访问/user/profile这种路径时,服务器上并没有对应文件,如果按常规逻辑就 404 了。正确做法是把所有未命中的路径回退到入口文件:

location / { try_files $uri $uri/ /index.html; }

含义是:先找同名文件,再找同名目录,都没有就内部转发到/index.html,由前端路由处理。注意最后这个是内部转发,浏览器地址栏不变,所以前端拿到的仍是原始路径,路由才能正常工作。

这里有个副作用要留意:因为任何路径都会返回index.html且状态码是 200,搜索引擎可能会把一堆无效路径都当成有效页面。如果站点在意这个,可以在入口文件里做判断,或者在服务端对明显的无效路径单独返回 404。另外接口路径一定要在这条规则之前用更具体的前缀拦掉,否则前端请求/api/xxx也会拿到一个 HTML,前端解析 JSON 直接报错,而这个错误信息往往会误导你去查前端。

4.3 root 和 alias 的分水岭,一个字之差结果完全不同

这两个指令经常被混用,但行为差别很大。

root是把请求路径拼在指定目录后面。alias是把匹配到的那部分路径替换成指定目录。

location /img/ { root /data/assets; } # 请求 /img/a.png → /data/assets/img/a.png location /img/ { alias /data/assets/; } # 请求 /img/a.png → /data/assets/a.png

看出区别了吗?用 root 时,/img/这段还留在路径里;用 alias 时被替换掉了。所以什么时候用哪个,取决于你的磁盘目录结构是不是和 URL 结构一致。一致就用 root,不一致(比如你想把/download/映射到一个完全无关的目录)就用 alias。

alias 有个经典坑:结尾的斜杠。location 以斜杠结尾,alias 也必须以斜杠结尾,否则拼出来的路径会少一层或者多一层,表现为 404。这个问题不会报配置错误,只能靠访问验证发现,所以在写完 alias 之后一定要实际请求一个文件确认。

5. 静态资源的三个性能开关:压缩、缓存、连接

5.1 gzip 该开多大,什么时候该用预压缩

文本类资源(HTML、CSS、JS、JSON、SVG)压缩比很高,开 gzip 通常能省掉六七成的传输体积。配置大概是这样:

gzip on; gzip_comp_level 5; gzip_min_length 1024; gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_vary on;

gzip_comp_level从 1 到 9,越高压缩率越好但 CPU 消耗越大。实测下来 5 左右是性价比拐点,再往上压缩率提升很有限,CPU 却明显上涨。gzip_min_length设个下限,太小的文件压缩后可能反而更大,还白白消耗 CPU。gzip_vary on会加上Vary: Accept-Encoding响应头,告诉中间缓存同一资源有压缩和未压缩两个版本,避免缓存串味。

图片、视频这类本身已经压缩过的格式不要再 gzip,压不动还费 CPU,所以gzip_types里不要放它们。

如果你的静态资源是构建产物、内容不常变,更好的做法是构建时预压缩:提前生成.gz文件放在旁边,然后用gzip_static on;让 NGINX 直接发预压缩好的文件。这样运行时零压缩开销,效果比动态压缩好,代价是需要构建流程配合。

5.2 缓存头给多长,取决于文件名的变化规律

缓存是静态站性能的核心,也是最容易让用户“看到旧版本”的地方。关键思路是:内容会变的文件,靠文件名变化来失效;文件名不变的入口文件,不要长缓存。

现代前端构建工具输出的资源文件名通常带内容哈希,比如app.3f9a2b.js,内容一变文件名就变。这类文件可以给很长的缓存:

location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; }

immutable是告诉浏览器“这个资源在有效期内绝对不变,不用再发请求验证”,能省掉大量条件请求。

但入口 HTML 千万不能这么配。index.html名字是固定的,内容却随每次发布变化,如果给它一年缓存,用户可能几个月都看不到新版本。入口文件应该用短缓存或者协商缓存:

location = /index.html { add_header Cache-Control "no-cache"; }

no-cache不是不缓存,而是“每次使用前都要向服务器确认”。配合 ETag 或 Last-Modified,内容没变时服务器返回 304,只传几个头,开销很小。

这套“长缓存 + 文件名哈希 + 入口不缓存”的组合,我用了几年,基本没再遇到用户反馈“改了但看不到”的问题。之前不做区分的时候,每次发布都得让用户手动清缓存,非常难受。

5.3 sendfile、keepalive 和文件描述符上限

这几个是底层的开关,配好了对吞吐和延迟有实际帮助。

sendfile on;让文件发送在内核态直接完成,省掉从内核缓冲区到用户缓冲区的拷贝。传静态文件时这个开关收益明显。配合tcp_nopush on;可以把响应头和文件内容攒在一起发,减少小包数量。注意tcp_nopush和tcp_nodelay在某些场景下会互相影响,一般静态文件为主的站点开tcp_nopush,反向代理场景更看重tcp_nodelay。

keepalive_timeout控制长连接保持多久。设太短,频繁建连开销大;设太长,空闲连接占资源。一般 30 到 65 秒之间,看实际流量特征调。

还有一个经常被忽略的:文件描述符上限。每个连接占一个描述符,并发一高就容易撞上限,表现是日志里出现大量“too many open files”。除了系统层面的限制,NGINX 自己的worker_connections也要跟着调,而且要注意worker_connections是每个 worker 的连接数,总容量还要乘 worker 数量。

6. 反向代理:让NGINX站在应用前面该注意什么

6.1 proxy_pass 结尾有没有斜杠,路径完全不同

这是反向代理里最经典的坑,没有之一。

location /api/ { proxy_pass http://127.0.0.1:8080/; }

结尾带斜杠,请求/api/user转发到后端会变成/user,/api这段被剥掉了。

location /api/ { proxy_pass http://127.0.0.1:8080; }

结尾不带斜杠,转发后路径保持/api/user。

后端到底期望哪种,取决于它的路由是怎么注册的。前端调/api/user,后端路由也写成/api/user,那就不能剥;后端路由写的是/user,那就得剥。这个一定要和后端对齐,别靠猜。我曾经因为这个问题查了一下午,前端说“我请求发出去了”,后端说“我没收到这个路由”,最后发现请求确实到了后端,只是路径被改写了。

6.2 真实IP、超时和缓冲区,三个必须补的配置

真实 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;

后端要读这些头才能拿到真实来源。X-Forwarded-For是个列表,多级代理时会不断往后追加,后端取的时候要注意取哪个位置。另外这些头是客户端可以伪造的,如果前面还有一层可信代理,要配合real_ip模块做可信来源校验,不能无条件相信。

超时。默认的超时时间对某些慢接口来说太短,会导致偶发的 504。可以分开调:

proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 60s;

连接超时设短一点没关系,那是建连阶段,建不上就是有问题;读超时要根据后端最慢的接口来定,别一刀切设成很大,否则后端真卡住时前端会一直干等。

缓冲区。后端返回的头特别大(比如带了很多 Cookie 或者超长跳转地址)时,可能触发“upstream sent too big header”这类错误。这时候需要调proxy_buffer_size(专门管响应头)和proxy_buffers(管响应体)。我遇到过接口返回的头部超过默认大小导致 502 的情况,排查时看后端日志一切正常,最后是靠 NGINX 的错误日志里那句话定位到的。

还有一个实用的小配置:proxy_http_version 1.1;加上清空Connection头,可以让 NGINX 和后端之间也用长连接,减少建连开销。默认走 1.0,每次请求都要重新建连,QPS 高的时候差别很明显。

7. HTTPS 配置里那些不会报错但会出事的细节

7.1 证书文件的配置和权限

拿到证书之后,配置大致是这样:

server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; }

几个要点。fullchain.pem必须包含完整的证书链,只放服务器证书的话,某些客户端会因为找不到中间证书而握手失败,而这种失败在桌面浏览器上可能看不出来(浏览器会自己补链),在移动端或者命令行工具上就暴露了。私钥文件的权限要收紧,属主设成 root,权限 600 或 640,确保只有必要进程能读。

ssl_protocols里把老旧协议关掉,只留 1.2 和 1.3,这是现在的通行做法。ssl_prefer_server_ciphers off在现代客户端上是推荐值,让客户端根据自己的能力选,握手更快。

会话缓存这块,ssl_session_cache shared:SSL:10m大概能放四万个会话,对多数站点足够。握手是 HTTPS 里开销最大的环节,缓存能显著减少重复握手。

7.2 强制跳转与证书续期,别让站“突然挂了”

配好 HTTPS 之后,通常还要把 HTTP 请求导过去:

server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; }

用 301 是永久跳转,浏览器会记住,后续直接走 HTTPS,省一次往返。但要注意:如果哪天你想关掉 HTTPS,这个 301 会因为被浏览器缓存而很难撤销。测试阶段建议先用 302,确认整套流程没问题再换成 301。

证书续期是另一个高频事故点。免费证书通常有效期不长,靠自动任务定期续。这里有两个坑:一是续期任务执行成功但 NGINX 没有 reload,新证书没被加载,到期那天照样报错;二是续期任务本身失败了但没人知道,因为它是静默跑的。我的做法是续期任务里明确加一步 reload,并且加一个到期前若干天的检查,快到期还没续上就主动告警。听起来麻烦,但比起某天早上发现站点打不开,这点配置成本完全可以接受。

另外,如果你的站点同时提供 HTTP 和 HTTPS,注意 Cookie 的Secure属性、页面上引用的资源是否也走 HTTPS。混用内容会被浏览器拦,控制台里一堆红色报错,用户看到的就是样式全乱。

8. 排错实录:403、404、502、413这几个高频报错怎么定位

8.1 403 的三种来路,从权限查到索引

403 表示服务器理解了请求但拒绝执行。在静态站场景下,绝大多数是权限或索引问题,按下面顺序查效率最高。

第一种,目录或文件权限不足。worker 进程的用户对目标路径没有读权限,或者中间某一级父目录没有执行权限(进不去)。检查方式是逐级看权限,从站点根目录一直到目标文件。这里有个容易漏的点:父目录的执行权限。文件本身 644 看起来没问题,但它的上级目录是 700 且属主不是 worker 用户,一样进不去。

第二种,缺少索引文件。请求的是一个目录,但该目录下没有index指令指定的默认文件,同时autoindex又是关闭的,NGINX 就会拒绝列出目录内容。这种情况下返回 403 而不是 404,很多人会觉得奇怪,其实逻辑上是“我知道这个目录存在,但我不给你看”。

第三种,访问控制规则拦截。配置里有deny规则,或者.htaccess之类的隐藏文件被规则挡住了。这类情况查配置里有没有deny all、allow相关的段落就行。

排查时最有效的手段是看错误日志。错误日志的路径在全局配置里由error_log指定,默认通常在/var/log/nginx/下。把日志级别临时调到error甚至warn,复现一次请求,日志里通常会直接告诉你“权限被拒绝”或者“目录索引被禁止”,比盲猜快得多。记住改完级别要 reload,很多人改完配置没 reload 就去看日志,然后说日志里什么都没有。

8.2 502 和 413:一个查后端,一个查大小限制

502 表示 NGINX 作为代理时,后端返回了无效响应或者根本连不上。定位顺序是:先确认后端进程在跑、端口在听;再用curl直接打后端地址,绕过 NGINX 看是否正常;如果后端正常而经过 NGINX 就 502,那就是代理配置的问题——可能是路径改写出错、可能是超时太短、可能是响应头太大。

我之前遇到过一次很隐蔽的 502:后端接口在返回大量数据时偶尔失败,日志里写的是“upstream prematurely closed connection”。查下来是后端在处理长请求时自己设了超时,比 NGINX 的超时还短,于是后端先断开,NGINX 收到一个不完整的响应。这类问题不看两边的日志对不上,只看一边永远想不明白。

413 是请求体太大被拒。默认的client_max_body_size只有 1MB,上传图片稍微大一点就撞上。改法很简单:

client_max_body_size 20m;

放在 http、server 或 location 层都行,按需要控制粒度。注意这个值不要无脑设很大,它是防止超大请求打满内存的一道闸门,按业务实际需要给就行。另外有些后端框架自己也有请求体大小限制,NGINX 放开了但后端还是拒,那就得两头一起改。

还有一类不那么起眼的问题:配置改了但不生效。除了前面说的改错文件、忘记 reload,还有一种情况是浏览器或者中间缓存住了旧响应。排查时用curl加-H "Cache-Control: no-cache",或者直接换个查询串绕过缓存,能排除掉这一层干扰。

9. 上线前我会过一遍的那张清单

搭完能访问只是及格线,真正上线前我一般会按下面这张表过一遍,很多问题都是在这一步提前发现的。

检查项具体动作不做的后果
配置语法nginx -t通过改错一个字整站起不来
默认站点确认兜底 server 块行为未匹配域名落到奇怪页面
目录权限用 worker 用户身份试读文件随机出现 403
缓存策略入口文件短缓存、带哈希资源长缓存用户长期看到旧版本
压缩文本资源已开 gzip 或预压缩首屏体积翻倍
日志访问日志和错误日志路径确认、有轮转磁盘被日志写满
上传限制client_max_body_size符合业务上传功能偶发失败
证书链完整、到期时间确认、续期任务有 reload到期当天站点不可用
健康检查用 curl 走一遍主要路径上线后才发现某类请求挂了

日志这一项特别值得单独说。默认的日志格式只记了 IP、时间、请求行、状态码,够用但不够查。我一般会在格式里加上 upstream 响应时间和请求总耗时,这样排查慢请求时能直接看出时间花在哪一段:

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';

$request_time是整个请求耗时,$upstream_response_time是后端处理耗时,两个一减就是 NGINX 自己的开销。如果差值很大,说明问题在代理层或者网络;如果urt本身就很大,那要去查后端。这一条在我排查性能问题时救过好几次场。

日志轮转也别忘了配。访问日志涨得比想象中快,一个中等流量的站,日志文件几个月就能到几个 G。用系统自带的日志轮转工具配一份规则,按天切分、保留一定天数、切完给 NGINX 发信号让它重新打开文件句柄——最后这一步不做的话,切走的文件其实还在被占用,磁盘空间不会释放。

最后说个我自己的习惯:每次改完配置,先在测试环境用同样的配置跑一遍,再上生产。听起来是老生常谈,但真到了赶时间的时候,人最容易跳过这一步,而事故往往就发生在那次“就改一行,不用测了”的时候。踩过几次之后,我现在改配置之前会先把原文件备份一份,改完nginx -t通过再 reload,reload 之后立刻 curl 一下主要路径确认状态码正常。这三步加起来不到一分钟,能挡掉绝大多数自己给自己制造的事故。

RELATED READING

延伸阅读

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