App::before/after无法准确测耗时,因其非严格请求起止点且自身有开销;应改用hrtime(true)在中间件中记录起点终点,并通过Monolog异步日志避免I/O阻塞。

为什么 App::before 和 App::after 不能直接拿到准确耗时
因为 ThinkPHP 的 App::before 和 App::after 是事件钩子,不是严格意义上的“请求开始/结束”时间点——before 触发时路由可能还没解析完,after 触发时响应可能已输出但日志还没刷盘。更关键的是,这两个钩子本身有执行开销,叠加多次注册容易漂移。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 改用
think\App类的bind方法,在应用初始化阶段注入一个轻量级计时器,从App实例创建后立刻打点 - 在
Response::send被调用前(可通过中间件或response_send事件)读取结束时间,避免被输出缓冲干扰 - 别依赖
microtime(true)多次调用——PHP 7.4+ 推荐用hrtime(true),纳秒级且不受系统时钟调整影响
如何在中间件里安全记录 API 耗时并避免污染业务逻辑
中间件是最可控的位置:它天然包裹请求生命周期,且可按需启用(比如只对 api/ 前缀路由生效)。但要注意两点:一是不能在 handle 里直接写日志文件(I/O 阻塞主线程),二是不能把耗时数据塞进 $request 或 $response 的任意属性(ThinkPHP 内部可能覆盖)。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 在中间件
handle开头调用hrtime(true)记录起点,存入$request->attr('start_time') - 在
return $next($request)后立即计算差值,用Log::record()写入自定义通道(如api_monitor),而非默认info - 给日志加结构化字段:
route、method、status、cost_ms,方便后续用 ELK 或 Loki 聚合 - 避免在中间件里做耗时操作(如数据库写入、HTTP 请求),否则测出来的“API 耗时”其实是“API + 监控自身耗时”
Log::record 写入慢?用 monolog 配置异步 handler
默认的 ThinkPHP 日志驱动是同步写文件,单次 Log::record 可能卡住几十毫秒,尤其在高并发 API 场景下会反向拖慢真实耗时统计。这不是日志内容的问题,而是 I/O 模式问题。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 在
config/log.php中为api_monitor通道单独配置'type' => 'monolog',并指定'handler' => \Monolog\Handler\StreamHandler::class - 关键一步:给该 handler 加上
'processors' => [\Monolog\Processor\MemoryUsageProcessor::class],避免内存暴涨影响计时精度 - 若用 Swoole,必须禁用
FileHandler,改用RotatingFileHandler并设置max_files => 10,否则多进程写同一文件会丢日志 - 测试时用
ab -n 100 -c 10 http://test.dev/api/user对比开关监控前后的平均响应时间,偏差应
为什么 cost_ms 在某些接口里总是 0 或负数
常见于短平快接口(如空 JSON 返回)或启用了 OPcache + JIT 的环境。根本原因是 PHP 执行太快,hrtime(true) 的纳秒值在两次调用间没变化,或者因 CPU 乱序执行导致时间戳回跳。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 强制用
hrtime(true) / 1_000_000转成毫秒再四舍五入,不要用round(),改用(int)bcadd($ms, '0.5', 0)避免浮点误差 - 在中间件开头加一行
if (function_exists('opcache_get_status')) @opcache_get_status();,触发一次 OPcache 状态检查,稳定 JIT 行为 - 生产环境务必关闭
display_errors和debug模式——开启后 ThinkPHP 会注入大量调试信息,严重干扰计时基线
真正难的不是记下那个数字,而是确保每次记录时,PHP 还没开始优化掉你刚写的那行计时代码。



















