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 的健康检查系统需要覆盖数据库、缓存、存储、队列、外部服务和系统资源等多个方面。通过完善的健康检查体系,可以及时发现系统问题,保障应用稳定运行。