怎样在PHP项目中实现流程监控?

wen java案例 8

本文目录导读:

怎样在PHP项目中实现流程监控?

  1. 通用核心思路
  2. 方案一:数据库/日志 + 自定义后台(最常用)
  3. 方案二:Redis 实时监控(适合实时看板)
  4. 方案三:日志文件 + ELK / Grafana (生产级聚合)
  5. 方案四:分布式追踪(链路监控)
  6. 如何选择

在PHP项目中实现流程监控,通常指监控业务请求的处理过程任务执行的状态(如队列任务、定时任务),或者长流程的流转(如订单审核、审批流),PHP 本身是“短生命周期”语言(请求结束进程销毁),所以监控思路和 Java 的长驻进程不同。

下面给出从简单到复杂的几种常用实现方案,以及对应场景。


通用核心思路

无论哪种方案,流程监控的本质都是:在关键节点(开始/结束/异常)记录时间、状态和数据,然后通过查询或实时推送呈现。

  • 记录点:流程开始、每个关键步骤、流程结束、异常捕获。
  • 流程ID(如订单号)、步骤名称时间状态附加数据(如用户ID、参数摘要)
  • 存储:文件、数据库、Redis、消息队列。
  • 展示:自定义后台页面、日志系统、APM(应用性能监控)工具。

数据库/日志 + 自定义后台(最常用)

适合:对监控实时性要求不高,需要查看历史记录和统计的内部管理功能(如工单审批流程、订单处理流程)。

设计数据表(MySQL/PostgreSQL)

