Request 到 Response 的事件点列表
解读
国内一线/二线公司面试时,这道题常被用来区分“只会写业务代码”与“真正理解 PHP 生命周期”的候选人。面试官想听到的是:一次 HTTP 请求从进入 FPM/Apache 到浏览器收到响应,PHP 内核、SAPI、扩展、框架、业务代码到底在哪些“事件点”上依次触发,以及你能不能在每个点上给出可落地的钩子或调试手段。回答时务必按时间线展开,兼顾 CLI、FPM、Swoole 三种主流运行时差异,并指出线上排障与性能调优常用的观测位置。
知识点
- SAPI 层:fpm_main、apache_handler、cli 三种入口差异
- PHP 生命周期宏:MINIT、RINIT、RSHUTDOWN、MSHUTDOWN
- 内核钩子:compile_file、zend_execute_ex、zend_error_cb
- FPM 特有事件:fastcgi_request_before、fastcgi_request_after、fpm_scoreboard_update
- 扩展钩子:opcache_compile_file、xdebug_collect_params、swoole_server_onRequest
- Composer 自动加载机制:loadClass 与 preload_file 触发点
- 框架级事件:Laravel Kernel handle → send、Symfony kernel.request / kernel.response
- 输出缓冲区:ob_start 层级、implicit_flush、fastcgi_buffer_size
- 错误与异常:set_error_handler、set_exception_handler、register_shutdown_function
- 性能观测:tideways/xhprof 采样点、opcache_reset 时机、strace 系统调用边界
答案
以 PHP-FPM + Laravel 为例,一次 Request → Response 的完整事件点如下(按时间顺序,给出可观测的函数或钩子名):
- Nginx 接收 80/443 端口数据 → 通过 FastCGI 协议转发到 php-fpm:9000
- FPM master 进程 accept,worker 进程被唤醒,触发 fpm_request_info() 初始化
- 进入 SAPI 层 php_fpm_request_startup(),内核依次调用
3.1 MINIT(仅首次启动)
3.2 RINIT 所有扩展,例如 session RINIT 生成 PHPSESSID - 激活 OPcache:若脚本已缓存,直接命中;未缓存则进入 zend_compile_file() 编译为 opcodes
- 调用 zend_execute_ex() 开始执行主脚本:public/index.php
- Composer 自动加载器注册 spl_autoload_register(),首次加载类时触发 loadClass 事件
- 创建 Laravel Application 容器,触发 bootstrapping 事件序列:
7.1 LoadEnvironmentVariables
7.2 LoadConfiguration
7.3 HandleExceptions → set_error_handler / set_exception_handler
7.4 RegisterFacades
7.5 RegisterProviders → 执行各 ServiceProvider 的 register()
7.6 BootProviders → 执行 boot(),可监听 artisan 事件 - HTTP Kernel 接管:request)
8.1 事件 kernel.request:中间件 StartSession、EncryptCookies、Authenticate 依次触发
8.2 路由匹配 Router::dispatch(),若命中路由,触发 route.matched
8.3 中间件管道层层进/出,可观测点 pipeline:then()
8.4 进入控制器方法,业务代码开始执行
8.5 控制器返回响应前,可触发 kernel.controller_arguments - 生成 Response 对象,触发 kernel.response:
9.1 AddQueuedCookiesToResponse
9.2 ShareErrorsFromSession
9.3 中间件 AfterMiddleware 可做跨域、压缩 gzip - 发送响应体:send() → fastcgi_send_response(),内核层 flush,输出缓冲区 ob_end_flush() 逐级关闭
- 终止阶段:register_shutdown_function() 最先触发,可写日志、上报 Trace
- 调用 request, $response),触发 kernel.terminate,可用于异步队列、延迟任务
- FPM 调用 php_request_shutdown(),进入 RSHUTDOWN 序列:
13.1 保存 $_SESSION 到存储
13.2 释放内存、关闭数据库连接
13.3 若开启 opcache.validate_timestamps,检查文件变更 - worker 进程回归空闲池,fpm_scoreboard 更新状态为“idle”,等待下一次请求
线上排障常用观测命令:
- 查看 FPM 事件:strace -p
pgrep php-fpm | head -1-e trace=accept,read,write - 查看内核编译:php -d opcache.enable_cli=1 -d opcache.opt_debug_level=0x10000 public/index.php
- 查看框架事件:php artisan event:list | grep kernel
- 查看扩展 RINIT:gdb -p
pgrep php-fpm→ b zif_session_start
拓展思考
- 如果切换到 Swoole 协程模式,事件点会多出 onWorkerStart、onRequest、协程调度器切换,传统 RINIT/RSHUTDOWN 不再每次请求触发,如何重置静态变量与连接池?
- 在微服务网关层做灰度发布,想在第 8 步 kernel.request 之前动态替换 $_SERVER['HOST'],有哪些不重启 FPM 的注入方案?(openresty + lua、nginx map、或者 preload 脚本)
- OPcache preload 在 MINIT 阶段就把框架类加载到持久内存,若业务代码热更新,如何设计“文件变更→preload 重载→无中断”流水线?
- 当请求出现 502/504,如何根据事件点快速定位是“内核 RSHUTDOWN 超时”还是“框架 terminate 死循环”?(结合 php-fpm slowlog、strace 时间戳、register_shutdown_function 最后日志)