Skip to content

监控与告警

监控系统是保障 PHP 应用在生产环境稳定运行的关键。通过实时监控应用性能指标、服务器资源使用率和业务 KPI,可以在问题影响用户之前发现并解决潜在问题。本节将介绍 Prometheus + Grafana 监控方案、APM(应用性能管理)、健康检查配置以及告警策略。

前置知识

阅读本节前,建议先了解:日志管理PHP-FPM 调优

基础概念

监控三大支柱

支柱工具数据特征
日志(Logging)ELK、Loki离散事件记录
指标(Metrics)Prometheus、Grafana时间序列数值
链路追踪(Tracing)Jaeger、Zipkin请求调用链路

核心监控指标

text
USE 方法(利用率、饱和度、错误率):
- Utilization(利用率):资源使用百分比
- Saturation(饱和度):资源排队或等待的程度
- Errors(错误率):请求失败的比率

RED 方法(请求指标):
- Rate(速率):每秒请求数
- Errors(错误):每秒错误数
- Duration(延迟):请求处理时间

PHP 应用关键指标:
- 请求响应时间(P50、P95、P99)
- 错误率(5xx 比例)
- PHP-FPM 进程数和队列长度
- 内存使用量
- CPU 使用率
- 数据库查询时间
- 缓存命中率

Prometheus + Grafana

PHP 应用暴露指标

php
<?php
declare(strict_types=1);

namespace App\Monitoring;

use Prometheus\CollectorRegistry;
use Prometheus\Storage\Redis;
use Prometheus\RenderTextFormat;

class MetricsExporter
{
    private CollectorRegistry $registry;

    public function __construct()
    {
        $redis = new Redis(['host' => '127.0.0.1', 'port' => 6379]);
        $this->registry = new CollectorRegistry($redis);
    }

    /**
     * 注册所有指标
     */
    public function registerMetrics(): void
    {
        // 请求计数器
        $requestCounter = $this->registry->getOrRegisterCounter(
            'app',
            'http_requests_total',
            'Total HTTP requests',
            ['method', 'route', 'status_code'],
        );

        // 请求延迟直方图
        $requestHistogram = $this->registry->getOrRegisterHistogram(
            'app',
            'http_request_duration_seconds',
            'HTTP request duration in seconds',
            ['method', 'route'],
            [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
        );

        // 当前活跃请求数
        $activeRequests = $this->registry->getOrRegisterGauge(
            'app',
            'http_requests_active',
            'Number of active HTTP requests',
            ['route'],
        );

        // 错误计数器
        $errorCounter = $this->registry->getOrRegisterCounter(
            'app',
            'errors_total',
            'Total errors',
            ['type', 'severity'],
        );

        // 缓存命中率
        $cacheHits = $this->registry->getOrRegisterCounter(
            'app',
            'cache_hits_total',
            'Cache hits',
            ['driver', 'operation'],
        );

        $cacheMisses = $this->registry->getOrRegisterCounter(
            'app',
            'cache_misses_total',
            'Cache misses',
            ['driver', 'operation'],
        );

        // 数据库查询计数器
        $dbQueries = $this->registry->getOrRegisterCounter(
            'app',
            'db_queries_total',
            'Database queries',
            ['connection', 'operation'],
        );

        $dbQueryDuration = $this->registry->getOrRegisterHistogram(
            'app',
            'db_query_duration_seconds',
            'Database query duration',
            ['connection'],
            [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5],
        );
    }

    /**
     * 暴露指标端点
     */
    public function renderMetrics(): string
    {
        $renderer = new RenderTextFormat();
        $result = $renderer->render($this->registry->getMetricFamilySamples());

        header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
        echo $result;
    }

    public function getRegistry(): CollectorRegistry
    {
        return $this->registry;
    }
}

Prometheus 配置

yaml
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  # PHP 应用指标
  - job_name: 'php-app'
    static_configs:
      - targets: ['app:9090']
    metrics_path: '/metrics'
    scrape_interval: 10s

  # PHP-FPM 指标
  - job_name: 'php-fpm'
    static_configs:
      - targets: ['php-fpm-exporter:9253']
    scrape_interval: 10s

  # MySQL 指标
  - job_name: 'mysql'
    static_configs:
      - targets: ['mysql-exporter:9104']
    scrape_interval: 30s

  # Redis 指标
  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']
    scrape_interval: 15s