-- 流程实例表,每条记录代表一个流程
CREATE TABLE `process_instances` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `process_name` varchar(100) NOT NULL COMMENT '流程名称,如 order_audit',
  `biz_id` varchar(64) NOT NULL COMMENT '业务ID,如订单号',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '0: 进行中, 1: 成功, 2: 失败, 3: 超时',
  `current_step` varchar(100) DEFAULT NULL COMMENT '当前所在步骤',
  `started_at` datetime DEFAULT NULL,
  `finished_at` datetime DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY `uk_biz` (`process_name`,`biz_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB;
-- 流程流转日志表,记录每一步的日志
CREATE TABLE `process_logs` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `process_name` varchar(100) NOT NULL,
  `biz_id` varchar(64) NOT NULL,
  `step_name` varchar(100) NOT NULL COMMENT '步骤名,如 validate_order, call_payment_api',
  `status` varchar(20) NOT NULL COMMENT 'success / fail / running',
  `message` text COMMENT '日志详情或错误信息',
  `cost_ms` int unsigned DEFAULT '0' COMMENT '该步骤耗时(毫秒)',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  KEY `idx_biz` (`process_name`,`biz_id`),
  KEY `idx_time` (`created_at`)
) ENGINE=InnoDB;

PHP 代码埋点

<?php
class ProcessMonitor
{
    private string $processName;
    private string $bizId;
    private array $stepTimes = []; // 用于计算耗时
    public function __construct(string $processName, string $bizId)
    {
        $this->processName = $processName;
        $this->bizId = $bizId;
        // 可选:插入流程实例记录
        DB::table('process_instances')->updateOrInsert(
            ['process_name' => $processName, 'biz_id' => $bizId],
            ['status' => 0, 'started_at' => now(), 'current_step' => 'init']
        );
    }
    public function startStep(string $stepName): void
    {
        $this->stepTimes[$stepName] = microtime(true);
    }
    public function endStepSuccess(string $stepName, string $message = ''): void
    {
        $cost = $this->calcCost($stepName);
        // 写入日志表
        DB::table('process_logs')->insert([
            'process_name' => $this->processName,
            'biz_id' => $this->bizId,
            'step_name' => $stepName,
            'status' => 'success',
            'message' => $message,
            'cost_ms' => $cost,
            'created_at' => now(),
        ]);
        // 更新实例表当前步骤
        DB::table('process_instances')
            ->where(['process_name' => $this->processName, 'biz_id' => $this->bizId])
            ->update(['current_step' => $stepName]);
    }
    public function endStepFail(string $stepName, string $error): void
    {
        $cost = $this->calcCost($stepName);
        DB::table('process_logs')->insert([
            'process_name' => $this->processName,
            'biz_id' => $this->bizId,
            'step_name' => $stepName,
            'status' => 'fail',
            'message' => $error,
            'cost_ms' => $cost,
        ]);
        // 标记整个流程失败
        DB::table('process_instances')
            ->where(['process_name' => $this->processName, 'biz_id' => $this->bizId])
            ->update(['status' => 2, 'finished_at' => now()]);
    }
    public function finishProcess(): void
    {
        DB::table('process_instances')
            ->where(['process_name' => $this->processName, 'biz_id' => $this->bizId])
            ->update(['status' => 1, 'finished_at' => now()]);
    }
    private function calcCost(string $stepName): int
    {
        if (isset($this->stepTimes[$stepName])) {
            return (int)((microtime(true) - $this->stepTimes[$stepName]) * 1000);
        }
        return 0;
    }
}
// 使用示例:下单流程监控
$monitor = new ProcessMonitor('order_create', $orderNo);
$monitor->startStep('validate_param');
// ... 校验参数逻辑 ...
$monitor->endStepSuccess('validate_param');
$monitor->startStep('deduct_stock');
// ... 扣库存逻辑 ...
$monitor->endStepSuccess('deduct_stock');
// 如果扣库存失败
// $monitor->endStepFail('deduct_stock', '库存不足');
$monitor->startStep('save_order');
// ... 保存订单 ...
$monitor->endStepSuccess('save_order');
$monitor->finishProcess(); // 标记整个流程成功

后台展示

写一个简单的管理后台,SQL 查询:SELECT * FROM process_logs WHERE process_name='order_create' AND biz_id='xxx' ORDER BY created_at,按步骤时间线展示。


Redis 实时监控(适合实时看板)

适合:需要实时掌握当前正在执行的任务数量、吞吐量、耗时分布(如数据采集管道、异步任务处理器)。

不使用数据库(写磁盘慢),而是使用 Redis 的 ListHashSorted Set 结构。

class RealTimeMonitor
{
    private Redis $redis;
    private string $prefix = 'flow:';
    // 记录当前活跃流程数
    public function incrActive(string $processName): void
    {
        $this->redis->incr($this->prefix . 'active:' . $processName);
    }
    public function decrActive(string $processName): void
    {
        $this->redis->decr($this->prefix . 'active:' . $processName);
    }
    // 记录耗时到有序集合(用于计算 P50/P99)
    public function recordLatency(string $processName, int $milliseconds): void
    {
        $this->redis->zAdd($this->prefix . 'latency:' . $processName, $milliseconds, uniqid());
        // 可选:设置过期时间,只保留最近1小时的数据
        $this->redis->expire($this->prefix . 'latency:' . $processName, 3600);
    }
    // 获取当前活跃数
    public function getActiveCount(string $processName): int
    {
        return (int)$this->redis->get($this->prefix . 'active:' . $processName);
    }
    // 获取平均耗时
    public function getAvgLatency(string $processName): float
    {
        $count = $this->redis->zCard($this->prefix . 'latency:' . $processName);
        if ($count == 0) return 0;
        $sum = 0;
        // 注意:大数据量下不要这样做,仅示例,实际应使用 Redis 聚合或后台计算
        $all = $this->redis->zRange($this->prefix . 'latency:' . $processName, 0, -1, true);
        return array_sum($all) / $count;
    }
    // 记录日志到 List(用于看最新N条)
    public function logEvent(string $processName, string $bizId, string $message): void
    {
        $this->redis->lPush($this->prefix . 'log:' . $processName, json_encode([
            'biz_id' => $bizId, 'msg' => $message, 'time' => now()
        ]));
        $this->redis->lTrim($this->prefix . 'log:' . $processName, 0, 99); // 只保留最近100条
    }
}

前端看板:通过 API 读取上述 Redis 数据(GET /api/monitor?process=order),前端轮询展示卡片、折线图等。


日志文件 + ELK / Grafana (生产级聚合)

适合:微服务架构、大量服务器,需要集中式分析和告警,这是很多大厂的方式。

  1. PHP 日志输出:使用 Monolog 或自定义 Logger,按固定 JSON 格式输出到文件。

    // 在流程关键点
    $logger->info('process.step', [
        'process' => 'order_audit',
        'biz_id'  => $orderId,
        'step'    => 'call_third_party',
        'status'  => 'success',
        'duration_ms' => 123,
        'extra'   => ['api_name' => 'risk_verify']
    ]);
  2. 日志收集:Filebeat / Fluentd 读取日志文件,发送到 Elasticsearch。

  3. 展示:Kibana 创建仪表板,可以按 process 聚合,查看 avg(duration_ms)count per minuteerror rate,甚至可以设置告警(当 duration_ms > 500 发邮件)。

  4. 替代方案:如果不想引入 ELK 那么重,可以用 Grafana + Loki(轻量日志聚合)或者 DatadogSentry 等 SaaS 服务(Sentry 可以监控错误和性能)。


分布式追踪(链路监控)

如果流程跨越多个微服务(PHP 调用 Java 服务,再调用 PHP 异步任务),上述单进程监控就不够了,需要实现分布式追踪

  • 标准:OpenTelemetry(推荐)、Zipkin、Jaeger。
  • PHP 实现:使用 opentelemetry/opentelemetry-php 包,在 HTTP 请求入口、RPC 调用、队列消息发送/接收处创建 Span(跨度)。
  • 效果:可以在 Jaeger UI 上看到一个请求从 Nginx -> PHP-FPM -> MySQL -> RabbitMQ -> Worker 的完整调用链路,每一步耗时一目了然。
// 使用 OpenTelemetry API 示例(伪代码)
$tracer = OpenTelemetry::getTracer('my-app');
$span = $tracer->spanBuilder('process.order')->startSpan();
$span->setAttribute('order_id', $orderId);
try {
    // 步骤1
    $childSpan = $tracer->spanBuilder('validate')->startSpan();
    // ... 校验逻辑 ...
    $childSpan->end();
    // 步骤2
    $childSpan2 = $tracer->spanBuilder('deduct_stock')->startSpan();
    // ...
    $childSpan2->end();
    $span->end(); // 成功结束
} catch (\Throwable $e) {
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    $span->end();
}

如何选择

场景 推荐方案 复杂度
内部管理后台,看单个订单的审核流程细节 数据库 + 自定义列表
实时运维大屏,看当前处理中的任务数和耗时 Redis + API + 前端轮询
微服务/多服务器,历史数据分析和告警 JSON 日志 + ELK / Loki / Grafana
跨服务调用链跟踪,用于定位性能瓶颈 OpenTelemetry + Jaeger

建议先从简单的方案一开始,建两个表、写一个 Trait,很快就能看到效果,后续如果流量变大、需要实时性,再增加方案二或三。

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