监控与告警
监控系统是保障 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: 15sGrafana 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 APM | PHP 专用,轻量 | SaaS |
| Tideways | PHP 专用,详细 | 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:最佳实践
- 监控 RED 指标:速率、错误率、延迟是核心业务指标
- 分级告警:Critical(立即处理)、Warning(工作时间处理)、Info(仅供参考)
- 避免告警疲劳:合理设置阈值和持续时间,减少误报
- 可视化 Dashboard:Grafana 创建清晰的监控面板
- 自动化响应:关键告警触发自动扩容或重启
- SLI/SLO/SLA:基于业务目标设置服务质量指标
下一节
继续学习:持续集成