PHP怎么记录慢业务日志

wen PHP项目 1

PHP应用性能优化实战:如何精准记录与定位慢业务日志(附完整代码)


目录导读

  1. 为什么你的PHP应用需要“慢业务日志”?
  2. 方案选型:从microtime到Tracing(链路追踪)的演进
  3. 手写一个轻量级“慢业务日志”记录器(附可运行代码)
  4. 进阶:基于中间件的自动拦截与参数上下文采集
  5. 问答环节:解决最常见的5个痛点(含误报、并发、内存泄漏)
  6. 最佳实践:日志格式标准化与ELK/阿里云SLS对接

为什么你的PHP应用需要“慢业务日志”?

当用户反馈“页面很慢”时,很多开发者第一反应是查看Nginx访问日志和MySQL慢查询日志,但这两者都存在盲区:Nginx只能看到请求总耗时,却看不到PHP内部哪个函数/数据库查询拖了后腿;MySQL慢查询只能定位SQL,无法关联具体的业务逻辑分支。

PHP怎么记录慢业务日志

举个例子:一个下单接口耗时3秒,慢查询日志显示某条SQL用了1.2秒,但剩余的1.8秒消耗在外部API调用Redis锁等待上,这时,request_terminate_timeoutmax_execution_time这两个致命错误日志往往只记录“超时被杀”,不会告诉你超时前发生了什么。

PHP层必须存在一种机制,能够监控单个请求内具体业务操作(如:调用第三方支付接口、循环处理数组、写文件)的执行时间,当超过阈值(800ms)时,自动将完整调用栈、入参、内存峰值写入独立日志文件,这就是“慢业务日志”的核心价值——将性能问题从“黑盒”变成“白盒”

方案选型:从microtime到Tracing(链路追踪)的演进

初级方案(手动打点):在每个需要监控的代码块前后加microtime(true),计算差值,这种方式侵入性强、容易遗漏、无法形成统一视图。

进阶方案(AOP/中间件):通过PHP框架(Laravel、ThinkPHP、Hyperf)的中间件机制,在请求进入Controller前开启计时,在响应返回前结束计时,如果超时,则记录当前$_GET$_POST$_SERVER关键信息及日志轨迹,这是目前中小型项目最平衡的方案。

前沿方案(分布式追踪):引入SkyWalking、Jaeger或Zipkin,通过OpenTracing协议自动埋点,但缺点是部署复杂、需安装扩展,对于单体PHP应用而言“杀鸡用牛刀”。

本文重点展开第二种方案,因为它在零扩展依赖可操作性上最符合80%的PHPer需求。

手写一个轻量级“慢业务日志”记录器(附可运行代码)

我们直接写一个通用类,不依赖任何框架,可在任何PHP 7.4+项目中使用。

<?php
declare(strict_types=1);
class SlowBusinessLogger
{
    private float $startTime;
    private float $threshold; // 阈值,单位秒
    private array $logData = [];
    private static ?self $instance = null;
    public static function getInstance(float $threshold = 0.8): self
    {
        if (self::$instance === null) {
            self::$instance = new self($threshold);
        }
        return self::$instance;
    }
    private function __construct(float $threshold)
    {
        $this->threshold = $threshold;
        $this->startTime = microtime(true);
        // 注册关闭函数,在脚本结束时自动检测
        register_shutdown_function(function () {
            $this->checkAndLog();
        });
    }
    public function point(string $name): void
    {
        $this->logData[] = [
            'name' => $name,
            'time' => microtime(true) - $this->startTime,
            'mem' => memory_get_usage(true),
        ];
    }
    private function checkAndLog(): void
    {
        $totalTime = microtime(true) - $this->startTime;
        if ($totalTime < $this->threshold) {
            return;
        }
        // 组装上下文
        $context = [
            'uri' => $_SERVER['REQUEST_URI'] ?? 'cli',
            'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
            'total_time_s' => round($totalTime, 4),
            'peak_mem_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
            'points' => $this->logData,
            'post_data' => $this->sensitiveFilter($_POST ?? []),
            'server_ip' => $_SERVER['SERVER_ADDR'] ?? '',
        ];
        $logLine = date('Y-m-d H:i:s') . ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        file_put_contents('/var/log/php_slow_business.log', $logLine, FILE_APPEND | LOCK_EX);
    }
    private function sensitiveFilter(array $data): array
    {
        // 屏蔽密码字段
        foreach ($data as $k => $v) {
            if (stripos((string)$k, 'passwd') !== false || stripos((string)$k, 'token') !== false) {
                $data[$k] = '***';
            }
        }
        return $data;
    }
}
// ---------- 使用示例 ----------
$logger = SlowBusinessLogger::getInstance(1.0); // 1秒阈值
// 模拟业务块1:查询订单
$logger->point('start_query_order');
usleep(500000); // 模拟500ms
$logger->point('query_order_done');
// 模拟业务块2:调用外部API
$logger->point('start_http_call');
usleep(600000); // 模拟600ms
$logger->point('http_call_done');
// 总耗时1.1秒 > 阈值,自动写日志
// 此时检查 /var/log/php_slow_business.log

