LNMP环境下Nginx与PHP-FPM协作原理及动静分离配置实战 📅 发布时间:2026/9/16 8:01:12 👁 浏览次数: 开头上个月帮朋友部署一套PHP项目环境是全新的云服务器系统装完、Nginx也起来了结果PHP页面死活就是不对——浏览器访问一个简单的index.php不是直接变成下载文件就是返回空白页。后来发现是fastcgi_pass指向的PHP-FPM监听方式和配置里写的不一致折腾了将近一个小时才定位到。今天把整个LNMP的搭建思路和动静分离的配置整理出来虽然网上教程一搜一大把但大多数只告诉你粘贴这段配置就能跑不讲为什么这么写、改了会有什么后果、踩坑了怎么排查。这篇文章会用实战的方式完整走一遍从裸机到LNMP跑通、再到动静分离生效的整个过程重点放在配置背后各组件如何协作的原理上。如果你是刚开始接触LNMP、想在Nginx里部署PHP项目或者被动静分离这个概念绕晕过这篇文章应该能帮到你。1. 先搞清楚LNMP是怎么协作的再动手搭建1.1 四个组件各管一摊别指望Nginx会解析PHPLNMP这四个字母代表Linux、Nginx、MySQL、PHP。很多人第一次看到这个组合时会以为Nginx装了就能跑PHP这是一个非常普遍的误解。Nginx本身就是一个HTTP服务器加反向代理服务器它天生只会处理静态文件、转发请求、做负载均衡并不具备解析PHP代码的能力。PHP代码的解析需要PHP-FPM这个独立的进程管理器来完成。如果把一次完整的用户请求比作去餐厅吃饭那么Nginx就是门口的迎宾负责接客、引路、上菜返回静态资源PHP-FPM是后厨负责把菜做出来解析PHP代码MySQL是仓库负责储存食材和账本存取数据Linux是整个餐厅的地基。菜品做不做得出来、好不好吃取决于后厨的水平和食材的新鲜度但迎宾必须知道什么样的需求该转给后厨什么窗口这个窗口就是fastcgi_pass指定的地址。1.2 核心桥梁FastCGI协议连接Nginx与PHP-FPM说到fastcgi_pass必须搞清楚它到底是什么。PHP-FPM在系统中跑起来后会监听一个地址这个地址可以是一个Unix套接字文件比如unix:/run/php-fpm/www.sock也可以是一个TCP端口比如127.0.0.1:9000。Nginx要做的事情就是把匹配到的PHP请求按照FastCGI协议的标准格式打包转发给这个地址PHP-FPM接到请求后解析执行PHP脚本再把执行结果原样返回给Nginx最后由Nginx拼上HTTP响应头返回给浏览器。这里最容易出问题的地方是Nginx配置里写的fastcgi_pass地址必须和PHP-FPM实际监听的地址完全一致。我朋友那次踩的坑就是PHP-FPM默认监听的是sock文件但Nginx里写的是127.0.0.1:9000两边对不上请求转不过去Nginx就直接返回了502或干脆把PHP文件当静态文件下载了。1.3 先理解静态和动态的区别动静分离才有意义再来说动静分离。要理解动静分离先要建立静态请求和动态请求这对概念。静态请求请求的是一个实实在在的文件比如logo.png、style.css、app.js、.html页面。服务器不需要做任何计算直接在磁盘上找到这个文件扔回给浏览器就行。动态请求请求的是一段需要执行的程序比如index.php、api/user.php。服务器必须启动PHP解析器来执行这段代码代码里可能会有数据库查询、业务计算最后生成一段HTML文本或者JSON数据返回给浏览器。动静分离的核心思想就是让Nginx充分发挥它的特长——处理静态文件非常快、非常省资源而让PHP-FPM专心处理动态请求。在配置上就是通过location匹配规则把静态文件请求和PHP请求分流到不同的处理路径静态请求由Nginx直接读取文件返回动态请求才转发给PHP-FPM。这个设计在生产环境带来的性能提升非常可观尤其是高并发场景下PHP进程不会因为轰炸式的图片、CSS、JS请求而白白消耗宝贵的内存和CPU时间。2. 环境准备版本选型和最容易卡住的环境坑2.1 操作系统与版本组合本文以CentOS 7.9为例对应的Nginx版本为1.20.xPHP选择7.4MySQL选择5.7使用yum安装的MariaDB 10.3也可以两者兼容度足够日常项目使用。这个版本组合不是拍脑袋选的。Nginx 1.20系列是2019到2020年间最稳定的维护版本向下兼容性好配置语法与最新的1.24、1.25差别不大。PHP 7.4是PHP 8.0发布前最稳的版本绝大多数旧项目的依赖包都能在上面正常运行PHP 8.0以上对部分老代码的兼容性问题更多新项目可以用8.0但如果是给现有项目搭环境7.4更省心。MySQL 5.7是使用量最大的生产版本它的事务、索引、性能在中等业务量下表现完全够用。如果你是用的yum安装建议先执行一次yum -y update把系统基础软件包版本统一避免后期出现依赖库版本冲突。我碰到过的最小众坑是新装系统的curl版本过低导致后面yum install某些扩展包时出现莫名其妙的依赖解析失败更新一遍系统后问题直接消失。2.2 千万记住Nginx命令找不到多半不是没装上在热搜词里我看到nginx: 未找到命令这是新手期遇到最多的一个问题。假如执行nginx -v提示bash: nginx: command not found第一反应往往是是不是没装成功。但实际上80%的情况是装成功了只是Nginx的可执行文件路径不在当前用户的PATH环境变量里。yum安装Nginx时可执行文件默认放在/usr/sbin/nginx这个目录对普通用户不在PATH中甚至某些最小化安装的系统中/usr/sbin也不在PATH里。所以正确姿势有两种# 方式1使用绝对路径 /usr/sbin/nginx -v # 方式2找到nginx所在位置后建立软链接 find / -name nginx -type f 2/dev/null ln -s /usr/sbin/nginx /usr/local/bin/nginx # 方式3直接切换到root用户 su - root nginx -v从运维习惯来看方式2最推荐因为软链接之后无论是nginx -v、nginx -t还是nginx -s reload普通用户具备sudo权限即可都能在任意目录下直接执行不用每次都敲全路径。2.3 端口占用检查80端口没让出来Nginx起不来另一个高频报错是Nginx启动失败错误日志里写着Address already in use。这台机器上如果已经跑着Apache、Tomcat或者其他Web服务80端口被占用了。排查命令netstat -tlnp | grep :80 # 或 ss -tlnp | grep :80确认占用进程后要么停掉旧服务要么修改Nginx默认端口。这里顺带提一下修改端口的方法编辑/etc/nginx/nginx.conf找到listen 80 default_server;把80改成你想要的端口。注意如果你的server块里写了listen 80;后又写了listen [::]:80;IPv6监听两个都要改否则改完依然会有一个监听在80上。3. LNMP四件套安装与配置每一步都讲清楚为什么3.1 一次安装完成的四个步骤CentOS 7.9上的安装命令非常集中依次执行下面四段命令即可# 1. 安装Nginx yum install -y nginx # 2. 安装PHP含常用扩展 yum install -y php php-fpm php-mysqlnd php-gd php-xml php-mbstring php-json php-curl # 3. 安装数据库CentOS默认源为MariaDB与MySQL API兼容 yum install -y mariadb-server mariadb # 4. 启动并设为开机自启 systemctl start nginx systemctl start php-fpm systemctl start mariadb systemctl enable nginx systemctl enable php-fpm systemctl enable mariadb几个细节说明php-mysqlnd是我们连数据库的驱动没有它PHP代码里调用mysqli或者PDO连接MySQL会直接报Class mysqli not found。php-fpm安装后默认的监听方式是Unix套接字监听在/run/php-fpm/www.sock这个路径后面在Nginx配置里要用到。安装完成后建议执行一次php -v确认版本再执行php-fpm -v确认PHP-FPM已经就绪。3.2 Nginx核心配置详解从全局到server逐层拆解Nginx配置文件的主结构是嵌套的主要由以下几层组成main全局配置worker进程数、日志路径、pid文件 ├── events事件模型 └── httpHTTP核心模块 ├── 全局配置日志格式、sendfile、gzip等 ├── server虚拟主机 │ ├── 监听端口、域名 │ └── locationURL匹配规则我一般在/etc/nginx/conf.d/下新建一个独立的配置文件比如lnmp.conf然后在主配置文件nginx.conf的http块中使用include /etc/nginx/conf.d/*.conf;引入。这是因为nginx.conf文件本身会越来越长拆分后逻辑清晰维护方便也更容易定位问题。下面是一个带详细注释的核心配置直接对应LNMP和动静分离场景server { listen 80; server_name _; # 使用任意域名或IP访问 root /var/www/html; # 网站根目录 index index.php index.html index.htm; # 静态文件图片、CSS、JS、字体等 # 命中location后Nginx直接从root目录读取文件返回 location ~* \.(gif|jpg|jpeg|png|css|js|ico|svg|woff|woff2|ttf)$ { expires 7d; # 客户端缓存7天减少重复请求 access_log off; # 静态资源访问频繁不记日志可减少IO压力 } # PHP文件转发给PHP-FPM location ~ \.php$ { root /var/www/html; fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这段配置里值得重点理解的三个参数fastcgi_pass负责把PHP请求转发到PHP-FPM监听的地址。这里是sock方式如果你前面PHP-FPM改成了监听9000端口那这里就要写成fastcgi_pass 127.0.0.1:9000;。SCRIPT_FILENAME告诉PHP-FPM要执行的文件路径。很多小白在这里栽跟头——如果把$document_root$fastcgi_script_name写成$root$fastcgi_script_namePHP-FPM就找不到文件返回404。document_root其实就对应Nginx配置中的root指令的值。include fastcgi_params;这个文件里定义了大量的FastCGI协议参数包括REQUEST_METHOD、QUERY_STRING、REQUEST_URI、REMOTE_ADDR等。缺少这个include会导致PHP代码里拿不到$_SERVER里的很多关键信息。3.3 启动与测试写一个phpinfo页面验证全链路配置完成后依次执行nginx -t # 检查语法 systemctl reload nginx # 重新加载配置平滑不中断服务然后在/var/www/html下新建index.php?php phpinfo();浏览器访问http://你的服务器IP/如果能正常看到PHP的信息页包含PHP版本、已加载扩展、环境变量等就说明整个链路已经通了Nginx收到请求→识别出.php→通过sock转发给PHP-FPM→PHP解析执行→结果返回Nginx→Nginx返回浏览器。如果出现502大概率是Nginx连不上PHP-FPM优先检查fastcgi_pass的地址与sock文件是否存在ls -l /run/php-fpm/www.sock如果出现404重点检查root路径是否写对以及文件是否存在ls -l /var/www/html/index.php如果在浏览器里直接把PHP源码输出成了纯文本说明location ~ \.php$这个匹配规则没生效看看是不是配置文件里少写了include fastcgi_params;或者fastcgi_pass那行被注释掉了。4. 动静分离实战让Nginx干它最擅长的活4.1 动静分离的目录规划与Nginx location匹配优先级在做动静分离之前先想清楚一个实际问题你的项目里静态文件都放在哪个路径下以传统的PHP项目为例常见结构是这样的/var/www/html/ ├── index.php # 动态入口 ├── api/ # 接口目录动态 │ └── user.php ├── static/ # 静态目录 │ ├── css/ │ ├── js/ │ └── images/ └── uploads/ # 用户上传图片目录动静分离的思路有两种落地方式方式一按扩展名分离。通过正则匹配命中.css/.js/.png等扩展名的请求直接由Nginx返回文件其余请求全部交给PHP-FPM。上面的示例配置就是这种方式优点是配置量少适用于静态文件分散在多个目录的情况。方式二按目录分离。把静态文件统一放在某个前缀目录下如/static/、/uploads/用location ^~ alias来精确控制。我推荐方式二因为可以提前在Nginx层面拦截掉大量请求连PHP-FPM的门都进不去。4.2 完整配置示例按目录分离 浏览器缓存server { listen 80; server_name example.com; root /var/www/html; index index.php; # 第一优先级带^~的精确前缀匹配 # 所有/static/和/uploads/下的请求都由Nginx直接处理 location ^~ /static/ { alias /var/www/html/static/; expires 30d; # 静态文件客户端缓存30天 access_log off; } location ^~ /uploads/ { alias /var/www/html/uploads/; expires 7d; access_log off; } # 第二优先级PHP请求转发PHP-FPM location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php-fpm/www.sock; } # 第三优先级其余所有未匹配到的请求 location / { try_files $uri $uri/ /index.php?$query_string; } }关于location的匹配优先级很多人背了又忘。这里用最直白的方式做个总结表写法优先级匹配方式示例location /最高精确匹配只有访问域名根路径时命中location ^~ /static/较高优先前缀匹配以/static/开头即命中不再检查正则location ~ \.php$中正则匹配以.php结尾的URL命中location /最低通用前缀匹配所有请求兜底对^~要有一个直觉印象它一旦命中就不会再往下做正则匹配了。所以如果你有/static/目录又想拦截其中的/static/xxx.php那^~会优先命中它PHP-FPM就收不到这个请求了——PHP文件放静态目录是一个常见的配置错误这时浏览器会直接下载xxx.php源码非常危险。4.3 动静分离的验证方法看日志和文件大小配置完毕后重启Nginx然后做两个验证验证一确认静态请求没有进PHP-FPM。观察PHP-FPM的请求日志默认路径为/var/log/php-fpm/www-error.log或通过access.log配置如果只记录了.php请求说明动静分离生效了。验证二请求头里的Server和响应字节数。用curl -I命令看HTTP响应头curl -I http://你的IP/static/css/style.css返回的HTTP头里会包含Content-Length字段这个值是静态文件的字节数。再去对比一下未分离前的配置你会发现两种方式的字节数是一致的但响应时间和Nginx进程占用有巨大差异——在静态文件量大的网站里动静分离后Nginx的负载能下降50%以上。个人经验动静分离的收益在日志里最直观。开启access_log off后Nginx的访问日志条数会急剧减少因为静态请求全部不记录这时你再去tail -f日志剩下的全是真正的业务请求排查问题会清爽很多。4.4 一个必须警惕的坑上传文件目录也被正则匹配劫持我实际部署时踩过这样一个坑项目有一个/uploads/avatar/目录里面的用户头像都是.jpg文件。一开始我只用按扩展名的正则方式做动静分离所以location ~* \.(jpg|png)$会命中这些头像请求Nginx直接返回文件一切正常。后来同事给/uploads/目录加了一个upload.php上传接口这时候问题就来了——上传接口的URL是/uploads/upload.php按正则匹配规则.php$会优先生效所以请求被转发给了PHP-FPM没问题。但上传之后这些新上传的图片文件存放在/uploads/avatar/下访问时命中的是location ~* \.(jpg|png)$也被正确返回了文件。真正的问题出现在图片防盗链需求上——我后来给/uploads/目录的访问加了一个Referer校验结果把/uploads/upload.php的动态请求也拦了上传接口一直报来源非法。这个坑告诉我们的经验是如果把静态文件和动态脚本混在同一个目录树里动静分离规则之间的边界要非常清晰。最简单的做法是静态文件和动态脚本不要放在同一个目录前缀下比如上传接口独立部署在/api/upload.php图片统一走/uploads/这样分离规则就永远不会互相干扰。5. 上线前的安全检查与性能调优让LNMP更稳、更快5.1 配置文件里什么是必须禁掉的LNMP刚搭完PHP默认配置存在一些安全隐患作为实践总结有必要提一下。打开/etc/php.ini搜索并修改以下配置项; 关闭PHP版本号在前面的响应头中暴露 expose_php Off ; 关闭危险函数按需启用 disable_functions exec,shell_exec,passthru,system,proc_open,show_source ; 控制上传大小根据项目情况按需调整 upload_max_filesize 20M post_max_size 20M修改后重启PHP-FPMsystemctl restart php-fpm这些配置的目的不是纯堆安全性更深层的逻辑是即使将来某个站点被上传了恶意脚本它能调用的系统函数和能提交的请求大小都被限制在可控范围内。把攻击面先缩小应用层再配合权限控制安全性会好很多。5.2 Nginx性能参数调优基于事实的保守优化Nginx主配置文件/etc/nginx/nginx.conf中两个最重要的参数是worker_processes和worker_connections。# 设置worker进程数为CPU核心数也可设为auto worker_processes auto; # 每个worker最大并发连接数 worker_connections 1024; # 高效传输模型 sendfile on; # 开启gzip压缩大幅度减少传输体积 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss image/svgxml; gzip_min_length 1k;参数的简单计算方法是最大并发连接数 ≈ worker_processes × worker_connections。一台2核4G的云服务器按auto通常就是2个worker1024连接数最大并发连接数约2048。但这只是连接数上限实际能支撑多少取决于PHP脚本执行时间、数据库连接数、带宽等因素存在木桶效应别只看这个公式就给系统下结论。sendfile on很多人不理解。简单说传统方式下读取静态文件数据从磁盘读到内核再从内核复制到用户态程序再由程序写回内核发给网卡中间有一到两次多余复制。开启sendfile后数据在Linux内核里直接完成磁盘→网络的传递效率高很多对静态文件多的场景提升非常明显。5.3 日志路径和轮转排查问题的基础设施日志是你查一切的底牌。默认情况下Nginx访问日志/var/log/nginx/access.logNginx错误日志/var/log/nginx/error.logPHP-FPM日志/var/log/php-fpm/www-error.log错误日志和/var/log/php-fpm/access.log若开启MySQL日志/var/log/mariadb/mariadb.log平时排查502时我会养成一个习惯同时打开两个终端一个tail -f /var/log/nginx/error.log一个tail -f /var/log/php-fpm/www-error.log然后在浏览器里再次发起请求哪边先跳出错误信息问题就在哪一层。日志轮转是个容易被忽略的点。Nginx的logrotate配置一般位于/etc/logrotate.d/nginx默认会按天轮转保留52份。如果某个项目日志量巨大比如被扫描器盯上建议自定义轮转周期和保留份数避免磁盘被日志占满。具体做法是在配置文件中调整/var/log/nginx/*.log { daily missingok rotate 14 # 保留14天 compress delaycompress notifempty create 0640 nginx adm sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 cat /var/run/nginx.pid fi endscript }postrotate里的kill -USR1是让Nginx重新打开日志文件的关键如果不加这一步轮转后日志会继续写入已删除的文件句柄磁盘空间无法释放。6. 部署完的验证清单与后续扩展6.1 一张表理清部署后的自检项LNMP搭建完成后不要急着部署业务代码先按下面的清单过一遍验证项命令或方法预期结果Nginx运行状态systemctl status nginxactive (running)PHP-FPM运行状态systemctl status php-fpmactive (running)数据库运行状态systemctl status mariadbactive (running)Nginx配置语法nginx -tsyntax is okPHP与数据库连通写一个mysqli_connect测试脚本连接成功静态文件直接返回curl -I http://IP/static/test.css200 OKContent-Type为text/css动态请求正常转发访问http://IP/index.php正常显示PHP信息页端口监听情况ss -tlnpgrep -E :80其中第五项PHP与数据库连通特别重要。很多项目部署后白屏不是因为Nginx或PHP-FPM没配好而是PHP代码连不上数据库。验证方式是在网站根目录放一个临时脚本?php $conn new mysqli(127.0.0.1, root, 你的密码, test); if ($conn-connect_error) { die(连接失败: . $conn-connect_error); } echo 数据库连接成功;验证后立刻删除这个脚本千万别留在服务器上——它暴露了数据库地址和账号信息被扫描器看到是重大安全隐患。6.2 动静分离后的缓存策略思考动静分离不只是把静态交给Nginx这么简单还涉及到静态资源的缓存策略。上面配置里写的expires 7d、expires 30d是Nginx给浏览器返回的Cache-Control和Expires响应头告诉浏览器这个文件在指定时间内可以直接从本地缓存读取不用再向服务器发起请求。这带来一个需要留意的副作用如果哪天你更新了页面里的style.css但文件名没变客户端浏览器会继续用本地缓存里的旧文件新样式不生效。所以现在主流做法是给静态资源文件名加上摘要值style.abc123.css或者版本号参数style.css?v20250101。在Nginx层面expires指令照配文件名变了URL就变了缓存自然失效。在LNMP架构下做前端资源发布时建议遵循一条原则静态资源文件名变更走重命名式更新不要覆盖式更新。这样既能享受长缓存带来的性能收益又不至于让用户看到破旧的页面样式。6.3 进阶方向从LNMP到HTTPS与负载均衡LNMP跑通、动静分离做完基本功就已经扎实了一大半。下一步可以考虑的扩展方向有三个。HTTPS证书部署。用certbot申请Lets Encrypt免费证书在Nginx配置里增加443端口监听和证书路径把location /中的HTTP请求做301跳转到HTTPS。这一块不难关键是证书自动续期的定时任务不要漏掉。PHP-FPM进程数调优。编辑/etc/php-fpm.d/www.conf中的pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers四个参数需要配合服务器内存大小来设置。每个PHP进程平均占内存约30~50MB一台2GB内存的机器建议把max_children设为20~30太高了会导致内存耗尽太低了并发一上来就502。多站点配置。在/etc/nginx/conf.d/下新增不同的配置文件每个server块绑定不同的域名和站点根目录。注意每个server里都要有独立的root和index否则所有站点都会落到同一个目录里。我个人在实际部署中的体会是LNMP这套架构的价值不在于单个组件多强大而在于组件间的协作方式足够清晰——静态请求和动态请求各走各的通道互不拖累。把这份协作思路吃透后面再去理解Nginx的负载均衡、反向代理、缓存策略基本上就是水到渠成的事。最后再分享一个小技巧写完Nginx配置后改动前一定先备份改动后一定先执行nginx -t再reload。我被改了个配置顺手reload结果直接502这件事教育过太多次现在每次动配置都遵循这个流程基本没再出过生产事故。