ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

nginx + php-fpm 架构详解:从配置到性能调优的实战指南

nginx + php-fpm 架构详解:从配置到性能调优的实战指南 从 Apache 切到 nginx php-fpm 那一年我大约折腾了一周才把线上所有站点迁移干净。中间踩过的坑不少后来帮同事排查问题时发现大家纠结的无非就那么几件事环境装好了但访问 502、动态页面能跑但静态资源全挂、配了 socket 却报权限错误、服务器内存明明够却频繁白屏。其实这些问题背后都指向同一个点——对 nginx 和 php-fpm 之间的协作方式理解不透。这篇文章我会从架构思路讲起把环境搭建、核心配置、性能调优、问题排查这几块完整过一遍重点是解释每个配置背后的为什么以及在真实部署中容易踩的坑。不管你是第一次搭建 PHP 环境的入门者还是接手了一台线上服务器需要快速定位问题的运维这篇文章都能帮上忙。1. 为什么推荐 nginx php-fpm架构取舍与适用场景1.1 从 Apache mod_php 切过来的那些理由很多人第一次认识 PHP 是在 Apache 的环境里mod_php方式安装简单把模块加载进 Apache 就能跑。但这种模式的短板很明显Apache 采用进程/线程模型每个请求要占用一定内存在并发上来之后资源消耗很凶。曾经有个压测数据让我印象很深——同样的 2C4G 机器Apache 跑到 500 并发时 CPU 已经接近满载切换到 nginx php-fpm 之后同等并发下 CPU 只用了六成吞吐量反而翻了一倍多。nginx 的高性能主要来自它的事件驱动架构。它不像传统 Web 服务器那样为每个连接分配一个进程而是用少量 worker 进程配合 epoll 这类事件机制处理大量连接。换句话说nginx 擅长的是接待,真正干活的 PHP 代码执行是交给 php-fpm 完成的。两者分工协作各管一摊才是这套架构的核心思想。1.2 一次完整请求的链路拆解先看一个最常见的场景用户在浏览器输入http://example.com/index.php然后按下回车这背后发生了什么浏览器请求先到达 nginx 的 worker 进程nginx 根据server_name和location规则判断这个请求该由哪个站点处理。如果命中的是location ~ \.php$这样的规则nginx 不会自己去解析执行 PHP 代码——它根本没这能力。nginx 会把请求通过 fastcgi 协议转发给 php-fpm 监听的地址php-fpm 的 master 进程收到后把任务交给空闲的 worker 进程去执行 PHP 脚本脚本处理完的结果再沿原路返回给 nginx最后由 nginx 组装成 HTTP 响应返回给浏览器。理解这个链路有很实际的意义当你看到 502 错误时问题几乎总是出在nginx 连不上 php-fpm这一环当你看到 504 时问题多半出在php-fpm 执行太慢这一环。链路先搞清楚了排查问题的思路就清晰一大半。1.3 什么情况下不适合这组组合nginx php-fpm 是个好搭档但并非所有场景都适合。如果你的项目完全没用到 PHP只是几个静态页面那用 nginx 直接托管文件就行不需要装 php-fpm 画蛇添足。反过来如果你的服务器上跑的是重量级的 PHP 单体应用且所有请求都需要 PHP 处理那 nginx 的作用主要是充当流量入口、做静态资源缓存和负载均衡真正性能瓶颈反而在 php-fpm 的进程池大小和 PHP 代码质量上。还有一类场景要特别提醒如果你要用 PHP 做 WebSocket 长连接服务php-fpm 默认的短生命周期进程模型并不擅长管理持久连接建议改用 Swoole 这类常驻内存方案。所以架构选型这件事别只看别人都在用得对着自己的业务场景想清楚。2. 环境准备与安装从零到能跑2.1 版本选择与前置检查动手安装前先把版本选对。nginx 目前主力版本是 1.24 和 1.26 系列建议直接用系统的稳定版或者官方源里的最新稳定版。PHP 的版本决定了后续很多行为的差异PHP 7.4 之前和之后php-fpm的配置方式几乎一致但 PHP 8.0 开始引入了 JIT、属性类型增强等能力对性能影响不小。我个人的建议是新项目直接上 PHP 8.1 或 8.2老项目如果跑在 7.4 且升级代价大也可以先维持但至少要保证 PHP 8 仍在官方安全支持期内。安装前先做三件事看操作系统版本和架构cat /etc/os-release要确认 CentOS 7 还是 Ubuntu 22.04安装命令差异很大确认 80/443 端口没被占用ss -lntp | grep -E :(80|443)否则装完发现端口冲突排查起来相当费时间确认服务器内存和 CPU 核心数free -h和nproc这两个数字后面调优 php-fpm 参数时要用。2.2 包管理器安装Ubuntu 与 CentOS 两条路线Ubuntu / Debian 系是最省心的路线官方源里 nginx 和 php-fpm 都有现成的包。如果你只是想快速起一个环境直接执行sudo apt update sudo apt install -y nginx php-fpm装完以后Ubuntu 会自动把 php-fpm 注册成 systemd 服务启动命令是sudo systemctl start php8.1-fpm具体版本号视系统而定。nginx 的配置目录在/etc/nginx/站点的默认配置在/etc/nginx/sites-available/defaultphp-fpm 的配置文件在/etc/php/8.1/fpm/其中 pool 配置在pool.d/www.conf。CentOS / RHEL 系要注意CentOS 7 默认源里的 nginx 版本太老php-fpm 干脆没有。通常的做法是先加 EPEL 和 Remi 源sudo yum install -y epel-release sudo yum install -y nginx sudo yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm sudo yum install -y php81-php-fpmCentOS 7 默认源里的 PHP 最高只有 5.4这个版本已经停止维护很多年了如果有安全合规要求强烈建议通过 Remi 源装高版本。CentOS 8/Stream 或 Rocky Linux 上的操作类似命令大同小异。安装完成后一定要做验证动作别急着配站点。依次执行php -v php-fpm -t sudo systemctl status php-fpm nginx -tphp-fpm -t是检查配置语法nginx -t同样如此这两条命令应该被刻进肌肉记忆里——每次改完配置都先跑一遍能避免大量低级错误。2.3 什么时候才需要编译安装网上很多教程推崇源码编译安装 nginx原因无非是能自定义模块、能加第三方扩展。但我的经验是除非你有极特殊的需求否则不要在一台生产服务器上花时间编译原因有三官方源和发行版源里的 nginx 经过充分测试安全补丁跟得更及时编译安装后一些路径和配置文件的组织方式和发行版不一致后续维护容易踩坑第三方模块比如需要自己编译的 headers-more 模块多数可以通过动态模块方式加载不必整体重编。真正需要编译安装的场景主要是 nginx 需要集成特定版本的openssl、pcre或者在架构特殊的嵌入式平台比如前面热词里提到的aarch64 纯内网环境上做离线部署。这时才建议走源码编译路线。不过那属于相对边缘的场景日常用包管理器完全够使。3. 核心配置拆解fastcgi 通信与动静分离3.1 unix socket 与 TCP 两种通信方式怎么选nginx 转发 PHP 请求给 php-fpm 的核心指令是fastcgi_pass它有两种写法对应两种通信方式# 方式一unix socket常用路径形如 fastcgi_pass unix:/run/php/php8.1-fpm.sock; # 方式二TCP监听的端口默认 9000 fastcgi_pass 127.0.0.1:9000;这两种方式该怎么选我用一个生活化的类比来解释unix socket 就像同办公室的两个人直接递纸条不需要经过邮政系统速度快、开销小TCP 就像隔着一条街打电话虽然后者能跨机器、跨网络但多了一层网络协议栈的开销时延和 CPU 消耗都会增大。如果你的 php-fpm 和 nginx 在同一台机器上优先用 unix socket。实测下来小并发场景两者差别不大但高并发下 socket 能省下一笔可观的内核态开销。唯一要留意的是 socket 文件的权限问题这个后面专门讲。如果你的 php-fpm 跑在别的机器上那就只能走 TCP 了这时要确保只监听内网地址比如127.0.0.1:9000或内网 IP别暴露到公网。3.2 fastcgi_params 和 fastcgi.conf 的细微差别配置 PHP 转发时有两句指令经常出现include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;第一句把 nginx 里的常见变量映射成 CGI 环境变量传给 php-fpm第二句告诉 php-fpm 要执行哪个 PHP 文件这是整个配置里最容易出错的地方。fastcgi_params和fastcgi.conf的区别在于fastcgi.conf里默认带了SCRIPT_FILENAME这一行而fastcgi_params里没有。如果你include fastcgi_params却忘了手动加SCRIPT_FILENAME会发现访问 PHP 页面一直返回空白或者 File not found。这个坑我见太多人踩过了。关键点在location ~ \.php$这个块里SCRIPT_FILENAME必须用$document_root$fastcgi_script_name或者$request_filename。前者是标准的写法意思是从 nginx 的root配置项拼接出文件的完整路径。如果用了$document_root但root路径配错php-fpm 就会告诉你找不到文件。3.3 一份可直接套用的站点配置下面是我常用的一个 PHP 站点配置文件适用于 Laravel、ThinkPHP 这类框架也适用于传统的index.php入口模式。把server块放到/etc/nginx/conf.d/example.conf后重载即可server { listen 80; server_name example.com www.example.com; root /var/www/example/public; index index.php index.html; # 日志配置分开记录便于排查 access_log /var/log/nginx/example.access.log; error_log /var/log/nginx/example.error.log; # 处理正常请求优先检查文件是否存在否则交给 index.php location / { try_files $uri $uri/ /index.php?$query_string; } # PHP 请求统一走 fastcgi location ~ \.php$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS on; # 如果你开了 SSL记得传这个参数 } # 静态文件单独处理加长缓存时间 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; access_log off; try_files $uri 404; } # 禁止访问隐藏文件 location ~ /\. { deny all; } }这套配置经历了多次生产环境验证可以直接抄。框架的伪静态规则通过try_files实现不需要rewrite那些繁琐规则。location /里的try_files $uri $uri/ /index.php?$query_string意思是请求先找真实文件找不到再找目录还找不到就交给index.php让框架路由去处理。这种写法是 Laravel、Symfony 等主流框架的标准做法。4. php-fpm 进程池调优参数计算与压测验证4.1 三种进程管理模式何时用哪种php-fpm 的进程池配置在www.conf里核心参数是pm。它有三个可选值代表了三种截然不同的进程管理策略模式行为适用场景static固定大量 worker 进程常驻内存内存充裕、流量稳定的业务dynamic动态伸缩有上下限和起步值大多数常规业务默认推荐ondemand有请求才启动进程空闲回收内存紧张、流量波动大的场景dynamic和ondemand的区别在于前者会维持一定数量的空闲进程来应对突发请求后者完全按需创建进程启动的延迟会高一些。我自己的习惯是生产环境内存大于 4G 用dynamic小于 2G 用ondemand用static的时机非常少。一个dynamic模式的标准配置长这样pm dynamic pm.max_children 50 pm.start_servers 10 pm.min_spare_servers 5 pm.max_spare_servers 15 pm.max_requests 1000pm.max_requests这个参数很多人会忽略它的作用是让每个 worker 处理足够多的请求后主动退出重建避免长期运行的进程积累内存碎片和代码状态污染。我把它设成 1000 到 5000 之间收益明显。4.2 max_children 怎么算内存预算公式pm.max_children直接决定了 php-fpm 最多能同时处理多少请求这是整个调优环节里最重要的决策。如果硬编码一个数字往往会导致两个极端设置太小并发一高就 502设置太大内存直接打满触发 OOM Killer。正确的做法是从内存出发反推。假设你的服务器物理内存是 4G操作系统和 nginx 等基础服务预留 1G剩下 3G 可以给 PHP 用。然后你跑这样一个命令看单个 PHP 进程的平均占用ps aux | grep -E php-fpm | grep -v grep | awk {sum $6} END {print sum/NR/1024 MB}假设结果是 30MB那么理论上的最大进程数就是max_children 可用内存 / 单进程内存 (4096 - 1024) / 30 ≈ 102出于保守考虑我会再打八折也就是设成 80 左右别把内存压到极限。因为 PHP 进程的内存占用不是恒定的——业务高峰期跑稍微复杂的查询单进程可能飙到 80MB 甚至更高。留出余量否则流量一冲就全线崩溃。4.3 慢日志与超时参数设置排查 PHP 性能问题最趁手的工具是慢日志。在www.conf里加这几行slowlog /var/log/php-fpm-slow.log request_slowlog_timeout 5s request_terminate_timeout 120srequest_slowlog_timeout设为 5 秒意思是执行超过 5 秒的请求会记录到慢日志里并打印出当时 PHP 函数的调用堆栈。这个堆栈就是定位性能瓶颈的线索——是数据库查询慢、外部 API 调用慢还是某个函数死循环一眼就能看出来。request_terminate_timeout设成 120 秒超过时间直接掐断。这个参数设太短会让长时间运行的任务比如导入导出、批量计算被误杀设太长又会让僵死的请求长期占用 worker。120 秒是一个大多数场景都能接受的中间值。4.4 状态页与压测工具的使用php-fpm 自带一个状态页方便观察实时运行指标。配置方法是在www.conf里加pm.status_path /php_fpm_status然后在 nginx 站点配置里为这个路径单独建一个 location并做访问限制location /php_fpm_status { fastcgi_pass unix:/run/php/php8.1-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/php_fpm_status.php; allow 127.0.0.1; deny all; }配置好之后访问http://127.0.0.1/php_fpm_status就能看到accepted conn累计接收连接数、listen queue等待队列长度、active processes当前活跃进程数等指标。listen queue大于 0 通常意味着请求在排队说明max_children可能不够了。压测工具方面我常用abApacheBench做快速验证。例如模拟 1000 个请求、100 并发ab -n 1000 -c 100 http://example.com/index.php重点看两个指标Requests per second吞吐量和Failed requests失败请求数。压测时如果出现大量失败配合上面状态页的指标基本就能确定是进程池容量问题还是业务代码问题。5. 常见问题排查从 502 到 504 的完整思路5.1 502 Bad Gateway 的五种典型原因502 是 nginx php-fpm 环境里出现频率最高的错误。我把典型原因和排查方法整理成了一份速查表现象原因排查与解决重启后立刻 502php-fpm 服务没启动sudo systemctl status php8.1-fpm然后启动一直 502但进程存在fastcgi_pass地址与 php-fpm 监听地址不一致检查监听ss -lx | grep php与配置比对connect() failed (13: Permission denied)socket 文件权限不足配置listen.owner、listen.group并确保 nginx 用户可读压测中 502max_children设置过小进程池被打满查看状态页listen queue适当增大max_children偶发 502日志无异常某些 PHP 进程触发了request_terminate_timeout被 kill调大超时或用慢日志定位超时请求重点是第二条。很多人配 nginx 时发现 php-fpm 明明启动了但ss -lx | grep php看到的 socket 路径和fastcgi_pass不一致——比如 Ubuntu 上 php-fpm 实际监听的是php8.1-fpm.sock而你在 nginx 里写的是php-fpm.sock那当然是 502。先验证监听路径再改配置。5.2 504 Gateway Timeout慢脚本与超时设置502 是连不上504 是等太久。nginx 在转发请求给 php-fpm 后如果等待响应超过了自身的超时时间就会返回 504。与 504 相关的有三个超时参数fastcgi_connect_timeout 10s; fastcgi_send_timeout 60s; fastcgi_read_timeout 60s;fastcgi_read_timeout是等待 php-fpm 返回响应的超时时间默认 60 秒。如果你的业务里有导入大量数据的脚本跑一次要几分钟那就得把这个参数调大否则脚本执行到一半会被 nginx 中断。但调大 nginx 超时只是治标。真正要治本得通过慢日志看是哪个脚本慢了。我就遇到过一个案例线上 504 数据持续上升慢日志显示某个报表接口的 SQL 没走索引全表扫描花了 40 秒。这时候加个联合索引接口耗时降到 200 毫秒504 自然消失。先看日志定位根因再考虑调参。5.3 静态文件 404 与路径匹配问题配置好 PHP 站点后经常出现的一个现象是动态页面能打开但图片、CSS、JS 全部 404。这个问题的根源大多数是location匹配顺序导致的。nginx 的location匹配优先级从高到低是精确匹配 前缀匹配^~ 正则匹配~ 普通前缀匹配。如果你写了一个比较宽泛的location /配了try_files又期望静态文件被某个正则在后面兜住一旦顺序不对正则就不会生效。我推荐的做法是上面配置示例里的写法把静态文件的正则匹配放在location /之后nginx 会先处理location /的前缀匹配再继续找正则。只要location ~* \.(js|css|png|...)$存在静态文件就会被正确命中。另外注意try_files $uri 404的兜底作用——如果静态文件确实不存在就返回 404 而不要再转给 PHP。否则一个不存在的图片路径会被 PHP 框架接管白白浪费资源。5.4 权限、SELinux 与安全加固这套环境的权限问题主要集中在两个方面一是 socket 文件权限二是站点目录的读写权限。socket 文件权限的典型解法是在www.conf里显式指定属主和属组listen.owner www-data listen.group www-data listen.mode 0660同时用user www-data和group www-data让 php-fpm 进程以该用户运行。这样 nginx 的 worker 进程同样以 www-data 运行才能通过这个 socket 通信。如果是 CentOS注意系统有nginx用户也有apache用户httpd到底用哪个取决于你 nginx 的启动用户。目录权限方面站点根目录的所有者建议设为运行 php-fpm 的用户sudo chown -R www-data:www-data /var/www/example sudo find /var/www/example -type d -exec chmod 755 {} \; sudo find /var/www/example -type f -exec chmod 644 {} \;如果业务需要上传文件那就至少保证上传目录本身可写别图省事把整个站点目录都改成 777安全隐患很大。CentOS 上还有个常见的隐形杀手是 SELinux。在 CentOS 系列系统上即使权限配置看起来完全正常nginx 也可能访问不了 PHP socket或者反向代理时连接不上后端端口。快速判断是不是 SELinux 的问题setenforce 0 # 临时关闭 SELinux如果关闭后问题消失那基本确认就是 SELinux 拦截了。这时候不要直接永久关闭 SELinux生产环境安全要求不允许应该单独放行 nginx 的网络权限setsebool -P httpd_can_network_connect 1 setsebool -P httpd_can_network_relay 15.5 平滑重载与日志切割管理最后聊一下日常维护。修改了 nginx 配置后的标准操作是nginx -t nginx -s reloadnginx -t检查语法nginx -s reload让主进程重新加载配置。reload 是平滑的——正在处理请求的 worker 会继续完成手上的工作新的 worker 再按新配置启动不会产生中断。PHP 配置修改后同理php-fpm -t sudo systemctl reload php8.1-fpm有人喜欢直接restart但 restart 会中断所有正在处理的请求线上环境尽量用reload。如果 nginx 版本升级需要换二进制文件平滑升级比 reload 更彻底那属于相对少见的操作建议参考官方文档谨慎执行。日志切割是运维中很容易被忽略的事。nginx 和 php-fpm 的日志会无限增长时间一长可能撑爆磁盘。Linux 自带的logrotate通常会在安装软件时自动配置好但你要定期检查它的切分策略是否满足需求。比如确认/etc/logrotate.d/nginx里daily和周保留份数是否合理一般保留 7 到 14 份就够了。我个人在实际操作中的一个体会是这套环境真正难的不是安装过程而是出了问题之后能否快速定位是哪个环节挂了。我的习惯是先看/var/log/nginx/error.log再看/var/log/php-fpm.log接着用ss或ps确认进程和监听状态。把这条排查路径固定下来绝大多数问题都能在几分钟内定位。最后再分享一个小技巧每次改完配置除了nginx -t顺手敲一条curl -I http://127.0.0.1/index.php确认返回的 HTTP 状态码是 200 再收工。线上环境的稳定性就是靠这些看似琐碎的习惯一点点堆出来的。
返回列表