关键点解释

  • 使用register_shutdown_function确保脚本无论正常结束还是dieexit都能触发检测。
  • memory_get_usage(true)获取的是系统分配的内存,比memory_get_usage()更接近真实峰值。
  • 日志文件写入采用LOCK_EX防止并发写冲突。

进阶:基于中间件的自动拦截与参数上下文采集

手写类解决了“怎么记录”,但在生产环境中,你不可能在每段代码前都手动调用point(),更优雅的方式是利用框架中间件。

以Laravel为例,在app/Http/Middleware下创建SlowBusinessMiddleware

public function handle($request, Closure $next)
{
    $start = microtime(true);
    $response = $next($request);
    $execTime = microtime(true) - $start;
    if ($execTime > config('slow_threshold', 1.5)) {
        // 获取路由信息
        $route = $request->route();
        $payload = [
            'route' => $route ? $route->uri() : 'unknown',
            'input' => $request->except(['password', 'password_confirmation']),
            'time' => round($execTime, 3),
            'mem' => memory_get_peak_usage(true),
            'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
        ];
        Log::channel('slow')->warning('slow business executed', $payload);
    }
    return $response;
}

原理:所有请求都会经过该中间件,自动测量从“进入控制器”到“响应发出”的耗时,这种方法无需修改任何业务代码,通过配置文件即可动态调整阈值。

问答环节:解决最常见的5个痛点(含误报、并发、内存泄漏)

Q1:如何避免日志文件无限增长,导致磁盘满? A:采用按天切割,Linux下使用logrotate,配置/etc/logrotate.d/php_slow/var/log/php_slow_business.log { daily rotate 7 compress missingok notifempty },或者使用PHP写入时通过date('Ymd')后缀区分文件:/var/log/slow_{$date}.log

Q2:高并发下写入日志会阻塞业务吗? A:有几种解决方案,① 启用async日志库如MonologBufferHandler,攒够50条再批量写,② 落盘到/dev/shm(内存缓存盘),再由后台任务搬运,③ 使用Redis队列,写日志改LPUSH,由独立消费者做持久化。建议套用第二种,性能损失最小。

Q3:日志中显示时间很长,但业务实际没慢,是什么原因? A:排查外部依赖,例如使用了curl请求外网,DNS解析慢、本地代理配置错误等,这些时间会计入业务耗时,解决办法:给curl设置CURLOPT_TIMEOUT(如5秒),同时开启CURLOPT_DNS_USE_GLOBAL_CACHE,务必在日志里记录isset($_SERVER['HTTP_X_FORWARDED_FOR']),排查是否反向代理导致时间偏移。

Q4:如何通过慢日志快速定位是哪个表出了问题? A:在日志里附加最后的SQL查询,借助DB::listen事件,在每次查询执行完时记录['sql' => $sql, 'bindings' => $bindings, 'time' => $queryTime],存入一个$GLOBALS['__last_queries']数组,最后随慢日志一并输出。

Q5:记录慢日志本身会不会造成内存泄漏? A:在长驻进程(如Swoole)中需特别注意,不要存储无限增长的logData,建议在point()里只保留最近20个点array_slice),同时在调用checkAndLog()后必须重置logData为空数组,普通FPM模式下无此问题。

最佳实践:日志格式标准化与ELK/阿里云SLS对接

为了让慢日志能被搜索引擎快速检索和统计,建议统一使用JSON格式,每个字段使用固定键名。

{
  "@timestamp": "2025-04-11T14:23:01+08:00",
  "level": "warning",
  "app": "order_service",
  "uri": "/api/order/detail",
  "method": "GET",
  "total_time_sec": 2.31,
  "memory_peak_mb": 78.5,
  "slow_point_count": 6,
  "points": [
    {"name": "check_user_auth", "time": 0.2, "mem": 20971520},
    {"name": "load_product_info", "time": 1.8, "mem": 31457280}
  ]
}
  • 对接ELK:直接使用Filebeat监听该log文件,配置json.keys_under_root: true,然后通过Grok filter提取total_time_sec字段,在Kibana中创建聚合图表即可实现“超过2秒的接口TOP排行榜”。
  • 对接阿里云SLS:在日志服务控制台创建Logstore,使用Logtail插件收集文件,配置索引时勾选total_time_sec为数字类型,打开“快速分析”功能,即可秒级查询。

最后的核心忠告:慢业务日志不是用来优化速度,而是用来量化“究竟慢在哪一步”,记录完日志后,必须建立定期阅读的SOP(每天早晨10点查看昨晚峰值时段日志),配合Xdebug.profiler_enable_trigger生成CacheGrind文件,做二次性能剖析,才能形成闭环优化,如果只是记录而不看,那这个日志只是一堆占着磁盘的垃圾数据。

抱歉,评论功能暂时关闭!