必须在中间件 handle() 中用 microtime(true) 埋点,起点在最前、终点在 $next($request) 返回后,不可依赖 $_SERVER['REQUEST_TIME_FLOAT'];需覆盖异常路径,写入响应头与结构化日志,并避开静态资源、Swoole 同步日志及 DEBUG 模式干扰。

必须在中间件的 handle() 方法里用 microtime(true) 埋点,起点放最前、终点放 $next($request) 返回后,别信 $_SERVER['REQUEST_TIME_FLOAT'] —— 它可能被代理或 CLI 环境污染。
为什么不能只在控制器里统计
控制器执行时,中间件链、模型初始化、查询构造、模板编译等环节早已发生。你只在 index() 里打时间戳,漏掉的耗时可能占总响应时间 60% 以上。真实基线必须从框架真正接管请求那一刻开始算起,也就是全局中间件的 handle() 入口。
- ThinkPHP 的
think\App::run()启动后才进入应用层,但中间件加载本身也有开销; - 入口前的路由解析、配置加载不计入,但中间件是第一个可编程拦截点;
- 异常路径(如中间件抛出
ValidateException)必须覆盖,否则慢请求日志会大量缺失。
怎么写一个可靠的 PerformanceMiddleware
新建 app/middleware/PerformanceMiddleware.php,核心逻辑就三步:记起点、调下游、算差值。注意别在 $response->send() 后取时间——那已超出 PHP 控制范围。
- 起点必须是
$startTime = microtime(true),放在$next($request)前; - 终点用
microtime(true) - $startTime,乘 1000 取毫秒并round(..., 2); - 写入响应头:
$response->header('X-Response-Time', $duration . 'ms'); - 结构化日志必须用数组字段,例如
Log::info('api_duration', ['uri' => $request->url(), 'duration_ms' => $duration]),别拼字符串。
容易踩的坑
常见错误不是“不会写”,而是“写得看似对、实则失效”:
立即学习“PHP免费学习笔记(深入)”;
- 用
$_SERVER['REQUEST_TIME_FLOAT']当起点:它在 FPM 初始化时就设定了,如果入口文件有长耗时 require 或扩展加载,这部分偏差无法剔除; - 把计时逻辑塞进
AppServiceProvider::boot()或事件监听:ThinkPHP 6+ 的think\ResponseSend事件触发时,响应体已生成完毕,Gzip、输出缓冲等环节未计入; - 没过滤静态资源:
.js、.css、favicon.ico也会走中间件,拉低平均值,干扰 P95 判断; - 在 Swoole 环境下同步写日志:会阻塞 worker,吞吐骤降,必须用异步队列或
Log::channel('async')驱动。
最易被忽略的一点:基线必须关闭 APP_DEBUG=true 后采集。Debugbar 自身增加 20–50ms 开销,且只在 HTML 响应中生效,API 接口根本不会触发它——拿它做参考值,等于拿假数据调优。



