  # Node (主机) 指标
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']
    scrape_interval: 15s

Grafana Dashboard

告警规则

yaml
# alert_rules.yml
groups:
  - name: php_app_alerts
    rules:
      - alert: HighErrorRate
        expr: |
          rate(app_http_requests_total{status_code=~"5.."}[5m])
          / rate(app_http_requests_total[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "High error rate detected ({{ $value | humanizePercentage }})"
          description: "5xx error rate exceeded 5% for the last 2 minutes"

      - alert: HighResponseTime
        expr: |
          histogram_quantile(0.95, rate(app_http_request_duration_seconds_bucket[5m])) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High P95 response time ({{ $value }}s)"

      - alert: PHPFPMProcessExhaustion
        expr: |
          php_fpm_active_processes / php_fpm_max_children > 0.9
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "PHP-FPM processes near exhaustion"

      - alert: HighMemoryUsage
        expr: |
          (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.9
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Memory usage above 90%"

      - alert: DiskSpaceLow
        expr: |
          (1 - (node_filesystem_avail_bytes{fstype=~"ext4|xfs"} / node_filesystem_size_bytes{fstype=~"ext4|xfs"})) > 0.85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Disk space below 15%"

APM(应用性能管理)

APM 工具对比

工具特点部署方式
New Relic功能全面,商业产品SaaS
Datadog全栈监控,价格较高SaaS
Scout APMPHP 专用,轻量SaaS
TidewaysPHP 专用,详细SaaS / 自托管
Jaeger开源链路追踪自托管
OpenTelemetry标准化,多语言自托管

健康检查

应用健康检查端点

php
<?php
declare(strict_types=1);

namespace App\Http\Controller;

use App\Monitoring\HealthChecker;

class HealthController
{
    public function __construct(
        private readonly HealthChecker $checker,
    ) {}

    public function __invoke(): void
    {
        $checks = $this->checker->runAll();

        $isHealthy = array_every(
            $checks,
            fn (array $check) => $check['status'] === 'healthy',
        );

        header('Content-Type: application/json');

        if (!$isHealthy) {
            http_response_code(503);
        }

        echo json_encode([
            'status' => $isHealthy ? 'healthy' : 'unhealthy',
            'timestamp' => date('c'),
            'version' => getenv('APP_VERSION') ?: 'unknown',
            'checks' => $checks,
        ]);
    }
}
php
<?php
declare(strict_types=1);

namespace App\Monitoring;

class HealthChecker
{
    public function __construct(
        private readonly \PDO $db,
        private readonly \Redis $redis,
    ) {}

    public function runAll(): array
    {
        return [
            'database' => $this->checkDatabase(),
            'redis' => $this->checkRedis(),
            'filesystem' => $this->checkFilesystem(),
            'memory' => $this->checkMemory(),
        ];
    }

    private function checkDatabase(): array
    {
        try {
            $stmt = $this->db->query('SELECT 1');
            $result = $stmt->fetchColumn();

            return [
                'status' => $result ? 'healthy' : 'unhealthy',
                'message' => $result ? 'Database connection OK' : 'Database query failed',
            ];
        } catch (\PDOException $e) {
            return [
                'status' => 'unhealthy',
                'message' => 'Database connection failed: ' . $e->getMessage(),
            ];
        }
    }

    private function checkRedis(): array
    {
        try {
            $this->redis->ping();

            return [
                'status' => 'healthy',
                'message' => 'Redis connection OK',
            ];
        } catch (\RedisException $e) {
            return [
                'status' => 'unhealthy',
                'message' => 'Redis connection failed: ' . $e->getMessage(),
            ];
        }
    }

    private function checkFilesystem(): array
    {
        $storagePath = storage_path();
        $freeSpace = disk_free_space($storagePath);
        $totalSpace = disk_total_space($storagePath);
        $usagePercent = (1 - $freeSpace / $totalSpace) * 100;

        return [
            'status' => $usagePercent < 90 ? 'healthy' : 'unhealthy',
            'message' => sprintf('Disk usage: %.1f%%', $usagePercent),
            'usage_percent' => round($usagePercent, 1),
        ];
    }

    private function checkMemory(): array
    {
        $memoryLimit = ini_get('memory_limit');
        $currentUsage = memory_get_usage(true);
        $peakUsage = memory_get_peak_usage(true);

        return [
            'status' => 'healthy',
            'message' => sprintf(
                'Memory: current %s / peak %s (limit: %s)',
                $this->formatBytes($currentUsage),
                $this->formatBytes($peakUsage),
                $memoryLimit,
            ),
            'current_usage' => $currentUsage,
            'peak_usage' => $peakUsage,
        ];
    }

    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];

        $power = array_search(
            (int) floor(log($bytes, 1024)),
            range(0, 3, 1),
            true,
        );

        return number_format($bytes / (1024 ** $power), 1) . $units[$power];
    }
}

实战示例

Docker Compose 监控栈

yaml
# docker-compose.monitoring.yml
version: '3.9'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD:-admin}"
      GF_USERS_ALLOW_SIGN_UP: "false"

  alertmanager:
    image: prom/alertmanager:latest
    container_name: alertmanager
    restart: unless-stopped
    ports:
      - "9093:9093"
    volumes:
      - ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    ports:
      - "9100:9100"

  php-fpm-exporter:
    image: hipages/php-fpm_exporter:latest
    container_name: php-fpm-exporter
    restart: unless-stopped
    ports:
      - "9253:9253"
    command:
      - '--endpoint'
      - 'tcp://app-php-fpm:9000/status'

  mysql-exporter:
    image: prom/mysqld-exporter:latest
    container_name: mysql-exporter
    restart: unless-stopped
    ports:
      - "9104:9104"
    environment:
      DATA_SOURCE_NAME: "root:${DB_ROOT_PASSWORD}@(mysql:3306)/"

  redis-exporter:
    image: oliver006/redis_exporter:latest
    container_name: redis-exporter
    restart: unless-stopped
    ports:
      - "9121:9121"
    environment:
      REDIS_ADDR: "redis:6379"

volumes:
  prometheus_data:
  grafana_data:

最佳实践

  1. 监控 RED 指标:速率、错误率、延迟是核心业务指标
  2. 分级告警:Critical(立即处理)、Warning(工作时间处理)、Info(仅供参考)
  3. 避免告警疲劳:合理设置阈值和持续时间,减少误报
  4. 可视化 Dashboard:Grafana 创建清晰的监控面板
  5. 自动化响应:关键告警触发自动扩容或重启
  6. SLI/SLO/SLA:基于业务目标设置服务质量指标

下一节

继续学习:持续集成

参考链接