LNMP动静分离实战:从零搭建到Nginx核心配置详解
2026/9/15 7:20:53 网站建设 项目流程

做运维和开发这些年,我很清楚一件事:很多人第一次接触“LNMP”都是靠一键安装包,装完能看到一个 phpinfo 页面就算完成了。可真到了生产项目上,同一个站点下既要跑 PHP 动态接口,又要处理图片、CSS、JS 这些静态资源,用户体验和服务器负载立刻原形毕露。这就是这篇文章想解决的问题。作为 Nginx 系列实战的第三篇,我把 LNMP 完整搭建和动静分离从环境规划、组件安装、核心配置、多站点部署到日志运维、性能调优,按我实际走过的顺序完整梳理一遍。这篇不是那种跑通即删的教程,而是能直接复用到项目里的经验总结。

1. 动静分离之前,先把 LNMP 三者的分工理清楚

很多新手一上来就写 Nginx 配置,结果碰到“PHP 页面打不开”“图片 404”“CSS 样式全乱了”,根本不知道问题出在哪一环。原因很简单:你还没搞清楚 LNMP 里每个组件到底负责什么,就去改它们之间的通信规则,自然容易被配置反噬。

1.1 Nginx 不是 PHP 解释器

先记住这条底层认知:Nginx 本质上是一个高性能的 Web 服务器和反向代理服务器,它自己不具备执行 PHP 代码的能力。一个浏览器请求 .php 结尾的 URL 时,Nginx 能做的只是把请求以 FastCGI 协议转发给 PHP-FPM 进程,等 PHP-FPM 解析完脚本、拿到输出后,再原样返回给客户端。

这里得强调一个容易搞混的概念:PHP 和 PHP-FPM 不是一回事。PHP 是解释器,负责把 PHP 源码编译成机器可执行的指令;PHP-FPM 是 PHP FastCGI Process Manager,负责管理一批 PHP-CGI 进程,统一接收 Nginx 转发过来的请求,分配空闲进程去执行。Nginx 通过fastcgi_pass指令把请求交给 PHP-FPM,两者之间要么通过 TCP 端口(比如 127.0.0.1:9000)通信,要么通过 Unix Socket 通信。

MySQL 在链路里则完全是另一个节点。PHP 脚本在处理业务时,通过 PDO 或 mysqli 扩展连接 MySQL 读取数据,数据库并不直接和 Nginx 打交道。所以一个请求走过的完整路径是:浏览器 → Nginx → PHP-FPM → PHP 脚本 → MySQL → PHP 组装 HTML → PHP-FPM → Nginx → 浏览器。

这个链路能帮你做第一层定位:页面白屏,先看 PHP-FPM 是否存活;接口报数据库错误,才需要去排查 MySQL;如果是 CSS/图片打不开,那大概率问题出在 Nginx 本身的静态文件处理配置上。

1.2 动静分离的本质是请求分流

所谓“动静分离”,就是把静态文件和动态请求从一条链路里拆开。静态资源,比如图片、CSS、JS、字体,本质是磁盘文件,Nginx 可以直接读文件返回,完全不经过 PHP-FPM 和 MySQL;动态请求,比如用户登录、订单查询、评论提交,才交给 PHP-FPM 去执行脚本、访问数据库。

我见过很多项目最初的写法,是把所有请求一股脑地转发给 PHP-FPM。这种做法在访问量很低时看不出什么大问题,但流量一起来就很糟糕。一个 PHP-FPM 进程在处理图片时,也会占用内存和 CPU,甚至因为日志、Session 锁等问题被拖住,导致后面真正需要处理业务的请求排队。为了更直观地理解,可以看一下静态和动态两种请求的特点对比:

对比维度静态请求(图片/CSS/JS)动态请求(PHP 接口/页面)
资源载体磁盘文件PHP 脚本 + 数据库
处理方Nginx 直接处理PHP-FPM 执行脚本
常见耗时几毫秒到几十毫秒几十毫秒到几秒
并发瓶颈文件句柄和带宽PHP 进程数、MySQL 连接数
缓存策略Nginx 层 expires / cache-controlRedis / opcache 等

