4.3 动态内容:CGI、FastCGI与PHP集成


文档摘要

4.3 动态内容:CGI、FastCGI 与 PHP 集成 本节摘要:动态内容的生成方式经历了三代:CGI 每请求起一个进程,快写快死但代价惨烈;modphp 把解释器嵌进 httpd,省了进程开销却把两者焊死;FastCGI/PHP-FPM 用常驻进程池承接请求,实现了"服务器与解释器解耦"。本节拆解三代方案的进程模型与演进逻辑,给出 PHP-FPM 与 httpd 集成的完整配置,并回答那个经典问题——为什么该告别"Apache 直接跑 PHP"。 学完这节你能做什么 阅读完本节,你应当能够: 画出三种动态内容方案的进程模型并说明请求流转路径; 解释 CGI 高代价的根源与 FastCGI 的改进点; 配置 httpd 经 proxyfcgi 对接 PHP-FPM;

4.3 动态内容:CGI、FastCGI 与 PHP 集成

本节摘要:动态内容的生成方式经历了三代:CGI 每请求起一个进程,快写快死但代价惨烈;mod_php 把解释器嵌进 httpd,省了进程开销却把两者焊死;FastCGI/PHP-FPM 用常驻进程池承接请求,实现了"服务器与解释器解耦"。本节拆解三代方案的进程模型与演进逻辑,给出 PHP-FPM 与 httpd 集成的完整配置,并回答那个经典问题——为什么该告别"Apache 直接跑 PHP"。

学完这节你能做什么

阅读完本节,你应当能够:

  1. 画出三种动态内容方案的进程模型并说明请求流转路径;
  2. 解释 CGI 高代价的根源与 FastCGI 的改进点;
  3. 配置 httpd 经 proxy_fcgi 对接 PHP-FPM;
  4. 理解 PHP-FPM 进程池参数与第 2 章 MPM 参数的关系;
  5. 判断自己环境该用哪一代方案。

CGI:一切的开端,也是一切问题的源头

CGI(公共网关接口)是 Web 服务器与外部程序之间最古老的合同:服务器把请求信息塞进环境变量,起一个子进程执行脚本,脚本从标准输入读请求体、向标准输出写响应,写完进程退出。

合同的简洁成就了它的普及——任何语言写的程序都能当 CGI 脚本。但代价藏在进程模型里:每个请求一次完整的进程创建与销毁。对解释型语言尤其残忍:起一个解释器、加载运行时库、编译脚本、执行、销毁,真正的业务执行可能只占整个生命周期的零头。一秒十个请求就是一秒十次"开机—关机"。

今天 CGI 在生产里近乎绝迹,只剩两个残留场景:极低频的管理脚本(一天被调用几次,性能无所谓);以及嵌入式设备的迷你服务器。认识它的意义在于理解演进:后面两代方案都在解决同一个问题——别为每个请求付一次启动费

mod_php:把厨房搬进餐厅

第二代的思路激进:干脆把 PHP 解释器编译成 httpd 的模块,直接长在服务器进程里。请求进来,工作进程自己就是解释器,零进程创建开销,还能缓存编译结果。

但"长在身上"带来三个结构性问题。命运绑定:PHP 与 httpd 同生共死,升级 PHP 必须重启整个 Web 服务器,所有站点一起停。模型绑定:解释器要跟着 MPM 走,老的非线程安全 PHP 只能配 prefork——第 2 章说过,这等于放弃了 event 的现代并发能力。资源错配:每个 httpd 工作进程都背着一份完整的解释器与扩展内存,哪怕它在伺候纯静态请求;prefork 下 256 个进程就是 256 份 PHP 运行时,静态内容被动态内容的地板价拖累。

FastCGI 与 PHP-FPM:常驻的车间

第三代的解法是回到独立进程、但不再用完即弃:应用服务器常驻运行,维护一个工作进程池,通过套接字接收 Web 服务器转发的请求,处理完进程不退、回到池里等下一个。启动费一次付清,之后每个请求只付"执行费"。

