
“使用 Nginx 搭建 Web 网站”这句话读起来平平无奇真动手做起来牵扯的细节一点都不少源码编译时哪些模块该剪、哪些必须留server 块怎么组织才不至于越写越乱worker 进程和连接数该怎么算日志满了往哪切页面突然 502 了该从哪一层开始查。这些问题在官方文档里散落在不同章节在一键脚本里又全被藏了起来。这篇就把我这些年反复搭站点、迁站点、救站点攒下来的流程完整走一遍从一台空机器开始一直做到能稳定对外提供服务的状态。内容适合两类读者刚接手服务器、需要把静态站或后端服务挂上去的同学以及之前用脚本装过 Nginx、配置一多就发懵、遇到问题只会重启的开发者。文中路径、参数都可以直接照搬系统维度用 Debian 系和 RHEL 系两条线对照说明域名和 IP 统一用保留地址代替避免误导。1. 整体设计与选型先想清楚要搭的到底是什么动手敲命令之前我习惯先在纸上把“网站”这个词拆开。它到底是纯静态的一堆 HTML 和图片还是一个需要转发到后端进程的动态应用抑或是好几个子域名共用一个入口的站群。这三种形态在 Nginx 上的组织方式差别很大先定下来能省掉后面大量的返工。1.1 为什么不建议直接套一键面板一键面板确实快装完就有图形界面点几下就能建站。但它在生产环境里有两个绕不开的问题。第一是封装太厚面板自己会生成一套配置模板你想改一个location匹配规则得先搞清楚它把配置写在哪一层、有没有被面板下次更新覆盖掉。第二是排查链路变长出问题时你先得判断是 Nginx 的锅、面板的锅还是后端的锅中间隔着一层你不认识的东西定位成本陡增。从零手搭的好处不在于“显得专业”而在于每一行配置你都知道它为什么在那儿。等站点规模上来需要加限流、加灰度、加多级缓存的时候你心里有一张清晰的图知道改哪个文件、影响哪些请求。我的建议是学习阶段和中小规模生产环境老老实实手写配置只有那种需要几十个人协作、非技术人员也要建站的场景再考虑上管理平台。还有一个常被忽略的点面板往往自带一套默认的 PHP 或数据库运行环境版本固定升级受制于面板发布节奏。手搭的话运行时版本你自己说了算这在需要跟进安全补丁的时候很关键。1.2 部署结构规划目录、用户、权限三层分离我见过太多把网站文件直接丢在/root/www下面、Nginx 进程用 root 跑起来的机器。这种结构一旦站点有文件上传功能基本等于把半条命交出去了。合理的做法是从一开始就把三层分清楚。运行用户层Nginx 的 master 进程需要绑定 80/443 这类特权端口所以它以 root 启动但处理请求的 worker 进程必须降权通常跑在www-dataDebian 系或nginxRHEL 系这个专用账户下。在nginx.conf里用user指令指定别偷懒不写。站点目录层我习惯统一放在/data/www/站点名/下面再往下分public对外根目录和logs站点级日志两个子目录。放/data而不是/var/www的理由很实际——/var在很多云主机上挂的是系统盘容量小站点文件和日志涨起来容易把系统盘撑满导致整机异常。/data通常单独挂一块数据盘写满了也不影响系统运行。权限层站点目录的属主设为部署账户比如deploy属组设为 Nginx 运行组目录权限 755、文件权限 644上传目录单独放宽到 775 并加上 setgid 位让新建文件自动继承属组。这样部署账户能写Nginx 能读其他账户碰不到。提示chmod -R 777是新手最常见的“解决权限报错”手段它同时解决了安全性和可维护性。403 报错十有八九是某一级父目录缺少执行位x从上往下逐级namei -l /data/www/site/public/index.html看一眼就清楚了。1.3 版本与安装方式的取舍Nginx 有稳定版stable和主线版mainline两条线。稳定版只收关键修复适合不敢折腾的生产环境主线版会合入新特性和性能改进我个人的习惯是新项目起步用主线版跑稳之后再跟着主线走因为它对 HTTP/2、TLS 1.3 这些协议的支持跟进得更快。发行版仓库里带的版本通常偏旧如果你需要较新的特性要么加官方源要么源码编译。安装方式上包管理器安装省事能自动处理依赖和 systemd 单元升级也方便源码编译的好处是模块可裁剪、编译选项可调、版本完全可控。我的判断标准是只是跑个静态站或者简单反代用包管理器足够需要第三方模块比如一些缓存清理、图片处理的扩展模块或者对编译优化有要求那就上源码编译。下面第 2 章会以源码编译为主线讲因为把编译这一关过了包管理器那套你自然就懂了。2. 环境准备与编译安装的实操细节这一章是全篇最“硬”的部分也是最容易卡住的地方。我按顺序把检查、编译、托管、验证四步拆开每一步都说明为什么这么做。2.1 上手前的系统检查清单拿到一台新机器我不急着装软件先花五分钟做几项确认能避掉后面一半的莫名其妙。第一项看系统版本和内核。cat /etc/os-release确认发行版uname -r看内核。内核版本低于 3.10 的话后面想开 HTTP/2 或者用较新的 TLS 特性会受限。第二项看磁盘布局。df -h确认/data是否独立挂载du -sh /var看看系统盘剩余空间。编译安装大概需要 200MB 左右的临时空间如果/tmp是独立小分区且已用满编译到一半会失败。第三项看端口占用。ss -lntp | grep -E :80|:443确认没有别的进程占着这两个端口。常见情况是系统预装的某个轻量 Web 服务默认监听 80装完 Nginx 起不来报“Address already in use”新手容易在这里绕半天。第四项确认时间同步。timedatectl status看时间是否准确。时间偏差超过几分钟后面配 HTTPS 证书校验和日志时间戳都会出问题。第五项装编译依赖。Debian 系和 RHEL 系的包名不一样我列个对照表照着装就行。依赖用途Debian 系包名RHEL 系包名编译器与构建工具build-essentialgcc、gcc-c、makePCRE 正则库libpcre3-devpcre-develzlib 压缩库zlib1g-devzlib-develOpenSSL 库libssl-devopenssl-devel下载工具curl、wgetcurl、wget注意PCRE2 和 PCRE1 的库名不同较新的 Nginx 版本默认支持 PCRE2编译参数写--with-pcre让它在系统库里自动找即可不用手动指定源码路径除非你要用非默认版本。2.2 编译参数逐项拆解很多人编译时直接抄一段网上的参数不知道每一项在干嘛。我把自己常用的一套拆开讲你按需增减。./configure \ --prefix/usr/local/nginx \ --sbin-path/usr/local/nginx/sbin/nginx \ --conf-path/usr/local/nginx/conf/nginx.conf \ --pid-path/usr/local/nginx/logs/nginx.pid \ --userwww-data \ --groupwww-data \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_gzip_static_module \ --with-http_stub_status_module \ --with-http_sub_module \ --with-stream \ --with-stream_ssl_module \ --with-pcre \ --with-zlib--prefix决定安装根目录我固定用/usr/local/nginx理由是不与包管理器安装的路径冲突将来要换版本只需备份这一个目录。--user和--group是编译期默认值也可以在配置里覆盖但写在这里能保证 worker 进程一开始就以低权限运行。--with-http_ssl_module和--with-http_v2_module是现在的标配前者不开连 HTTPS 都配不了后者是 HTTP/2 的前提。--with-http_realip_module特别重要——只要你的站点前面挂了负载均衡或者 CDN客户端的真实 IP 就藏在X-Forwarded-For头里没这个模块拿不到真实来源日志里全是上游的 IP风控和限流都没法做。--with-http_gzip_static_module允许直接返回预压缩好的.gz文件比每次请求现压缩省 CPU。--with-http_stub_status_module提供一个简单的状态页能看到当前连接数、请求数排查性能问题时有奇效。--with-http_sub_module用来做响应内容的字符串替换适合临时改个域名或者加个标记。--with-stream系列是四层代理能力默认不编译。你现在不做 TCP/UDP 转发不代表将来不做一并编译进去成本很低省得以后重新编译。反过来哪些模块我建议关掉--with-http_dav_module如果你不用 WebDAV 就别开多一个入口多一分风险--with-mail这套邮件代理模块在现代架构里基本用不上了纯属增加攻击面。2.3 从源码到 systemd 托管的完整过程参数定好之后整个流程是一条直线我按顺序写中间的关键点加注释说明。# 1. 下载并解压源码包目录名按实际版本改 curl -O https://nginx.org/download/nginx-1.25.3.tar.gz tar -zxvf nginx-1.25.3.tar.gz cd nginx-1.25.3 # 2. 执行上面的 configure 命令 ./configure --prefix/usr/local/nginx ... # 3. 编译-j 后面的数字取 CPU 核数能明显加速 make -j$(nproc) # 4. 安装注意这一步可能需要 root 权限 make install编译完成后/usr/local/nginx下会出现sbin、conf、html、logs四个目录。conf/nginx.conf是主配置html里有个默认的欢迎页用它做首轮验证最方便。接下来是把它交给 systemd 托管。这一步很多人跳过直接./sbin/nginx手动启动结果机器重启后服务没了或者日志散落在终端里。写一个单元文件就能解决。[Unit] Descriptionnginx web server Afternetwork.target [Service] Typeforking PIDFile/usr/local/nginx/logs/nginx.pid ExecStartPre/usr/local/nginx/sbin/nginx -t -q -g daemon on; master_process on; ExecStart/usr/local/nginx/sbin/nginx -g daemon on; master_process on; ExecReload/usr/local/nginx/sbin/nginx -g daemon on; master_process on; -s reload ExecStop/bin/kill -s QUIT $MAINPID PrivateTmptrue LimitNOFILE65535 [Install] WantedBymulti-user.target几个细节值得说。Typeforking配合PIDFile是 Nginx 这种自己后台化的服务的标准写法。ExecStartPre里带-t做配置语法检查语法有错直接启动失败而不是带着坏配置跑起来。LimitNOFILE65535提高单进程文件描述符上限默认值在并发稍高的场景下会撞墙报“Too many open files”。systemctl daemon-reload systemctl enable --now nginx systemctl status nginx顺手把日志目录的属主也改一下否则 worker 进程写日志会失败。2.4 装完必做的四项验证装完不要急着改配置先确认基础功能是通的不然后面出问题说不清是安装的锅还是配置的锅。第一项二进制自检/usr/local/nginx/sbin/nginx -V输出的编译参数里应该能看到你刚才加的所有--with-*版本号也对得上。第二项语法检查nginx -t正常输出是“syntax is ok”和“test is successful”两行。改配置后养成先-t再 reload 的习惯能挡掉大量低级错误。第三项本地访问curl -I http://127.0.0.1返回 200 且带Server:头就说明服务在跑。这一步不要用浏览器从外网测本地环回通不过的话外网肯定不通先缩小范围。第四项外部访问从另一台机器curl -I http://你的地址。不通的话按这个顺序排查——本机防火墙是否放行firewall-cmd --list-ports或iptables -L -n、云主机安全组是否放行、Nginx 是否监听在0.0.0.0而不是127.0.0.1ss -lntp看。3. 站点配置把静态站、多站点、反向代理一次配明白配置文件的组织方式直接决定了半年后你还能不能看懂自己的站点。这一章从结构讲到具体指令最后落到多站点共存的实践。3.1 配置文件的层次结构主配置nginx.conf里有一堆块理解它们的嵌套关系是看懂一切的前提。最外层是main往下依次能嵌套events、http、server、location。http块里的指令对所有站点生效server块代表一个虚拟主机一个站点或一个域名location块匹配具体路径。指令的继承规则是个关键点。子块会继承父块的指令值除非子块自己重新定义。举个例子http块里写了gzip on所有server都会继承某个server里写了gzip off那这个站点就单独关闭。这个机制很省事但也容易踩坑——你在http里加了个add_header结果某个server里也写了add_header那么父级的会被完全覆盖而不是叠加很多人在这里排查半天“为什么我加的安全头不生效”。我的习惯是把主配置当“骨架”站点的具体内容全部拆到/usr/local/nginx/conf/conf.d/下的独立文件里主配置的http块末尾加一行include conf.d/*.conf;。这样新增一个站点就是新增一个文件删站点就是删文件互不干扰也方便版本管理。3.2 最小可用的 server 块先给一个能跑起来的最简版本再逐行解释。server { listen 80; server_name www.example.com example.com; root /data/www/example/public; index index.html index.htm; access_log /data/www/example/logs/access.log main; error_log /data/www/example/logs/error.log warn; location / { try_files $uri $uri/ 404; } }listen 80监听所有网卡的 80 端口。如果这台机器有多个 IP 且只想监听其中一个写成listen 192.0.2.10:80。server_name是虚拟主机的匹配依据可以写多个域名用空格分隔。匹配优先级是有顺序的精确匹配最高其次是通配符*.example.com再其次是正则最后是default_server。所有server都没匹配上时请求会落到监听同一端口的第一个server上这也是为什么我建议显式指定一个default_server来兜底返回 444 直接断连能挡掉大量扫描流量。root和index配合决定请求的物理路径。root定义根目录请求/a/b.html会映射到/data/www/example/public/a/b.html。index指定目录请求时默认找哪个文件。try_files $uri $uri/ 404是静态站的核心指令逻辑是先找请求路径对应的文件找不到就当成目录再找一次还找不到返回 404。这一行比默认配置更明确也避免了目录遍历的一些隐患。access_log和error_log拆到站点级别好处是排查某个站点的问题时不用在几百 MB 的全局日志里翻。main是日志格式名在主配置的http块里定义第 5 章会细讲。3.3 多站点共存的两种组织策略一台机器跑多个站点是常态组织方式主要有两种各有适用场景。第一种按域名分文件。每个域名一个.conf文件文件命名带上域名比如example.com.conf、api.example.com.conf。这种方式下每个站点配置完整独立包含自己的listen、server_name、root、日志路径。优点是隔离性好删改一个站点绝不牵连其他缺点是listen 80这类公共指令重复写了多遍将来要从 80 改成 8080 得挨个改。第二种主 server 加分域名 server。先定义一个default_server处理所有未匹配请求再为每个域名写一个server块公共部分用include抽成片段文件。优点是公共逻辑集中改动方便缺点是片段文件的引入顺序和覆盖关系需要仔细设计否则出问题很难定位是哪个片段在作祟。我自己的选择是站点数量在 10 个以内用第一种配置直观改起来不费脑站点数量多或者需要统一做限流、统一加安全头的时候用第二种把公共指令抽到conf.d/common/*.conf里。这里有个实操细节值得单独说。多站点共用 80 端口时如果某个域名的证书配置写错了nginx -t会报错但错误信息里不一定直接指向出错的文件。这时候用nginx -T大写 T把合并后的完整配置打印出来配合grep -n定位比逐个文件看快得多。nginx -T 2/dev/null | grep -n server_name3.4 几个容易混淆的指令边界有几个指令名字相近用途完全不同我见过不少配置事故出在这里。root和alias的区别root是拼接alias是替换。举个具体例子location /img/ { root /data/static; }请求/img/a.png会去找/data/static/img/a.png换成alias同样配置会去找/data/static/a.png路径里的/img/被吃掉了。用alias时location和alias的结尾斜杠必须成对出现否则会拼出双斜杠或者路径截断。try_files和rewrite的区别try_files是按顺序尝试文件是否存在命中就返回内容适合静态资源回退rewrite是正则改写 URL再做内部或外部跳转适合做 URL 美化。两者能实现的场景有重叠但try_files更轻能不用rewrite就不用。location的匹配优先级也值得记牢精确匹配最高接着是^~前缀匹配命中后不再检查正则然后是~和~*的正则匹配按配置顺序先命中先赢最后是普通前缀匹配。很多人写了个正则匹配发现被前面的普通前缀规则抢走了原因就是优先级没理顺。提示正则 location 是按文件里的书写顺序匹配的不是按长度。这条规则和很多人的直觉相反配置一多就容易出诡异问题。我现在的做法是尽量用^~前缀匹配代替正则性能更好行为也更可预测。4. 性能与安全默认配置离能上线还差多少装完能访问只是起点默认配置基本是为“能跑”服务的不是为“跑得好”服务的。这一章讲几处我每次都要调的参数以及背后的计算逻辑。4.1 worker 数量与连接上限的计算主配置里有三个参数决定并发能力worker_processes、worker_connections、worker_rlimit_nofile。worker_processes一般设成 CPU 核数或者auto。设多了进程间切换开销变大设少了压不满多核。auto会读 CPU 核数绝大多数场景直接用auto就行。有个例外如果你的站点是 CPU 密集型的比如大量动态计算跟核数对齐如果是 IO 密集型的大量等待上游响应可以略微超过核数但不要超过 2 倍。worker_connections是单个 worker 能同时处理的最大连接数。理论最大并发 worker_processes × worker_connections。但这里有个反直觉的点如果这台 Nginx 同时作为反向代理一个客户端请求会占用两个连接一个对客户端一个对上游服务所以实际能承载的客户端并发大约只有理论值的一半。举个具体计算。假设 4 核机器worker_processes auto得到 4worker_connections 10240理论连接数是 40960作为反向代理时有效客户端并发约 20480。再考虑每个连接还要占一个文件描述符worker_rlimit_nofile至少要设到worker_connections × 2以上我一般直接给 65535。worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 10240; use epoll; multi_accept on; }use epoll在 Linux 上是默认的显式写出来是为了明确意图。multi_accept on让 worker 一次尽可能多地接受新连接高并发下能减少唤醒次数。还有一个容易被忽略的参数keepalive_timeout。默认值偏保守静态资源多的站点调到 60 秒左右能明显减少 TCP 握手。但如果你前面有负载均衡这个值要比负载均衡的空闲超时短否则会出现连接被上游单方面关闭、Nginx 还以为是活的、结果请求发过去直接报错的情况。4.2 压缩与缓存哪些该开、哪些别乱开gzip 是性价比最高的优化之一但配错了会适得其反。gzip on; gzip_min_length 1024; gzip_comp_level 5; gzip_vary on; gzip_types text/plain text/css application/json application/javascript application/xml image/svgxml; gzip_static on;gzip_min_length 1024的意思是小于 1KB 的内容不压缩。原因是压缩本身有固定开销小文件压完可能反而变大还多消耗 CPU。gzip_comp_level从 1 到 9收益曲线在 5 到 6 之间就明显变平了往上调压缩率提升有限但 CPU 开销陡增生产环境我固定在 5。gzip_types里绝对不要放图片和视频格式。JPEG、PNG、MP4 这些都是已经压缩过的再压一遍纯属浪费 CPU还可能让文件变大。image/svgxml是个例外SVG 是文本格式压缩收益很高。gzip_vary on会加上Vary: Accept-Encoding响应头这是给中间缓存层看的告诉它“同一 URL 在有无压缩时是两个不同版本”。不加这个头某些缓存服务可能把压缩版返回给不支持压缩的客户端导致乱码。gzip_static on需要对应模块支持作用是优先返回预先生成的.gz文件。这个对 CPU 敏感的场景很有价值——压缩工作在构建阶段就做完了运行时只是读文件。不过前提是你的构建流程里确实生成了.gz文件否则这个指令不会生效只是静默跳过。浏览器缓存这块用expires配合add_header控制。location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2)$ { expires 30d; add_header Cache-Control public, immutable; access_log off; }expires 30d会同时生成Expires和Cache-Control: max-age两个头。immutable告诉浏览器这个资源在有效期内绝对不会变可以跳过重新验证的请求。这个头只适合带哈希指纹的文件名比如app.a1b2c3.js如果你的文件名是固定的app.js加了immutable会导致用户永远拿不到新版本这就成了事故。4.3 基础安全加固的几个动作安全加固不是装个软件就完事几行配置能挡掉大部分自动化扫描。server_tokens off;这一行去掉响应头里的 Nginx 版本号让扫描器没法针对特定版本找已知漏洞。虽然属于“隐蔽式安全”但成本为零没理由不做。location ~ /\. { deny all; return 404; }禁止访问所有以点开头的文件和目录。.git、.env、.svn这些目录被直接暴露在 Web 根目录下是最常见的低级事故很多源码泄露就是这么来的。注意返回 404 而不是 403403 等于告诉对方“这儿有东西但不让你看”404 更含糊一些。限流是另一个必做项。limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; location /api/ { limit_req zoneapi_limit burst20 nodelay; limit_req_status 429; }limit_req_zone定义在http块里$binary_remote_addr以客户端 IP 为维度计数10m是共享内存区大小大概能存 16 万个 IP 的计数状态rate10r/s是每秒放行 10 个请求。burst20允许突发 20 个nodelay表示突发部分立即处理而不是排队延迟。这套组合的效果是正常用户几乎无感爬虫和压测流量会在超出阈值后收到 429。limit_conn用来限制单 IP 的并发连接数防止有人用大量长连接拖垮服务一般设在 20 到 50 之间看站点性质调整。注意如果你前面挂了负载均衡或 CDN$binary_remote_addr拿到的是上游的 IP所有请求会被算成同一个来源限流会误伤全部用户。这种情况必须先用set_real_ip_from和real_ip_header还原真实 IP再启用限流。这个坑我踩过一次上线后限流直接把整个站点限死了。4.4 HTTPS 配置的落地要点HTTPS 已经是标配配置本身不复杂关键是几个参数的取舍。server { listen 443 ssl; http2 on; server_name www.example.com; ssl_certificate /etc/nginx/cert/example.com.pem; ssl_certificate_key /etc/nginx/cert/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; add_header Strict-Transport-Security max-age31536000 always; }ssl_protocols只保留 TLS 1.2 和 1.3TLS 1.0 和 1.1 早已被主流浏览器标记为不安全留着只是徒增风险。ssl_ciphers只用 ECDHE 加 AES-GCM 的组合兼顾安全和性能老的 RC4、3DES 一律不要。ssl_prefer_server_ciphers off看起来反直觉其实是对的——现代客户端的加密套件选择逻辑比服务端更优让客户端先选。ssl_session_cache shared:SSL:10m缓存 TLS 会话10MB 大概能存 4 万个会话对减少重复握手效果显著。ssl_session_tickets off是出于前向安全的考虑会话票据的密钥如果泄露历史流量可能被解密对安全要求高的站点建议关掉。HSTS 那个头要谨慎加。一旦浏览器收到max-age31536000在一年内这个域名只能走 HTTPS你如果哪天子域名没配证书用户会直接访问失败且无法降级。上线前务必确认所有子域名都已经支持 HTTPS或者用includeSubDomains之外的更保守策略。配置完别忘了 80 端口的跳转。server { listen 80; server_name www.example.com example.com; return 301 https://$host$request_uri; }5. 上线后的运维日志、重载与排错站点跑起来只是开始真正耗时间的是上线之后的日常维护和突发故障处理。这一章讲三件事日志怎么管、配置怎么安全地改、出问题怎么快速定位。5.1 日志格式定制与切割默认的combined格式能用但信息不够。我一般在http块里自定义一个格式把排查时最需要的字段都带上。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 ua$upstream_addr;rt是 Nginx 处理这个请求的总耗时urt是上游服务的响应耗时。排查性能问题时这两个数字一对比立刻能判断是 Nginx 自己的问题还是后端慢。ua是上游地址挂了多个后端时能看出请求打到了哪台。这两个时间字段是排查 502、504 的第一手材料没有它们等于蒙眼开车。日志切割交给系统自带的 logrotate不要自己写脚本。/data/www/*/logs/*.log { daily rotate 14 missingok notifempty compress delaycompress sharedscripts postrotate /bin/kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid) 2/dev/null || true endscript }关键在postrotate里那个USR1信号。Nginx 收到它之后会重新打开日志文件而不是继续往已经被移走的旧文件句柄里写。如果不发这个信号切割后新日志文件是空的旧文件却还在长这是很常见的配置疏漏。rotate 14保留 14 天compress开启压缩delaycompress让最新的一个不压缩方便随时tail查看。提示如果日志量特别大access_log的写入本身会成为瓶颈。可以考虑用access_log ... buffer32k flush5s;开启缓冲减少磁盘写入次数。代价是日志会延迟几秒才落盘排障时需要多等一会儿。5.2 平滑重载与变更验证修改配置后的标准动作是三段式先nginx -t检查语法再nginx -s reload平滑重载最后验证效果。reload的工作原理值得理解一下。它不会中断现有连接而是让 master 进程读新配置、启动新的 worker然后通知旧 worker 优雅退出等手里的请求处理完再退。这个过程中如果有长连接比如 WebSocket旧 worker 会一直挂着直到连接断开这属于正常现象不要以为是 reload 没生效。什么时候必须完全重启而不是 reload换监听端口、改user指令、换编译进去的模块这些场景reload 是不生效的得systemctl restart。改这些之前最好挑低峰期重启会有秒级的服务中断。配置文件本身的管理我的建议是纳入版本控制。每次改动提交一次commit message 写清楚改了什么、为什么改。线上出问题时能立刻 diff 出最近一次变更比凭记忆回想靠谱得多。5.3 常见故障速查这部分是我这些年遇到问题后整理的按报错码归类遇到时直接查表能省掉不少翻文档的时间。状态码典型原因排查方向403文件权限不足或目录缺少执行位用namei -l逐级检查权限确认 worker 用户有读权限404root/alias路径拼错或try_files落空nginx -T打印实际配置对比请求路径和物理路径413上传文件超出client_max_body_size该值默认只有 1MB按业务需要调大499客户端主动断开连接多数是后端响应太慢导致前端超时去看urt字段500后端脚本报错或 Nginx 配置指令用错先看error_log再看后端自己的日志502后端进程没起来、端口不通、或超时ss -lntp看后端是否监听用curl直连后端验证504后端处理超过proxy_read_timeout先确认后端是否真慢再决定调超时还是优化后端502 是最常见也最容易被误判的。它字面意思是“网关收到无效响应”本质就是 Nginx 连上了后端但没拿到合法响应或者根本没连上。排查顺序应该是先确认后端进程活着ps、再确认端口在监听ss、再确认从本机curl后端能通、最后才怀疑 Nginx 配置。按这个顺序走八成问题在第一步就定位了。5.4 反向代理场景的高频坑静态站的问题相对简单一旦加上反向代理坑的数量会翻倍。我挑几个最常踩的说。Host 头丢失。默认情况下 Nginx 会把Host头改成proxy_pass里写的地址后端拿到的Host是127.0.0.1:8080而不是用户的域名。如果后端做了基于域名的路由或者生成了绝对 URL就会出错。解决办法是显式传proxy_set_header Host $host;。真实 IP 传递。后端要拿用户 IP需要proxy_set_header X-Real-IP $remote_addr;和proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;。后者会把已有的链路追加进去形成一条 IP 链后端取第一个非内网地址才是真实来源。注意这个头是可以被伪造的信任它之前必须确认流量来源可信。缓冲区不够。后端返回的响应头或响应体超过proxy_buffer_size和proxy_buffers的容量时Nginx 会把响应写到临时文件然后报错 “upstream sent too big header”。多见于后端返回了大量Set-Cookie。解决办法是适当调大比如proxy_buffer_size 16k; proxy_buffers 8 16k;。WebSocket 连不上。需要在location里加两行头把连接升级的意图透传过去。proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;少了这两行握手阶段就会失败表现为浏览器控制台一直显示连接中。超时的层次。反代场景涉及三个超时proxy_connect_timeout连后端、proxy_send_timeout发给后端、proxy_read_timeout等后端响应。默认都是 60 秒。如果后端有个接口要跑两分钟只在应用层改超时是不够的Nginx 这层会先断开。但也不要把所有超时都无脑调大那样一旦后端真挂了请求会长时间挂起连接池很快耗尽。我在实际运维中的体会是Nginx 这套东西的难点不在配置语法而在“知道每个数字背后对应哪条链路”。同样一个 504可能是后端慢、可能是网络抖动、可能是超时配置比上游的负载均衡还短也可能是缓冲区设置导致响应被截断。把日志字段开全、把每个超时的层次理清、出问题时按“客户端 → Nginx → 后端”的顺序逐段验证比盯着配置文件猜要高效得多。还有一个小习惯值得坚持每次改配置前先把当前版本备份成nginx.conf.bak.日期三个月后你会感谢当时那个谨慎的自己。