动静分离要解决的问题,就是让“快的请求更快,慢的请求不堵住快的请求”。

这就像餐厅上菜:凉菜和饮料是现成的,后厨直接端上来就行;热菜需要厨师现炒。如果所有客人点的菜都排队等同一个厨师,那饮料也要等半天。动静分离相当于设置了两个窗口:一个窗口专门出凉菜饮料,一个窗口专门处理热菜,各干各的,效率自然上来了。

2. 从零搭建:环境规划、组件安装和首个 PHP 页面验证

这一节开始真正动手。假设你手上有一台 Linux 服务器,2 核 4G 配置,系统是 Ubuntu 22.04。这个配置做中小型网站的 LNMP 测试环境完全够用,实际上跑个几千 PV 的站点也没问题。

2.1 版本组合与安装源的选择

LNMP 对版本搭配没有特别复杂的约束,但建议不要追新也不要太旧。我的这次实践用的是 Nginx 1.22、MySQL 8.0、PHP 8.1。这三个版本在 Ubuntu 22.04 官方源里都有,直接 apt 安装就能得到互相兼容的版本。

如果你用的是 CentOS/RHEL 系统,安装方式会换成 yum,PHP 官方源需要额外配置 EPEL 和 Remi 仓库。这里给一条通用经验:优先使用系统包管理器的官方源,不要随便去网上找一个所谓“一键编译安装脚本”。包管理器能自动处理依赖关系、日志目录、systemd 服务文件,后续维护会省掉大量麻烦。

有同学会遇到没有外网的生产内网环境,那就需要用离线安装的方式:找一台同架构、同系统版本的机器,用apt-get downloadyumdownloader把 nginx、php-fpm、依赖包全部下载好,再拷到内网机器本地安装。这个过程最麻烦的是依赖树整理,建议生成下载清单后逐项检查,不要漏掉。

2.2 安装 Nginx、MySQL、PHP-FPM 的关键步骤

更新源后一次性安装所有组件:

apt update apt install -y nginx mysql-server php8.1-fpm php8.1-cli php8.1-mysql

很多人装完 PHP 只会安装 php-fpm 和 php-cli,结果发现连不上 MySQL,其实是因为少装了php8.1-mysql这个扩展。PHP 连接数据库走的是扩展,而不是 PHP 核心自带的,这一点要牢记。

MySQL 安装完成后,建议立刻执行安全初始化命令:

mysql_secure_installation

这个操作会引导你设置 root 密码、删除匿名用户、禁止 root 远程登录、删除测试数据库。很多没做这步的服务器,后期很容易被扫描到弱口令和未授权访问。

如果安装时提示nginx: 未找到命令,但包管理器又显示已安装,大概率是执行用户 PATH 环境变量里没有/usr/sbin目录。直接用whereis nginx找到绝对路径,或者用/usr/sbin/nginx执行即可。

2.3 服务启动、端口确认和开机自启

安装完成后,依次启动三个服务并设置开机自启:

systemctl enable --now nginx php8.1-fpm mysql

然后检查监听端口:

ss -lntp | grep -E ':(80|9000|3306)'

正常情况下应该能看到:Nginx 监听 80 端口,MySQL 监听 3306,PHP-FPM 默认监听 Unix Socket。这里特别说一下 PHP-FPM 的监听方式,因为它直接决定了后面 Nginx 配置里的fastcgi_pass怎么写。

PHP-FPM 默认配置通常监听 Unix Socket,路径类似/run/php/php8.1-fpm.sock。Unix Socket 不走网络协议栈,性能比 TCP 略好,适合 Nginx 和 PHP-FPM 在同一台机器上的场景。另一种是 TCP 方式,PHP-FPM 监听 127.0.0.1:9000,适合 Nginx 和 PHP 分离在不同机器上的架构。如果你用了listen = 127.0.0.1:9000,那 Nginx 里的fastcgi_pass必须也是127.0.0.1:9000,两边不一致会导致 502 Bad Gateway。

