应使用PerformanceMiddleware中间件在handle()中用microtime(true)埋点计时,覆盖正常与异常路径,将耗时写入响应头X-Response-Time及结构化日志,避免依赖$_SERVER['REQUEST_TIME_FLOAT']。

在 PerformanceMiddleware 中埋点计时
ThinkPHP 的请求耗时必须从框架真正开始处理时计起,不能只在控制器里打时间戳——中间件、模型初始化、查询构造这些都算真实耗时。推荐新建 app/middleware/PerformanceMiddleware.php,用 microtime(true) 在 handle() 开头记起点,结尾算差值并写入 Request 对象:
public function handle($request, \Closure $next)
{
$startTime = microtime(true);
$response = $next($request);
$duration = round((microtime(true) - $startTime) * 1000, 2);
<pre class='brush:php;toolbar:false;'>// 写入请求上下文,供后续日志或响应头使用
$request->withAttr('response_time_ms', $duration);
return $response;}
注意:别在 $next($request) 之后再调 $response->send() ——那是框架内部行为,你无法控制;$response 返回时 PHP 层逻辑已结束,此时取的时间最准。
把耗时写进响应头或日志
浏览器开发者工具、Nginx 日志、APM 工具都依赖 X-Response-Time 这类响应头。直接在中间件里改 $response 即可:
立即学习“PHP免费学习笔记(深入)”;
$response->header('X-Response-Time', $duration . 'ms');如果要写结构化日志(比如用于基线分析),在中间件末尾加一行:
\think\facade\Log::info('api_duration', [
'uri' => $request->url(),
'method' => $request->method(),
'duration_ms' => $duration,
'status' => $response->getCode(),
]);常见错误是用 Log::info('耗时:' . $duration) 拼字符串——日志解析器没法提取字段,后期查 P95 或做告警就抓瞎。
为什么不用 $_SERVER['REQUEST_TIME_FLOAT']?
它确实更“底层”,但只适合统计整个 HTTP 请求生命周期(含 FPM 排队、路由解析前的开销)。在 ThinkPHP 场景下,你真正想优化的是「框架启动后干了什么」,不是「PHP-FPM 子进程等了多久才轮到我」。
-
$_SERVER['REQUEST_TIME_FLOAT']包含了请求进入 PHP 前的排队时间,FPMpm.max_requests配得太小会导致该值剧烈波动 - 中间件里的
microtime(true)起点更贴近业务代码执行边界,和 ORM 查询、模板渲染这些耗时强相关 - 如果你同时用了 Redis 缓存或消息队列,它们的耗时也会被自然包含进来,不用额外 patch
避免踩坑:异常路径也要覆盖
中间件的 handle() 不会捕获未处理的异常,一旦控制器抛出异常,流程就跳去 app/ExceptionHandle.php 了,上面的耗时统计就断了。
必须在 render() 方法末尾补上同样逻辑:
public function render($request, Throwable $e): Response
{
$startTime = $request->hasAttr('response_time_ms')
? $_SERVER['REQUEST_TIME_FLOAT']
: microtime(true);
<pre class='brush:php;toolbar:false;'>$duration = round((microtime(true) - $startTime) * 1000, 2);
\think\facade\Log::info('api_duration_exception', [
'uri' => $request->url(),
'method' => $request->method(),
'duration_ms' => $duration,
'exception' => get_class($e),
'message' => $e->getMessage(),
]);
return parent::render($request, $e);}
真正难的不是记录成功请求,而是确保每次 500、404、422 都有对应耗时数据——慢查询引发的异常、序列化失败卡住的响应,这些才是性能恶化的信号源。



















