Laravel 13 应用健康检查是保障系统稳定运行的重要手段,本文介绍如何构建完善的健康检查系统。
健康检查概述
健康检查接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
namespace App\Http\Controllers;
use App\Services\Health\HealthChecker; use Illuminate\Http\JsonResponse;
class HealthController extends Controller { public function __construct( protected HealthChecker $healthChecker ) {} public function check(): JsonResponse { $result = $this->healthChecker->check(); return response()->json($result->toArray(), $result->status); } public function liveness(): JsonResponse { return response()->json([ 'status' => 'ok', 'timestamp' => now()->toIso8601String(), ]); } public function readiness(): JsonResponse { $result = $this->healthChecker->check([ 'database', 'redis', 'storage', ]); return response()->json( $result->toArray(), $result->isHealthy() ? 200 : 503 ); } }
|
健康检查器
基础检查器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <?php
namespace App\Services\Health;
use Illuminate\Support\Collection;
class HealthChecker { protected Collection $checks; public function __construct() { $this->checks = collect(); } public function register(HealthCheck $check): self { $this->checks->push($check); return $this; } public function check(array $names = null): HealthResult { $checks = $names ? $this->checks->whereIn('name', $names) : $this->checks; $results = $checks->map(function (HealthCheck $check) { return $this->runCheck($check); }); $overallStatus = $results->every(fn($r) => $r['status'] === 'healthy') ? 200 : 503; return new HealthResult( status: $overallStatus, checks: $results->toArray(), timestamp: now()->toIso8601String() ); } protected function runCheck(HealthCheck $check): array { $startTime = microtime(true); try { $result = $check->check(); return [ 'name' => $check->name, 'status' => 'healthy', 'message' => $result['message'] ?? 'OK', 'details' => $result['details'] ?? [], 'duration' => round((microtime(true) - $startTime) * 1000, 2), ]; } catch (\Throwable $e) { return [ 'name' => $check->name, 'status' => 'unhealthy', 'message' => $e->getMessage(), 'details' => [], 'duration' => round((microtime(true) - $startTime) * 1000, 2), ]; } } }
class HealthResult { public function __construct( public int $status, public array $checks, public string $timestamp ) {} public function isHealthy(): bool { return $this->status === 200; } public function toArray(): array { return [ 'status' => $this->isHealthy() ? 'healthy' : 'unhealthy', 'timestamp' => $this->timestamp, 'checks' => $this->checks, ]; } }
|
健康检查接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php
namespace App\Services\Health;
abstract class HealthCheck { public string $name; protected int $timeout = 5; protected bool $critical = true; abstract public function check(): array; public function getName(): string { return $this->name; } public function isCritical(): bool { return $this->critical; } public function getTimeout(): int { return $this->timeout; } }
|
系统检查
数据库检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\DB;
class DatabaseHealthCheck extends HealthCheck { public string $name = 'database'; public function check(): array { $connections = config('database.connections'); $results = []; foreach (array_keys($connections) as $connection) { try { $start = microtime(true); DB::connection($connection)->select('SELECT 1'); $results[$connection] = [ 'status' => 'connected', 'latency' => round((microtime(true) - $start) * 1000, 2) . 'ms', ]; } catch (\Throwable $e) { $results[$connection] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } $allConnected = collect($results)->every(fn($r) => $r['status'] === 'connected'); if (!$allConnected) { throw new \RuntimeException('Database connection failed'); } return [ 'message' => 'All database connections are healthy', 'details' => $results, ]; } }
|
Redis 检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Redis;
class RedisHealthCheck extends HealthCheck { public string $name = 'redis'; public function check(): array { $connections = config('database.redis', []); $results = []; foreach (array_keys($connections) as $connection) { if ($connection === 'options') { continue; } try { $start = microtime(true); $redis = Redis::connection($connection === 'default' ? 'default' : $connection); $redis->ping(); $info = $redis->info('memory'); $memory = $info['used_memory_human'] ?? 'unknown'; $results[$connection] = [ 'status' => 'connected', 'latency' => round((microtime(true) - $start) * 1000, 2) . 'ms', 'memory' => $memory, ]; } catch (\Throwable $e) { $results[$connection] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } $allConnected = collect($results)->every(fn($r) => $r['status'] === 'connected'); if (!$allConnected) { throw new \RuntimeException('Redis connection failed'); } return [ 'message' => 'All Redis connections are healthy', 'details' => $results, ]; } }
|
存储检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Storage;
class StorageHealthCheck extends HealthCheck { public string $name = 'storage'; public function check(): array { $disks = config('filesystems.disks'); $results = []; foreach (array_keys($disks) as $disk) { try { $start = microtime(true); $storage = Storage::disk($disk); $testFile = 'health_check_' . time() . '.txt'; $storage->put($testFile, 'health check'); $content = $storage->get($testFile); $storage->delete($testFile); if ($content !== 'health check') { throw new \RuntimeException('Storage read/write mismatch'); } $results[$disk] = [ 'status' => 'healthy', 'latency' => round((microtime(true) - $start) * 1000, 2) . 'ms', ]; } catch (\Throwable $e) { $results[$disk] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } $allHealthy = collect($results)->every(fn($r) => $r['status'] === 'healthy'); if (!$allHealthy) { throw new \RuntimeException('Storage check failed'); } return [ 'message' => 'All storage disks are healthy', 'details' => $results, ]; } }
|
队列检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Queue;
class QueueHealthCheck extends HealthCheck { public string $name = 'queue'; protected int $timeout = 10; public function check(): array { $connections = config('queue.connections'); $results = []; foreach (array_keys($connections) as $connection) { try { $queue = Queue::connection($connection); $size = $queue->size(); $results[$connection] = [ 'status' => 'healthy', 'size' => $size, ]; } catch (\Throwable $e) { $results[$connection] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } return [ 'message' => 'Queue connections checked', 'details' => $results, ]; } }
|
缓存检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Cache;
class CacheHealthCheck extends HealthCheck { public string $name = 'cache'; public function check(): array { $stores = config('cache.stores'); $results = []; foreach (array_keys($stores) as $store) { try { $start = microtime(true); $cache = Cache::store($store); $key = 'health_check_' . $store . '_' . time(); $cache->put($key, 'test', 60); $value = $cache->get($key); $cache->forget($key); if ($value !== 'test') { throw new \RuntimeException('Cache read/write mismatch'); } $results[$store] = [ 'status' => 'healthy', 'latency' => round((microtime(true) - $start) * 1000, 2) . 'ms', ]; } catch (\Throwable $e) { $results[$store] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } $allHealthy = collect($results)->every(fn($r) => $r['status'] === 'healthy'); if (!$allHealthy) { throw new \RuntimeException('Cache check failed'); } return [ 'message' => 'All cache stores are healthy', 'details' => $results, ]; } }
|
外部服务检查
HTTP 服务检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Http;
class ExternalServiceHealthCheck extends HealthCheck { public string $name = 'external_services'; protected array $services = []; public function __construct(array $services = null) { $this->services = $services ?? config('health.external_services', []); } public function check(): array { $results = []; foreach ($this->services as $name => $config) { try { $start = microtime(true); $response = Http::timeout($config['timeout'] ?? 5) ->get($config['url']); $latency = round((microtime(true) - $start) * 1000, 2); if ($response->successful()) { $results[$name] = [ 'status' => 'healthy', 'latency' => $latency . 'ms', 'status_code' => $response->status(), ]; } else { $results[$name] = [ 'status' => 'unhealthy', 'latency' => $latency . 'ms', 'status_code' => $response->status(), ]; } } catch (\Throwable $e) { $results[$name] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } $allHealthy = collect($results)->every(fn($r) => $r['status'] === 'healthy'); if (!$allHealthy) { throw new \RuntimeException('Some external services are unhealthy'); } return [ 'message' => 'All external services are healthy', 'details' => $results, ]; } }
|
API 端点检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck; use Illuminate\Support\Facades\Http;
class ApiEndpointHealthCheck extends HealthCheck { public string $name = 'api_endpoints'; protected array $endpoints = []; public function __construct(array $endpoints = null) { $this->endpoints = $endpoints ?? config('health.api_endpoints', []); } public function check(): array { $results = []; foreach ($this->endpoints as $name => $config) { try { $start = microtime(true); $request = Http::timeout($config['timeout'] ?? 5); if (!empty($config['headers'])) { $request = $request->withHeaders($config['headers']); } if (!empty($config['token'])) { $request = $request->withToken($config['token']); } $method = strtoupper($config['method'] ?? 'GET'); $response = $request->send($method, $config['url']); $latency = round((microtime(true) - $start) * 1000, 2); $expectedStatus = $config['expected_status'] ?? 200; if ($response->status() === $expectedStatus) { $results[$name] = [ 'status' => 'healthy', 'latency' => $latency . 'ms', 'status_code' => $response->status(), ]; } else { $results[$name] = [ 'status' => 'unhealthy', 'latency' => $latency . 'ms', 'status_code' => $response->status(), 'expected' => $expectedStatus, ]; } } catch (\Throwable $e) { $results[$name] = [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } return [ 'message' => 'API endpoints checked', 'details' => $results, ]; } }
|
系统资源检查
内存检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck;
class MemoryHealthCheck extends HealthCheck { public string $name = 'memory'; protected float $warningThreshold = 0.8; protected float $criticalThreshold = 0.95; public function check(): array { $memoryLimit = $this->parseMemory(ini_get('memory_limit')); $memoryUsage = memory_get_usage(true); $memoryPeak = memory_get_peak_usage(true); $usagePercent = $memoryUsage / $memoryLimit; $peakPercent = $memoryPeak / $memoryLimit; $details = [ 'limit' => $this->formatBytes($memoryLimit), 'usage' => $this->formatBytes($memoryUsage), 'peak' => $this->formatBytes($memoryPeak), 'usage_percent' => round($usagePercent * 100, 2) . '%', 'peak_percent' => round($peakPercent * 100, 2) . '%', ]; if ($usagePercent >= $criticalThreshold) { throw new \RuntimeException('Memory usage is critical: ' . $details['usage_percent']); } if ($usagePercent >= $warningThreshold) { $details['warning'] = 'Memory usage is high'; } return [ 'message' => 'Memory usage is within acceptable limits', 'details' => $details, ]; } protected function parseMemory(string $value): int { $unit = strtoupper(substr($value, -1)); $value = (int) $value; return match ($unit) { 'G' => $value * 1024 * 1024 * 1024, 'M' => $value * 1024 * 1024, 'K' => $value * 1024, default => $value, }; } protected function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0; return number_format($bytes / pow(1024, $power), 2) . ' ' . $units[$power]; } }
|
磁盘空间检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php
namespace App\Services\Health\Checks;
use App\Services\Health\HealthCheck;
class DiskSpaceHealthCheck extends HealthCheck { public string $name = 'disk_space'; protected float $warningThreshold = 0.85; protected float $criticalThreshold = 0.95; protected array $paths = ['/']; public function __construct(array $paths = null) { $this->paths = $paths ?? config('health.disk_paths', ['/']); } public function check(): array { $results = []; foreach ($this->paths as $path) { $total = disk_total_space($path); $free = disk_free_space($path); $used = $total - $free; $usagePercent = $used / $total; $results[$path] = [ 'total' => $this->formatBytes($total), 'used' => $this->formatBytes($used), 'free' => $this->formatBytes($free), 'usage_percent' => round($usagePercent * 100, 2) . '%', ]; if ($usagePercent >= $criticalThreshold) { $results[$path]['status'] = 'critical'; } elseif ($usagePercent >= $warningThreshold) { $results[$path]['status'] = 'warning'; } else { $results[$path]['status'] = 'healthy'; } } $hasCritical = collect($results)->contains('status', 'critical'); if ($hasCritical) { throw new \RuntimeException('Disk space is critically low'); } return [ 'message' => 'Disk space is within acceptable limits', 'details' => $results, ]; } protected function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0; return number_format($bytes / pow(1024, $power), 2) . ' ' . $units[$power]; } }
|
健康检查服务提供者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php
namespace App\Providers;
use App\Services\Health\Checks\CacheHealthCheck; use App\Services\Health\Checks\DatabaseHealthCheck; use App\Services\Health\Checks\DiskSpaceHealthCheck; use App\Services\Health\Checks\MemoryHealthCheck; use App\Services\Health\Checks\QueueHealthCheck; use App\Services\Health\Checks\RedisHealthCheck; use App\Services\Health\Checks\StorageHealthCheck; use App\Services\Health\HealthChecker; use Illuminate\Support\ServiceProvider;
class HealthCheckServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(HealthChecker::class, function ($app) { $checker = new HealthChecker(); $checker->register(new DatabaseHealthCheck()); $checker->register(new RedisHealthCheck()); $checker->register(new StorageHealthCheck()); $checker->register(new CacheHealthCheck()); $checker->register(new QueueHealthCheck()); $checker->register(new MemoryHealthCheck()); $checker->register(new DiskSpaceHealthCheck()); return $checker; }); } }
|
健康检查命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Console\Commands;
use App\Services\Health\HealthChecker; use Illuminate\Console\Command;
class HealthCheckCommand extends Command { protected $signature = 'health:check {--json : Output as JSON}'; protected $description = 'Run health checks'; public function handle(HealthChecker $checker): int { $result = $checker->check(); if ($this->option('json')) { $this->line(json_encode($result->toArray(), JSON_PRETTY_PRINT)); return $result->isHealthy() ? self::SUCCESS : self::FAILURE; } $this->info('=== Health Check Report ==='); $this->newLine(); foreach ($result->checks as $check) { $status = $check['status'] === 'healthy' ? '<info>✓</info>' : '<error>✗</error>'; $this->line("{$status} {$check['name']} ({$check['duration']}ms)"); if ($check['status'] !== 'healthy') { $this->line(" Error: {$check['message']}"); } } $this->newLine(); if ($result->isHealthy()) { $this->info('All systems healthy'); return self::SUCCESS; } $this->error('Some systems are unhealthy'); return self::FAILURE; } }
|
总结
Laravel 13 的健康检查系统需要覆盖数据库、缓存、存储、队列、外部服务和系统资源等多个方面。通过完善的健康检查体系,可以及时发现系统问题,保障应用稳定运行。