2.4 最小化配置让 PHP 页面跑起来

在配置动静分离之前,先用最精简的 Nginx server 块让 PHP-FPM 跑通,排除后面排查时的干扰项。

先创建站点目录:

mkdir -p /var/www/blog cat > /var/www/blog/index.php <<'EOF' <?php phpinfo(); EOF

然后修改 Nginx 默认站点配置,或者新建一个站点配置文件:

server { listen 80; server_name blog.example.com; root /var/www/blog; index index.php index.html; location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; } }

这段配置里最关键的是include snippets/fastcgi-php.conf,它会把 FastCGI 请求需要的一组默认参数带进来,其中包含SCRIPT_FILENAME等关键参数。如果没有这一行,你可能会遇到“PHP 文件被下载而不是执行”的问题——浏览器打开一个 .php 地址,屏幕上显示的是源码,这就是 Nginx 把 PHP 文件当成普通文件直接返回了。

保存配置后,先用nginx -t校验语法,再systemctl reload nginx,最后用 curl 验证:

curl -I http://127.0.0.1/

能看到HTTP/1.1 200 OK,并且页面内容是 PHP 渲染出的 HTML,说明 Nginx 和 PHP-FPM 已经打通了。

3. 动静分离配置核心:location 优先级、缓存与 fastcgi 参数

PHP 页面跑通只是完成了第一阶段,接下来才是这篇文章的重头戏:动静分离。而动静分离配置的核心,就是 Nginx 的location匹配机制。

3.1 location 匹配规则,决定你的请求到底去哪

Nginx 的location指令按匹配优先级分为几档,很多人只记得“正则匹配优先于普通前缀”,这个理解其实不完整。完整的优先级从高到低是这样的:

匹配方式写法示例优先级
精确匹配location = /logo.png最高
前缀匹配且不再检查正则location ^~ /static/
正则匹配(按配置文件顺序)location ~* \.(js|css)$
普通前缀匹配(取最长匹配)location /static/
默认匹配location /最低

这段规则在配置中是有实际后果的。如果我把location ~* \.(png|jpg|css|js)$写在前面,而location ^~ /static/写在后面,图片请求依然会命中正则规则,因为正则的优先级高于普通前缀。

但如果不希望正则去匹配 /static/ 目录下的请求,就必须用^~表示法,告知 Nginx 一旦命中该前缀,就不要再继续查找正则规则了。

3.2 root 和 alias,静态资源 404 的头号原因

讲静态资源配置,逃不开rootalias的区分。这是我以为自己懂了、结果实际踩坑时最多次的一个点。

root指令会把完整 URI 拼接到指定目录后面。比如:

location /static/ { root /var/www/blog; }

请求/static/a.png,实际去磁盘找的是/var/www/blog/static/a.png

alias指令会用 alias 指定的路径替换掉 location 匹配到的部分:

location /static/ { alias /var/www/blog/static/; }

请求/static/a.png,实际去找的是/var/www/blog/static/a.png。如果这里误写为root /var/www/blog/static/,那 Nginx 会去/var/www/blog/static/static/a.png找文件,返回 404。

写静态资源 location 时,一个常用配置是:

location ^~ /static/ { alias /var/www/blog/static/; expires 30d; add_header Cache-Control "public, immutable"; }

这样 Nginx 直接从磁盘返回静态文件,并且告诉浏览器这些文件 30 天内可以强缓存,后续用户刷新页面时浏览器都不会再发起请求。

3.3 静态缓存和防盗链的补充配置

文件缓存配置里,expires 30d是一个比较粗的粒度。我的习惯是,带 hash 指纹的文件名(比如 app.8f3k2a.js)用immutable缓存一年;普通图片用Cache-Control: public, max-age=86400缓存一天;HTML 页面不要设长缓存,否则改版后用户会看到旧页面。缓存策略要根据静态资源的更新频率来定,不可能一套配置吃遍所有场景。

如果图片资源比较宝贵,还可以加一个简单的防盗链:

location ~* \.(gif|jpg|png|webp|svg)$ { valid_referers none blocked *.example.com example.com; if ($invalid_referer) { return 403; } }

valid_referers里的none表示允许直接通过地址栏访问,blocked表示允许没有带 Referer 的请求,*.example.com则限定只有这些域名来的请求才能使用图片。对个人站点来说,这个配置能挡住大部分简单盗链,但也别指望它能防御任何技术攻击,这点认知要有。

3.4 fastcgi 参数,PHP 请求不出幺蛾子的底线

动态请求的 location 写法如下:

location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }

这里重点说两个细节。

第一个是try_files $uri =404;。这个指令在 fastcgi-php.conf 里通常已经被引入,它的作用是:如果请求的 PHP 文件在磁盘上不存在,就直接返回 404,不把请求传给 PHP-FPM。没有它,一个不存在的 .php 路径也可能被 PHP-FPM 接收执行,产生安全问题。

第二个是SCRIPT_FILENAME。它的值是实际执行脚本的绝对路径,由$document_root$fastcgi_script_name组合而成。如果文档根目录配置错了,或者 root 和 alias 混用后 document_root 不符合预期,这里就会出现“PHP 文件找不到”的 404,或者在日志里看到Primary script unknown错误。排这类问题,第一件事永远是去 error.log 看 fastcgi 相关报错,别在配置文件里瞎猜。

4. 多项目部署与反向代理:一台 Nginx 的多种挂载姿势

完成了单站点动静分离之后,现实里更常遇到的问题是如何在一台 Nginx 上部署多个 Web 项目。有些是多个域名,有些是同一个域名下的不同路径,有些项目甚至不是 PHP 写的,而是跑在另一个端口上的服务,这时候 Nginx 的角色就从 Web 服务器变成了反向代理。

4.1 用 server_name 规划多站点

最推荐的多项目方式是一个项目一个独立域名,在 Nginx 里就是多个server块。每个server块独立监听、独立配置 root、独立定义日志路径,互不干扰。

server { listen 80; server_name blog.example.com; root /var/www/blog; access_log /var/log/nginx/blog.access.log; } server { listen 80; server_name shop.example.com; root /var/www/shop; access_log /var/log/nginx/shop.access.log; }

这种方式的优点是配置隔离清晰,一个站点挂了不影响另一个。缺点是需要有多个域名,而且每个域名要正确解析到这台服务器上。如果没有域名,也可以用端口区分:

server { listen 8080; server_name localhost; root /var/www/project-b; }

端口区分适合内部测试,但对于线上生产不建议常态化使用,因为非标准端口在防火墙策略、安全扫描上总会多出不少麻烦事。

4.2 子路径挂载项目时容易踩的坑

当项目用同一个域名的不同子路径区分时,配置的复杂度会明显上升,比如业务要求既是www.example.com又是www.example.com/project-b

第一层是 location 前缀匹配:

server { listen 80; server_name www.example.com; root /var/www/main; location / { try_files $uri $uri/ /index.php?$query_string; } location ^~ /project-b/ { alias /var/www/project-b/; index index.php; location ~ \.php$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; } } }

这里最容易踩的坑是:子项目里的 PHP 文件和静态文件路径全乱套。原因还是 root 和 alias 的区别。在location ^~ /project-b/内部,PHP 请求的SCRIPT_FILENAME必须对应 alias 后的真实路径,否则 PHP-FPM 接收到的脚本路径不正确,就会 404。

第二层是前端层面的问题:如果项目 B 是一个 Vue 或 React 应用,打包后引用的资源路径是/assets/app.js,那么它加载资源时会跳到根路径/assets/去找,而不是/project-b/assets/。解决方案是要在打包工具里配置 base 路径,Vue 项目在vite.config.js里设置base: '/project-b/',这才算真正把这个项目挂在了子路径下。

4.3 反向代理场景与 IP 五元组的实际影响

说到 Nginx 反向代理,就得解决一个新手经常问的问题:Nginx 转发请求后,后端服务收到的客户端 IP 到底是不是真实 IP?