PHP-FPM 是 PHP 官方的 FastCGI 进程管理器,就是这套思想的工业实现。httpd 侧通过 proxy_fcgi 模块对接(这正好复用上一章的代理知识——FastCGI 本质上是一种专用代理协议):

# 把 php 请求交给 fpm 套接字 <FilesMatch \.php$> SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost" </FilesMatch> # 目录索引包含 index.php DirectoryIndex index.php index.html

这一行 SetHandler 的意思是:凡匹配的文件,处理器是"代理到本地 unix 套接字上的 FastCGI 服务"。路径分隔符的写法(竖杠连接)是固定格式,记不住没关系,发行版的 php 包通常自带这份配置片段。

PHP-FPM 侧的进程池参数(它的池配置里)与第 2 章 MPM 的思路完全同构:

pm = dynamic pm.max_children = 30 ; 池内最大工作进程数,按"可用内存÷单进程内存"算 pm.start_servers = 6 pm.min_spare_servers = 4 pm.max_spare_servers = 12

这里出现了一个新手常踩的双重容量问题:httpd 的 MPM 有自己的工作线程上限,PHP-FPM 有自己的进程上限,两者独立调参。若 httpd 允许 800 并发而 FPM 只有 30 个进程,超出的动态请求会在套接字排队,表现为"静态快、动态慢";反之 FPM 开太大则内存爆。正确算法是从后往前:先按内存算 FPM 的 max_children,再让 MPM 的并发能力与"动态请求占比乘 FPM 容量"匹配,而不是两边各自拍脑袋。

图 4-3 三代动态内容方案的请求路径

图 4-3 三代动态内容方案的请求路径

验证集成与常见故障

集成完成的验证三连:

# 1 fpm 服务在跑、套接字存在 systemctl status php8.2-fpm --no-pager | head -3 ls -l /run/php/php8.2-fpm.sock # 2 php 页面真的经过 fpm:输出 phpinfo 的 Server API 应为 FPM curl -s https://www.example.com/info.php | grep -o "FPM-FCGI" | head -1 # 3 源码绝不外泄:请求一个 php 文件应执行而非显示源码 curl -s https://www.example.com/index.php | head -3

两类高频故障。页面显示源码:SetHandler 没生效——文件匹配规则写错、proxy_fcgi 没加载、或重写规则抢先把请求映射成了静态文件。间歇性 502:FPM 池打满,请求在套接字排队超时——按上节的容量算法检查 max_children,同时看 FPM 慢日志(池配置里开 slowlog)定位拖慢的脚本。

一个值得写进部署清单的实践:FPM 的池可以按站点拆分——每个站点一个池、独立用户与进程参数。共享主机时代这是隔离的基石,自有服务器上它同样是"一个站点的慢脚本不拖垮别人"的保险。

与代理路线合流

把本章两条线合起来看会发现它们是同一件事:动态内容的处理,本质都是"把生成工作交给专门的进程"。区别只在近远——PHP-FPM 在本机套接字一墙之隔,应用服务器在隔壁机房走 HTTP。所以上一章的故障排查(502/504、超时、容量匹配)在本节原样适用,只是对象从"远端集群"换成了"本机进程池"。

选型判断收拢成三句话:遗留 mod_php 环境先迁到 FPM 再谈其他优化;本机 PHP 用 FPM 套接字对接;跨语言后端(Python、Node、Java)一律走上一章的反向代理。

本节要点回顾

  • CGI:每请求一进程,启动费全额,生产近乎绝迹但概念是全部演进的原点;
  • mod_php:零进程开销但三重绑定——版本、MPM、内存,是 prefork 时代的化石;
  • FPM 常驻池:启动费一次付清,服务器与解释器解耦,httpd 得以回归 event;
  • 对接配置:SetHandler 指向 unix 套接字,一行完成;
  • 双重容量:先算 FPM 的 max_children,再配 MPM 匹配,方向不能反;
  • 故障对照:源码外泄查 SetHandler,间歇 502 查池容量与慢日志。

💡 一句话记住本节:三代方案的全部历史,就是一部"把启动费摊薄到无穷小"的奋斗史。

旅程的生成区到此完结。下一章响应踏上归途:缓存、压缩与加密。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U