Request 到 Response 的事件点列表

解读

国内一线/二线公司面试时,这道题常被用来区分“只会写业务代码”与“真正理解 PHP 生命周期”的候选人。面试官想听到的是:一次 HTTP 请求从进入 FPM/Apache 到浏览器收到响应,PHP 内核、SAPI、扩展、框架、业务代码到底在哪些“事件点”上依次触发,以及你能不能在每个点上给出可落地的钩子或调试手段。回答时务必按时间线展开,兼顾 CLI、FPM、Swoole 三种主流运行时差异,并指出线上排障与性能调优常用的观测位置。

知识点

  1. SAPI 层:fpm_main、apache_handler、cli 三种入口差异
  2. PHP 生命周期宏:MINIT、RINIT、RSHUTDOWN、MSHUTDOWN
  3. 内核钩子:compile_file、zend_execute_ex、zend_error_cb
  4. FPM 特有事件:fastcgi_request_before、fastcgi_request_after、fpm_scoreboard_update
  5. 扩展钩子:opcache_compile_file、xdebug_collect_params、swoole_server_onRequest
  6. Composer 自动加载机制:loadClass 与 preload_file 触发点
  7. 框架级事件:Laravel Kernel handle → send、Symfony kernel.request / kernel.response
  8. 输出缓冲区:ob_start 层级、implicit_flush、fastcgi_buffer_size
  9. 错误与异常:set_error_handler、set_exception_handler、register_shutdown_function
  10. 性能观测:tideways/xhprof 采样点、opcache_reset 时机、strace 系统调用边界

答案

以 PHP-FPM + Laravel 为例,一次 Request → Response 的完整事件点如下(按时间顺序,给出可观测的函数或钩子名):

  1. Nginx 接收 80/443 端口数据 → 通过 FastCGI 协议转发到 php-fpm:9000
  2. FPM master 进程 accept,worker 进程被唤醒,触发 fpm_request_info() 初始化
  3. 进入 SAPI 层 php_fpm_request_startup(),内核依次调用
    3.1 MINIT(仅首次启动)
    3.2 RINIT 所有扩展,例如 session RINIT 生成 PHPSESSID
  4. 激活 OPcache:若脚本已缓存,直接命中;未缓存则进入 zend_compile_file() 编译为 opcodes
  5. 调用 zend_execute_ex() 开始执行主脚本:public/index.php
  6. Composer 自动加载器注册 spl_autoload_register(),首次加载类时触发 loadClass 事件
  7. 创建 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 事件
  8. HTTP Kernel 接管:kernel>handle(kernel->handle(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
  9. 生成 Response 对象,触发 kernel.response:
    9.1 AddQueuedCookiesToResponse
    9.2 ShareErrorsFromSession
    9.3 中间件 AfterMiddleware 可做跨域、压缩 gzip
  10. 发送响应体:send() → fastcgi_send_response(),内核层 flush,输出缓冲区 ob_end_flush() 逐级关闭
  11. 终止阶段:register_shutdown_function() 最先触发,可写日志、上报 Trace
  12. 调用 kernel>terminate(kernel->terminate(request, $response),触发 kernel.terminate,可用于异步队列、延迟任务
  13. FPM 调用 php_request_shutdown(),进入 RSHUTDOWN 序列:
    13.1 保存 $_SESSION 到存储
    13.2 释放内存、关闭数据库连接
    13.3 若开启 opcache.validate_timestamps,检查文件变更
  14. 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

拓展思考

  1. 如果切换到 Swoole 协程模式,事件点会多出 onWorkerStart、onRequest、协程调度器切换,传统 RINIT/RSHUTDOWN 不再每次请求触发,如何重置静态变量与连接池?
  2. 在微服务网关层做灰度发布,想在第 8 步 kernel.request 之前动态替换 $_SERVER['HOST'],有哪些不重启 FPM 的注入方案?(openresty + lua、nginx map、或者 preload 脚本)
  3. OPcache preload 在 MINIT 阶段就把框架类加载到持久内存,若业务代码热更新,如何设计“文件变更→preload 重载→无中断”流水线?
  4. 当请求出现 502/504,如何根据事件点快速定位是“内核 RSHUTDOWN 超时”还是“框架 terminate 死循环”?(结合 php-fpm slowlog、strace 时间戳、register_shutdown_function 最后日志)