先明确 TCP/IP 层面的五元组概念:源 IP、源端口、目标 IP、目标端口、协议。假设客户端 A 访问 Nginx(假设 Nginx 和业务服务在同一台机器),Nginx 作为代理会新建一条到后端的 TCP 连接,后端看到的连接源 IP 是 127.0.0.1,源端口是一个随机端口,目标 IP 是 127.0.0.1,目标端口是业务服务端口,协议是 TCP。也就是说,纯粹站在 TCP 五元组的角度,后端永远看不到客户端 A 的真实 IP。

要让后端获取真实客户端 IP,需要在 Nginx 这一层把信息通过 HTTP 头传递过去:

location /api/ { proxy_pass http://127.0.0.1:8081/; 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-Real-IP只带一层的真实 IP,X-Forwarded-For则是把整个代理链上的 IP 都记录下来,多个值之间用逗号分隔。后端应用如果要取 IP,优先取X-Forwarded-For里最左边那个值,才是最初客户端的地址。如果有多层代理,也要注意不要直接信任这个头,否则客户端也能伪造,这就涉及到可信代理白名单的问题了。

4.4 静态资源走 Nginx,动态接口走反向代理,动静分离的另一种形态

动静分离不只针对 PHP。很多现代项目的架构是 Nginx + 后端 API 服务,后端用 Java、Go 或 Node 写的,监听在 8081 端口或通过 Unix Socket 通信。这时 Nginx 配置通常写成:

server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Connection ""; } }

这种场景下,静态资源如果也放在同一台机器上,就需要单独设计一套静态文件目录或 CDN 策略,而不是继续让 API 服务去读文件。上个月我部署一个前后端分离项目时,就是让 Nginx 直接管理/var/www/fe目录的静态产物,接口请求才反向代理到 8081 端口,整体性能和部署复杂度都比原来直接用 Node 托管静态文件好得多。

5. 日志、状态页与检查工具:排查问题时最愿意看到的东西

配置写的再花哨,上线后真正考验你的是定位问题的速度。日志是这一节的主角,不管什么疑难杂症,第一条排查路径基本都是打开日志看报错。

5.1 access_log 和 error_log 的路径与格式

Nginx 的日志分访问日志和错误日志两条线。访问日志记录每个请求的基本信息,错误日志记录 Nginx 处理过程中的异常。默认路径一般在:

/var/log/nginx/access.log /var/log/nginx/error.log

访问日志默认把所有字段混在一行里,可读性一般。建议自定义日志格式,方便后续归档和分析:

log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main;

这里我加上了$http_x_forwarded_for,因为经过代理后,$remote_addr很可能都是 Nginx 本机地址,只有通过 X-Forwarded-For 才能看到真实客户端来源。

错误日志的级别一般用warn就够了。error只记录严重错误,noticeinfo信息量太大,容易把真正的问题刷过去。调试阶段可以临时改成debug,但上线前必须改回来,否则磁盘会被日志撑爆。

error_log /var/log/nginx/error.log warn;

5.2 开启 stub_status 状态页

Nginx 自带的stub_status模块可以直接输出当前连接状态,是一个轻量又实用的监控入口:

location /nginx_status { stub_status on; access_log off; allow 10.0.0.0/8; deny all; }

访问后你会看到类似这样的输出:

Active connections: 12 server accepts handled requests 10245 10245 20831 Reading: 0 Writing: 1 Waiting: 11

Active connections是当前活跃连接数;accepts是从启动以来接受的连接总数;handled是处理的握手总数,正常情况下这两个数应该相等,如果相差过大,说明有连接被丢弃,很可能是文件句柄不够;requests是总请求数;ReadingWritingWaiting分别表示正在读请求头、正在写响应、等待请求的空闲连接数。这台状态页不要暴露到公网,用allow白名单限制住是最基本的底线。

5.3 可视化配置工具与 nginx -t

每次改动 Nginx 配置后,第一件事一定是先执行:

nginx -t

