韧性:熔断器与重试
在部分故障下保持系统健康
韧性:熔断器与重试 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
弹性模式
在分布式系统中,部分故障是常态——某个依赖项总会处于缓慢、重启或过载状态。弹性设计的目标,是让每个服务都能在某个依赖项异常时不被拖垮,进而避免整个系统随之失效。
本课将介绍核心工具集:超时、带退避的重试、断路器、舱壁隔离和优雅降级——全部使用 PHP 讲解。
先设置超时
最重要的弹性设置就是超时。如果没有超时,缓慢的下游服务会让 PHP-FPM 工作进程一直等待;请求不断堆积;工作进程耗尽;您的服务会因为其他服务缓慢而宕机。这就是教科书式的级联故障。
请为每个出站调用同时设置连接超时和请求总超时,务必如此。
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$http = new Client([
'connect_timeout' => 0.5, // fail fast if we can't even connect
'timeout' => 2.0, // hard cap on the whole call
]);
// A hung dependency now fails in 2s instead of holding a worker forever.谨慎重试
暂时性故障(短暂波动、短暂的网络中断、503)通常在第二次尝试时能够成功。但重试很危险:过于急切地重试会放大负载,让本已苦苦支撑的服务承受更大压力。规则如下:
- 只重试幂等操作和可重试错误(超时、5xx、429)。
- 限制尝试次数。
- 绝不要重试 4xx 客户端错误——它们只会再次失败。
指数退避与抖动
来自许多客户端的固定间隔重试会彼此同步,形成一场惊群,一波又一波地冲击正在恢复的服务。解决办法是使用指数退避(每次尝试都将延迟加倍),再加上抖动(随机化),将负载分散开来。
<?php
function backoffDelay(int $attempt, float $base = 0.1, float $cap = 5.0): float {
$exp = min($cap, $base * (2 ** $attempt)); // 0.1, 0.2, 0.4, ...
return mt_rand(0, (int)($exp * 1000)) / 1000; // full jitter: 0..exp
}
for ($i = 0; $i < 5; $i++) {
printf("attempt %d -> wait %.3fs\n", $i, backoffDelay($i));
}重试循环
将各个部分组合起来:限制尝试次数,只在出现可重试错误时重试,在尝试之间使用带抖动的退避进行休眠,如果所有尝试都失败,则报告最后一次失败。
<?php
function withRetry(callable $op, int $maxAttempts = 4): mixed {
$attempt = 0;
while (true) {
try {
return $op();
} catch (\Throwable $e) {
$attempt++;
if ($attempt >= $maxAttempts || !isRetryable($e)) {
throw $e; // give up
}
usleep((int)(backoffDelay($attempt) * 1_000_000));
}
}
}
function isRetryable(\Throwable $e): bool { return $e->getCode() === 0 || $e->getCode() >= 500; }
function backoffDelay(int $a): float { return min(5.0, 0.1 * (2 ** $a)) * (mt_rand(0, 100) / 100); }断路器
重试有助于应对短暂波动,但如果某个依赖项确实宕机,对每个请求进行重试只是在浪费时间和资源。断路器会跟踪失败次数,一旦超过阈值就会打开,让调用短路并立即失败,而不是等待已失效的服务。
它有三种状态:关闭(调用正常通过并统计失败次数)、打开(立即拒绝调用)和半开(通过少量试探性调用测试恢复情况)。
断路器状态机
状态转换如下:失败次数超过阈值时由关闭转为打开。冷却期结束后由打开转为半开。探测调用成功时由半开转为关闭,失败时则回到打开。状态必须在多个 PHP 进程之间共享(Redis/APCu),因为每个请求都会启动一个新进程。
<?php
final class CircuitBreaker {
public function __construct(
private int $threshold = 5,
private int $coolDown = 30, // seconds
) {}
public function call(callable $op, array &$state): mixed {
if ($state['status'] === 'open') {
if (time() - $state['openedAt'] < $this->coolDown) {
throw new \RuntimeException('Circuit open - failing fast');
}
$state['status'] = 'half-open'; // time to probe
}
try {
$result = $op();
$state = ['status' => 'closed', 'failures' => 0]; // recovered
return $result;
} catch (\Throwable $e) {
$state['failures'] = ($state['failures'] ?? 0) + 1;
if ($state['failures'] >= $this->threshold || $state['status'] === 'half-open') {
$state['status'] = 'open';
$state['openedAt'] = time();
}
throw $e;
}
}
}舱壁模式
舱壁模式得名于船舶的隔舱,它会隔离资源,使一个区域的故障无法让整艘船沉没。如果所有工作进程都能调用缓慢的报表服务,那么报表服务的中断就可能耗尽所有工作进程,使结算服务无法获得资源。
请对资源进行分区——为每个依赖项分别设置连接池、工作进程池或队列,或者为每个下游服务设置并发限制——这样一个故障依赖项只会耗尽分配给它的那部分资源。
优雅降级与回退
当非关键依赖项不可用时,请进行降级,而不是直接报错。您可以提供缓存值、默认值或功能有所减少的体验。断路器的打开状态正好可以作为回退路径的触发条件。
<?php
function getRecommendations(callable $remoteCall, Redis $cache, string $userId): array {
try {
$recs = $remoteCall($userId);
$cache->setex("recs:$userId", 3600, json_encode($recs));
return $recs;
} catch (\Throwable $e) {
// Fallback 1: last-known-good from cache
if ($cached = $cache->get("recs:$userId")) {
return json_decode($cached, true);
}
// Fallback 2: generic popular items - never block the page
return ['popular-1', 'popular-2'];
}
}使用幂等键确保重试安全
重试只有在幂等操作上才是安全的。对于“扣款”这类非幂等操作,请附加一个幂等键,由服务器用它进行去重:如果第一次尝试已经成功但响应丢失,随后到达的重试请求会让服务器返回原始结果,而不是再次扣款。
<?php
function chargeWithRetry(callable $http, string $orderId, int $cents): array {
// Same key across all retries of THIS logical charge
$key = 'charge-' . $orderId;
return withRetry(fn() => $http('POST', '/charges', [
'headers' => ['Idempotency-Key' => $key],
'json' => ['order' => $orderId, 'amount' => $cents],
]));
}
function withRetry(callable $op) { return $op(); } // see earlier scene组合使用
这些模式可以组合,而且顺序很重要。一个健壮的出站调用通常按如下方式嵌套:
- 在每次单独尝试上设置超时(最内层)。
- 用带退避的重试包裹带超时的调用,以应对暂时性波动。
- 用断路器包裹重试,以便在持续中断时快速触发。
- 使用舱壁限制此依赖项能够消耗的容量。
- 最外层使用回退,捕获所有向上冒泡的错误。
虽然存在类似 Resilience4PHP 的包装器库,但理解分层方式比掌握某个特定软件包更重要。
快速检查
选择合适的模式。
回顾
应对部分故障:
- 每次调用都设置超时,防止工作进程耗尽和级联故障。
- 只对幂等操作和可重试错误进行重试,并使用指数退避与抖动。
- 当依赖项宕机时,断路器会快速失败(关闭 → 打开 → 半开)。
- 舱壁隔离资源,防止一次故障耗尽所有资源。
- 优雅降级 / 回退让核心流程保持运行。
将这些模式分层组合后,就能把不可避免的故障转变为受控且可恢复的事件。
常见问题解答
「韧性:熔断器与重试」课时是免费的吗?
是的 — 「韧性:熔断器与重试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「韧性:熔断器与重试」这节课中我会学到什么?
在部分故障下保持系统健康 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「韧性:熔断器与重试」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 从单体架构到微服务
- 服务通信:REST 和 gRPC
- API 网关与服务发现
- 韧性:熔断器与重试