如果配置里有语法错误,它会直接告诉你错在哪个文件哪一行。没有报错再 reload:

systemctl reload nginx

关于可视化配置工具,网上确实有一些,比如部分 Nginx 管理面板,可以点一点生成 server 块、查看状态。我个人的态度是:可以作为辅助查看流量和状态,但不要完全依赖它来管理线上配置。原因很简单,这类工具生成的配置往往带有自己的模板和格式,一旦它生成的配置和你手工维护的配置风格不一致,后续合并、排查反而更麻烦。真正稳妥的做法是,自己掌握配置文件的基本语法,把域名、证书、日志路径、location 规则这些基础内容管理好。

5.4 用命令行快速分析访问日志

排查问题时,awk 和 grep 是一对很好用的工具。比如查看访问日志里响应状态码的分布:

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

看今天累计访问量前 10 的 URL:

awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

看最近一分钟有没有 500 报错:

tail -1000 /var/log/nginx/access.log | awk '$9>=500 {print}'

这些命令不用记太复杂,但关键时刻能帮你快速缩小问题范围。日志能告诉你的不只是有没有问题,还能告诉你问题集中在哪个 URL、哪个 IP、哪个时间段,这些信息在排查现场非常救命。

6. 上线后的参数调优与高频踩坑复盘

LNMP 跑起来之后,还有一个绕不开的阶段:调优。调优这件事不能盲目照抄网上的所谓“最佳配置”,而是要通过压测和观察逐步修正。我在这里整理几个通用的参数基线,以及我踩过、也帮别人解决的问题清单。

6.1 Nginx 核心参数基线

worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 4096; use epoll; } http { sendfile on; tcp_nopush on; keepalive_timeout 30; keepalive_requests 1000; gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/javascript application/json image/svg+xml; client_max_body_size 20m; }

worker_processes auto会让 Nginx 按 CPU 核心数启动 worker 进程,一般不用手动改。worker_connections是每个 worker 能同时处理的连接数上限。一个经验公式是:最大并发连接数 ≈ worker_processes × worker_connections。如果你的机器是 2 核,理论上这个配置能支撑 8000 左右的并发连接,但注意这是一个理论值,实际还要受内存、带宽、PHP-FPM 处理能力等因素限制。

sendfile on是让 Nginx 直接通过内核态发送文件,减少数据从磁盘到用户态的拷贝;tcp_nopush配合sendfile可以减少网络包的数量;keepalive_requests 1000表示一个长连接最多可以复用 1000 次请求,而不是每次请求都重新建 TCP 连接,这点对 TLS 场景尤其重要。

6.2 PHP-FPM 侧容易被忽视的两个设置

PHP-FPM 的默认配置在www.conf里,几个关键参数是:

pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 1000

pm.max_children决定 PHP-FPM 最多能启动多少个 worker 进程。不要盲目调大,因为每个 worker 占用的内存大约是 20~50MB,如果机器 4G 内存,max_children 设到 200 直接就会 OOM。一个比较稳的计算方式是:可分配内存 / 单个 PHP 进程平均内存,先设一个小值,配合监控数据逐步调大。

pm.max_requests建议必开。它的作用是让 PHP-FPM 在 worker 处理超过一定数量请求后自动重启该进程,防止 PHP 脚本里的少量内存泄漏无限累积。这个参数能帮你省下很多半夜被内存告警吵醒的时间。

另外强烈建议开启慢日志,定位那些写得比较慢的 SQL 或接口:

slowlog = /var/log/php8.1-fpm-slow.log request_slowlog_timeout = 5s

当某个 PHP 请求执行超过 5 秒,就会被记录到慢日志里,并直接打印出调用堆栈。这是排查“页面偶尔卡住”问题的利器。

6.3 高频问题复盘清单

现象根本原因解决方案
访问 .php 显示文件源码Nginx 没把 PHP 交给 FPM加上include snippets/fastcgi-php.conffastcgi_pass
502 Bad GatewayNginx 连不上 PHP-FPM检查 socket 路径或 9000 端口是否匹配、FPM 进程是否存活
静态资源 404root 和 alias 混用按 3.2 的逻辑重新设计 location
改完配置不生效没有 reload 或nginx -t没通过每次改配置先 test 再 reload
页面响应慢可能是 PHP-FPM 进程数不足看慢日志和 php-fpm 状态页调整 max_children
日志里全是客户端真实 IP 丢失代理层没传 XFF 头配置 4.3 的 proxy_set_header
安全扫描报告旧版 Nginx 漏洞版本过旧不要无视扫描结果,及时升级到修复版本

关于最后一条多说一句,漏洞扫描报出 Nginx 的 CVE 编号时,第一反应不是在网上找各种奇奇怪怪的补丁脚本,而是去官方源里升级版本。多数情况一条apt upgrade nginx就能解决,前提是你当时安装时用了官方源,而不是来路不明的编译包。

Windows 下部署 Nginx 的同学也要注意,nginx.exe直接双击启动虽然方便,但关闭窗口时 Nginx 进程并不会自动退出。生产环境在 Windows 上运行最好注册成 Windows 服务,或者用start /b nginx.exe的方式后台启动,否则很容易出现“我把窗口关了,服务怎么还在占着 80 端口”的尴尬。

7. 生产级配置参考与一次真实验证

最后给一个我实际用过的 LNMP 动静分离完整配置模板,你可以直接另存后按域名和目录替换使用。

7.1 完整参考配置

server { listen 80; server_name www.example.com; root /var/www/example; index index.php index.html; access_log /var/log/nginx/example.access.log main; error_log /var/log/nginx/example.error.log warn; # 静态资源:交给 Nginx 直接返回,设置缓存 location ^~ /static/ { alias /var/www/example/static/; expires 30d; add_header Cache-Control "public, immutable"; } # 图片:走正则匹配,加防盗链 location ~* \.(gif|jpg|jpeg|png|webp|svg|ico)$ { expires 7d; valid_referers none blocked *.example.com example.com; if ($invalid_referer) { return 403; } } # 动态 PHP 请求:交给 PHP-FPM location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # 默认规则 location / { try_files $uri $uri/ /index.php?$query_string; } }

这份配置的关键思路:静态目录用^~直接锁定,不让正则干扰;图片正则匹配后加防盗链;PHP 请求通过精确的.php$正则进入 FastCGI;默认 location 兜底所有未匹配的请求,转发到入口文件。

7.2 验证动静分离是否生效

配置完成后,用 curl 看响应头:

curl -I https://www.example.com/static/logo.png curl -I https://www.example.com/list.php

静态资源响应头里能看到Cache-Control: public, max-age=2592000,说明缓存生效;PHP 页面响应头里可以看到X-Powered-By: PHP/8.1之类标记,说明请求确实走过了 PHP-FPM。再用nginx -t最终校验一下配置文件,然后systemctl reload nginx,完成。

如果条件允许,建议用压测工具模拟一下 200 个并发,观察静态资源和动态接口的响应时间差异。动静分离生效后,两类请求的表现会有非常明显的区分度,静态资源占用非常稳定,动态接口则更依赖 PHP-FPM 和数据库的支撑能力。

7.3 一点实践体会

做 Nginx 配置这些年,我最深的感受是:不要为了“看起来高级”而做动静分离,也不要今天听人说反代好,明天就把所有流量都导过去。每一个配置决策都应该能讲清楚它解决的是什么问题。动静分离解决的是静态资源占用 PHP 进程的问题,缓存解决的是重复计算的问题,反向代理解决的是服务拆分后统一入口的问题——问题对了,方案自然就对了。

这次完整走了一遍 LNMP 搭建和动静分离的配置,我把整个过程中最小可复用的经验都提炼到了上面。如果你正在部署自己的建站环境,照着这份思路搭一遍,遇到问题时按日志和 location 匹配规则去排查,基本能把绝大多数日常问题控制在半小时内解决。下一期我应该会聊聊 Nginx 的缓存策略和 HTTPS 配置,这两块也是实战里绕不开的硬骨头。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询